Back to Home

Voice Input Claude Russian Offline

Instructions for creating a local voice input system for Claude.ai in Russian with Whisper, Flask server and Chrome extension. Menu Bar app manages services, everything works offline without limits. Full code and solutions to common errors.

Offline Voice in Claude: Whisper + rumps + Chrome
Advertisement 728x90

Local Voice Input for Claude in Russian: Built in an Evening

A Flask server on localhost:5555 receives audio from a Chrome extension, transcribes it locally with Whisper (small model), and sends the text to the Claude.ai input field. A Menu Bar app built with rumps handles starting and stopping, with the menu icon switching from 🤖 to 🎤. Everything runs fully offline, no limits, completely free. Works on macOS, easily adaptable to Linux.

Prerequisites

Install dependencies:

  • Whisper: brew install openai-whisper
  • ffmpeg: brew install ffmpeg
  • Python 3 and Chrome

Test Whisper:

Google AdInline article slot
whisper --version

Look for Russian in the output. Set up the project structure:

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

Virtual environment is essential on macOS Ventura+ to avoid conflicts with system Python.

Transcription Server (server.py)

The server handles POST /transcribe with base64 or file audio, calls Whisper with --language ru --fp16 False (critical for Apple Silicon), and returns JSON with the text. Includes /ping and /shutdown endpoints.

Google AdInline article slot
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)

Key tip: --fp16 False prevents crashes on M1/M2/M3 chips.

Menu Bar Controller (menubar.py)

The app checks ports every 5 seconds with rumps.Timer, starts/stops servers in threads. TOOLS config lets you add services with one line. Status shows as emojis: ✅ running, ⏸ paused.

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": "Russian voice input",
    },
]

# ... (rest of code as in original, abbreviated for example)

class AILauncher(rumps.App):
    # implementation as above
    pass

if __name__ == "__main__":
    AILauncher().run()

Full code includes ToolController with toggle, check_all, and graceful shutdown.

Google AdInline article slot

Chrome Extension

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 handles mixed content: base64 audio from content.js → fetch in 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 to Blob to FormData to POST /transcribe
}

Content.js adds a 🎤 button next to the input field and captures MediaRecorder.

Key Highlights

  • Local Whisper small model: ~1 GB, transcribes Russian offline in 10-30 seconds.
  • Extensible architecture: add new AI tools to TOOLS without code changes.
  • Security: localhost-only, no cloud calls, CORS for Chrome.
  • Compatibility: macOS Apple Silicon (fp16=False), Linux with path tweaks.
  • Performance: 60-second timeout, automatic temp file cleanup.

— Editorial Team

Advertisement 728x90

Read Next