Removing ffprobe thingy that crashes the renderer
This commit is contained in:
+64
-8
@@ -3649,14 +3649,18 @@ def _format_time(seconds: float) -> str:
|
|||||||
return f"{mins:02d}:{secs:05.2f}"
|
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
|
from .models import CAMERA_PRESETS
|
||||||
|
|
||||||
|
events_by_id = {e["id"]: e for e in (events or [])}
|
||||||
|
|
||||||
print("\n RENDER PLAN:")
|
print("\n RENDER PLAN:")
|
||||||
print(" " + "-" * 76)
|
print(" " + "-" * 76)
|
||||||
|
|
||||||
@@ -3765,17 +3769,31 @@ def _print_render_plan_details(plan, marker_timings, slides: dict) -> None:
|
|||||||
)
|
)
|
||||||
if marker_id.startswith(p)
|
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
|
# Find corresponding event by video_id
|
||||||
event = video_events_by_id.get(video_id)
|
event = video_events_by_id.get(video_id)
|
||||||
if event:
|
if event:
|
||||||
cutout_name = event.cutout_name
|
cutout_name = event.cutout_name
|
||||||
end_on = event.video_source.end_on or "next_slide"
|
end_on = event.video_source.end_on or "next_slide"
|
||||||
layer_tag = f" [{event.layer}]"
|
layer_tag = f" [{event.layer}]"
|
||||||
|
else:
|
||||||
|
# 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:
|
else:
|
||||||
cutout_name = "?"
|
cutout_name = "?"
|
||||||
end_on = "next_slide"
|
|
||||||
layer_tag = ""
|
layer_tag = ""
|
||||||
|
end_on = "next_slide"
|
||||||
|
|
||||||
cache_ind = " 📁" if video_id in plan.cached_files else ""
|
cache_ind = " 📁" if video_id in plan.cached_files else ""
|
||||||
print(
|
print(
|
||||||
@@ -3798,8 +3816,14 @@ def _print_render_plan_details(plan, marker_timings, slides: dict) -> None:
|
|||||||
print(f' {marker_id:6} {_st} "{context}"')
|
print(f' {marker_id:6} {_st} "{context}"')
|
||||||
else:
|
else:
|
||||||
unaligned_count += 1
|
unaligned_count += 1
|
||||||
# Check if this is a slide that was interpolated into the plan
|
# The aligner couldn't place this marker, but events.json interpolates
|
||||||
if marker_id in slides:
|
# 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(
|
interp_event = next(
|
||||||
(e for e in plan.slide_events if e.slide_id == marker_id), None
|
(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)
|
print(f"Run 'gnommo -p {project_path.name} preprocess' first.", file=sys.stderr)
|
||||||
return 1
|
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.
|
# Talking-head cutout/zoom/audio settings come from the first segment.
|
||||||
narration_source = narration_map[narration_schedule[0].seg_id]
|
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}"
|
f"→ {_scaffold.EVENTS_FILE} + {_scaffold.SCAFFOLD_FILE} + {_scaffold.TRANSCRIBED_FILE}"
|
||||||
)
|
)
|
||||||
# The full render plan is printed here at build time — render just executes it.
|
# 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 plan_only:
|
||||||
if _summ["interpolated"]:
|
if _summ["interpolated"]:
|
||||||
print(
|
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.
|
# "[N:a] matches no streams" errors that crash the render.
|
||||||
_audio_probe_cache: dict[Path, bool] = {}
|
_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:
|
if path not in _audio_probe_cache:
|
||||||
_audio_probe_cache[path] = _has_audio_stream(path)
|
_audio_probe_cache[path] = _has_audio_stream(path)
|
||||||
return _audio_probe_cache[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
|
remaining = event.video_source.duration - skip
|
||||||
needs_loop = remaining < clip_duration - 0.1 # 0.1 s tolerance
|
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
|
# Audio presence from stored metadata (no ffprobe unless it's missing).
|
||||||
# files moved from local to external disk). Results are cached per path so each
|
has_audio = _has_audio(event.video_source, video_path)
|
||||||
# unique file is only probed once per render call.
|
|
||||||
has_audio = _probe_has_audio(video_path)
|
|
||||||
if has_audio:
|
if has_audio:
|
||||||
video_events_with_audio.add(i)
|
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
|
videos_dir, event.video_source, shared_assets_dir, project_path
|
||||||
)
|
)
|
||||||
skip = event.video_source.skip or 0.0
|
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:
|
if has_audio:
|
||||||
outro_events_with_audio.add(i)
|
outro_events_with_audio.add(i)
|
||||||
|
|
||||||
|
|||||||
@@ -1489,7 +1489,10 @@ def _extract_outro_events(
|
|||||||
)
|
)
|
||||||
if is_cached and cached_files is not None:
|
if is_cached and cached_files is not None:
|
||||||
cached_files.add(video_id)
|
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)
|
full_duration = get_video_duration(video_path)
|
||||||
else:
|
else:
|
||||||
full_duration = 10.0 # Fallback
|
full_duration = 10.0 # Fallback
|
||||||
|
|||||||
Reference in New Issue
Block a user