mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Vision : le modèle peut regarder des vidéos (outil see_video et vidéos jointes), repris d'AJEAN
Reprise des commits 91f7f977 (PR #100 de Trombo38) et 1dfac0cf d'AJEAN. Une vidéo jointe (mp4, webm, mov, mkv, avi) part au modèle en partie input_video, et l'outil see_video charge une vidéo du disque, mais seulement si le moteur actif sait la lire : preset llama.cpp local avec projecteur (MMPROJ) et binaire dont l'aide annonce --video-fps (videoInputSupported, aide relue quand le binaire change, chemin des bibliothèques passé à la seule commande pour ne pas figer binHelp). Strata et les API externes ignoraient la partie en silence : la vidéo reste alors un fichier, annoncé avec la marche à suivre (ffmpeg puis see_image), et see_video n'est pas proposé. llama.cpp a --video-fps depuis la PR #24318 (fin août 2026) ; l'image server-cuda officielle, base du conteneur, installe ffmpeg. La consigne de repli dit quoi faire si ffmpeg manque (installation hors conteneur). Adapté à Loki : - la vidéo de see_video est toujours éphémère (jamais dans l'historique, KEEP_TURN_IMAGES compris) ; même ordre de refus que see_image, sonde /props en dernier, refus sur un poste distant ; - une vidéo jointe est rangée par référence (loki-vid:, LOKI_HOME/chatvid, chiffrée comme le fil), parties recopiées et non mutées comme les images ; retirée d'un fil rouvert (stripImageParts, videoLostMarker) et effacée par le ménage des médias ; - marqueur de perte à la compaction, élision dans l'export JSON, relais par l'API interne des outils (vision_video). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6a45f19831
commit
776de75dd0
12 files changed
+886
-38
No files matched your search
+42
-19
@@ -152,10 +152,19 @@ func attachNote(files []attachInfo) string {
|
||||
head = "Fichiers joints à ce message, déposés dans ton dossier de travail :"
|
||||
}
|
||||
var lines []string
|
||||
video := false
|
||||
for _, f := range files {
|
||||
lines = append(lines, fmt.Sprintf("- %s (%s)", f.Path, humanBytes(f.Size)))
|
||||
video = video || videoMime(f.Name) != ""
|
||||
}
|
||||
return head + "\n" + strings.Join(lines, "\n") + "\n\n"
|
||||
note := head + "\n" + strings.Join(lines, "\n") + "\n"
|
||||
// Vidéo jointe que le moteur ne sait pas lire (chat_video.go) : dire comment
|
||||
// la regarder quand même, au lieu de laisser le modèle tâtonner — Strata et
|
||||
// les API externes ignoraient la vidéo en silence.
|
||||
if video && visionEnabled() && !videoInputSupported() {
|
||||
note += videoFallbackNote
|
||||
}
|
||||
return note + "\n"
|
||||
}
|
||||
|
||||
// imageMimes : les extensions qu'on peut ENVOYER AU MODÈLE comme image (contenu
|
||||
@@ -189,9 +198,11 @@ func visionEnabled() bool {
|
||||
|
||||
// userMessageContent construit le champ Content du message utilisateur. Cas
|
||||
// courant : une simple chaîne (note des fichiers joints + texte). Quand la vision
|
||||
// est active ET qu'au moins une pièce jointe est une image, on renvoie le format
|
||||
// multimodal d'OpenAI — une partie `text` suivie d'une partie `image_url` par
|
||||
// image (data URI base64) — que llama-server comprend une fois --mmproj chargé :
|
||||
// est active ET qu'au moins une pièce jointe est une image (ou une vidéo que le
|
||||
// moteur sait lire), on renvoie le format multimodal d'OpenAI — une partie
|
||||
// `text` suivie d'une partie `image_url` par image (data URI base64) et d'une
|
||||
// partie `input_video` par vidéo — que llama-server comprend une fois --mmproj
|
||||
// chargé :
|
||||
// le modèle VOIT alors l'image au lieu de devoir l'ouvrir comme un fichier binaire.
|
||||
// Les images ainsi intégrées sortent de la note textuelle (inutile de dire au
|
||||
// modèle d'aller ouvrir un fichier qu'il a déjà sous les yeux) ; les autres
|
||||
@@ -209,31 +220,43 @@ func userMessageContent(files []attachInfo, prompt string) any {
|
||||
if err != nil {
|
||||
return attachNote(files) + prompt
|
||||
}
|
||||
var imgParts []map[string]any
|
||||
var imgParts, vidParts []map[string]any
|
||||
var textFiles []attachInfo
|
||||
for _, f := range files {
|
||||
mime := imageMime(f.Name)
|
||||
if mime == "" {
|
||||
textFiles = append(textFiles, f)
|
||||
if mime := imageMime(f.Name); mime != "" {
|
||||
b, err := os.ReadFile(filepath.Join(dir, f.Name))
|
||||
if err != nil {
|
||||
textFiles = append(textFiles, f) // illisible ici : au moins l'annoncer comme fichier
|
||||
continue
|
||||
}
|
||||
// Photo prise au téléphone : l'orientation vit dans un tag EXIF que le
|
||||
// projecteur ignore (il voit les pixels bruts, donc l'image couchée), et
|
||||
// une image de plusieurs milliers de pixels ne fait que gonfler le base64
|
||||
// — voir prepareImageForModel (web_upload_orient.go).
|
||||
b, mime = prepareImageForModel(b, mime)
|
||||
imgParts = append(imgParts, imageURLPart(b, mime))
|
||||
continue
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(dir, f.Name))
|
||||
if err != nil {
|
||||
textFiles = append(textFiles, f) // illisible ici : au moins l'annoncer comme fichier
|
||||
// Vidéo : seulement si le moteur sait la lire (chat_video.go) ; sinon
|
||||
// elle reste un fichier, annoncé avec la marche à suivre (attachNote).
|
||||
// Pas de redimensionnement : le moteur échantillonne les images lui-même.
|
||||
if mime := videoMime(f.Name); mime != "" && videoInputSupported() {
|
||||
b, err := os.ReadFile(filepath.Join(dir, f.Name))
|
||||
if err != nil || len(b) == 0 || len(b) > maxVideoBytes {
|
||||
textFiles = append(textFiles, f)
|
||||
continue
|
||||
}
|
||||
vidParts = append(vidParts, videoPart(b, mime))
|
||||
continue
|
||||
}
|
||||
// Photo prise au téléphone : l'orientation vit dans un tag EXIF que le
|
||||
// projecteur ignore (il voit les pixels bruts, donc l'image couchée), et
|
||||
// une image de plusieurs milliers de pixels ne fait que gonfler le base64
|
||||
// — voir prepareImageForModel (web_upload_orient.go).
|
||||
b, mime = prepareImageForModel(b, mime)
|
||||
imgParts = append(imgParts, imageURLPart(b, mime))
|
||||
textFiles = append(textFiles, f)
|
||||
}
|
||||
if len(imgParts) == 0 {
|
||||
if len(imgParts) == 0 && len(vidParts) == 0 {
|
||||
return attachNote(files) + prompt
|
||||
}
|
||||
parts := []map[string]any{{"type": "text", "text": attachNote(textFiles) + prompt}}
|
||||
return append(parts, imgParts...)
|
||||
parts = append(parts, imgParts...)
|
||||
return append(parts, vidParts...)
|
||||
}
|
||||
|
||||
// workspaceRel dit si `abs` se trouve DANS le dossier de travail de l'agent et,
|
||||
|
||||
Reference in new issue
Block a user