Files
Loki/internal/ajean/backend_models_shards.go
T
nathaninline 52baab5786 v0.8.4 : l'historique s'exporte, le moteur se laisse joindre, les modeles en plusieurs fichiers marchent
Trois retours d'utilisateurs apres la 0.8.3.

Export de la conversation (regression de la 0.8 : conversation.json a
disparu dans ajean.db, verrouille et binaire). Bouton dans l'UI +
commande 'ajean export [--json] [fichier]', endpoint /api/chat/export.

Ecoute reseau du moteur : interrupteur 'joignable depuis le reseau local'
dans le panneau Acces OpenAI (HOST + regle de pare-feu Windows), commande
'ajean network [on|off|status]'. HOST devient un reglage de machine
preserve au changement de preset.

Modeles GGUF decoupes en tranches -00001-of-000NN : une famille = un
modele. Telechargement de toutes les tranches depuis n'importe quel lien,
sonde d'espace disque sur le total, liste et suppression regroupees,
demarrage refuse avec le nom du fichier manquant.
2026-08-08 20:01:54 +02:00

128 lines
4.6 KiB
Go

package ajean
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// Modèles découpés en plusieurs fichiers. Au-delà d'une cinquantaine de Go, un
// dépôt Hugging Face publie son GGUF en tranches nommées par convention
// llama.cpp : `<base>-00001-of-00003.gguf`, `…-00002-of-00003.gguf`, etc.
// llama-server n'a besoin QUE de la première : il ouvre les suivantes tout seul,
// à condition qu'elles soient dans le même dossier.
//
// Tout le reste d'AJEAN traitait chaque tranche comme un modèle indépendant :
// le lien collé ne rapatriait qu'un fichier sur trois (le moteur mourait sur
// « tensor not found »), la liste du sélecteur montrait trois entrées dont deux
// ne démarrent pas, et supprimer « le modèle » laissait les deux autres tranches
// occuper le disque. Ce fichier ramène la tranche au rang de détail : une
// famille de tranches = UN modèle, désigné par sa première.
// shardRe reconnaît le suffixe de tranche en fin de nom de fichier. Le nombre de
// chiffres n'est pas figé à 5 : le convertisseur de llama.cpp en pose 5, mais
// des dépôts en publient avec moins.
var shardRe = regexp.MustCompile(`(?i)-(\d{2,5})-of-(\d{2,5})\.gguf$`)
// shardInfo décompose un nom de tranche : rang (1-based) et nombre total. ok
// vaut false pour un modèle d'un seul fichier.
func shardInfo(name string) (idx, total int, ok bool) {
m := shardRe.FindStringSubmatch(baseName(name))
if m == nil {
return 0, 0, false
}
idx, _ = strconv.Atoi(m[1])
total, _ = strconv.Atoi(m[2])
if idx < 1 || total < 1 || idx > total {
return 0, 0, false // numérotation incohérente : on le traite en fichier simple
}
return idx, total, true
}
// shardNameAt renvoie le nom de la tranche de rang i dans la même famille, en
// conservant la largeur de la numérotation d'origine (00002, pas 2).
func shardNameAt(name string, i int) string {
m := shardRe.FindStringSubmatchIndex(name)
if m == nil {
return name
}
width := m[3] - m[2]
totalPart := name[m[4]:m[5]]
return name[:m[2]] + fmt.Sprintf("%0*d", width, i) + "-of-" + totalPart + name[m[5]:]
}
// isFollowerShard : tranche 2..N, celles qu'on ne montre jamais comme un modèle
// à part entière (elles ne démarrent pas seules).
func isFollowerShard(name string) bool {
idx, _, ok := shardInfo(name)
return ok && idx > 1
}
// shardFamily renvoie les noms de TOUTES les tranches de la famille de name (y
// compris name), dans l'ordre. Un fichier simple se renvoie lui-même.
func shardFamily(name string) []string {
base := baseName(name)
_, total, ok := shardInfo(base)
if !ok {
return []string{base}
}
out := make([]string, 0, total)
for i := 1; i <= total; i++ {
out = append(out, shardNameAt(base, i))
}
return out
}
// shardFamilySize additionne la taille des tranches PRÉSENTES dans dir. Sert à
// afficher « 42 Go » sur la première tranche au lieu des 15 Go qu'elle pèse
// seule, ce qui laissait croire à un modèle trois fois plus petit.
func shardFamilySize(dir, name string) int64 {
var sum int64
for _, n := range shardFamily(name) {
if st, err := os.Stat(filepath.Join(dir, n)); err == nil && !st.IsDir() {
sum += st.Size()
}
}
return sum
}
// shardFamilyMissing liste les tranches ABSENTES du dossier. Un modèle dont il
// manque une tranche démarre puis meurt sur une erreur de tenseur introuvable :
// autant le dire dans la liste plutôt que dans les logs du moteur.
func shardFamilyMissing(dir, name string) []string {
var missing []string
for _, n := range shardFamily(name) {
if st, err := os.Stat(filepath.Join(dir, n)); err != nil || st.IsDir() {
missing = append(missing, n)
}
}
return missing
}
// shardURLSet transforme l'URL d'UNE tranche en la liste complète des URLs de la
// famille, dans l'ordre, avec les noms de fichiers correspondants. Le lien collé
// peut désigner n'importe quelle tranche (on tombe souvent sur la deuxième en
// parcourant l'arborescence du dépôt) : on remonte toujours à la première.
func shardURLSet(dlURL, name string) (urls, names []string) {
fam := shardFamily(name)
if len(fam) == 1 {
return []string{dlURL}, fam
}
base := baseName(name)
for _, n := range fam {
// Remplacement du seul nom de fichier, en fin d'URL : le nom peut réapparaître
// ailleurs dans le chemin (dossier de quantification portant le même libellé),
// et un ReplaceAll y toucherait aussi.
i := strings.LastIndex(dlURL, base)
if i < 0 {
return []string{dlURL}, []string{base} // lien inattendu : on ne devine rien
}
urls = append(urls, dlURL[:i]+n+dlURL[i+len(base):])
names = append(names, n)
}
return urls, names
}