Asterisk + Whisper open source : transcription 100% locale
Dans notre article precedent, nous avons vu comment raccorder Asterisk a l'API Whisper d'OpenAI. Le probleme ? Vos conversations audio sont envoyees sur les serveurs d'OpenAI. Pour beaucoup d'entreprises — sante, juridique, finance, operateurs telecoms — c'est un deal-breaker.
La bonne nouvelle : le modele Whisper est open source (licence MIT). Vous pouvez l'heberger vous-meme et transcrire sans envoyer une seule donnee a l'exterieur.
Ce guide utilise faster-whisper, une reimplementation optimisee de Whisper qui tourne 4x plus vite avec moins de VRAM.
Pourquoi faster-whisper plutot que Whisper vanilla ?
| Whisper (OpenAI) | faster-whisper | API OpenAI | |
|---|---|---|---|
| Licence | MIT | MIT | Proprietaire |
| Donnees | Locales | Locales | Envoyees a OpenAI |
| Vitesse | 1x | 4x (CTranslate2) | Variable |
| VRAM GPU | ~10 Go (large-v3) | ~4 Go (large-v3 int8) | N/A |
| CPU only | Lent | Acceptable (AVX2) | N/A |
| Cout | GPU one-shot | GPU one-shot | $0.006/min |
| VAD | Non | Oui (Silero) | Non |
faster-whisper utilise CTranslate2 pour l'inference, ce qui permet la quantification int8 et divise par 2 la VRAM necessaire.
Prerequis
- Serveur Debian 12/13 avec Asterisk 18+ fonctionnel
- Option GPU (recommandee) : NVIDIA avec 8 Go VRAM min (RTX 3060, RTX 4060, T4, A10)
- Option CPU : 8+ coeurs avec AVX2 (Intel Xeon, AMD EPYC)
- Python 3.10+
- 10 Go d'espace disque pour le modele
1. Installer faster-whisper
Option A : GPU NVIDIA (recommandee)
# Installer les drivers NVIDIA + CUDA
apt install -y nvidia-driver firmware-misc-nonfree
# Verifier CUDA
nvidia-smi
# Installer faster-whisper avec support CUDA
pip install faster-whisper
Option B : CPU seulement
# faster-whisper fonctionne aussi en CPU (plus lent mais viable)
pip install faster-whisper
# Verifier le support AVX2 (important pour les performances)
grep -o 'avx2' /proc/cpuinfo | head -1
Telecharger le modele
# Le modele se telecharge automatiquement au premier lancement
# Vous pouvez aussi le pre-telecharger :
python3 -c "
from faster_whisper import WhisperModel
model = WhisperModel('large-v3', device='cuda', compute_type='int8_float16')
print('Modele telecharge avec succes')
"
| Modele | Taille | VRAM (int8) | Qualite |
|---|---|---|---|
| tiny | 75 Mo | ~1 Go | Basique |
| base | 140 Mo | ~1 Go | Correcte |
| small | 460 Mo | ~2 Go | Bonne |
| medium | 1.5 Go | ~3 Go | Tres bonne |
| large-v3 | 3 Go | ~4 Go | Excellente |
2. Script de transcription locale
#!/usr/bin/env python3
# /usr/local/bin/whisper-local-transcribe.py
# Transcription locale avec faster-whisper — aucune donnee externe.
import sys
import os
import json
import logging
from datetime import datetime
from pathlib import Path
from faster_whisper import WhisperModel
# ── Configuration ──
MODEL_SIZE = os.environ.get('WHISPER_MODEL', 'large-v3')
DEVICE = os.environ.get('WHISPER_DEVICE', 'cuda') # 'cuda' ou 'cpu'
COMPUTE_TYPE = os.environ.get('WHISPER_COMPUTE', 'int8_float16') # GPU: int8_float16, CPU: int8
LANGUAGE = os.environ.get('WHISPER_LANG', 'fr')
DB_PATH = os.environ.get('TRANSCRIPTION_DB', '/var/lib/asterisk/transcriptions.jsonl')
WEBHOOK_URL = os.environ.get('WEBHOOK_URL', '')
# ── Logging ──
logging.basicConfig(
filename='/var/log/whisper-transcribe.log',
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s'
)
log = logging.getLogger(__name__)
# ── Charger le modele une seule fois (si lance comme service) ──
_model = None
def get_model():
global _model
if _model is None:
log.info(f'Loading model {MODEL_SIZE} on {DEVICE} ({COMPUTE_TYPE})')
_model = WhisperModel(
MODEL_SIZE,
device=DEVICE,
compute_type=COMPUTE_TYPE,
)
log.info('Model loaded')
return _model
def transcribe(file_path: str) -> dict:
model = get_model()
segments, info = model.transcribe(
file_path,
language=LANGUAGE,
beam_size=5,
vad_filter=True, # Silero VAD : ignore les silences
vad_parameters=dict(
min_silence_duration_ms=500,
speech_pad_ms=200,
),
word_timestamps=True,
)
result_segments = []
full_text = []
for seg in segments:
result_segments.append({
'start': round(seg.start, 2),
'end': round(seg.end, 2),
'text': seg.text.strip(),
})
full_text.append(seg.text.strip())
return {
'text': ' '.join(full_text),
'language': info.language,
'language_probability': round(info.language_probability, 3),
'duration': round(info.duration, 2),
'segments': result_segments,
}
def save_transcription(unique_id: str, result: dict):
entry = {
'unique_id': unique_id,
'timestamp': datetime.now().isoformat(),
**result,
}
db_path = Path(DB_PATH)
db_path.parent.mkdir(parents=True, exist_ok=True)
with open(db_path, 'a') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
log.info(f'Saved {unique_id}: {len(result["text"])} chars, {result["duration"]}s')
def send_webhook(unique_id: str, result: dict):
if not WEBHOOK_URL:
return
import requests
try:
requests.post(WEBHOOK_URL, json={
'unique_id': unique_id,
'text': result['text'],
'duration': result['duration'],
}, timeout=10)
except Exception as e:
log.error(f'Webhook error: {e}')
def main():
if len(sys.argv) < 3:
print(f'Usage: {sys.argv[0]} <audio_file> <unique_id>')
sys.exit(1)
audio_file = sys.argv[1]
unique_id = sys.argv[2]
if not os.path.isfile(audio_file):
log.error(f'Not found: {audio_file}')
sys.exit(1)
file_size = os.path.getsize(audio_file)
if file_size < 1024:
log.warning(f'Too small ({file_size}B), skipping')
sys.exit(0)
log.info(f'Transcribing {audio_file} ({file_size}B) for {unique_id}')
try:
result = transcribe(audio_file)
save_transcription(unique_id, result)
send_webhook(unique_id, result)
log.info(f'Done: {unique_id}')
except Exception as e:
log.error(f'Failed {unique_id}: {e}')
sys.exit(1)
if __name__ == '__main__':
main()
chmod +x /usr/local/bin/whisper-local-transcribe.py
3. Configurer Asterisk
Le dialplan est quasiment identique a la version API — seul le script appele change :
; extensions.conf
[from-internal]
exten => _0XXXXXXXXX,1,NoOp(Appel sortant vers ${EXTEN})
same => n,Set(RECORDING_FILE=/var/spool/asterisk/recordings/${UNIQUEID})
same => n,MixMonitor(${RECORDING_FILE}.wav,b)
same => n,Dial(PJSIP/${EXTEN}@trunk-operateur,60,tT)
same => n,Hangup()
exten => h,1,NoOp(Post-hangup : transcription locale)
same => n,System(/usr/local/bin/whisper-local-transcribe.sh ${RECORDING_FILE}.wav ${UNIQUEID} &)
Script shell wrapper
#!/bin/bash
# /usr/local/bin/whisper-local-transcribe.sh
RECORDING="$1"
UNIQUEID="$2"
[ ! -s "$RECORDING" ] && exit 1
# Convertir en 16kHz mono pour de meilleures performances
OPTIMIZED="${RECORDING%.wav}_16k.wav"
sox "$RECORDING" -r 16000 -c 1 "$OPTIMIZED" 2>/dev/null || cp "$RECORDING" "$OPTIMIZED"
# Variables d'environnement
export WHISPER_MODEL="${WHISPER_MODEL:-large-v3}"
export WHISPER_DEVICE="${WHISPER_DEVICE:-cuda}"
export WHISPER_COMPUTE="${WHISPER_COMPUTE:-int8_float16}"
/usr/local/bin/whisper-local-transcribe.py "$OPTIMIZED" "$UNIQUEID"
rm -f "$OPTIMIZED"
chmod +x /usr/local/bin/whisper-local-transcribe.sh
4. Mode service (recommande pour la production)
Lancer le script a chaque appel charge le modele a chaque fois (~10-20s). En production, on veut un service persistant qui garde le modele en memoire.
API REST locale avec FastAPI
pip install fastapi uvicorn python-multipart
#!/usr/bin/env python3
# /opt/whisper-service/server.py
# Service de transcription locale persistent.
import os
import tempfile
import logging
from fastapi import FastAPI, UploadFile, File
from faster_whisper import WhisperModel
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
MODEL_SIZE = os.environ.get('WHISPER_MODEL', 'large-v3')
DEVICE = os.environ.get('WHISPER_DEVICE', 'cuda')
COMPUTE_TYPE = os.environ.get('WHISPER_COMPUTE', 'int8_float16')
app = FastAPI(title='Whisper Local Service')
log.info(f'Loading {MODEL_SIZE} on {DEVICE}...')
model = WhisperModel(MODEL_SIZE, device=DEVICE, compute_type=COMPUTE_TYPE)
log.info('Model loaded, ready to serve')
@app.post('/transcribe')
async def transcribe_endpoint(
file: UploadFile = File(...),
language: str = 'fr',
):
# Sauvegarder le fichier temporaire
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp:
content = await file.read()
tmp.write(content)
tmp_path = tmp.name
try:
segments, info = model.transcribe(
tmp_path,
language=language,
beam_size=5,
vad_filter=True,
)
result_segments = []
full_text = []
for seg in segments:
result_segments.append({
'start': round(seg.start, 2),
'end': round(seg.end, 2),
'text': seg.text.strip(),
})
full_text.append(seg.text.strip())
return {
'text': ' '.join(full_text),
'language': info.language,
'duration': round(info.duration, 2),
'segments': result_segments,
}
finally:
os.unlink(tmp_path)
Service systemd
# /etc/systemd/system/whisper-service.service
[Unit]
Description=Whisper Local Transcription Service
After=network.target
[Service]
Type=simple
User=asterisk
Environment=WHISPER_MODEL=large-v3
Environment=WHISPER_DEVICE=cuda
Environment=WHISPER_COMPUTE=int8_float16
ExecStart=/usr/local/bin/uvicorn server:app --host 127.0.0.1 --port 8765 --workers 1
WorkingDirectory=/opt/whisper-service
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now whisper-service
# Tester
curl -X POST http://127.0.0.1:8765/transcribe \
-F "file=@/tmp/test-recording.wav" \
-F "language=fr"
Appeler le service depuis Asterisk
#!/bin/bash
# /usr/local/bin/whisper-local-transcribe.sh (version service)
RECORDING="$1"
UNIQUEID="$2"
[ ! -s "$RECORDING" ] && exit 1
# Appel au service local (modele deja en memoire = rapide)
RESULT=$(curl -s -X POST http://127.0.0.1:8765/transcribe \
-F "file=@${RECORDING}" -F "language=fr")
# Sauvegarder en JSON Lines
echo "{\"unique_id\":\"${UNIQUEID}\",\"timestamp\":\"$(date -Iseconds)\",\"result\":${RESULT}}" \
>> /var/lib/asterisk/transcriptions.jsonl
5. Docker Compose (tout-en-un)
# docker-compose.yml
services:
whisper:
build:
context: .
dockerfile: Dockerfile.whisper
container_name: whisper-service
restart: unless-stopped
ports:
- "127.0.0.1:8765:8765"
environment:
- WHISPER_MODEL=large-v3
- WHISPER_DEVICE=cuda
- WHISPER_COMPUTE=int8_float16
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
- whisper_cache:/root/.cache/huggingface
volumes:
whisper_cache:
# Dockerfile.whisper
FROM nvidia/cuda:12.2.2-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
RUN pip3 install faster-whisper fastapi uvicorn python-multipart
WORKDIR /app
COPY server.py .
EXPOSE 8765
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8765", "--workers", "1"]
6. Analyse locale avec Ollama (bonus)
Plutot que d'envoyer la transcription a GPT, utilisez Ollama pour l'analyse de sentiment et le resume — toujours 100% local :
# Installer Ollama
curl -fsSL https://ollama.com/install.sh | sh
# Telecharger un modele (Mistral 7B est un bon compromis)
ollama pull mistral
Script d'analyse
#!/usr/bin/env python3
# analyse_transcription.py
import json
import requests
OLLAMA_URL = 'http://localhost:11434/api/generate'
def analyze(transcription_text: str) -> dict:
prompt = (
"Analyse cette transcription d'appel telephonique.\n"
"Retourne un JSON avec : sentiment, score, resume, mots_cles.\n\n"
f"Transcription :\n{transcription_text}\n\nJSON:"
)
resp = requests.post(OLLAMA_URL, json={
'model': 'mistral',
'prompt': prompt,
'stream': False,
'format': 'json',
}, timeout=60)
return json.loads(resp.json()['response'])
7. Performances et benchmarks
Benchmarks sur un appel de 5 minutes (WAV 16kHz mono) :
| Config | Modele | Temps | Ratio |
|---|---|---|---|
| RTX 4060 8 Go | large-v3 int8 | ~12s | 25x temps reel |
| RTX 3060 12 Go | large-v3 int8 | ~15s | 20x temps reel |
| Tesla T4 16 Go | large-v3 int8 | ~18s | 17x temps reel |
| CPU Xeon 8 cores | large-v3 int8 | ~90s | 3x temps reel |
| CPU Xeon 8 cores | medium int8 | ~45s | 6x temps reel |
| CPU Xeon 8 cores | small int8 | ~20s | 15x temps reel |
En mode service (modele en memoire), la latence de chargement (~15s) est eliminee.
8. Supervision du service
# Verifier que le service tourne
systemctl status whisper-service
# Metriques GPU
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv -l 5
# Logs
journalctl -u whisper-service -f
# Test de charge
for i in $(seq 1 10); do
curl -s -X POST http://127.0.0.1:8765/transcribe \
-F "file=@test.wav" -F "language=fr" &
done
wait
Conclusion
Avec faster-whisper, vous obtenez la meme qualite de transcription que l'API OpenAI, mais 100% en local. Vos donnees audio ne quittent jamais vos serveurs — ideal pour la conformite RGPD, le secteur sante, juridique ou les operateurs telecoms.
Combine avec Ollama pour l'analyse, vous disposez d'une stack IA complete et souveraine pour votre telephonie.
Chez Technixis, nous deployons cette stack pour des operateurs et des entreprises : installation GPU, optimisation des modeles, integration Asterisk/FreeSWITCH, supervision. Contactez-nous ou appelez le 0800 012 013.
Liens & sources open source :
- faster-whisper — GitHub
- CTranslate2 — GitHub
- Whisper — GitHub (modele original OpenAI)
- Ollama — Site officiel
- Silero VAD — GitHub
- Asterisk — Site officiel
Laisser un commentaire