diff --git a/gnommo/cli.py b/gnommo/cli.py index fa7301c..d6f04af 100644 --- a/gnommo/cli.py +++ b/gnommo/cli.py @@ -3691,6 +3691,32 @@ def _parse_slide_range(slides_arg: str) -> tuple[str, Optional[str]]: return start_slide, end_slide +def _resolve_pause_duration( + entry: dict, video_id: str, videos_dir: Path, shared_duration: dict[str, float] +) -> Optional[float]: + """Resolve a clip's duration for auto-setting pause_narration. + + Tries, in order: the local videos.json entry's own 'duration', the shared + library's duration (for is_shared clips whose metadata lives elsewhere), then + a direct ffprobe of the source file. Returns None if none succeed. + """ + dur = entry.get("duration") or shared_duration.get(video_id.lower()) + if dur: + return float(dur) + + source_file = entry.get("source_file") + if source_file: + candidate = videos_dir / source_file + if candidate.exists(): + try: + from .preprocessor import get_video_duration + + return round(get_video_duration(candidate), 3) + except Exception: + return None + return None + + def _project_markers_to_videos( markers: list[str], videos_json_path: Path, config, project_path: Path = None ) -> None: @@ -3737,11 +3763,12 @@ def _project_markers_to_videos( if not projection: return - # Build a case-insensitive index of shared_assets pause_narration values. - # When a video is marked is_shared but its local entry is missing pause_narration, - # we pull the value from the shared canonical entry so it's never lost when - # the ETL writes back cutout/layer under a lowercase key. + # Build case-insensitive indexes of shared_assets pause_narration and duration. + # When a video is marked is_shared its metadata lives in the shared canonical + # entry, not the local one — so a pause-prefix marker on a shared clip would + # otherwise never get pause_narration set (the local entry has no duration). _shared_pause: dict[str, float] = {} + _shared_duration: dict[str, float] = {} for _shared_candidate in [ project_path / "shared_assets" / "videos.json", project_path.parent / "shared_assets" / "videos.json", @@ -3754,6 +3781,9 @@ def _project_markers_to_videos( pn = _v.get("pause_narration") if pn: _shared_pause[_k.lower()] = float(pn) + dur = _v.get("duration") + if dur: + _shared_duration[_k.lower()] = float(dur) except (json.JSONDecodeError, OSError): pass break @@ -3773,21 +3803,28 @@ def _project_markers_to_videos( video_changed = False for field, value in fields.items(): if field == "_auto_pause": - # Write pause_narration = duration only when: + # A pause-prefix marker (vftp:, vfbp:, …) means "freeze the + # narration for this clip's whole length", so pause_narration + # must equal the clip's duration. Set it whenever: # - marker is a pause-prefix (value is True) # - pause_narration not already set (preserve manual overrides) - # - duration is known (probed by import) - if value and not entry.get("pause_narration") and entry.get("duration"): - entry["pause_narration"] = entry["duration"] - changed = True - video_changed = True + # - a duration can be resolved (local entry, shared library, + # or by probing the file as a last resort) + if value and not entry.get("pause_narration"): + dur = _resolve_pause_duration( + entry, video_id, json_path.parent, _shared_duration + ) + if dur: + entry["pause_narration"] = dur + changed = True + video_changed = True elif entry.get(field) != value: entry[field] = value changed = True video_changed = True # For is_shared entries: inherit pause_narration from shared_assets if - # not already set locally (handles case where explicit pause_narration - # lives on a different-case key in the shared library). + # still not set (handles an explicit pause_narration that lives on a + # different-case key in the shared library). if entry.get("is_shared") and not entry.get("pause_narration"): shared_pn = _shared_pause.get(video_id.lower()) if shared_pn: