Host STT automation¶
Advanced host-side transcription helpers. Mainstream path (Import Transcript, Transcribe Audio command generation): Transcription. Audio merge/preprocess before STT: Audio prep.
There is no transcriptx transcript … terminal subcommand. Validate and import from the GUI, or from code as below.
Finding whispermlx¶
which whispermlx
If not found, set WHISPERMLX in whisperx.env at the repo root to the full binary path.
Host note (macOS whispermlx): pyannote may dump a long torchcodec is not installed correctly warning (FFmpeg ABI / PyTorch mismatch while probing libtorchcodec). If VAD and transcription continue and segments appear, treat it as noise — whispermlx usually hands pyannote a preloaded waveform. Worth aligning torchcodec + PyTorch + FFmpeg only if a later job needs pyannote to decode a file path directly and fails.
Environment defaults (whisperx.env)¶
Copy docs/recipes/whisperx/whisperx.env.example to whisperx.env and configure:
Variable |
Purpose |
|---|---|
|
Path to whispermlx binary |
|
Default model (e.g. |
|
Default language |
|
Default diarization on/off |
|
Per-file timeout (0 = no limit) |
|
Required when diarization is on |
whispermlx-missing bulk script¶
Install once from the repo root (not shipped as a package entrypoint):
mkdir -p ~/.local/bin
install -m 755 scripts/whispermlx-missing.py ~/.local/bin/whispermlx-missing
# ensure ~/.local/bin is on PATH (new shell, or: export PATH="$HOME/.local/bin:$PATH")
which whispermlx-missing
If command not found, either PATH is missing ~/.local/bin or the install step was skipped. You can also run without installing:
python3 scripts/whispermlx-missing.py --dry-run …
It processes MP3s in a source folder that lack matching JSON in a transcripts output folder.
When convert_wavs is on (default), it first converts .wav files in the source folder to
16 kHz mono 64k MP3 (same settings as inbox-watch), then moves the WAV into the WAV backup folder.
WAV → MP3 + archive: On by default. Converts .wav in --source to MP3 in the same folder,
then moves the WAV to --wav-backup (TRANSCRIPTX_WAV_BACKUP_DIR / portable default).
Skips when an MP3 with the same stem already exists. Disable with --no-convert-wavs or
"convert_wavs": false in JSON.
Resume / duplicates: stems with matching JSON are skipped by default — in --transcripts (typically …/originals), in the parent library root when that folder is named originals (already-imported canonical JSON), as foo (N).json import-archive names, or as a sidecar next to the MP3. Use --force / --rerun to replace after a valid new JSON is produced. --fuzzy-json-match also treats foo-….json / foo_….json / foo.….json as already done. Writes still go only to originals/ (see STORAGE.md).
Skip likely serial parts: --skip-serial (also JSON skip_serial / WHISPERMLX_SKIP_SERIAL) does not transcribe MP3s that Tools → Auto-merge would group as split parts or voice-note runs (meeting_part2, timestamp _1/_2, WhatsApp bursts, …). Merge those files first, then transcribe the *_merged.mp3. Standalone files still run. --force does not override this; use --no-skip-serial. When TranscriptX is importable (repo checkout / installed package), detection uses the same Auto-merge profiles; otherwise a filename + common voice-note fallback. Opt-in (off by default).
Dry-run: --dry-run previews work without requiring HF_TOKEN or a working whispermlx binary.
Partial failures: failed items leave temps under transcripts/.whispermlx-missing/tmp/; no partial JSON is written to the transcripts root. --clean-failed removes those temps.
Spaces: pass paths via quoted CLI args (the Transcribe Audio generator does this) or via the JSON config file.
Local config (gitignored): copy config/whispermlx-missing.example.json to .transcriptx/whispermlx-missing.json and set your paths. For standalone use outside the repo, pass --config /path/to/config.json or set WHISPERMLX_MISSING_CONFIG.
Config merge order: portable repo defaults ← TRANSCRIPTX_* / WHISPERMLX* env ← local JSON ← CLI flags.
When the script will process
sourceandtranscriptsare each set via CLI, local JSON, orTRANSCRIPTX_*env (not portable defaults alone).A fresh clone with no local JSON and no env overrides will not auto-run batch transcription.
When it only saves or prints config
--show-config— print effective settings; never runs whispermlx.--save-configwithout meaningful paths — writes.transcriptx/whispermlx-missing.jsonand exits.Normal run with portable defaults only — prints guidance; does not process.
Transcripts path semantics
Source |
Meaning |
|---|---|
|
Transcripts base directory; script appends |
|
Exact output directory — must be |
|
Maps directly to |
WAV archive |
|
Convert WAV in source |
|
ffmpeg for WAV convert |
|
Host helpers write raw engine JSON under originals/ only. Library admission requires Import Transcript, Settings → Watcher, or optional inbox-watch --admit (admit_and_register), which writes canonical schema_version / source markers plus an import sidecar.
whisperx.env is used only for the whispermlx subprocess environment (HF_TOKEN, etc.), not for resolving config paths. Repo .env is loaded early (without overriding existing shell env) for TRANSCRIPTX_* path overrides — same pattern as Docker/native TranscriptX.
Host inbox watcher (inbox-watch)¶
Optional host-side companion (not the in-app Settings → Watcher). Watches a drop folder for new audio and/or transcripts. Streamlit never runs it. Admission into the library is off by default; pass --admit (or "admit_to_library": true) to run python -m transcriptx.admit_originals after convert/copy/whispermlx-missing.
Install once from the repo root:
mkdir -p ~/.local/bin
install -m 755 scripts/inbox-watch.py ~/.local/bin/inbox-watch
which inbox-watch
Or run without installing: python3 scripts/inbox-watch.py --once --dry-run ….
Mode |
What it does |
Skip when |
|---|---|---|
|
Convert new inbox audio to 16 kHz mono 64k MP3 in the recordings folder, then run |
Recordings already has that stem (any audio extension). With |
|
Copy new JSON/SRT/VTT/txt/html into the transcripts dest |
Dest already has that stem (any transcript extension) |
|
After audio/transcript handling, admit eligible files in the transcripts dest (typically |
Already-imported stems; |
|
After admit, auto-write speaker display names (implies |
Fail-open: leaves |
|
After admit, create longitudinal profile links for matched enrolled / named profiles ( |
No new profiles from first-meeting names; does not enrol voice samples |
|
Disable that mode |
At least one mode must stay on |
# Preview (no ffmpeg, no copy, no whispermlx)
inbox-watch --once --dry-run \
--inbox /path/to/drop \
--recordings /path/to/recordings \
--transcripts /path/to/transcripts/originals
# One scan (cron / launchd)
inbox-watch --once --inbox … --recordings … --transcripts …
# Poll until Ctrl-C. If the USB inbox path is missing, --watch keeps running
# (empty scans) and still runs whispermlx-missing + --admit on the first cycle.
inbox-watch --watch --interval 5
# Same, and admit new originals/ JSON into the library
inbox-watch --watch --admit
# USB → diarized transcript with named speakers (and profile links)
inbox-watch --watch --auto-name
# names only:
inbox-watch --watch --auto-name --no-auto-link
--auto-name / --auto-link pass through to python -m transcriptx.admit_originals. Standalone: python -m transcriptx.identify_speakers --path FILE --auto-name --auto-link. Settings → Speakers stores ingest defaults in config_dir/identify.json. Full operator reference (knobs, fusion, review): Auto-identify speakers.
ffmpeg (audio mode): -nostdin -y -ac 1 -ar 16000 -c:a libmp3lame -b:a 64k -f mp3. Writes a temp .mp3.partial file then renames into recordings so whispermlx-missing never sees a half-written MP3. -f mp3 is required so ffmpeg 8+ can mux even when the temp name does not end in .mp3.
Local staging (removable inbox)¶
--inbox is still the drop folder (USB volume or any configured path). When that folder is on a removable / ejectable volume, audio is copied to a local staging directory before ffmpeg runs, so a flaky USB stick is not held open for a multi-hour convert.
Setting |
Default |
Override |
|---|---|---|
When to stage |
Auto: macOS |
|
Stage dir |
|
|
A complete staged file with the same size as the inbox original is reused on the next cycle (resume after a failed convert). A half-written .inbox-staging.{name}.partial is discarded and the copy is retried. After a successful convert, the staged copy is moved into the WAV backup folder when --backup-wav is on; otherwise it is deleted. Inbox sources are still kept unless --delete-originals / --move-processed.
Local/internal inbox folders skip staging unless you pass --stage-local.
# Force staging even when the inbox is already on local disk
inbox-watch --once --stage-local --inbox ~/Drop --recordings ~/Documents/recordings …
# Never stage (ffmpeg reads the inbox path, as before)
inbox-watch --once --no-stage-local --inbox /Volumes/USB-DISK/RECORD …
Terminal feedback¶
Host output mirrors the analysis CLI Review before run / Run summary shape (plain text; the script does not import transcriptx or Rich):
Review before cycle — inbox / recordings / transcripts paths, modes, and candidate file list
Processing —
[i/n] audio|transcript: filename, then indented convert/copy/skip lines; long encodes print elapsed time and stream ffmpegtime=/speed=on stderrTranscription (whispermlx-missing) — when audio mode ran (child process output follows)
Library admit — when
--admitran (python -m transcriptx.admit_originalsoutput follows)Run summary —
Status(completed/partial/failed/dry-run), counts, and limited bullet lists for converted / skipped / failed
Example (abbreviated):
---
Review before cycle
---
Mode: once
Inbox: /Volumes/USB-DISK/RECORD
Candidates: 1 (1 audio, 0 transcript)
Will consider:
• audio: R20260814-175320.WAV
---
---
Processing
---
[1/1] audio: R20260814-175320.WAV
Staging: R20260814-175320.WAV -> R20260814-175320.WAV (… GiB)
Staged: R20260814-175320.WAV (… GiB) in 12.3s
Converting: R20260814-175320.WAV -> R20260814-175320.mp3 (… GiB)
ffmpeg progress on stderr (time=/speed=)…
Converted: R20260814-175320.WAV -> R20260814-175320.mp3 (… MiB) in 123.4s
---
---
Run summary
---
Status: completed
Converted: 1
…
---
Long WAV→MP3 converts can take minutes with little stdout while ffmpeg prints progress on stderr — that is expected. Ctrl-C stops the cycle (Stopped.); a half-written .mp3.partial is discarded on the next failed/interrupted convert. A finished local staged copy is kept so the next cycle can skip the USB copy.
Inbox sources are kept by default. After a successful convert (audio) or copy (transcript):
Option |
Effect |
|---|---|
|
Copy (or move the staged local copy when staging ran) the audio original into the WAV backup folder ( |
|
Delete the inbox source (after backup, if backup was requested and succeeded) |
|
Relocate the inbox source instead of deleting (mutually exclusive with |
|
Overwrite an existing destination stem |
--backup-wav and --delete-originals are independent (use either or both). Deleting with no backup prints a warning. A failed WAV backup skips delete for that file.
Local config (gitignored): copy config/inbox-watch.example.json to .transcriptx/inbox-watch.json. Repo .env is loaded with setdefault (existing shell env wins). Merge order: portable repo defaults ← .env / TRANSCRIPTX_* / INBOX_WATCH_* ← local JSON ← CLI.
Key |
JSON |
Env |
|---|---|---|
Drop folder |
|
|
Recordings |
|
|
Transcripts dest |
|
|
WAV archive |
|
|
Convert audio |
|
|
Copy transcripts |
|
|
Admit to library (default off) |
|
|
Auto-name speakers after admit |
|
|
Auto-link matched profiles |
|
|
Admit interpreter |
|
|
Config path |
— |
|
Stage off removable inbox first |
|
|
Staging folder |
|
|
Also |
|
|
Library admit needs a native TranscriptX install (the JSON/--admit-python interpreter must import transcriptx). It does not enter the Docker analysis container. Set TRANSCRIPTX_TRANSCRIPTS_DIR and TRANSCRIPTX_OUTPUT_DIR to the same host folders Docker mounts so the GUI index stays in sync.
Enable admit in local JSON (and/or .env INBOX_WATCH_ADMIT=1):
"admit_to_library": true,
"admit_python": "/path/to/python3"
macOS login agent (optional): scripts/macos/inbox-watch-agent.sh plus scripts/macos/com.transcriptx.inbox-watch.plist can run --watch at login. The plist is a template (/Users/you/...); copy it to ~/Library/LaunchAgents and replace those paths. The agent script uses the repo .venv / .transcriptx interpreter when present, otherwise python3 on PATH. Admit is controlled by local JSON / .env, not by the plist. If the USB inbox is unplugged, --watch keeps polling empty cycles; the first cycle still catch-up transcribes missing MP3s and admits originals/. Logs: .transcriptx/inbox-watch.launchd.log.
Do not point this inbox at the same folder as the in-app G2 watcher unless you intend both to handle new transcripts (G2 admits; inbox-watch copies). See directory_watcher.md.
Import a whole folder (details)¶
On Import Transcript, section Import all from folder scans an absolute local directory (Docker: mount the host folder — typically HOST_TRANSCRIPT_INBOX_DIR → /mnt/transcript-inbox; do not scan /mnt/transcripts or its subdirs) and imports only eligible files:
Supported extensions:
.json,.srt,.vtt,.txt,.html,.htm(case-insensitive).Eligible statuses: new, incomplete (repairable), needs registration. Already-imported stems, stem conflicts, size/symlink/special-file failures, and unrepairable incompletes are blocked (preview uses human labels).
Skips stems that are already in the library (canonical JSON + import sidecar). Incomplete JSON without a safe
originals/provenance is not treated as a new import.Duplicate stems in the folder (including case variants) are all marked conflict — none are imported.
Source files in the scanned folder are never deleted or modified; the app copies into
transcripts/imports/then admits them.Defaults: 100 MiB per file (
TRANSCRIPTX_FOLDER_IMPORT_MAX_FILE_BYTES) and 500 candidates (TRANSCRIPTX_FOLDER_IMPORT_MAX_CANDIDATES). Exceeding the candidate limit fails the scan closed (Import eligible stays disabled).Preview is invalidated if the path, transcripts root, limits, or admission policy change. Use Rescan; a successful folder import auto-rescans so statuses refresh.
Preview includes a read-only audio column: same-stem companions under approved recordings roots (
found: stem.mp3/none). No automatic copy — upload via section 3 or place matching audio for playback linking.
Programmatic admission with registration under one lock: transcriptx.io.admit_and_register.admit_and_register.
Python API¶
Canonical validation is required for library admission and analysis — see STORAGE.md.
Validate a document already loaded as a dict (raises ValueError if invalid):
import json
from pathlib import Path
from transcriptx.io.transcript_schema import validate_transcript_document
path = Path("path/to/transcript.json")
data = json.loads(path.read_text(encoding="utf-8"))
validate_transcript_document(data)
Import raw or legacy transcript files (e.g. WhisperX JSON, SRT, VTT) into the library:
from pathlib import Path
from transcriptx.core.utils.paths import PATHS
from transcriptx.io.managed_import_workflow import run_managed_import_workflow
result = run_managed_import_workflow(
PATHS.transcripts_originals_dir / "whisperx_output.json",
overwrite=False,
)
print(result.json_path)
print(result.sidecar_path)
print(result.archived_original_path)
The import workflow detects the format, normalizes speakers (missing or empty → SPEAKER_UNKNOWN where applicable), writes canonical schema_version/source/metadata, writes a sidecar, and archives the original source. Web flow does not overwrite existing canonical JSON by default; programmatic callers may opt into overwrite as a new import attempt.
Downstream analysis APIs (for example AnalysisRequest + run_analysis) assume transcript_path is a library transcript produced by this workflow or an equivalent loader. Then analyse from the web interface or via AnalysisRequest + run_analysis (see generated/cli.md).
Other engines (AssemblyAI, Deepgram, Google, manual edits): each segment needs start, end, speaker, and text. Use validate (above) and the import workflow so metadata, sidecar, and archive exist.
Multi-language variants¶
Import alternate-language versions of an existing transcript using a flat filename suffix in the same directory:
Base (default):
meeting.jsonFrench variant:
meeting_fr.jsonEnglish explicit variant:
meeting_en.json(optional;meeting.jsonremains the conventional default English path)
Workflow:
Import and identify speakers on the base transcript first (Speaker ID page, or segment-derived names on import).
Import the language variant via the same path (web Import Transcript or
run_managed_import_workflow).On import, speaker-map inheritance runs automatically when the base has a speaker-map sidecar and the variant does not yet.
Requirements: variant segments should use the same diarized speaker IDs as the base (SPEAKER_00, SPEAKER_01, …). See STORAGE.md for variant rules.
What is copied: display names, ignored speakers, and speaker_id_to_db_id. Each variant gets its own sidecar under metadata/speaker_maps/ (see STORAGE.md).
When inheritance is skipped: the filename is not {base}_{lang}, the base transcript is missing, the base has no speaker-map sidecar, or the variant already has its own speaker-map sidecar (re-import safe).
Fallback: if inheritance does not apply, segment original_cue.original_speaker names are used as on a normal import.