diff --git a/gnommo/cli.py b/gnommo/cli.py index 6e012e4..35264a6 100644 --- a/gnommo/cli.py +++ b/gnommo/cli.py @@ -4817,10 +4817,28 @@ def _cmd_render_impl( print(f" Warning: {w}") print(" Passed.") - # Stage 3: Transform (alignment, unless a prior events.json pins the times) + # Stage 3: Transform. + # build (plan_only): align the manuscript to the transcript (or reuse an + # existing events.json), producing the timing layer. + # render: NEVER aligns — it requires the precomputed events.json/scaffold.json + # and executes them. If they're missing it errors, so `down` + `render` is + # all the rig ever needs and the output can't silently diverge from the build. from . import scaffold as _scaffold _existing_events = None if realign else _scaffold.read_events(project_path) + if not plan_only: + if _existing_events is None or _scaffold.read_scaffold(project_path) is None: + print( + f"Error: render requires the precomputed timing layer " + f"({_scaffold.EVENTS_FILE} + {_scaffold.SCAFFOLD_FILE}), which is missing.", + file=sys.stderr, + ) + print( + f" Render does not align — run 'gnommo -p {project_path.name} build' first " + f"(then 'up'; on the rig, 'down' brings them over).", + file=sys.stderr, + ) + return 1 _timings_override = ( _scaffold.events_to_marker_timings(_existing_events) if _existing_events else None ) @@ -4895,10 +4913,10 @@ def _cmd_render_impl( ] _write_tasks_file(project_path, missing_videos, alignment_issues) - # --- Timing layer: derive events, keep hand-edits, persist events.json + scaffold.json --- - # Only for a full-timeline pass — partial (--slides) and internal chunk renders - # read the override but must never overwrite the complete events.json/scaffold.json. - if slide_range is None and _output_path_override is None: + # --- Timing layer (BUILD only): derive events, keep hand-edits, persist + # events.json + scaffold.json, and print the plan. Render never runs this — it + # only consumes the files — so the timing layer is written exactly once, by build. + if plan_only and slide_range is None and _output_path_override is None: _events = _scaffold.derive_events(marker_timings, slides, videos, audio) # merge restores each id's human `adjustment` (the relative tweak). _events = _scaffold.merge_events(_events, _existing_events) diff --git a/gnommo/transfer.py b/gnommo/transfer.py index 379206c..9d69e02 100644 --- a/gnommo/transfer.py +++ b/gnommo/transfer.py @@ -27,7 +27,10 @@ from typing import Optional _COMMITS_LOG = "commits.log" -# Dirs/patterns excluded on down (mirrors what up never pushes). +# Dirs/patterns excluded on down (mirrors what up never pushes). Note: events.json +# and scaffold.json are NOT excluded — they must reach the rig so `down` + `render` +# is all that's needed there. The render never rewrites them, so they stay identical +# on both ends (no clobbering). _DOWN_EXCLUDES = [ "media/narration/processed/", "media/narration/intermediate/", @@ -57,8 +60,18 @@ def _build_manifest(project_path: Path) -> list[str]: except (json.JSONDecodeError, OSError): pass - # Fixed project-root files - for fixed in ["project.json", "manuscript.txt", _COMMITS_LOG, "citations.json"]: + # Fixed project-root files. events.json / scaffold.json are the precomputed + # timing layer the render needs — events.json also carries the human `adjustment` + # tweaks — so they must travel UP to the rig. (They're excluded on `down` so the + # rig's regenerated copies can't clobber the local edits.) + for fixed in [ + "project.json", + "manuscript.txt", + _COMMITS_LOG, + "citations.json", + "events.json", + "scaffold.json", + ]: if (project_path / fixed).exists(): files.add(fixed)