"""captions.py: live captions on every viewer's page, in Python. It reads a transcript as it is spoken, one line at a time on its input, and publishes each line to a captions channel. It never joins anything: only HTTP pushes, signed with a key that may publish there and nowhere else. your-transcriber | python captions.py Each input line is plain text (a finished caption), or JSON from a speech-to-text stream such as Transcribe Today: {"text": "…", "final": true}. Unfinished text ("final": false) is sent as "partial", at most 4 times a second, so the page can show words as they come. Python 3.10+. pip install -r requirements.txt (reads .env). Its key (dashboard, New API key): right publish, kind public, channels: prefix captions-. """ import json import sys import time from pikoman_bot import Rest, env CHANNEL = env("CHANNEL", "captions-talk-1") PARTIAL_EVERY = 0.25 # seconds: at most 4 partial updates a second rest = Rest() # PIKOMAN_APP and PIKOMAN_API_KEY; PIKOMAN_HTTP only for another server sent = 0 last_partial = 0.0 def publish(event, text): global sent try: rest.publish(CHANNEL, event, {"text": text, "at": int(time.time() * 1000)}) if event == "line": sent += 1 except Exception as e: # noqa: BLE001: the next line goes anyway print(f"publish {event}:", e, file=sys.stderr) def read(line): """One input line: a caption as text, or {"text", "final"} from a speech-to-text stream.""" line = line.strip() if not line: return None if line.startswith("{"): try: data = json.loads(line) text = data.get("text") if isinstance(text, str) and text.strip(): return text.strip()[:500], data.get("final", True) return None except ValueError: pass # not JSON after all: a caption that starts with { return line[:500], True try: # Lines are sent in the order spoken, each as it comes. A partial is sent only when 250 ms have # passed since the last one: between them, newer words replace it. for raw in sys.stdin: caption = read(raw) if caption is None: continue text, final = caption if final: publish("line", text) elif time.monotonic() - last_partial >= PARTIAL_EVERY: last_partial = time.monotonic() publish("partial", text) except KeyboardInterrupt: pass print(f"Done: {sent} lines to {CHANNEL}")