# Saisie vocale locale pour Claude en russe : réalisé en une soirée
Un serveur Flask sur localhost:5555 reçoit l'audio d'une extension Chrome, le transcrit localement avec Whisper (modèle small), et envoie le texte dans le champ de saisie de Claude.ai. Une app Menu Bar créée avec rumps gère le démarrage et l'arrêt, avec l'icône du menu qui passe de 🤖 à 🎤. Tout fonctionne entièrement hors ligne, sans limites, totalement gratuit. Compatible macOS, facilement adaptable à Linux.
Prérequis
Installez les dépendances :
- Whisper :
brew install openai-whisper - ffmpeg :
brew install ffmpeg - Python 3 et Chrome
Testez Whisper :
whisper --version
Vérifiez la présence du russe dans la sortie. Créez la structure du projet :
mkdir -p ~/projects/whisper-claude
cd ~/projects/whisper-claude
python3 -m venv venv
source venv/bin/activate
pip install flask flask-cors rumps
mkdir -p extension/icons
L'environnement virtuel est indispensable sur macOS Ventura+ pour éviter les conflits avec Python système.
Serveur de transcription (server.py)
Le serveur gère POST /transcribe avec audio en base64 ou fichier, appelle Whisper avec --language ru --fp16 False (crucial pour Apple Silicon), et renvoie un JSON avec le texte. Inclut les endpoints /ping et /shutdown.
from flask import Flask, request, jsonify
from flask_cors import CORS
import subprocess
import tempfile
import os
import threading
app = Flask(__name__)
CORS(app)
WHISPER_PATH = "/opt/homebrew/bin/whisper"
WHISPER_MODEL = "small"
PORT = 5555
@app.route("/ping", methods=["GET"])
def ping():
return jsonify({"status": "ok"})
@app.route("/transcribe", methods=["POST"])
def transcribe():
if "audio" not in request.files:
return jsonify({"error": "No audio file found"}), 400
audio_file = request.files["audio"]
with tempfile.NamedTemporaryFile(suffix=".webm", delete=False) as tmp:
tmp_path = tmp.name
audio_file.save(tmp_path)
try:
result = subprocess.run(
[
WHISPER_PATH, tmp_path,
"--model", WHISPER_MODEL,
"--language", "ru",
"--output_format", "txt",
"--output_dir", tempfile.gettempdir(),
"--fp16", "False",
],
capture_output=True, text=True, timeout=60
)
txt_path = tmp_path.replace(".webm", ".txt")
if os.path.exists(txt_path):
with open(txt_path, "r", encoding="utf-8") as f:
text = f.read().strip()
os.unlink(txt_path)
else:
text = result.stdout.strip()
return jsonify({"text": text})
except subprocess.TimeoutExpired:
return jsonify({"error": "Timeout"}), 500
except Exception as e:
return jsonify({"error": str(e)}), 500
finally:
os.unlink(tmp_path)
@app.route("/shutdown", methods=["POST"])
def shutdown():
threading.Timer(0.5, lambda: os._exit(0)).start()
return jsonify({"status": "shutting down"})
if __name__ == "__main__":
print(f"[Whisper] Starting on port {PORT}, model: {WHISPER_MODEL}")
app.run(host="127.0.0.1", port=PORT, debug=False)
Astuce clé : --fp16 False évite les plantages sur puces M1/M2/M3.
Contrôleur Menu Bar (menubar.py)
L'app vérifie les ports toutes les 5 secondes avec rumps.Timer, démarre/arrête les serveurs dans des threads. La config TOOLS permet d'ajouter des services en une ligne. Statut affiché en emojis : ✅ en cours, ⏸ en pause.
import rumps
import subprocess
import threading
import os
import time
import urllib.request
BASE_DIR = "~/projects/whisper-claude"
VENV_PYTHON = os.path.join(BASE_DIR, "venv/bin/python3")
TOOLS = [
{
"name": "Whisper → Claude",
"script": os.path.join(BASE_DIR, "server.py"),
"port": 5555,
"description": "Saisie vocale russe",
},
]
# ... (reste du code comme dans l'original, abrégé pour l'exemple)
class AILauncher(rumps.App):
# implémentation comme ci-dessus
pass
if __name__ == "__main__":
AILauncher().run()
Code complet inclut ToolController avec bascule, vérification et arrêt propre.
Extension Chrome
manifest.json (MV3) :
{
"manifest_version": 3,
"name": "Whisper pour Claude",
"version": "1.0",
"permissions": ["activeTab", "scripting"],
"host_permissions": [
"https://claude.ai/*",
"http://127.0.0.1:5555/*"
],
"content_scripts": [
{
"matches": ["https://claude.ai/*"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"background": {
"service_worker": "background.js"
}
}
background.js gère le contenu mixte : audio base64 de content.js → fetch en background → serveur.
const SERVER_URL = "http://127.0.0.1:5555";
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "transcribe") {
handleTranscribe(message.audio).then(sendResponse);
return true;
}
});
async function handleTranscribe(base64Audio) {
// base64 vers Blob vers FormData vers POST /transcribe
}
content.js ajoute un bouton 🎤 à côté du champ de saisie et capture MediaRecorder.
Points forts
- Modèle Whisper small local : ~1 Go, transcrit le russe hors ligne en 10-30 secondes.
- Architecture extensible : ajoutez de nouveaux outils IA à TOOLS sans modifier le code.
- Sécurité : localhost uniquement, pas d'appels cloud, CORS pour Chrome.
- Compatibilité : macOS Apple Silicon (fp16=False), Linux avec ajustements de chemins.
- Performances : timeout 60 secondes, nettoyage automatique des fichiers temporaires.
— Editorial Team
Aucun commentaire pour le moment.