Production-grade Text-to-Video system that transforms Stoic philosophy into viral YouTube Shorts (1080×1920, 30-60s) with professional effects, karaoke subtitles, background music, and automatic YouTube upload.
Fully automated pipeline: Topic → Script → Audio → Images → Video → YouTube Upload
- Generates trending Stoic topics (scroll-stopping hooks)
- Writes viral scripts with SEO metadata (title, description, hashtags, tags)
- Produces cinematic voiceover with dramatic pauses (hook → quote → elaboration → CTA)
- Creates 5-7 AI-generated vertical images
- Assembles professional video with:
- Karaoke word-by-word subtitles (2 rows, amber highlight)
- 6 visual effects (Ken Burns, grain, glow, flakes, rays)
- Pinned background music + thunderstorm overlay
- Fade-to-black ending
- Uploads privately to YouTube with SEO metadata
```bash
pip install -r requirements.txt
- One for scripts: https://gemini.google.com/app/XXXXX
- One for images: https://gemini.google.com/app/YYYYY
CHROME_PROFILE_DIR = r"D:\SeleniumChromeProfile"
- Go to https://console.cloud.google.com
python validate_setup.py
python main.py --new ```
First upload: Browser opens once for Google consent → tokens cached → never asks again.
```bash
python main.py --new
python upload.py
python upload.py --id 12
python upload.py --dry-run
python main.py --status
python main.py ```
Per video directory: `output/video_N/` ``` voiceover.wav # 4-part narration with pauses voiceover.timestamps.json # Word-level timing for karaoke images/ ├── scene_1.png # 5-7 AI images (count varies) ├── scene_2.png └── ... short_N.mp4 # Final 1080×1920 video ```
YouTube: Private video uploaded with:
- Title (from LLM)
- Description with hashtags (from LLM)
- 8-12 SEO tags (from LLM)
| What to Change | Where | Examples |
|---|---|---|
| Voice speed/pitch | config.py | `VOICE_SPEED = 0.86`, `VOICE_PITCH_SEMITONES = -1.0` |
| Pause durations | config.py | `HOOK_PAUSE_SEC = 0.50`, `QUOTE_PAUSE_SEC = 0.85` |
| Background music volume | config.py | `BG_MUSIC_VOLUME = 0.15` |
| Caption styling | config.py | `CAPTION_FONTSIZE = 52`, `CAPTION_COLOR = "white"` |
| Video effects | engines/hands.py | `_build_scene_with_effects()` |
| Script prompts | prompts.py | `SCRIPT_SYSTEM`, `IMAGE_PROMPT_TEMPLATE` |
| Image count (5-7) | prompts.py | LLM decides count in script |
| Problem | Solution |
|---|---|
| Chrome crashes / locks | Close all Chrome windows, delete profile lock files |
| Gemini input not found | Verify logged into Gemini, check chat URLs are correct |
| YouTube consent every time | First upload only; tokens cached in `yt_token.json` |
| TTS voice quality | Adjust `VOICE_SPEED`, `VOICE_PITCH_SEMITONES` in config.py |
| Images too dark | Lower `DARKEN_FACTOR` in config.py (default 0.85) |
| No background music | Place `_pinned_bgm.m4a` in `assets/music/` |
| Upload fails | Check `client_secret.json` exists, API enabled in Google Cloud |
``` YTAutomation/ ├── config.py # Central settings (URLs, audio, video, effects) ├── database.py # SQLAlchemy ORM + pipeline state machine ├── prompts.py # All Gemini prompt templates ├── main.py # CLI orchestrator with state machine ├── upload.py # Batch YouTube upload script ├── batch.py # Generate multiple videos sequentially ├── validate_setup.py # Pre-flight environment checks ├── requirements.txt # All Python dependencies ├── client_secret.json # YouTube OAuth credentials (user-provided) ├── yt_token.json # Cached YouTube tokens (auto-created) ├── videos.db # SQLite state database (auto-created) └── engines/ ├── browser.py # Shared Selenium helpers (Chrome automation) ├── brain.py # Topic + script generation (Gemini) ├── voice.py # 4-part TTS with pauses (Kokoro + faster-whisper) ├── eyes.py # Image generation + download (Gemini) ├── hands.py # Video assembly + effects (MoviePy) └── uploader.py # YouTube upload (Data API v3) ```
- Connects to your Gemini script chat URL
- Step A: Generates trending Stoic topic with scroll-stopping hook
- Step B: Writes complete script JSON: ```json { "title": "YouTube title with emoji", "hook": "Scroll-stopping opening line", "quote": "The main Stoic quote", "elaboration": "3-4 sentences expanding the quote", "cta": "Closing call-to-action", "image_prompts": ["5-7 vertical image descriptions"], "description": "SEO description with #hashtags", "tags": ["8-12", "seo", "keywords"] } ```
- Output: Status `SCRIPT`, full JSON stored in DB
- Uses local Kokoro ONNX (am_adam voice, deep narrator)
- 4-part structure with pauses:
- Hook → `HOOK_PAUSE_SEC` (0.50s) →
- Quote → `QUOTE_PAUSE_SEC` (0.85s) →
- Elaboration → `ELAB_PAUSE_SEC` (0.60s) →
- CTA
- Applies deliberate slowdown: `VOICE_SPEED = 0.86` + pitch drop `-1.0 semitones`
- Generates word-level timestamps via faster-whisper for karaoke sync
- Output: `voiceover.wav` + `voiceover.timestamps.json` + status `AUDIO`
- Connects to your Gemini image chat URL
- Processes 5-7 image prompts (count decided by LLM in script)
- For each prompt:
- Output: 5-7 PNG files in `images/` folder + status `VISUALS`
Uses MoviePy for professional video production:
Core Assembly:
- Resizes images to 1080×1920 (center crop)
- Divides audio by image count for timing
- Applies 1-second crossfade transitions
Visual Effects (randomized per scene):
- Ken Burns — 7 zoom/pan presets (organic movement)
- Flakes — particle overlay (snow/dust effect)
- Rays — light beam overlay (divine/epic feel)
- Glow — radial brightness (soft focus)
- Grain — film texture (vintage authenticity)
Global Effects:
- Vignette (darkened edges)
- Color grading (darken by 0.85× for text contrast)
Karaoke Subtitles:
- 2 rows × 3 words (6 words visible)
- Active word: amber (#ff8c00) highlight
- Other words: white text with black shadow
- Word-synced using faster-whisper timestamps
- All words always visible (no appear/disappear)
Audio:
- Pinned BGM: `assets/music/_pinned_bgm.m4a` (starts 12s in, volume 15%)
- Thunder overlay: `assets/music/_thunder.m4a` (volume 2.2%)
Ending:
- Fade-to-black: Last 1.2 seconds
Output: `short_N.mp4` (1080×1920, 30fps, H.264, ~10Mbps) + status `RENDER`
- Uses YouTube Data API v3 with OAuth 2.0
- Extracts metadata from script JSON:
- Title
- Description (with hashtags)
- Tags (8-12 keywords)
- Uploads in private mode (change `privacy="private"` in code if needed)
- First run: Opens browser for Google consent (tokens cached to `yt_token.json`)
- Subsequent runs: Auto-refreshes tokens silently (no browser)
- Output: YouTube video ID + status `UPLOAD`
- CLI entry point (main.py)
- State machine: `TOPIC → SCRIPT → AUDIO → VISUALS → RENDER → UPLOAD`
- Crash-proof: resumes from last completed stage
- Rich console output with colored progress tables
- Timestamped logging to `logs/` directory
SQLite table: `videos`
| Column | Type | Description |
|---|---|---|
| `id` | INTEGER | Primary key (auto-increment) |
| `topic` | STRING | The Stoic topic chosen |
| `title` | STRING | YouTube video title |
| `script_json` | TEXT | Full JSON from Brain engine |
| `quote` | TEXT | Main Stoic quote |
| `elaboration` | TEXT | 3-4 sentence expansion |
| `image_prompts` | TEXT | JSON array of image descriptions |
| `audio_path` | STRING | Path to `voiceover.wav` |
| `image_paths` | TEXT | JSON array of PNG paths |
| `video_path` | STRING | Path to final MP4 |
| `youtube_id` | STRING | YouTube video ID (after upload) |
| `status` | ENUM | `TOPIC`, `SCRIPT`, `AUDIO`, `VISUALS`, `RENDER`, `UPLOAD`, `FAILED` |
| `error_msg` | TEXT | Human-readable error if `FAILED` |
| `created_at` | DATETIME | Project creation timestamp |
| `updated_at` | DATETIME | Last modification timestamp |
Key Functions:
- `get_next_pending()` — Returns oldest unfinished project
- `get_last_n_titles(n)` — For topic deduplication
Auto-created on first run:
``` YTAutomation/ ├── output/ │ ├── video_1/ │ │ ├── voiceover.wav │ │ ├── voiceover.timestamps.json │ │ ├── images/ │ │ │ ├── scene_1.png │ │ │ ├── scene_2.png │ │ │ └── ... (5-7 total) │ │ └── short_1.mp4 ← FINAL VIDEO │ └── video_2/ ... ├── assets/ │ ├── fonts/ (reserved for custom fonts) │ ├── music/ │ │ ├── _pinned_bgm.m4a ← YOUR background music │ │ └── _thunder.m4a ← Thunderstorm overlay │ └── kokoro/ │ └── kokoro-v1.0.onnx (TTS model, auto-downloaded) ├── logs/ │ └── run_20260221_143055.log ← Timestamped logs ├── temp/ (temporary processing files) └── videos.db ← SQLite database ```
```python VOICE_MODEL = "am_adam" # Deep male narrator VOICE_SPEED = 0.86 # Slower, deliberate pacing VOICE_PITCH_SEMITONES = -1.0 # Deeper tone SAMPLE_RATE = 24000 # Kokoro default
HOOK_PAUSE_SEC = 0.50 # After scroll-stopping hook QUOTE_PAUSE_SEC = 0.85 # After main quote (most dramatic) ELAB_PAUSE_SEC = 0.60 # Before closing CTA
BG_MUSIC_VOLUME = 0.15 # 15% volume (audible but not overpowering) ```
```python CROSSFADE_DURATION = 1.0 # Transition between scenes DARKEN_FACTOR = 0.85 # Color grading (0-1, lower = darker) ZOOM_RANGE = (1.0, 1.12) # Ken Burns zoom range PAN_DRIFT_PX = 60 # Max camera drift pixels FADE_TO_BLACK = 1.2 # End-of-video fade duration ```
```python CAPTION_FONT = "Georgia" # Serif for classic feel CAPTION_FONTSIZE = 52 # Large, readable CAPTION_COLOR = "white" # Main text color CAPTION_STROKE_CLR = "black" # Shadow/outline color CAPTION_STROKE_W = 3 # Shadow thickness CAPTION_FADE_DUR = 0.6 # Fade in/out duration
WORDS_PER_KARAOKE_LINE = 6 # Total words visible WORDS_PER_ROW = 3 # 2 rows × 3 words KARAOKE_ACTIVE_COLOR = "#ff8c00" # Amber highlight ```
```python YT_CLIENT_SECRET = BASE_DIR / "client_secret.json" YT_TOKEN_FILE = BASE_DIR / "yt_token.json" YT_CATEGORY_ID = "22" # 22 = People & Blogs ```
Each scene gets one random effect from this pool (never repeats consecutively):
-
Flakes — Particle overlay (falling snow/dust)
- Density: 40-60 particles
- Opacity: 60-80%
- Movement: Vertical drift with random horizontal
-
Rays — Light beam overlay (divine/epic atmosphere)
- 8-12 radial beams from top
- Animated rotation
- Soft blend mode
-
Glow — Radial soft-focus effect
- Center brightness boost
- Gaussian blur overlay
- Creates dreamy, ethereal look
-
Grain — Film texture overlay
- Per-frame noise (not static)
- Intensity: 10-15 units
- Adds analog authenticity
-
Ken Burns Zoom + Pan
- 7 randomized presets (zoom in/out + drift directions)
- Smooth easing curves
- Creates organic, non-static feel
-
Vignette
- Radial darkening from center outward
- Focuses viewer attention
- Adds cinematic moodiness
-
Color Grading
- Global darken by 0.85× (configurable)
- Increases white text legibility
- Professional color correction
Layout: ``` [word1] [word2] [word3] ← Row 1 [word4] [word5] [word6] ← Row 2 ```
Behavior:
- 6 words always visible (2 rows × 3 words)
- Active word (currently spoken): Amber (#ff8c00) with glow
- Inactive words (context): White with black shadow
- Synced to word-level timestamps from faster-whisper
- Smooth transitions as speaking progresses
Why this works:
- Viewer always has context (see upcoming words)
- Amber highlight guides eye to current word
- Feels like karaoke, not robotic subtitles
```bash
python upload.py
python upload.py --id 12
python upload.py --dry-run ```
```bash
python main.py
python main.py --new ```
```bash
python -c " from database import get_session, Video s = get_session() for v in s.query(Video).order_by(Video.id.desc()).limit(5): print(f'{v.id}: {v.title} [{v.status.value}]') s.close() "
```
Edit config.py, save. Existing projects continue with old URLs; new projects use updated URLs.
- Place your audio file in `assets/music/_pinned_bgm.m4a`
- Downloads will use this file automatically
- Adjust volume: `BG_MUSIC_VOLUME` in config.py
- Place `.ttf` file in `assets/fonts/`
- Update `CAPTION_FONT` in config.py to filename (without extension)
- Restart pipeline
Problem: `SessionNotCreatedException` or Chrome crashes
Solutions:
- Fully close all Chrome windows (check Task Manager)
- Delete lock files in `CHROME_PROFILE_DIR`:
- `Default/Lockfile`
- `SingletonLock`
- browser.py auto-kills processes, but manual close is safer
- Ensure `CHROME_PROFILE_DIR` exists and is not used elsewhere
Problem: `RuntimeError: Could not locate Gemini chat input`
Solutions:
- Open each Gemini URL in Chrome manually
- Verify you're logged into Google/Gemini
- Ensure text input box is visible
Problem: Image generation times out
Solutions:
- Increase `ELEMENT_WAIT` in config.py (default 45s)
- Check Gemini chat manually — is image generation working?
Problem: `ModuleNotFoundError: kokoro-onnx`
Solution: ```bash pip install kokoro-onnx soundfile scipy faster-whisper ```
Problem: Voice sounds robotic
Solutions:
- Increase `VOICE_SPEED` (closer to 1.0 = faster but less robotic)
- Adjust `VOICE_PITCH_SEMITONES` (try 0 or -0.5)
Problem: MoviePy rendering hangs at 0%
Solutions:
- Install `ffmpeg` if not already:
- Windows: `choco install ffmpeg`
- Mac: `brew install ffmpeg`
- Linux: `apt install ffmpeg`
Problem: Video has no background music
Solutions:
- Ensure `_pinned_bgm.m4a` exists in `assets/music/`
- Check volume: `BG_MUSIC_VOLUME` in config.py
Problem: Upload always asks for consent
Solution: This should only happen once. Check:
- `yt_token.json` was created after first consent
- File has valid credentials (JSON format)
Problem: `FileNotFoundError: client_secret.json`
Solution:
- Create Google Cloud project at https://console.cloud.google.com
- Enable "YouTube Data API v3"
- Create OAuth 2.0 credentials (Desktop application type)
- Download JSON → save as `client_secret.json` in project root
Problem: Upload fails with `403 Forbidden`
Solutions:
- Check YouTube Data API v3 is enabled in Google Cloud Console
- Verify OAuth credentials are for correct Google account
| Stage | Duration | Notes |
|---|---|---|
| Brain (topic + script) | 1-2 min | Depends on Gemini response speed |
| Voice (TTS + timestamps) | 30-60s | Faster-whisper can be slow on first run |
| Eyes (5-7 images) | 3-5 min | ~30s per image from Gemini |
| Hands (assembly + effects) | 2-4 min | Depends on CPU; GPU doesn't help |
| Upload (YouTube) | 1-2 min | Depends on file size + internet |
| Total | 8-15 min | End-to-end automation |
Core:
- `selenium` + `undetected-chromedriver` — Gemini automation
- `kokoro-onnx` — Local TTS
- `faster-whisper` — Word-level timestamps for karaoke
- `moviepy` — Video assembly
- `sqlalchemy` — State management
- `Pillow` — Caption rendering
- `rich` — Pretty CLI output
YouTube:
- `google-api-python-client` — YouTube Data API v3
- `google-auth-httplib2`, `google-auth-oauthlib` — OAuth 2.0
See requirements.txt for complete list
- Local TTS: No audio sent to cloud (Kokoro runs offline)
- YouTube tokens: Stored locally in `yt_token.json` (gitignored)
- OAuth credentials: `client_secret.json` is user-provided (not in repo)
Important:
- Do NOT commit `client_secret.json` or `yt_token.json` to version control
- Do NOT share your Gemini chat URLs (they contain session tokens)
| File | Lines | Purpose |
|---|---|---|
| config.py | ~125 | Central settings |
| database.py | ~113 | ORM + state machine |
| prompts.py | ~120 | Gemini prompts |
| main.py | ~273 | Pipeline orchestrator |
| upload.py | ~135 | Batch YouTube upload |
| engines/browser.py | ~240 | Selenium automation |
| engines/brain.py | ~170 | Script generation |
| engines/voice.py | ~255 | TTS with pauses |
| engines/eyes.py | ~250 | Image download |
| engines/hands.py | ~819+ | Video assembly + effects |
| engines/uploader.py | ~203 | YouTube upload engine |
Completed:
- ✅ Full pipeline automation
- ✅ 4-part TTS with pauses
- ✅ Karaoke word-sync subtitles
- ✅ Dynamic image count (5-7)
- ✅ Per-scene effects + BGM
- ✅ YouTube upload with SEO metadata
Future Enhancements:
- Batch upload scheduling
- Thumbnail generation
- Instagram Reels export
- TikTok upload integration
- Analytics dashboard
Generated: February 21, 2026
Status: ✅ Production-Ready
For detailed build history, see BUILD_STATUS.md