diff --git a/moteur/__init__.py b/moteur/__init__.py index 5323fac..75ce117 100644 --- a/moteur/__init__.py +++ b/moteur/__init__.py @@ -59,4 +59,12 @@ def choisir_transcripteur(nom, modele, langue, amorce): if nom == "mlx": from moteur.mlx import TranscripteurMLX return TranscripteurMLX(modele, langue, amorce) + if nom in ("faster-cpu", "faster-gpu"): + # C'est ICI, et nulle part dans TranscripteurFasterWhisper, que le + # peripherique et le type de calcul sont decides : la classe reste + # testable sans jamais sonder de vrai materiel. + from moteur.faster import TranscripteurFasterWhisper + peripherique = "cuda" if nom == "faster-gpu" else "cpu" + type_calcul = "float16" if nom == "faster-gpu" else "int8" + return TranscripteurFasterWhisper(modele, langue, amorce, peripherique, type_calcul) raise NotImplementedError(f"Moteur non pris en charge à cette étape : {nom}") diff --git a/moteur/faster.py b/moteur/faster.py new file mode 100644 index 0000000..3ff153e --- /dev/null +++ b/moteur/faster.py @@ -0,0 +1,48 @@ +"""Transcription par faster-whisper (CTranslate2) — tourne partout : Mac Intel, +Windows avec ou sans GPU. Complement de moteur/mlx.py, qui lui est reserve a +l'Apple Silicon. +""" +import numpy as np + +from core import clean_transcript + + +class TranscripteurFasterWhisper: + """Implementation du contrat Transcripteur pour faster-whisper. + + Le peripherique (`cpu`/`cuda`) et le type de calcul (`int8`/`float16`...) + sont recus en parametres plutot que devines ici : cette classe ne sonde + jamais le materiel elle-meme. C'est `moteur/__init__.py::choisir_transcripteur` + qui tranche, d'apres `_cuda_disponible` — garder la decision hors de cette + classe la rend testable sans machine CUDA ni GPU. + """ + + def __init__(self, modele, langue, amorce, peripherique, type_calcul): + self.modele = modele + self.langue = langue + self.amorce = amorce + self.peripherique = peripherique + self.type_calcul = type_calcul + self._instance = None # modele CTranslate2 charge paresseusement + + def warmup(self): + """Charge le modele une fois, sur une seconde de silence.""" + self.transcrire(np.zeros(16000, dtype="float32")) + + def transcrire(self, audio): + # Import differe dans la methode, jamais au niveau module : sinon toute + # machine sans faster-whisper installe casserait a l'import de ce + # fichier, y compris pour les tests qui n'exercent que la fabrique + # (meme raison que moteur/mlx.py pour mlx_whisper). + from faster_whisper import WhisperModel + + if self._instance is None: + self._instance = WhisperModel( + self.modele, device=self.peripherique, compute_type=self.type_calcul + ) + segments, _info = self._instance.transcribe( + audio, language=self.langue, initial_prompt=self.amorce + ) + # faster-whisper rend un generateur de segments, pas un texte unique : + # il faut le consommer entierement avant de pouvoir le joindre. + return clean_transcript("".join(segment.text for segment in segments)) diff --git a/requirements.txt b/requirements.txt index 800b970..3d05395 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,9 @@ +# mlx-whisper : GPU Apple Silicon uniquement (moteur "mlx", machine actuelle). mlx-whisper>=0.4.3 +# faster-whisper : coeur CTranslate2, tourne partout (Mac Intel, Windows avec ou +# sans GPU). Les deux coexistent car aucun des deux ne couvre a lui seul toutes +# les plateformes visees par le portage — cf moteur/__init__.py::nom_moteur_pour. +faster-whisper>=1.0 sounddevice>=0.5.5 numpy>=2.4 pynput>=1.8.2 diff --git a/tests/test_moteur.py b/tests/test_moteur.py index 5cda145..cf06e39 100644 --- a/tests/test_moteur.py +++ b/tests/test_moteur.py @@ -64,3 +64,23 @@ def test_cuda_sonde_ne_leve_jamais_meme_si_le_chargeur_explose(): def charger(nom): raise RuntimeError("chargeur casse, pas seulement OSError") assert _cuda_disponible(plateforme="win32", charger=charger) is False + + +# --- faster-whisper : meme garantie que MLX ci-dessus — la fabrique rend un +# transcripteur complet sans jamais importer faster_whisper au niveau module, +# pour rester executable sur une machine qui ne l'a pas installe. + +def test_la_fabrique_rend_un_transcripteur_faster_cpu_complet(): + t = choisir_transcripteur(nom="faster-cpu", modele="Systran/faster-whisper-small", + langue="fr", amorce="") + assert callable(t.warmup) and callable(t.transcrire) + assert t.peripherique == "cpu" + assert t.type_calcul == "int8" + + +def test_la_fabrique_rend_un_transcripteur_faster_gpu_complet(): + t = choisir_transcripteur(nom="faster-gpu", modele="Systran/faster-whisper-small", + langue="fr", amorce="") + assert callable(t.warmup) and callable(t.transcrire) + assert t.peripherique == "cuda" + assert t.type_calcul == "float16"