Subtext is one local-first project with two companion modes: a private iPhone-friendly web service for downloading, transcribing, and running lightweight transcript analysis over Tailscale, and a full desktop app for transcript review, Ollama analysis, and exports.
Subtext helps you do two related jobs without sending your media to random web tools:
- Private web service: use Safari on your iPhone to paste a URL, upload a file, transcribe media, run optional preset transcript analysis, or download the original video.
- Desktop app: work locally on your Mac or PC with transcript review, AI analysis, and export tools.
Best when you want an always-on personal media tool you can reach from your phone.
- Paste a supported URL and transcribe it
- Run meme-focused transcript analysis presets from the resulting transcript
- Paste a supported URL and download the original video
- Upload a local audio/video file from Safari
- Reach it privately through Tailscale
Best when you want the full Subtext workflow on one computer.
- Download or import media locally
- Generate transcripts with captions-first + Whisper fallback
- Review and edit transcripts
- Run Ollama analysis and export results
Important: the private web service now supports preset transcript analysis, but it still does not include the full PySide desktop transcript editing or export workflow.
This is the easiest path if your goal is a private always-on service for your phone.
- Install
uv: https://docs.astral.sh/uv/getting-started/installation/ - Install FFmpeg: https://www.ffmpeg.org/download.html
- Install Tailscale on your Mac and iPhone, then sign in on both devices
Make sure ffmpeg and ffprobe are available in your terminal PATH.
uv syncOptional faster backend:
uv sync --extra fasterexport SUBTEXT_SERVER_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
export SUBTEXT_SERVER_HOST=127.0.0.1
export SUBTEXT_SERVER_PORT=8000
export SUBTEXT_MODEL=small.en
export SUBTEXT_ANALYSIS_MODEL=gemma3:4bSUBTEXT_SERVER_KEY is the shared secret your phone sends to the service.
uv run python run_web.pyCheck that it is alive locally:
curl http://127.0.0.1:8000/healthSubtext listens on localhost only (127.0.0.1:8000). Tailscale Serve proxies your tailnet to that port — this is the supported “phone from anywhere” path. It is not a public-internet deployment; only devices on your Tailnet can reach it.
tailscale serve --bg 8000 http://127.0.0.1:8000Then get the Tailnet URL:
tailscale serve statusWhich URL to use: open exactly one of the URLs printed by tailscale serve status (copy from your terminal). Examples look like http://your-mac.tail-xxxx.ts.net:8000 or http://your-mac:8000, with / proxied to http://127.0.0.1:8000. Different Tailscale versions may show https:// or a URL without :8000 — use whatever your serve status lists; that is the supported entry point. Subtext itself still listens only on loopback; Serve is what makes it reachable from other tailnet devices.
Open that URL in Safari on your iPhone (or any browser on your computer) and enter your SUBTEXT_SERVER_KEY.
There is no separate “public” plist or LAN-focused LaunchAgent in this repo on purpose: binding to 0.0.0.0 or same-Wi-Fi URLs is not the default security model.
From Safari on iPhone you can:
- paste a supported media URL and tap
Transcribe - run
Caption Ideas,Hook Rewrites,Title Pack, or a custom prompt on the transcript with a selected humor style - paste a supported media URL and tap
Download Audiofor the best available audio-only stream - paste a supported media URL and tap
Download Video Only - upload a local audio/video file and transcribe it
If the private web service is already running, you can bypass the browser UI and call the same transcribe/download features from the command line. Results are saved to Downloads/ in the Subtext project folder by default.
uv run python -m src.cli transcribe "https://example.com/video"
uv run python -m src.cli transcribe /path/to/local-audio.m4a
uv run python -m src.cli download "https://example.com/video"
uv run python -m src.cli download-audio "https://example.com/video"
uv run python -m src.cli download-list --audio-only crates/morpher_demo_crate.youtube.urls.txtThe CLI uses http://127.0.0.1:8000 and SUBTEXT_SERVER_KEY automatically. To target a Tailnet URL or another local port:
uv run python -m src.cli --server-url "https://your-mac.tail-xxxx.ts.net" --key "$SUBTEXT_SERVER_KEY" transcribe "https://example.com/video"For scripts running outside the repo folder, keep the project explicit:
uv run --directory /Users/copeharder/Programming/Subtext python -m src.cli download "https://example.com/video"The first Morpher demo crate lives at crates/morpher_demo_crate.txt. To check each title against YouTube search metadata and generate review files:
uv run python scripts/resolve_youtube_titles.py crates/morpher_demo_crate.txtThis writes:
crates/morpher_demo_crate.youtube.tsvfor reviewcrates/morpher_demo_crate.youtube.jsonlfor scriptscrates/morpher_demo_crate.youtube.urls.txtfor reviewed URL input
Review the candidates before downloading. The resolver picks a top search result; it does not prove that the result is official, licensed, or the best audio source.
To run the reviewed Morpher crate and write the downloaded file paths to text:
scripts/download_morpher_crate.shThat script reads the private web service key from the LaunchAgent at runtime, downloads the best available audio-only stream for each reviewed URL, and writes crates/morpher_demo_crate.downloaded-files.txt. It does not store the real key in the repo.
Use one tracked template — everything else should match it:
| What | Path |
|---|---|
| LaunchAgent plist (template; edit secret after copy, or use the installer) | scripts/com.subtext.private-web.plist |
Installer (rewrites paths, optional key, installs to ~/Library/LaunchAgents/) |
scripts/install_launchd.sh |
| Wrapper the plist runs | scripts/start_private_web.sh |
Install (from the repo root; pass your key, or set SUBTEXT_SERVER_KEY in the generated plist afterward):
bash scripts/install_launchd.sh "$SUBTEXT_SERVER_KEY"The LaunchAgent only starts Subtext on localhost at login. It does not run tailscale serve for you — run step 5 once (or add your own automation) so your phone still uses the Tailnet URL from tailscale serve status.
Important:
- Label in the template:
com.subtext.private-web(restart withlaunchctl kickstart -k gui/$(id -u)/com.subtext.private-web). - Subtext stays bound to
127.0.0.1:8000by default. - Tailscale proxies traffic privately; the service is not exposed on
0.0.0.0. - Prefer the URL(s) from
tailscale serve status(hostname form) instead of guessinghttp://<100.x.x.x>:8000, unless you know exactly how your tailnet routes that port.
Use this mode when you want the full local review and AI workflow.
uv run python run.pyOr use the launchers:
- macOS:
./mac-run.command - Windows:
win-run.bat
- Install Ollama: https://ollama.com/download
- Pull a model:
ollama pull gemma3:4b
- In the Desktop app:
- open the
AI Analysistab - click
Refresh Models - select a model
- click
Test Model
- open the
- Paste URL(s) or browse local files
- Start download/transcription
- Review the transcript
- Run AI analysis
- Export JSON, Markdown, HTML, PDF, or TXT
- URL transcription
- Local file transcription
- URL video download
- iPhone/Safari access over Tailscale
- Warm-loaded Whisper model for faster repeat requests
- On-demand transcript analysis presets with Ollama
- URL + file queueing
- Transcript review/editing
- Ollama analysis
- Results tab and exports
- Richer local workflow controls
Private web service:
cd /Users/copeharder/Programming/Subtext
uv run python run_web.py
tailscale serve statusLaunchAgent-managed restart:
launchctl kickstart -k gui/$(id -u)/com.subtext.private-web
curl http://127.0.0.1:8000/healthDesktop app:
cd /Users/copeharder/Programming/Subtext
uv run python run.py- Run Desktop app:
uv run python run.py - Run private web service:
uv run python run_web.py - Transcribe from CLI:
uv run python -m src.cli transcribe "<url-or-file>" - Download video from CLI:
uv run python -m src.cli download "<url>" - Download audio from CLI:
uv run python -m src.cli download-audio "<url>" - Download URL list from CLI:
uv run python -m src.cli download-list [--audio-only] "<url-file>" - Resolve demo crate YouTube candidates:
uv run python scripts/resolve_youtube_titles.py crates/morpher_demo_crate.txt - Install faster backend:
uv sync --extra faster - Update dependencies:
uv sync --upgrade - Check Ollama models:
ollama list - Check Tailscale Serve status:
tailscale serve status - Local health check:
curl http://127.0.0.1:8000/health
small.enis the default model because it is a good speed/quality tradeoff for an always-on Apple Silicon service.gemma3:4bis the default transcript-analysis model for the private web service and desktop analysis.- If installed,
faster-whispercan be enabled throughuv sync --extra faster. - Whisper device selection is automatic:
cudawhen availablempson supported Apple Silicon setupscpuotherwise
-
VIRTUAL_ENV does not matchwarning: Useuv run ...from the project folder. You do not need to activate a virtualenv manually. -
FFmpeg missing: Install FFmpeg and make sure both
ffmpegandffprobeare on yourPATH. -
Private service returns
503 Access control is not configured: SetSUBTEXT_SERVER_KEYand restart the service. -
Phone cannot connect: Confirm the web service is running locally, then run
tailscale serve statusand open the listed Tailnet URL. -
YouTube caption rate limiting (
429): Retry later, provide cookies if needed, or let Subtext fall back to Whisper. -
High memory usage: Use a smaller Whisper model such as
small.enorbase.en, and use smaller Ollama models likegemma3:4bfor transcript analysis.
MIT