Host-side Windows / Linux compatibility (2026-09-03)¶
Maintainer assessment under docs/reviews/. Dated snapshot, not a contract. Where this disagrees with contracts or src/, code and contracts win. Hosted at /guide/reviews/host-side-windows-linux-compat-2026-09-03/ after make docs.
Parked (2026-09-03): do not implement Windows/Linux host-script portability before 1.0. Tracked as 1.x theme G3 in ROADMAP.md. 1.0 stays macOS-typical inbox-watch / whispermlx + Docker (WSL2 on Windows).
Static reconstruction of the host-side TranscriptX tree (processes that run outside transcriptx-web) as of 2026-09-03. No Windows or Linux host was executed. CI is ubuntu-latest only (.github/workflows/ci.yml). Where docs and code disagree, code wins.
Claimed OS policy already on file: install_verification_matrix.md (macOS supported-with-caveats, Linux supported, Windows best-effort / WSL2+Docker) and release_ops_support_1_0.md (macOS and Linux for supported cells; Windows best-effort). This review checks whether host companions match that story.
Verdict¶
The analysis app in Docker is a Linux container on every host OS. The host companions that feed it audio and transcripts are macOS-shaped.
Surface |
Linux host |
Windows host |
Notes |
|---|---|---|---|
|
Mostly portable Python |
Runnable with gaps |
pathlib + argv subprocess; Unix venv discovery and Unix install docs |
Audio convert + STT via inbox-watch |
Blocked by whispermlx |
Blocked by whispermlx |
|
Transcript-copy / |
Should work |
Admit venv lookup broken |
|
|
Not a Linux STT path |
Not a Windows STT path |
Apple MLX; flags are whispermlx-specific |
Transcribe Audio copyable commands |
WhisperX Docker snippet is the Linux path |
Bash-only; will not paste into cmd/PowerShell |
|
|
None |
None |
Hardcoded to one macOS user even on Darwin |
|
Intended |
No |
bash + |
In-app G2 watcher |
OK native Linux; flaky in Docker bind mounts |
OK native; flaky in Docker Desktop |
|
Docker Compose bind mounts |
Supported; UID interpolation trap |
Docker Desktop + WSL2 |
|
Product-honest reading: Linux users are expected to run analysis in Docker and STT via the WhisperX (or Whisper-WebUI) copyable recipes, then Import Transcript. Windows users are expected to use WSL2 + Docker and the same import path. The host inbox watcher is documented as a Mac companion (directory_watcher.md: “runs on the Mac host”). That is accurate. It is not a cross-platform host daemon.
What “outside the container” actually is¶
flowchart TB
subgraph host [Host OS process]
Inbox[USB / drop folder]
IW[inbox-watch.py]
WM[whispermlx-missing.py]
MLX[whispermlx binary]
FFMPEG[ffmpeg]
Admit["python -m transcriptx.admit_originals"]
CmdGen[Copyable bash from Transcribe Audio]
WX[WhisperX docker run]
end
subgraph ctr [transcriptx-web Linux container]
GUI[Streamlit]
G2[G2 watchdog Observer]
Import[admit_and_register]
end
Inbox --> IW
IW --> FFMPEG
IW --> WM
WM --> MLX
IW --> Admit
CmdGen --> MLX
CmdGen --> WX
MLX -->|originals JSON| Import
WX -->|originals JSON| Import
IW -->|copy transcripts| Import
G2 -->|in-process| Import
GUI --> CmdGen
In-process (inside whatever runs Streamlit): Settings → Watcher (G2). If the operator uses Compose, that is the Linux container. If they use ./transcriptx.sh, that is native host Python.
Out-of-process host scripts (never imported by Streamlit):
Path |
Role |
Inventory platform |
|---|---|---|
|
Poll inbox → ffmpeg MP3 + copy transcripts; optional admit |
“macOS typical” |
|
Batch STT for MP3s missing JSON |
“Apple Silicon typical” |
|
login |
Darwin only |
|
Copyable host commands (not executed) |
bash / macOS / Linux Docker recipes |
|
Native venv + Streamlit |
macOS/Linux |
|
ffmpeg helpers (also callable from GUI Tools) |
“any” + ffmpeg |
|
ZIP backup/restore |
pathlib ZIP |
Maintainer bash (Makefile, scripts/release/*.sh, scripts/docker-smoke-test.sh) is Unix/CI. Out of scope for an unfamiliar Windows operator; listed only where it blocks Linux/Windows product use.
A. inbox-watch.py¶
~1.7k lines, stdlib only, does not import transcriptx. Optional --admit subprocesses python -m transcriptx.admit_originals.
What is already portable¶
pathlib.Path,expanduser(),mkdir(parents=True),os.replacefor the.mp3.partial→ dest hop (same-volume atomic replace on POSIX and NT).subprocess.run(list_of_args)— noshell=True.Classification and stem matching use
.suffix.lower()/.stem.lower()(classify_path,find_stem_match). Windows.WAV/.JSONfrom recorders is handled.Dotfile skip (
path.name.startswith(".")) hides the convert temp.inbox-watch.{stem}.mp3.partial.Poll loop (
--watch+time.sleep) rather than inotify. Correct choice for USB volumes that appear and disappear (wait+ empty cycles when the inbox path is missing).shutil.which("ffmpeg")(findsffmpeg.exeon Windows if PATH/PATHEXT is set).KeyboardInterrupt→ clean stop; failed convert unlinks the partial.stdout/stderr
reconfigure(line_buffering=True)withOSError/ValueErrorswallowed (Windows consoles that refuse reconfigure).
Linux — expected behaviour¶
Convert + copy + admit, given ffmpeg + a native TranscriptX venv, should run. The script does not call Darwin APIs.
STT after convert will not, unless a whispermlx binary exists. watch_audio (default on) requires whispermlx-missing even when every MP3 already has JSON (find_whispermlx_missing / maybe_run_missing). There is no hook for WhisperX, faster-whisper, or a no-op. Practical Linux use today:
python3 scripts/inbox-watch.py --once --no-watch-audio --watch-transcripts \
--inbox … --transcripts …/originals
# optional: --admit --admit-python .transcriptx/bin/python
Audio conversion without STT is also unused: converting always tries to invoke missing afterwards when watch_audio is on.
find_admit_python looks at Unix venv layouts only:
for rel in (
".transcriptx/bin/python",
".transcriptx/bin/python3",
".venv/bin/python",
):
On Linux that matches transcriptx.sh / a normal venv. On Windows those paths never exist (Scripts\python.exe). --admit-python still works if the operator passes it explicitly.
Install docs (host-stt.md, module docstring) use install -m 755 and ~/.local/bin — fine on Linux, absent on Windows.
No systemd user unit exists. --once is cron-shaped; --watch is a long-running foreground process. The macOS agent is not a template for transcriptx-inbox-watch.service.
USB inbox paths: docs and .env.example show /Volumes/USB-DISK/RECORD. Linux equivalents are /media/$USER/… or /run/media/$USER/…. The script does not care; only examples do.
Windows — extra gaps¶
Issue |
Why it bites |
|---|---|
Shebang |
cmd.exe does not run extensionless copies; use |
|
No GNU |
|
Never finds |
|
Fails with |
|
USB recorders and Explorer previews often hold the source; delete/move then fails. FAT/exFAT + AV is the usual USB inbox. |
|
FAT mtime granularity is 2s. Size still changing saves you; preallocated files that fill in place can look “stable” early. |
Hidden files |
Only names starting with |
|
Case-sensitive. |
Console encoding |
Recorders with non-cp1252 names can |
No Task Scheduler / NSSM / Windows service sample |
|
chmod(0o600) on saved JSON is a no-op besides the read-only bit on NT; harmless.
wait_for_directory is defined but unused; --watch inlines the same idea. Not an OS bug.
Coupling that is not OS-specific but blocks non-Mac STT¶
build_missing_cmd always passes --source / --transcripts to whispermlx-missing. There is no --transcribe-cmd or provider switch. Until that exists, inbox-watch audio mode is a macOS STT pipeline, not a generic convert+queue.
B. whispermlx-missing.py¶
Same portability profile as inbox-watch for paths/config/chmod/os.replace, plus:
shutil.which("whispermlx")— binary name, Apple MLX stack.WhisperMLXProvider.is_availablefails closed unlesssys.platform == "darwin"(whispermlx_provider.py).shlex.joinfor dry-run command printing (POSIX quoting; cosmetic on Windows).subprocess.run(..., text=True)withoutencoding="utf-8"— Windowstext=Trueuses the locale encoding; whispermlx stderr with UTF-8 can throw on decode if anyone ever ran it there.Live run requires a whispermlx binary (dry-run does not). Extra args (
--output_dir,--diarize,-f) are whispermlx CLI, not WhisperX.
Linux/Windows operators who want bulk “skip existing JSON” need a different driver (or a wrapper that only pretends to be whispermlx). The Transcribe Audio page and host-stt.md already say macOS host / not inside the analysis container. They do not say “this script is useless on Linux”; the failure mode is a missing binary at runtime.
C. macOS login agent (not portable, not a template)¶
scripts/macos/inbox-watch-agent.sh and com.transcriptx.inbox-watch.plist are machine-local, not repo-portable:
Absolute Python:
/Users/89298/.pyenv/versions/3.10.13/bin/python3Absolute repo, logs, and
TRANSCRIPTX_*dirs under/Users/89298/Documents/…PATH: pyenv shims + Homebrew
Comment: waits for
/Volumes/USB-DISK/RECORD
Even another Mac checkout cannot load this plist unchanged. Linux needs a systemd user unit (or cron --once). Windows needs Task Scheduler. None exist. Docs call this “optional macOS login agent” — correct, but the files are a private LaunchAgent checked into the tree.
D. Copyable host commands (command_gen.py)¶
Streamlit never executes these. The operator pastes them into a host terminal. Every generator emits POSIX shell:
Helper |
Constructs |
|---|---|
|
|
|
|
|
|
|
|
_q() is shlex.quote (single-quote POSIX). PowerShell / cmd quoting is different. Git Bash or WSL can run the snippets on a Windows box; Windows Terminal + PowerShell cannot.
Further Windows/Linux nits in the same module:
params.input_path.rstrip("/")then.endswith((".mp3", …))to choose single-file vs folder loop. Backslash paths (C:\rec\a.mp3) still end with.mp3, but.MP3does not match (no.lower()). Windows Explorer often shows uppercase extensions; the generator would emit a folder loop for a single file.rstrip("/")does not strip\. WhisperX-v C:\data\audio\:/audiois a broken bash mount; Docker Desktop PowerShell wants a different spelling; Git Bash wants/c/data/audio.Default WebUI clone dir
$HOME/Whisper-WebUIis POSIX.looks_like_container_install_pathonly treats/opt/venvas the Docker install. Fine.
Linux GPU STT is this WhisperX docker run recipe (docs/recipes/whisperx/), not inbox-watch. That path is the one that matches “Linux supported” for transcription, and it is already bash/Linux-shaped.
E. Native launcher transcriptx.sh¶
Supported inventory row: macOS/Linux. Concrete Unix assumptions:
#!/bin/bash,source .env,source .transcriptx/bin/activatecommand -v python3.10(notpython/py -3.10)write_install_profile→$HOME/.config/transcriptx(XDG; no%APPDATA%)which python,rm -rf, ANSIecho -e
Windows native GUI is python -m venv .venv + .venv\Scripts\activate as mentioned once in installation-advanced.md. There is no transcriptx.ps1 / transcriptx.bat. Compose remains the advertised Windows path.
Linux: this script is the native path. It will not help inbox-watch find whispermlx.
F. In-app G2 watcher (runs wherever Streamlit runs)¶
Not a host script, but it is the other “watcher”, and it does run outside Docker on a native install.
WatchObserver constructs watchdog.observers.Observer() — native backends:
OS |
Backend |
USB / network / Docker bind |
|---|---|---|
Linux native |
inotify |
Often silent on NFS, SMB, some FUSE; USB ext4/exFAT usually OK |
Linux in Compose watching a host drop folder |
inotify inside the VM |
Docker Desktop (Mac/Windows) virtiofs/osxfs frequently does not deliver inotify for host-side creates. Native Linux docker bind-mounts are more reliable. |
Windows native |
|
USB usually OK; SMB mixed |
macOS native |
FSEvents |
USB OK; Docker bind from host → container is the weak case |
There is no PollingObserver fallback. Debounce key is str(path) (no normcase). G2 never converts audio (auto_transcribe rejected until theme H).
This is why directory_watcher.md points Mac operators at host inbox-watch for convert+STT. On Linux Docker, the same virtiofs inotify hole exists for G2 if they watch a Desktop-mounted inbox. Host-side inbox-watch polling would still be the robust drop-folder design — if STT were not whispermlx-only.
H. Other host Python helpers¶
audio_merge.py / audio_preprocess.py / workspace_backup.py insert src/ on sys.path and import transcriptx. They are as portable as the package + ffmpeg. ZIP backup uses stdlib; path separators inside archives are typically POSIX from zipfile — restore on Windows of a Mac-made ZIP is a general zip issue, not unique here.
They are not wired to inbox-watch. A Linux operator merging serial parts still uses Tools → Auto-merge (in the container, with image ffmpeg) or a native install.
I. Findings (ranked)¶
Design / honesty (not defects if the Mac-only story is the contract)
Audio inbox-watch is a whispermlx pipeline. Linux and Windows cannot complete convert→STT→originals with supported tools. WhisperX remains a separate copy-paste recipe.
No Linux/Windows service analogue to the LaunchAgent.
--watchis a terminal process;--onceis cron-shaped with no unit files.Copyable commands are bash. Matches Linux and macOS; not Windows PowerShell. WSL is the undocumented escape hatch.
Defects if anyone runs these scripts on Windows (best-effort claim)
find_admit_pythonignoresScripts\python.exe.--admitfails unless--admit-pythonis set.Command generator extension check is case-sensitive and strips only
/. Wrong snippet forfile.MP3/C:\dir\.Install/run docs (
install,python3,~/.local/bin,.transcriptx/bin/python) have no Windows stanza.Layout checks are case-sensitive unlike
canonicalise_path.
Linux operational
G2 + Docker bind-mount inotify can miss host drops (especially Docker Desktop). Polling host
inbox-watchis the robust pattern — but STT is missing on Linux.Compose
${UID}often defaults to 1000. Documentexport UID=$(id -u) GID=$(id -g)or put them in.env.ffmpeg fallback paths in Tools are Unix-only (host native GUI on Linux is OK via
/usr/bin; Windows native GUI is not).
Hygiene
scripts/macos/*.plistis a personal LaunchAgent (absolute/Users/89298/...). Do not treat as a distribution artefact; it will confuse Linux/Windows readers who openscripts/macos/.CI never runs host scripts on Windows (or macOS).
tests/scripts/test_inbox_watch.pyis stdlib-mockable and will pass on Ubuntu while#4stays latent.
J. What would make Linux/Windows real¶
Small, local, still “best-effort Windows”:
Discover
.transcriptx/Scripts/python.exeand.venv/Scripts/python.exe; mentionpy -3 scripts\inbox-watch.pyinhost-stt.md.command_gen:Path(input).suffix.lower(); strip/and\; say “Git Bash / WSL / macOS / Linux shell”.Document Linux inbox-watch as transcript copy + admit only, or convert-without-STT, until a provider exists.
Document Compose
UID/GIDon Linux; keep Windows on WSL2+Docker.
Larger (theme H / G follow-on):
Decouple inbox-watch STT:
--transcribe {whispermlx-missing,whisperx-docker,none}.PollingObserverfallback for G2 on bind mounts.systemd unit + a templated (not user-absolute) macOS plist; optional Windows Task Scheduler XML.
transcriptx.ps1only if native Windows GUI is promoted off “best-effort”.
K. Scope limits¶
Not reviewed as host daemons: Sphinx make docs, release *.sh, count_sloc.py, eval/bench scripts, workspaces frontend copy-css.mjs. Those are maintainer/Unix.
Not executed: Windows 11, WSL2, Debian/Fedora, exFAT USB, Defender-on-replace, Docker Desktop inotify on a watched inbox.