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
68 lines
2.4 KiB
Python
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"
|