{"id":51358260,"url":"https://github.com/ygit/videodb-demo","last_synced_at":"2026-07-02T21:08:52.626Z","repository":{"id":355354618,"uuid":"1227745803","full_name":"ygit/videodb-demo","owner":"ygit","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-03T08:27:43.000Z","size":29,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-03T08:27:47.382Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"CSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ygit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-03T05:28:43.000Z","updated_at":"2026-05-03T08:27:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ygit/videodb-demo","commit_stats":null,"previous_names":["ygit/videodb-demo"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ygit/videodb-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygit%2Fvideodb-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygit%2Fvideodb-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygit%2Fvideodb-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygit%2Fvideodb-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ygit","download_url":"https://codeload.github.com/ygit/videodb-demo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygit%2Fvideodb-demo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35062887,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-02T21:08:51.834Z","updated_at":"2026-07-02T21:08:52.604Z","avatar_url":"https://github.com/ygit.png","language":"CSS","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🎬 VideoDB Reel Builder\n\n\u003e Turn long-form videos into short, vertical reels — paste a URL, write a few topic queries, get clips you can embed and share.\n\nA 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.\n\n![Reel builder home page in dark mode](docs/screenshots/home-dark.png)\n\n---\n\n## Table of contents\n\n1. [Features](#-features)\n2. [Quick start](#-quick-start)\n3. [Walkthrough](#-walkthrough)\n4. [Architecture](#-architecture)\n5. [Pipeline](#-pipeline)\n6. [Project structure](#-project-structure)\n7. [Configuration](#%EF%B8%8F-configuration)\n8. [Writing good queries](#-writing-good-queries)\n9. [Limits \u0026amp; known issues](#-limits--known-issues)\n10. [Troubleshooting](#-troubleshooting)\n11. [How it works](#-how-it-works-under-the-hood)\n12. [Tech stack](#%EF%B8%8F-tech-stack)\n\n---\n\n## ✨ Features\n\n- **Multi-video batches** — submit many videos in one form; each becomes its own job, all run in parallel.\n- **Per-query reels** — one short topic per line ⇒ one reel; up to 5 reels per video reframe concurrently.\n- **Multiple languages** — English / Hindi / Hinglish for the spoken-word index, so non-English content actually gets transcribed.\n- **Configurable output** — 9:16 vertical / 1:1 square / 16:9 landscape; smart object-tracking or simple centre crop.\n- **Live progress** — animated rainbow gradient bar with per-phase milestones; auto-retries reframe once on processing timeouts.\n- **Dark mode** — auto-follows OS preference, with a toggle that persists per-browser. No flash-of-incorrect-theme.\n- **Friendly errors** — sentence-style queries are caught before they fail; SDK timeouts surface phase-aware actionable messages.\n- **XSS-safe** — all dynamic UI is built with `textContent`/`createElement`, never `innerHTML`.\n\n## 🚀 Quick start\n\nYou'll need Python 3.10+ and a VideoDB API key.\n\n```bash\n# 1. Clone\ngit clone https://github.com/ygit/videodb-demo\ncd videodb-demo\n\n# 2. Get a free API key (50 free uploads, no credit card)\n# https://console.videodb.io\ncp .env.example .env\n# then edit .env and set VIDEO_DB_API_KEY=your-key-here\n\n# 3. Install\npython3 -m venv .venv\nsource .venv/bin/activate\npip install -r requirements.txt\n\n# 4. Smoke-test the SDK connection\npython main.py\n# → Connected to collection: \u003cname\u003e (\u003cid\u003e)\n# → Videos: 0\n\n# 5. Run the UI\nflask --app app run --host 127.0.0.1 --port 5057\n```\n\nOpen [http://127.0.0.1:5057](http://127.0.0.1:5057).\n\n\u003e 💡 **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.\n\n## 🎬 Walkthrough\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  🌈 (rainbow brand bar)                              ☾ ◀ toggle│\n├─────────────────────────────────────────────────────────────┤\n│                                                             │\n│  Reel builder           ◀── gradient hero title             │\n│  Generate short reels from one or more long-form videos.    │\n│                                                             │\n│  ▸ How to write good queries                                │\n│                                                             │\n│  ┌──────────── Video 1 ──────────────────────────────── × ┐ │\n│  │  Video URL                                              │ │\n│  │  [ https://youtube.com/watch?v=...                  ]   │ │\n│  │  Queries · one per line · one reel per line             │ │\n│  │  ⓘ One short topic per line, e.g. `key highlights`.     │ │\n│  │  ┌─────────────────────────────────────────────────┐    │ │\n│  │  │ intro                                           │    │ │\n│  │  │ key insight                                     │    │ │\n│  │  │ demo                                            │    │ │\n│  │  └─────────────────────────────────────────────────┘    │ │\n│  └─────────────────────────────────────────────────────────┘ │\n│  + Add another video                                        │\n│                                                             │\n│  Shared settings                                            │\n│  Length: [60]   Aspect: ◉ 9:16  ◯ 1:1  ◯ 16:9                │\n│                 Mode:   ◉ smart ◯ simple                    │\n│                 Lang:   ◉ English ◯ Hindi ◯ Hinglish        │\n│                                                             │\n│  [ Generate reels ]                                         │\n└─────────────────────────────────────────────────────────────┘\n```\n\n**The flow:**\n\n1. Paste a video URL — YouTube, Vimeo, or any direct video URL VideoDB can ingest.\n2. Write **one topic per line** in the queries box. Each line becomes one reel.\n3. (Optional) Click **+ Add another video** to bundle multiple sources into one batch.\n4. Pick your output settings — they apply to every reel in the batch.\n5. Submit. You're redirected to a live status page that polls every 1.5s.\n6. Each reel shows up inline as it finishes. Click to play, or open in the VideoDB player.\n\n## 🏗️ Architecture\n\n```mermaid\nflowchart LR\n    Browser([\"💻 Browser\"])\n\n    subgraph Flask[\"Flask app — single process\"]\n        Routes[\"Routes\u003cbr/\u003eGET /\u003cbr/\u003ePOST /generate\u003cbr/\u003eGET /jobs/\u003cid\u003e\u003cbr/\u003eGET /batches/\u003cid\u003e\"]\n        JobStore[(\"In-memory\u003cbr/\u003ejob + batch store\u003cbr/\u003e+ video URL cache\")]\n        Worker[\"Worker thread\u003cbr/\u003e(daemon, per job)\"]\n        Pool[\"ThreadPoolExecutor\u003cbr/\u003eper-reel parallelism\"]\n        Routes \u003c--\u003e JobStore\n        Routes -. spawns .-\u003e Worker\n        Worker --\u003e Pool\n        Worker \u003c--\u003e JobStore\n        Pool \u003c--\u003e JobStore\n    end\n\n    VideoDB[(\"☁️ VideoDB API\")]\n\n    Browser -- \"form submit\" --\u003e Routes\n    Routes -- \"302 → /jobs/\u003cid\u003e\" --\u003e Browser\n    Browser -. \"polls /status every 1.5s\" .-\u003e Routes\n    Worker -- \"upload, index, search, reframe\" --\u003e VideoDB\n    VideoDB -- \"stream URLs\" --\u003e Browser\n```\n\n**Key pieces:**\n\n- **`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).\n- **`templates/_base.html`** — shared layout: brand bar, hero, theme toggle, footer. Other templates extend this.\n- **`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.\n\n## 🔄 Pipeline\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant U as User\n    participant F as Flask\n    participant W as Worker\n    participant V as VideoDB\n\n    U-\u003e\u003eF: POST /generate (URL + queries)\n    F-\u003e\u003eF: Validate (URL, sentence-detection,\u003cbr/\u003elength, aspect, mode, lang)\n    F-\u003e\u003eW: Spawn daemon thread per video\n    F--\u003e\u003eU: 302 → /jobs/\u003cid\u003e or /batches/\u003cid\u003e\n\n    par Worker pipeline\n        Note over W,V: phase = uploading\n        W-\u003e\u003eV: collection.upload(url)\n        V--\u003e\u003eW: video_id\n        Note over W,V: phase = indexing\n        W-\u003e\u003eV: index_spoken_words(language_code)\n        V--\u003e\u003eW: indexed\n        Note over W,V: phase = reeling — up to 5 in parallel\n        loop each query\n            W-\u003e\u003eV: video.search(query)\n            V--\u003e\u003eW: shots\n            W-\u003e\u003eW: widen window to length seconds\n            W-\u003e\u003eV: video.reframe(start, end, target, mode)\n            V--\u003e\u003eW: stream_url\n            opt reframe stuck\n                W-\u003e\u003eV: retry once\n                V--\u003e\u003eW: stream_url\n            end\n        end\n    and Browser polls\n        loop every 1.5s until terminal\n            U-\u003e\u003eF: GET /jobs/\u003cid\u003e/status\n            F--\u003e\u003eU: { state, progress%, reels[] }\n            U-\u003e\u003eU: render rainbow bar + reel cards\n        end\n    end\n```\n\n**Phase model:** `queued → uploading → indexing → reeling → done`\n\n- On error, `failed_phase` is captured so the UI can mark the right milestone with a red ✗.\n- Per-reel state moves through `pending → matching → reframing → ready` (or `skipped` / `error`).\n\n## 📁 Project structure\n\n```\nvideodb-demo/\n├── app.py                  # Flask app: routes, job store, worker pipeline, helpers\n├── main.py                 # Smoke test: connect and list videos\n├── requirements.txt        # videodb, python-dotenv, flask\n├── .env                    # VIDEO_DB_API_KEY (gitignored)\n├── .env.example            # template for new clones\n├── README.md               # this file\n├── templates/\n│   ├── _base.html          # shared layout: brand bar, hero, theme toggle, footer\n│   ├── index.html          # form: dynamic video blocks + shared settings\n│   ├── job.html            # single-job status page\n│   └── batch.html          # multi-video batch status page\n└── static/\n    ├── style.css           # design tokens, light/dark themes, animations\n    ├── job.js              # progress bar, milestones, reel rendering, polling\n    └── theme.js            # dark-mode persistence + OS preference\n```\n\n## ⚙️ Configuration\n\n### Required environment variable\n\n| Variable             | Required | Notes                                                          |\n| -------------------- | -------- | -------------------------------------------------------------- |\n| `VIDEO_DB_API_KEY` | yes      | Get a free key at[console.videodb.io](https://console.videodb.io) |\n\nThe app reads `.env` from the working directory at startup via `python-dotenv`.\n\n### Knobs in `app.py`\n\n| Constant / route field    | Default                 | Purpose                                                                                                                                                |\n| ------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `MAX_REEL_CONCURRENCY`  | `5`                   | Cap on parallel reframe calls per job                                                                                                                  |\n| `ASPECT_MAP`            | 3 entries               | UI labels (`9:16` / `1:1` / `16:9`) → VideoDB reframe presets (`vertical` / `square` / `landscape`)                                       |\n| `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. |\n| Length input              | 60s, range `[5, 300]` | Each reel is widened symmetrically around the matched shot's midpoint                                                                                  |\n| `TEMPLATES_AUTO_RELOAD` | `True`                | Template edits show on next request without a server restart                                                                                           |\n\n## 💡 Writing good queries\n\nEach 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.\n\n| ✓ Works well            | ✗ Avoid                                                                                 |\n| ------------------------ | ---------------------------------------------------------------------------------------- |\n| `match highlights`     | `give me key highlights, predictions, coach, transfer market \u0026 some laughing moments.` |\n| `goal celebration`     | `show me everything funny in this video`                                               |\n| `funny moment`         | `make me a fun reel about football transfers`                                          |\n| `transfer rumour`      | a single line longer than ~12 words                                                      |\n| `post-match interview` | sentences with `give me`, `show me`, `find me` openers                             |\n\nThe 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.\n\n**Other tips:**\n\n- **Match the speaker's vocabulary.** If commentators say \"best moments\", `best moments` will hit better than abstract synonyms.\n- **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.\n- **Short videos are reliable.** Free-tier processing has time limits — see Limits below.\n\n## 🚧 Limits \u0026 known issues\n\n| Limit                               | Notes                                                                                                                                                                    |\n| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| **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.   |\n| **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. |\n| **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.        |\n| **In-memory state**           | Job and batch state lives in process memory. Flask restart = jobs in flight are lost. Don't deploy this as-is.                                                           |\n| **Single-process**            | The current design assumes one worker. Multi-worker `gunicorn` would need Redis-backed state — out of scope for this playground.                                      |\n\n## 🩺 Troubleshooting\n\n**`Address already in use` on port 5000 (macOS)**\nModern macOS uses port 5000 for AirPlay Receiver. Use a different port (`--port 5057`) or disable AirPlay Receiver in *System Settings → General → AirDrop \u0026 Handoff*.\n\n**`AuthenticationError: Authentication failed`**\nYour `VIDEO_DB_API_KEY` is missing or wrong. Confirm `.env` exists and contains `VIDEO_DB_API_KEY=...`. Restart the server after editing.\n\n**Job state shows \"failed\" with no detail**\nCheck 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.\n\n**All reels skip with \"no match\"**\nThe 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.\n\n**Form rejects my query as \"looks like one long sentence\"**\nThat's the friendly server-side guard. Split your topics into multiple lines.\n\n**Job goes to \"failed\" during reframe**\nThe 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.\n\n## 🔧 How it works under the hood\n\nA few non-obvious choices worth pointing out:\n\n- **`_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.\n- **`_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.\n- **`_looks_like_compound_sentence(qtext)`** catches the most common query mistake: one long sentence containing multiple topics. Three signals: word count \u003e 12, sentence-style opener (`give me`, `show me`, `find me`, etc.), or a separator (`,`, `;`, `\u0026`, `and`) combined with \u003e 5 words.\n- **`_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.\n- **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 `\u003cscript\u003e` tags inside transcript text or query strings.\n- **Inline pre-body theme script** in `_base.html` reads `localStorage.theme` *before* `\u003cbody\u003e` parses, so users in dark mode never see a flash of light. The toggle button just flips `data-theme` on `\u003chtml\u003e` and persists.\n\n## 🛠️ Tech stack\n\n- **Backend**: Python 3.12, Flask 3, `threading` + `concurrent.futures.ThreadPoolExecutor`\n- **Frontend**: Vanilla JavaScript (no framework), Inter font, CSS custom properties for theming\n- **Video pipeline**: [VideoDB Python SDK](https://github.com/video-db/videodb-python) (`videodb`)\n\n---\n\n## Adding more screenshots\n\nThe hero image above lives at `docs/screenshots/home-dark.png`. Drop additional PNGs in the same directory and reference them with `![alt](docs/screenshots/foo.png)` — they'll render on GitHub. Fast capture on macOS:\n\n```bash\nmkdir -p docs/screenshots\n# Cmd+Shift+4 → drag-select → screenshot lands on Desktop\nmv ~/Desktop/Screen*.png docs/screenshots/job-running.png\n```\n\n---\n\nBuilt with [VideoDB](https://videodb.io). PRs welcome.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fygit%2Fvideodb-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fygit%2Fvideodb-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fygit%2Fvideodb-demo/lists"}