mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Chaque dictée relançait whisper-cli, qui relisait le modèle depuis le disque avant de transcrire. Ce chargement dominait le temps de réponse — 3,2 s de calcul pour 3,4 s d'audio — et aucun réglage ne pouvait le rattraper. whisper-server garde le modèle en mémoire entre deux phrases. Loki le supervise : démarrage au premier clic sur le micro, extinction après dix minutes sans dictée. Une dictée n'est pas un service permanent, et garder un modèle chargé toute la journée priverait le moteur de chat de sa VRAM. - Réglages serveur (modèle, langue, matériel, réactivité) dans bkState, avec leurs routes. La langue par défaut passe de « auto » à « fr » : sur quelques secondes d'audio la détection se trompe, et une langue mal détectée produit du charabia — des suites de caractères géorgiens ont été observées. - Catalogue de quatre modèles, de small à large-v3 ; large-v3-turbo par défaut. Téléchargement par identifiant, dans un .part renommé à la fin : un transfert interrompu ne laisse plus un .bin tronqué qu'on croirait bon. - Le GPU se choisit par CUDA_VISIBLE_DEVICES, posé en REMPLAÇANT toute valeur héritée. Dupliquée, la variable laisse le gagnant dépendre de la libc. - Les marqueurs de whisper ([BLANK_AUDIO], (silence)) ne sont plus collés dans le champ de saisie comme s'ils étaient du texte dicté. L'image construit DEUX binaires whisper-server, CPU et CUDA. LLAMACPP_IMAGE accepte la variante CPU de l'image amont ; sur cette base les .so CUDA sont absentes et un binaire lié à CUDA n'a même pas de quoi démarrer, donc aucun repli n'est possible depuis le programme. Le garde-fou du Dockerfile vérifie maintenant les deux étapes : les drapeaux de portabilité de la PR #18 doivent survivre à l'arrivée de CUDA. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestConfigRouteAllerRetour(t *testing.T) {
|
|
testHome(t)
|
|
corps := strings.NewReader(`{"model":"small-q5_1","lang":"en","device":"1","reactivity":"court"}`)
|
|
rec := httptest.NewRecorder()
|
|
handleDictateConfig(rec, httptest.NewRequest("POST", "/api/dictate/config", corps))
|
|
if rec.Code != 200 {
|
|
t.Fatalf("POST = %d, corps %s", rec.Code, rec.Body.String())
|
|
}
|
|
rec = httptest.NewRecorder()
|
|
handleDictateConfig(rec, httptest.NewRequest("GET", "/api/dictate/config", nil))
|
|
var got DictateCfg
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("réponse illisible : %v", err)
|
|
}
|
|
if got.Device != "1" || got.Lang != "en" || got.Model != "small-q5_1" || got.Reactivity != "court" {
|
|
t.Errorf("relu %+v, attendu le réglage enregistré", got)
|
|
}
|
|
}
|
|
|
|
// Le refus doit venir de la ROUTE, pas seulement du magasin : sinon l'UI reçoit
|
|
// un 200 et croit son réglage pris en compte.
|
|
func TestConfigRouteRefuseModeleInconnu(t *testing.T) {
|
|
testHome(t)
|
|
corps := strings.NewReader(`{"model":"nawak","lang":"fr","device":"cpu","reactivity":"moyen"}`)
|
|
rec := httptest.NewRecorder()
|
|
handleDictateConfig(rec, httptest.NewRequest("POST", "/api/dictate/config", corps))
|
|
if rec.Code == 200 {
|
|
t.Errorf("modèle hors catalogue accepté (%d) : %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigRouteCorpsIllisible(t *testing.T) {
|
|
testHome(t)
|
|
rec := httptest.NewRecorder()
|
|
handleDictateConfig(rec, httptest.NewRequest("POST", "/api/dictate/config", strings.NewReader("pas du json")))
|
|
if rec.Code != 400 {
|
|
t.Errorf("code = %d, attendu 400", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestModelsRoute(t *testing.T) {
|
|
testHome(t)
|
|
rec := httptest.NewRecorder()
|
|
handleDictateModels(rec, httptest.NewRequest("GET", "/api/dictate/models", nil))
|
|
if rec.Code != 200 {
|
|
t.Fatalf("GET = %d", rec.Code)
|
|
}
|
|
var l []map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &l); err != nil {
|
|
t.Fatalf("réponse illisible : %v", err)
|
|
}
|
|
if len(l) < 4 {
|
|
t.Errorf("%d modèles renvoyés, au moins 4 attendus", len(l))
|
|
}
|
|
}
|
|
|
|
// Un identifiant inconnu ne doit pas lancer de téléchargement : whisperDownload
|
|
// bâtirait une URL vide et le message d'erreur n'arriverait qu'en arrière-plan.
|
|
func TestDownloadRouteRefuseInconnu(t *testing.T) {
|
|
testHome(t)
|
|
rec := httptest.NewRecorder()
|
|
corps := strings.NewReader(`{"id":"nawak"}`)
|
|
handleDictateDownload(rec, httptest.NewRequest("POST", "/api/dictate/models/download", corps))
|
|
if rec.Code != 400 {
|
|
t.Errorf("code = %d, attendu 400 pour un modèle inconnu", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestStateRoute(t *testing.T) {
|
|
testHome(t)
|
|
rec := httptest.NewRecorder()
|
|
handleDictateState(rec, httptest.NewRequest("GET", "/api/dictate/state", nil))
|
|
if rec.Code != 200 {
|
|
t.Fatalf("GET = %d", rec.Code)
|
|
}
|
|
var e map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &e); err != nil {
|
|
t.Fatalf("réponse illisible : %v", err)
|
|
}
|
|
for _, clef := range []string{"actif", "modele", "present", "device", "dl"} {
|
|
if _, ok := e[clef]; !ok {
|
|
t.Errorf("clé %q manquante dans l'état", clef)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Un audio trop court est rejeté AVANT toute tentative de lancer whisper-server :
|
|
// inutile de charger un modèle pour un clic parasite.
|
|
func TestTranscribeAudioTropCourt(t *testing.T) {
|
|
testHome(t)
|
|
rec := httptest.NewRecorder()
|
|
handleTranscribe(rec, httptest.NewRequest("POST", "/api/transcribe", strings.NewReader("court")))
|
|
if rec.Code != 400 {
|
|
t.Errorf("code = %d, attendu 400", rec.Code)
|
|
}
|
|
}
|