Files
InkFlow/backend/tests/test_gemma_reasoning.py
colgora ba1813c583 Voicebank : vraies voix françaises (CML-TTS) + pool anonyme + garde-fou Qwen3
Remplace la voicebank générée par Kokoro (timbre anglais sur français phonémisé
-> accent que Qwen3 clonait) par 41 vraies voix FR issues de CML-TTS (livres
audio studio) : 1 narrateur dédié, 18F/14M nommées, 4F/4M anonymes réservées.

- scripts/import_voices.py : import multi-shards parquet, 1 clip/locuteur (le
  plus propre via levenshtein), genre estimé par F0 (YIN, anti-octave), filtre
  débit de parole (ref_text aligné sur l'audio).
- VoiceEntry.anonymous + assign_voices : les figurants « anonyme (...) » tirent
  dans un pool réservé, jamais mélangé avec les voix nommées ; narrateur dédié
  (fr_narrator remplace fr_f_siwis).
- dedup._anon_attrs : genre/âge déduits du nom anonyme (bon genre de voix).
- tts/qwen3.py : garde-fou anti-dérive (rejette/réessaie les sorties en boucle
  ou coupées en estimant la durée plausible du chunk).

Limite connue : Qwen3 ne sait pas synthétiser les fragments d'1-2 mots (incises,
titres) -> trous ; à traiter (repli Kokoro ou fusion des incises).

Inclut aussi du travail en cours antérieur (refacto backend LLM pluggable
mlx/lmstudio, benchmark, ajustements frontend/API).

Claude-Session: https://claude.ai/code/session_01XSVvcy1mfb4k1xDgib9vVU
2026-06-21 21:32:31 +02:00

68 lines
2.4 KiB
Python

"""Tests purs de `_strip_reasoning` (retrait de la chaine de pensee).
Sans charger de modele : on verifie que la pensee est retiree et que
`_extract_json` recupere bien la reponse FINALE (et non un fragment JSON
parasite present dans la pensee).
"""
from __future__ import annotations
from inkflow.analysis.llm._text import (
_extract_json,
_has_complete_json,
_strip_reasoning,
)
def test_has_complete_json_arret_anticipe():
# JSON complet -> True (on peut stopper la generation)
assert _has_complete_json('voici: {"speaker": "Marie"}')
assert _has_complete_json('[{"a": 1}]')
# JSON tronque (reponse pas encore finie) -> False (on continue)
assert not _has_complete_json('{"speaker": "Mar')
assert not _has_complete_json('texte sans json')
# cas streaming reel : pensee close + fence json en cours mais objet complet
buf = _strip_reasoning('<think>...</think>```json\n{"speaker": "Marie"}')
assert _has_complete_json(buf)
def test_format_a_canaux_gemma4():
raw = (
"<|channel>thought\n"
"Thinking Process: la capitale est Paris. Exemple: {\"capitale\": \"...\"}\n"
"<channel|>```json\n{\"capitale\": \"Paris\"}\n```"
)
cleaned = _strip_reasoning(raw)
# la pensee (et son JSON d'exemple parasite) a disparu
assert "Thinking Process" not in cleaned
assert '"..."' not in cleaned
# le JSON extrait est bien la reponse finale
assert _extract_json(cleaned) == {"capitale": "Paris"}
def test_balises_think_deepseek():
raw = "<think>je reflechis, peut-etre [1,2]</think>\n[{\"speaker\": \"Holden\"}]"
cleaned = _strip_reasoning(raw)
assert "reflechis" not in cleaned
assert _extract_json(cleaned) == [{"speaker": "Holden"}]
def test_sans_raisonnement_inchange():
raw = '{"speaker": "Kajri"}'
assert _strip_reasoning(raw) == raw
assert _extract_json(_strip_reasoning(raw)) == {"speaker": "Kajri"}
def test_pensee_tronquee_sans_fermeture():
# pensee non fermee (budget de tokens epuise) : le prefixe de canal saute,
# on ne renvoie pas le marqueur d'ouverture.
raw = "<|channel>thought\nje commence a reflechir mais c'est coupe"
cleaned = _strip_reasoning(raw)
assert not cleaned.startswith("<|channel")
assert "<channel" not in cleaned
def test_dernier_marqueur_gagne():
# plusieurs blocs : seule la derniere reponse finale compte
raw = "<think>a</think>milieu<think>b</think>FINAL"
assert _strip_reasoning(raw) == "FINAL"