ai-video-intelligence-agent / scripts /generate_sample_outputs.py
pmootr's picture
Initial commit: AI Video Intelligence Agent (multimodal MVP)
a668326
Raw
History Blame Contribute Delete
13.8 kB
"""Generate the precomputed demo artifacts in ``data/sample_outputs/``.
This does NOT call the heavy CV/ASR models. Instead it pairs hand-authored,
realistic transcripts with deterministic synthetic visual streams and then runs
them through the **real** bookmark, fusion and metrics logic.
That gives us:
* internally consistent artifacts (timeline aligns with transcript),
* a genuine exercise of the production code paths,
* stable, license-free demo data with no large video committed to git.
The script is data-driven: add another entry to ``VIDEO_SPECS`` to publish a new
sample. Two samples ship by default:
demo_001 — Thai-English ML deployment tutorial (code-switching)
demo_002 — English-only "Intro to MLOps" slide-heavy explainer
Run: python -m scripts.generate_sample_outputs
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Set, Tuple
from src.bookmark_generation import generate_bookmarks
from src.config import CONFIG
from src.metrics import MetricsTracker
from src.storage import write_json, sample_path
from src.timeline_fusion import fuse_timeline
from src.utils import format_timestamp
FRAME_INTERVAL_SEC = 2.0
_SCREEN_LIKE = {"tv", "laptop", "cell phone"}
# --------------------------------------------------------------------------- #
# Spec model
# --------------------------------------------------------------------------- #
@dataclass
class VideoSpec:
video_id: str
title: str
duration_sec: float
# (start, end, language, text)
segments: List[Tuple[float, float, str, str]]
# (start, end, [objects present])
visual_sections: List[Tuple[float, float, List[str]]]
scene_change_times: Set[int]
stage_timings: Dict[str, float]
search_latencies_ms: List[float]
fps: float = 30.0
width: int = 1280
height: int = 720
source: str = "Synthetic demo content (no copyrighted video committed)."
default_language: str = "en"
extra_meta: Dict[str, object] = field(default_factory=dict)
# --------------------------------------------------------------------------- #
# Builders (parameterized by spec)
# --------------------------------------------------------------------------- #
def build_transcript(spec: VideoSpec) -> List[Dict[str, object]]:
return [
{"id": i, "start_sec": s, "end_sec": e, "text": text, "language": lang}
for i, (s, e, lang, text) in enumerate(spec.segments)
]
def _objects_at(spec: VideoSpec, t: float) -> List[str]:
for s, e, objs in spec.visual_sections:
if s <= t < e:
return objs
return ["person"]
def _conf_for(label: str, t: float) -> float:
"""Deterministic, plausible confidence in a sensible per-class range."""
base = {"person": 0.90, "laptop": 0.80, "tv": 0.74, "cell phone": 0.66}.get(
label, 0.70
)
jitter = ((int(t) * 7 + len(label)) % 9) / 100.0 # 0.00..0.08, deterministic
return round(min(0.99, base + jitter), 3)
def build_visual_events(spec: VideoSpec) -> List[Dict[str, object]]:
events: List[Dict[str, object]] = []
t = 0.0
while t <= spec.duration_sec:
objs = _objects_at(spec, t)
confidence = {o: _conf_for(o, t) for o in objs}
ordered = sorted(confidence, key=confidence.get, reverse=True)
if any(o in _SCREEN_LIKE for o in ordered) and "screen" not in ordered:
ordered = ordered + ["screen"]
scene_change = round(t) in spec.scene_change_times
events.append(
{
"time_sec": round(t, 3),
"time_label": format_timestamp(t),
"visual_events": ordered,
"confidence": confidence,
"scene_change": scene_change,
"method": "yolo11n.pt+scene (precomputed sample)",
}
)
t += FRAME_INTERVAL_SEC
return events
def build_metrics(
spec: VideoSpec, n_frames: int, n_segments: int, n_visual: int, n_bm: int
) -> Dict:
tracker = MetricsTracker(video_id=spec.video_id)
tracker.stages_sec = dict(spec.stage_timings)
for ms in spec.search_latencies_ms:
tracker.record_search_latency(ms)
tracker.count("frames_processed", n_frames)
tracker.count("transcript_segments", n_segments)
tracker.count("visual_events_detected", n_visual)
tracker.count("bookmarks", n_bm)
doc = tracker.to_dict()
doc["video_metadata"] = {
"video_id": spec.video_id,
"title": spec.title,
"duration_sec": spec.duration_sec,
"fps": spec.fps,
"width": spec.width,
"height": spec.height,
"frame_count": int(spec.duration_sec * spec.fps),
"source": spec.source,
}
doc["config"] = {
"frame_interval_sec": FRAME_INTERVAL_SEC,
"asr_model_size": "base",
"visual_model": "yolo11n.pt",
"device": "Apple M4 CPU (int8)",
}
return doc
def generate_one(spec: VideoSpec) -> None:
transcript = build_transcript(spec)
visual_events = build_visual_events(spec)
bookmarks = generate_bookmarks(transcript, visual_events, config=CONFIG)
timeline = fuse_timeline(
video_id=spec.video_id,
duration_sec=spec.duration_sec,
transcript=transcript,
visual_events=visual_events,
bookmarks=bookmarks,
config=CONFIG,
)
n_visual = sum(len(v["visual_events"]) for v in visual_events)
metrics = build_metrics(
spec,
n_frames=len(visual_events),
n_segments=len(transcript),
n_visual=n_visual,
n_bm=len(bookmarks),
)
write_json(sample_path(spec.video_id, "transcript"), transcript)
write_json(sample_path(spec.video_id, "visual_events"), visual_events)
write_json(sample_path(spec.video_id, "bookmarks"), bookmarks)
write_json(sample_path(spec.video_id, "timeline"), timeline)
write_json(sample_path(spec.video_id, "metrics"), metrics)
print(
f" {spec.video_id}: segments={len(transcript)} frames={len(visual_events)} "
f"bookmarks={len(bookmarks)} visual_events={n_visual}"
)
# --------------------------------------------------------------------------- #
# Sample 1 — demo_001: Thai-English ML deployment tutorial (code-switching)
# --------------------------------------------------------------------------- #
DEMO_001 = VideoSpec(
video_id="demo_001",
title="Deploying ML Models: Model Serving & API Deployment (demo)",
duration_sec=184.0,
default_language="th",
segments=[
(0.0, 6.2, "th", "สวัสดีครับ วันนี้เราจะมาเรียนรู้เรื่อง model serving และ API deployment กันนะครับ"),
(6.2, 13.5, "th", "ก่อนอื่นเรามาดูภาพรวมของ pipeline ที่เราจะ build กันก่อน"),
(13.5, 21.0, "en", "We start by training a simple machine learning model, then we save it to disk."),
(21.0, 29.4, "th", "ขั้นตอนต่อไปคือการสร้าง REST API ด้วย FastAPI เพื่อให้ model ของเราพร้อมใช้งาน"),
(29.4, 37.8, "en", "Here you can see the project structure on my screen, with the model file and the server code."),
(37.8, 46.0, "th", "เรามาเขียน endpoint แรกกันครับ ชื่อว่า slash predict สำหรับรับ input"),
(46.0, 54.5, "en", "The predict endpoint takes a JSON payload and returns the model prediction."),
(54.5, 63.0, "th", "ตรงนี้สำคัญมากนะครับ เราต้อง validate input ก่อนเสมอเพื่อความปลอดภัย"),
(63.0, 72.2, "en", "Now let's talk about Docker. We containerize the application so it runs anywhere."),
(72.2, 80.5, "th", "ผมจะเขียน Dockerfile แบบง่าย ๆ ให้ดูนะครับ เริ่มจาก base image ของ Python"),
(80.5, 89.0, "en", "After building the image, we run the container and expose port eight thousand."),
(89.0, 98.3, "th", "คำถามที่หลายคนสงสัยคือ แล้วเราจะ deploy ขึ้น production ยังไง?"),
(98.3, 107.0, "en", "We can deploy this container to a cloud service or to Hugging Face Spaces for a demo."),
(107.0, 116.4, "th", "เรื่องของ latency ก็สำคัญครับ เราควรวัด response time ของ API เสมอ"),
(116.4, 125.0, "en", "Let me show you how to add simple metrics logging to track p50 and p95 latency."),
(125.0, 134.7, "th", "ต่อไปเป็นเรื่อง monitoring เราจะใช้ log file เก็บ metric แบบง่าย ๆ ก่อน"),
(134.7, 144.0, "en", "Finally, remember to add a health check endpoint so the platform knows the service is alive."),
(144.0, 153.5, "th", "สรุปนะครับ วันนี้เราได้เรียนรู้เรื่อง model serving, API, Docker และ deployment"),
(153.5, 162.8, "en", "In summary, you now have a complete pipeline from model to a deployed API."),
(162.8, 172.0, "th", "ถ้าชอบ video นี้ อย่าลืมกด like และ subscribe เพื่อไม่พลาด content ใหม่ ๆ นะครับ"),
(172.0, 184.0, "en", "Thank you for watching, and see you in the next tutorial. Goodbye!"),
],
visual_sections=[
(0.0, 13.0, ["person"]),
(13.0, 29.0, ["person", "tv"]),
(29.0, 63.0, ["person", "laptop", "tv"]),
(63.0, 90.0, ["person", "laptop"]),
(90.0, 134.0, ["person", "laptop", "tv"]),
(134.0, 153.0, ["person", "laptop"]),
(153.0, 162.0, ["person"]),
(162.0, 184.0, ["person", "cell phone"]),
],
scene_change_times={13, 29, 63, 90, 116, 134, 153, 162},
stage_timings={
"frame_extraction": 3.18,
"audio_extraction": 1.05,
"visual_analysis": 22.41,
"asr": 39.76,
"bookmark_generation": 0.012,
"timeline_fusion": 0.031,
},
search_latencies_ms=[3.1, 2.4, 4.8, 2.9, 3.6, 5.2, 2.7, 3.3, 4.1, 6.0],
)
# --------------------------------------------------------------------------- #
# Sample 2 — demo_002: English-only "Intro to MLOps" slide-heavy explainer
# --------------------------------------------------------------------------- #
DEMO_002 = VideoSpec(
video_id="demo_002",
title="Intro to MLOps: Datasets, Training, Serving & Monitoring (demo)",
duration_sec=150.0,
default_language="en",
segments=[
(0.0, 7.0, "en", "Welcome to this short introduction to MLOps, the practice of taking machine learning models to production."),
(7.0, 16.0, "en", "In this overview you'll see the full pipeline on the slides: data, training, serving, and monitoring."),
(16.0, 25.0, "en", "Everything starts with a good dataset. We collect, clean, and label our training data."),
(25.0, 35.0, "en", "On screen you can see the dataset summary, with the number of samples and the class balance."),
(35.0, 45.0, "en", "Next we move on to training. We fit a simple model and track its accuracy on a validation set."),
(45.0, 55.0, "en", "Here is the training curve. Notice how the validation loss flattens after a few epochs."),
(55.0, 65.0, "en", "Once training is done, we save the model and wrap it in an inference function."),
(65.0, 75.0, "en", "Now let's talk about serving. We expose the model through a REST API so other services can call it."),
(75.0, 85.0, "en", "The API has a predict endpoint that takes a JSON request and returns the model's prediction."),
(85.0, 95.0, "en", "How do we ship this to production reliably? We package everything into a Docker container."),
(95.0, 105.0, "en", "After deployment, monitoring becomes critical. We track latency, errors, and prediction quality."),
(105.0, 115.0, "en", "Let me show you the metrics dashboard, with p50 and p95 latency for each endpoint."),
(115.0, 125.0, "en", "We also add a health check endpoint so the platform can restart the service if it fails."),
(125.0, 135.0, "en", "A quick question: what happens when the data distribution changes over time? That is called drift."),
(135.0, 145.0, "en", "To summarize, MLOps connects data, training, serving, deployment, and monitoring into one pipeline."),
(145.0, 150.0, "en", "Thanks for watching this overview. In the next video we'll build the API step by step."),
],
visual_sections=[
(0.0, 16.0, ["person", "tv"]),
(16.0, 35.0, ["person", "tv"]),
(35.0, 65.0, ["person", "laptop", "tv"]),
(65.0, 95.0, ["person", "laptop"]),
(95.0, 135.0, ["person", "tv"]),
(135.0, 150.0, ["person"]),
],
scene_change_times={16, 35, 65, 95, 115, 135},
stage_timings={
"frame_extraction": 2.61,
"audio_extraction": 0.88,
"visual_analysis": 18.24,
"asr": 31.07,
"bookmark_generation": 0.010,
"timeline_fusion": 0.026,
},
search_latencies_ms=[2.2, 3.0, 2.6, 4.1, 3.4, 2.8, 3.9, 2.5, 3.1, 4.6],
)
VIDEO_SPECS = [DEMO_001, DEMO_002]
def main() -> None:
print(f"Writing sample outputs to {CONFIG.sample_outputs_dir}")
for spec in VIDEO_SPECS:
generate_one(spec)
if __name__ == "__main__":
main()