
Video Annotations
- 1 installs
- 7 repo stars
- Updated April 12, 2026
- isaac-flath/agent-starter-skills
Add animated annotations to video via the DaVinci Resolve Fusion API that reinforce the speaker's message.
About
Adds animated annotations to video via the DaVinci Resolve Fusion API to reinforce the speaker's message rather than decorate. A developer uses it when editing a video and wants annotations that clarify what is being said.
- Adds animated annotations that reinforce the speaker's message via DaVinci Resolve Fusion
- Prioritizes meaning over decoration with editorial principles
Video Annotations by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,200 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 8, 2026 (Skillselion catalog sync)
npx skills add https://github.com/isaac-flath/agent-starter-skills --skill video-annotationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 7 |
| Last updated | April 12, 2026 |
| Repository | isaac-flath/agent-starter-skills ↗ |
What it does
Add animated annotations to video via the DaVinci Resolve Fusion API that reinforce the speaker's message.
Files
Video Annotations Skill
Add animated annotations to videos that reinforce the speaker's message. Not decoration — communication. Read the top-level references/annotation-editorial-principles.md before making ANY annotation decisions.
The Most Important Rule
Every annotation must add clarity to the speaker's message. If it doesn't help the viewer understand what's being said, don't add it. Circling random things on screen is the weakest form of annotation. Prefer creating new visual content (animated text, symbols, conceptual drawings) over highlighting existing elements.
Prerequisites
- DaVinci Resolve Studio must be running
ffmpeg/ffprobe(frame extraction)uv(Python scripts for coordinate detection)- Gemini API key in
.env(GEMINI_API_KEY=...)
Annotation Types (ranked by editorial value)
| Rank | Type | Fusion Tool | When to use |
|---|---|---|---|
| 1 | Animated text | TextPlus | Speaker says key terms — write them on screen as they speak |
| 2 | Emphasis underline/circle | EllipseMask | One KEY word in a sentence needs emphasis |
| 3 | Relationship arrows | shapes + masks | Speaker describes flow/connection between things |
| 4 | Highlight box | RectangleMask | Region contains dense info viewer might miss |
Workflow
Step 1: Transcribe (MANDATORY first step)
Always transcribe before annotating. Annotations must match narration.
uv run .claude/skills/video-editor/scripts/transcribe.py <video_path>Step 2: Read transcript and make editorial decisions
Read the transcript with timestamps. For each potential annotation, apply the Transcript Test from references/annotation-editorial-principles.md: 1. What is the speaker's key point right now? 2. Does this annotation reinforce that exact point? 3. Would a viewer who only saw the annotation (no audio) understand the message?
Prefer fewer, higher-value annotations. 3-6 per 30 seconds max. Sometimes zero is right.
Step 3: Detect coordinates (when annotating existing elements)
# Extract frame at annotation timestamp
ffmpeg -ss <seconds> -i <video> -frames:v 1 -update 1 frame.png
# Gemini vision for text AND non-text elements
uv run .claude/skills/video-annotations/scripts/detect_bounds.py frame.png "target description"Step 4: Build annotations in Fusion
Use the fusion-animations skill for implementation details. Annotations are added as Fusion comp nodes on the timeline clip — they're editable in Resolve's UI.
Step 5: Verify with Gemini and self-review
Extract frames at each annotation timestamp. For each: 1. Look at the frame yourself — does the annotation land on the right element? 2. Send to Gemini for positioning critique 3. Adjust and re-check until every annotation is precisely placed
Gemini is good at positioning critique but bad at editorial judgment. Don't ask Gemini what to annotate — that's your job based on the transcript and editorial principles.
References
Editorial guidance (in top-level references/):
references/annotation-editorial-principles.md— Read this first. Editorial judgment, mobile-first sizing, what to annotate and why.references/visual-editing-principles.md— Overlay placement, caption design, mobile-first principles.
Implementation:
fusion-animations/SKILL.md— Fusion API recipes: draw-on circles, animated text, node wiring, coordinate system.references/technical-learnings.md— Hard-won technical notes: coordinate detection, Gemini usage patterns.
File Structure
scripts/
detect_bounds.py # Gemini vision coordinate detection
references/
technical-learnings.md # Coordinate detection, Gemini patterns, timing notes
mixedbread-doc-search.md # Vector search for reference docsBud1-runbwsp @� @� @� @test-runbwspblob�bplist00�]ShowStatusBar[ShowToolbar[ShowTabView_ContainerShowSidebar\WindowBounds[ShowSidebar _{{721, 590}, {920, 436}} #/;R_klmno�
�test-runlsvCblob5bplist00�
TUVWYXiconSize_showIconPreviewWcolumns_calculateAllSizes_scrollPositionYXtextSize_scrollPositionXZsortColumn_useRelativeDates_viewOptionsVersion#@0 �!%*/49=BFJN�ZidentifierUwidthYascendingWvisibleTname, �Xubiquity#�\dateModified� �"[dateCreated�&'Tsizea �+,Tkinds �01Ulabeld �56WversionK �:Xcomments �>?^dateLastOpened��C?ZshareOwner�G?_shareLastEditor�KYdateAdded�PR�_invitationStatus#@w0#@*#Tname &8@Tfo�������������"/123<HIJSXZ[\ejlmnw}������������������������
./8AJOPZatest-runlsvpblob�bplist00�
GHIJLXiconSize_showIconPreviewWcolumns_calculateAllSizes_scrollPositionYXtextSize_scrollPositionXZsortColumn_useRelativeDates_viewOptionsVersion#@0 � %*/48=AXcommentsUlabelWversion[dateCreatedTsize\dateModifiedTkindTname^dateLastOpened�WvisibleUwidthYascendingUindex, �"$d �')K �,.��13 a�,7 �:< s �@ �CE�#@w0#@*#Tname &8@Tfo������������(06@FGJKMVWYZ\efhiktuwxz�����������������������������M�test-runvSrnlongEDSDB `� @� @� @ewOptionsVersion#@0 � %*/48=AXcommentsUlabelWversion[dateCreatedTsize\dateModifiedTkindTname^dateLastOpened�WvisibleUwidthYascendingUindex, �"$d �')K �,.��13 a�,7 �:< s �@ �CE�#@w0#@*#Tname &8@Tfo������������(06@FGJKMVWYZ\efhiktuwxz�����������������������������M�test-runvSrnlong*.pyc
__pycache__/
3.11
Mixedbread Semantic Search for Reference Docs
Use Mixedbread vector stores to search over reference documentation (like the Fusion 8 Scripting Guide) with natural language queries. This is how to look things up when debugging or learning an API.
Setup
The mxbai CLI is pre-installed. API key is configured via:
mxbai config keys add <your_key>Or set MXBAI_API_KEY environment variable.
Workflow: Indexing a new reference doc
1. Create a store
mxbai store create "store-name"2. Upload documents
# Upload a PDF with high quality chunking
mxbai store upload "store-name" "/path/to/document.pdf" --strategy high_quality
# Upload multiple files with glob patterns
mxbai store upload "store-name" "docs/**/*.md" "*.pdf"The high_quality strategy produces better chunks but takes longer to process (~3-5 minutes for a 200-page PDF).
3. Wait for processing
# Check status
mxbai store files list "store-name" --format json
# Status will be "in_progress" then "completed"4. Search
# Semantic search
mxbai store search "store-name" "how to animate an ellipse mask"
# Get JSON output for programmatic use
mxbai store search "store-name" "Merge blend modes" --format json
# Ask a question (includes AI-generated answer)
mxbai store qa "store-name" "What are the valid ApplyMode values for a Merge tool?"Existing stores
| Store name | Contents | Use case |
|---|---|---|
fusion-scripting-docs | Fusion 8 Scripting Guide PDF | DaVinci Resolve Fusion API reference |
mgrep | Various project files | General codebase search |
Management commands
mxbai store list # List all stores
mxbai store get "store-name" # Get store details
mxbai store files list "store-name" # List files in store
mxbai store files delete "store-name" <id> # Delete a file
mxbai store delete "store-name" # Delete entire storeWhen to use this
- Debugging Fusion scripting: Search for tool properties, connection patterns, animation APIs
- Learning a new API: Index the docs, then ask natural language questions
- Any reference PDF/doc: Works with PDF, Markdown, text files
- Grounding AI answers: Search for specific API details rather than guessing
Tips
- Use
--strategy high_qualityfor technical docs — it produces better chunks for code-heavy content - The
qacommand gives an AI-synthesized answer;searchreturns raw chunks. Usesearchwhen you want exact doc text,qawhen you want a direct answer. - Processing large PDFs takes 3-5 minutes. Check with
mxbai store files listuntil status shows "completed".
Technical Learnings — Hard-Won Notes
Things that were painful to figure out. Read before building annotations.
Coordinate Detection
Use detect_bounds.py for coordinate detection
The scripts/detect_bounds.py script uses Gemini with structured prompting to return precise JSON bounding boxes for both text AND objects.
# Find specific target (text or object)
uv run scripts/detect_bounds.py frame.png "the robot character"
# Returns: [{"target": "the robot character", "x": 411, "y": 463, "width": 171, "height": 291, ...}]
# Find text
uv run scripts/detect_bounds.py frame.png "the words 'fully understand'"
# Returns: [{"target": "...", "x": 536, "y": 253, "width": 169, "height": 22, ...}]
# List all elements
uv run scripts/detect_bounds.py frame.png --allKey insight: the old approach of asking Gemini "give me the center of X" gave rough estimates (50-150px off). The structured prompting in detect_bounds.py asks for tight bounding boxes with explicit JSON schema and gets results within 10-20px on the first try. The difference was the prompt, not the model.
Annotation Positioning Gotchas
Use ovals for text, circles for objects
A true circle around text has massive empty space above and below (text is wide and short). For text emphasis, use underlines or oval shapes that hug the text.
Annotation timing is relative to the source video
If the video has been cut/edited, annotation timestamps need to be adjusted to match the edited timeline, not the raw source. When integrating with an edited Resolve project, map source timestamps to timeline timestamps using the EDL.
Gemini Usage Patterns
Gemini is good for:
- Positioning critique ("is this circle on the robot or the lego blocks?")
- Mobile readability review ("can you read this on a 6-inch screen?")
- Specific pixel adjustment recommendations ("move 20px left, 30px down")
- Getting approximate coordinates of illustration elements
Gemini is bad for:
- Editorial decisions about WHAT to annotate (always decides to circle everything)
- Distinguishing between nearby elements in illustrations (confused robot with lego blocks multiple times)
- Understanding that sometimes NO annotation is the right choice
- Judging whether an annotation adds meaning vs. decoration
The Gemini iteration loop
1. Add annotation → extract frame → send to Gemini with context about what the annotation is supposed to highlight 2. Gemini gives specific pixel adjustments (up/down/left/right + size changes) 3. Apply adjustments, re-check 4. Typically takes 2-4 rounds per element for illustrations, 1-2 for text overlays
Always tell Gemini what the annotation SHOULD be targeting
Don't just ask "is this positioned well?" — say "this red circle should be around the ROBOT CHARACTER, not the text. Is it on the robot?" Otherwise Gemini will approve circles that technically look fine but target the wrong element.
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "google-generativeai",
# "pyyaml",
# ]
# ///
"""Detect bounding boxes for text or objects in a video frame using Gemini vision.
Usage:
uv run scripts/detect_bounds.py <frame.png> "the robot character"
uv run scripts/detect_bounds.py <frame.png> "the words 'fully understand'"
uv run scripts/detect_bounds.py <frame.png> --all "List all distinct visual elements"
Returns JSON bounding boxes: {"target": ..., "bounds": {"x": ..., "y": ..., "width": ..., "height": ...}}
This replaces both EasyOCR (for text) and rough Gemini coordinate guessing (for objects)
by using structured prompting that asks Gemini for precise bounding boxes.
"""
import base64
import json
import os
import sys
from pathlib import Path
import yaml
def get_api_key() -> str:
key = os.environ.get("GEMINI_API_KEY")
if not key:
config_path = Path.home() / ".content" / "config.yaml"
if config_path.exists():
with open(config_path) as f:
config = yaml.safe_load(f) or {}
key = config.get("api_keys", {}).get("gemini")
if not key:
# Try .env in repo root
env_path = Path(__file__).parent.parent.parent.parent / ".env"
if env_path.exists():
for line in env_path.read_text().splitlines():
if line.startswith("GEMINI_API_KEY="):
key = line.split("=", 1)[1].strip()
if not key:
print("ERROR: GEMINI_API_KEY not found", file=sys.stderr)
sys.exit(1)
return key
def detect_bounds(frame_path: str, target: str) -> list[dict]:
"""Ask Gemini for precise bounding boxes of a target in an image."""
import google.generativeai as genai
api_key = get_api_key()
genai.configure(api_key=api_key)
with open(frame_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode("utf-8")
prompt = f"""You are a precise visual annotation tool. The image is a 1920x1080 video frame.
Find this target: "{target}"
Return ONLY a JSON array of bounding boxes. Each bounding box must have:
- "target": what was found (brief description)
- "x": left edge in pixels from frame left (0-1920)
- "y": top edge in pixels from frame top (0-1080)
- "width": width in pixels
- "height": height in pixels
- "center_x": center x coordinate
- "center_y": center y coordinate
- "confidence": your confidence 0-1
Rules:
- Coordinates are pixels from top-left corner of the 1920x1080 frame
- The bounding box must TIGHTLY contain the target — no extra padding
- If the target is text, the box should wrap the text baseline to ascender
- If the target is an object/character, the box should contain the full object
- Return [] if the target is not found
- Return ONLY the JSON array, no markdown fences, no explanation
Example response:
[{{"target": "the word hello", "x": 400, "y": 200, "width": 120, "height": 30, "center_x": 460, "center_y": 215, "confidence": 0.95}}]"""
model = genai.GenerativeModel("gemini-2.5-pro")
response = model.generate_content([
prompt,
{"mime_type": "image/png", "data": image_data},
])
text = response.text.strip()
# Strip markdown fences if present
if text.startswith("```"):
text = text.split("\n", 1)[1]
if text.endswith("```"):
text = text.rsplit("```", 1)[0]
text = text.strip()
try:
return json.loads(text)
except json.JSONDecodeError:
print(f"WARNING: Could not parse Gemini response as JSON:", file=sys.stderr)
print(text, file=sys.stderr)
return []
def detect_all(frame_path: str) -> list[dict]:
"""Ask Gemini to identify all major visual elements with bounding boxes."""
import google.generativeai as genai
api_key = get_api_key()
genai.configure(api_key=api_key)
with open(frame_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode("utf-8")
prompt = """You are a precise visual annotation tool. The image is a 1920x1080 video frame.
Identify ALL distinct visual elements: text labels, UI components, characters, icons, buttons, images.
Return ONLY a JSON array of bounding boxes. Each must have:
- "target": description of the element
- "x": left edge pixels from frame left (0-1920)
- "y": top edge pixels from frame top (0-1080)
- "width": width in pixels
- "height": height in pixels
- "center_x": center x coordinate
- "center_y": center y coordinate
Rules:
- Coordinates are pixels from top-left of the 1920x1080 frame
- Bounding boxes should TIGHTLY contain each element
- Return ONLY the JSON array, no markdown fences, no explanation"""
model = genai.GenerativeModel("gemini-2.5-pro")
response = model.generate_content([
prompt,
{"mime_type": "image/png", "data": image_data},
])
text = response.text.strip()
if text.startswith("```"):
text = text.split("\n", 1)[1]
if text.endswith("```"):
text = text.rsplit("```", 1)[0]
text = text.strip()
try:
return json.loads(text)
except json.JSONDecodeError:
print(f"WARNING: Could not parse Gemini response as JSON:", file=sys.stderr)
print(text, file=sys.stderr)
return []
def main():
if len(sys.argv) < 3:
print("Usage:")
print(' detect_bounds.py <frame.png> "target description"')
print(' detect_bounds.py <frame.png> --all')
sys.exit(1)
frame_path = sys.argv[1]
if not Path(frame_path).exists():
print(f"ERROR: Frame not found: {frame_path}", file=sys.stderr)
sys.exit(1)
if sys.argv[2] == "--all":
results = detect_all(frame_path)
else:
target = sys.argv[2]
results = detect_bounds(frame_path, target)
print(json.dumps(results, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests",
# "pymupdf",
# ]
# ///
"""Index a PDF into Mixedbread vector store for semantic search.
Usage:
uv run scripts/index_docs.py <pdf_path> [--collection <name>]
uv run scripts/index_docs.py --search "how to create outline stroke from mask"
Chunks the PDF into paragraphs, uploads to Mixedbread vector store.
"""
import json
import os
import sys
from pathlib import Path
import fitz # pymupdf
import requests
def get_config():
"""Load API key and store ID from environment or .env file."""
api_key = os.environ.get("MIXEDBREAD_API_KEY")
store_id = os.environ.get("MIXEDBREAD_STORE_ID")
if not api_key or not store_id:
env_path = Path(__file__).parent.parent.parent.parent / ".env"
if env_path.exists():
for line in env_path.read_text().splitlines():
if line.startswith("MIXEDBREAD_API_KEY="):
api_key = api_key or line.split("=", 1)[1].strip()
elif line.startswith("MIXEDBREAD_STORE_ID="):
store_id = store_id or line.split("=", 1)[1].strip()
if not api_key:
print("ERROR: MIXEDBREAD_API_KEY not found", file=sys.stderr)
sys.exit(1)
if not store_id:
print("ERROR: MIXEDBREAD_STORE_ID not found", file=sys.stderr)
sys.exit(1)
return api_key, store_id
def extract_chunks(pdf_path: str, chunk_size: int = 1500, overlap: int = 200) -> list[dict]:
"""Extract text chunks from a PDF with page numbers."""
doc = fitz.open(pdf_path)
chunks = []
for page_num in range(len(doc)):
page = doc[page_num]
text = page.get_text()
if not text.strip():
continue
# Split into paragraphs
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
# Combine small paragraphs into chunks of target size
current_chunk = ""
for para in paragraphs:
if len(current_chunk) + len(para) > chunk_size and current_chunk:
chunks.append({
"text": current_chunk.strip(),
"page": page_num + 1,
"source": Path(pdf_path).name,
})
# Keep overlap
words = current_chunk.split()
overlap_words = words[-overlap // 5:] if len(words) > overlap // 5 else []
current_chunk = " ".join(overlap_words) + "\n\n" + para
else:
current_chunk += "\n\n" + para if current_chunk else para
if current_chunk.strip():
chunks.append({
"text": current_chunk.strip(),
"page": page_num + 1,
"source": Path(pdf_path).name,
})
doc.close()
return chunks
def upload_chunks(chunks: list[dict], api_key: str, store_id: str, collection: str = "fusion-docs"):
"""Upload text chunks to Mixedbread vector store."""
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
base_url = "https://api.mixedbread.com/v2"
uploaded = 0
# Upload in batches of 20
batch_size = 20
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i + batch_size]
documents = []
for j, chunk in enumerate(batch):
documents.append({
"content": {
"type": "text",
"text": chunk["text"],
},
"metadata": {
"page": chunk["page"],
"source": chunk["source"],
"collection": collection,
"chunk_index": i + j,
},
})
resp = requests.post(
f"{base_url}/vector_stores/{store_id}/documents",
headers=headers,
json={"documents": documents},
)
if resp.status_code in (200, 201):
uploaded += len(batch)
print(f" Uploaded {uploaded}/{len(chunks)} chunks", file=sys.stderr)
else:
print(f" ERROR uploading batch: {resp.status_code} {resp.text}", file=sys.stderr)
return uploaded
def search(query: str, api_key: str, store_id: str, top_k: int = 5, collection: str = None) -> list[dict]:
"""Semantic search over the vector store."""
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
body = {
"query": query,
"top_k": top_k,
}
if collection:
body["filters"] = {"metadata.collection": {"$eq": collection}}
resp = requests.post(
f"https://api.mixedbread.com/v2/vector_stores/{store_id}/search",
headers=headers,
json=body,
)
if resp.status_code != 200:
print(f"Search error: {resp.status_code} {resp.text}", file=sys.stderr)
return []
data = resp.json()
results = []
for item in data.get("data", []):
doc = item.get("document", {})
content = doc.get("content", [{}])
text = content[0].get("text", "") if isinstance(content, list) else content.get("text", "")
metadata = doc.get("metadata", {})
results.append({
"text": text,
"page": metadata.get("page", "?"),
"source": metadata.get("source", "?"),
"score": item.get("score", 0),
})
return results
def main():
api_key, store_id = get_config()
if "--search" in sys.argv:
idx = sys.argv.index("--search")
query = sys.argv[idx + 1]
collection = None
if "--collection" in sys.argv:
cidx = sys.argv.index("--collection")
collection = sys.argv[cidx + 1]
results = search(query, api_key, store_id, collection=collection)
for r in results:
print(f"\n--- Page {r['page']} (score: {r['score']:.3f}) ---")
print(r["text"][:500])
return
if len(sys.argv) < 2:
print("Usage:")
print(' index_docs.py <pdf_path> [--collection <name>]')
print(' index_docs.py --search "query" [--collection <name>]')
sys.exit(1)
pdf_path = sys.argv[1]
collection = "fusion-docs"
if "--collection" in sys.argv:
cidx = sys.argv.index("--collection")
collection = sys.argv[cidx + 1]
print(f"Extracting chunks from {pdf_path}...", file=sys.stderr)
chunks = extract_chunks(pdf_path)
print(f" {len(chunks)} chunks extracted", file=sys.stderr)
print(f"Uploading to Mixedbread store {store_id[:8]}... (collection: {collection})", file=sys.stderr)
uploaded = upload_chunks(chunks, api_key, store_id, collection)
print(f"Done! {uploaded} chunks indexed.", file=sys.stderr)
if __name__ == "__main__":
main()