
News Digest Video Agent
- 1 installs
- 42 repo stars
- Updated April 21, 2026
- video-db/agentic-streams
Helps with ai & agent building tasks.
About
news-digest-video-agent is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- news-digest-video-agent
- AI & Agent Building
- AI-coding skill
News Digest Video Agent by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,098 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/video-db/agentic-streams --skill news-digest-video-agentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 42 |
| Last updated | April 21, 2026 |
| Repository | video-db/agentic-streams ↗ |
What it does
Helps with ai & agent building tasks.
Files
News Digest Video Agent Skill
Use this skill when the user wants to:
- Create a news digest video on any topic
- Turn a breaking news story into a multi-perspective video summary
- Combine YouTube clips, tweets, and articles into a professional broadcast-style video
Setup (run once)
Before using this skill, ensure these dependencies are installed:
1. VideoDB skill & SDK
# Install VideoDB skill (provides video upload, indexing, search, TTS, timeline editing)
npx skills add video-db/skills
# Install Python SDK
pip install videodb python-dotenvSet your API key (get one free at https://console.videodb.io):
export VIDEO_DB_API_KEY=your_key_here2. Browser use skill
# Install browser-use skill (provides screenshot, scroll recording, web page capture)
mkdir -p ~/.claude/skills/browser-use
curl -o ~/.claude/skills/browser-use/SKILL.md \
https://raw.githubusercontent.com/browser-use/browser-use/main/skills/browser-use/SKILL.mdFollow the browser-use SKILL.md instructions for any additional setup (e.g., playwright install).
3. Verify setup
from dotenv import load_dotenv
load_dotenv(".env")
import videodb
conn = videodb.connect()
print("VideoDB connected:", conn)Required tools / capabilities
Strictly required:
- VideoDB skill — video upload, indexing, search, TTS, timeline editing
- Browser use skill — ALL internet research, searching, screenshots, scroll recordings
- File read/write — manage registry, scripts, and output files
- Bash / Python — run the build script
CRITICAL: No Hallucinated Data
Every URL, every fact, every piece of content MUST come from actual browser navigation.
- You MUST use browser-use to visit real websites and find real URLs
- You MUST NOT generate YouTube URLs from memory — go to youtube.com and search
- You MUST NOT create fake tweet images — go to Twitter/X and screenshot real tweets
- You MUST NOT write research from LLM knowledge alone — browse real articles first
- If browser-use is not working, STOP and tell the user. Do NOT proceed with made-up data.
Browser-use is your internet. It handles both discovery (searching Google, YouTube, Twitter) AND capture (screenshots, scroll recordings). There is no other search tool.
Video Structure (fixed, deterministic)
Every news digest follows the same structure:
Title card (4s) → Intro voiceover
→ News clip 1 (hook + clip) → News clip 2 (hook + clip) → News clip 3 (hook + clip)
→ Tweet transition → Tweet 1 → Tweet 2 → Tweet 3
→ Article transition → Article 1 → Article 2
→ Outro (5s)
Total: ~3-4 minutes (varies based on generated voiceover lengths)Note: Only the title card (4s) and outro (5s) have fixed durations. Everything else is dynamically sized based on the actual voiceover durations returned by VideoDB TTS. The build_video.py template reads exact durations from registry.json — nothing is hardcoded.
Inputs
The only required input is:
- topic — e.g., "iran war", "claude source code leak", "us tariffs china"
Optional:
- Custom background image (default: shipped
assets/news-bg.jpg) - Custom background music (default: shipped
assets/news-music.mp3)
Workflow
Step 1: Research the topic (USE BROWSER-USE)
Use browser-use to actually browse the internet and gather real information:
1. Open Google → search <topic> news coverage 2024/2025/2026 2. Visit 2-3 top results from trusted news sites 3. Read the actual article content 4. Extract key facts: what happened, who's involved, timeline, impact
Do NOT write research from LLM memory. Every fact in research.md must trace back to a real URL you visited.
Save to outputs/<topic-slug>/data/research.md — include source URLs for every claim.
Step 2: Find 3 YouTube news videos (USE BROWSER-USE)
Use browser-use to navigate to YouTube and find real videos:
1. Open youtube.com → search <topic> BBC News 2. Click on a real result → copy the URL from the browser 3. Repeat for 2 more channels
Preferred YouTube channels to search:
- Al Jazeera English
- BBC News
- CNN
- Reuters
- CNBC
- DW News
- Sky News
- PBS NewsHour
Search strategy: Go to youtube.com and search for each channel + topic. Do NOT construct URLs from memory.
Criteria:
- 3 different news sources for diverse perspectives
- Recent and relevant to the topic
- 2-15 minutes long, English language
- Each should present a distinct "take" or angle
- Every URL must come from actually visiting youtube.com — not from LLM knowledge
Save URLs to outputs/<topic-slug>/data/videos.json
Step 3: Process YouTube videos with VideoDB
For each of the 3 videos: 1. Upload to VideoDB: coll.upload(url=youtube_url) 2. Index spoken words: video.index_spoken_words(force=True) 3. Semantic search for the best ~25 second clip: video.search(query) 4. Select a clip that is self-contained, ends at a natural sentence, and shows the channel's perspective
Update registry.json with video IDs, clip start times, and durations.
Step 4: Capture 3 tweet screenshots (USE BROWSER-USE)
Use browser-use to visit X.com profiles of key figures and find real tweets:
1. Visit public profiles directly: x.com/<handle> (works without login in headless mode) 2. Find 3 tweets from notable people with different perspectives 3. Screenshot each tweet via the embed endpoint: platform.twitter.com/embed/Tweet.html?id=<ID>
Note: X.com search (x.com/search?q=...) requires login and won't work. Stick to visiting profiles directly. See AGENTS.md Step 4 for the full workflow.
Do NOT create fake/placeholder tweet images. Every screenshot must be of a real tweet from a real browser session.
Save to outputs/<topic-slug>/assets/tweets/tweet_1.png, tweet_2.png, tweet_3.png Upload each to VideoDB and update data/registry.json.
Step 5: Capture 2 article assets (USE BROWSER-USE)
Use browser-use to find and capture real articles:
1. Open Google → search <topic> site:apnews.com (or other preferred sites) 2. Visit the actual article in the browser 3. Screenshot the key section (chart, data, key quote) 4. Record a scroll video (~8-13 seconds) by scrolling through the article
Preferred article sources (search these first):
- AP News (apnews.com)
- Council on Foreign Relations (cfr.org)
- Reuters (reuters.com)
- BBC News (bbc.com/news)
- The Guardian (theguardian.com)
- NPR (npr.org)
Do NOT create fake scroll videos with ffmpeg from static images. The scroll must be a real browser recording of a real article.
Criteria:
- Article 1: economic/domestic impact angle
- Article 2: historical context / geopolitical analysis angle
Save to outputs/<topic-slug>/assets/articles/ Update data/registry.json.
Step 6: Write scripts & generate voiceovers
Write 11 voiceover scripts based on research and collected sources:
| Script | Guideline | Content |
|---|---|---|
| intro | 2-3 sentences | Quick hook — what happened, why it matters |
| hook_1 | 1-2 sentences | Introduce news source 1, what they report |
| hook_2 | 1-2 sentences | Introduce news source 2, what they report |
| hook_3 | 1-2 sentences | Introduce news source 3, what they report |
| tweet_transition | 1-2 sentences | Bridge to social media reactions |
| tweet_1 | 2-3 sentences | Narrate tweet 1 content and context |
| tweet_2 | 2-3 sentences | Narrate tweet 2 content and context |
| tweet_3 | 2-3 sentences | Narrate tweet 3 content and context |
| article_transition | 1 sentence | Bridge to deep analysis section |
| article_1 | 3-4 sentences | Summarize article 1 key findings |
| article_2 | 3-4 sentences | Summarize article 2 key findings |
Tone: Professional broadcast news narrator. Concise, factual, no speculation.
Duration note: Do NOT target specific second counts. Write natural scripts following the sentence guidelines above. Generate via TTS, then record the exact duration returned by audio.length into registry.json. The build template handles all timing dynamically.
Generate each using VideoDB TTS: coll.generate_voice(text=script, voice_name="Default")
Save scripts to data/scripts.json. Update data/registry.json with audio IDs and exact durations (from audio.length).
Step 7: Build the video
Run the build template:
python templates/build_video.py outputs/<topic-slug>/data/registry.jsonThis assembles the 5-track timeline and generates the final stream URL. Output saved to outputs/<topic-slug>/output/output.json.
Per-run folder structure
All runs go inside outputs/ within the skill directory. Never create run folders at the skill root. Never place loose files at the topic folder root — everything goes in a subfolder.
agentic-video-creator/news-digest/
├── SKILL.md
├── AGENTS.md
├── README.md
├── assets/ # Shipped defaults (bg, music)
├── templates/
└── outputs/ # ALL runs go here
└── <topic-slug>/ # e.g., "epstein-files"
│
├── assets/ # Physical files (viewable media)
│ ├── tweets/ # Tweet screenshot PNGs
│ │ ├── tweet_1.png
│ │ ├── tweet_2.png
│ │ └── tweet_3.png
│ └── articles/ # Article scroll videos + screenshots
│ ├── article_1_scroll.mp4
│ ├── article_1_screenshot.png
│ ├── article_2_scroll.mp4
│ └── article_2_screenshot.png
│
├── data/ # Digital metadata (IDs, scripts, research)
│ ├── research.md # Topic research brief
│ ├── scripts.json # All 11 voiceover scripts
│ ├── registry.json # All VideoDB asset IDs + durations
│ └── videos.json # YouTube URLs + clip metadata
│
├── _build/ # Intermediate/working files (agent scratch)
│ └── ... # Upload results, processing logs, etc.
│
└── output/ # Final deliverables
└── output.json # Stream URL, player URL, durationRules:
assets/= physical media files you can open and previewdata/= metadata, IDs, scripts — everything VideoDB and the build template need_build/= intermediate files created during processing (prefixed with_to signal "ignore")output/= the final video URL and metadata- Zero files at the topic root — everything in a subfolder
Shipped assets
Default background and music are in assets/:
assets/news-bg.jpg— Professional dark news backgroundassets/news-music.mp3— News-style background music (125s, looped at 15% volume)
Users can replace these with their own files. Upload to VideoDB and update registry.
Deliverables
A successful run produces:
output/output.jsonwith playable stream URL- All physical media preserved in
assets/ - Full
data/registry.jsonfor reproducibility
Reference
See outputs/ folder for complete agent-generated examples with full registry structure and asset organization.
Example output: Iran War 2026 Video
Reference docs
README.md— Overview, setup, and usage guideAGENTS.md— Detailed agent workflow, error handling, quality criteriatemplates/build_video.py— Timeline builder (reads registry.json)
News Digest Video — Agent Instructions
Detailed workflow for agents building news digest videos. Read SKILL.md first for the overview.
CRITICAL RULE: No Hallucinated Data
You MUST use browser-use for ALL internet research. This means:
- Every YouTube URL must come from navigating to youtube.com and searching
- Every tweet screenshot must come from navigating to Twitter/X
- Every article must come from navigating to the actual news site
- Research facts must come from reading real web pages, not LLM memory
If browser-use is not available or not working, STOP immediately and tell the user. Do NOT proceed by generating URLs from memory or creating placeholder images. The entire value of this video is real, sourced content.
Prerequisites
- VideoDB SDK installed:
pip install videodb python-dotenv VIDEO_DB_API_KEYset in environment or.envfile- Browser use skill installed and working — this is your only way to access the internet
- Read
~/.claude/skills/browser-use/SKILL.mdbefore starting - Note: Chrome
--profileflag requires Google Chrome (not Chromium). If you need authenticated sessions, install Chrome first.
CRITICAL: Close Popups/Ads Before Screenshots
Before taking ANY screenshot or starting ANY scroll recording, you MUST close all popups, overlays, and ads.
Common overlays to close:
- Cookie consent banners
- Newsletter signup popups
- Donation prompts (especially Guardian, Wikipedia)
- "Support us" overlays
- Privacy policy notices
- Mobile app download prompts
How to close overlays:
1. Wait for page to fully load: sleep 2 after navigation 2. Identify overlay elements: browser-use state to see clickable elements 3. Close via clicking: browser-use click <close-button-index> 4. Or hide via JavaScript:
browser-use eval "document.querySelectorAll('[class*=\"modal\"], [class*=\"popup\"], [class*=\"overlay\"]').forEach(el => el.remove())"Example workflow:
# Navigate to article
browser-use open "https://www.theguardian.com/us-news/2026/..."
# Wait for popups to appear
sleep 2
# Remove overlays
browser-use eval "document.querySelectorAll('[class*=\"Support\"], [id*=\"banner\"]').forEach(el => el.remove())"
# Now screenshot/scroll
browser-use screenshot article_screenshot.pngQuality check: Before saving, visually verify no overlays are visible in the captured image/video.
Step-by-Step Workflow
Step 1: Research Topic (BROWSER-USE REQUIRED)
Goal: Build enough context to write accurate voiceover scripts later.
You MUST use browser-use to do real web research. Do NOT write this from LLM memory.
CRITICAL: Do NOT use search engines (Google, DuckDuckGo, Bing). They show CAPTCHAs in headless mode.
Instead, navigate directly to news site search pages:
1. BBC search: browser-use open "https://www.bbc.com/search?q=<topic>" 2. AP News hub or search: browser-use open "https://apnews.com/hub/<topic-slug>" or "https://apnews.com/search?q=<topic>" 3. NBC News search: browser-use open "https://www.nbcnews.com/search/?q=<topic>" 4. The Guardian: browser-use open "https://www.theguardian.com/us-news" (then search or browse)
Navigate to articles:
- Use
browser-use stateto see clickable element indices - Use
browser-use click <index>to navigate to articles - Do NOT hardcode BBC article URLs — they often land on generic pages. Always navigate through BBC search results.
Extract article text:
# Get clean text (first 5000 chars)
browser-use eval "document.querySelector('article')?.innerText?.substring(0, 5000)"
# For longer articles, paginate
browser-use eval "document.querySelector('article')?.innerText?.substring(5000, 10000)"
browser-use eval "document.querySelector('article')?.innerText?.substring(10000, 15000)"Visit 2-3 trusted sources and extract key facts.
Write a concise brief (10-20 lines) to outputs/<topic-slug>/data/research.md. Include:
- What happened (key events in chronological order)
- Who is involved (countries, leaders, organizations)
- Why it matters (impact, stakes)
- Current status
- Source URLs for every claim (the actual pages you visited)
This file is your reference for all script writing later. Every fact must trace to a real URL.
---
Step 2: Find 3 YouTube News Videos (BROWSER-USE REQUIRED)
Goal: Find 3 real videos from different trusted sources showing diverse perspectives.
You MUST use browser-use to navigate to YouTube and search. Do NOT construct YouTube URLs from memory.
How to find videos:
YouTube works fine in headless mode (no CAPTCHA). Use direct YouTube search:
browser-use open "https://www.youtube.com/results?search_query=<topic>+BBC+News+2026"Extract video URLs:
browser-use eval "Array.from(document.querySelectorAll('a#video-title')).slice(0, 5).map(a => ({title: a.title, href: a.href}))"This returns an array of {title, href} objects with real YouTube URLs.
Preferred channels to search:
- Al Jazeera English, BBC News, CNN, Reuters, CNBC, DW News, Sky News, PBS NewsHour
If a channel has no results: Try the next channel. Keep going until you have 3 real URLs.
Selection criteria:
- 3 different news organizations (never 2 from the same source)
- Each presents a distinct perspective or angle
- Video is 2-15 minutes long (enough content to find a good 25s clip)
- English language
- Recent and directly about the topic
- The URL must come from YouTube search results you actually saw in the browser
Save to outputs/<topic-slug>/data/videos.json:
{
"video_1": { "url": "https://youtube.com/watch?v=...", "source": "Al Jazeera", "title": "..." },
"video_2": { "url": "https://youtube.com/watch?v=...", "source": "BBC News", "title": "..." },
"video_3": { "url": "https://youtube.com/watch?v=...", "source": "CNN", "title": "..." }
}---
Step 3: Process YouTube Videos with VideoDB
Goal: Upload, index, and find the best ~25 second clip from each video.
For each video:
from dotenv import load_dotenv
load_dotenv(".env")
import videodb
from videodb import SearchType
conn = videodb.connect()
coll = conn.get_collection()
# Upload
video = coll.upload(url="https://youtube.com/watch?v=...")
# Index transcript
video.index_spoken_words(force=True)
# Search for best segment — KEEP QUERIES SHORT (3-5 words)
# Long queries often return "No results found"
# Good: "climate summit agreement", "tech regulation policy"
# Bad: "climate summit leaders powerful nations agreement emissions targets"
results = video.search("climate summit agreement", search_type=SearchType.semantic)
shots = results.get_shots()
# Pick the best shot, trim to ~25 seconds
shot = shots[0]
clip_start = float(shot.start)
clip_duration = min(float(shot.end) - float(shot.start), 25.0)Clip selection criteria:
- ~25 seconds (20-30s acceptable)
- Starts and ends at natural sentence boundaries
- Self-contained (makes sense without surrounding context)
- Shows the source's unique perspective/angle
- No dead air, no ads, no off-topic segments
Error handling:
- If YouTube upload fails with "Download failed": try an alternate URL from the same source. Some YouTube videos have download restrictions. Try 2-3 different videos per channel before giving up on that channel.
- If a channel's videos consistently fail: swap to another preferred channel (e.g., if CNN fails, try CNBC or DW News)
- If semantic search returns "No results found": Your query is too long or specific. Retry with a shorter, more generic query (3-5 words). Example: change "climate summit leaders powerful nations emissions targets" → "climate summit agreement"
- If clip is too short (<15s): widen the selection or try a different search query
- CRITICAL: Do NOT skip news clips entirely. The 3 YouTube clips are the core of the video. Keep trying different URLs until you have 3 working clips.
Update registry.json with the video_id, clip_start, clip_duration, and label.
---
Step 4: Capture 3 Tweet Screenshots
Goal: Find and screenshot 3 high-profile tweets showing different reactions.
CRITICAL UPDATE: X.com public profiles work perfectly in headless mode without login. Only the search feature requires login.
What works:
- ✅ Profile pages:
x.com/ZelenskyyUa,x.com/elonmusk, etc. (full access, no login) - ✅ Embed endpoint:
platform.twitter.com/embed/Tweet.html?id=<ID>(for clean screenshots) - ❌ Search pages:
x.com/search?q=...(requires login, doesn't work in headless mode)
Recommended workflow (fastest):
Step A: Identify relevant X handles from research
From your research step, identify 3-5 key figures related to your topic. Examples:
- Government officials/leaders (presidents, foreign ministers, press secretaries)
- Key organizations (official accounts like @WhiteHouse, @UN, @NATO)
- Prominent journalists covering the story
- Affected parties or their representatives
Step B: Visit X.com profiles directly
browser-use open "https://x.com/ZelenskyyUa"The profile loads fully without login. You can see all recent tweets.
Step C: Extract tweet IDs from the profile
browser-use eval "Array.from(document.querySelectorAll('a[href*=\"/status/\"]')).slice(0, 10).map(a => ({text: a.closest('article')?.innerText?.substring(0, 200) || 'No preview', href: a.href}))"This returns the 10 most recent tweets with preview text and full URLs.
Pick 3 tweets that show different perspectives on your topic.
Step D: Screenshot via Twitter embed endpoint
For each tweet ID (extract from URL like x.com/user/status/1234567890):
browser-use open "https://platform.twitter.com/embed/Tweet.html?id=1234567890"
sleep 1 # wait for embed to load
browser-use screenshot /tmp/tweet_raw.pngCrop with PIL to remove excess whitespace:
from PIL import Image
img = Image.open("/tmp/tweet_raw.png")
cropped = img.crop((0, 0, 560, 480)) # Adjust as needed
cropped.save("outputs/<topic-slug>/assets/tweets/tweet_1.png")Fallback: NBC News articles (if profile approach fails)
If you can't find relevant handles or profiles are private, fall back to extracting tweet links from NBC News articles:
browser-use open "https://www.nbcnews.com/search/?q=<topic>"
# Click into articles and extract x.com links
browser-use eval "Array.from(document.querySelectorAll('a')).filter(a => a.href.includes('x.com/') && a.href.includes('/status/')).map(a => a.href)"Note: Most NBC articles don't embed tweet links (hit rate ~10%), so this is less reliable than visiting profiles directly.
Find 3 tweets from different perspectives:
- A key political leader or official directly involved
- A critic or analyst offering an opposing view
- A supporter/defender offering a contrasting take
Quality criteria:
- From verified/notable accounts
- Directly about the topic (not tangential)
- Different viewpoints (avoid 3 tweets saying the same thing)
- Clean screenshot (no browser chrome, no other tweets visible)
- CLOSE ALL POPUPS before screenshotting (see popup section above)
Save as:
outputs/<topic-slug>/assets/tweets/tweet_1.png
outputs/<topic-slug>/assets/tweets/tweet_2.png
outputs/<topic-slug>/assets/tweets/tweet_3.pngUpload each to VideoDB:
img = coll.upload(file_path="outputs/<topic-slug>/assets/tweets/tweet_1.png")
# Record img.id in data/registry.json---
Step 5: Capture 2 Article Assets
Goal: Find 2 trusted articles and capture scroll videos + key screenshots.
CRITICAL: Do NOT use search engines (Google, DuckDuckGo, Bing). They show CAPTCHAs.
Navigate directly to news site search pages:
# AP News
browser-use open "https://apnews.com/search?q=<topic>"
# BBC News
browser-use open "https://www.bbc.com/search?q=<topic>"
# The Guardian
browser-use open "https://www.theguardian.com/us-news" # or /world
# NBC News
browser-use open "https://www.nbcnews.com/search/?q=<topic>"
# Reuters
browser-use open "https://www.reuters.com/site-search/?query=<topic>"
# NPR
browser-use open "https://www.npr.org/search?query=<topic>"Article selection:
- Article 1: Economic/domestic impact angle (how does this affect markets, people, daily life?)
- Article 2: Historical/geopolitical context (how did we get here? what's the broader picture?)
For each article, use the browser-use skill:
1. Record scroll video (~10 seconds):
CRITICAL: `browser-use` Python doesn't support async screen recording. Use frame capture + ffmpeg instead:
# Navigate and wait for page load
browser-use open "https://apnews.com/article/..."
sleep 2
# CLOSE ALL POPUPS FIRST (see popup section above)
browser-use eval "document.querySelectorAll('[class*=\"modal\"], [class*=\"banner\"]').forEach(el => el.remove())"
# VERIFY PAGE CONTENT IS RELEVANT
# Before capturing, check that the article actually loaded (not "404" or "article not found")
browser-use eval "document.body.innerText.substring(0, 500)"
# Review output: Does it match your topic? Is it article content or an error page?
# If you see "not found", "404", or generic news listings → navigate to a different article
# Scroll to top
browser-use eval "window.scrollTo(0, 0)"
# Capture 10 frames while scrolling
for i in $(seq 0 9); do
browser-use screenshot "/tmp/scroll_frame_${i}.png"
browser-use scroll down --amount 300
sleep 0.3
done
# Stitch into video with ffmpeg
ffmpeg -y -framerate 1 -i /tmp/scroll_frame_%d.png \
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" \
-c:v libx264 -pix_fmt yuv420p -r 30 \
outputs/<topic-slug>/assets/articles/article_1_scroll.mp4This creates a 10-second video (10 frames at 1fps, encoded at 30fps).
2. Capture key screenshot:
Navigate to the most impactful section (chart, key quote, headline), close all popups, then screenshot:
browser-use screenshot outputs/<topic-slug>/assets/articles/article_1_screenshot.pngSave to:
outputs/<topic-slug>/assets/articles/article_1_scroll.mp4
outputs/<topic-slug>/assets/articles/article_1_screenshot.png
outputs/<topic-slug>/assets/articles/article_2_scroll.mp4
outputs/<topic-slug>/assets/articles/article_2_screenshot.pngUpload all 4 files to VideoDB and update registry.json.
---
Step 6: Write Scripts & Generate Voiceovers
Goal: Write 11 voiceover scripts and generate TTS audio for each.
Use research.md and your collected sources as context.
Script writing guidelines:
- Professional broadcast news tone
- Concise and factual — no speculation, no opinion
- Intro: 2-3 sentences, sets up the whole story as a quick hook
- Hooks: 1-2 sentences, introduce what the news source reports
- Tweet narrations: Read the tweet content with brief context
- Article summaries: Key findings in 3-4 sentences
- Transitions: Short bridge sentences connecting sections
Generate each voiceover:
voice = coll.generate_voice(text=script, voice_name="Default")
# IMPORTANT: voice.length returns a STRING, not a float
# Cast to float before using in calculations or formatting
audio_id = voice.id
exact_duration = float(voice.length) # This is the ground truthSave all scripts to outputs/<topic-slug>/data/scripts.json:
{
"intro": "[Date]. [Major event description]. [Key parties involved]. [What's at stake].",
"hook_1": "[Source name] reports that [their key perspective]...",
...
}Update registry.json voiceovers section with:
id— VideoDB audio asset IDduration— exact duration fromfloat(voice.length)(NOT estimated, cast to float)script— the text used
Example:
"intro": {
"id": "a-z-019d...",
"duration": 16.431020,
"script": "February 2026. Major event description..."
}---
Step 7: Build the Video
Goal: Assemble the final video using the build template.
python templates/build_video.py outputs/<topic-slug>/data/registry.jsonThis reads data/registry.json, builds a 5-track timeline, and generates the stream.
Output is saved to outputs/<topic-slug>/output/output.json:
{
"stream_url": "https://play.videodb.io/v1/...",
"player_url": "https://console.videodb.io/player?url=...",
"duration_seconds": 211.9,
"duration_formatted": "3:31"
}Note: The code outputs console URLs. Convert to player URLs (https://player.videodb.io/watch?v=...) manually when sharing publicly.
If the build fails:
Clip duration greater than audio length→ Yourregistry.jsonduration is wrong. Re-checkvoice.lengthfor that audio ID.Invalid request→ Check that all asset IDs in registry.json are valid and uploaded.
---
Registry Schema
registry.json is the single source of truth. Every asset ID, every duration, everything the build script needs.
{
"topic": "string — the topic name",
"created": "YYYY-MM-DD",
"background": {
"image_id": "img-z-... — VideoDB image ID for background",
"music_id": "a-z-... — VideoDB audio ID for background music",
"music_duration": 125.257
},
"videos": {
"video_1": {
"label": "AL JAZEERA — displayed as text overlay",
"source_url": "YouTube URL",
"video_id": "m-z-... — VideoDB video ID",
"clip_start": 10.6,
"clip_duration": 25.8
},
"video_2": { "..." : "same structure" },
"video_3": { "..." : "same structure" }
},
"tweets": {
"tweet_1": { "image_id": "img-z-..." },
"tweet_2": { "image_id": "img-z-..." },
"tweet_3": { "image_id": "img-z-..." }
},
"articles": {
"article_1": {
"scroll_id": "m-z-... — VideoDB video ID of scroll recording",
"scroll_duration": 8.0,
"screenshot_id": "img-z-... — VideoDB image ID of key screenshot"
},
"article_2": { "..." : "same structure" }
},
"voiceovers": {
"intro": { "id": "a-z-...", "duration": 10.213, "script": "..." },
"hook_1": { "id": "a-z-...", "duration": 8.855, "script": "..." },
"hook_2": { "id": "a-z-...", "duration": 7.889, "script": "..." },
"hook_3": { "id": "a-z-...", "duration": 7.758, "script": "..." },
"tweet_transition": { "id": "a-z-...", "duration": 8.907, "script": "..." },
"tweet_1": { "id": "a-z-...", "duration": 13.557, "script": "..." },
"tweet_2": { "id": "a-z-...", "duration": 15.360, "script": "..." },
"tweet_3": { "id": "a-z-...", "duration": 11.363, "script": "..." },
"article_transition": { "id": "a-z-...", "duration": 6.817, "script": "..." },
"article_1": { "id": "a-z-...", "duration": 16.326, "script": "..." },
"article_2": { "id": "a-z-...", "duration": 19.591, "script": "..." }
}
}---
Background Assets
Default background image and music are shipped in assets/:
assets/news-bg.jpg— dark professional news backgroundassets/news-music.mp3— news-style music (125.3s)
First-time setup: Upload both to VideoDB and record their IDs.
bg_img = coll.upload(file_path="assets/news-bg.jpg")
bg_music = coll.upload(file_path="assets/news-music.mp3")These IDs can be reused across multiple runs (same VideoDB collection). Store them at the top of each registry.json.
---
Quality Checklist
Before delivering the final video:
- [ ] 3 YouTube clips from 3 different sources
- [ ] Each clip is ~25s, self-contained, ends naturally
- [ ] 3 tweet screenshots are clean and from notable accounts
- [ ] 3 tweets show different perspectives
- [ ] 2 articles from trusted sources, different angles
- [ ] Scroll videos are smooth, 8-13 seconds
- [ ] All 11 voiceover scripts are factual and concise
- [ ] All durations in registry.json match actual
voice.lengthvalues - [ ] Build completes without errors
- [ ] Video plays correctly at the output URL
News Digest Video Agent
Autonomous multi-source news video creator — give it a topic, get a professional 3-4 minute broadcast-style video.

---
What It Does
This skill autonomously creates professional news digest videos from any topic by:
1. Researching the topic via real web sources (BBC, AP News, NBC, Guardian) 2. Finding 3 YouTube news clips from diverse sources (Al Jazeera, BBC, CNN, etc.) 3. Capturing 3 tweet screenshots showing different perspectives 4. Recording 2 article scroll videos with key screenshots 5. Writing 11 voiceover scripts and generating TTS audio 6. Assembling everything into a polished 5-track timeline with background music
Output: A 3-4 minute video with title card, intro, news clips, social media reactions, deep analysis, and outro.
---
Quick Start
Prerequisites
1. VideoDB SDK:
pip install videodb python-dotenv2. API Key (get free at console.videodb.io):
export VIDEO_DB_API_KEY=your_key_here3. Browser-use skill (for web research):
mkdir -p ~/.claude/skills/browser-use
curl -o ~/.claude/skills/browser-use/SKILL.md \
https://raw.githubusercontent.com/browser-use/browser-use/main/skills/browser-use/SKILL.mdUsage
Just give the agent a news topic:
Create a news digest video about the Iran-US conflictMake a news digest covering the recent tech regulation summitThe agent will autonomously:
- Research the topic from trusted sources
- Find and process YouTube videos
- Capture tweets and article assets
- Write scripts and generate voiceovers
- Build and deliver the final video
Output location: outputs/<topic-slug>/output/output.json
---
File Structure
news-digest/
├── SKILL.md # Main skill definition (agents read this)
├── AGENTS.md # Detailed workflow instructions
├── README.md # This file
│
├── assets/ # Shipped defaults
│ ├── news-bg.jpg # Professional dark news background
│ └── news-music.mp3 # Background music (125s, 15% volume)
│
├── templates/
│ └── build_video.py # Timeline builder (reads registry.json)
│
└── outputs/ # Agent-generated runs (one folder per topic)
└── <topic-slug>/
├── data/ # Metadata (registry.json, scripts.json, research.md)
├── assets/ # Physical media (tweets/, articles/)
├── _build/ # Intermediate scratch files
└── output/ # Final video URL---
Video Structure
Every news digest follows this deterministic structure:
[4s] Title card: "<TOPIC>\nNEWS DIGEST"
[~10s] Intro voiceover (with first tweet visual)
[~8s] Hook 1 voiceover + [3s] news source label + [~5s] muted preview + [25s] news clip 1
[~8s] Hook 2 voiceover + [3s] news source label + [~5s] muted preview + [25s] news clip 2
[~8s] Hook 3 voiceover + [3s] news source label + [~5s] muted preview + [25s] news clip 3
[~8s] Tweet transition: "SOCIAL MEDIA\nREACTIONS"
[~12s] Tweet 1 narration + visual
[~12s] Tweet 2 narration + visual
[~12s] Tweet 3 narration + visual
[~6s] Article transition: "DEEP ANALYSIS"
[~17s] Article 1 narration (scroll video + key screenshot)
[~17s] Article 2 narration (scroll video + key screenshot)
[5s] Outro: "POWERED BY VIDEODB"Total: ~3-4 minutes (varies based on TTS durations)
---
Technical Details
Timeline Architecture
5-track composition: 1. bg_track — Background image (full duration, Fit.crop) 2. visual_track — Content: images/videos (scale 0.75, Fit.contain, centered) 3. text_track — Section labels (white text on blue box, auto-sized) 4. audio_track — Voiceovers (full volume) 5. music_track — Background music (15% volume, looped)
Text Styling
"Option C" design:
- Font: Clear Sans, 80-96pt, white (#FFFFFF)
- Background: Blue box (#1a3a5c, 90% opacity), auto-sized to text length
- Border: White 2px
- Shadow: Black 3px offset
- Auto-wrapping: Lines longer than 35 chars wrap at word boundaries
Asset Requirements
- 3 YouTube videos — Different news sources, 2-15 min length, English
- 3 tweet screenshots — Different perspectives (leader, critic, supporter)
- 2 article assets — One economic/impact, one historical/context
- 11 voiceover scripts — Intro, 3 hooks, tweet transition, 3 tweet narrations, article transition, 2 article summaries
---
Quality Standards
Every output includes:
- 3 YouTube clips from different news sources (~25s each, self-contained)
- 3 tweet screenshots from notable accounts showing diverse perspectives
- 2 articles from trusted sources with scroll videos and key screenshots
- 11 professionally narrated voiceover segments
- Clean visuals with no popups or error pages
- Accurate metadata in registry.json
---
Key Features
Real Web Research
- Navigates directly to trusted news sources (BBC, AP News, NBC, Guardian)
- Finds YouTube videos from major news channels
- Captures tweets from public X.com profiles
- Verifies article content before capturing assets
Deterministic Output
- Same structure every time
- Predictable timeline architecture
- Consistent quality standards
Professional Quality
- Broadcast-style text overlays with auto-sizing
- Clean typography and smooth transitions
- Background music at optimal volume
- Natural voiceover pacing
---
Example Output
Iran War 2026 — Multi-source analysis with Al Jazeera, BBC, CNN clips: ▶ Watch Video
For complete registry structure and asset organization, see agent-generated runs in outputs/ folder.
---
Community & Support
- Docs: docs.videodb.io
- Issues: GitHub Issues
- Discord: Join community
- Console: Get API key
---
<p align="center">Made with ❤️ by the <a href="https://videodb.io">VideoDB</a> team</p>
---
"""
News Digest Video Builder
Reads registry.json and assembles a professional news digest video using VideoDB.
Usage:
python build_video.py <path-to-registry.json>
Architecture: 5-track timeline
1. bg_track — Background image (full duration, Fit.crop)
2. visual_track — Content: images/videos (scale 0.75, Fit.contain, centered)
3. text_track — Section labels (Option C: white on blue box)
4. audio_track — Voiceovers (full volume)
5. music_track — Background music (15% volume, looped)
Structure: title (4s) → intro → 3 news clips → 3 tweets → 2 articles → outro (5s)
"""
import json
import sys
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(".env")
import videodb
from videodb.editor import (
Timeline, Track, Clip,
VideoAsset, ImageAsset, AudioAsset, TextAsset,
Fit, Position, Transition,
Font, Border, Shadow, Background, TextAlignment,
)
# ============================================================
# CONSTANTS
# ============================================================
TITLE_DUR = 4.0
OUTRO_DUR = 5.0
LABEL_DUR = 3.0 # text label shown before muted video preview during hooks
SCALE = 0.75 # content scale (padding around visuals)
FADE = Transition(in_="fade", out="fade", duration=0.5)
FADE_IN = Transition(in_="fade", duration=0.5)
FADE_OUT = Transition(out="fade", duration=0.5)
def make_text(text, size=88, width=None, height=None, max_chars_per_line=35):
"""Option C style: white text, wide blue box, white border, shadow.
Auto-sizes background box to fit text if width/height not specified.
Long single lines are automatically wrapped to max_chars_per_line.
Formula: width ≈ longest_line × size × 0.65 + 40px padding
height ≈ line_count × size × 1.3 + 30px padding
"""
# Auto-wrap long lines
lines = text.split('\n')
wrapped_lines = []
for line in lines:
if len(line) > max_chars_per_line:
# Wrap long line into chunks
words = line.split(' ')
current_line = []
for word in words:
test_line = ' '.join(current_line + [word])
if len(test_line) <= max_chars_per_line:
current_line.append(word)
else:
if current_line:
wrapped_lines.append(' '.join(current_line))
current_line = [word]
if current_line:
wrapped_lines.append(' '.join(current_line))
else:
wrapped_lines.append(line)
text = '\n'.join(wrapped_lines)
if width is None:
lines = text.split('\n')
longest_line = max(len(line) for line in lines)
width = int(longest_line * size * 0.65 + 40)
width = max(width, 400) # minimum 400px
width = min(width, 1600) # maximum 1600px (safe for 1920px screen)
if height is None:
line_count = text.count('\n') + 1
height = int(line_count * size * 1.3 + 30)
height = max(height, 100) # minimum 100px
height = min(height, 500) # maximum 500px (allow taller for wrapped text)
return TextAsset(
text=text,
font=Font(family="Clear Sans", size=size, color="#FFFFFF", opacity=1.0),
background=Background(
width=width,
height=height,
color="#1a3a5c",
opacity=0.90,
text_alignment=TextAlignment.center,
),
border=Border(color="#FFFFFF", width=2.0),
shadow=Shadow(color="#000000", x=3, y=3),
)
def build_video(registry_path: str) -> dict:
"""Build a news digest video from a registry.json file.
Args:
registry_path: Path to registry.json
Returns:
dict with stream_url, player_url, duration
"""
reg = json.loads(Path(registry_path).read_text())
conn = videodb.connect()
bg = reg["background"]
vids = reg["videos"]
tweets = reg["tweets"]
articles = reg["articles"]
vo = reg["voiceovers"]
# --- Calculate total duration ---
content_dur = (
TITLE_DUR
+ vo["intro"]["duration"]
+ vo["hook_1"]["duration"] + vids["video_1"]["clip_duration"]
+ vo["hook_2"]["duration"] + vids["video_2"]["clip_duration"]
+ vo["hook_3"]["duration"] + vids["video_3"]["clip_duration"]
+ vo["tweet_transition"]["duration"]
+ vo["tweet_1"]["duration"]
+ vo["tweet_2"]["duration"]
+ vo["tweet_3"]["duration"]
+ vo["article_transition"]["duration"]
+ vo["article_1"]["duration"]
+ vo["article_2"]["duration"]
)
total_dur = content_dur + OUTRO_DUR
# --- Build timeline ---
timeline = Timeline(conn)
timeline.background = "#0d0d1a"
timeline.resolution = "1920x1080"
bg_track = Track()
visual_track = Track()
text_track = Track()
audio_track = Track()
music_track = Track()
# Background image
bg_track.add_clip(0, Clip(
asset=ImageAsset(id=bg["image_id"]),
duration=total_dur,
fit=Fit.crop,
))
# Background music looped at 15%
music_len = bg["music_duration"]
music_loops = int(total_dur // music_len) + 1
for i in range(music_loops):
start = i * music_len
remaining = total_dur - start
d = min(music_len, remaining)
if d > 0:
music_track.add_clip(start, Clip(
asset=AudioAsset(id=bg["music_id"], volume=0.15),
duration=d,
))
t = 0.0
# --- TITLE CARD ---
text_track.add_clip(t, Clip(
asset=make_text(
reg["topic"].upper() + "\nNEWS DIGEST",
size=96, # auto-sizes width/height based on text length
),
duration=TITLE_DUR,
position=Position.center,
))
t += TITLE_DUR
# --- INTRO ---
audio_track.add_clip(t, Clip(
asset=AudioAsset(id=vo["intro"]["id"]),
duration=vo["intro"]["duration"],
))
# Show first tweet as intro visual
visual_track.add_clip(t, Clip(
asset=ImageAsset(id=tweets["tweet_1"]["image_id"]),
duration=vo["intro"]["duration"],
fit=Fit.contain, scale=SCALE, position=Position.center, transition=FADE,
))
t += vo["intro"]["duration"]
# --- 3 NEWS CLIPS ---
for i in range(1, 4):
vid = vids[f"video_{i}"]
hook = vo[f"hook_{i}"]
hook_dur = hook["duration"]
preview_dur = hook_dur - LABEL_DUR
clip_start = vid["clip_start"]
clip_dur = vid["clip_duration"]
preview_start = max(0, clip_start - preview_dur)
# Text label (3s)
text_track.add_clip(t, Clip(
asset=make_text(vid["label"], size=88), # auto-sizes
duration=LABEL_DUR,
position=Position.center,
))
# Hook voiceover
audio_track.add_clip(t, Clip(
asset=AudioAsset(id=hook["id"]),
duration=hook_dur,
))
# Muted video preview after text
visual_track.add_clip(t + LABEL_DUR, Clip(
asset=VideoAsset(id=vid["video_id"], start=preview_start, volume=0),
duration=preview_dur,
fit=Fit.contain, scale=SCALE, position=Position.center, transition=FADE_IN,
))
t += hook_dur
# Actual clip with original audio
visual_track.add_clip(t, Clip(
asset=VideoAsset(id=vid["video_id"], start=clip_start, volume=1),
duration=clip_dur,
fit=Fit.contain, scale=SCALE, position=Position.center, transition=FADE,
))
t += clip_dur
# --- SOCIAL MEDIA ---
text_track.add_clip(t, Clip(
asset=make_text("SOCIAL MEDIA\nREACTIONS", size=80), # auto-sizes
duration=vo["tweet_transition"]["duration"],
position=Position.center,
))
audio_track.add_clip(t, Clip(
asset=AudioAsset(id=vo["tweet_transition"]["id"]),
duration=vo["tweet_transition"]["duration"],
))
t += vo["tweet_transition"]["duration"]
for i in range(1, 4):
tw_vo = vo[f"tweet_{i}"]
visual_track.add_clip(t, Clip(
asset=ImageAsset(id=tweets[f"tweet_{i}"]["image_id"]),
duration=tw_vo["duration"],
fit=Fit.contain, scale=SCALE, position=Position.center, transition=FADE,
))
audio_track.add_clip(t, Clip(
asset=AudioAsset(id=tw_vo["id"]),
duration=tw_vo["duration"],
))
t += tw_vo["duration"]
# --- ARTICLES ---
text_track.add_clip(t, Clip(
asset=make_text("DEEP ANALYSIS", size=80), # auto-sizes
duration=vo["article_transition"]["duration"],
position=Position.center,
))
audio_track.add_clip(t, Clip(
asset=AudioAsset(id=vo["article_transition"]["id"]),
duration=vo["article_transition"]["duration"],
))
t += vo["article_transition"]["duration"]
for i in range(1, 3):
art = articles[f"article_{i}"]
art_vo = vo[f"article_{i}"]
art_dur = art_vo["duration"]
scroll_dur = art["scroll_duration"]
screenshot_dur = art_dur - scroll_dur
audio_track.add_clip(t, Clip(
asset=AudioAsset(id=art_vo["id"]),
duration=art_dur,
))
visual_track.add_clip(t, Clip(
asset=VideoAsset(id=art["scroll_id"], volume=0),
duration=scroll_dur,
fit=Fit.contain, scale=SCALE, position=Position.center, transition=FADE_IN,
))
visual_track.add_clip(t + scroll_dur, Clip(
asset=ImageAsset(id=art["screenshot_id"]),
duration=screenshot_dur,
fit=Fit.contain, scale=SCALE, position=Position.center, transition=FADE_OUT,
))
t += art_dur
# --- OUTRO ---
text_track.add_clip(t, Clip(
asset=make_text("POWERED BY VIDEODB", size=80), # auto-sizes
duration=OUTRO_DUR,
position=Position.center,
))
# --- ASSEMBLE ---
timeline.add_track(bg_track)
timeline.add_track(visual_track)
timeline.add_track(text_track)
timeline.add_track(audio_track)
timeline.add_track(music_track)
stream_url = timeline.generate_stream()
total = t + OUTRO_DUR
result = {
"stream_url": stream_url,
"player_url": f"https://console.videodb.io/player?url={stream_url}",
"duration_seconds": round(total, 1),
"duration_formatted": f"{int(total // 60)}:{int(total % 60):02d}",
}
return result
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python build_video.py <path-to-registry.json>")
sys.exit(1)
registry_path = sys.argv[1]
result = build_video(registry_path)
# Save output.json in output/ folder (sibling of data/)
topic_dir = Path(registry_path).parent.parent # data/ -> topic-slug/
output_dir = topic_dir / "output"
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "output.json"
output_path.write_text(json.dumps(result, indent=2))
print(json.dumps(result, indent=2))
print(f"\nPlayer: {result['player_url']}")