Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/install-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ jobs:
- name: Install tapscribe + ${{ matrix.family }} extra
# `summarize` on non-macOS pulls llama-cpp-python, which builds from
# source by default (needs cmake + a C++ toolchain — see the "Verify
# cmake on PATH" step above). Mirror start.sh / start.ps1's own
# SUMMARIZE_PIP_ARGS logic and pass the maintainer's prebuilt
# CPU-wheel index so this job matches what an operator's bring-up
# actually runs, not a naive install. `shell: bash` so the same
# cmake on PATH" step above). Pass the maintainer's prebuilt CPU-wheel
# index that `tapscribe.preflight.LLAMA_CPP_WHEEL_INDEX` declares, so
# this job matches what an operator's bring-up actually runs, not a
# naive install. tests/test_install_matrix.py pins the two equal. `shell: bash` so the same
# conditional runs on the Windows runner too (default shell there is
# pwsh).
shell: bash
Expand Down
9 changes: 9 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ Verb/noun split: **the Recorder** writes **a recording**, one WAV per
[Utterance](#utterance) per speaker. `recording_enabled` gates new recordings
only; live transcription is independent of it.

## Bring-up defaults

The launch values the start scripts hand the Recorder: bind host, recorder
port, live port, live model, language hint. They have one Python-side owner,
`tapscribe/bringup_defaults.py`: the scripts run `python -m
tapscribe.bringup_defaults` at bring-up and parse the `KEY=value` lines it
prints. An `SX_*` environment variable overrides each one, and empty counts as
unset. The module docstring has the precedence rule and the wire format.

## Transcriber

The protocol for "something that can transcribe one WAV":
Expand Down
37 changes: 27 additions & 10 deletions start.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
# .\start.ps1 -NoAuth # disable dashboard auth + /tap token gate (DEV ONLY)
# .\start.ps1 -Tls # serve https:// + wss:// (auto self-signed)
# .\start.ps1 -NonInteractive # install the saved/default selection in-terminal (no browser)
#
# Configurable via env vars (resolved by `python -m tapscribe.bringup_defaults`):
# SX_HOST bind address (default localhost, 0.0.0.0 under -Lan; a set
# SX_HOST beats -Lan)
# SX_PORT_WLK WhisperLiveKit port (default: ephemeral)
# SX_PORT_REC recorder port (default 8001)
# SX_MODEL initial live Whisper model (default tiny.en; switch live from dashboard)
# SX_LANG language hint (default en)

[CmdletBinding()]
param(
Expand Down Expand Up @@ -107,17 +115,26 @@ if (-not (Test-Path ".tapscribe-install.json") -and -not $NonInteractive) {
& python -m tapscribe.preflight

# --- Configuration ----------------------------------------------------------
$Model = if ($env:SX_MODEL) { $env:SX_MODEL } else { "tiny.en" }
$LangCode = if ($env:SX_LANG) { $env:SX_LANG } else { "en" }
$PortRec = if ($env:SX_PORT_REC) { $env:SX_PORT_REC } else { "8001" }
$PortWlk = if ($env:SX_PORT_WLK) { $env:SX_PORT_WLK } else { "" }
if ($env:SX_HOST) {
$BindHost = $env:SX_HOST
} elseif ($Lan) {
$BindHost = "0.0.0.0"
} else {
$BindHost = "localhost"
# `tapscribe.bringup_defaults` owns the bring-up values, and start.sh reads
# the same source. Fatal by design, unlike the preflight above: without the
# launch config there is nothing to launch.
$BringupArgs = @()
if ($Lan) { $BringupArgs += "--lan" }
$BringupLines = @(& python -m tapscribe.bringup_defaults @BringupArgs)
if ($LASTEXITCODE -ne 0) {
Write-Error "[start] Could not read bring-up defaults; aborting."
exit 1
}
$Bringup = @{}
foreach ($line in $BringupLines) {
$key, $value = $line -split "=", 2
$Bringup[$key] = $value
}
$BindHost = $Bringup["HOST"]
$PortRec = $Bringup["PORT_REC"]
$PortWlk = $Bringup["PORT_WLK"]
$Model = $Bringup["MODEL"]
$LangCode = $Bringup["LANG"]

$ExtraArgs = @()
if ($NoMlx) { $ExtraArgs += "--no-mlx" }
Expand Down
39 changes: 29 additions & 10 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
# 6. Stream all logs to this terminal; Ctrl+C stops everything cleanly.
#
# Configurable via env vars:
# SX_HOST bind address (default localhost, overridden by --lan to 0.0.0.0)
# SX_HOST bind address (default localhost, 0.0.0.0 under --lan; a set
# SX_HOST beats --lan)
# SX_PORT_WLK WhisperLiveKit port (default: ephemeral — WLK is internal,
# only the recorder talks to it; pin only if you have a reason)
# SX_PORT_REC recorder port (default 8001)
Expand Down Expand Up @@ -200,16 +201,34 @@ fi
python -m tapscribe.preflight || true

# --- Configuration ----------------------------------------------------------
MODEL="${SX_MODEL:-tiny.en}"
LANG="${SX_LANG:-en}"
PORT_WLK="${SX_PORT_WLK:-}"
PORT_REC="${SX_PORT_REC:-8001}"
# `tapscribe.bringup_defaults` owns the bring-up values, and start.ps1 reads
# the same source. Fatal by design, unlike the preflight above: without the
# launch config there is nothing to exec.

# Reads the KEY=value lines on stdin into HOST, PORT_REC, PORT_WLK, MODEL and
# LANG_CODE. LANG_CODE, not LANG: LANG is the exported locale every child reads.
parse_bringup() {
local key val
while IFS='=' read -r key val; do
case "$key" in
HOST) HOST="$val" ;;
PORT_REC) PORT_REC="$val" ;;
PORT_WLK) PORT_WLK="$val" ;;
MODEL) MODEL="$val" ;;
LANG) LANG_CODE="$val" ;;
esac
done
}

