tapscribe 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. tapscribe/__init__.py +28 -0
  2. tapscribe/__main__.py +319 -0
  3. tapscribe/app.py +2197 -0
  4. tapscribe/audio.py +228 -0
  5. tapscribe/auth.py +148 -0
  6. tapscribe/batch_pipeline.py +199 -0
  7. tapscribe/batch_strip.py +246 -0
  8. tapscribe/batch_summarize.py +164 -0
  9. tapscribe/batch_transcribe.py +515 -0
  10. tapscribe/bridges_catalog.py +37 -0
  11. tapscribe/chunking.py +76 -0
  12. tapscribe/config.py +256 -0
  13. tapscribe/config_store.py +451 -0
  14. tapscribe/cuda_torch.py +156 -0
  15. tapscribe/hallucinations.py +207 -0
  16. tapscribe/install_picker.py +1307 -0
  17. tapscribe/install_target.py +114 -0
  18. tapscribe/language_select.py +155 -0
  19. tapscribe/live.py +1043 -0
  20. tapscribe/live_control.py +209 -0
  21. tapscribe/live_relay.py +392 -0
  22. tapscribe/logging_setup.py +96 -0
  23. tapscribe/moonshine_live.py +603 -0
  24. tapscribe/name_resolution.py +238 -0
  25. tapscribe/nb_whisper.py +193 -0
  26. tapscribe/people.py +199 -0
  27. tapscribe/preflight.py +225 -0
  28. tapscribe/recorder.py +762 -0
  29. tapscribe/roster.py +120 -0
  30. tapscribe/runtime_probe.py +194 -0
  31. tapscribe/session_maintenance.py +528 -0
  32. tapscribe/session_merge.py +432 -0
  33. tapscribe/session_paths.py +199 -0
  34. tapscribe/sessions.py +908 -0
  35. tapscribe/setup_install.py +226 -0
  36. tapscribe/setup_state.py +144 -0
  37. tapscribe/speech_gate.py +336 -0
  38. tapscribe/strip_silence.py +255 -0
  39. tapscribe/summarizers/__init__.py +103 -0
  40. tapscribe/summarizers/api.py +147 -0
  41. tapscribe/summarizers/base.py +177 -0
  42. tapscribe/summarizers/catalog.py +413 -0
  43. tapscribe/summarizers/command.py +181 -0
  44. tapscribe/summarizers/local.py +234 -0
  45. tapscribe/tap_fan_out.py +498 -0
  46. tapscribe/tap_relay.py +338 -0
  47. tapscribe/text.py +199 -0
  48. tapscribe/tls.py +123 -0
  49. tapscribe/transcribers/__init__.py +448 -0
  50. tapscribe/transcribers/_chunked.py +164 -0
  51. tapscribe/transcribers/_moonshine_window.py +327 -0
  52. tapscribe/transcribers/_parakeet_tdt.py +103 -0
  53. tapscribe/transcribers/_voxtral_common.py +208 -0
  54. tapscribe/transcribers/base.py +292 -0
  55. tapscribe/transcribers/catalog.py +862 -0
  56. tapscribe/transcribers/faster_whisper.py +177 -0
  57. tapscribe/transcribers/mlx_parakeet.py +217 -0
  58. tapscribe/transcribers/mlx_voxtral.py +114 -0
  59. tapscribe/transcribers/mlx_whisper.py +173 -0
  60. tapscribe/transcribers/moonshine_mlx.py +68 -0
  61. tapscribe/transcribers/moonshine_onnx.py +80 -0
  62. tapscribe/transcribers/parakeet.py +172 -0
  63. tapscribe/transcribers/voxtral.py +137 -0
  64. tapscribe/wav_append.py +114 -0
  65. tapscribe/wav_cache.py +599 -0
  66. tapscribe/wav_predecode.py +62 -0
  67. tapscribe/web/components/active-taps.html +64 -0
  68. tapscribe/web/components/config-card.html +35 -0
  69. tapscribe/web/components/live-channel.html +108 -0
  70. tapscribe/web/components/live-feed.html +36 -0
  71. tapscribe/web/components/merged-transcript.html +73 -0
  72. tapscribe/web/components/next/people.html +49 -0
  73. tapscribe/web/components/next/recordings.html +185 -0
  74. tapscribe/web/components/next/sessions.html +138 -0
  75. tapscribe/web/components/next/spine.html +60 -0
  76. tapscribe/web/components/next/summary.html +139 -0
  77. tapscribe/web/components/next/taps.html +103 -0
  78. tapscribe/web/components/next/views.html +475 -0
  79. tapscribe/web/dashboard.css +705 -0
  80. tapscribe/web/js/api.js +480 -0
  81. tapscribe/web/js/api.test.js +261 -0
  82. tapscribe/web/js/components/active-taps.js +232 -0
  83. tapscribe/web/js/components/active-taps.test.js +55 -0
  84. tapscribe/web/js/components/config-card.js +178 -0
  85. tapscribe/web/js/components/live-channel.js +322 -0
  86. tapscribe/web/js/components/live-feed.js +274 -0
  87. tapscribe/web/js/components/live-feed.test.js +167 -0
  88. tapscribe/web/js/components/merged-transcript.js +229 -0
  89. tapscribe/web/js/formatters.js +82 -0
  90. tapscribe/web/js/lib/chrome.js +75 -0
  91. tapscribe/web/js/lib/format.js +141 -0
  92. tapscribe/web/js/lib/render.js +268 -0
  93. tapscribe/web/js/lib/templates.js +198 -0
  94. tapscribe/web/js/model-select.js +121 -0
  95. tapscribe/web/js/model-select.test.js +77 -0
  96. tapscribe/web/js/next/components/engine.js +102 -0
  97. tapscribe/web/js/next/components/language-picker.js +38 -0
  98. tapscribe/web/js/next/components/spine.js +381 -0
  99. tapscribe/web/js/next/components/spine.test.js +70 -0
  100. tapscribe/web/js/next/components/summarizer-controls.js +259 -0
  101. tapscribe/web/js/next/components/waveform.js +251 -0
  102. tapscribe/web/js/next/components/waveform.test.js +28 -0
  103. tapscribe/web/js/next/main.js +713 -0
  104. tapscribe/web/js/next/poll-pacer.js +46 -0
  105. tapscribe/web/js/next/poll-pacer.test.js +78 -0
  106. tapscribe/web/js/next/shell.js +326 -0
  107. tapscribe/web/js/next/shell.test.js +100 -0
  108. tapscribe/web/js/next/subtitles.js +50 -0
  109. tapscribe/web/js/next/subtitles.test.js +54 -0
  110. tapscribe/web/js/next/ui.js +77 -0
  111. tapscribe/web/js/next/views/capture.js +216 -0
  112. tapscribe/web/js/next/views/people.js +245 -0
  113. tapscribe/web/js/next/views/recordings.js +892 -0
  114. tapscribe/web/js/next/views/sessions.js +727 -0
  115. tapscribe/web/js/next/views/settings.js +402 -0
  116. tapscribe/web/js/next/views/summary.js +490 -0
  117. tapscribe/web/js/next/views/taps.js +96 -0
  118. tapscribe/web/js/next/views/transcript.js +798 -0
  119. tapscribe/web/js/next/views/transcript.test.js +76 -0
  120. tapscribe/web/js/setup/setup.css +164 -0
  121. tapscribe/web/js/setup/setup.js +253 -0
  122. tapscribe/web/js/speakers.js +26 -0
  123. tapscribe/web/js/templates.js +288 -0
  124. tapscribe/web/js/types.d.ts +637 -0
  125. tapscribe/web/js/vc/components/alert/alert.css +32 -0
  126. tapscribe/web/js/vc/components/alert/alert.element.js +14 -0
  127. tapscribe/web/js/vc/components/alert/alert.html +10 -0
  128. tapscribe/web/js/vc/components/alert/alert.js +61 -0
  129. tapscribe/web/js/vc/components/button/button.css +57 -0
  130. tapscribe/web/js/vc/components/button/button.element.js +17 -0
  131. tapscribe/web/js/vc/components/button/button.html +7 -0
  132. tapscribe/web/js/vc/components/button/button.js +81 -0
  133. tapscribe/web/js/vc/components/chip/chip.css +44 -0
  134. tapscribe/web/js/vc/components/chip/chip.element.js +15 -0
  135. tapscribe/web/js/vc/components/chip/chip.html +7 -0
  136. tapscribe/web/js/vc/components/chip/chip.js +27 -0
  137. tapscribe/web/js/vc/components/empty-state/empty-state.css +19 -0
  138. tapscribe/web/js/vc/components/empty-state/empty-state.element.js +10 -0
  139. tapscribe/web/js/vc/components/empty-state/empty-state.html +8 -0
  140. tapscribe/web/js/vc/components/empty-state/empty-state.js +28 -0
  141. tapscribe/web/js/vc/components/field/field.css +41 -0
  142. tapscribe/web/js/vc/components/field/field.html +8 -0
  143. tapscribe/web/js/vc/components/field/field.js +68 -0
  144. tapscribe/web/js/vc/components/panel/panel.css +47 -0
  145. tapscribe/web/js/vc/components/panel/panel.html +7 -0
  146. tapscribe/web/js/vc/components/panel/panel.js +58 -0
  147. tapscribe/web/js/vc/components/progress/progress.css +26 -0
  148. tapscribe/web/js/vc/components/progress/progress.element.js +12 -0
  149. tapscribe/web/js/vc/components/progress/progress.html +7 -0
  150. tapscribe/web/js/vc/components/progress/progress.js +32 -0
  151. tapscribe/web/js/vc/components/spinner/spinner.css +28 -0
  152. tapscribe/web/js/vc/components/spinner/spinner.element.js +11 -0
  153. tapscribe/web/js/vc/components/spinner/spinner.html +7 -0
  154. tapscribe/web/js/vc/components/spinner/spinner.js +27 -0
  155. tapscribe/web/js/vc/lib/component.js +42 -0
  156. tapscribe/web/js/vc/lib/element.js +113 -0
  157. tapscribe/web/js/vc/lib/templates.js +198 -0
  158. tapscribe/web/js/vc/lib/tone.js +48 -0
  159. tapscribe/web/next.css +1322 -0
  160. tapscribe/web/next.html +52 -0
  161. tapscribe/web/setup.html +29 -0
  162. tapscribe/web/tokens.css +55 -0
  163. tapscribe/web/tones.css +21 -0
  164. tapscribe-1.1.0.dist-info/METADATA +399 -0
  165. tapscribe-1.1.0.dist-info/RECORD +169 -0
  166. tapscribe-1.1.0.dist-info/WHEEL +5 -0
  167. tapscribe-1.1.0.dist-info/entry_points.txt +2 -0
  168. tapscribe-1.1.0.dist-info/licenses/LICENSE +21 -0
  169. tapscribe-1.1.0.dist-info/top_level.txt +1 -0
