From 734bba31da3ac15c5eafcdd05d7406c5ef171f7d Mon Sep 17 00:00:00 2001 From: Ralph Mayola Date: Sun, 23 Aug 2026 18:05:31 +0200 Subject: [PATCH] Extrait la transcription derriere le contrat Transcripteur Co-Authored-By: Claude Opus 5 --- moteur/__init__.py | 33 +++++++++++++++++++++++++++++++++ moteur/mlx.py | 27 +++++++++++++++++++++++++++ tests/test_moteur.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 moteur/__init__.py create mode 100644 moteur/mlx.py create mode 100644 tests/test_moteur.py diff --git a/moteur/__init__.py b/moteur/__init__.py new file mode 100644 index 0000000..888854b --- /dev/null +++ b/moteur/__init__.py @@ -0,0 +1,33 @@ +"""Choisit le moteur de transcription d'après le matériel.""" +import platform +import sys + + +def nom_moteur_pour(plateforme, machine, cuda_disponible): + """Décide du moteur. Fonction PURE : aucune interrogation de la machine ici, + ce qui rend les quatre configurations testables depuis un seul poste.""" + if plateforme.startswith("darwin") and machine in ("arm64", "aarch64"): + return "mlx" + return "faster-gpu" if cuda_disponible else "faster-cpu" + + +def detecter_moteur(): + """Interroge la machine, puis délègue la décision à `nom_moteur_pour`.""" + return nom_moteur_pour(sys.platform, platform.machine(), _cuda_disponible()) + + +def _cuda_disponible(): + try: + import ctypes + ctypes.CDLL("libcudart.so") + return True + except Exception: + return False + + +def choisir_transcripteur(nom, modele, langue, amorce): + """Rend l'implémentation du contrat Transcripteur portant ce nom.""" + if nom == "mlx": + from moteur.mlx import TranscripteurMLX + return TranscripteurMLX(modele, langue, amorce) + raise NotImplementedError(f"Moteur non pris en charge à cette étape : {nom}") diff --git a/moteur/mlx.py b/moteur/mlx.py new file mode 100644 index 0000000..d5c3500 --- /dev/null +++ b/moteur/mlx.py @@ -0,0 +1,27 @@ +"""Transcription par MLX-Whisper — GPU Apple Silicon uniquement.""" +import numpy as np + +from core import clean_transcript + + +class TranscripteurMLX: + """Implémentation du contrat Transcripteur pour Apple Silicon.""" + + def __init__(self, modele, langue, amorce): + self.modele = modele + self.langue = langue + self.amorce = amorce + + def warmup(self): + """Charge le modèle une fois, sur une seconde de silence.""" + self.transcrire(np.zeros(16000, dtype="float32")) + + def transcrire(self, audio): + import mlx_whisper + resultat = mlx_whisper.transcribe( + audio, + path_or_hf_repo=self.modele, + language=self.langue, + initial_prompt=self.amorce, + ) + return clean_transcript(resultat.get("text", "")) diff --git a/tests/test_moteur.py b/tests/test_moteur.py new file mode 100644 index 0000000..a6a4b9f --- /dev/null +++ b/tests/test_moteur.py @@ -0,0 +1,31 @@ +"""La fabrique choisit le moteur d'après le matériel — décision pure, testable +depuis n'importe quelle machine.""" +import pytest + +from moteur import choisir_transcripteur, nom_moteur_pour + + +@pytest.mark.parametrize("plateforme, machine, cuda, attendu", [ + ("darwin", "arm64", False, "mlx"), + ("darwin", "x86_64", False, "faster-cpu"), + ("win32", "AMD64", True, "faster-gpu"), + ("win32", "AMD64", False, "faster-cpu"), +]) +def test_la_decision_suit_le_materiel(plateforme, machine, cuda, attendu): + assert nom_moteur_pour(plateforme, machine, cuda) == attendu + + +def test_apple_silicon_prend_mlx_meme_sans_cuda(): + assert nom_moteur_pour("darwin", "arm64", cuda_disponible=False) == "mlx" + + +def test_la_fabrique_rend_un_transcripteur_complet(): + t = choisir_transcripteur(nom="mlx", modele="mlx-community/whisper-medium", + langue="fr", amorce="") + assert callable(t.warmup) and callable(t.transcrire) + + +def test_un_moteur_inconnu_est_refuse_clairement(): + with pytest.raises(NotImplementedError) as e: + choisir_transcripteur(nom="vosk", modele="x", langue="fr", amorce="") + assert "vosk" in str(e.value)