Removing ffprobe thingy that crashes the renderer

This commit is contained in:
2026-07-25 13:52:22 +02:00
parent a91acca695
commit 9f30698801
3 changed files with 81 additions and 16 deletions
+65 -9
View File
@@ -3649,14 +3649,18 @@ def _format_time(seconds: float) -> str:
return f"{mins:02d}:{secs:05.2f}"
def _print_render_plan_details(plan, marker_timings, slides: dict) -> None:
def _print_render_plan_details(plan, marker_timings, slides: dict, events=None) -> None:
"""
Print a detailed render plan showing each marker with its aligned time.
Print a detailed render plan showing each marker with its (final) time.
Uses marker_timings from the transformer which contains alignment info.
`events` is the resolved events.json list — the source of truth. Every marker
has an interpolated final_time there even when the aligner couldn't place it,
so markers the raw alignment marks unaligned still show their real position.
"""
from .models import CAMERA_PRESETS
events_by_id = {e["id"]: e for e in (events or [])}
print("\n RENDER PLAN:")
print(" " + "-" * 76)
@@ -3765,7 +3769,10 @@ def _print_render_plan_details(plan, marker_timings, slides: dict) -> None:
)
if marker_id.startswith(p)
)
video_id = marker_id[pfx_len:]
# Handles are stored lowercased in videos.json (and the plan's video
# events), so lowercase before the lookup — otherwise a camel-cased
# marker like vst:KnightRotating misses and shows '?'.
video_id = marker_id[pfx_len:].lower()
# Find corresponding event by video_id
event = video_events_by_id.get(video_id)
if event:
@@ -3773,9 +3780,20 @@ def _print_render_plan_details(plan, marker_timings, slides: dict) -> None:
end_on = event.video_source.end_on or "next_slide"
layer_tag = f" [{event.layer}]"
else:
cutout_name = "?"
# No resolved event — but the shorthand prefix itself fixes the
# cutout and layer (vst: = square/above), so never show '?'.
from .transformer import _SHORTHAND_PREFIXES
_pfx = next(
(p for p in _SHORTHAND_PREFIXES if marker_id.startswith(p)), None
)
if _pfx:
cutout_name, _layer = _SHORTHAND_PREFIXES[_pfx]
layer_tag = f" [{_layer}]"
else:
cutout_name = "?"
layer_tag = ""
end_on = "next_slide"
layer_tag = ""
cache_ind = " 📁" if video_id in plan.cached_files else ""
print(
@@ -3798,8 +3816,14 @@ def _print_render_plan_details(plan, marker_timings, slides: dict) -> None:
print(f' {marker_id:6} {_st} "{context}"')
else:
unaligned_count += 1
# Check if this is a slide that was interpolated into the plan
if marker_id in slides:
# The aligner couldn't place this marker, but events.json interpolates
# EVERY marker (slide/video/audio/camera), so show that final time — it's
# where the render actually puts it. Only truly-missing markers read '??'.
_ev = events_by_id.get(marker_id)
if _ev and _ev.get("final_time") is not None:
interp_str = _format_time(_ev["final_time"])
print(f' {marker_id:20} ~{interp_str} INTERPOLATED - "{context}"')
elif marker_id in slides:
interp_event = next(
(e for e in plan.slide_events if e.slide_id == marker_id), None
)
@@ -4704,6 +4728,38 @@ def _cmd_render_impl(
)
print(f"Run 'gnommo -p {project_path.name} preprocess' first.", file=sys.stderr)
return 1
# Detect segments rendering from RAW because the processed file is missing.
# get_preprocessed_path silently falls back to raw_mov (the render-before-
# preprocess preview path), but for a real render that means un-keyed/un-graded
# footage — and large raw camera files with -probesize 1000 + -ss seeks are
# exactly what chokes ffmpeg on the render rig. Refuse rather than crash/ship it.
_raw_fallback = [
s.seg_id for s in narration_schedule if "_processed" not in s.source_path.name
]
if _raw_fallback and not force:
print(
f"\nError: narration would render from RAW footage — the processed files "
f"are missing for: {', '.join(_raw_fallback)}",
file=sys.stderr,
)
print(
" That produces un-keyed (green screen), un-graded output, and reading the "
"large raw camera files can crash ffmpeg on the render rig.",
file=sys.stderr,
)
print(
f" Run 'gnommo -p {project_path.name} preprocess' first, or pass -f/--force to "
"render from raw anyway (quick preview only).",
file=sys.stderr,
)
return 1
elif _raw_fallback:
print(
f" ⚠ WARNING: rendering narration from RAW (processed missing): "
f"{', '.join(_raw_fallback)} — un-keyed/un-graded preview."
)
# Talking-head cutout/zoom/audio settings come from the first segment.
narration_source = narration_map[narration_schedule[0].seg_id]
@@ -4871,7 +4927,7 @@ def _cmd_render_impl(
f"{_scaffold.EVENTS_FILE} + {_scaffold.SCAFFOLD_FILE} + {_scaffold.TRANSCRIBED_FILE}"
)
# The full render plan is printed here at build time — render just executes it.
_print_render_plan_details(plan, marker_timings, slides)
_print_render_plan_details(plan, marker_timings, slides, events=_events)
if plan_only:
if _summ["interpolated"]:
print(
+12 -6
View File
@@ -495,7 +495,15 @@ def build_ffmpeg_command(plan: RenderPlan, output_path: Path) -> list[str]:
# "[N:a] matches no streams" errors that crash the render.
_audio_probe_cache: dict[Path, bool] = {}
def _probe_has_audio(path: Path) -> bool:
def _has_audio(video_source, path: Path) -> bool:
"""Trust the import-time `has_audio` in videos.json so we don't spawn an
ffprobe per video at render time — that per-file probing is what can hang or
crash the render rig on large/remote inputs. Only live-probe when the stored
value is absent (unimported entry). Keep videos.json fresh by re-running
import after swapping a file, so a stale value can't cause '[N:a] matches no
streams'."""
if video_source.has_audio is not None:
return bool(video_source.has_audio)
if path not in _audio_probe_cache:
_audio_probe_cache[path] = _has_audio_stream(path)
return _audio_probe_cache[path]
@@ -518,10 +526,8 @@ def build_ffmpeg_command(plan: RenderPlan, output_path: Path) -> list[str]:
remaining = event.video_source.duration - skip
needs_loop = remaining < clip_duration - 0.1 # 0.1 s tolerance
# Always live-probe audio presence — cached has_audio can be stale (e.g. when
# files moved from local to external disk). Results are cached per path so each
# unique file is only probed once per render call.
has_audio = _probe_has_audio(video_path)
# Audio presence from stored metadata (no ffprobe unless it's missing).
has_audio = _has_audio(event.video_source, video_path)
if has_audio:
video_events_with_audio.add(i)
@@ -555,7 +561,7 @@ def build_ffmpeg_command(plan: RenderPlan, output_path: Path) -> list[str]:
videos_dir, event.video_source, shared_assets_dir, project_path
)
skip = event.video_source.skip or 0.0
has_audio = _probe_has_audio(video_path)
has_audio = _has_audio(event.video_source, video_path)
if has_audio:
outro_events_with_audio.add(i)
+4 -1
View File
@@ -1489,7 +1489,10 @@ def _extract_outro_events(
)
if is_cached and cached_files is not None:
cached_files.add(video_id)
if video_path.exists():
# Prefer the import-time duration from videos.json; only probe when absent.
if video_source.duration is not None:
full_duration = video_source.duration
elif video_path.exists():
full_duration = get_video_duration(video_path)
else:
full_duration = 10.0 # Fallback