Asterisk + Whisper open source : transcription vocale 100% locale sans API cloud
Author

admin

Asterisk + Whisper open source : transcription vocale 100% locale sans API cloud

Guide complet pour raccorder Asterisk a faster-whisper (Whisper open source) pour transcrire les appels en local, sans envoyer de donnees a OpenAI. RGPD natif, GPU/CPU, Ollama pour l'analyse.

Asterisk + Whisper open source : transcription 100% locale

Architecture Asterisk + faster-whisper local

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

Aucun commentaire pour le moment. Soyez le premier !

Laisser un commentaire