Lokaler Sprachinput für Claude auf Russisch: In einem Abend gebaut
Ein Flask-Server auf localhost:5555 empfängt Audio von einer Chrome-Erweiterung, transkribiert es lokal mit Whisper (small-Modell) und sendet den Text ins Eingabefeld von Claude.ai. Eine Menüleisten-App mit rumps steuert Start und Stopp, das Menü-Symbol wechselt von 🤖 zu 🎤. Alles läuft vollständig offline, ohne Limits, komplett kostenlos. Funktioniert auf macOS, leicht anpassbar für Linux.
Voraussetzungen
Installiere die Abhängigkeiten:
- Whisper:
brew install openai-whisper - ffmpeg:
brew install ffmpeg - Python 3 und Chrome
Teste Whisper:
whisper --version
Suche nach Russisch in der Ausgabe. Richte die Projektstruktur ein:
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
Ein virtuelles Environment ist auf macOS Ventura+ essenziell, um Konflikte mit dem System-Python zu vermeiden.
Transkriptionsserver (server.py)
Der Server verarbeitet POST /transcribe mit base64- oder Datei-Audio, ruft Whisper mit --language ru --fp16 False auf (maßgeblich für Apple Silicon) und liefert JSON mit dem Text zurück. Enthält /ping- und /shutdown-Endpunkte.
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)
Wichtiger Tipp: --fp16 False verhindert Abstürze auf M1/M2/M3-Chips.
Menüleisten-Steuerung (menubar.py)
Die App prüft alle 5 Sekunden die Ports mit rumps.Timer, startet/stoppt Server in Threads. TOOLS-Konfig erlaubt das Hinzufügen von Services mit einer Zeile. Status als Emojis: ✅ läuft, ⏸ pausiert.
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": "Russischer Sprachinput",
},
]
# ... (Rest des Codes wie im Original, gekürzt für Beispiel)
class AILauncher(rumps.App):
# Implementierung wie oben
pass
if __name__ == "__main__":
AILauncher().run()
Vollständiger Code enthält ToolController mit Toggle, check_all und sauberem Shutdown.
Chrome-Erweiterung
manifest.json (MV3):
{
"manifest_version": 3,
"name": "Whisper for 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 behandelt gemischten Content: base64-Audio aus content.js → fetch im Background → Server.
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 zu Blob zu FormData zu POST /transcribe
}
Content.js fügt einen 🎤-Button neben dem Eingabefeld hinzu und erfasst MediaRecorder.
Highlights
- Lokales Whisper small-Modell: ~1 GB, transkribiert Russisch offline in 10-30 Sekunden.
- Erweiterbare Architektur: Neue AI-Tools zu TOOLS hinzufügen ohne Code-Änderungen.
- Sicherheit: Nur localhost, keine Cloud-Anrufe, CORS für Chrome.
- Kompatibilität: macOS Apple Silicon (fp16=False), Linux mit Pfadanpassungen.
- Performance: 60-Sekunden-Timeout, automatisches Tempfile-Cleanup.
— Editorial Team
Noch keine Kommentare.