commit 20cdfe48d3bd89f5a69f309c4dfb437a74651506 Author: brian Date: Wed May 27 13:14:46 2026 -0700 Voice app with floating record/cancel buttons and terminal view diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..638747e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.11-slim +RUN apt-get update && apt-get install -y --no-install-recommends tmux docker.io git curl sudo && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py . +COPY static/ static/ +CMD ["python", "app.py"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..4197e89 --- /dev/null +++ b/app.py @@ -0,0 +1,86 @@ +from flask import Flask, request, jsonify, send_from_directory +import subprocess +import requests +import tempfile +import os + +app = Flask(__name__, static_folder="static") +WHISPER_URL = os.environ.get("WHISPER_URL", "http://192.168.86.11:8950/v1/audio/transcriptions") +WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "small.en") +TMUX_SOCKET = "/tmp/tmux-host/default" +TMUX_SESSION = os.environ.get("TMUX_SESSION", "kiro") + + +def tmux(*args): + return subprocess.run(["tmux", "-S", TMUX_SOCKET] + list(args), capture_output=True, text=True) + + +def get_pane_content(): + r = tmux("capture-pane", "-t", TMUX_SESSION, "-p", "-S", "-50") + return r.stdout + + +@app.route("/") +def index(): + return send_from_directory("static", "index.html") + + +@app.route("/send", methods=["POST"]) +def send_voice(): + audio = request.files.get("audio") + if not audio: + return jsonify({"error": "no audio"}), 400 + + with tempfile.NamedTemporaryFile(suffix=".webm", delete=False) as f: + audio.save(f.name) + tmp = f.name + + try: + resp = requests.post( + WHISPER_URL, + files={"file": (audio.filename or "audio.webm", open(tmp, "rb"), audio.content_type or "audio/webm")}, + data={"model": WHISPER_MODEL}, + ) + finally: + os.unlink(tmp) + + if resp.status_code != 200: + return jsonify({"error": f"whisper {resp.status_code}: {resp.text[:200]}"}), 400 + text = resp.json().get("text", "").strip() + + if not text: + return jsonify({"error": "empty transcription"}), 400 + + r = tmux("has-session", "-t", TMUX_SESSION) + if r.returncode != 0: + return jsonify({"error": f"no host tmux session '{TMUX_SESSION}' — start it on the host first"}), 400 + + tmux("send-keys", "-t", TMUX_SESSION, text, "Enter") + return jsonify({"text": text}) + + +@app.route("/output") +def output(): + return jsonify({"output": get_pane_content()}) + + +@app.route("/cancel", methods=["POST"]) +def send_cancel(): + tmux("send-keys", "-t", TMUX_SESSION, "C-c") + return jsonify({"ok": True}) + + +@app.route("/text", methods=["POST"]) +def send_text(): + text = request.json.get("text", "").strip() + if not text: + return jsonify({"error": "empty"}), 400 + r = tmux("has-session", "-t", TMUX_SESSION) + if r.returncode != 0: + return jsonify({"error": f"no host tmux session '{TMUX_SESSION}'"}), 400 + tmux("send-keys", "-t", TMUX_SESSION, text, "Enter") + return jsonify({"text": text}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8951) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..84ab22b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + voice-kiro: + build: . + restart: unless-stopped + ports: + - "8952:8951" + volumes: + - /mnt/data/home/brian/.local/bin/kiro-cli:/usr/local/bin/kiro-cli:ro + - /mnt/data/home/brian/.local/bin/kiro-cli-chat:/usr/local/bin/kiro-cli-chat:ro + - /mnt/data/home/brian/.local/share/kiro-cli:/root/.local/share/kiro-cli + - /mnt/data/home/brian/.kiro:/root/.kiro + - /mnt/data/home/brian/services:/services + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/tmux-3000:/tmp/tmux-host + environment: + - WHISPER_URL=http://192.168.86.11:8950/v1/audio/transcriptions + - WHISPER_MODEL=small.en diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0c2e19b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask==3.1.1 +requests==2.32.3 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..a3cd4ca --- /dev/null +++ b/static/index.html @@ -0,0 +1,131 @@ + + + + + +Voice → Kiro + + + +
+
+ +
+
+ + + + +