https://github.com/ygit/videodb-demo
https://github.com/ygit/videodb-demo
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/ygit/videodb-demo
- Owner: ygit
- Created: 2026-05-03T05:28:43.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-05-03T08:27:43.000Z (3 months ago)
- Last Synced: 2026-05-03T08:27:47.382Z (3 months ago)
- Language: CSS
- Size: 28.3 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# π¬ VideoDB Reel Builder
> Turn long-form videos into short, vertical reels β paste a URL, write a few topic queries, get clips you can embed and share.
A small Flask playground built on top of the [VideoDB Python SDK](https://github.com/video-db/videodb-python). Designed to be readable in a single sitting and easy to fork.

---
## Table of contents
1. [Features](#-features)
2. [Quick start](#-quick-start)
3. [Walkthrough](#-walkthrough)
4. [Architecture](#-architecture)
5. [Pipeline](#-pipeline)
6. [Project structure](#-project-structure)
7. [Configuration](#%EF%B8%8F-configuration)
8. [Writing good queries](#-writing-good-queries)
9. [Limits & known issues](#-limits--known-issues)
10. [Troubleshooting](#-troubleshooting)
11. [How it works](#-how-it-works-under-the-hood)
12. [Tech stack](#%EF%B8%8F-tech-stack)
---
## β¨ Features
- **Multi-video batches** β submit many videos in one form; each becomes its own job, all run in parallel.
- **Per-query reels** β one short topic per line β one reel; up to 5 reels per video reframe concurrently.
- **Multiple languages** β English / Hindi / Hinglish for the spoken-word index, so non-English content actually gets transcribed.
- **Configurable output** β 9:16 vertical / 1:1 square / 16:9 landscape; smart object-tracking or simple centre crop.
- **Live progress** β animated rainbow gradient bar with per-phase milestones; auto-retries reframe once on processing timeouts.
- **Dark mode** β auto-follows OS preference, with a toggle that persists per-browser. No flash-of-incorrect-theme.
- **Friendly errors** β sentence-style queries are caught before they fail; SDK timeouts surface phase-aware actionable messages.
- **XSS-safe** β all dynamic UI is built with `textContent`/`createElement`, never `innerHTML`.
## π Quick start
You'll need Python 3.10+ and a VideoDB API key.
```bash
# 1. Clone
git clone https://github.com/ygit/videodb-demo
cd videodb-demo
# 2. Get a free API key (50 free uploads, no credit card)
# https://console.videodb.io
cp .env.example .env
# then edit .env and set VIDEO_DB_API_KEY=your-key-here
# 3. Install
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# 4. Smoke-test the SDK connection
python main.py
# β Connected to collection: ()
# β Videos: 0
# 5. Run the UI
flask --app app run --host 127.0.0.1 --port 5057
```
Open [http://127.0.0.1:5057](http://127.0.0.1:5057).
> π‘ **Why port 5057 instead of 5000?** Modern macOS hijacks port 5000 for AirPlay Receiver (Control Center). Pick any free port; 5057 is uncommon enough to almost always be available.
## π¬ Walkthrough
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β π (rainbow brand bar) βΎ β toggleβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Reel builder βββ gradient hero title β
β Generate short reels from one or more long-form videos. β
β β
β βΈ How to write good queries β
β β
β βββββββββββββ Video 1 ββββββββββββββββββββββββββββββββ Γ β β
β β Video URL β β
β β [ https://youtube.com/watch?v=... ] β β
β β Queries Β· one per line Β· one reel per line β β
β β β One short topic per line, e.g. `key highlights`. β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β intro β β β
β β β key insight β β β
β β β demo β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β + Add another video β
β β
β Shared settings β
β Length: [60] Aspect: β 9:16 β― 1:1 β― 16:9 β
β Mode: β smart β― simple β
β Lang: β English β― Hindi β― Hinglish β
β β
β [ Generate reels ] β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
**The flow:**
1. Paste a video URL β YouTube, Vimeo, or any direct video URL VideoDB can ingest.
2. Write **one topic per line** in the queries box. Each line becomes one reel.
3. (Optional) Click **+ Add another video** to bundle multiple sources into one batch.
4. Pick your output settings β they apply to every reel in the batch.
5. Submit. You're redirected to a live status page that polls every 1.5s.
6. Each reel shows up inline as it finishes. Click to play, or open in the VideoDB player.
## ποΈ Architecture
```mermaid
flowchart LR
Browser(["π» Browser"])
subgraph Flask["Flask app β single process"]
Routes["Routes
GET /
POST /generate
GET /jobs/
GET /batches/"]
JobStore[("In-memory
job + batch store
+ video URL cache")]
Worker["Worker thread
(daemon, per job)"]
Pool["ThreadPoolExecutor
per-reel parallelism"]
Routes <--> JobStore
Routes -. spawns .-> Worker
Worker --> Pool
Worker <--> JobStore
Pool <--> JobStore
end
VideoDB[("βοΈ VideoDB API")]
Browser -- "form submit" --> Routes
Routes -- "302 β /jobs/" --> Browser
Browser -. "polls /status every 1.5s" .-> Routes
Worker -- "upload, index, search, reframe" --> VideoDB
VideoDB -- "stream URLs" --> Browser
```
**Key pieces:**
- **`app.py`** β all routes, the in-memory job store (a `dict` guarded by `threading.Lock`), the per-job worker, and the shared video URL cache (so repeat submits of the same URL skip re-upload).
- **`templates/_base.html`** β shared layout: brand bar, hero, theme toggle, footer. Other templates extend this.
- **`static/job.js`** β shared rendering for both single-job and batch pages: progress bar, milestones, per-reel cards. Polls in a loop until the job reaches a terminal state.
## π Pipeline
```mermaid
sequenceDiagram
autonumber
participant U as User
participant F as Flask
participant W as Worker
participant V as VideoDB
U->>F: POST /generate (URL + queries)
F->>F: Validate (URL, sentence-detection,
length, aspect, mode, lang)
F->>W: Spawn daemon thread per video
F-->>U: 302 β /jobs/ or /batches/
par Worker pipeline
Note over W,V: phase = uploading
W->>V: collection.upload(url)
V-->>W: video_id
Note over W,V: phase = indexing
W->>V: index_spoken_words(language_code)
V-->>W: indexed
Note over W,V: phase = reeling β up to 5 in parallel
loop each query
W->>V: video.search(query)
V-->>W: shots
W->>W: widen window to length seconds
W->>V: video.reframe(start, end, target, mode)
V-->>W: stream_url
opt reframe stuck
W->>V: retry once
V-->>W: stream_url
end
end
and Browser polls
loop every 1.5s until terminal
U->>F: GET /jobs//status
F-->>U: { state, progress%, reels[] }
U->>U: render rainbow bar + reel cards
end
end
```
**Phase model:** `queued β uploading β indexing β reeling β done`
- On error, `failed_phase` is captured so the UI can mark the right milestone with a red β.
- Per-reel state moves through `pending β matching β reframing β ready` (or `skipped` / `error`).
## π Project structure
```
videodb-demo/
βββ app.py # Flask app: routes, job store, worker pipeline, helpers
βββ main.py # Smoke test: connect and list videos
βββ requirements.txt # videodb, python-dotenv, flask
βββ .env # VIDEO_DB_API_KEY (gitignored)
βββ .env.example # template for new clones
βββ README.md # this file
βββ templates/
β βββ _base.html # shared layout: brand bar, hero, theme toggle, footer
β βββ index.html # form: dynamic video blocks + shared settings
β βββ job.html # single-job status page
β βββ batch.html # multi-video batch status page
βββ static/
βββ style.css # design tokens, light/dark themes, animations
βββ job.js # progress bar, milestones, reel rendering, polling
βββ theme.js # dark-mode persistence + OS preference
```
## βοΈ Configuration
### Required environment variable
| Variable | Required | Notes |
| -------------------- | -------- | -------------------------------------------------------------- |
| `VIDEO_DB_API_KEY` | yes | Get a free key at[console.videodb.io](https://console.videodb.io) |
The app reads `.env` from the working directory at startup via `python-dotenv`.
### Knobs in `app.py`
| Constant / route field | Default | Purpose |
| ------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `MAX_REEL_CONCURRENCY` | `5` | Cap on parallel reframe calls per job |
| `ASPECT_MAP` | 3 entries | UI labels (`9:16` / `1:1` / `16:9`) β VideoDB reframe presets (`vertical` / `square` / `landscape`) |
| `LANGUAGES` | 3 entries | UI labels β`(display_name, language_code)`. `Hinglish` maps to the Hindi code path because VideoDB has no documented code-switched language code. |
| Length input | 60s, range `[5, 300]` | Each reel is widened symmetrically around the matched shot's midpoint |
| `TEMPLATES_AUTO_RELOAD` | `True` | Template edits show on next request without a server restart |
## π‘ Writing good queries
Each line in the queries box is a **separate semantic search** against the spoken-word transcript. Think about what the speaker would actually *say*, not how you'd describe what you want.
| β Works well | β Avoid |
| ------------------------ | ---------------------------------------------------------------------------------------- |
| `match highlights` | `give me key highlights, predictions, coach, transfer market & some laughing moments.` |
| `goal celebration` | `show me everything funny in this video` |
| `funny moment` | `make me a fun reel about football transfers` |
| `transfer rumour` | a single line longer than ~12 words |
| `post-match interview` | sentences with `give me`, `show me`, `find me` openers |
The form catches the most common mistake (one long sentence asking for many things) before it ever reaches VideoDB. If your query gets rejected with *"that looks like one long sentence β split it"*, that's why.
**Other tips:**
- **Match the speaker's vocabulary.** If commentators say "best moments", `best moments` will hit better than abstract synonyms.
- **Pick the right language.** If the audio is Hindi or Hinglish but you submit with English selected, the transcript will be sparse and English queries won't match. Switch the language radio.
- **Short videos are reliable.** Free-tier processing has time limits β see Limits below.
## π§ Limits & known issues
| Limit | Notes |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Max video length: 3 hours** | Free-tier upload cap. Longer videos return*"Video is over 3 hours, please reachout to contact@videodb.io to increase the quota"*. Email them or trim before uploading. |
| **50 free uploads** | Resets per-account; check[console.videodb.io](https://console.videodb.io) for current usage. Easy to burn through if you make the URL public β see deployment notes below. |
| **Reframe timeout** | The SDK can give up with*"Stuck on processing status"* on long source videos. The app retries once automatically, then surfaces a phase-aware actionable message. |
| **In-memory state** | Job and batch state lives in process memory. Flask restart = jobs in flight are lost. Don't deploy this as-is. |
| **Single-process** | The current design assumes one worker. Multi-worker `gunicorn` would need Redis-backed state β out of scope for this playground. |
## π©Ί Troubleshooting
**`Address already in use` on port 5000 (macOS)**
Modern macOS uses port 5000 for AirPlay Receiver. Use a different port (`--port 5057`) or disable AirPlay Receiver in *System Settings β General β AirDrop & Handoff*.
**`AuthenticationError: Authentication failed`**
Your `VIDEO_DB_API_KEY` is missing or wrong. Confirm `.env` exists and contains `VIDEO_DB_API_KEY=...`. Restart the server after editing.
**Job state shows "failed" with no detail**
Check the Flask process logs β the full SDK traceback is logged via `log.exception(...)`. The UI message is intentionally generic for non-VideoDB exceptions to avoid leaking internals; the logs have the truth.
**All reels skip with "no match"**
The query didn't semantically match the transcript. See [Writing good queries](#-writing-good-queries). If the audio language is wrong, fix the language radio. If the video has very little speech, no query will match.
**Form rejects my query as "looks like one long sentence"**
That's the friendly server-side guard. Split your topics into multiple lines.
**Job goes to "failed" during reframe**
The SDK couldn't keep up with VideoDB's processing. The app already retries once. If both attempts fail, the message will tell you to try a shorter source video or contact VideoDB support.
## π§ How it works under the hood
A few non-obvious choices worth pointing out:
- **`_get_or_upload(url)`** caches by URL with a per-URL `threading.Lock` so simultaneous form submits of the same URL don't race and double-upload. The cache only evicts on a "not found" SDK error β transient errors keep the cached id intact.
- **`_compute_progress(state, reels, failed_phase)`** computes both the rainbow-bar percent (0β100) and the per-step milestone state (`done` / `active` / `pending` / `error`). When a job errors, the bar fills to *the failing phase's percent* (12 / 30 / 70) and the right step gets a red β β instead of the all-grey-with-full-red rendering that earlier versions had.
- **`_looks_like_compound_sentence(qtext)`** catches the most common query mistake: one long sentence containing multiple topics. Three signals: word count > 12, sentence-style opener (`give me`, `show me`, `find me`, etc.), or a separator (`,`, `;`, `&`, `and`) combined with > 5 words.
- **`_is_processing_timeout(exc)`** pattern-matches the SDK's *bare* `Exception("Stuck on processing status")` β which isn't a typed `VideodbError` β so we can detect it, retry once via `_reframe_with_retry`, and surface `_processing_timeout_msg(phase)` if it persists.
- **Per-reel rendering is XSS-safe.** `static/job.js` builds DOM exclusively with `textContent` and `createElement`. Earlier versions used `innerHTML` interpolation, which would have executed `` tags inside transcript text or query strings.
- **Inline pre-body theme script** in `_base.html` reads `localStorage.theme` *before* `<body>` parses, so users in dark mode never see a flash of light. The toggle button just flips `data-theme` on `<html>` and persists.
## π οΈ Tech stack
- **Backend**: Python 3.12, Flask 3, `threading` + `concurrent.futures.ThreadPoolExecutor`
- **Frontend**: Vanilla JavaScript (no framework), Inter font, CSS custom properties for theming
- **Video pipeline**: [VideoDB Python SDK](https://github.com/video-db/videodb-python) (`videodb`)
---
## Adding more screenshots
The hero image above lives at `docs/screenshots/home-dark.png`. Drop additional PNGs in the same directory and reference them with `` β they'll render on GitHub. Fast capture on macOS:
```bash
mkdir -p docs/screenshots
# Cmd+Shift+4 β drag-select β screenshot lands on Desktop
mv ~/Desktop/Screen*.png docs/screenshots/job-running.png
```
---
Built with [VideoDB](https://videodb.io). PRs welcome.