davinci-resolve-mcp 2.59.0 → 2.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,643 @@
1
+ """Perception-strata queries — editorial questions as cross-track joins.
2
+
3
+ The strata store (events/curves/words/beats) is the vocabulary; these are
4
+ the sentences: take_diff (compare deliveries of the same line),
5
+ cut_candidates (rank joint frames with reasons), strata_query (windowed
6
+ cross-track fetch, timeline-projectable).
7
+
8
+ The line these never cross: measure, compare, flag, aim — never decide.
9
+ take_diff reports deltas, not a winner; cut_candidates ranks frames with
10
+ evidence, the editor picks.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import difflib
16
+ import math
17
+ import statistics
18
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
19
+
20
+ from src.utils import strata, timeline_brain_db
21
+
22
+ _WORD_STRIP = ".,!?;:\"'()[]—–-…"
23
+
24
+
25
+ def _norm_word(word: str) -> str:
26
+ return word.strip().strip(_WORD_STRIP).lower()
27
+
28
+
29
+ def _resolve(project_root: str, clip_ref: Any) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
30
+ from src.utils import analysis_store
31
+
32
+ conn = timeline_brain_db.connect(project_root)
33
+ clip_uuid = analysis_store.resolve_clip_uuid(conn, clip_ref)
34
+ if not clip_uuid:
35
+ return None, {
36
+ "success": False,
37
+ "error": f"Unknown clip ref: {clip_ref!r} (older analysis root? run db_ingest first)",
38
+ }
39
+ return clip_uuid, None
40
+
41
+
42
+ def _finite(values: Sequence[float]) -> List[float]:
43
+ return [v for v in values if isinstance(v, (int, float)) and not math.isnan(v)]
44
+
45
+
46
+ def _curve_slice(curve: Optional[Dict[str, Any]], start: float, end: float) -> List[float]:
47
+ if not curve or not curve.get("values"):
48
+ return []
49
+ rate = float(curve["sample_rate"])
50
+ offset = float(curve.get("start_seconds") or 0.0)
51
+ lo = max(0, int((start - offset) * rate))
52
+ hi = min(len(curve["values"]), int((end - offset) * rate) + 1)
53
+ return curve["values"][lo:hi] if hi > lo else []
54
+
55
+
56
+ def _delivery_metrics(
57
+ conn,
58
+ clip_uuid: str,
59
+ words: List[Dict[str, Any]],
60
+ ) -> Dict[str, Any]:
61
+ """Prosodic profile of one take's aligned words. Measures; no judgment."""
62
+ timed = [w for w in words if isinstance(w.get("start_seconds"), (int, float))]
63
+ if not timed:
64
+ return {"word_count": len(words), "timed": False}
65
+ start = float(timed[0]["start_seconds"])
66
+ last = timed[-1]
67
+ end = float(last.get("end_seconds") or last["start_seconds"])
68
+ duration = max(end - start, 1e-6)
69
+
70
+ pauses = strata.read_events(conn, clip_uuid, "pause", start_seconds=start, end_seconds=end)
71
+ hesitations = strata.read_events(conn, clip_uuid, "hesitation", start_seconds=start, end_seconds=end)
72
+
73
+ metrics: Dict[str, Any] = {
74
+ "timed": True,
75
+ "start_seconds": start,
76
+ "end_seconds": end,
77
+ "duration_seconds": round(duration, 3),
78
+ "word_count": len(words),
79
+ "words_per_second": round(len(timed) / duration, 3),
80
+ "pause_count": len(pauses),
81
+ "pause_total_seconds": round(sum(float(p.get("duration_seconds") or 0.0) for p in pauses), 3),
82
+ "longest_pause_seconds": round(max((float(p.get("duration_seconds") or 0.0) for p in pauses), default=0.0), 3),
83
+ "hesitation_count": len(hesitations),
84
+ }
85
+
86
+ pitch_raw = _curve_slice(strata.read_curve(conn, clip_uuid, "pitch"), start, end)
87
+ pitch = _finite(pitch_raw)
88
+ if len(pitch) >= 4:
89
+ metrics["pitch"] = {
90
+ "mean_hz": round(statistics.fmean(pitch), 1),
91
+ "stdev_hz": round(statistics.pstdev(pitch), 1),
92
+ "range_hz": round(max(pitch) - min(pitch), 1),
93
+ "voiced_ratio": round(len(pitch) / max(1, len(pitch_raw)), 3),
94
+ }
95
+ energy = _finite(_curve_slice(strata.read_curve(conn, clip_uuid, "vocal_energy"), start, end))
96
+ if len(energy) >= 4:
97
+ metrics["energy"] = {
98
+ "mean": round(statistics.fmean(energy), 4),
99
+ "stdev": round(statistics.pstdev(energy), 4),
100
+ "peak": round(max(energy), 4),
101
+ }
102
+ return metrics
103
+
104
+
105
+ def take_diff(
106
+ project_root: str,
107
+ clip_a: Any,
108
+ clip_b: Any,
109
+ *,
110
+ text: Optional[str] = None,
111
+ ) -> Dict[str, Any]:
112
+ """Compare two deliveries of the same (or similar) line, with evidence.
113
+
114
+ Aligns the takes on transcript words (difflib on normalized tokens; when
115
+ `text` is given, each take is first narrowed to its best window matching
116
+ that line). Returns per-take delivery metrics + deltas. It never says
117
+ which take is better — that is the editor's call.
118
+ """
119
+ uuid_a, err = _resolve(project_root, clip_a)
120
+ if err:
121
+ return err
122
+ uuid_b, err = _resolve(project_root, clip_b)
123
+ if err:
124
+ return err
125
+ conn = timeline_brain_db.connect(project_root)
126
+ words_a = strata.read_words(conn, uuid_a)
127
+ words_b = strata.read_words(conn, uuid_b)
128
+ if not words_a or not words_b:
129
+ return {
130
+ "success": False,
131
+ "error": "both takes need transcript_words rows (run transcription / backfill_words first)",
132
+ "words_a": len(words_a),
133
+ "words_b": len(words_b),
134
+ }
135
+
136
+ if text:
137
+ words_a = _best_text_window(words_a, text)
138
+ words_b = _best_text_window(words_b, text)
139
+ if not words_a or not words_b:
140
+ return {"success": False, "error": f"line not found in one of the takes: {text!r}"}
141
+
142
+ tokens_a = [_norm_word(w["word"]) for w in words_a]
143
+ tokens_b = [_norm_word(w["word"]) for w in words_b]
144
+ matcher = difflib.SequenceMatcher(a=tokens_a, b=tokens_b, autojunk=False)
145
+ blocks = [b for b in matcher.get_matching_blocks() if b.size > 0]
146
+ aligned_a = [words_a[b.a + i] for b in blocks for i in range(b.size)]
147
+ aligned_b = [words_b[b.b + i] for b in blocks for i in range(b.size)]
148
+ ratio = matcher.ratio()
149
+
150
+ metrics_a = _delivery_metrics(conn, uuid_a, aligned_a)
151
+ metrics_b = _delivery_metrics(conn, uuid_b, aligned_b)
152
+
153
+ deltas: Dict[str, Any] = {}
154
+ for key in ("duration_seconds", "words_per_second", "pause_count", "pause_total_seconds", "longest_pause_seconds", "hesitation_count"):
155
+ va, vb = metrics_a.get(key), metrics_b.get(key)
156
+ if isinstance(va, (int, float)) and isinstance(vb, (int, float)):
157
+ deltas[key] = round(vb - va, 3)
158
+ if isinstance(metrics_a.get("pitch"), dict) and isinstance(metrics_b.get("pitch"), dict):
159
+ deltas["pitch_mean_hz"] = round(metrics_b["pitch"]["mean_hz"] - metrics_a["pitch"]["mean_hz"], 1)
160
+ deltas["pitch_range_hz"] = round(metrics_b["pitch"]["range_hz"] - metrics_a["pitch"]["range_hz"], 1)
161
+ if isinstance(metrics_a.get("energy"), dict) and isinstance(metrics_b.get("energy"), dict):
162
+ deltas["energy_mean"] = round(metrics_b["energy"]["mean"] - metrics_a["energy"]["mean"], 4)
163
+
164
+ return {
165
+ "success": True,
166
+ "alignment": {
167
+ "ratio": round(ratio, 3),
168
+ "aligned_word_count": len(aligned_a),
169
+ "note": "metrics are computed over the aligned words only",
170
+ },
171
+ "take_a": {"clip_uuid": uuid_a, "text": " ".join(w["word"] for w in aligned_a), **{"metrics": metrics_a}},
172
+ "take_b": {"clip_uuid": uuid_b, "text": " ".join(w["word"] for w in aligned_b), **{"metrics": metrics_b}},
173
+ "deltas_b_minus_a": deltas,
174
+ "judgment": "none — measure/compare only; the pick is the editor's",
175
+ }
176
+
177
+
178
+ # ── strata_query — windowed cross-track fetch + word find + timeline scope ──
179
+
180
+ # Ordering hints only — the tracks actually fetched are whatever exists in the
181
+ # DB for the clip (the vocabulary is open: analyzers and humans may add tracks
182
+ # freely, and every recorded track must be reachable through the query layer).
183
+ _EVENT_TRACK_ORDER = ("pause", "breath", "hesitation", "beat", "downbeat", "blink", "gesture_boundary")
184
+ _CURVE_TRACK_ORDER = ("pitch", "vocal_energy", "speech_rate", "loudness", "motion_energy",
185
+ "gaze_x", "gaze_y", "expression_mouth_open", "expression_brow_raise")
186
+
187
+
188
+ def _present_tracks(conn, clip_uuid: str, table: str, order_hint: Sequence[str]) -> List[str]:
189
+ present = {
190
+ row["track"]
191
+ for row in conn.execute(
192
+ f"SELECT DISTINCT track FROM {table} WHERE clip_uuid = ?", (clip_uuid,)
193
+ ).fetchall()
194
+ }
195
+ ordered = [t for t in order_hint if t in present]
196
+ ordered.extend(sorted(present.difference(order_hint)))
197
+ return ordered
198
+
199
+
200
+ def _clip_bundle(
201
+ conn,
202
+ clip_uuid: str,
203
+ start: Optional[float],
204
+ end: Optional[float],
205
+ *,
206
+ include_curve_values: bool = False,
207
+ ) -> Dict[str, Any]:
208
+ """Everything the strata know about one clip window, joined."""
209
+ bundle: Dict[str, Any] = {"clip_uuid": clip_uuid}
210
+ row = conn.execute(
211
+ "SELECT clip_name, duration_seconds, fps FROM clips WHERE clip_uuid = ?", (clip_uuid,)
212
+ ).fetchone()
213
+ if row:
214
+ bundle["clip_name"] = row["clip_name"]
215
+ bundle["fps"] = row["fps"]
216
+
217
+ bundle["words"] = strata.read_words(conn, clip_uuid, start_seconds=start, end_seconds=end)
218
+
219
+ events: Dict[str, List[Dict[str, Any]]] = {}
220
+ for track in _present_tracks(conn, clip_uuid, "events", _EVENT_TRACK_ORDER):
221
+ hits = strata.read_events(conn, clip_uuid, track, start_seconds=start, end_seconds=end)
222
+ if hits:
223
+ events[track] = hits
224
+ bundle["events"] = events
225
+
226
+ curves: Dict[str, Any] = {}
227
+ for track in _present_tracks(conn, clip_uuid, "curves", _CURVE_TRACK_ORDER):
228
+ curve = strata.read_curve(conn, clip_uuid, track)
229
+ if curve is None:
230
+ continue
231
+ lo = start if start is not None else 0.0
232
+ hi = end if end is not None else (lo + len(curve["values"]) / float(curve["sample_rate"]))
233
+ window = _finite(_curve_slice(curve, lo, hi))
234
+ entry: Dict[str, Any] = {"sample_rate": curve["sample_rate"], "source": curve["source"]}
235
+ if window:
236
+ entry["window_stats"] = {
237
+ "min": round(min(window), 4),
238
+ "max": round(max(window), 4),
239
+ "mean": round(sum(window) / len(window), 4),
240
+ }
241
+ if include_curve_values:
242
+ entry["values"] = _curve_slice(curve, lo, hi)
243
+ entry["start_seconds"] = lo
244
+ curves[track] = entry
245
+ bundle["curves"] = curves
246
+
247
+ beat_sql = "SELECT * FROM story_beats WHERE clip_uuid = ? AND superseded_at IS NULL"
248
+ args: List[Any] = [clip_uuid]
249
+ if start is not None:
250
+ beat_sql += " AND end_seconds > ?"
251
+ args.append(start)
252
+ if end is not None:
253
+ beat_sql += " AND start_seconds < ?"
254
+ args.append(end)
255
+ bundle["story_beats"] = [
256
+ {
257
+ "beat_uuid": r["beat_uuid"],
258
+ "start_seconds": r["start_seconds"],
259
+ "end_seconds": r["end_seconds"],
260
+ "beat_type": r["beat_type"],
261
+ "label": r["label"],
262
+ "summary": r["summary"],
263
+ }
264
+ for r in conn.execute(beat_sql + " ORDER BY start_seconds", args).fetchall()
265
+ ]
266
+ return bundle
267
+
268
+
269
+ def strata_query(
270
+ project_root: str,
271
+ *,
272
+ clip_ref: Any = None,
273
+ start_seconds: Optional[float] = None,
274
+ end_seconds: Optional[float] = None,
275
+ match_word: Optional[str] = None,
276
+ context_seconds: float = 2.0,
277
+ include_curve_values: bool = False,
278
+ limit: int = 20,
279
+ ) -> Dict[str, Any]:
280
+ """The strata as one queryable surface.
281
+
282
+ Modes:
283
+ - clip window: clip_ref (+ start/end) → the full cross-track bundle.
284
+ - word find: match_word (project-wide or within clip_ref) → each hit
285
+ with a ±context_seconds bundle around it — "find the moment", joined.
286
+ """
287
+ conn = timeline_brain_db.connect(project_root)
288
+
289
+ if match_word:
290
+ escaped = match_word.replace("\\", "\\\\").replace("%", r"\%").replace("_", r"\_")
291
+ sql = (
292
+ "SELECT w.clip_uuid, w.word, w.start_seconds, w.end_seconds, c.clip_name "
293
+ "FROM transcript_words w JOIN clips c ON c.clip_uuid = w.clip_uuid "
294
+ "WHERE w.word LIKE ? ESCAPE '\\'"
295
+ )
296
+ args: List[Any] = [f"%{escaped}%"]
297
+ if clip_ref is not None:
298
+ uuid_, err = _resolve(project_root, clip_ref)
299
+ if err:
300
+ return err
301
+ sql += " AND w.clip_uuid = ?"
302
+ args.append(uuid_)
303
+ sql += " ORDER BY w.clip_uuid, w.start_seconds LIMIT ?"
304
+ args.append(int(limit))
305
+ hits = []
306
+ for row in conn.execute(sql, args).fetchall():
307
+ t = row["start_seconds"]
308
+ hit: Dict[str, Any] = {
309
+ "clip_uuid": row["clip_uuid"],
310
+ "clip_name": row["clip_name"],
311
+ "word": row["word"],
312
+ "time_seconds": t,
313
+ }
314
+ if isinstance(t, (int, float)):
315
+ hit["context"] = _clip_bundle(
316
+ conn,
317
+ row["clip_uuid"],
318
+ max(0.0, t - context_seconds),
319
+ t + context_seconds,
320
+ include_curve_values=include_curve_values,
321
+ )
322
+ hits.append(hit)
323
+ return {"success": True, "mode": "word_find", "match": match_word, "hits": hits}
324
+
325
+ if clip_ref is None:
326
+ return {"success": False, "error": "strata_query needs clip_id and/or match_word"}
327
+ uuid_, err = _resolve(project_root, clip_ref)
328
+ if err:
329
+ return err
330
+ return {
331
+ "success": True,
332
+ "mode": "clip_window",
333
+ "start_seconds": start_seconds,
334
+ "end_seconds": end_seconds,
335
+ **_clip_bundle(conn, uuid_, start_seconds, end_seconds, include_curve_values=include_curve_values),
336
+ }
337
+
338
+
339
+ def timeline_strata(
340
+ project_root: str,
341
+ timeline_name: str,
342
+ *,
343
+ timeline_version: Optional[int] = None,
344
+ record_start_frame: Optional[int] = None,
345
+ record_end_frame: Optional[int] = None,
346
+ fps: Optional[float] = None,
347
+ include_curve_values: bool = False,
348
+ ) -> Dict[str, Any]:
349
+ """Project clip strata through a timeline's recorded placements.
350
+
351
+ Reads timeline_clip_usage (snapshotted at archive time), resolves each
352
+ placed media-pool item back to its analyzed clip, and returns per-
353
+ placement strata bundles. Placement rows carry RECORD in/out frames but
354
+ not the source offset, so bundles are whole-clip; exact source↔record
355
+ frame mapping needs the live timeline item and is labeled as such.
356
+
357
+ Frame convention: in_frame/out_frame are ABSOLUTE record frames as
358
+ snapshotted from item.GetStart()/GetEnd() — they include the timeline
359
+ start-timecode offset (e.g. 86400 for a 01:00:00:00 start at 24fps).
360
+ record_start_frame/record_end_frame filters are compared in that same
361
+ absolute space. Snapshots taken at schema v14+ also store the timeline's
362
+ fps and start frame, which are used to add timeline-relative frames and
363
+ seconds per placement; older snapshots omit those derived fields unless
364
+ the caller supplies fps.
365
+ """
366
+ from src.utils import analysis_store
367
+
368
+ conn = timeline_brain_db.connect(project_root)
369
+ if timeline_version is None:
370
+ row = conn.execute(
371
+ "SELECT MAX(timeline_version) AS v FROM timeline_clip_usage WHERE timeline_name = ?",
372
+ (timeline_name,),
373
+ ).fetchone()
374
+ if row is None or row["v"] is None:
375
+ return {
376
+ "success": False,
377
+ "error": f"no clip-usage snapshots recorded for timeline {timeline_name!r}",
378
+ }
379
+ timeline_version = int(row["v"])
380
+
381
+ meta = conn.execute(
382
+ "SELECT fps, start_frame FROM timeline_versions WHERE timeline_name = ? AND version = ?",
383
+ (timeline_name, timeline_version),
384
+ ).fetchone()
385
+ stored_fps = float(meta["fps"]) if meta and meta["fps"] else None
386
+ start_frame = int(meta["start_frame"]) if meta and meta["start_frame"] is not None else None
387
+ fps_source = "caller" if fps is not None else ("snapshot" if stored_fps else None)
388
+ if fps is None:
389
+ fps = stored_fps
390
+ if fps is not None and (not isinstance(fps, (int, float)) or not math.isfinite(fps) or fps <= 0):
391
+ return {"success": False, "error": f"fps must be a positive number, got {fps!r}"}
392
+ fps = float(fps) if fps is not None else None
393
+
394
+ sql = (
395
+ "SELECT * FROM timeline_clip_usage WHERE timeline_name = ? AND timeline_version = ?"
396
+ )
397
+ args: List[Any] = [timeline_name, timeline_version]
398
+ if record_start_frame is not None:
399
+ sql += " AND out_frame > ?"
400
+ args.append(int(record_start_frame))
401
+ if record_end_frame is not None:
402
+ sql += " AND in_frame < ?"
403
+ args.append(int(record_end_frame))
404
+ sql += " ORDER BY track_type, track_index, in_frame"
405
+
406
+ placements = []
407
+ unresolved = []
408
+ for row in conn.execute(sql, args).fetchall():
409
+ clip_uuid = analysis_store.resolve_clip_uuid(conn, row["media_pool_item_id"])
410
+ placement = {
411
+ "media_pool_item_id": row["media_pool_item_id"],
412
+ "track_type": row["track_type"],
413
+ "track_index": row["track_index"],
414
+ "record_in_frame": row["in_frame"],
415
+ "record_out_frame": row["out_frame"],
416
+ }
417
+ if start_frame is not None:
418
+ placement["timeline_in_frame"] = row["in_frame"] - start_frame
419
+ placement["timeline_out_frame"] = row["out_frame"] - start_frame
420
+ if fps:
421
+ placement["record_in_seconds"] = round((row["in_frame"] - start_frame) / fps, 3)
422
+ placement["record_out_seconds"] = round((row["out_frame"] - start_frame) / fps, 3)
423
+ if clip_uuid:
424
+ placement["strata"] = _clip_bundle(
425
+ conn, clip_uuid, None, None, include_curve_values=include_curve_values
426
+ )
427
+ else:
428
+ unresolved.append(row["media_pool_item_id"])
429
+ placements.append(placement)
430
+
431
+ return {
432
+ "success": True,
433
+ "timeline_name": timeline_name,
434
+ "timeline_version": timeline_version,
435
+ "fps": fps,
436
+ "fps_source": fps_source,
437
+ "timeline_start_frame": start_frame,
438
+ "placements": placements,
439
+ "unresolved_media_ids": sorted(set(unresolved)),
440
+ "note": (
441
+ "record_in/out_frame are absolute snapshot frames (start-timecode-inclusive); "
442
+ "record_start/end_frame filters use that same space. Bundles are whole-clip "
443
+ "(clip time); usage snapshots record placement, not source offset — frame-exact "
444
+ "source↔record mapping needs the live item"
445
+ + (
446
+ ""
447
+ if start_frame is not None
448
+ else "; this snapshot predates the v14 timebase columns, so timeline-relative "
449
+ "frames/seconds are unavailable (re-archive to record them)"
450
+ )
451
+ ),
452
+ }
453
+
454
+
455
+ # ── cut_candidates — the joint solver ────────────────────────────────────────
456
+ #
457
+ # Frame-level cut-point grammar, scored from whatever tracks exist:
458
+ # cut on the blink · cut inside movement, not at its poles · don't cut
459
+ # mid-word · don't bisect a breath · pauses are doors · land on the beat.
460
+ # Each feature contributes points AND a human-readable reason; missing
461
+ # tracks are reported, never silently treated as "no signal".
462
+
463
+ _CUT_WEIGHTS = {
464
+ "mid_word": -3.0,
465
+ "word_gap": 1.0,
466
+ "in_pause": 2.0,
467
+ "on_blink": 2.0,
468
+ "bisects_breath": -1.5,
469
+ "clears_breath": 0.75,
470
+ "on_beat": 1.0,
471
+ "in_motion_max": 0.75,
472
+ "dead_still": -0.5,
473
+ }
474
+
475
+ _BLINK_TOLERANCE_FRAMES = 1.5
476
+ _BEAT_TOLERANCE_SECONDS = 0.05
477
+ _BREATH_CLEAR_SECONDS = 0.3
478
+
479
+
480
+ def cut_candidates(
481
+ project_root: str,
482
+ clip_ref: Any,
483
+ time_seconds: float,
484
+ *,
485
+ window_seconds: float = 0.35,
486
+ fps: Optional[float] = None,
487
+ limit: int = 7,
488
+ ) -> Dict[str, Any]:
489
+ """Rank candidate cut frames around an intended joint, with reasons.
490
+
491
+ Scores every frame in ±window_seconds on the cut-point grammar using the
492
+ clip's available strata. Returns ranked candidates; the top row is a
493
+ recommendation to *look at*, not a decision — aim, never decide.
494
+ """
495
+ uuid_, err = _resolve(project_root, clip_ref)
496
+ if err:
497
+ return err
498
+ conn = timeline_brain_db.connect(project_root)
499
+ if fps is None:
500
+ row = conn.execute("SELECT fps, duration_seconds FROM clips WHERE clip_uuid = ?", (uuid_,)).fetchone()
501
+ fps = float(row["fps"]) if row and row["fps"] else 24.0
502
+ if not isinstance(fps, (int, float)) or not math.isfinite(fps) or fps <= 0:
503
+ return {"success": False, "error": f"fps must be a positive number, got {fps!r}"}
504
+ fps = float(fps)
505
+ frame_dur = 1.0 / fps
506
+
507
+ lo = max(0.0, time_seconds - window_seconds)
508
+ hi = time_seconds + window_seconds
509
+ fetch_lo, fetch_hi = lo - 2.0, hi + 2.0
510
+
511
+ words = strata.read_words(conn, uuid_, start_seconds=fetch_lo, end_seconds=fetch_hi)
512
+ pauses = strata.read_events(conn, uuid_, "pause", start_seconds=fetch_lo, end_seconds=fetch_hi)
513
+ breaths = strata.read_events(conn, uuid_, "breath", start_seconds=fetch_lo, end_seconds=fetch_hi)
514
+ blinks = strata.read_events(conn, uuid_, "blink", start_seconds=fetch_lo, end_seconds=fetch_hi)
515
+ beats = strata.read_events(conn, uuid_, "beat", start_seconds=fetch_lo, end_seconds=fetch_hi)
516
+ motion = strata.read_curve(conn, uuid_, "motion_energy")
517
+
518
+ tracks_used = []
519
+ tracks_missing = []
520
+ for name, present in (
521
+ ("transcript_words", bool(words)),
522
+ ("pause", bool(pauses)),
523
+ ("breath", bool(breaths)),
524
+ ("blink", bool(blinks)),
525
+ ("beat", bool(beats)),
526
+ ("motion_energy", motion is not None),
527
+ ):
528
+ (tracks_used if present else tracks_missing).append(name)
529
+
530
+ def score_frame(t: float) -> Tuple[float, List[str]]:
531
+ score = 0.0
532
+ reasons: List[str] = []
533
+
534
+ if words:
535
+ inside = next(
536
+ (
537
+ w for w in words
538
+ if isinstance(w.get("start_seconds"), (int, float))
539
+ and isinstance(w.get("end_seconds"), (int, float))
540
+ and w["start_seconds"] <= t < w["end_seconds"]
541
+ ),
542
+ None,
543
+ )
544
+ if inside:
545
+ score += _CUT_WEIGHTS["mid_word"]
546
+ reasons.append(f"mid-word — bisects “{inside['word']}”")
547
+ else:
548
+ score += _CUT_WEIGHTS["word_gap"]
549
+ reasons.append("between words")
550
+
551
+ for pause in pauses:
552
+ start = float(pause["time_seconds"])
553
+ dur = float(pause.get("duration_seconds") or 0.0)
554
+ if start <= t < start + dur:
555
+ score += _CUT_WEIGHTS["in_pause"]
556
+ reasons.append(f"inside a {dur:.2f}s pause")
557
+ break
558
+
559
+ for blink in blinks:
560
+ if abs(t - float(blink["time_seconds"])) <= _BLINK_TOLERANCE_FRAMES * frame_dur:
561
+ score += _CUT_WEIGHTS["on_blink"]
562
+ reasons.append("lands on a blink")
563
+ break
564
+
565
+ for breath in breaths:
566
+ b_start = float(breath["time_seconds"])
567
+ b_end = b_start + float(breath.get("duration_seconds") or 0.25)
568
+ if b_start <= t < b_end:
569
+ score += _CUT_WEIGHTS["bisects_breath"]
570
+ reasons.append("bisects a breath")
571
+ break
572
+ if 0.0 <= t - b_end <= _BREATH_CLEAR_SECONDS:
573
+ score += _CUT_WEIGHTS["clears_breath"]
574
+ reasons.append("clears the inhale")
575
+ break
576
+
577
+ for beat in beats:
578
+ if abs(t - float(beat["time_seconds"])) <= _BEAT_TOLERANCE_SECONDS:
579
+ score += _CUT_WEIGHTS["on_beat"]
580
+ reasons.append("on the musical beat")
581
+ break
582
+
583
+ if motion is not None:
584
+ v = strata.curve_value_at(motion, t)
585
+ if v is not None:
586
+ if v >= 0.2:
587
+ bonus = _CUT_WEIGHTS["in_motion_max"] * min(v, 1.0)
588
+ score += bonus
589
+ reasons.append(f"inside movement (energy {v:.2f})")
590
+ elif v < 0.05:
591
+ score += _CUT_WEIGHTS["dead_still"]
592
+ reasons.append("dead stillness")
593
+
594
+ return score, reasons
595
+
596
+ candidates = []
597
+ n_frames = int(round((hi - lo) / frame_dur)) + 1
598
+ for i in range(n_frames):
599
+ t = lo + i * frame_dur
600
+ score, reasons = score_frame(t)
601
+ candidates.append(
602
+ {
603
+ "time_seconds": round(t, 4),
604
+ "frame_offset": int(round((t - time_seconds) * fps)),
605
+ "score": round(score, 3),
606
+ "reasons": reasons,
607
+ }
608
+ )
609
+ candidates.sort(key=lambda c: (-c["score"], abs(c["frame_offset"])))
610
+
611
+ return {
612
+ "success": True,
613
+ "clip_uuid": uuid_,
614
+ "requested_time_seconds": time_seconds,
615
+ "fps": fps,
616
+ "window_seconds": window_seconds,
617
+ "candidates": candidates[: max(1, limit)],
618
+ "tracks_used": tracks_used,
619
+ "tracks_missing": tracks_missing,
620
+ "note": "ranked evidence, not a decision — the editor picks the frame",
621
+ }
622
+
623
+
624
+ def _best_text_window(words: List[Dict[str, Any]], text: str) -> List[Dict[str, Any]]:
625
+ """Narrow a take's words to the window best matching `text`."""
626
+ target = [_norm_word(t) for t in text.split() if _norm_word(t)]
627
+ if not target:
628
+ return words
629
+ tokens = [_norm_word(w["word"]) for w in words]
630
+ n = len(target)
631
+ if len(tokens) <= n:
632
+ return words
633
+ best_start, best_score = 0, -1.0
634
+ # Slide a window of the target length (± slack) and score similarity.
635
+ for start in range(0, len(tokens) - 1):
636
+ window = tokens[start:start + n + max(2, n // 3)]
637
+ score = difflib.SequenceMatcher(a=target, b=window, autojunk=False).ratio()
638
+ if score > best_score:
639
+ best_score, best_start = score, start
640
+ if best_score < 0.4:
641
+ return []
642
+ end = min(len(words), best_start + n + max(2, n // 3))
643
+ return words[best_start:end]