Capturer le Microphone avec PyAudio
Mis à jour le 29 juillet 2026
Du fichier au microphone
La leçon précédente a établi une architecture dans laquelle la source audio est un simple générateur asynchrone, interchangeable à volonté. Vous allez en tirer parti immédiatement : en remplaçant le générateur qui lisait un fichier par un générateur qui lit la carte son, votre programme se met à transcrire votre voix en direct, sans que le reste du code bouge d’une ligne.
L’outil de cette leçon est PyAudio, la bibliothèque Python de référence pour l’accès aux périphériques audio. Elle s’appuie sur PortAudio, une couche native qu’il faut parfois installer séparément selon votre système.
Installation de PyAudio
# Linux (Debian/Ubuntu)
sudo apt-get install portaudio19-dev
pip install pyaudio
# macOS
brew install portaudio
pip install pyaudio
# Windows
pip install pyaudio
Le générateur de microphone
Le cœur de la leçon tient dans une trentaine de lignes. La fonction ouvre un flux d’entrée sur le périphérique par défaut, boucle indéfiniment en lisant des blocs de taille fixe, et livre chacun d’eux au moteur de transcription. Le bloc finally n’est pas décoratif : il garantit que le flux est arrêté, fermé et que PyAudio libère le matériel même si le programme est interrompu brutalement, faute de quoi le microphone peut rester bloqué pour les autres applications.
import asyncio
import pyaudio
async def iter_microphone(
sample_rate: int = 16000,
chunk_duration_ms: int = 480
):
"""Capture le microphone et produit des chunks PCM.
Args:
sample_rate: Taux d'échantillonnage (16000 Hz pour Voxtral)
chunk_duration_ms: Durée de chaque chunk en millisecondes
Yields:
bytes: Chunks d'audio PCM 16-bit mono
"""
p = pyaudio.PyAudio()
# Nombre d'échantillons par chunk
chunk_samples = int(sample_rate * chunk_duration_ms / 1000)
# Ouvrir le flux microphone
stream = p.open(
format=pyaudio.paInt16, # PCM 16-bit
channels=1, # Mono
rate=sample_rate, # 16 kHz
input=True, # Mode capture
frames_per_buffer=chunk_samples
)
loop = asyncio.get_running_loop()
try:
print("Microphone actif — parlez...")
while True:
# Lire le chunk de manière non-bloquante
data = await loop.run_in_executor(
None,
stream.read,
chunk_samples,
False # exception_on_overflow
)
yield data
finally:
stream.stop_stream()
stream.close()
p.terminate()
print("Microphone fermé")
Chaque paramètre a une raison d’être précise. sample_rate=16000 traduit une exigence du modèle et non une préférence : Voxtral attend du 16 kHz, ne touchez pas à cette valeur. pyaudio.paInt16 correspond au format PCM 16 bits signé, l’équivalent exact du pcm_s16le déclaré côté API, et channels=1 impose le mono, également requis. Reste chunk_duration_ms=480, le seul réglage réellement discutable : chaque chunk représente 480 millisecondes d’audio, ce qui constitue un bon compromis entre réactivité et efficacité réseau. Un chunk plus court, disons 100 ms, améliore la réactivité mais multiplie le nombre de requêtes réseau ; un chunk plus long économise la bande passante au prix d’un délai supplémentaire avant que le modèle ne voie l’audio.
L’appel loop.run_in_executor mérite lui aussi une explication. stream.read est une fonction bloquante : elle immobilise le thread jusqu’à ce que le nombre d’échantillons demandé soit disponible. Exécutée directement dans une coroutine, elle gèlerait toute la boucle asyncio, y compris la réception des événements de transcription. En la déportant dans un exécuteur, vous rendez la lecture compatible avec le reste du programme asynchrone.
Le calcul de la taille des blocs découle mécaniquement des deux réglages, pour sample_rate=16000 et chunk_duration_ms=480 :
chunk_samples = 16000 × 480 / 1000 = 7680 échantillons
taille_octets = 7680 × 2 octets = 15360 octets par chunk
Programme complet : du micro à la transcription
Le script suivant assemble la capture et la transcription en un outil de dictée utilisable tel quel. La nouveauté par rapport au générateur précédent est la gestion propre de l’arrêt : un gestionnaire de signal intercepte le Ctrl+C, bascule le drapeau en_cours à False, ce qui fait sortir la boucle de capture, ferme le flux et permet au programme d’enregistrer le texte accumulé. Sans ce mécanisme, une interruption au clavier tuerait le processus avant la sauvegarde et vous perdriez la dictée.
import asyncio
import os
import signal
from mistralai import Mistral
from mistralai.models import (
AudioFormat,
TranscriptionStreamTextDelta,
TranscriptionStreamDone
)
import pyaudio
# Flag pour arrêter proprement
en_cours = True
def signal_handler(sig, frame):
global en_cours
print("\n\nArrêt en cours...")
en_cours = False
signal.signal(signal.SIGINT, signal_handler)
async def iter_microphone(sample_rate=16000, chunk_duration_ms=480):
"""Capture le microphone jusqu'à interruption (Ctrl+C)."""
p = pyaudio.PyAudio()
chunk_samples = int(sample_rate * chunk_duration_ms / 1000)
stream = p.open(
format=pyaudio.paInt16,
channels=1,
rate=sample_rate,
input=True,
frames_per_buffer=chunk_samples
)
loop = asyncio.get_running_loop()
try:
while en_cours:
data = await loop.run_in_executor(
None, stream.read, chunk_samples, False
)
yield data
finally:
stream.stop_stream()
stream.close()
p.terminate()
async def main():
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
audio_format = AudioFormat(encoding="pcm_s16le", sample_rate=16000)
print("=" * 50)
print("TRANSCRIPTION EN DIRECT")
print("Parlez dans le microphone (Ctrl+C pour arrêter)")
print("=" * 50)
print()
texte_complet = []
async for event in client.audio.realtime.transcribe_stream(
audio_stream=iter_microphone(),
model="voxtral-mini-transcribe-realtime-2602",
audio_format=audio_format,
):
if isinstance(event, TranscriptionStreamTextDelta):
print(event.text, end="", flush=True)
texte_complet.append(event.text)
elif isinstance(event, TranscriptionStreamDone):
break
# Sauvegarder
texte_final = "".join(texte_complet)
if texte_final.strip():
with open("dictee.txt", "w", encoding="utf-8") as f:
f.write(texte_final)
print(f"\n\nTranscription sauvegardée dans dictee.txt")
print(f"Longueur : {len(texte_final)} caractères")
else:
print("\n\nAucun texte transcrit.")
asyncio.run(main())
Lancez-le, parlez, arrêtez avec Ctrl+C : le texte s’affiche au fil de votre parole puis se retrouve dans dictee.txt.
python dictee_vocale.py
# Parlez dans votre microphone
# Le texte s'affiche en temps réel
# Ctrl+C pour arrêter
Faites l’essai sur une trentaine de secondes de français continu, en variant le débit, et comparez ce que vous avez dit avec ce que le fichier contient. C’est le meilleur moyen de calibrer vos attentes avant d’aller régler la latence dans la leçon suivante.
Choisir le bon périphérique
Sur un poste équipé d’un micro intégré, d’un casque USB et d’une webcam, le périphérique par défaut n’est pas toujours celui que vous croyez, et une transcription vide s’explique le plus souvent par ce détail. Listez d’abord les entrées disponibles avec leur index :
def lister_microphones():
"""Liste tous les périphériques d'entrée audio disponibles."""
p = pyaudio.PyAudio()
print("Périphériques d'entrée disponibles :")
for i in range(p.get_device_count()):
info = p.get_device_info_by_index(i)
if info["maxInputChannels"] > 0:
print(f" [{i}] {info['name']} "
f"(canaux: {info['maxInputChannels']}, "
f"taux: {int(info['defaultSampleRate'])} Hz)")
p.terminate()
lister_microphones()
Il ne reste plus qu’à imposer l’index retenu au moment d’ouvrir le flux, via le paramètre input_device_index :
stream = p.open(
format=pyaudio.paInt16,
channels=1,
rate=sample_rate,
input=True,
input_device_index=2, # Index du périphérique
frames_per_buffer=chunk_samples
)
Points clés à retenir
- PyAudio capture le microphone en PCM 16-bit mono 16 kHz, exactement ce que Voxtral attend
- Le générateur
iter_microphoneproduit des chunks de 480 ms par défaut loop.run_in_executorrend la lecture du microphone non bloquante dans asyncio- Ctrl+C arrête proprement la capture via un signal handler
- Listez les périphériques avec
get_device_count()pour choisir le bon microphone