tapscribe/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """TapScribe — a local-first transcription recorder + dashboard.
2
+
3
+ TapScribe captures one WAV per utterance per speaker over a WebSocket,
4
+ runs Whisper batch transcription on demand, and manages a long-running
5
+ WhisperLiveKit child process for live captioning. A single FastAPI app
6
+ exposes both a REST API and an operator dashboard at /.
7
+
8
+ The package is split into focused modules:
9
+
10
+ config Module-level constants, paths, env, and feature detection.
11
+ text Pure string/text helpers (prompt+hotwords reading, slug parsing).
12
+ hallucinations Parser + matcher for the hallucination filter file.
13
+ audio WAV I/O and PCM helpers.
14
+ transcribers Stateful Transcriber adapters + load_transcriber factory.
15
+ nb_whisper NB-Whisper CT2 weight download (used by FW batch + live).
16
+ wav_cache Per-WAV transcript sidecar read/write.
17
+ sessions Recording-session bookkeeping (folder layout, meta, strip-silence).
18
+ session_merge Pure selection + merge of per-WAV results into a session.
19
+ live WhisperLiveKit child-process management.
20
+ live_relay Recorder-side WebSocket client to the live child.
21
+ tap_fan_out Per-/tap-WS lifecycle: WAV write + WlK relay fan-out.
22
+ recorder The Recorder context object + composed sub-components.
23
+ auth Basic-auth middleware + /tap subprotocol gate.
24
+ app The FastAPI app object and its routes.
25
+ """
26
+
27
+ __all__ = ["__version__"]
28
+ __version__ = "1.1.0"
tapscribe/__main__.py ADDED
@@ -0,0 +1,319 @@
1
+ """Entry point: `python -m tapscribe [options]`.
2
+
3
+ Parses CLI flags, constructs the Recorder, attaches it to
4
+ `app.state.recorder`, then hands off to uvicorn. When
5
+ `config.AUTO_START_LIVE` is set (opt-in via `--auto-live`; off by
6
+ default), the FastAPI app's lifespan auto-starts the WhisperLiveKit
7
+ child (and stops it again on shutdown) via the Recorder's LiveChannel.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+
14
+ import uvicorn
15
+
16
+ from . import config, install_target
17
+ from .app import app
18
+ from .live import LiveConfig
19
+ from .recorder import Recorder
20
+
21
+
22
+ def build_parser() -> argparse.ArgumentParser:
23
+ """Construct the `python -m tapscribe` argument parser.
24
+
25
+ Extracted from `main()` as a seam so a fast in-process test can pin
26
+ the actual flag-string wiring (e.g. `--auto-live` / the retained
27
+ deprecated `--no-auto-live`) without booting uvicorn.
28
+ """
29
+ p = argparse.ArgumentParser(
30
+ prog="python -m tapscribe",
31
+ description="TapScribe — local-first transcription recorder + dashboard.",
32
+ )
33
+ p.add_argument("--host", default="localhost", help="Bind address. Use 0.0.0.0 to expose on LAN.")
34
+ p.add_argument("--port", type=int, default=8001)
35
+ p.add_argument(
36
+ "--live-model",
37
+ default="tiny.en",
38
+ help="WhisperLiveKit model name (tiny.en, small.en, large-v3, ...). Changeable from the dashboard.",
39
+ )
40
+ p.add_argument("--live-language", default="en", help="WhisperLiveKit language hint (en, no, auto, ...)")
41
+ p.add_argument(
42
+ "--live-host",
43
+ default=None,
44
+ help="Bind host for the live channel. Defaults to 127.0.0.1 — WhisperLiveKit "
45
+ "is internal (only the recorder's live_relay talks to it), so exposing it "
46
+ "on the LAN serves no purpose and just widens the attack surface.",
47
+ )
48
+ p.add_argument(
49
+ "--live-port",
50
+ type=int,
51
+ default=0,
52
+ help="Bind port for the live channel. 0 (default) = pick a free ephemeral "
53
+ "port at spawn time. WhisperLiveKit is internal — only the recorder talks "
54
+ "to it — so a stable well-known port is rarely useful, and a fixed 8000 "
55
+ "is the most common cause of `EADDRINUSE` after a hard-killed prior run.",
56
+ )
57
+ # WhisperLiveKit streaming knobs. Each one trades latency for accuracy;
58
+ # operators tune them against the per-tap lag reading in the live-channel
59
+ # dashboard panel. Unset = use WLK's own default.
60
+ p.add_argument(
61
+ "--live-min-chunk-size",
62
+ type=float,
63
+ default=None,
64
+ help="Minimum audio chunk size in seconds the live channel decodes at "
65
+ "a time. Higher = more future context per decode = better accuracy + "
66
+ "more lag. Unset = use WhisperLiveKit's default.",
67
+ )
68
+ p.add_argument(
69
+ "--live-buffer-trimming",
70
+ choices=("sentence", "segment"),
71
+ default=None,
72
+ help="When the live channel resets its rolling buffer: on sentence "
73
+ "boundaries (more conservative, better accuracy) or segment "
74
+ "boundaries. Unset = use WhisperLiveKit's default.",
75
+ )
76
+ p.add_argument(
77
+ "--live-buffer-trimming-sec",
78
+ type=float,
79
+ default=None,
80
+ help="Buffer length (s) above which trimming is triggered. Pairs with "
81
+ "--live-buffer-trimming. Unset = use WhisperLiveKit's default.",
82
+ )
83
+ p.add_argument(
84
+ "--live-max-context-tokens",
85
+ type=int,
86
+ default=None,
87
+ help="Max prior-text tokens the live decoder can condition on. Higher "
88
+ "= better continuations + more compute per tick. Unset = use "
89
+ "WhisperLiveKit's default.",
90
+ )
91
+ p.add_argument(
92
+ "--live-gate-min-speech-ms",
93
+ type=int,
94
+ default=None,
95
+ help="Minimum confirmed-speech window (ms) before the TapScribe speech "
96
+ "gate releases audio to the live backend. 0 (default) opens on the "
97
+ "first VAD 'start'; higher values suppress brief noise blips (key "
98
+ "taps, single coughs). Tunable from the dashboard at runtime.",
99
+ )
100
+ p.add_argument(
101
+ "--no-mlx",
102
+ action="store_true",
103
+ help="Disable MLX for BOTH live and batch even on Apple Silicon (back-compat alias for --backend=cpu).",
104
+ )
105
+ p.add_argument(
106
+ "--backend",
107
+ choices=("auto", "mlx", "cuda", "cpu"),
108
+ default="auto",
109
+ help="Default backend preference. `auto` picks MLX on Apple Silicon, CUDA on NVIDIA, "
110
+ "CPU otherwise. The dashboard's backend chip can override per-job.",
111
+ )
112
+ p.add_argument(
113
+ "--auto-live",
114
+ action="store_true",
115
+ help="Auto-start the live channel on boot (default: off).",
116
+ )
117
+ p.add_argument(
118
+ "--no-auto-live",
119
+ action="store_true",
120
+ help="[deprecated — off is the default] Accepted for backwards-compat; no-op.",
121
+ )
122
+ p.add_argument(
123
+ "--no-auth",
124
+ action="store_true",
125
+ help="Disable HTTP Basic auth on the dashboard AND the /tap WebSocket token gate. "
126
+ "Only safe on a trusted single-user localhost.",
127
+ )
128
+ p.add_argument(
129
+ "--rotate-password",
130
+ action="store_true",
131
+ help="Delete the persisted password and generate a new one. Invalidates browser sessions.",
132
+ )
133
+ p.add_argument(
134
+ "--rotate-tap-token",
135
+ action="store_true",
136
+ help="Delete the persisted /tap bearer token and generate a new one. "
137
+ "Bridges with the old token will be refused at the WS upgrade.",
138
+ )
139
+ p.add_argument(
140
+ "--tls",
141
+ action="store_true",
142
+ help="Serve the dashboard + /tap over TLS (https:// and wss://). If --cert/--key "
143
+ "are not supplied, a self-signed pair is generated next to .auth-password.",
144
+ )
145
+ p.add_argument(
146
+ "--cert",
147
+ default=None,
148
+ help="Path to a PEM certificate. Implies --tls. Defaults to .tapscribe-cert.pem.",
149
+ )
150
+ p.add_argument(
151
+ "--key", default=None, help="Path to the PEM private key for --cert. Defaults to .tapscribe-key.pem."
152
+ )
153
+ p.add_argument(
154
+ "--log-json",
155
+ action="store_true",
156
+ help="Emit one JSON line per log record instead of uvicorn's plaintext format. "
157
+ "Useful when piping into journalctl -o json / vector / fluent-bit.",
158
+ )
159
+ p.add_argument(
160
+ "--install-spec",
161
+ default=None,
162
+ help="What pip installs TapScribe from when /setup installs model "
163
+ "backends: omitted (a dev checkout, the default), a path to the "
164
+ "Windows Bundle's shipped .whl, or a pinned 'tapscribe==X.Y.Z'. "
165
+ "The Bundle's Launcher passes its wheel. See ADR-0015.",
166
+ )
167
+ return p
168
+
169
+
170
+ def main() -> None:
171
+ parser = build_parser()
172
+ args = parser.parse_args()
173
+
174
+ # Validate before anything boots. This value is a CLI string that ends up in
175
+ # a pip argv (via `/setup` → `picker_install_argv`), which CodeQL treats as
176
+ # external input regardless of who launched the process (CLAUDE.md). Failing
177
+ # here beats discovering a Bundle's wheel is missing on the operator's first
178
+ # model install. `parser.error` so a bad value exits 2 on stderr like every
179
+ # other malformed flag, rather than inventing a second failure convention.
180
+ try:
181
+ install_target.resolve_install_spec(args.install_spec)
182
+ except install_target.InstallSpecError as exc:
183
+ parser.error(str(exc))
184
+
185
+ # Boot-time constants that affect every transcribe-call route the
186
+ # Recorder hands out. `backend` is the per-Recorder preference;
187
+ # `AUTH_ENABLED` and `AUTO_START_LIVE` are still module-level since
188
+ # they're security/boot toggles that don't change at runtime.
189
+ # --no-mlx is the legacy alias: it folds into --backend by forcing
190
+ # `cpu` over whatever `auto` would have chosen on Apple Silicon.
191
+ backend_pref = args.backend
192
+ if args.no_mlx and backend_pref == "auto":
193
+ backend_pref = "cpu"
194
+ config.AUTH_ENABLED = not args.no_auth
195
+ config.AUTO_START_LIVE = args.auto_live
196
+
197
+ live_config_kwargs: dict[str, object] = dict(
198
+ model=args.live_model,
199
+ language=args.live_language,
200
+ host=args.live_host or "127.0.0.1",
201
+ port=args.live_port,
202
+ min_chunk_size=args.live_min_chunk_size,
203
+ buffer_trimming=args.live_buffer_trimming,
204
+ buffer_trimming_sec=args.live_buffer_trimming_sec,
205
+ max_context_tokens=args.live_max_context_tokens,
206
+ )
207
+ if args.live_gate_min_speech_ms is not None:
208
+ live_config_kwargs["gate_min_speech_ms"] = args.live_gate_min_speech_ms
209
+ live_config = LiveConfig(**live_config_kwargs) # type: ignore[arg-type]
210
+
211
+ recorder = Recorder(
212
+ recordings_dir=config.RECORDINGS_DIR,
213
+ config_dir=config.CONFIG_DIR,
214
+ live_config=live_config,
215
+ backend=backend_pref,
216
+ auth_password_file=config.AUTH_PASSWORD_FILE,
217
+ tap_token_file=config.TAP_TOKEN_FILE,
218
+ )
219
+
220
+ if args.rotate_password:
221
+ recorder.auth.rotate()
222
+ if args.rotate_tap_token:
223
+ recorder.tap.rotate()
224
+
225
+ app.state.recorder = recorder
226
+ # Handed to `picker_install_argv` when /setup installs model backends, so a
227
+ # Bundle installs from the wheel it shipped rather than an absent checkout.
228
+ app.state.install_spec = args.install_spec
229
+ app.state.log_json = bool(args.log_json)
230
+
231
+ if args.host == "0.0.0.0":
232
+ print(
233
+ "[tapscribe] WARNING: binding to 0.0.0.0 exposes the recorder to "
234
+ "the LAN. Make sure you trust your network.",
235
+ flush=True,
236
+ )
237
+
238
+ from .runtime_probe import available_backends, resolve_backend_preference
239
+
240
+ avail = sorted(available_backends())
241
+ try:
242
+ resolved = resolve_backend_preference(backend_pref)
243
+ except RuntimeError as e:
244
+ # Operator picked an unavailable kind explicitly — surface the
245
+ # error before uvicorn starts so they see it instead of catching
246
+ # it on the first transcribe call.
247
+ print(f"[tapscribe] FATAL: {e}", flush=True)
248
+ raise SystemExit(2) from e
249
+
250
+ print(
251
+ f"[tapscribe] backend preference={backend_pref} (resolves to {resolved} on this machine). "
252
+ f"Available backends: {avail}. Auto-start live: {config.AUTO_START_LIVE}.",
253
+ flush=True,
254
+ )
255
+
256
+ # Auth banner.
257
+ if config.AUTH_ENABLED:
258
+ bar = "=" * 64
259
+ print(bar, flush=True)
260
+ print("[tapscribe] Dashboard auth: HTTP Basic", flush=True)
261
+ print(f"[tapscribe] user: {config.AUTH_USER}", flush=True)
262
+ print(f"[tapscribe] password: {recorder.auth.value}", flush=True)
263
+ print(f"[tapscribe] stored in: {config.AUTH_PASSWORD_FILE}", flush=True)
264
+ print("[tapscribe] (persists across restarts — your browser stays logged in.", flush=True)
265
+ print("[tapscribe] Rotate via --rotate-password or delete the file.)", flush=True)
266
+ print("[tapscribe] /tap WebSocket auth: bearer token (Sec-WebSocket-Protocol)", flush=True)
267
+ print(f"[tapscribe] tap token: {recorder.tap.value}", flush=True)
268
+ print(f"[tapscribe] stored in: {config.TAP_TOKEN_FILE}", flush=True)
269
+ print("[tapscribe] (paste into the bridge popup. Rotate via --rotate-tap-token.)", flush=True)
270
+ print(bar, flush=True)
271
+ else:
272
+ print("[tapscribe] WARNING: --no-auth — dashboard AND /tap are UNAUTHENTICATED.", flush=True)
273
+ if args.host == "0.0.0.0":
274
+ print("[tapscribe] WARNING: combined with LAN binding, anyone on the", flush=True)
275
+ print("[tapscribe] network can view/delete recordings. Re-enable auth.", flush=True)
276
+
277
+ # TLS wiring — opt-in via --tls (or implied by --cert/--key). When no
278
+ # cert path is given, generate a self-signed pair next to the secret
279
+ # files; reuse it across restarts so browsers only prompt once.
280
+ ssl_certfile: str | None = None
281
+ ssl_keyfile: str | None = None
282
+ use_tls = args.tls or bool(args.cert or args.key)
283
+ if use_tls:
284
+ from pathlib import Path as _Path
285
+
286
+ from .tls import ensure_self_signed_cert
287
+
288
+ cert_path = _Path(args.cert) if args.cert else config.TLS_CERT_FILE
289
+ key_path = _Path(args.key) if args.key else config.TLS_KEY_FILE
290
+ if args.cert or args.key:
291
+ if not (cert_path.is_file() and key_path.is_file()):
292
+ print(
293
+ f"[tapscribe] ERROR: --cert/--key set but {cert_path} or {key_path} missing.", flush=True
294
+ )
295
+ raise SystemExit(2)
296
+ pair = type("Pair", (), {"cert_file": cert_path, "key_file": key_path})()
297
+ else:
298
+ pair = ensure_self_signed_cert(cert_path, key_path, host=args.host)
299
+ print(f"[tapscribe] TLS: using self-signed cert at {pair.cert_file}", flush=True)
300
+ print(
301
+ "[tapscribe] (first browser visit will show a 'not secure' prompt — accept once.)",
302
+ flush=True,
303
+ )
304
+ ssl_certfile = str(pair.cert_file)
305
+ ssl_keyfile = str(pair.key_file)
306
+ print(f"[tapscribe] TLS enabled: https://{args.host}:{args.port}/", flush=True)
307
+
308
+ uvicorn.run(
309
+ app,
310
+ host=args.host,
311
+ port=args.port,
312
+ log_level="info",
313
+ ssl_certfile=ssl_certfile,
314
+ ssl_keyfile=ssl_keyfile,
315
+ )
316
+
317
+
318
+ if __name__ == "__main__":
319
+ main()