LAN_FLAG=()
if [ "$LAN" -eq 1 ]; then
HOST="${SX_HOST:-0.0.0.0}"
else
HOST="${SX_HOST:-localhost}"
LAN_FLAG=(--lan)
fi
BRINGUP=$(python -m tapscribe.bringup_defaults "${LAN_FLAG[@]}") || {
echo "[start] Could not read bring-up defaults; aborting." >&2
exit 1
}
parse_bringup <<< "$BRINGUP"

LAN_IP=""
if command -v ipconfig >/dev/null 2>&1; then
Expand Down Expand Up @@ -258,14 +277,14 @@ if [ "$BROWSER_SETUP" -eq 1 ]; then
fi
echo " Live channel $LIVE_LABEL"
echo " Backend $BACKEND_LABEL"
echo " Initial model $MODEL (lang=$LANG; change from the dashboard or via SX_MODEL=…)"
echo " Initial model $MODEL (lang=$LANG_CODE; change from the dashboard or via SX_MODEL=…)"
echo ""

python -m tapscribe \
--host "$HOST" \
--port "$PORT_REC" \
--live-model "$MODEL" \
--live-language "$LANG" \
--live-language "$LANG_CODE" \
"${EXTRA_ARGS[@]}" &
REC_PID=$!

Expand Down
26 changes: 18 additions & 8 deletions tapscribe/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import uvicorn

