mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 22:48:11 +02:00
Façade : see_video exécuté par la façade, vidéos jointes rangées relues
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>
This commit is contained in:
1 parent
776de75dd0
commit
ac74ecde0b
12 files changed
+1084
-42
No files matched your search
@@ -0,0 +1,197 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user