Removing ffprobe thingy that crashes the renderer
This commit is contained in:
+65
-9
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user