Upload 2 files
Browse files- portable_env.py +87 -0
- transcribe_audio.py +211 -0
portable_env.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Bootstrap environnement portable pour BOB.
|
| 4 |
+
Force l'utilisation des ressources locales (ffmpeg, modèles, caches) et
|
| 5 |
+
offre un point unique pour configurer les variables d'environnement.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _prepend_path(dir_path: Path) -> None:
|
| 15 |
+
p = str(dir_path)
|
| 16 |
+
current = os.environ.get("PATH", "")
|
| 17 |
+
if p and p not in current:
|
| 18 |
+
os.environ["PATH"] = p + (";" if current else "") + current
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def setup_portable_env(base_dir: Path | None = None, force_ollama_portable: bool | None = None) -> Path:
|
| 22 |
+
"""
|
| 23 |
+
Configure l'environnement pour n'utiliser que le dossier courant.
|
| 24 |
+
|
| 25 |
+
- Ajoute vendor/ffmpeg/bin au PATH
|
| 26 |
+
- Fige les caches HF/Transformers/Whisper dans resources/models
|
| 27 |
+
- Définit le répertoire des modèles Ollama local
|
| 28 |
+
- Optionnellement force l'hôte Ollama portable (localhost:11435 par défaut)
|
| 29 |
+
|
| 30 |
+
Retourne base_dir normalisé.
|
| 31 |
+
"""
|
| 32 |
+
if base_dir is None:
|
| 33 |
+
# base_dir = racine du projet (.. depuis EXE)
|
| 34 |
+
here = Path(__file__).resolve().parent
|
| 35 |
+
base_dir = (here.parent).resolve()
|
| 36 |
+
|
| 37 |
+
# Expo pour d'autres modules
|
| 38 |
+
os.environ.setdefault("BOB_BASE_DIR", str(base_dir))
|
| 39 |
+
|
| 40 |
+
# 1) FFmpeg local
|
| 41 |
+
ffmpeg_bin = base_dir / "vendor" / "ffmpeg" / "bin"
|
| 42 |
+
if ffmpeg_bin.exists():
|
| 43 |
+
_prepend_path(ffmpeg_bin)
|
| 44 |
+
|
| 45 |
+
# 2) Caches et modèles locaux
|
| 46 |
+
models_root = base_dir / "resources" / "models"
|
| 47 |
+
hf_cache = models_root / "huggingface"
|
| 48 |
+
whisper_cache = models_root / "whisper"
|
| 49 |
+
numba_cache = models_root / "numba_cache"
|
| 50 |
+
for d in (hf_cache, whisper_cache, numba_cache):
|
| 51 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
# Hugging Face caches
|
| 54 |
+
os.environ.setdefault("HF_HOME", str(hf_cache))
|
| 55 |
+
os.environ.setdefault("TRANSFORMERS_CACHE", str(hf_cache))
|
| 56 |
+
os.environ.setdefault("HUGGINGFACE_HUB_CACHE", str(hf_cache))
|
| 57 |
+
# Whisper cache (openai-whisper regarde XDG_CACHE_HOME/WHISPER_CACHE_DIR)
|
| 58 |
+
os.environ.setdefault("WHISPER_CACHE_DIR", str(whisper_cache))
|
| 59 |
+
os.environ.setdefault("XDG_CACHE_HOME", str(models_root))
|
| 60 |
+
# NumPy/Numba
|
| 61 |
+
os.environ.setdefault("NUMBA_CACHE_DIR", str(numba_cache))
|
| 62 |
+
|
| 63 |
+
# 3) Dossiers d'E/S par défaut
|
| 64 |
+
os.environ.setdefault("BOB_INPUT_DIR", str(base_dir / "input"))
|
| 65 |
+
os.environ.setdefault("BOB_TRANSCRIPTIONS_DIR", str(base_dir / "output" / "transcriptions"))
|
| 66 |
+
os.environ.setdefault("BOB_OUTPUT_FILE", str(base_dir / "output" / "resume_bob.txt"))
|
| 67 |
+
|
| 68 |
+
# 4) Ollama portable (modèles + host)
|
| 69 |
+
ollama_models = base_dir / "resources" / "ollama" / "models"
|
| 70 |
+
ollama_models.mkdir(parents=True, exist_ok=True)
|
| 71 |
+
# OLLAMA_MODELS est reconnu par Ollama pour localiser les modèles
|
| 72 |
+
os.environ.setdefault("OLLAMA_MODELS", str(ollama_models))
|
| 73 |
+
|
| 74 |
+
# Forçage Ollama portable si demandé
|
| 75 |
+
if force_ollama_portable is None:
|
| 76 |
+
force_ollama_portable = os.environ.get("BOB_FORCE_PORTABLE_OLLAMA", "0") == "1"
|
| 77 |
+
|
| 78 |
+
if force_ollama_portable:
|
| 79 |
+
os.environ["BOB_FORCE_PORTABLE_OLLAMA"] = "1"
|
| 80 |
+
# Permettre override externe, sinon 11435
|
| 81 |
+
portable_host = os.environ.get("PORTABLE_OLLAMA_HOST", "http://localhost:11435")
|
| 82 |
+
os.environ["OLLAMA_HOST"] = portable_host
|
| 83 |
+
|
| 84 |
+
return base_dir
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
__all__ = ["setup_portable_env"]
|
transcribe_audio.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script de transcription audio avec Whisper
|
| 4 |
+
Traite tous les fichiers audio du dossier input et génère les transcriptions dans output/transcriptions
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import whisper
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import time
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
# Bootstrap environnement portable
|
| 14 |
+
try:
|
| 15 |
+
from portable_env import setup_portable_env
|
| 16 |
+
setup_portable_env()
|
| 17 |
+
except Exception:
|
| 18 |
+
pass
|
| 19 |
+
# Ajouter FFmpeg au PATH si nécessaire
|
| 20 |
+
ffmpeg_paths = [
|
| 21 |
+
r"C:\FFmpeg\bin",
|
| 22 |
+
r"C:\Program Files\FFmpeg\bin",
|
| 23 |
+
r"C:\Users\victo\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-8.0-full_build\bin"
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
for ffmpeg_path in ffmpeg_paths:
|
| 27 |
+
if os.path.exists(ffmpeg_path) and ffmpeg_path not in os.environ.get("PATH", ""):
|
| 28 |
+
os.environ["PATH"] = ffmpeg_path + ";" + os.environ.get("PATH", "")
|
| 29 |
+
# FFmpeg: déjà géré via portable_env (vendor/ffmpeg/bin)
|
| 30 |
+
|
| 31 |
+
# Configuration
|
| 32 |
+
INPUT_DIR = Path(os.environ.get("BOB_INPUT_DIR", Path(__file__).parent.parent / "input"))
|
| 33 |
+
OUTPUT_DIR = Path(os.environ.get("BOB_TRANSCRIPTIONS_DIR", Path(__file__).parent.parent / "output" / "transcriptions"))
|
| 34 |
+
WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "small") # peut être override par GUI
|
| 35 |
+
SUPPORTED_FORMATS = ['.mp3', '.wav', '.m4a', '.flac', '.ogg', '.mp4', '.avi', '.mov']
|
| 36 |
+
|
| 37 |
+
def load_whisper_model(model_name):
|
| 38 |
+
"""Charge un modèle de transcription.
|
| 39 |
+
- 'faster-whisper:small|medium|large-v3|...' utilise faster_whisper.WhisperModel
|
| 40 |
+
- sinon utilise openai whisper.load_model
|
| 41 |
+
Retourne un tuple (backend, model)
|
| 42 |
+
backend in {"openai", "faster"}
|
| 43 |
+
"""
|
| 44 |
+
print(f"Chargement du modèle Whisper: {model_name}")
|
| 45 |
+
if isinstance(model_name, str) and model_name.startswith("faster-whisper"):
|
| 46 |
+
# Extraire la taille après le ':'
|
| 47 |
+
size = model_name.split(":", 1)[1] if ":" in model_name else "small"
|
| 48 |
+
try:
|
| 49 |
+
from faster_whisper import WhisperModel
|
| 50 |
+
except Exception as e:
|
| 51 |
+
raise RuntimeError(f"faster-whisper non disponible: {e}")
|
| 52 |
+
|
| 53 |
+
# Le téléchargement ira dans HF_HOME/HUGGINGFACE cache local
|
| 54 |
+
compute_type = "int8" # CPU-friendly par défaut; peut être ajusté
|
| 55 |
+
model = WhisperModel(size, device="cpu", compute_type=compute_type)
|
| 56 |
+
print("Modèle faster-whisper prêt.")
|
| 57 |
+
return ("faster", model)
|
| 58 |
+
else:
|
| 59 |
+
model = whisper.load_model(model_name)
|
| 60 |
+
print("Modèle OpenAI Whisper prêt.")
|
| 61 |
+
return ("openai", model)
|
| 62 |
+
|
| 63 |
+
def get_audio_files(input_dir):
|
| 64 |
+
"""Récupère tous les fichiers audio du dossier input"""
|
| 65 |
+
audio_files = []
|
| 66 |
+
if not input_dir.exists():
|
| 67 |
+
return audio_files
|
| 68 |
+
# Récupérer tous les fichiers du dossier
|
| 69 |
+
for file in input_dir.iterdir():
|
| 70 |
+
if file.is_file():
|
| 71 |
+
# Vérifier l'extension (case insensitive)
|
| 72 |
+
file_ext = file.suffix.lower()
|
| 73 |
+
if file_ext in [ext.lower() for ext in SUPPORTED_FORMATS]:
|
| 74 |
+
audio_files.append(file)
|
| 75 |
+
|
| 76 |
+
return sorted(list(set(audio_files))) # Éliminer les doublons
|
| 77 |
+
|
| 78 |
+
def transcribe_file(model, audio_file, output_dir):
|
| 79 |
+
"""Transcrit un fichier audio et sauvegarde le résultat"""
|
| 80 |
+
print(f"Transcription de: {audio_file.name}")
|
| 81 |
+
print(f"Chemin complet: {audio_file.absolute()}")
|
| 82 |
+
print(f"Fichier existe: {audio_file.exists()}")
|
| 83 |
+
print(f"Taille du fichier: {audio_file.stat().st_size if audio_file.exists() else 'N/A'} bytes")
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
# Vérifier que le fichier existe
|
| 87 |
+
if not audio_file.exists():
|
| 88 |
+
print(f"✗ Fichier introuvable: {audio_file}")
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
+
# Transcription avec Whisper - utiliser le chemin sans espaces si possible
|
| 92 |
+
audio_path = str(audio_file.absolute())
|
| 93 |
+
print(f"Chemin utilisé pour Whisper: {audio_path}")
|
| 94 |
+
|
| 95 |
+
start_time = time.time()
|
| 96 |
+
# Support des deux backends
|
| 97 |
+
if isinstance(model, tuple) and model and model[0] in ("openai", "faster"):
|
| 98 |
+
backend, engine = model
|
| 99 |
+
else:
|
| 100 |
+
# Rétrocompatibilité: ancien code passait juste l'objet
|
| 101 |
+
backend, engine = ("openai", model)
|
| 102 |
+
|
| 103 |
+
if backend == "openai":
|
| 104 |
+
result = engine.transcribe(audio_path, language="fr", verbose=False)
|
| 105 |
+
text = result["text"]
|
| 106 |
+
else:
|
| 107 |
+
# faster-whisper retourne des segments + info
|
| 108 |
+
segments, info = engine.transcribe(audio_path, language="fr")
|
| 109 |
+
pieces = []
|
| 110 |
+
for seg in segments:
|
| 111 |
+
pieces.append(seg.text)
|
| 112 |
+
text = " ".join(pieces).strip()
|
| 113 |
+
end_time = time.time()
|
| 114 |
+
|
| 115 |
+
# Nom du fichier de sortie
|
| 116 |
+
output_filename = audio_file.stem + "_transcription.txt"
|
| 117 |
+
output_path = output_dir / output_filename
|
| 118 |
+
|
| 119 |
+
# Sauvegarde de la transcription
|
| 120 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 121 |
+
f.write(f"Fichier source: {audio_file.name}\n")
|
| 122 |
+
f.write(f"Date de transcription: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n")
|
| 123 |
+
f.write(f"Durée de traitement: {end_time - start_time:.2f} secondes\n")
|
| 124 |
+
f.write(f"Modèle utilisé: {WHISPER_MODEL}\n")
|
| 125 |
+
f.write("-" * 50 + "\n\n")
|
| 126 |
+
f.write(text)
|
| 127 |
+
|
| 128 |
+
print(f"✓ Transcription sauvegardée: {output_filename}")
|
| 129 |
+
print(f" Durée de traitement: {end_time - start_time:.2f}s")
|
| 130 |
+
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
print(f"✗ Erreur lors de la transcription de {audio_file.name}: {e}")
|
| 135 |
+
print(f"Type d'erreur: {type(e).__name__}")
|
| 136 |
+
import traceback
|
| 137 |
+
traceback.print_exc()
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
def main():
|
| 141 |
+
"""Fonction principale"""
|
| 142 |
+
print("=" * 60)
|
| 143 |
+
print("TRANSCRIPTION AUTOMATIQUE DES BOB")
|
| 144 |
+
print("=" * 60)
|
| 145 |
+
|
| 146 |
+
# Vérification des dossiers - chemins absolus
|
| 147 |
+
script_dir = Path(__file__).parent.absolute()
|
| 148 |
+
input_dir = Path(os.environ.get("BOB_INPUT_DIR", script_dir.parent / "input"))
|
| 149 |
+
output_dir = Path(os.environ.get("BOB_TRANSCRIPTIONS_DIR", script_dir.parent / "output" / "transcriptions"))
|
| 150 |
+
|
| 151 |
+
print(f"Dossier script: {script_dir}")
|
| 152 |
+
print(f"Dossier input: {input_dir}")
|
| 153 |
+
print(f"Dossier output: {output_dir}")
|
| 154 |
+
print()
|
| 155 |
+
|
| 156 |
+
if not input_dir.exists():
|
| 157 |
+
print(f"Erreur: Le dossier input n'existe pas: {input_dir}")
|
| 158 |
+
return
|
| 159 |
+
|
| 160 |
+
# Création du dossier de sortie si nécessaire
|
| 161 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 162 |
+
|
| 163 |
+
# Recherche des fichiers audio
|
| 164 |
+
audio_files = get_audio_files(input_dir)
|
| 165 |
+
|
| 166 |
+
if not audio_files:
|
| 167 |
+
print(f"Aucun fichier audio trouvé dans {input_dir}")
|
| 168 |
+
print(f"Formats supportés: {', '.join(SUPPORTED_FORMATS)}")
|
| 169 |
+
return
|
| 170 |
+
|
| 171 |
+
print(f"Trouvé {len(audio_files)} fichier(s) audio à traiter:")
|
| 172 |
+
for i, file in enumerate(audio_files, 1):
|
| 173 |
+
print(f" {i}. {file.name}")
|
| 174 |
+
|
| 175 |
+
print()
|
| 176 |
+
|
| 177 |
+
# Chargement du modèle Whisper
|
| 178 |
+
try:
|
| 179 |
+
model = load_whisper_model(WHISPER_MODEL)
|
| 180 |
+
except Exception as e:
|
| 181 |
+
print(f"Erreur lors du chargement du modèle: {e}")
|
| 182 |
+
return
|
| 183 |
+
|
| 184 |
+
print()
|
| 185 |
+
|
| 186 |
+
# Traitement des fichiers
|
| 187 |
+
success_count = 0
|
| 188 |
+
total_start_time = time.time()
|
| 189 |
+
|
| 190 |
+
for i, audio_file in enumerate(audio_files, 1):
|
| 191 |
+
print(f"[{i}/{len(audio_files)}] ", end="")
|
| 192 |
+
|
| 193 |
+
if transcribe_file(model, audio_file, output_dir):
|
| 194 |
+
success_count += 1
|
| 195 |
+
|
| 196 |
+
print()
|
| 197 |
+
|
| 198 |
+
total_end_time = time.time()
|
| 199 |
+
|
| 200 |
+
# Résumé
|
| 201 |
+
print("=" * 60)
|
| 202 |
+
print("RÉSUMÉ")
|
| 203 |
+
print("=" * 60)
|
| 204 |
+
print(f"Fichiers traités: {len(audio_files)}")
|
| 205 |
+
print(f"Réussites: {success_count}")
|
| 206 |
+
print(f"Échecs: {len(audio_files) - success_count}")
|
| 207 |
+
print(f"Durée totale: {total_end_time - total_start_time:.2f} secondes")
|
| 208 |
+
print(f"Transcriptions sauvegardées dans: {output_dir}")
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
main()
|