File size: 13,799 Bytes
a668326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
"""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()