Inspiration
I run faceless quote channels, and every video was the same four chores: pick a quote and background I hadn't used yet, edit it to 9:16 in an external tool, write a title/description/hashtags that don't scream "auto-generated," then upload and remember to post it at the right hour. At 10 videos a day across 3 channels, that stopped being a hobby and became a second job. I wanted the whole loop collapsed into one HTTP request — or a scheduled entry that fires while I sleep.
What it does
Quotient is a self-hosted Flask app that automates the entire short-form content pipeline for quote/faceless channels:
- Renders 9:16 vertical video from a brand's asset pool — resizes and letterboxes backgrounds, lays styled text with a background box and watermark, matches audio length to video length via FFmpeg, and muxes the result.
- Never repeats itself. Every brand keeps a history of recently used quote/image/audio combinations, so successive videos don't reuse the same pairing.
- Generates captions with AI. Google Gemini writes the title, description, and hashtags from the quote and channel identity. Multiple API keys rotate automatically on quota errors, and a local Ollama model can be used instead for zero-cost, fully offline generation.
- Writes real SEO metadata. Titles and descriptions are clamped to YouTube's limits before upload — a 101-character title fails the whole request rather than getting silently trimmed.
- Uploads to YouTube over OAuth2, with resumable uploads and a channel- identity check that refuses to publish if the authorized token doesn't match the selected brand — the failure mode that actually hurts.
- Schedules uploads. A background scheduler thread publishes queued videos at their appointed time and catches up on anything that came due while the app was offline.
- Runs jobs concurrently via a
ProcessPoolExecutor, so a batch of 30 videos uses every core, with a live queue view and one-click retry. - Manages many channels at once, each an isolated directory of images, audio, fonts, quotes, and OAuth tokens.
- Onboards new channels through a guided AI wizard — a deterministic stage machine that collects topic, name, audience, and tone, then has Gemini generate the starter quote set.
- Reports and exports — a cross-brand analytics dashboard with CSV export for videos, schedules, channels, and tasks.
How I built it
- Flask backend (
app.py, 51 routes) with a background scheduler thread and aProcessPoolExecutorfor CPU-bound rendering. video_creator.py— anAssetManagerandVideoPipelinedoing the OpenCV/Pillow compositing and FFmpeg muxing.api_clients.py— Gemini/Ollama caption generation with key rotation, plus YouTube OAuth2 and resumable upload.safe_io.py— every piece of state (schedule, channel registry, history) is written temp-file-then-os.replace, so a crash mid-write can't corrupt state.config.py/database.py— env-driven config, filesystem path validation, and a JSON-file channel registry. Deliberately not a full database: this is a single-operator tool, and a relational DB would add a migration story in exchange for nothing at this scale.- Security hardening — regex validation and path-traversal checks on every filesystem-touching input, hardened session cookies, rate limiting on wizard endpoints, and debug mode off by default.
Challenges I ran into
- Crash-safe state writes. A plain
open(path, 'w')truncates the target before the new bytes land — a crash mid-write left an empty schedule or channel list.safe_io.pyfixes this by serializing fully in memory, writing to a temp file, then atomicallyos.replace-ing it. - Gemini quota exhaustion mid-batch. Solved with multi-key rotation and an offline Ollama fallback, so a render batch never just stops partway through.
- The wrong-channel failure mode. Publishing content for one channel's identity to another's authorized token is unrecoverable once it's live — so the uploader now verifies the authorized channel against the selected brand before every publish.
- The GIL blocking batch renders. OpenCV/PIL work is CPU-bound and holds
the GIL under threads, so a 30-video batch was stuck on one core. Moving
rendering to a
ProcessPoolExecutorfixed that.
Accomplishments that I'm proud of
- A judge can clone the repo, run
python app.py, hit Create on the bundledexample_brand, and get a real rendered video in under a minute — zero API keys, zero setup. - The whole pipeline is scriptable headlessly (
auto_create_and_post.py) for real unattended daily operation, not just a UI demo. - Multi-provider AI failover — cloud and fully offline — so the tool degrades gracefully instead of breaking when a provider hits an outage or quota limit.
What I learned
Building the atomic-write layer taught me how much "obviously correct" file
handling code (open(path, 'w'), write, close) is actually a silent data-
loss bug waiting for a bad crash timing. It also reframed how I think about
failure modes in automation generally — the scary bugs aren't the ones that
throw exceptions, they're the ones that silently publish the wrong thing to
the wrong channel, which is why the identity check exists.
What's next for Quotient
- TikTok and Instagram Reels publishing (currently YouTube-only by design).
- Pulling real performance metrics (views, retention) back from the YouTube API — current analytics are pipeline-side only (videos rendered, uploads scheduled), not audience data.
- Multi-user/auth support if this moves beyond single-operator use.

Log in or sign up for Devpost to join the conversation.