Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions app/services/director_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -4356,6 +4356,19 @@ def _rejoin_clips_impl(out_dir: str, pid: str) -> dict:
state = _ensure_h3_segment_state(state)
clips = state.get("clips", [])
video_files = []
# Image reruns keep video_stale on the clip even when a Studio selection or
# H3 segment list still points at playable files. Gate Rejoin before the
# H3 branch, which otherwise treats those files as current.
stale_clip_numbers = [
str(index + 1)
for index, clip in enumerate(clips)
if clip.get("video_stale")
]
if stale_clip_numbers:
raise ValueError(
"Regenerate stale video clip(s) "
f"{', '.join(stale_clip_numbers)} before rejoining."
)
legacy_h3_segments = (
_is_sequential_h3_model(state.get("video_model"))
and any(clip.get("h3_segments") for clip in clips)
Expand Down Expand Up @@ -4390,17 +4403,6 @@ def _rejoin_clips_impl(out_dir: str, pid: str) -> dict:
if stale:
raise ValueError("Regenerate stale H3 continuations before rejoining the final video")
else:
stale_clip_numbers = [
str(index + 1)
for index, clip in enumerate(clips)
if clip.get("video_stale")
]
if stale_clip_numbers:
raise ValueError(
"Regenerate stale video clip(s) "
f"{', '.join(stale_clip_numbers)} before rejoining."
)

if shot_images_required(_saved_pipeline_shot_image_policy(state)):
invalid_start_numbers = _invalid_saved_media_numbers(
[clip.get("start_image_filename") for clip in clips],
Expand Down
50 changes: 50 additions & 0 deletions tests/test_director_h3_workflow_edits.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from pathlib import Path
from unittest.mock import patch

import pytest

from app.services import director_pipeline


Expand Down Expand Up @@ -346,3 +348,51 @@ def concatenate_multi_clip_videos(paths, destination, _audio, **_kwargs):
director_pipeline.rejoin_clips(str(tmp_path), "h3-selection")

assert joined == ["shot0_studio.mp4", "shot1.mp4"]


def test_h3_rejoin_rejects_stale_clip_even_when_segments_are_playable(tmp_path: Path):
filenames = ("shot0_a.mp4", "shot0_b.mp4", "shot1.mp4")
for filename in filenames:
(tmp_path / filename).write_bytes(b"video")
_write_pipeline(tmp_path, {
"pipeline_id": "h3-stale-rejoin",
"created_at": 10.0,
"status": "completed",
"pipeline_type": "short_film_story",
"video_model": "minimax_h3",
"clips": [
{
"index": 0,
"video_filename": "shot0_b.mp4",
"video_stale": True,
"video_prompt": "Whole shot zero",
"h3_segments": [
{"index": 0, "filename": "shot0_a.mp4", "stale": False},
{"index": 1, "filename": "shot0_b.mp4", "stale": False},
],
},
{
"index": 1,
"video_filename": "shot1.mp4",
"video_prompt": "Whole shot one",
"h3_segments": [
{"index": 0, "filename": "shot1.mp4", "stale": False},
],
},
],
"output_files": list(filenames),
"workspace": "default",
})
joined = []

class FakeWgp:
@staticmethod
def concatenate_multi_clip_videos(paths, destination, _audio, **_kwargs):
joined.extend(Path(path).name for path in paths)
Path(destination).write_bytes(b"joined")
return True

with patch.object(director_pipeline, "_wgp", FakeWgp()):
with pytest.raises(ValueError, match="stale video clip.*1.*before rejoining"):
director_pipeline.rejoin_clips(str(tmp_path), "h3-stale-rejoin")
assert joined == []
Loading