diff --git a/backend/cli.py b/backend/cli.py index eef097a..6625cea 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -277,6 +277,10 @@ def _has_successful_results(results: list) -> bool: def _should_enter_post_render_loop(config: dict, interrupted: bool, results: list) -> bool: """Open the post-render rerender flow on explicit config or partial interrupt recovery.""" + # The post-render loop is interactive (prompt_toolkit); never enter it + # without a real terminal or it crashes with OSError [Errno 22]. + if not sys.stdin.isatty(): + return False return bool(config.get("post_render_review", False) or (interrupted and _has_successful_results(results))) @@ -669,7 +673,7 @@ def _transcribe_progress(pct, msg): print(f"\n [4/4] Exporting {len(clips)} clips{_ai_label} to {output_dir}/") results = [] t0 = time.time() - _skip_review = not config.get("review_each_clip", False) + _skip_review = not config.get("review_each_clip", False) or not sys.stdin.isatty() interrupted = False try: @@ -1167,6 +1171,13 @@ def _find_moment_with_claude(description: str, segments: list, existing_clips: l def _review_clips(clips: list, segments: list, energy_scores: list | None, config: dict) -> list: """Interactive clip review — user can select/deselect, ask for more, or find specific moments.""" + # Non-interactive (piped/scripted/no TTY): skip the picker and render all + # suggested clips. The picker uses prompt_toolkit, which raises + # OSError [Errno 22] when stdin isn't a real terminal. + if not sys.stdin.isatty(): + print(f"\n Non-interactive run — rendering all {len(clips)} suggested clips.") + return clips + import questionary from questionary import Style diff --git a/tests/test_cli_output_dir.py b/tests/test_cli_output_dir.py index e6300bc..66f58b9 100644 --- a/tests/test_cli_output_dir.py +++ b/tests/test_cli_output_dir.py @@ -75,14 +75,25 @@ def test_has_successful_results_false_without_outputs(self): ) def test_should_enter_post_render_loop_when_interrupted_with_completed_results(self): - should_enter = cli_mod._should_enter_post_render_loop( - config={"post_render_review": False}, - interrupted=True, - results=[{"output_path": "/tmp/clip.mp4"}], - ) + with mock.patch("sys.stdin.isatty", return_value=True): + should_enter = cli_mod._should_enter_post_render_loop( + config={"post_render_review": False}, + interrupted=True, + results=[{"output_path": "/tmp/clip.mp4"}], + ) self.assertTrue(should_enter) + def test_should_not_enter_post_render_loop_without_tty(self): + with mock.patch("sys.stdin.isatty", return_value=False): + should_enter = cli_mod._should_enter_post_render_loop( + config={"post_render_review": True}, + interrupted=True, + results=[{"output_path": "/tmp/clip.mp4"}], + ) + + self.assertFalse(should_enter) + def test_should_not_enter_post_render_loop_when_interrupted_without_completed_results(self): should_enter = cli_mod._should_enter_post_render_loop( config={"post_render_review": False}, @@ -93,11 +104,12 @@ def test_should_not_enter_post_render_loop_when_interrupted_without_completed_re self.assertFalse(should_enter) def test_should_enter_post_render_loop_when_config_enabled(self): - should_enter = cli_mod._should_enter_post_render_loop( - config={"post_render_review": True}, - interrupted=False, - results=[], - ) + with mock.patch("sys.stdin.isatty", return_value=True): + should_enter = cli_mod._should_enter_post_render_loop( + config={"post_render_review": True}, + interrupted=False, + results=[], + ) self.assertTrue(should_enter)