Local-first AI faceless video studio. Desktop + PWA + Android APK from one Flask codebase. No paid APIs. Founding lifetime $79.
Live: https://phantomline.xyz Repo: https://github.com/Kyle-Anson-McCool/phantomline
Phantomline turns ideas into scripts, narration, captions, music, B-roll, SEO packages, and scheduled YouTube posts. Heavy AI inference runs on the user's machine (Ollama + Kokoro + MusicGen) on desktop, or in the browser (WebLLM + Web Speech + Web Audio + ffmpeg.wasm) on the hosted PWA. Either way, no SaaS bill on the AI side.
Phantomline runs the same Flask app in two modes depending on where it's hosted:
- Slim deploy: Flask + numpy + Pillow + requests only (
requirements.txt). - The server is a thin web tier: serves the landing page, license issuance, billing webhooks, and cheap API helpers (YouTube research, channel insights).
- All AI inference happens in the user's browser via WebLLM (Llama 3.2 1B in WebGPU), Web Speech API for TTS, Web Audio for music, and ffmpeg.wasm for MP4 assembly.
- The bundled music pack ships in
static/library/music/for users without WebGPU. - Auto-deploys on every push to
mainviarender.yaml.
- Full deploy: also installs
requirements-desktop.txt(kokoro, transformers, soundfile, lameenc). - Runs server-side AI: Ollama (LLM), Kokoro (TTS), MusicGen via HuggingFace transformers (music), ffmpeg subprocess (video).
- Optional: Forge / AUTOMATIC1111 at
http://127.0.0.1:7861for AI-generated scenes. - This is the "power user" mode where everything is local, including the model weights.
The same server.py boots in either mode. Heavy ML imports (tts, music, video_assembler) are wrapped in try/except so the hosted deploy doesn't crash without them.
git clone https://github.com/Kyle-Anson-McCool/phantomline.git
cd phantomline
# Python venv
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Mac/Linux
# Install web tier + desktop AI tier
pip install -r requirements.txt
pip install -r requirements-desktop.txt
# Pull at least one Ollama model
# (install Ollama first from https://ollama.com)
ollama pull llama3.1
# Set up local env
cp .env.example .env # then edit values
# ^ if .env.example doesn't exist, create .env with:
# GHOSTLINE_LICENSE_SECRET=<openssl rand -hex 32>
# YOUTUBE_API_KEY_2=<optional, for YouTube research routes>
# Run
python server.py
# → http://localhost:5000If you only want the slim web tier (no Ollama / Kokoro / MusicGen):
pip install -r requirements.txt
python server.pyThe server boots, the landing page works, but generation routes will return 503 until you install desktop deps.
phantomline/
├── server.py # Flask app entry. Imports route blueprints,
│ registers them, and handles startup.
│
├── core.py # Shared module-level state — paths, project store
│ singleton, JSON helpers, YouTube connection cache.
│ Routes import from here.
│
├── routes/ # Flask blueprints, one per concern
│ ├── billing.py # License keys, tier gating, Stripe checkout URLs,
│ │ quota enforcement (free tier: 5 renders/mo)
│ ├── system.py # Health checks, model listing
│ ├── launch.py # First-run readiness probes + test render
│ ├── insights.py # Channel analytics ingest (Pro tier)
│ ├── research.py # YouTube keyword research (Pro tier)
│ ├── optimize.py # Per-video repackaging (Pro tier)
│ └── bundles.py # Project import/export
│
├── story_generator.py # Long-form Ollama story engine (CLI + library)
├── tts.py # Kokoro TTS (lazy-loaded; desktop only)
├── music.py # MusicGen + crossfade loop + narration mixdown (desktop)
├── video_assembler.py # ffmpeg subprocess MP4 builder (desktop)
├── projects.py # Persistent project store with atomic writes
├── youtube_publish.py # YouTube Data API v3 client + OAuth dance
├── youtube_research.py # vidIQ-style autocomplete + ranking signals
├── channel_insights.py # Analytics CSV → ranked keyword profile
├── license.py # Offline HMAC-SHA256 license validation
│
├── static/
│ ├── phantomline.css # Studio app styles
│ ├── landing.css # Marketing page styles
│ ├── phantomline.js # Studio app JS (~5000 lines)
│ ├── engines.js # Browser-side AI adapters (WebLLM, Web Speech,
│ │ ffmpeg.wasm, Pexels, bundled music library)
│ ├── manifest.json # PWA manifest
│ ├── sw.js # Service worker (cache-first /static, network-first HTML)
│ ├── phantomline-logo.svg # Brand icon (vector, no text)
│ └── library/music/ # 8 royalty-free ambient tracks (~11 MB total)
│
├── templates/
│ ├── index.html # Studio app shell
│ └── landing.html # Marketing site at /landing
│
├── supabase/
│ └── functions/
│ └── issue-license/ # Deno Edge Function: Stripe webhook → email license
│
├── render.yaml # Render Blueprint declarative deploy config
├── capacitor.config.json # Android APK wrapper config
├── requirements.txt # Web tier deps (slim)
├── requirements-desktop.txt # Desktop AI tier deps (heavy)
└── output/ # User-generated content. Gitignored.
├── projects/ # One folder per saved project
├── projects.json # Project index (atomic writes)
└── publishing/ # YouTube OAuth tokens cache
The project was originally called Ghostline, then rebranded to Phantomline. Filenames, env var names, JS globals, localStorage keys, and the license key prefix (GHL1.) were intentionally preserved during the rebrand to avoid breaking existing installs and license keys. All user-facing strings say Phantomline. When editing, follow the same rule:
- ✅ User-visible text → "Phantomline"
- ❌ File paths, env vars, JS globals, license prefix → leave as
ghostline.*/GHOSTLINE_*/GhostlineEngines/GHL1.
Set these in .env for local dev, or in Render's Environment tab for production.
| Variable | Required? | Purpose |
|---|---|---|
GHOSTLINE_LICENSE_SECRET |
yes | HMAC secret for license signing/verification. Generate with openssl rand -hex 32. Must match whatever issuer (Supabase Edge Function) issues keys. |
YOUTUBE_API_KEY_2 |
for research | YouTube Data API v3 key. Each key is 10k units/day; add YOUTUBE_API_KEY_3, _4, etc. to scale. |
YOUTUBE_CLIENT_ID |
for publish | OAuth client ID for in-app YouTube publishing. |
YOUTUBE_CLIENT_SECRET |
for publish | OAuth client secret. |
GHOSTLINE_TELEMETRY_URL |
optional | If set, error events POST here. Local JSONL in output/telemetry/ works regardless. |
GHOSTLINE_LICENSE_SECRETS_LEGACY |
rotation | Comma-separated old secrets, accepted during rotation windows. |
Never commit .env. It's in .gitignore.
Offline-verifiable HMAC keys. Format:
GHL1.<base64-payload>.<base64-signature>
Payload is JSON: {tier: "pro", email: "...", iat: <unix>, exp: <unix>, lifetime: bool}. Signature is HMAC-SHA256 of the payload with GHOSTLINE_LICENSE_SECRET.
Validation lives in license.py::validate(key). Tier gating uses routes.billing::enforce_tier(required) which returns (ok, error). Quota enforcement uses consume_quota() (free tier: 5 renders/month).
To rotate the secret, set the new value in GHOSTLINE_LICENSE_SECRET and add the old value to GHOSTLINE_LICENSE_SECRETS_LEGACY (comma-separated) until you reissue all customer keys.
main is the production branch. Render's autoDeploy: true ships every push to main. Don't push WIP commits to main — branch first.
git checkout -b feature/whatever
# work, commit, push
git push -u origin feature/whatever
gh pr create --title "..." --body "..."When the PR is approved and CI's clean (no CI yet — TODO), merge to main and Render auto-deploys.
There aren't formal tests yet. Smoke-test by:
python server.pyboots without import errors- http://localhost:5000 loads the studio
- http://localhost:5000/landing loads the marketing page
/api/system/healthreturns{"ok": true}
If any of those break, your change broke something.
The studio app's CSS is static/phantomline.css (~3000 lines) and JS is static/phantomline.js (~5000 lines). Both are intentionally not bundled — they're served as-is and cache-busted via ?v=<mtime> (the versioned Jinja filter in server.py).
If you add a new static asset and the browser doesn't pick it up, bump CACHE_VERSION in static/sw.js to invalidate the service worker cache.
Each route blueprint owns its concern. Keep handlers thin — push logic into the module-level functions (story_generator.py, youtube_research.py, etc.) so they're testable without Flask.
When you add a new blueprint, register it in server.py:
from routes.your_module import your_bp
app.register_blueprint(your_bp)Render reads render.yaml from main and provisions. Push to main → auto-deploy. Logs at https://dashboard.render.com/.
Already wired: phantomline.xyz and www.phantomline.xyz → Render's free tier instance via:
- A record
@→216.24.57.1 - CNAME
www→phantomline.onrender.com
SSL is auto-provisioned via Let's Encrypt.
npm i
npx cap sync android
npx cap open android # opens Android StudioThe wrapper points at https://phantomline.xyz so the APK is just a chrome around the live site. See MOBILE_BUILD.md for the keystore + signing dance.
pip install -r requirements-desktop.txt fails on Windows
PyTorch wheels for some Python versions are flaky. Try Python 3.11 if you're on 3.13.
The hosted server (Render) returns 503 for /api/render
Expected. Hosted is web-tier-only; rendering happens browser-side via engines.js. Use the desktop install if you want server-side rendering.
ollama offline badge on local install
Open the Ollama desktop app, or run ollama serve in another terminal.
Service worker is serving stale assets after a deploy
Bump CACHE_VERSION in static/sw.js. The activate handler clears any cache whose key doesn't match the new version.
Render deploy fails with "Exited with status 1"
Check the Logs tab. 99% of the time it's a missing dep or import error — wrap the offending import in try/except if it's an optional desktop module.
Proprietary. Contact kyle@makko.ai for commercial licensing.
The repo is private — collaborators have explicit access via GitHub. Do not redistribute.