A free, fully local pipeline for branded short-form video: paste a video URL → download it → crop + brand it on a 1080×1920 canvas (your logo, name, @handle, hook text) → schedule it → deliver to YouTube (API upload) and Instagram (Reels API publish), or push it to Android phones over USB for manual posting. Runs entirely on your machine — no VPS, no SaaS, $0/month.
- How it works
- What you need
- First-time setup
- A tour of the app
- Step by step: your first video
- Connecting YouTube
- Connecting Instagram
- Optional: the phone-push lane
- Customizing
- Where your data lives
- Daily workflow
- Troubleshooting
- Costs & limits
Two services run side by side on your machine:
| Service | Port | Role |
|---|---|---|
| Studio (frontend/) | 3000 | The app you work in: link board, brand kit, video editor/canvas, schedule UI |
| Worker (server/) | 8080 | The always-on half: watches the Incoming folder, runs the post queue + scheduler, talks to the YouTube/Instagram APIs, pushes to phones over adb |
You find a clip (YouTube / Instagram / TikTok / X)
│
▼
┌─────────────────────┐
│ Board (Studio) │ ← paste URL + caption idea + notes, per niche tab
└─────────┬───────────┘
▼
┌─────────────────────┐
│ Media (Studio) │ ← yt-dlp downloads the file LOCALLY (your home IP,
│ │ which is why it works where cloud scrapers fail)
│ 1080×1920 canvas │ ← black bg · your logo/name/@handle · hook caption ·
│ │ drag/resize crop box · trim · zoom
└─────────┬───────────┘
▼ Export (WebCodecs render in the browser, ~12 Mbps MP4)
┌─────────────────────┐
│ Worker Incoming │ ← ~/repost-incoming (the watch folder)
└─────────┬───────────┘
▼
┌─────────────────────┐
│ Schedule (Studio) │ ← pick file + destination + date/time
│ Scheduler (Worker) │ ← every 15s: deliver anything due
└─────────┬───────────┘
├─▶ YouTube — official resumable upload (free API quota)
├─▶ Instagram — official Reels publish, bytes uploaded straight
│ from your machine (no public hosting needed)
└─▶ Phone — adb push → you tap "post" on the phone
Everything is stored locally: a SQLite file for the Studio, JSON files for the worker. Both self-create on first run — there is no database to install.
Required (all free):
- macOS / Linux / Windows with Node.js 22.x (
node -vto check;brew install node@22on a Mac) - Python 3 on macOS/Linux (
python3 --version) — yt-dlp ships as a Python zipapp there. Already present on any recent macOS. - That's it. ffmpeg and yt-dlp are bundled as npm packages — nothing to install system-wide.
Optional, per delivery lane:
- YouTube posting — a Google account + a free Google Cloud project (§6)
- Instagram posting — an Instagram professional account (Business or Creator — flip it in the IG app under Settings → Account type) + a free Meta developer app (§7)
- Phone lane — an Android phone with USB debugging + adb (§8)
git clone <your-repo-url> && cd video-automation # skip if you have it
cp .env.example .env # defaults work out of the box — edit later for APIs
npm --prefix frontend install
npm --prefix server install
./start-dev.sh # starts BOTH: studio :3000 + worker :8080Open http://localhost:3000. You should see the Brand Kit landing page.
The worker's log appears in the same terminal; Ctrl-C stops both.
The worker's Incoming watch folder defaults to
~/repost-incoming(created automatically). Override withPHONEDECK_WATCH_DIRin.env.
The left sidebar has five sections:
- Deck — the phone rig (only relevant for the phone lane): connected devices, the Incoming file list, push buttons, phone/theme/account management, scrcpy screen mirroring.
- Brand Kit — your identity, used on every branded post: display name, @handle, verified-badge toggle, and a logo library (uploads are stored locally). No logo yet? Posts render a colored initial-letter avatar.
- Media — the editor. Paste a URL (or upload a local file), fetch, then
position the crop box on the canvas, trim, zoom, write the hook caption
(type
@for the emoji picker). Two formats: Twitter (black background, post-style header — the branded look) and Caption (white, caption-only). Export renders the 1080×1920 MP4 and drops it into the worker's Incoming folder automatically. - Board — a lightweight spreadsheet for triaging clip links before you make anything: URL, caption idea, context, notes, posted/unusable flags. Three niche tabs; rows auto-expire after 5 days. The floating board widget on the Media tab can send a row straight into the editor, and the row is auto-marked Posted once its export is pushed/delivered.
- Schedule — the queue. Pick a rendered file, a destination (YouTube account / Instagram account / phone theme), caption/title and a time. The worker delivers it when the time comes. Failed items show the error and a Retry button. Dry run runs everything except the final publish — use it to test credentials safely.
- Start the app:
./start-dev.sh, open http://localhost:3000. - Brand Kit → set display name + @handle, upload a square logo, Save.
- Media → paste a video URL → press the arrow. Wait a few seconds — the file downloads locally (title/author fill in automatically).
- Position it: drag the box edges to crop, scroll/pinch to zoom, drag the trim handles under the canvas to cut the clip, type the hook caption.
- Press Export and wait for the progress bar. The MP4 lands in
~/repost-incoming(if the worker is down it falls back to a browser download so nothing is lost). - Schedule → New scheduled post → select the file → destination YouTube → add account → Connect this account (do §6 first) → pick a time a few minutes ahead → tick Dry run → Schedule.
- Watch the item flip
pending → deliveredwith "dry run ok". That proves auth + upload plumbing end to end without posting anything. - Re-schedule the same file with Dry run unticked. That one goes live.
One-time, ~10 minutes, free.
- Go to console.cloud.google.com → create a project (any name).
- APIs & Services → Library → search YouTube Data API v3 → Enable.
- APIs & Services → OAuth consent screen → External → fill the two required fields → add your own Google account under Test users.
- APIs & Services → Credentials → Create credentials → OAuth client ID →
Application type Web application → under Authorized redirect URIs
add exactly:
http://localhost:8080/api/oauth/youtube/callback - Copy the Client ID and Client secret into
.env:RestartGOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=..../start-dev.sh. - In Schedule → destination YouTube → add account (type the channel name) → Connect this account → Google sign-in opens → approve → "✅ YouTube account connected."
Two Google gotchas worth knowing:
- While the consent screen is in Testing status, refresh tokens expire after 7 days — you'd reconnect weekly. Fix: OAuth consent screen → Publish app (to "In production"). You do not need Google's verification for your own use; you'll just click through an "unverified app" warning once when connecting.
- Uploads from an API project that hasn't passed YouTube's (free) API audit may be locked private regardless of the privacy setting you send. The upload still works — you can flip visibility manually in YouTube Studio, or submit the audit form to lift it permanently.
One-time, ~15 minutes, free. Your IG account must be professional (Business or Creator).
- Go to developers.facebook.com → My Apps → Create app → choose the setup that includes the Instagram API with Instagram Login product (no Facebook Page needed with this variant).
- In the app dashboard, open the Instagram product's API setup with
Instagram login → under Set up Instagram business login add the
redirect URI exactly:
http://localhost:8080/api/oauth/instagram/callback - Add your account as a tester: App roles → Roles → Add people → Instagram Tester → enter your IG username. Then open Instagram (app or web) → Settings → Website permissions → Apps and websites → Tester invites → accept.
- From the Instagram product's API setup page copy the Instagram app ID
and Instagram app secret into
.env:RestartIG_APP_ID=... IG_APP_SECRET=..../start-dev.sh. - In Schedule → destination Instagram → add account → Connect this account → Instagram sign-in → approve the two permissions → "✅ Instagram account connected."
Notes:
- The app can stay in Development mode forever for your own accounts — Meta's app review is only needed to onboard strangers.
- Reels via API: keep exports 9:16 (the canvas already is), 3s–15min, H.264 — the studio's output ticks all three.
- The long-lived token lasts ~60 days. When deliveries start failing with an auth error, just press Connect this account again.
- The video bytes upload directly from the worker (resumable upload) — your machine needs no public URL, no tunnel.
For accounts you'd rather post manually (or anything the APIs gate):
- On the phone: Settings → About → tap Build number 7× → enable USB debugging in Developer options; plug in via USB and accept the RSA prompt.
- adb on the computer:
brew install android-platform-tools(or use Android Studio's copy) and point.envat it, e.g.PHONEDECK_ADB=~/Library/Android/sdk/platform-tools/adb. - Deck → your phone appears → name it. Create a theme (a group of phones, e.g. one per niche) and add the phone's accounts to it.
- Schedule with destination Phone push + the theme — at the scheduled time the video lands in each phone's camera roll (with a media scan so the gallery sees it instantly). Post it from the phone like any normal upload. Every push is also archived under the worker's reel catalogue folder by date/phone.
scrcpy (optional, brew install scrcpy) gives you on-desktop screen
control of the phones from the Deck tab.
- Niche names — edit frontend/src/lib/niches.ts (one label per line). Board tabs and the widget follow.
- Brand — everything is in the Brand Kit UI; nothing is hardcoded.
- Outro clip — the editor's include edit toggle appends an outro to the
export. Drop your outro at
frontend/public/edit.mp4(1080×1920 MP4) to use it. - Global dry run —
DELIVERY_DRY_RUN=truein.envforces every delivery into dry-run, no matter what the queue item says. Good while testing credentials. - Ports/folders —
PHONEDECK_PORT,PHONEDECK_WATCH_DIR,NEXT_PUBLIC_PHONEDECK_URLin.env.
All gitignored, all local, all recreated automatically if deleted:
| Path | Contents |
|---|---|
frontend/data/studio.db |
SQLite: brand kit, board rows, emoji prefs |
frontend/storage/brand-logos/ |
uploaded logos |
frontend/storage/downloads/ |
downloaded source videos (auto-purged after ~3 days) |
~/repost-incoming/ |
rendered exports awaiting scheduling (the watch folder) |
server/data/queue.json |
the post queue |
server/data/credentials.json |
your platform tokens — never share/commit this |
server/data/store.json |
phones, accounts, themes |
Back up = copy those paths. Reset = delete them.
./start-dev.sh- Triage links into the Board as you find clips during the day.
- Batch-produce: send each row to Media, crop + caption, Export.
- Schedule each export across your accounts at spread-out times.
- Leave the worker running; it delivers on time. Check the queue for green
deliveredrows (each shows the resulting video URL / media id).
| Symptom | Fix |
|---|---|
| Schedule tab says "Worker not reachable" | The worker isn't running — ./start-dev.sh (or npm run dev inside server/) |
| Download fails on a valid URL | Platforms change their sites; update the extractor: npm update youtube-dl-exec in frontend/, restart. Private/region-locked posts can't be fetched. |
| YouTube video uploaded but private | Unaudited-project lock (§6) — flip it public in YouTube Studio or complete the free audit |
| YouTube connect works, deliveries fail after a week | Consent screen still in Testing → publish the app to production and reconnect (§6) |
| Instagram delivery fails with an auth error after ~2 months | Long-lived token expired — press Connect this account again |
Instagram container ends in ERROR |
Check the export: 9:16, ≥3s, H.264 MP4 (studio defaults satisfy this — usually it's a hand-uploaded file) |
| Export finishes but no file in Schedule | Worker was down at export time — the MP4 went to your browser Downloads; drop it into ~/repost-incoming manually |
| Phone push fails | Phone unplugged / USB debugging revoked — check Deck shows state device, re-accept the RSA prompt |
| Port 3000 or 8080 already taken | Stop the other process or change PHONEDECK_PORT / Next's port |
| Thing | Cost | Limit that matters |
|---|---|---|
| Everything local (studio, worker, SQLite, yt-dlp, ffmpeg) | $0 | your disk + the machine must be awake for scheduled deliveries |
| YouTube Data API | $0 | ~6 uploads/day per project on the default 10k quota (more via free quota request) |
| Instagram API | $0 | per-account daily publish cap (Meta-side); tokens refresh every ~60 days |
| Google Cloud / Meta developer accounts | $0 | one-time paperwork |
A word on rights: reposting content you don't own — even cropped and branded — can draw copyright strikes or account removal. The transformation helps but is not legal immunity. Keep the review step; prefer content you have rights to or permission for.