Ctrl K
ComunidadGuías…

internal/autogen/companions.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package autogen

import (
	_ "embed"
	"encoding/json"
	"strings"
	"sync"
)

// Companion downloads: the files a split diffusion model needs beside its DiT
// (VAE, text encoder, tokenizer), and where to get each one.
//
// The two halves of that answer have different owners. WHICH roles are missing
// is resolveComponents' call, run against the live encoder pool exactly as a
// regen would run it, so the browser offers precisely what the next config would
// otherwise warn about and never an encoder already on disk. WHERE a role comes
// from is data (companions.json): nothing machine-readable states it. A DiT
// names the encoder WIDTH it wants, not a repo, and sd-server's own model
// catalog lists slots and labels but no URLs.

//go:embed companions.json
var companionsJSON []byte

// CompanionSource is one downloadable file of a recipe.
type CompanionSource struct {
	Repo  string `json:"repo"`
	Path  string `json:"path"`
	Label string `json:"label"`
}

type companionFamily struct {
	ID         string                     `json:"id"`
	Arch       []string                   `json:"arch"`
	Name       []string                   `json:"name"`
	CondHidden int64                      `json:"condHidden"`
	Parts      map[string]CompanionSource `json:"parts"`
}

var (
	companionOnce sync.Once
	companionFams []companionFamily
	companionErr  error
)

func companionFamilies() ([]companionFamily, error) {
	companionOnce.Do(func() {
		var doc struct {
			Families []companionFamily `json:"families"`
		}
		companionErr = json.Unmarshal(companionsJSON, &doc)
		companionFams = doc.Families
	})
	return companionFams, companionErr
}

// matches reports whether every condition the family states holds. An absent
// condition is no constraint, but a family must state at least one.
func (f companionFamily) matches(arch, name string, condHidden int64) bool {
	if len(f.Arch) == 0 && len(f.Name) == 0 && f.CondHidden == 0 {
		return false
	}
	if len(f.Arch) > 0 {
		ok := false
		for _, a := range f.Arch {
			if a = strings.ToLower(a); arch == a || strings.HasPrefix(arch, a) {
				ok = true
				break
			}
		}
		if !ok {
			return false
		}
	}
	if len(f.Name) > 0 {
		ok := false
		for _, n := range f.Name {
			if strings.Contains(name, strings.ToLower(n)) {
				ok = true
				break
			}
		}
		if !ok {
			return false
		}
	}
	return f.CondHidden == 0 || f.CondHidden == condHidden
}

// CompanionFile is one file a companion plan would download, for one role.
type CompanionFile struct {
	Role string `json:"role"`
	CompanionSource
}

// CompanionPlan is the answer for one DiT.
type CompanionPlan struct {
	// Family is the matched recipe id, "" when none matched.
	Family string `json:"family"`
	// Missing are the roles the next regen would warn about (resolveComponents'
	// missing list): the roles nothing on disk satisfies.
	Missing []string `json:"missing"`
	// Files are the recipe's sources for the missing roles, in Missing order.
	Files []CompanionFile `json:"files"`
	// Unsourced are missing roles the recipe names no source for (or every
	// missing role, when no recipe matched). The browser says so rather than
	// implying the model is complete.
	Unsourced []string `json:"unsourced"`
	// Have are the roles the disk already satisfies, with the file the next
	// regen will wire to each. Without them a model needing nothing looks the
	// same as a model the planner does not understand (the browser shows no box
	// either way), and a wrong same-width encoder pick is invisible until the
	// first render comes out off-prompt.
	Have []CompanionHave `json:"have"`
}

// CompanionHave is one role already satisfied from disk, and by which file.
type CompanionHave struct {
	Role string `json:"role"`
	Path string `json:"path"`
}

// PlanImageCompanions answers "what else does this DiT need, and from where",
// given a header read before the file is downloaded. name is any string that
// carries the model's name (the repo path is best: "unsloth/MOSS-Image-SFT-GGUF/
// MOSS-Image-SFT-Q8_0.gguf"), since several families are told apart only by
// name, exactly as in resolveComponents.
//
// ok is false for anything that is not a split diffusion model: an LLM, or a
// full SD/SDXL checkpoint whose encoders are baked in. Those need nothing.
func PlanImageCompanions(s Settings, meta Metadata, name string) (plan CompanionPlan, ok bool) {
	vid := videoInfoFrom(meta)
	arch := effectiveImageArch(meta)
	if !vid.is() && !isImageArch(arch) {
		return CompanionPlan{}, false
	}
	// The same gate imageCmdLines applies: a full checkpoint wires nothing
	// external, so it can be missing nothing.
	if !vid.is() && isFullCheckpointArch(arch) {
		return CompanionPlan{}, false
	}
	comps, missing := resolveComponents(s.Encoders, nil, arch, name, encoderPoolFor(s.RootList()), meta.CondHidden, vid)
	plan.Missing = append([]string{}, missing...)
	plan.Have = comps.roles()
	plan.Files = []CompanionFile{}
	plan.Unsourced = []string{}
	if len(missing) == 0 {
		return plan, true
	}
	a := strings.ToLower(strings.TrimSpace(arch))
	n := strings.ToLower(name)
	var fam *companionFamily
	if fams, err := companionFamilies(); err == nil {
		for i := range fams {
			if fams[i].matches(a, n, meta.CondHidden) {
				fam = &fams[i]
				break
			}
		}
	}
	if fam == nil {
		plan.Unsourced = append(plan.Unsourced, missing...)
		return plan, true
	}
	plan.Family = fam.ID
	for _, role := range missing {
		src, have := fam.Parts[role]
		if !have || src.Repo == "" || src.Path == "" {
			plan.Unsourced = append(plan.Unsourced, role)
			continue
		}
		plan.Files = append(plan.Files, CompanionFile{Role: role, CompanionSource: src})
	}
	return plan, true
}