End-to-end short-form content automation. What started as a Reddit-to-Reels pipeline has grown into a full suite for faceless-video operators:
- Reels — Reddit posts or AI-generated scripts → vertical 1080×1920 video with TTS narration, word-level captions, optional auto B-roll overlay (Pexels-sourced topic-relevant footage), background music auto-picked by tone, and optional auto-publishing to YouTube Shorts / TikTok / Instagram Reels / Snapchat Spotlight.
- Brand Profiles — saved snapshots of every "what this channel looks like" config key (title card, captions, watermark, default voice, BG selector, music tags, auto-broll style). Switch via a header pill before each render — every Generate dialog confirms which brand will style the output, every rendered video is tagged with its brand, and the Videos page filters by brand. Run a multi-channel operation from one install.
- Dialogue Mode — AI generates a back-and-forth between two characters (label A / label B with optional personas, 3-10 exchanges, tone + content-filter axes). Speaker labels stay baked into the captions so viewers can follow the conversation. Renders through the existing Custom Script pipeline (one-click "Render now" or queue), inheriting the active brand's voice / captions / title-card / avatar settings.
- Comment Replier — pulls top-level YouTube comments from your tracked uploads via the API key, AI drafts replies in the active brand voice (skipping spam/hostile/low-quality automatically), you review + edit + post via OAuth — all from inside the suite. Closes the algorithm-engagement loop without leaving the dashboard.
- Content Calendar — schedule Generate-with-AI runs for specific datetimes per brand. A worker fires due slots, auto-switches the active brand, generates one variant, enqueues for render. Replaces the "what's posting tomorrow?" mental ledger when running multiple channels.
- Smart Performance Diagnoses — Performance page gets an LLM-powered "Diagnose" button that compares your top-5 vs bottom-5 vs by-brand averages and surfaces specific patterns (wins / losses / next-5 pitches) with evidence quotes from your actual titles.
- Caption styles: karaoke fill + boxed word — two new FFmpeg-engine-compatible animation modes. Karaoke fill cumulatively colours every word at-or-before the active one (karaoke prompter sweep). Boxed word renders every word inside a coloured pill background (active in highlight, inactive at configurable opacity). Both work on top of every existing caption knob (drop shadow, single-line, per-word highlight, etc).
- Avatar Reels ("PNG-tuber" mode) — upload a stack of character PNGs (idle / talking variants per emotion) and any rendered reel automatically composites the avatar overlaid above captions. Audio amplitude drives mouth swaps, the LLM tags emotional beats from the script for expression swaps, a sine-wave jiggle livens the talking pose, and a slower idle-breathing motion keeps the character feeling alive when quiet. One avatar per brand profile (snapshots with the rest of the brand). Works on top of any pipeline — Generate-with-AI, Custom Script, Reddit, Clip Maker.
- Niche Finder — answers "what channel should I start next?" with real data. Pulls live YouTube
mostPopularchart for your region + per-keyword top-videos for any seed interests you supply, feeds both into the LLM, and returns ranked niche cards (name, description, channel-name suggestions, first-video pitches, audience, saturation estimate, "why this is trending right now" rationale grounded in specific videos from the trend block). One click turns any niche into a new brand profile. - Clip Maker — long-form YouTube URL or uploaded mp4 → AI-picked Shorts-worthy clips. Event-driven mode detects in-game moments (gunshots, goals, HUD events) in footage without a transcript.
- Text Posts — generate tweets, Reddit comments, YouTube / TikTok / Instagram community posts, LinkedIn updates; with brand-voice presets and batch variants.
- Custom Script — paste your own narration text → runs through the same TTS + caption + render pipeline with no AI generation step. Single-shot or batch via the run queue.
- Quote Cards — single-image quote post for IG / X / Pinterest. Type your own quote OR paste any rendered post's id and the LLM extracts the 5 most-quotable lines for one-click reuse.
- Performance Analytics (
/performance) — pulls live YouTube view / like / comment stats for every upload tracked in the suite. Aggregated totals, top performer, 30-day daily-views sparkline, per-video table. - Music Library (
/music) — upload royalty-free tracks, tag each with the same tone vocabulary as Generate-with-AI (dramatic / funny / heartfelt / shocking / cringe). Pipeline auto-picks a matching track per render and mixes it under the narration; voice stays at unity, music attenuates per the configured dB. - Carousel Posts — multi-slide square or 4:5 portrait images for IG / TikTok / LinkedIn carousels. Paste a long story, AI splits it into hook + beat slides + CTA, edit per-slide, live-preview at exact output resolution, download all slides as a zip.
- News Roundup — pull RSS / Atom feeds (curated picks for tech / news / sports / entertainment / Reddit + custom URL), copy any headline + summary as a one-click prompt for Generate-with-AI.
- Hashtag Lab — paste any caption, get 12-20 ranked tag suggestions cross-referenced against top-performing videos in your niche (when a YouTube API key is set). Pick + copy a curated set in one click.
- Social Copy — per-video YouTube title/description + unified Reels/TikTok caption with a background batch queue; click "Social copy" on N videos and come back later.
- First-run wizard — fresh installs auto-redirect to
/setup. Four-step flow picks an AI provider + key, TTS provider + key, optional starter brand, optional YouTube key. Each provider key gets a "Test" button that pings the provider's models endpoint before saving so a typo'd key fails at setup time instead of 30 seconds into the first generation. - Virality scoring + retry loop — Generate-with-AI candidates come back with a 0–100 virality score (sub-scores for hook strength, payoff, structure, and coherence) graded against a structured rubric. The picker sorts and color-codes badges; an optional "Minimum virality" slider tells the backend to regenerate up to N attempts until at least one candidate clears the bar. Plot-hole and meta-label detection automatically caps the score (a story that contains literal
Stakes:labels can't sneak past 35). - Workspace backup — Config → Output → Workspace backup. One-click zip of every config block, brand profile, queue state, music metadata, and per-post social copy. Audio / video / backgrounds are excluded (large + regenerable). Restore on a new machine via the same panel — atomic swap with a timestamped backup of the prior workspace under
.cache/imports/<ts>/.
All rendering is local (FFmpeg). Narration uses your choice of ElevenLabs / Streamlabs Polly / LazyPy TikTok / VibeVoice / Qwen3 TTS. Content generation uses your configured LLM (Gemini / OpenRouter / Ollama / Nvidia NIM).
This project is a fork of FaheemAlvii/reddit-to-reels — credit for the original Reddit-to-Reels codebase belongs there. See Fork Additions below for the feature work that turned it into the Social Automation Suite.
Upstream Notice
The original repo is not actively maintained per its author. This fork has diverged with feature work; the credits and author info at the bottom still apply to the original codebase.
- ElevenLabs provider — live voice fetching from
/v2/voices(no stale hardcoded IDs); stability / similarity / style / speaker-boost sliders; model selection (Multilingual v2 / Turbo v2.5 / Monolingual v1); configs that stored a name instead of a voice_id self-heal on next run. Includes a 21-voice ElevenLabs library preset list so common voices (Rachel, Adam, Bella, Brian, etc.) are always pickable even when the API hasn't been queried yet. - Native per-word timestamps from ElevenLabs — synthesis goes through the
/v1/text-to-speech/{voice_id}/with-timestampsendpoint, which returns the audio PLUS per-character start/end times. We aggregate those into per-word timings and attach them directly to each segment, skipping the whisper re-alignment step entirely. Eliminates the chronic caption-sync drift that whisper's listen-back approach produces on short articles, numbers, and unusual names. Prefersnormalized_alignmentover raw so "25M" read aloud as "twenty-five million" stays synced. ~15–30s faster per render on large-v3. Cached as a<audio>.words.jsonsidecar so Re-render / Resume reuse them without re-hitting the API. Graceful fallback to whisper if/with-timestampsever errors on a segment. Toggle off viatts.elevenlabs.use_native_timestamps: false. - Gendered voice presets — per-provider male/female defaults in the TTS tab. The Run dialog auto-detects narrator gender from the post title/body (regex on
(M32)/28f/as a 24F/my wife|husband) and picks the matching preset. Can be overridden per-run. - Pre-TTS cleanup with local Ollama — expands Reddit shorthand (
tho→though,cuz→because) and proofreads the script before sending to paid TTS. The prompt explicitly instructs the model to fix context-driven wrong-word typos (e.g. "all kids of secretive crap" → "all kinds of secretive crap", "could of sworn" → "could have sworn", "their/they're", "its/it's", "a hole/a whole", "breath/breathe"), unambiguous misspellings, missing apostrophes, and obviously-missing small function words — without paraphrasing or touching slang. Cached per-post. Silently skipped if Ollama is offline. - Pre-TTS prefilter (dedicated module, no LLM call) — expands age/gender tokens (
25M→ "twenty five male", only real ages soM3/F1don't get mangled), TL;DR → "too long; didn't read", and Reddit-subreddit acronyms (AITA/NTA/YTA/ESH/NAH, MIL/FIL/BIL/SIL/DH/DW). Smart-quotes are normalized to ASCII andU+FFFDis stripped so TTS engines don't choke. Doubled-word collapse removes adjacent duplicate function words (and and→and,the the→the,I I went→I went) using a conservative safelist plus a "3+ identical words in a row is always a typo" rule, while leaving emphatic repeats likevery veryandhad hadalone. - Playback speed is actually applied now — the speed slider was cosmetic upstream; I pipe each synthesized clip through FFmpeg
atempo(handles 0.25×–4× via chaining). Whisper alignment runs on the pre-stretch audio and timestamps are scaled by1/speedafterwards, which keeps whisper's accuracy (atempo distorts formants enough to degrade alignment) while still matching the final stretched clip. - FFmpeg concat filter for clip joining — replaces MoviePy's
concat_audioclips, which introduced audible boundary clicks between segments.
- Fully configurable — font (server-side font picker enumerating installed TTFs), size, color, stroke color/width, uppercase, background box, position (center/top/bottom), offset, max width %, words-per-chunk.
- Color picker — native swatch + hex input combo for text, stroke, BG, and highlight colors.
- Animations (MoviePy engine): fade, pop, fade+pop with tunable duration, overshoot, start-scale.
- Whisper forced alignment — optional local
faster-whisper(GPU-aware:cuda+float16when available, elsecpu+int8) produces word-level timestamps so captions snap to the actual spoken words. Cached per audio file (.whisper_v8.json). Model is unloaded + CUDA cache flushed before FFmpeg spawns so Windows doesn't hitWinError 1455(parent-process paging-file reservation). - Hybrid LCS + timing-consistency alignment — whisper hallucinations (
Subtitled by the Amara.org community,CastingWords transcription service,Thanks for watching) are filtered by a deny-list, then the remaining words are matched to the expected text via Longest-Common-Subsequence. A timing-consistency filter (speech-rate sanity check) drops outlier anchors so a misplaced word can't freeze a caption for 15 seconds. Captions always render the expected text — never the hallucinated text — even when whisper goes off the rails. - Per-word highlight — current spoken word rendered in a configurable color, optionally scaled up 100–150% for a TikTok-style bounce. Requires alignment.
- Per-word shrink-to-fit — if one word would overflow the caption width, just that word is scaled down (baseline-aligned with its neighbors); the rest stay at normal size.
- Runaway-caption cap —
max_chunk_durationprevents a single chunk from staying on screen for 15+ seconds when whisper leaves a gap.lead_in_gracecovers brief TTS leading silence. - "Fit on one line" mode — captions.single_line toggle. When a chunk doesn't fit at the base font, uniformly scales the whole chunk's font down until it does instead of wrapping. Fixes mid-word breaks like "CAPTION / S".
- Drop shadow — toggleable soft gaussian-blurred shadow behind captions (color / opacity / offset X & Y / blur radius). Rendered on a separate layer via
ImageFilter.GaussianBlur, then alpha-composited under the real text but above the pill box — so offsets larger than the corner radius spill outside the pill for that true mobile-text-sticker look. Canvas auto-pads by|offset| + 3·blurso the blur never clips. Works with per-word highlight, stroke, single-line mode, and animations simultaneously. The live caption preview simulates it with a matching CSStext-shadowlayer so WYSIWYG stays accurate. - Reddit vs Clip caption presets —
captionsandclip_captionsare independent config keys with their own full settings. A preset switcher at the top of Config → Captions flips which one you're editing; both save together. Clip Maker renders useclip_captions(falls back tocaptionswhen null). - Native-timing-aware highlight frames — when segments carry ElevenLabs
/with-timestampsdata, each per-word highlight frame is anchored to the real[word.start, next_word.start)window instead of the char-weight estimate we use for jittery whisper timings. The pre-existingMIN_FRAME/MIN_FINALguards were designed to swallow whisper's 40 ms flash frames and were silently deleting legit short-word highlights (a / the / I / to) — they're dropped on native runs so every word reliably highlights. - Title card over live background — during the hook + title TTS segments, the Reddit-style card widget is overlaid on top of the running background video (transparent surrounds, so the background keeps moving). Captions start once the body begins. Configurable entry/exit animations (fade, slide_up/down/left/right, fade_slide_*) work in both render engines: the MoviePy engine drives them via clip transforms, and the FFmpeg engine (the default) bakes per-frame translate+opacity into a sequence of overlay PNGs whose durations are snapped to exact 1/output-fps intervals so each input frame aligns 1:1 with an output frame — no jitter from re-sampling.
- Live caption preview — 9:16 mock frame on the Captions tab that reflects every setting in real time, including cycling active-word highlight. Also live-simulates
single_linemode with a CSStransform: scale()that matches what the backend will actually render.
- AI virality scorer — a "Score with AI" button scores up to 40 visible posts 0–100 using your configured LLM (Ollama / Gemini / OpenRouter / NVIDIA NIM). Results cached per-post. New "AI" sort.
- Virality (upvotes/hour), duration estimate (~155 wpm), per-subreddit cap, and fuzzy dedupe of titles against previously-used posts.
- Expanded filter bar — exclude keywords, must-contain, deny-subreddit list, min upvotes, min comments, min viral/hr, max duration, min AI score, hide near-duplicates.
- Filter presets — save/load/delete named filter bundles (persisted per browser).
- New top-level
Clip Makerpage (/clips, keyboardg l). Paste a YouTube URL or upload an mp4; the app auto-downloads via yt-dlp (with a duration cap atclipmaker.max_duration_s, default 1 hour), and pulls English auto-captions when YouTube has them — skipping a whisper pass on the source entirely. Falls back to local faster-whisper transcription when captions are unavailable. - AI clip proposals — the configured LLM reads the transcript with timecodes and returns its top N Shorts-worthy windows with
{start, end, hook_line, reason, score}. Five modes in the dialog:ai_only— transcript → LLM (default)ai_plus— transcript + audio-energy peaks (RMS over 1s windows, FFmpeg-extracted, no numpy) so laughter / shouting / dramatic beats inform the LLM's picksai_visual— adds scene cuts (FFmpeg'sselect='gt(scene,0.3)',showinfofilter, no vision model needed) so the LLM prefers clips that start/end on natural visual cutsevent_driven— no transcript, no LLM. Fuses heuristic detectors (see below) into event peaks and builds pre-roll/post-roll windows around each. Designed for gameplay / sports / dashcam / any footage without narration.manual— skip AI, curate yourself
- Event-driven detection stack (for
event_drivenmode). All signals fuse into 2 s buckets with per-kind weights; buckets with ≥2 kinds get a 1.3× multi-signal boost.- Audio transients — RMS spikes above a rolling-median baseline (not just the loudest windows). Catches gunshots, horns, hit stingers even on already-loud footage.
- Color flash — 1×1 FFmpeg downscale + luma/chroma delta vs 3-frame median. Catches muzzle flashes, damage overlays (red tint), explosions (white-out), goal-celebration colour bursts.
- HUD delta — scene-change filter restricted to a cropped region. Fires when the kill-feed adds an entry / the scoreboard ticks / the minimap pops — no false positives from in-world scene cuts. A "Draw on video" picker lets you drag a rectangle directly on the source player; the overlay auto-pauses the video, crosshair cursor, release commits the fractional
[x1,y1,x2,y2]region. - YAMNet audio tagger (Layer 2) — Google's 521-class AudioSet model running in
tflite-runtimeortensorflow.lite(whichever you have — fully optional). Model + label CSV auto-download tomodels/on first use (~15 MB). Preset class packs forfps/sports/racing/general_action, plus a custom-classes field for any AudioSet substring (e.g.Gunshot, gunfire,Cheering,Whistling,Siren). - Reference-sound template matching (Layer 3) — upload a short WAV of the exact sound you want to catch (goal horn, killstreak jingle, victory sting). Chunked FFT-based normalized cross-correlation at 4 kHz fires wherever NCC ≥ 0.5. Near-perfect recall on specific cues. Managed per-project via a new Upload/List/Delete UI, stored under
clips/<project_id>/refs/. - Windowing — every detected event at time
Tbecomes a clip window[T - pre_roll, T + post_roll](defaults 15 s / 3 s, adjustable inline). Overlaps > 50 % are deduped keeping the highest-scoring window. Whisper alignment is automatically skipped for event-driven proposals so gameplay footage without narration doesn't get hallucinated "Thanks for watching" captions.
- Review UI — embedded source player with the HUD-region picker overlay, clickable transcript cues that seek the player (transcript-based modes), per-proposal cards with score badge + hook line + fired-detector list, inline edit for start/end/title, approve/reject toggle, "Add manual clip @ player time" button, and a live inline preview of each rendered clip without opening a tab.
- Modular render pipeline on the backend (
clip_pipeline.py, composed frompipeline_core):SliceSourceStep(FFmpeg fast-seek + 9:16 crop) →WhisperAlignClipStep(word-level timings on the slice) →RenderClipStep(captions over original audio, no TTS) →ClipThumbnailStep→PersistStep. Each clip renders via the shared run queue — clips and Reddit posts drain through the same worker, withkind: "clip"discrimination so the Dashboard timeline + status bar show clip renders identically. - Independent caption config for clips —
clip_captionskey mirrorscaptions. A preset switcher at the top of Config → Captions flips which one the controls edit; both save together. - Persistent projects — everything lives under
clips/<project_id>/(source mp4,transcript.json,project.json,renders/). Comes back exactly where you left it after a server restart.
- Backgrounds library page (new top-level nav entry, also reachable via
g bor the command palette). A file-browser UI over thebackgrounds/folder — upload multiple videos via picker or drag-and-drop anywhere on the card, with live per-file progress bars. Create nested folders (e.g. minecraft-parkour, subway-surfers, GTA) to organize footage by theme. Per-video autoplay preview on click, per-folder / per-video delete with a recursive-confirm for non-empty folders. Path-traversal guards on the backend reject anything escapingbackgrounds/. - Default-background selector (Config → Video). Dropdown lists every subfolder under
backgrounds/with its video count plus a top-level "🎲 All backgrounds — random" option. The pipeline then resolves thevideo.background_selectorto: specific file → random within folder (recursive) → random across everything, with safe fallback if a saved selector disappears from disk. - Fully customizable title card with a live mini-preview next to the controls (same pattern as the captions preview). Config → Video → Title Card now exposes: circular profile-pic upload (masked onto the card), display handle, hide-stats toggle, 4 color pickers (card background / title text / username text / accent), and 4 dimension sliders (card width % / corner radius / title font size / username font size). Every tweak reflects instantly in the scaled 9:16 mockup, inner padding + avatar radius scale proportionally so the layout stays balanced at any font size, and the backend render reads every value from the
thumbnailconfig block — no hard-coded colors or sizes left.
- Social copy generator — per-video "Social Copy" button generates (a) YouTube Shorts titles (3 variants) + description + tags, and (b) a single Reels/TikTok caption used verbatim on both platforms (they share the same descriptive-caption + hashtag-tail format for Reddit-story videos). The LLM picks one of three explicitly named style templates based on the content: Format A — run-on story summary with ALL-CAPS emphasis on payoffs; Format B —
"only part."cliffhanger teaser + hook question; Format C — punchy single-line hot-take / quoted line. System prompt includes real top-performing examples for each format and enforces a Reddit-TikTok core hashtag tail (≥3 of#reddit #redditstories #redditstorytime #redditreadings #reddit_tiktok #redd #askreddit) plus the subreddit tag + algorithmic reach tags + 2-4 topic tags (8-14 total). Saved toposts/<id>/social.jsonwith per-field copy buttons; legacytiktok/instagramsections from pre-merge saves still render tagged(legacy)and prompt a regenerate. - Social copy batch queue — select any number of videos on the Videos page and hit Social copy in the selection bar; instead of blocking the browser while each LLM call finishes, the backend enqueues them into a persistent
.cache/social_queue.jsonand a dedicated worker drains them one at a time (serial so provider rate limits aren't tripped). A floating Social Copy chip in the lower-right corner shows live counts (queued / running / done / failed) and expands into a list with per-row cancel buttons and error text. Already-queued post_ids are deduped on re-submit. Survives server restart via arunning → queuedrecovery pass on startup. Each video card shows a ✨ Social badge whensocial.jsonexists on disk, so you can tell at a glance what still needs copy. - YouTube benchmarks for social copy — if a YouTube Data API v3 key is configured, the generator first pulls the top-performing short videos in the same niche (query:
r/<subreddit> reddit stories shorts) and feeds their titles / descriptions / tags into the prompt as style references. The LLM is instructed to emulate hook phrasing, tag density, and tone without copying verbatim. Results cached 24h per query (~110 quota units per fresh fetch, ~90 generations/day on the free tier). The dialog shows the referenced videos with view counts so you can see what inspired the output. Graceful no-op if the key is missing or quota is exhausted. - Project registry — fully persistent —
projects.jsonat the repo root now stores every flavor of entry (published, audio-only, fetched, failed), not just successful renders. The in-memoryvideos_dbis dumped to disk after every mutation (pipeline complete, resume, delete, Full Redo, Clear All). Audio +timeline.jsonare preserved undervideos/proj_<id>/even whenauto_cleanupnukesposts/<id>/, so the Videos page survives server restarts exactly as you left it — audio-only rows can still be resumed after a reboot. - Re-render — re-runs just the video step using the persisted audio and aligned timeline, so caption/video settings can be tweaked without re-spending TTS credits. Preview URLs include
?v=<mtime>+ no-cache headers so the new render shows immediately. - Full Redo dialog — re-runs the entire pipeline (fetch → TTS → render) for a post. Lets you force a male/female preset or override the voice for this one run without touching your global config. Deletes old audio/video/workspace first and re-marks the post as eligible for discovery. Explicit cost warning since this does spend TTS credits.
- Delete dialog — two options: "List only" (keeps files) and "Delete files too" (removes the .mp4s, thumbnail, and preserved workspace). Exact-path matching — deleting one video no longer wipes siblings with similar titles.
- Confirmation popups on Re-render, Full Redo, Delete, and Clear All — hard to accidentally nuke hours of rendering.
- Run queue — stage N posts from the Posts page (
Queuebutton on each card), walk away, wake up to N rendered shorts. Background worker drains the queue the moment the pipeline goes idle. Dashboard panel shows current running item with live spinner + elapsed, queued items with up/down/remove, collapsible history with Retry for failures, Pause/Resume toggle. Recovery pass on server start demotes anyrunningitem that got orphaned by a crash. - Resume panel on dashboard — every post that has preserved TTS audio but no rendered video surfaces in an amber-bordered card with one-click per-item Resume + Resume all N that sequences through them.
- Render history chart replaces the old flat stats cards — Today / Last Nd / Success rate / Avg render time plus a clickable 30-day stacked bar chart (blue=success, red=failure). 7/30/90-day toggle.
- Cost tracker panel — live ElevenLabs character balance via their
/v1/userendpoint (tier badge + next-reset date + traffic-light bar), plus a local ledger tracking AI token usage per provider (gemini / openrouter / ollama / nvidia_nim, approx token in/out via chars÷4), plus a 30-day daily-character sparkline. - YouTube publishing + scheduling — one-click upload to YouTube Shorts from any video card. Built-in scheduled release via YouTube's own
publishAtfield, so your server can be offline when the video actually goes live. Batch upload dialog on the Videos page can stage N videos at staggered release times in a single click (a week of Shorts in 30 seconds). Live quota widget on the Publishing tab shows units used today + "~N uploads left" + per-operation breakdown + 14-day sparkline + editable daily limit for users with a Google quota bump.
- Tabbed config page — sidebar navigation (General / Formatting / TTS / Video / Captions / AI Hooks / Publishing / Output & Discord) with a collapse-to-icons toggle, plus URL-backed tab state so the command palette can deep-link into any section.
- Command palette (⌘K / Ctrl+K) — fuzzy-search pages, actions, Config sub-tabs, and the last eight rendered videos by title from anywhere in the app.
- Persistent status bar at the bottom of every page — live pipeline status (current step + detail), backend + Ollama health dots, YouTube quota chip, disk-free gauge with current
videos/footprint. Clickable segments jump to the relevant page. - Keyboard shortcuts —
g h/g p/g v/g cto navigate,/focuses the first search/filter input on the page,?opens a cheatsheet modal. - Unsaved-changes detection — the Config page builds a signature of every editable field and compares to the last saved snapshot. Surfaces as an "Unsaved changes" pill next to both save buttons, an amber callout row in the Subreddits section, and a
beforeunloadwarning so refresh / tab close doesn't silently discard edits. - Videos page batch operations — checkbox on every card; select any → floating action bar appears with Select all published, Social copy (batch generate-social), YouTube (N) (staggered-schedule dialog), Delete (one confirm).
- Pipeline timeline shows per-step elapsed that ticks live while running and freezes on done/error.
start.ps1dev loop — wraps the server with Ctrl+C-restarts-server behavior (double-tap Ctrl+C within 2s to exit). Also checks whether Ollama is listening on:11434at startup and spawnsollama servein a separate window if not — Ollama survives supervisor restarts so you don't reload a 14B model every time you Ctrl+C.dev_supervisor.py— Python supervisor that uses WindowsCREATE_NEW_PROCESS_GROUP+CTRL_BREAK_EVENTso the child uvicorn process actually handles Ctrl+C (upstream's PowerShell loop was no-op on Windows).run_server.py— single-entry dev launcher that mounts the built frontend and runs uvicorn with the backend path correctly resolved.- Masked API-key inputs (
<SecretInput>) — every API key / webhook URL field (ElevenLabs, Gemini, OpenRouter, NVIDIA NIM, YouTube, Discord) uses CSS text-security + explicitdata-1p-ignore/data-lpignore/data-bwignore/data-form-type="other"attributes. Chrome / Firefox / 1Password / LastPass / Bitwarden no longer try to save or autofill them as passwords. Each field has a show/hide eye toggle. - Origin-aware API base URL — frontend uses
window.location.originsolocalhost:8000,127.0.0.1:8000, and LAN IPs all work without cross-origin CORS preflights. Override withVITE_API_URLfor split-port dev setups.
A 30-commit polish pass on top of the feature set above. Grouped by what changed for the user vs. what changed under the hood.
Generate-with-AI quality
- Three-act story structure baked into the prompts — the AI writer now explicitly hits setup → inciting incident → escalation → CLIMAX → resolution, with a self-critique pass that audits the timeline + structure + craft before returning JSON. Banned anti-patterns include literal meta-labels in the body (
Stakes:,The closer hit me…), AITA/TIFU title prefixes, hack-tier filler (viral,epic,insane), unearned emotional shifts, and movie-villain revenge plots. - Variant scorer with explicit caps — every candidate gets a
scoreblock: overall 0-100, hook strength, payoff strength, structure (penalises stacked half-reveals), coherence (catches plot holes / impossible knowledge / unearned shifts). Belt-and-suspenders Python clamp drivesscore = min(score, 35/50/65)based on the lower of structure/coherence so weak local-model self-grading can't smuggle slop past the gate. - Per-feature AI model overrides —
config.gemini.feature_models.<feature>(story_generation / scoring / social_copy / hashtag_analysis / comment_reply / niche_finder / dialogue) lets you pay flagship rates only on the calls that benefit. Surfaced as a section in Config → AI Model; legacygemini.scoring_modelfield still honored. - Custom niches — drop additional content niches into
config.ai_content_generation.custom_niches(same shape as the built-ins:name/subs/themesper id). They appear in the niche grid with a 🏷️ tag glyph next to the built-in entries. - Animated loading sequence — Generate-with-AI shows step-cycling progress (drafting candidate 1/N, proofreading, structure audit, scoring, comparing, retrying) instead of a static spinner; clamps at the last step rather than looping so a stuck request looks honestly stuck.
- Title card vs TTS title — Reddit posts with abbreviations like "Boyfriend (32M)" now render the original "(32M)" on the visual title card while the narration still says "thirty-two, male" (the prefilter already expanded it for natural speech).
- Narrator gender selector actually works — the per-run "Male / Female / Auto" pick now resolves to a built-in default voice for the active provider when the user hasn't configured a per-gender preset (Adam/Bella for ElevenLabs, Matthew/Joanna for Polly). Used to silently fall through to the channel default voice.
Quality of life
- Live updates everywhere — a single
/api/eventsSSE bus pushes state changes (run-queue, social-queue, calendar slot transitions, comment drafts, pipeline step events). Calendar / queue / comment-replier pages auto-update without polling. The 30-60s interval fallback only fires when the SSE stream is disconnected, so a healthy connection costs zero extra HTTP chatter. - Auto-enqueue when busy — clicking render on a Reddit post (or pasting a URL) while another render is in flight now silently enqueues instead of erroring with HTTP 409. The queue worker drains them serially. Toast says "Queued — pipeline busy" with the post title.
- Render-failure diagnostics — when the pipeline fails, the error gets classified into one of 11 categories (
ffmpeg_oom,ffmpeg_codec_missing,font_missing,disk_full,network_timeout,api_quota,api_auth, …) with a user-facing title + hint +recoverableflag. Surfaced via SSE so the queue card shows "Network timed out — usually transient" instead ofRuntimeError: 503. - Pipeline step skipping — Config → Output → "Pipeline Steps" section. Switches to skip thumbnail generation (saves 5-15 s/render) and Discord notify globally per render. Backed by
config.pipeline.disabled_stepsarray; the pipeline checks each gate and emits aSkipped (disabled in config)step status. - Undo toasts — destructive actions (delete calendar slot, clear run-queue history, clear social-copy queue history) defer the actual API call by 5 seconds and offer an inline Undo. Optimistic hide + restore on undo; failed commits roll back automatically.
- Polling fallback gated on SSE — was running 60s polls in parallel with live events; now the interval only fires when
isLiveConnected()returns false. Healthy stream = zero polling chatter. - No more loading-flash on SSE refreshes — Calendar + Comment Replier pages used to flash a spinner on every push event; now the spinner only appears on the initial mount.
Backend / architecture
- JsonLedger — one helper class replaces seven hand-rolled
_load(p)/_save(p, d)pairs acrosssocial_queue,run_queue,content_calendar,comment_replier,cost_tracker,render_history,ai_score_cache. Atomic writes (tmpfile + os.replace), per-path threading lock, mtime-invalidated parsed-dict cache so hot reads (queue snapshots, AI-score lookups) don't re-parse JSON every call. - Routes split —
api_server.pyshrunk by extractingroutes/dialogue.py,routes/niche.py,routes/hashtags.py,routes/calendar.py,routes/social.py,routes/analytics.py. Lazy imports inside handlers so routers don't depend on api_server at module load. - Pydantic on hot routes —
/api/ai/generate-variants,/api/pipeline/run-ai,/api/pipeline/run-custom-scriptvalidate request bodies via typed Pydantic models inapi_models.py. Type/range errors return 422 at the framework boundary instead of silent coercion. - Brand profiles auto-include new config blocks — was a hand-curated allow-list of keys (easy to forget). Now an
EXCLUDE_FROM_BRANDdeny-list lists keys that are app-wide (provider creds, output paths, scraper settings); everything else auto-snapshots per-brand. New features default to per-brand without touchingbrand_profiles.py. - Per-config atomicity —
_save_configis now atomic (tmp + os.replace). Was a direct overwrite; a concurrent reader could see a truncated file mid-write. - Pipeline state typed —
pipeline_stateglobal gets aTypedDictannotation documenting the seven fields it carries (steps, is_running, current_post, started_at, completed_at, error, diagnostic). Typing-only, no runtime change. scripts/smoke.py— 42 fast structural checks: every backend module imports clean, every router (the regression class we hit when first extracting routes/*) loads, every JsonLedger does a roundtrip in a tempdir, every render-diagnostics rule still classifies its example case, the FastAPI app boots and serves health/config/calendar/social/events endpoints under TestClient. Catches import-graph regressions before push.
Frontend perf
- Bundle splitting — main
indexchunk dropped from 740 KB → 165 KB. Vendor deps (react+radix+tanstacktogether asvendor-ui,lucide-react,framer-motion,recharts) split into separate cached chunks via VitemanualChunks. A code change in your app only invalidates the 165 KB index chunk; vendor stays cached across deploys. Total payload roughly the same — but parallelised + cacheable instead of one giant blocking blob. - Replaced
date-fnswith a 30-line custom formatter — saved ~70 KB of bundle for one function (formatDistanceToNow). brandsByIdMap memo on Calendar + CommentReplier — wasbrands.find()inside a.map()per row, O(N×M) on every paint.- Lazy-loaded routes — every page is a separate chunk loaded on demand (5–50 KB each).
A second polish pass focused on first-five-minutes UX, mid-pipeline operator control, and a couple of long-standing render papercuts.
Operator-edit gate (Script Review)
- Optional pause between Format and TTS — toggle
config.pipeline.script_review_enabledand the pipeline halts after preprocessing (prefilter + Ollama normalize) so you can fix OP typos / awkward phrasing / weird normalization artifacts before paying for voice generation. A modal pops up automatically with editable textareas for the title, body, and each comment, plus per-field char counters and a rough spoken-minute estimate. Approve & continue ships the edits to TTS; Cancel render aborts cleanly with no half-written audio. - 'Write a hook' button — one-click rewrite of the title via your configured AI model into a single punchy spoken-hook sentence (Reddit prefixes like
AITA/[Update]stripped). An Undo rewrite button appears next to it for one-click revert; manual edits clear the undo target so it only ever reverts the most recent AI action. - Per-run skip on Full Redo — the dialog gets a "Skip script review for this run" toggle (only visible when the global flag is on) so unattended overnight redos don't pause waiting for human input. Step status reads "Skipped per-run (Full Redo override)" so you can see why it didn't fire.
- Global modal mount + dashboard banner — the review dialog auto-opens regardless of which page you're on when the pipeline pauses (originally it only worked from the dashboard). The dashboard's Pipeline panel still gets an inline "Open editor" banner for the contextual case.
- 30-min defensive timeout — a forgotten review won't tie up the queue forever. Wrapped in try/except so any bug in the optional gate falls through to "skipped due to error" rather than killing an otherwise-fine render.
Title-card visual upgrade
- Configurable border —
config.thumbnail.border_color+border_width(0–16px, 0 = off / existing renders unchanged). Drawn on the same rounded rect as the card fill so the corner radius matches exactly. - Entry + exit animations — independent dropdowns:
none/fade/slide_up/slide_down/slide_left/slide_right/fade_slide_up/fade_slide_down. Cubic ease-out on entry, ease-in on exit; both phases auto-capped at 40% of the title segment. Live preview on the Config → Title Card panel uses the same easing math as the backend so what you configure is what you ship. Defaults tofadeon both sides — clear upgrade over the previous hard cut. - Animation no longer judders — both engines now force 30 fps end-to-end. The MoviePy path was already fine; the FFmpeg path's background re-encode used to inherit the source video's native framerate (often 24/25), and the overlay filter inherited that too — so even with
-r 30on output the animation was being sampled at 24/25 Hz and 3:2-pulldowned into a stutter. Background re-encode now appliesfps=30 + -fps_mode cfr, the FFmpeg engine renders per-frame title-card PNGs with translate+opacity baked in at 1/30s frame durations, and the final composite explicitly resamples both inputs to 30 fps before overlay.
Onboarding surface (5-piece bundle)
- Quick-Start checklist on the dashboard — auto-derives status from config + stats (AI configured, TTS, brand, first render, optional YouTube). Each row deep-links to the right Config tab; card auto-hides on full completion or "Hide forever". No manual checkbox state to maintain.
- In-app Guide page (
/guide) — six expandable sections (first video, AI providers, brands, post-filter explanations, voice/TTS, troubleshooting) written as plain prose, not a config-schema dump. Linked from the checklist + aBookOpenheader icon. Includes a "Reset tips" button that wipes the seen-set so first-time toasts fire again. - Notification center —
Bellicon in the header with unread badge. Subscribes torender.completevia the existing SSE bus and persists to localStorage so refreshes don't lose state. Cross-tab dedupe via a 3-second signature window so multiple open tabs don't multiply notifications. Click any row to navigate; mark-read / clear-all controls; max 30 retained. - First-time tips —
useFirstTimeTip(id, title, description)hook fires a one-shot toast per browser, ever. Two shipped: dashboard welcome (points at/guide) and Posts page (explains the Score-with-AI / AI-sort dependency). One-line addition per page. - Contextual help popovers —
<HelpHint>{children}</HelpHint>primitive that renders a(?)icon → popover with a paragraph of plain-prose explanation. Wired onto Min viral (▲/hr) and Min AI score in Posts (the two filter labels new users most reliably squint at). Reusable; sprinkle in wherever a question keeps coming up.
Pre-TTS quality
- Pre-TTS proofreader — expanded the Ollama normalization prompt with concrete examples for context-driven wrong-word typos (
all kids of→all kinds of,defiantly→definitely, etc.) so the model catches them instead of normalizing to a confidently-wrong narration. Plus a deterministic doubled-word collapse (and and,the the,I I) that runs before the LLM call so an obvious typo never wastes an API request. - Silent video-failure detection — when both render attempts return
None(rather than raising), the pipeline now preserves the audio + timeline tovideos/proj_<post_id>/, raises a recoverablevideo_silent_failurediagnostic, and auto-enqueues akind: "resume"job that runs the video step ONLY against the cached audio (no TTS re-spend). Was previously silently inserting anaudio_onlyrow with no error and forcing the user to manually click Resume.
Operator UX polish
- Theme contrast audit — both dark and light modes had layers that were ~1% L apart and effectively invisible (cards on background, borders on cards). Re-tuned: dark goes
bg 6 → card 11 → secondary 16 → border 24; light goesbg 98 → card 100 → secondary 92 → border 80. Every layer now has a clear lightness relationship with its neighbours. - Post Discovery filters collapsed — the filter section had grown to 8 numeric + 5 text + 3 narrator + 2 switches all visible at once; pushed the post grid below the fold. Now hidden behind a
Filterstoggle in the toolbar with an active-count badge so it's obvious how many constraints are silently narrowing the list. Hide-near-duplicates promoted up to the always-visible toolbar. - AI Hooks tab renamed
AI Model— the tab + section were both named after the smallest feature inside them (intro hooks), which made new users think they were configuring just the hooks rather than the master AI provider. Tab is now AI Model, master toggle is Enable AI with a one-line explanation of what features it powers, and the two intro-hook / thumbnail-text feature toggles are visually subordinate as "Optional AI add-ons" below the Test button. - Stale-chunk error recovery after deploy — a tab loaded before a rebuild used to fail with
TypeError: error loading dynamically imported modulewhen navigating to a lazy route whose hashed filename had changed. Two-prong fix: proactivevite:preloadErrorwindow listener auto-reloads once per session via asessionStorageguard; reactiveRouteErrorBoundarydetects the same shape and shows a friendly "A new version was deployed — Refresh now" card instead of a blank page. - HashRouter
?tab=Xdeep-links — under HashRouter the query string lives inside the hash fragment, sowindow.location.searchis always empty. Switched the Config page's tab parser to react-router'suseSearchParams, plus an effect that re-syncsactiveTabwhen the URL changes mid-session so clicking a different checklist row already on the Config page moves to the right tab. - Dropdown desc text contrast — Library + Engage header dropdowns hardcoded their per-row descriptions to
text-muted-foreground— fine on idle, near-invisible on the saturated purple hover/active background. Replaced withopacity-70so the desc inherits whatever color the parent currently uses. - Console rebrand + loopback bind —
dev_supervisor.pybanner now reads "Social Automation Suite dev loop" (was upstream's "Reddit-to-Reels");run_server.pybinds to127.0.0.1instead of0.0.0.0so uvicorn prints a clickable URL on Windows.
Bug fixes worth flagging
python-multipartmissing frombackend/requirements.txt— FastAPI raises at app-construction time when anUploadFile/Form()endpoint is registered without it. The profile-pic-upload endpoint usesUploadFile, so a fresh install crashed on import.
PROJECT_ROOToff-by-one — every module inbackend/src/computed this onedirnameshort of the repo root, which made the stock server silently read a stalebackend/config.json. Fixed across all 13 modules.- Caption overflow on wide fonts — upstream used a
fontsize * 0.5estimate for wrap width; display fonts like Gotham Ultra overflowed the frame. Rewrote withfont.getlength()pixel-accurate wrapping and stroke-aware canvas sizing. - Title card appearing too late and then vanishing — title/hook segments now carry a
segment_role: "title"tag so the card shows for the whole hook block and captions only engage once the body begins. - Delete endpoint substring-match — previously
if video_id in filename:deleted all posts with overlapping characters. Replaced with exact-path matching. - Caption drift across segment boundaries — the MP3 header duration reported a longer total than the actual FFmpeg-concatenated output by ~37 ms per segment. Fixed by rescaling per-segment durations to the measured concat duration (
effective_durs). - Video duplicated 3× on Videos page —
projects.json+posts/scan + loosevideos/*.mp4could each claim the same render. Scan passes now skip ids already invideos_db, and Full Redo deletes old mp4s before re-rendering. - Infinite recursion in TTS chunk splitter — smart-join of orphan punctuation occasionally produced a segment just over
MAX_TEXT_LENGTH, which re-entered the splitter on the same input. AddedHARD_SPLIT_OVER = MAX_TEXT_LENGTH * 1.5and a progress guard. - Windows
WinError 1455during render — faster-whisper large-v3 leaves ~5 GB committed on CUDA.CreateProcesson Windows pre-reserves swap equal to parent committed pages, so FFmpeg's concat subprocess failed with "paging file too small". Fix: unload the whisper model +torch.cuda.empty_cache()before Step 4.
Copy config.json.example to config.json on first run. All new keys have defaults.
Posting to YouTube Shorts:
- Google Cloud Console → APIs & Services → Credentials → Create OAuth 2.0 Client ID → application type Desktop app (same project where you enabled the YouTube Data API v3).
- Paste the
client_id+client_secretinto Config → Publishing → YouTube Shorts, click Save credentials, then Connect YouTube — a browser popup opens, you consent, it closes itself, and the panel shows "Connected as @yourchannel". - During testing / before Google app verification: add your Gmail address under Google Auth Platform → Audience → Test users. Refresh tokens in test mode expire after 7 days.
- Upload: click the red YouTube button on any published video card, edit the title/description/tags (pre-filled from
social.json), pick Public / Unlisted / Private or flip Release later and choose a time — scheduled releases fire entirely on YouTube's side, so your machine can be offline.
How to run (Windows):
# One-time: create venv + install backend deps
py -3.11 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r backend\requirements.txt
pnpm install
pnpm build
Copy-Item config.json.example config.json
# Daily dev loop (Ctrl+C restarts, Ctrl+C x2 exits)
.\start.ps1Open http://localhost:8000.
If you use faster-whisper with the large-v3 or distil-large-v3 model on Windows and see [WinError 1455] The paging file is too small for this operation to complete when FFmpeg spawns, set your pagefile to system-managed (or at least 16–32 GB fixed) and reboot. Root cause: Windows' CreateProcess pre-commits swap equal to the parent process's committed pages, and the whisper CUDA arena bloats Python enough to trip tiny pagefiles. The code also proactively unloads the whisper model before the render step — the pagefile tweak is belt-and-suspenders.
# Run elevated
wmic computersystem set AutomaticManagedPagefile=True
# Then reboot.Maintenance Notice
This repository is not actively maintained. It works at the time of writing and may receive occasional updates when time permits. Pull requests are welcome but reviews can be slow. For paid features or guaranteed support see the contact section below.
Social Automation Suite fetches content from Reddit or generates original scripts with a configured LLM, converts the text to speech, and renders vertical 1080×1920 shorts with word-level captions over a background clip. Beyond the Reddit-reels pipeline, it ships a Clip Maker (long-form → Shorts with AI / heuristic / event-driven proposal modes), a Text Posts page (tweets / comments / community posts / LinkedIn, with brand-voice presets and variant batches), a background Social Copy queue (YouTube + unified Reels/TikTok caption generated for N videos at once), and auto-publishing to YouTube Shorts with scheduled release. It includes a web dashboard, a CLI for terminals (including A-Shell on iOS), and a desktop build for Windows.
Real, in code features. No filler.
- Fetch Reddit posts from configured subreddits with filters for length, score, and age.
- AI content generation in four styles: Story, Q&A, Interactive "put a finger down", and Hot Take. Each run is shaped by an orthogonal content filter (Safe / Normal / Edgy), tone (Dramatic / Funny / Heartfelt / Shocking / Cringe), and free-text target audience (e.g. "women 18-35", "teenagers").
- Save any style + niche + filter + tone + audience combo as a named preset for one-click reuse. Every Generate-with-AI run goes through a Script Review screen — you read the candidate(s), regenerate the ones you don't like (per-card 🔄 or "Regenerate all"), and approve before anything renders. Pick 1, 2, 3 or 5 candidates per click; if you approve a single one it fires the pipeline immediately, if you tick multiple it queues each as its own run on the existing run queue (drains serially through the same worker that handles Reddit posts).
- Text Posts page for generating tweets, X threads, Reddit comments/posts, YouTube community posts, LinkedIn/Facebook/Instagram/TikTok posts, and long-form openers — 11 built-in formats with platform-aware character limits. Same filter/tone/audience axes as the video pipeline. Ground posts in real sources by pasting text or fetching a URL. Save reusable brand voices (your persona, recurring bits, words to avoid) for one-click consistency across runs. Generate 3 variants at once, rewrite any draft with a feedback instruction ("punchier hook", "drop the hashtags"), and visualize X threads as individual tweet-shaped cards with per-post character counts.
- Multiple AI providers: Google Gemini, OpenRouter, Ollama (local and cloud), and Nvidia NIM.
- Handles Ollama reasoning models that return answers in the
thinkingfield. - TTS via Streamlabs Polly (cloud, free) or VibeVoice (local).
- Two video engines: FFmpeg (lightweight, works on iOS A-Shell) and MoviePy (full Python).
- Blank color background fallback when no background videos are present.
- Thumbnail generation for each video.
- Optional publishing to YouTube Shorts, TikTok, Instagram Reels, Snapchat Spotlight (implementation present, not end to end tested).
- Discord webhook notifications for run status.
- Web dashboard built with React, Vite, Tailwind, and shadcn/ui.
- CLI / TUI entry point for terminals and iOS A-Shell.
- Single file desktop EXE build using PyInstaller and PyWebView.
- Autonomous bot mode that rotates through channel configs.
- Resume from audio: re-render the video step from existing audio files when rendering fails.
- 7 step pipeline tracking in the UI: AI Generate, Fetch, Format, TTS, Video, Thumbnail, Notify.
| Platform | Status |
|---|---|
| Local file output (MP4) | Tested, works |
| Discord webhook | Tested, works |
| YouTube Shorts | Implemented, not end to end tested by author |
| TikTok | Implemented, not end to end tested by author |
| Instagram Reels | Implemented, not end to end tested by author |
| Snapchat Spotlight | Implemented, not end to end tested by author |
If you successfully use any publisher, a confirmation in an issue or PR is appreciated.
- Frontend: React 18, Vite 5, TypeScript 5, Tailwind CSS 3, shadcn/ui, TanStack Query.
- Backend: Python 3.11, FastAPI, Uvicorn.
- Media: FFmpeg, optional MoviePy and Pillow.
- AI: Google Gemini, OpenRouter, Ollama, Nvidia NIM.
- TTS: Streamlabs Polly, VibeVoice.
Install Node 20, pnpm, Python 3.11, and FFmpeg.
# Frontend
pnpm install
pnpm dev
# Backend (in a second terminal)
python3.11 -m venv .venv
source .venv/bin/activate # Windows: .\.venv\Scripts\activate
pip install -r backend/requirements.txt
uvicorn api_server:app --app-dir backend/src --reload --port 8000Open http://localhost:8080 for the dashboard. The API runs on http://localhost:8000.
The included Dockerfile builds the frontend, copies it into a Python 3.11 image with FFmpeg, and serves both from one container on port 8000.
# Optional: create config files first (copy from examples)
cp config.json.example config.json
cp channels.json.example channels.json
# Build and run
docker compose up -d --buildThe dashboard is then available at http://localhost:8000.
docker-compose.yml mounts these directories so data survives container rebuilds:
| Host path | Container path | Purpose |
|---|---|---|
./posts |
/app/posts |
Generated posts, audio, summaries |
./videos |
/app/videos |
Final rendered MP4 files |
./backgrounds |
/app/backgrounds |
Background MP4 clips for video rendering |
./audio |
/app/audio |
Audio scratch space |
./config.json |
/app/config.json |
AI providers, TTS, publishers |
./channels.json |
/app/channels.json |
Bot channel rotation config |
pnpm install
pnpm build
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r backend/requirements.txt
# Serve the built frontend and the API together
uvicorn api_server:app --app-dir backend/src --host 0.0.0.0 --port 8000Place your config.json and channels.json at the project root and put background videos in backgrounds/.
This section explains every important setting in plain English. Configure either through the web dashboard at /config or by editing config.json directly.
| Setting | What it does | Default |
|---|---|---|
ai_provider |
Which AI to use for content generation | gemini |
formatting.default_mode |
Story style: story, qa, interactive, hottake |
story |
formatting.default_niche |
Topic category for AI generation | relationship_drama |
tts_engine |
Voice synthesis backend | streamlabs |
tts_voice |
Streamlabs Polly voice name | Matthew |
video.engine |
ffmpeg (light) or moviepy (full Python) |
ffmpeg |
video.split_duration |
Seconds per part when splitting long videos | 60 |
reddit.min_score |
Minimum upvotes to consider a Reddit post | 100 |
bot.posting_interval_minutes |
Minutes between bot runs | 60 |
Set formatting.default_mode (or pick in the AI Generation dialog) to control the style of generated scripts.
story — A narrated first person Reddit style story. Best for r/AmITheAsshole, r/relationships, r/MaliciousCompliance type content.
"So this happened last week. My sister in law showed up to my wedding wearing a white dress..."
qa — A short host question followed by a punchy answer. Best for r/AskReddit style hooks.
"What is the most expensive mistake you ever made? Mine was clicking 'reply all' on a 12,000 person email chain..."
interactive — "Put a finger down" challenge format. Hooks viewers by asking them to play along.
"Put a finger down if you have ever pretended to text someone to avoid a conversation. Put another finger down if..."
hottake — Opinionated short rant, controversial take, or unpopular opinion. High engagement bait.
"Pineapple on pizza is not the problem. The problem is people who put it on with ham and call it Hawaiian when..."
| Provider | Type | Cost | Speed | Recommended models |
|---|---|---|---|---|
| Gemini | Cloud | Free tier generous | Fast | gemini-2.0-flash, gemini-1.5-flash |
| OpenRouter | Cloud | Pay per token | Fast | meta-llama/llama-3.3-70b-instruct |
| Ollama | Local or cloud | Free local / paid cloud | Varies | llama3.1:8b, kimi-k2.5:cloud, deepseek-r1 |
| Nvidia NIM | Cloud | Free tier available | Fast | meta/llama-3.3-70b-instruct |
Reasoning models like deepseek-r1 and kimi-k2.5 return answers in a separate thinking field. The engine handles this automatically.
formatting.default_niche picks the topic the AI generates around. Built in options:
relationship_drama— Couple fights, dating disasters, in law conflicts.family_secrets— Hidden adoptions, infidelity reveals, inheritance feuds.workplace_chaos— Bad bosses, coworker drama, malicious compliance.childhood_nostalgia— 90s and 2000s memories, school stories.revenge_stories— Petty and pro revenge tales.wedding_disasters— Bridezillas, wedding crashers, mother in law drama.school_memories— Teacher stories, prank wars, awkward moments.roommate_horror— Bad roommates, lease nightmares.dating_apps— Tinder fails, ghosting stories, first date disasters.customer_service— Karens, retail wars, restaurant horror.
Streamlabs Polly (cloud, free) — Default. No setup, no API key. Voices include:
Matthew, Brian, Joanna, Salli, Joey, Justin, Kendra, Kimberly, Ivy, Amy, Emma, Russell, Nicole.
VibeVoice (local) — Runs on your machine, no internet required, no per request cost. Heavier setup, requires GPU for reasonable speed. Does not work on iOS A-Shell.
Set tts_engine to streamlabs or vibevoice and tts_voice to your chosen voice.
video.engine—ffmpegis fast, low memory, works on iOS A-Shell.moviepyis full Python, more flexible but heavier.video.split_duration— Long scripts are split into N second parts. Default 60 seconds matches Shorts and Reels.video.mode—singlekeeps everything in one file.splitbreaks it into parts.video.branding— Optional intro / outro overlay text or watermark.video.auto_cleanup— When true, removes intermediate audio and frames after rendering succeeds.
Configured under reddit in config.json:
subreddits— List of subreddit names to fetch from.min_score— Skip posts below this upvote count.max_age_days— Skip posts older than N days.min_length/max_length— Character limits on the post body.nsfw— Include or exclude NSFW posts.
All publishing modules are implemented but not end to end tested by the author. Expect to debug auth, scopes, or upload edge cases.
- YouTube Shorts — Requires Google Cloud OAuth 2.0 credentials. See
backend/src/youtube_publisher.pyand the YouTube Data API v3 docs. - TikTok — Requires a TikTok Developer App approved for the Content Posting API. See TikTok Content Posting API.
- Instagram Reels — Requires a Facebook App with
instagram_content_publishpermission and a Business or Creator account. See Instagram Graph API. - Snapchat Spotlight — Requires Snap Kit approval. See Snap Marketing API.
Credentials for each platform go in channels.json per channel.
Set discord_webhook_url to a webhook URL from any Discord channel (Server Settings, Integrations, Webhooks). The engine posts run start, success, and failure messages there.
The autonomous bot rotates through channels in channels.json.
bot.posting_interval_minutes— How often the bot wakes up and tries to post.bot.max_posts_per_day— Hard daily cap across all channels.bot.channels— Each channel has its own subreddits, AI prompt, TTS voice, and publishing targets.
{
// Pick one: gemini, openrouter, ollama, nvidia_nim
"ai_provider": "gemini",
"gemini_api_key": "AIza...",
"openrouter_api_key": "",
"nvidia_nim_api_key": "",
// Ollama: local URL or https://ollama.com for cloud
"ollama_url": "http://localhost:11434",
"ollama_model": "llama3.1:8b",
// TTS
"tts_engine": "streamlabs", // or "vibevoice"
"tts_voice": "Matthew",
// Video rendering
"video": {
"engine": "ffmpeg", // or "moviepy"
"mode": "split", // or "single"
"split_duration": 60, // seconds per part
"auto_cleanup": true,
"branding": ""
},
// Content style defaults
"formatting": {
"default_mode": "story", // story | qa | interactive | hottake
"default_niche": "relationship_drama"
},
// Reddit fetching
"reddit": {
"subreddits": ["AmItheAsshole", "relationships", "tifu"],
"min_score": 500,
"max_age_days": 30,
"min_length": 500,
"max_length": 4000,
"nsfw": false
},
// Publishers (all untested, see Publishing Status)
"publishers": {
"youtube": { "enabled": false },
"tiktok": { "enabled": false },
"instagram": { "enabled": false },
"snapchat": { "enabled": false }
},
// Notifications
"discord_webhook_url": "",
// Autonomous bot
"bot": {
"posting_interval_minutes": 60,
"max_posts_per_day": 6
}
}The CLI works on A-Shell (iOS, Python 3.13) and any standard terminal.
pip install requests Pillow
# Optional pretty TUI
pip install rich
python backend/src/cli_app.pyWhat works on iOS:
| Feature | Status |
|---|---|
| Fetch Reddit posts | Yes (needs requests) |
| Format / script gen | Yes |
| TTS (Streamlabs cloud) | Yes (needs network) |
| Video render (FFmpeg) | Yes (A-Shell bundles FFmpeg) |
| Thumbnails (Pillow) | Yes if Pillow installed |
| Video render (MoviePy) | No (moviepy/numpy not on iOS) |
| Local TTS (VibeVoice) | No |
py -3.11 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r backend\requirements.txt pyinstaller pywebview
pnpm install
pnpm build
.\.venv\Scripts\python.exe -m PyInstaller backend\src\desktop_app.py --name reddit-reel-desktop --onefile --add-data=dist:dist --copy-metadata=imageio --copy-metadata=imageio-ffmpegOutput: dist\reddit-reel-desktop.exe.
backend/
src/
api_server.py FastAPI server
main.py Pipeline orchestrator
auto_bot.py Autonomous bot
ai_content_generator.py
reddit_story_maker.py
tts_engine.py
video_generator.py
cli_app.py CLI / TUI entry point
desktop_app.py PyWebView desktop wrapper
requirements.txt All Python deps in one file
src/
components/ React UI
pages/ Route pages
hooks/
lib/api.ts API client
public/
Dockerfile
docker-compose.yml
LICENSE CC BY-NC 4.0
AUTHORS.md
README.md
Install Node 20, pnpm, Python 3.11, and FFmpeg. Run pnpm install && pnpm dev for the frontend and pip install -r backend/requirements.txt && uvicorn api_server:app --app-dir backend/src --reload for the backend.
Yes. Ollama is supported for local models. Reasoning models like deepseek-r1 and kimi-k2.5 are handled correctly by reading the thinking field returned by the Ollama API.
No. The license is CC BY-NC 4.0 which prohibits commercial use. Contact the author at faheemalvi2000@gmail.com for a commercial license.
Streamlabs Polly works since it is a cloud HTTP API. VibeVoice (local TTS) does not run on iOS.
The FFmpeg engine generates a solid color background automatically when the backgrounds/ directory is empty, so rendering still succeeds.
Run docker compose up -d --build. The dashboard and API are served on port 8000. Mount config.json, channels.json, backgrounds/, posts/, and videos/ as volumes for persistence.
The code is implemented for YouTube Shorts, TikTok, Instagram Reels, and Snapchat Spotlight, but the author has not personally completed end to end uploads on a live account. Expect to debug platform credentials and edge cases. Local MP4 output and Discord notifications are tested and working.
Four modes: story (first person Reddit narratives), qa (question and answer hooks), interactive ("put a finger down" challenges), and hottake (controversial opinions). Each is documented in the Configuration Guide above.
Licensed under CC BY-NC 4.0 (Creative Commons Attribution NonCommercial 4.0). You may use, modify, and share this project for personal, educational, and non-commercial purposes. Selling, reselling, or using this project as part of a commercial product or paid service is not permitted without a separate written agreement. See LICENSE.
Faheem Alvi
- GitHub: https://github.com/FaheemAlvii
- LinkedIn: https://www.linkedin.com/in/faheem-alvi
- Email: faheemalvi2000@gmail.com
Open to collaboration, freelance projects, and paid work in Python, FastAPI, React, FFmpeg pipelines, and AI integrations. Email is the fastest way to reach me.
This project is provided for educational and personal use. You are responsible for following the terms of service of any platform you publish to (YouTube, TikTok, Instagram, Snapchat, Reddit) and for the content you create with it.