diff --git a/app/services/director_pipeline.py b/app/services/director_pipeline.py index bfb6e209..c47b1928 100644 --- a/app/services/director_pipeline.py +++ b/app/services/director_pipeline.py @@ -2192,8 +2192,10 @@ def _backfill_clip_video_attempts(state: dict, state_dir: str) -> dict: selected = "" clip["selected_video_filename"] = selected or None if selected: + # A Studio selection is the playback authority, but it does not + # refresh inputs. Image reruns keep video_stale so Rejoin/export + # cannot assemble a take that no longer matches the start frame. clip["video_filename"] = selected - clip["video_stale"] = False clip["video_attempts"] = sorted( attempts_by_clip[index].values(), key=lambda item: (float(item.get("created_at") or 0), item["filename"]), diff --git a/tests/test_director_cancellation.py b/tests/test_director_cancellation.py index 65fef171..088cda30 100644 --- a/tests/test_director_cancellation.py +++ b/tests/test_director_cancellation.py @@ -1153,6 +1153,46 @@ def test_rejoin_rejects_stale_video_instead_of_omitting_clip(self): concatenate.assert_not_called() + def test_rejoin_rejects_stale_video_even_when_a_take_is_selected(self): + pid = "pipe-stale-selected-rejoin" + record = self._add_pipeline(pid, "completed") + record["clip_plans"] = [ + {"image_prompt": "one", "video_prompt": "one"}, + {"image_prompt": "two", "video_prompt": "two"}, + ] + record["_clip_video_files"] = ["one.mp4", "two.mp4"] + for filename in record["_clip_video_files"]: + self._write_media(filename, b"video") + self.assertTrue(pipeline._save_pipeline_state(pid)) + + def mark_selected_stale(state): + clip = state["clips"][0] + clip["selected_video_filename"] = clip["video_filename"] + clip["video_stale"] = True + + pipeline._update_saved_pipeline(self.temp_dir.name, pid, mark_selected_stale) + loaded = pipeline.load_pipeline_state(self.temp_dir.name, pid) + self.assertTrue(loaded["clips"][0]["video_stale"]) + self.assertEqual(loaded["clips"][0]["selected_video_filename"], "one.mp4") + + def keep_notes(state): + state["clips"][0]["review_notes"] = "keep stale" + + pipeline._update_saved_pipeline(self.temp_dir.name, pid, keep_notes) + raw_path = pipeline._find_pipeline_file(self.temp_dir.name, pid) + with open(raw_path, encoding="utf-8") as handle: + saved = json.load(handle) + self.assertTrue(saved["clips"][0]["video_stale"]) + self.assertEqual(saved["clips"][0]["review_notes"], "keep stale") + + concatenate = Mock(return_value=True) + pipeline._wgp.concatenate_multi_clip_videos = concatenate + with self.assertRaisesRegex( + ValueError, "stale video clip.*1.*before rejoining", + ): + pipeline.rejoin_clips(self.temp_dir.name, pid) + concatenate.assert_not_called() + def test_rejoin_rejects_clip_whose_start_image_is_missing(self): pid = "pipe-missing-rejoin-start" record = self._add_pipeline(pid, "completed") diff --git a/tests/test_director_review.py b/tests/test_director_review.py index 3b08f2c1..b94fdffb 100644 --- a/tests/test_director_review.py +++ b/tests/test_director_review.py @@ -75,3 +75,25 @@ def test_switching_an_approved_take_updates_h3_selection_without_approving_it(tm assert saved['clips'][0]['h3_segments'][0]['filename'] == 'old.mp4' assert saved['clips'][0]['h3_segments'][0]['stale'] is False assert 'old.mp4' in saved['output_files'] + + +def test_review_notes_keep_a_stale_selected_take_stale(tmp_path): + path, state = fixture(tmp_path) + state['clips'][0].update( + selected_video_filename='old.mp4', + video_filename='old.mp4', + video_stale=True, + tag='good', + ) + path.write_text(json.dumps(state)) + loaded = pipeline.load_pipeline_state(str(tmp_path), 'review-test') + assert loaded['clips'][0]['video_stale'] is True + assert loaded['clips'][0]['selected_video_filename'] == 'old.mp4' + save_review(str(tmp_path), 'review-test', [ + {'type': 'note_clip', 'pipelineId': 'review-test', 'clipIndex': 0, 'notes': 'keep stale'}, + ]) + saved = json.loads(path.read_text()) + assert saved['clips'][0]['video_stale'] is True + assert saved['clips'][0]['selected_video_filename'] == 'old.mp4' + assert saved['clips'][0]['review_notes'] == 'keep stale' + assert saved['clips'][0]['tag'] == 'good'