Compare commits
No commits in common. "2ccb614649be2d468e42a4d7d57aec3301a8cdd7" and "5cf8f448edb4d73a62a4cdad665eed910ef5ef17" have entirely different histories.
2ccb614649
...
5cf8f448ed
7
app.py
7
app.py
@ -29,7 +29,7 @@ from PyObjCTools import AppHelper
|
|||||||
from pynput import keyboard
|
from pynput import keyboard
|
||||||
|
|
||||||
from controller import ZonzaController
|
from controller import ZonzaController
|
||||||
from core import CONFIG, modele_retenu, validate_config
|
from core import CONFIG, validate_config
|
||||||
from interface import classe_overlay_pour
|
from interface import classe_overlay_pour
|
||||||
from moteur import choisir_transcripteur, detecter_moteur
|
from moteur import choisir_transcripteur, detecter_moteur
|
||||||
from systeme import choisir_systeme
|
from systeme import choisir_systeme
|
||||||
@ -141,10 +141,9 @@ def main():
|
|||||||
return
|
return
|
||||||
main._lock = lock # garder le verrou vivant tant que le process vit
|
main._lock = lock # garder le verrou vivant tant que le process vit
|
||||||
validate_config(CONFIG)
|
validate_config(CONFIG)
|
||||||
moteur = detecter_moteur()
|
|
||||||
transcripteur = choisir_transcripteur(
|
transcripteur = choisir_transcripteur(
|
||||||
nom=moteur,
|
nom=detecter_moteur(),
|
||||||
modele=modele_retenu(moteur, CONFIG["model"]),
|
modele=CONFIG["model"],
|
||||||
langue=CONFIG["language"],
|
langue=CONFIG["language"],
|
||||||
amorce=CONFIG["initial_prompt"],
|
amorce=CONFIG["initial_prompt"],
|
||||||
)
|
)
|
||||||
|
|||||||
41
core.py
41
core.py
@ -6,9 +6,7 @@ Tout ce qui est ici est testable sans micro, sans clavier, sans modèle.
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
# Forcage manuel du modele. None = on suit MODELES_PAR_MOTEUR, qui choisit
|
"model": "mlx-community/whisper-medium",
|
||||||
# d'apres le moteur detecte. Renseigner cette cle prime sur la table.
|
|
||||||
"model": None,
|
|
||||||
"language": "fr",
|
"language": "fr",
|
||||||
"hotkey": "<cmd>+<ctrl>+d", # Cmd+Ctrl+D (libre : ni macOS ni VS Code ne l'utilisent)
|
"hotkey": "<cmd>+<ctrl>+d", # Cmd+Ctrl+D (libre : ni macOS ni VS Code ne l'utilisent)
|
||||||
"sounds": True,
|
"sounds": True,
|
||||||
@ -165,43 +163,6 @@ def duree_vie_overlay_coherente(config):
|
|||||||
return config["duree_max_overlay_s"] >= config["max_duration_s"] + MARGE_TRANSCRIPTION_S
|
return config["duree_max_overlay_s"] >= config["max_duration_s"] + MARGE_TRANSCRIPTION_S
|
||||||
|
|
||||||
|
|
||||||
# --- modele par moteur ----------------------------------------------------
|
|
||||||
# Chaque moteur a son propre format de depot : MLX lit `mlx-community/*`,
|
|
||||||
# faster-whisper lit `Systran/faster-whisper-*`. Passer l'un a l'autre echoue.
|
|
||||||
#
|
|
||||||
# Mesure du 2026-08-23, trois echantillons, amorce de vocabulaire active,
|
|
||||||
# sur le CPU d'un Apple Silicon :
|
|
||||||
# faster-whisper base 0,52 s 10/13 termes techniques 142 Mo
|
|
||||||
# faster-whisper small 1,42 s 11/13 464 Mo
|
|
||||||
# faster-whisper medium 4,40 s 13/13 1460 Mo
|
|
||||||
# MLX medium (GPU) 0,75 s 11/13 489 Mo
|
|
||||||
#
|
|
||||||
# D'ou les choix : le GPU vise la precision (medium, 13/13, le GPU absorbe le
|
|
||||||
# cout) ; le CPU vise la latence (small, 11/13 — soit AUTANT que le MLX utilise
|
|
||||||
# aujourd'hui — pour 1,4 s ici). `medium` sur CPU serait plus juste, mais 4,4 s
|
|
||||||
# ont ete mesures sur un CPU Apple Silicon, rapide : sur un CPU Intel ou Windows,
|
|
||||||
# le meme modele peut demander trois a quatre fois plus. La commande de
|
|
||||||
# diagnostic (etape 4) mesurera la machine reelle et pourra promouvoir `medium`.
|
|
||||||
MODELES_PAR_MOTEUR = {
|
|
||||||
"mlx": "mlx-community/whisper-medium",
|
|
||||||
"faster-gpu": "Systran/faster-whisper-medium",
|
|
||||||
"faster-cpu": "Systran/faster-whisper-small",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def modele_retenu(nom_moteur, forcage=None):
|
|
||||||
"""Modele finalement utilise : le forcage manuel s'il est renseigne, sinon la
|
|
||||||
table. Une chaine vide est traitee comme une absence de forcage."""
|
|
||||||
return forcage or modele_pour_moteur(nom_moteur)
|
|
||||||
|
|
||||||
|
|
||||||
def modele_pour_moteur(nom):
|
|
||||||
"""Rend le depot de modele adapte a ce moteur. Leve KeyError si inconnu."""
|
|
||||||
if nom not in MODELES_PAR_MOTEUR:
|
|
||||||
raise KeyError(f"Aucun modele connu pour le moteur : {nom}")
|
|
||||||
return MODELES_PAR_MOTEUR[nom]
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config(config):
|
def validate_config(config):
|
||||||
"""Valide la config ; lève ValueError si une valeur est aberrante."""
|
"""Valide la config ; lève ValueError si une valeur est aberrante."""
|
||||||
if config["min_duration_s"] < 0:
|
if config["min_duration_s"] < 0:
|
||||||
|
|||||||
@ -59,12 +59,4 @@ def choisir_transcripteur(nom, modele, langue, amorce):
|
|||||||
if nom == "mlx":
|
if nom == "mlx":
|
||||||
from moteur.mlx import TranscripteurMLX
|
from moteur.mlx import TranscripteurMLX
|
||||||
return TranscripteurMLX(modele, langue, amorce)
|
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}")
|
raise NotImplementedError(f"Moteur non pris en charge à cette étape : {nom}")
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
"""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))
|
|
||||||
@ -1,9 +1,4 @@
|
|||||||
# mlx-whisper : GPU Apple Silicon uniquement (moteur "mlx", machine actuelle).
|
|
||||||
mlx-whisper>=0.4.3
|
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
|
sounddevice>=0.5.5
|
||||||
numpy>=2.4
|
numpy>=2.4
|
||||||
pynput>=1.8.2
|
pynput>=1.8.2
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
"""À chaque moteur son modèle.
|
|
||||||
|
|
||||||
`app.py` passait `CONFIG["model"]` — un dépôt MLX — quel que soit le moteur détecté.
|
|
||||||
Sur une machine sans Apple Silicon, `faster-whisper` aurait reçu un dépôt qu'il ne
|
|
||||||
sait pas charger. Le défaut était invisible ici, puisque la détection rend toujours
|
|
||||||
`mlx` sur ce Mac.
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from core import MODELES_PAR_MOTEUR, modele_pour_moteur
|
|
||||||
|
|
||||||
|
|
||||||
def test_chaque_moteur_connu_a_son_modele():
|
|
||||||
for nom in ("mlx", "faster-cpu", "faster-gpu"):
|
|
||||||
assert modele_pour_moteur(nom)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mlx_garde_le_modele_d_aujourd_hui():
|
|
||||||
assert modele_pour_moteur("mlx") == "mlx-community/whisper-medium"
|
|
||||||
|
|
||||||
|
|
||||||
def test_les_moteurs_faster_ne_recoivent_jamais_un_depot_mlx():
|
|
||||||
for nom in ("faster-cpu", "faster-gpu"):
|
|
||||||
assert "mlx" not in modele_pour_moteur(nom).lower()
|
|
||||||
|
|
||||||
|
|
||||||
def test_le_gpu_vise_la_precision_le_cpu_la_latence():
|
|
||||||
# mesuré le 2026-08-23 : medium = 13/13 termes mais 4,4 s sur CPU ; small = 11/13 en 1,4 s
|
|
||||||
assert "medium" in modele_pour_moteur("faster-gpu")
|
|
||||||
assert "small" in modele_pour_moteur("faster-cpu")
|
|
||||||
|
|
||||||
|
|
||||||
def test_un_moteur_inconnu_est_refuse_clairement():
|
|
||||||
with pytest.raises(KeyError) as e:
|
|
||||||
modele_pour_moteur("vosk")
|
|
||||||
assert "vosk" in str(e.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_la_table_ne_contient_que_les_moteurs_reconnus():
|
|
||||||
assert set(MODELES_PAR_MOTEUR) == {"mlx", "faster-cpu", "faster-gpu"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_le_reglage_manuel_prime_sur_la_table():
|
|
||||||
from core import modele_retenu
|
|
||||||
assert modele_retenu("faster-cpu", forcage=None) == modele_pour_moteur("faster-cpu")
|
|
||||||
assert modele_retenu("faster-cpu", forcage="Systran/faster-whisper-medium") \
|
|
||||||
== "Systran/faster-whisper-medium"
|
|
||||||
|
|
||||||
|
|
||||||
def test_un_forcage_vide_est_ignore():
|
|
||||||
from core import modele_retenu
|
|
||||||
assert modele_retenu("mlx", forcage="") == modele_pour_moteur("mlx")
|
|
||||||
@ -64,23 +64,3 @@ def test_cuda_sonde_ne_leve_jamais_meme_si_le_chargeur_explose():
|
|||||||
def charger(nom):
|
def charger(nom):
|
||||||
raise RuntimeError("chargeur casse, pas seulement OSError")
|
raise RuntimeError("chargeur casse, pas seulement OSError")
|
||||||
assert _cuda_disponible(plateforme="win32", charger=charger) is False
|
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"
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user