diff --git a/src/video_transcript_api/llm/processors/notes_processor.py b/src/video_transcript_api/llm/processors/notes_processor.py index 2b8022a4..a4db9a67 100644 --- a/src/video_transcript_api/llm/processors/notes_processor.py +++ b/src/video_transcript_api/llm/processors/notes_processor.py @@ -548,15 +548,6 @@ def call_notes(user_prompt: str) -> str: chapter_notes = retry_notes return f"{_format_chapter_heading(chapter)}\n{chapter_notes.strip()}" - if not mapping.slices: - return NotesResult( - text=None, - status=NotesStatus.FAILED, - error=mapping.error or "no chapter slices available", - fingerprint=mapping.current_fingerprint, - chapter_count=0, - ) - max_workers = min(len(mapping.slices), self.config.notes_concurrency) with ThreadPoolExecutor(max_workers=max_workers) as executor: future_positions = { diff --git a/tests/README.md b/tests/README.md index e4302d9d..aab0226e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -166,7 +166,7 @@ ffprobe / ffmpeg、无预置 fixture、没有 skip 分支。拒绝路径的完 需要真实 ffprobe 的文件覆盖。 LLM 处理器空输入守卫(#179):`tests/unit/test_empty_input_processors.py`。 -锁的不变量:`PlainTextProcessor._calibrate_segments`、`SpeakerAwareProcessor._calibrate_chunks` 与 `NotesProcessor.process` 在输入分段/分块列表为空时直接返回与空输入同构的空结果,不启动 `ThreadPoolExecutor(max_workers=0)`,不发起 LLM 调用;端到端空文本/空对话校对返回诚实状态 `calibration_status=none` 且不抛 `ValueError`。 +锁的不变量:`PlainTextProcessor._calibrate_segments` 与 `SpeakerAwareProcessor._calibrate_chunks` 在输入分段/分块列表为空时直接返回与空输入同构的空结果,不启动 `ThreadPoolExecutor(max_workers=0)`,不发起 LLM 调用;`NotesProcessor.process` 在空 chapters payload 下走 `is_valid` 校验失败返回 `FAILED`,不发起 LLM 调用;端到端空文本/空对话校对返回诚实状态 `calibration_status=none` 且不抛 `ValueError`。 ## 并发压测 diff --git a/tests/unit/test_empty_input_processors.py b/tests/unit/test_empty_input_processors.py index a7412eae..78a1ba42 100644 --- a/tests/unit/test_empty_input_processors.py +++ b/tests/unit/test_empty_input_processors.py @@ -9,8 +9,8 @@ zeroed calibration stats without launching a ThreadPoolExecutor. 4. SpeakerAwareProcessor.process(dialogs=[]) completes cleanly with empty output and honest calibration_status=none. -5. NotesProcessor with empty chapter slices returns a failed NotesResult rather - than crashing on max_workers=0. +5. NotesProcessor with empty chapters returns a failed NotesResult via + is_valid validation failure without issuing LLM calls. """ from unittest.mock import MagicMock, Mock @@ -112,6 +112,41 @@ def test_plain_text_process_empty_text(self, mock_config): assert stats["calibration_status"] == CalibrationStatus.NONE llm_client.call.assert_not_called() + def test_plain_text_process_whitespace_only_text(self, mock_config): + llm_client = Mock() + key_info_mock = Mock(spec=KeyInfo) + key_info_mock.to_dict.return_value = {} + key_info_mock.format_for_prompt.return_value = "" + key_info_extractor = Mock() + key_info_extractor.extract.return_value = key_info_mock + + processor = PlainTextProcessor( + config=mock_config, + llm_client=llm_client, + key_info_extractor=key_info_extractor, + quality_validator=Mock(), + ) + + result = processor.process( + text=" \t\n", + title="Whitespace Recording", + author="Author", + description="", + platform="test", + media_id="m1_whitespace", + ) + + assert result["calibrated_text"] == "" + stats = result["stats"] + assert stats["original_length"] == len(" \t\n") + assert stats["calibrated_length"] == 0 + assert stats["segment_count"] == 0 + assert stats["total_segments"] == 0 + assert stats["calibrated_segments"] == 0 + assert stats["fallback_segments"] == 0 + assert stats["calibration_status"] == CalibrationStatus.NONE + llm_client.call.assert_not_called() + def test_speaker_aware_calibrate_chunks_empty_input(self, mock_config): llm_client = Mock() processor = SpeakerAwareProcessor( @@ -174,7 +209,8 @@ def test_speaker_aware_process_empty_dialogs(self, mock_config): assert stats["calibration_stats"]["calibration_status"] == CalibrationStatus.NONE llm_client.call.assert_not_called() - def test_notes_processor_empty_slices_returns_failed_result(self, mock_config): + def test_notes_processor_empty_chapters_fails_validation(self, mock_config): + # 空 chapters -> map_notes_chapter_slices 校验失败 -> is_valid=False -> FAILED,非 slices 空守卫路径 llm_client = Mock() processor = NotesProcessor(llm_client, mock_config)