mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
Comme see_image (lot 45), see_video est tenu par la façade : imagetools::see_video porte toolSeeVideo (mêmes refus, même ordre, sonde du moteur en dernier) et video_input_supported la capacité du moteur (aide du binaire, --video-fps, relue quand il change). La vidéo relayée est toujours éphémère, KEEP_TURN_IMAGES compris (turn.rs) ; vision_video relu de /tool/exec quand le Go exécute l'outil. Vidéos jointes rangées (loki-vid:) : remises en data-URL avant l'envoi avec les images (expand_image_refs, cache des médias élargi à chatvid). Libellé et non-dédup de see_video, videoLostMarker à la compaction et au rechargement d'un fil, élision dans l'export. Parité : journal tiré du Go (parity_video_tool_test.go → tests/parity_images/video.json : refus, moteur sans vidéo, moteur aveugle, vidéos relayées à l'octet), rejoué par tests/videotools.rs ; journal multiplateforme (ni faux Playwright ni sh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
7.2 KiB
Go
198 lines
7.2 KiB
Go
package loki
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// parity_video_tool_test.go — journal de référence de l'outil see_video
|
|
// (repris d'AJEAN, tenu par la façade comme see_image, lot 45) : refus dans
|
|
// le même ordre que see_image, moteur qui ne lit pas la vidéo (consigne
|
|
// ffmpeg), sonde « le moteur voit-il » (faux /props) et vidéo relayée au
|
|
// modèle, à l'octet (une vidéo n'est jamais réencodée).
|
|
//
|
|
// La capacité vidéo du moteur (videoInputSupported : aide du binaire) est
|
|
// fixée par chaque étape : la façade la calcule de son côté par le même
|
|
// chemin (imagetools::video_input_supported) et la passe à see_video. Rangé
|
|
// dans rust/loki-rs/tests/parity_images/video.json ; tests/videotools.rs le
|
|
// rejoue contre le port.
|
|
//
|
|
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteOutilVideo
|
|
|
|
const parityVideoFile = "../../rust/loki-rs/tests/parity_images/video.json"
|
|
|
|
type parityVideoStep struct {
|
|
Name string `json:"name"`
|
|
Vision bool `json:"vision,omitempty"`
|
|
Sees bool `json:"sees,omitempty"` // le faux /props annonce la vision
|
|
Video bool `json:"video,omitempty"` // le moteur lit la vidéo (--video-fps)
|
|
Args map[string]any `json:"args"`
|
|
Label string `json:"label"`
|
|
Result string `json:"result"`
|
|
Part map[string]any `json:"part,omitempty"` // vidéo relayée
|
|
Probes int `json:"probes"`
|
|
}
|
|
|
|
func TestPariteOutilVideo(t *testing.T) {
|
|
testHome(t)
|
|
files := filepath.Join(t.TempDir(), "fichiers")
|
|
raw := map[string][]byte{
|
|
"clip.mp4": []byte("\x00\x00\x00\x18ftypmp42 fausse vidéo"),
|
|
"CLIP.MOV": []byte("\x00\x00\x00\x14ftypqt mov"),
|
|
"anim.webm": []byte("\x1aE\xdf\xa3 webm"),
|
|
"film.mkv": []byte("\x1aE\xdf\xa3 matroska"),
|
|
"vieux.avi": []byte("RIFF\x10\x00\x00\x00AVI LIST"),
|
|
"sous/court.mp4": []byte("ftyp sous-dossier"),
|
|
"texte.txt": []byte("pas une vidéo"),
|
|
"vide.mp4": {},
|
|
}
|
|
for name, b := range raw {
|
|
p := filepath.Join(files, name)
|
|
_ = os.MkdirAll(filepath.Dir(p), 0o755)
|
|
if err := os.WriteFile(p, b, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
_ = os.MkdirAll(filepath.Join(files, "dossier.mp4"), 0o755)
|
|
lourd, _ := os.Create(filepath.Join(files, "lourd.mp4"))
|
|
_ = lourd.Truncate(maxVideoBytes + 1)
|
|
lourd.Close()
|
|
setTaskWorkspace(files)
|
|
t.Cleanup(func() { setTaskWorkspace("") })
|
|
|
|
// Faux moteur : /props, authentifié.
|
|
var mu sync.Mutex
|
|
sees, probes := false, 0
|
|
eng := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if r.URL.Path != "/props" || r.Header.Get("Authorization") != "Bearer cle-moteur" {
|
|
w.WriteHeader(401)
|
|
return
|
|
}
|
|
probes++
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"modalities": map[string]any{"vision": sees}})
|
|
}))
|
|
defer eng.Close()
|
|
_ = SetConfigKey("PORT", strings.TrimPrefix(eng.URL, "http://127.0.0.1:"))
|
|
_ = putStr(bkState, "api_key", "cle-moteur")
|
|
prevVideo := videoInputSupported
|
|
t.Cleanup(func() { videoInputSupported = prevVideo })
|
|
|
|
var steps []parityVideoStep
|
|
run := func(st parityVideoStep) {
|
|
if st.Vision {
|
|
_ = SetConfigKey("MMPROJ", "vision.gguf")
|
|
} else {
|
|
_ = SetConfigKey("MMPROJ", "")
|
|
}
|
|
video := st.Video
|
|
videoInputSupported = func() bool { return video }
|
|
mu.Lock()
|
|
sees, probes = st.Sees, 0
|
|
mu.Unlock()
|
|
visionProbeMu.Lock()
|
|
visionProbeAt = time.Time{}
|
|
visionProbeMu.Unlock()
|
|
args := map[string]any{}
|
|
b, _ := json.Marshal(st.Args)
|
|
_ = json.Unmarshal([]byte(strings.ReplaceAll(string(b), "{{FICHIERS}}", filepath.ToSlash(files))), &args)
|
|
label := toolCallLabel("see_video", args)
|
|
st.Label = strings.ReplaceAll(label, filepath.ToSlash(files), "{{FICHIERS}}")
|
|
res := execTool(t.Context(), "see_video", args, label, Caps{Agent: true})
|
|
_, _, relay := toolStepExtras("see_video", label, res)
|
|
st.Result = strings.ReplaceAll(res.Result, filepath.ToSlash(files), "{{FICHIERS}}")
|
|
if relay != nil {
|
|
for _, p := range contentParts(relay.Content) {
|
|
switch p["type"] {
|
|
case "text":
|
|
st.Result += "\n[relais] " + strings.ReplaceAll(fmt.Sprint(p["text"]), filepath.ToSlash(files), "{{FICHIERS}}")
|
|
case "input_video":
|
|
st.Part = parityVideoFacts(t, p, raw)
|
|
}
|
|
}
|
|
}
|
|
mu.Lock()
|
|
st.Probes = probes
|
|
mu.Unlock()
|
|
if st.Args == nil {
|
|
st.Args = map[string]any{}
|
|
}
|
|
steps = append(steps, st)
|
|
}
|
|
a := func(f string) map[string]any { return map[string]any{"file": f} }
|
|
|
|
run(parityVideoStep{Name: "sans vision", Video: true, Args: a("clip.mp4")})
|
|
run(parityVideoStep{Name: "chemin manquant", Vision: true, Video: true, Args: a("")})
|
|
run(parityVideoStep{Name: "sans argument", Vision: true, Video: true})
|
|
run(parityVideoStep{Name: "moteur sans vidéo", Vision: true, Sees: true, Args: a("clip.mp4")})
|
|
run(parityVideoStep{Name: "pas une vidéo", Vision: true, Video: true, Args: a("texte.txt")})
|
|
run(parityVideoStep{Name: "image", Vision: true, Video: true, Args: a("photo.png")})
|
|
run(parityVideoStep{Name: "introuvable", Vision: true, Video: true, Args: a("absente.mp4")})
|
|
run(parityVideoStep{Name: "dossier", Vision: true, Video: true, Args: a("dossier.mp4")})
|
|
run(parityVideoStep{Name: "trop lourde", Vision: true, Video: true, Args: a("lourd.mp4")})
|
|
run(parityVideoStep{Name: "le moteur ne voit pas", Vision: true, Video: true, Args: a("clip.mp4")})
|
|
run(parityVideoStep{Name: "vide", Vision: true, Sees: true, Video: true, Args: a("vide.mp4")})
|
|
for _, f := range []string{"clip.mp4", "CLIP.MOV", "anim.webm", "film.mkv", "vieux.avi", "sous/court.mp4"} {
|
|
run(parityVideoStep{Name: "voir " + f, Vision: true, Sees: true, Video: true, Args: a(f)})
|
|
}
|
|
run(parityVideoStep{Name: "chemin absolu", Vision: true, Sees: true, Video: true, Args: a("{{FICHIERS}}/clip.mp4")})
|
|
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(map[string]any{"files": raw, "steps": steps}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := buf.Bytes()
|
|
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
|
if err := os.MkdirAll(filepath.Dir(parityVideoFile), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(parityVideoFile, got, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
want, err := os.ReadFile(parityVideoFile)
|
|
if err != nil {
|
|
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Errorf("see_video ne produit plus le journal rangé dans %s.\n"+
|
|
"LOKI_PARITY_UPDATE=1 pour le régénérer si le changement est voulu.", parityVideoFile)
|
|
}
|
|
}
|
|
|
|
// parityVideoFacts : le type et le fichier d'origine de la vidéo relayée (à
|
|
// l'octet : une vidéo part telle quelle).
|
|
func parityVideoFacts(t *testing.T, part map[string]any, raw map[string][]byte) map[string]any {
|
|
u := partVideoURL(part)
|
|
i := strings.Index(u, ";base64,")
|
|
if i < 0 {
|
|
t.Fatalf("partie vidéo sans base64 : %.60q", u)
|
|
}
|
|
b, err := base64.StdEncoding.DecodeString(u[i+8:])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := map[string]any{"mime": strings.TrimPrefix(u[:i], "data:")}
|
|
for name, r := range raw {
|
|
if bytes.Equal(r, b) {
|
|
out["same_as"] = name
|
|
}
|
|
}
|
|
return out
|
|
}
|