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.
- tapscribe/__init__.py +28 -0
- tapscribe/__main__.py +319 -0
- tapscribe/app.py +2197 -0
- tapscribe/audio.py +228 -0
- tapscribe/auth.py +148 -0
- tapscribe/batch_pipeline.py +199 -0
- tapscribe/batch_strip.py +246 -0
- tapscribe/batch_summarize.py +164 -0
- tapscribe/batch_transcribe.py +515 -0
- tapscribe/bridges_catalog.py +37 -0
- tapscribe/chunking.py +76 -0
- tapscribe/config.py +256 -0
- tapscribe/config_store.py +451 -0
- tapscribe/cuda_torch.py +156 -0
- tapscribe/hallucinations.py +207 -0
- tapscribe/install_picker.py +1307 -0
- tapscribe/install_target.py +114 -0
- tapscribe/language_select.py +155 -0
- tapscribe/live.py +1043 -0
- tapscribe/live_control.py +209 -0
- tapscribe/live_relay.py +392 -0
- tapscribe/logging_setup.py +96 -0
- tapscribe/moonshine_live.py +603 -0
- tapscribe/name_resolution.py +238 -0
- tapscribe/nb_whisper.py +193 -0
- tapscribe/people.py +199 -0
- tapscribe/preflight.py +225 -0
- tapscribe/recorder.py +762 -0
- tapscribe/roster.py +120 -0
- tapscribe/runtime_probe.py +194 -0
- tapscribe/session_maintenance.py +528 -0
- tapscribe/session_merge.py +432 -0
- tapscribe/session_paths.py +199 -0
- tapscribe/sessions.py +908 -0
- tapscribe/setup_install.py +226 -0
- tapscribe/setup_state.py +144 -0
- tapscribe/speech_gate.py +336 -0
- tapscribe/strip_silence.py +255 -0
- tapscribe/summarizers/__init__.py +103 -0
- tapscribe/summarizers/api.py +147 -0
- tapscribe/summarizers/base.py +177 -0
- tapscribe/summarizers/catalog.py +413 -0
- tapscribe/summarizers/command.py +181 -0
- tapscribe/summarizers/local.py +234 -0
- tapscribe/tap_fan_out.py +498 -0
- tapscribe/tap_relay.py +338 -0
- tapscribe/text.py +199 -0
- tapscribe/tls.py +123 -0
- tapscribe/transcribers/__init__.py +448 -0
- tapscribe/transcribers/_chunked.py +164 -0
- tapscribe/transcribers/_moonshine_window.py +327 -0
- tapscribe/transcribers/_parakeet_tdt.py +103 -0
- tapscribe/transcribers/_voxtral_common.py +208 -0
- tapscribe/transcribers/base.py +292 -0
- tapscribe/transcribers/catalog.py +862 -0
- tapscribe/transcribers/faster_whisper.py +177 -0
- tapscribe/transcribers/mlx_parakeet.py +217 -0
- tapscribe/transcribers/mlx_voxtral.py +114 -0
- tapscribe/transcribers/mlx_whisper.py +173 -0
- tapscribe/transcribers/moonshine_mlx.py +68 -0
- tapscribe/transcribers/moonshine_onnx.py +80 -0
- tapscribe/transcribers/parakeet.py +172 -0
- tapscribe/transcribers/voxtral.py +137 -0
- tapscribe/wav_append.py +114 -0
- tapscribe/wav_cache.py +599 -0
- tapscribe/wav_predecode.py +62 -0
- tapscribe/web/components/active-taps.html +64 -0
- tapscribe/web/components/config-card.html +35 -0
- tapscribe/web/components/live-channel.html +108 -0
- tapscribe/web/components/live-feed.html +36 -0
- tapscribe/web/components/merged-transcript.html +73 -0
- tapscribe/web/components/next/people.html +49 -0
- tapscribe/web/components/next/recordings.html +185 -0
- tapscribe/web/components/next/sessions.html +138 -0
- tapscribe/web/components/next/spine.html +60 -0
- tapscribe/web/components/next/summary.html +139 -0
- tapscribe/web/components/next/taps.html +103 -0
- tapscribe/web/components/next/views.html +475 -0
- tapscribe/web/dashboard.css +705 -0
- tapscribe/web/js/api.js +480 -0
- tapscribe/web/js/api.test.js +261 -0
- tapscribe/web/js/components/active-taps.js +232 -0
- tapscribe/web/js/components/active-taps.test.js +55 -0
- tapscribe/web/js/components/config-card.js +178 -0
- tapscribe/web/js/components/live-channel.js +322 -0
- tapscribe/web/js/components/live-feed.js +274 -0
- tapscribe/web/js/components/live-feed.test.js +167 -0
- tapscribe/web/js/components/merged-transcript.js +229 -0
- tapscribe/web/js/formatters.js +82 -0
- tapscribe/web/js/lib/chrome.js +75 -0
- tapscribe/web/js/lib/format.js +141 -0
- tapscribe/web/js/lib/render.js +268 -0
- tapscribe/web/js/lib/templates.js +198 -0
- tapscribe/web/js/model-select.js +121 -0
- tapscribe/web/js/model-select.test.js +77 -0
- tapscribe/web/js/next/components/engine.js +102 -0
- tapscribe/web/js/next/components/language-picker.js +38 -0
- tapscribe/web/js/next/components/spine.js +381 -0
- tapscribe/web/js/next/components/spine.test.js +70 -0
- tapscribe/web/js/next/components/summarizer-controls.js +259 -0
- tapscribe/web/js/next/components/waveform.js +251 -0
- tapscribe/web/js/next/components/waveform.test.js +28 -0
- tapscribe/web/js/next/main.js +713 -0
- tapscribe/web/js/next/poll-pacer.js +46 -0
- tapscribe/web/js/next/poll-pacer.test.js +78 -0
- tapscribe/web/js/next/shell.js +326 -0
- tapscribe/web/js/next/shell.test.js +100 -0
- tapscribe/web/js/next/subtitles.js +50 -0
- tapscribe/web/js/next/subtitles.test.js +54 -0
- tapscribe/web/js/next/ui.js +77 -0
- tapscribe/web/js/next/views/capture.js +216 -0
- tapscribe/web/js/next/views/people.js +245 -0
- tapscribe/web/js/next/views/recordings.js +892 -0
- tapscribe/web/js/next/views/sessions.js +727 -0
- tapscribe/web/js/next/views/settings.js +402 -0
- tapscribe/web/js/next/views/summary.js +490 -0
- tapscribe/web/js/next/views/taps.js +96 -0
- tapscribe/web/js/next/views/transcript.js +798 -0
- tapscribe/web/js/next/views/transcript.test.js +76 -0
- tapscribe/web/js/setup/setup.css +164 -0
- tapscribe/web/js/setup/setup.js +253 -0
- tapscribe/web/js/speakers.js +26 -0
- tapscribe/web/js/templates.js +288 -0
- tapscribe/web/js/types.d.ts +637 -0
- tapscribe/web/js/vc/components/alert/alert.css +32 -0
- tapscribe/web/js/vc/components/alert/alert.element.js +14 -0
- tapscribe/web/js/vc/components/alert/alert.html +10 -0
- tapscribe/web/js/vc/components/alert/alert.js +61 -0
- tapscribe/web/js/vc/components/button/button.css +57 -0
- tapscribe/web/js/vc/components/button/button.element.js +17 -0
- tapscribe/web/js/vc/components/button/button.html +7 -0
- tapscribe/web/js/vc/components/button/button.js +81 -0
- tapscribe/web/js/vc/components/chip/chip.css +44 -0
- tapscribe/web/js/vc/components/chip/chip.element.js +15 -0
- tapscribe/web/js/vc/components/chip/chip.html +7 -0
- tapscribe/web/js/vc/components/chip/chip.js +27 -0
- tapscribe/web/js/vc/components/empty-state/empty-state.css +19 -0
- tapscribe/web/js/vc/components/empty-state/empty-state.element.js +10 -0
- tapscribe/web/js/vc/components/empty-state/empty-state.html +8 -0
- tapscribe/web/js/vc/components/empty-state/empty-state.js +28 -0
- tapscribe/web/js/vc/components/field/field.css +41 -0
- tapscribe/web/js/vc/components/field/field.html +8 -0
- tapscribe/web/js/vc/components/field/field.js +68 -0
- tapscribe/web/js/vc/components/panel/panel.css +47 -0
- tapscribe/web/js/vc/components/panel/panel.html +7 -0
- tapscribe/web/js/vc/components/panel/panel.js +58 -0
- tapscribe/web/js/vc/components/progress/progress.css +26 -0
- tapscribe/web/js/vc/components/progress/progress.element.js +12 -0
- tapscribe/web/js/vc/components/progress/progress.html +7 -0
- tapscribe/web/js/vc/components/progress/progress.js +32 -0
- tapscribe/web/js/vc/components/spinner/spinner.css +28 -0
- tapscribe/web/js/vc/components/spinner/spinner.element.js +11 -0
- tapscribe/web/js/vc/components/spinner/spinner.html +7 -0
- tapscribe/web/js/vc/components/spinner/spinner.js +27 -0
- tapscribe/web/js/vc/lib/component.js +42 -0
- tapscribe/web/js/vc/lib/element.js +113 -0
- tapscribe/web/js/vc/lib/templates.js +198 -0
- tapscribe/web/js/vc/lib/tone.js +48 -0
- tapscribe/web/next.css +1322 -0
- tapscribe/web/next.html +52 -0
- tapscribe/web/setup.html +29 -0
- tapscribe/web/tokens.css +55 -0
- tapscribe/web/tones.css +21 -0
- tapscribe-1.1.0.dist-info/METADATA +399 -0
- tapscribe-1.1.0.dist-info/RECORD +169 -0
- tapscribe-1.1.0.dist-info/WHEEL +5 -0
- tapscribe-1.1.0.dist-info/entry_points.txt +2 -0
- tapscribe-1.1.0.dist-info/licenses/LICENSE +21 -0
- 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()
|