from . import config, install_target
from . import bringup_defaults, config, install_target
from .app import app
from .live import LiveConfig
from .recorder import Recorder
Expand All @@ -30,16 +30,26 @@ def build_parser() -> argparse.ArgumentParser:
prog="python -m tapscribe",
description="TapScribe — local-first transcription recorder + dashboard.",
)
p.add_argument("--host", default="localhost", help="Bind address. Use 0.0.0.0 to expose on LAN.")
# Defaulted from `bringup_defaults`, which the start scripts also read: a bare
# `python -m tapscribe` launch gets the same values as a scripted one.
p.add_argument(
"--host",
default=bringup_defaults.HOST,
help=f"Bind address. Use {bringup_defaults.HOST_LAN} to expose on LAN.",
)
# Defaulted FROM config, which `main()` then stamps back onto it: one declaration of the
# port, rather than a literal here that a launch skipping argparse would disagree with.
p.add_argument("--port", type=int, default=config.PORT)
p.add_argument(
"--live-model",
default="tiny.en",
default=bringup_defaults.MODEL,
help="WhisperLiveKit model name (tiny.en, small.en, large-v3, ...). Changeable from the dashboard.",
)
p.add_argument("--live-language", default="en", help="WhisperLiveKit language hint (en, no, auto, ...)")
p.add_argument(
"--live-language",
default=bringup_defaults.LANG,
help="WhisperLiveKit language hint (en, no, auto, ...)",
)
p.add_argument(
"--live-host",
default=None,
Expand All @@ -50,7 +60,7 @@ def build_parser() -> argparse.ArgumentParser:
p.add_argument(
"--live-port",
type=int,
default=0,
default=int(bringup_defaults.PORT_WLK or 0),
help="Bind port for the live channel. 0 (default) = pick a free ephemeral "
"port at spawn time. WhisperLiveKit is internal — only the recorder talks "
"to it — so a stable well-known port is rarely useful, and a fixed 8000 "
Expand Down Expand Up @@ -235,9 +245,9 @@ def main() -> None:
app.state.install_spec = args.install_spec
app.state.log_json = bool(args.log_json)

if args.host == "0.0.0.0":
if args.host == bringup_defaults.HOST_LAN:
print(
"[tapscribe] WARNING: binding to 0.0.0.0 exposes the recorder to "
f"[tapscribe] WARNING: binding to {bringup_defaults.HOST_LAN} exposes the recorder to "
"the LAN. Make sure you trust your network.",
flush=True,
)
Expand Down Expand Up @@ -277,7 +287,7 @@ def main() -> None:
print(bar, flush=True)
else:
print("[tapscribe] WARNING: --no-auth — dashboard AND /tap are UNAUTHENTICATED.", flush=True)
if args.host == "0.0.0.0":
if args.host == bringup_defaults.HOST_LAN:
print("[tapscribe] WARNING: combined with LAN binding, anyone on the", flush=True)
print("[tapscribe] network can view/delete recordings. Re-enable auth.", flush=True)

Expand Down
81 changes: 81 additions & 0 deletions tapscribe/bringup_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Bring-up defaults: the one Python owner of the values the start scripts launch with.

`start.sh` and `start.ps1` run `python -m tapscribe.bringup_defaults` and parse
the `KEY=value` lines it prints. `__main__.build_parser` imports the same
constants, so a bare `python -m tapscribe` launch gets the same defaults.
`resolve` is pure: an env mapping and the `--lan` flag go in, one
`BringupConfig` comes out.

Stdlib-only at import (`config` is stdlib-only too), so `-m` works against a
venv that holds nothing but pip. The scripts' line parsers truncate a value
that contains a newline, which no model id, port or host does.
"""

from __future__ import annotations

import argparse
import os
from collections.abc import Mapping
from dataclasses import asdict, dataclass

from tapscribe import config

#: Initial live Whisper model; changeable from the dashboard.
MODEL = "tiny.en"
#: Live language hint.
LANG = "en"
#: Bind host: loopback by default, all interfaces under `--lan`.
HOST = "localhost"
HOST_LAN = "0.0.0.0"
#: Empty means ephemeral: the recorder picks a free live port at spawn.
PORT_WLK = ""
#: `config.py` owns the one declaration of the recorder port; this is its str form.
PORT_REC = str(config.PORT)


@dataclass(frozen=True)
class BringupConfig:
"""The five launch values, resolved. All `str`, because they cross a shell boundary.

The field order is the wire order, and a field's upper-cased name is its key.
"""

host: str
port_rec: str
port_wlk: str
model: str
lang: str


def resolve(env: Mapping[str, str], *, lan: bool = False) -> BringupConfig:
"""Resolve the bring-up values: a set, non-empty `SX_*` var beats the
default, and `SX_HOST` beats `--lan`."""
return BringupConfig(
host=env.get("SX_HOST") or (HOST_LAN if lan else HOST),
port_rec=env.get("SX_PORT_REC") or PORT_REC,
port_wlk=env.get("SX_PORT_WLK") or PORT_WLK,
model=env.get("SX_MODEL") or MODEL,
lang=env.get("SX_LANG") or LANG,
)


def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(
prog="python -m tapscribe.bringup_defaults",
description="Print TapScribe's bring-up launch values as KEY=value lines.",
)
p.add_argument(
"--lan",
action="store_true",
help=f"Bind host: {HOST_LAN} instead of {HOST}.",
)
args = p.parse_args(argv)

cfg = resolve(os.environ, lan=args.lan)
for name, value in asdict(cfg).items():
print(f"{name.upper()}={value}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
2 changes: 1 addition & 1 deletion tapscribe/preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def _module_present(name: str) -> bool:
def summarize_probe_module(system: str, machine: str) -> str:
"""Which module proves the `[summarize]` extra is usable on this host.

Mirrors `LocalSummarizer.resolve_local_backend`'s routing: the MLX backend
Mirrors `catalog.resolve_local_backend`'s routing: the MLX backend
on Apple Silicon, the GGUF/llama.cpp one everywhere else. Probing the wrong
module would either reinstall on every boot or never install at all.
"""
Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,16 @@ def fake_wlk() -> Iterator[FakeWlkThread]:
wlk.stop()


@pytest.fixture
def reset_available_backends() -> Iterator[None]:
"""Restore the catalog's auto-probe after a test forces the backend set,
so a forced {'mlx'}/{'cpu'} cannot leak into another test's routing."""
from tapscribe.runtime_probe import set_available_backends_for_testing

yield
set_available_backends_for_testing(None)


# ---------------------------------------------------------------------------
# Lightweight transcriber stub — shared across route + cache tests
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading