Removing the narration_combined
This commit is contained in:
+100
-118
@@ -4221,6 +4221,42 @@ def _chunked_render(
|
||||
return 0
|
||||
|
||||
|
||||
def _build_merged_transcription(project_path: Path, config, verbose: bool = False):
|
||||
"""Deterministic merged transcript for slide alignment.
|
||||
|
||||
Builds a single word-level transcript from the per-segment transcripts +
|
||||
the current narration.json skip/take, re-timed into the combined timeline
|
||||
(see narration.build_narration_schedule). This keeps alignment in sync with
|
||||
narration.json and avoids re-transcribing the combined file.
|
||||
|
||||
Returns None (caller falls back to the on-disk transcript) when there are no
|
||||
narration segments or any segment is missing its per-segment transcript.
|
||||
"""
|
||||
from .parser import parse_narration, get_video_duration
|
||||
from .narration import build_narration_schedule
|
||||
|
||||
try:
|
||||
narration, narration_dir = parse_narration(project_path, config)
|
||||
except GnommoError:
|
||||
return None
|
||||
if not narration:
|
||||
return None
|
||||
|
||||
transcripts_dir = narration_dir / "transcripts"
|
||||
missing = [sid for sid in narration if not (transcripts_dir / f"{sid}.json").exists()]
|
||||
if missing:
|
||||
if verbose:
|
||||
print(f" Merged transcript unavailable (no per-segment transcript for: "
|
||||
f"{', '.join(missing)}) — using on-disk transcript.")
|
||||
return None
|
||||
|
||||
_segments, merged = build_narration_schedule(
|
||||
narration, narration_dir, get_video_duration,
|
||||
transcripts_dir=transcripts_dir, verbose=verbose,
|
||||
)
|
||||
return merged or None
|
||||
|
||||
|
||||
def cmd_render(
|
||||
project_path: Path,
|
||||
verbose: bool,
|
||||
@@ -4303,129 +4339,59 @@ def cmd_render(
|
||||
print(f" Using {res} dir: {videos_dir}")
|
||||
audio, audio_dir = parse_audio(project_path, config)
|
||||
|
||||
# Load whisper transcription JSON
|
||||
# Resolve the combined narration skeleton. The .mov file — not the videos.json
|
||||
# entry — is the source of truth: stitch run without the external drive leaves
|
||||
# the file in media/videos/ even when the videos.json entry is absent (e.g. a
|
||||
# metadata pull overwrote it). Legacy multi-segment projects are handled below.
|
||||
combined_path = videos_dir / "narration_combined.mov"
|
||||
resolved_combined = _resolve_narration_combined(project_path, videos_dir, config)
|
||||
narration_json = project_path / "media" / "narration" / "narration.json"
|
||||
_narr_segments = _read_json(narration_json) if narration_json.exists() else {}
|
||||
if resolved_combined and resolved_combined.exists():
|
||||
# File is available (locally or via process cache). Ensure a videos.json
|
||||
# entry exists — synthesizing one when stitch's entry was lost — then use it.
|
||||
if "narration_combined" not in videos:
|
||||
from .models import VideoSource
|
||||
# --- Narration: render-time concat of the processed segments ---
|
||||
# narration.json is the single source of truth. The processed segments are
|
||||
# concatenated directly in the render graph — there is no narration_combined
|
||||
# file anymore.
|
||||
from .narration import build_narration_schedule
|
||||
from .parser import parse_narration as _parse_narr, get_video_duration
|
||||
|
||||
_first_seg = next(iter(_narr_segments.values()), {})
|
||||
_seg_cutout = (
|
||||
_first_seg.get("cutout") if isinstance(_first_seg, dict) else None
|
||||
)
|
||||
videos["narration_combined"] = VideoSource(
|
||||
source_file="narration_combined.mov",
|
||||
cutout=_seg_cutout or "talkinghead",
|
||||
always_visible=True,
|
||||
volume=1.0,
|
||||
)
|
||||
if resolved_combined != combined_path:
|
||||
# File lives on external disk — point the VideoSource at the absolute
|
||||
# path so the renderer doesn't re-resolve it via the local videos_dir.
|
||||
videos["narration_combined"].source_file = str(resolved_combined)
|
||||
transcript_path = resolved_combined.with_suffix(".transcript.json")
|
||||
config.main_video = "narration_combined"
|
||||
if verbose:
|
||||
narration_map, narration_seg_dir = _parse_narr(project_path, config)
|
||||
narration_schedule: list = []
|
||||
narration_source = None
|
||||
transcript_path = None
|
||||
if narration_map:
|
||||
narration_schedule, _ = build_narration_schedule(
|
||||
narration_map, narration_seg_dir, get_video_duration
|
||||
)
|
||||
missing = [s.seg_id for s in narration_schedule if not s.source_path.exists()]
|
||||
if missing:
|
||||
print(
|
||||
f" Using combined narration: {resolved_combined.name} (volume={videos['narration_combined'].volume})"
|
||||
)
|
||||
elif isinstance(config.main_video, list) and len(config.main_video) > 1:
|
||||
# Legacy: Multi-segment narration with main_video array in project.json
|
||||
resolved_combined, _ = resolve_with_cache(combined_path, project_path)
|
||||
transcript_path = resolved_combined.with_suffix(".transcript.json")
|
||||
|
||||
if not resolved_combined.exists():
|
||||
print(
|
||||
f"Error: Combined narration not found: {combined_path}", file=sys.stderr
|
||||
)
|
||||
print(
|
||||
"Run 'gnommo -p <project> concat' first to concatenate segments.",
|
||||
f"Error: processed narration segment(s) not found: {', '.join(missing)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(f"Run 'gnommo -p {project_path.name} preprocess' first.", file=sys.stderr)
|
||||
return 1
|
||||
# Talking-head cutout/zoom/audio settings come from the first segment.
|
||||
narration_source = narration_map[narration_schedule[0].seg_id]
|
||||
|
||||
# Create a synthetic video entry for the combined narration
|
||||
# Inherit settings from the first segment
|
||||
first_segment_id = config.main_video[0]
|
||||
if first_segment_id in videos:
|
||||
first_segment = videos[first_segment_id]
|
||||
from .models import VideoSource
|
||||
|
||||
combined_video = VideoSource(
|
||||
source_file="narration_combined.mov",
|
||||
filter=first_segment.filter,
|
||||
output_file=None, # Already processed
|
||||
cutout=first_segment.cutout,
|
||||
always_visible=True,
|
||||
skip=0.0, # Already trimmed during concatenation
|
||||
take=None,
|
||||
)
|
||||
videos["_narration_combined"] = combined_video
|
||||
config.main_video = "_narration_combined"
|
||||
|
||||
# --- Transcript for slide alignment ---
|
||||
# Prefer the deterministic merged transcript (per-segment transcripts re-timed
|
||||
# into the concatenated timeline). Otherwise fall back to an on-disk transcript.
|
||||
transcription = _build_merged_transcription(project_path, config, verbose)
|
||||
if transcription is not None:
|
||||
if verbose:
|
||||
print(f" Using combined narration: {combined_path.name}")
|
||||
elif _narr_segments:
|
||||
# narration.json has segments, but the combined .mov could not be found.
|
||||
# Distinguish "stitched, file unreachable" from "never stitched" so the
|
||||
# hint is actionable instead of always blaming videos.json.
|
||||
if "narration_combined" in videos:
|
||||
print(
|
||||
f"Error: narration_combined.mov could not be found.", file=sys.stderr
|
||||
)
|
||||
print(
|
||||
f"videos.json references narration_combined, but the file is not on disk "
|
||||
f"(checked local media/videos and the process cache).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(_narration_combined_hint(project_path, config), file=sys.stderr)
|
||||
else:
|
||||
print(
|
||||
f"Error: narration_combined not found in videos.json", file=sys.stderr
|
||||
)
|
||||
print(
|
||||
f"You have narration segments in narration.json but haven't stitched them.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f"Run 'gnommo -p {project_path.name} stitch' first.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
print(f" Using merged per-segment transcript ({len(transcription)} words)")
|
||||
else:
|
||||
# Single video - look for .transcript.json next to the narration video
|
||||
result = _find_narration_video(config, videos)
|
||||
if result:
|
||||
video_id, narration_source = result
|
||||
config.main_video = video_id # Ensure main_video is set to the found video
|
||||
video_path = videos_dir / narration_source.source_file
|
||||
transcript_path = video_path.with_suffix(".transcript.json")
|
||||
if config.transcript_path and (project_path / config.transcript_path).exists():
|
||||
transcript_path = project_path / config.transcript_path
|
||||
elif narration_map:
|
||||
# Legacy on-disk transcript that used to accompany narration_combined.
|
||||
transcript_path = videos_dir / "narration_combined.transcript.json"
|
||||
else:
|
||||
transcript_path = project_path / "transcript.json"
|
||||
|
||||
# If project.json specifies a transcript path, prefer it (always local)
|
||||
if config.transcript_path:
|
||||
local_transcript = project_path / config.transcript_path
|
||||
if local_transcript.exists():
|
||||
transcript_path = local_transcript
|
||||
|
||||
# Try cache fallback for transcript
|
||||
transcript_path, _ = resolve_with_cache(transcript_path, project_path)
|
||||
if not transcript_path.exists():
|
||||
print(f"Error: Transcription not found: {transcript_path}", file=sys.stderr)
|
||||
print(f"Run 'gnommo -p {project_path.name} transcribe' first.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
transcription = load_transcript(transcript_path, project_path)
|
||||
result = _find_narration_video(config, videos)
|
||||
if result:
|
||||
_vid, _src = result
|
||||
config.main_video = _vid
|
||||
transcript_path = (videos_dir / _src.source_file).with_suffix(".transcript.json")
|
||||
else:
|
||||
transcript_path = project_path / "transcript.json"
|
||||
transcript_path, _ = resolve_with_cache(transcript_path, project_path)
|
||||
if not transcript_path.exists():
|
||||
print(f"Error: Transcription not found: {transcript_path}", file=sys.stderr)
|
||||
print(f"Run 'gnommo -p {project_path.name} transcribe' first.", file=sys.stderr)
|
||||
return 1
|
||||
transcription = load_transcript(transcript_path, project_path)
|
||||
|
||||
if verbose:
|
||||
print(f" - Markers in manuscript: {len(markers)}")
|
||||
@@ -4455,9 +4421,13 @@ def cmd_render(
|
||||
audio,
|
||||
audio_dir,
|
||||
slide_range=slide_range,
|
||||
narration_schedule=narration_schedule,
|
||||
narration_source=narration_source,
|
||||
)
|
||||
if plan.time_offset > 0:
|
||||
print(f" Time offset: {plan.time_offset:.1f}s (partial render)")
|
||||
if plan.narration_segments:
|
||||
print(f" Narration concat: {len(plan.narration_segments)} segment(s) at render time")
|
||||
|
||||
# Print detailed render plan with alignment info
|
||||
_print_render_plan_details(plan, marker_timings, slides)
|
||||
@@ -4584,10 +4554,19 @@ def cmd_render(
|
||||
("manuscript.txt", project_path / "manuscript.txt", _state.HASH),
|
||||
("project.json", project_path / "project.json", _state.HASH),
|
||||
("slides.json", _slides_json, _state.HASH),
|
||||
("transcript", transcript_path, _state.HASH),
|
||||
# narration.json (skip/take) drives the concat timeline + alignment.
|
||||
("narration.json", project_path / "media" / "narration" / "narration.json", _state.HASH),
|
||||
]
|
||||
if resolved_combined:
|
||||
specs.append(("narration_combined", resolved_combined, _state.META))
|
||||
if transcript_path:
|
||||
specs.append(("transcript", transcript_path, _state.HASH))
|
||||
# The concatenated narration segments (the render's main video input).
|
||||
for _seg in plan.narration_segments:
|
||||
specs.append((f"narr:{_seg.seg_id}", _seg.source_path, _state.META))
|
||||
# Per-segment transcripts feed the merged transcript for alignment.
|
||||
_tdir = project_path / "media" / "narration" / "transcripts"
|
||||
if _tdir.is_dir():
|
||||
for _tj in sorted(_tdir.glob("*.json")):
|
||||
specs.append((f"transcript:{_tj.stem}", _tj, _state.HASH))
|
||||
for _sid, _sdef in slides.items():
|
||||
specs.append((f"slide:{_sid}", _slides_dir / _sdef.image, _state.META))
|
||||
return specs
|
||||
@@ -4606,7 +4585,10 @@ def cmd_render(
|
||||
if _render_current and output_path.exists():
|
||||
try:
|
||||
_out_mtime = output_path.stat().st_mtime
|
||||
for _dep in (resolved_combined, transcript_path):
|
||||
_deps = [s.source_path for s in plan.narration_segments]
|
||||
if transcript_path:
|
||||
_deps.append(transcript_path)
|
||||
for _dep in _deps:
|
||||
if _dep and Path(_dep).exists() and Path(_dep).stat().st_mtime > _out_mtime:
|
||||
print(f" {Path(_dep).name} is newer than the render — regenerating.")
|
||||
_render_current = False
|
||||
|
||||
Reference in New Issue
Block a user