From ac3f1a91c7c024d102289ae302d133b7bfb73508 Mon Sep 17 00:00:00 2001 From: zj1123581321 Date: Tue, 6 Oct 2026 18:58:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=20notes=5Fprocessor=20?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E8=BE=BE=E7=A9=BA=E5=88=87=E7=89=87=E5=AE=88?= =?UTF-8?q?=E5=8D=AB=E5=B9=B6=E4=BF=AE=E6=AD=A3=E6=B5=8B=E8=AF=95=E8=A1=A8?= =?UTF-8?q?=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #185 Agent-Executor: agy Agent-Model: gemini-3.8-flash-high Agent-Effort: unknown Dispatch-Id: dlg-20261006-104938-bec2ec Task-Id: VideoTranscriptAPI-20261006-06 --- .../llm/processors/notes_processor.py | 9 ---- tests/README.md | 2 +- tests/unit/test_empty_input_processors.py | 42 +++++++++++++++++-- 3 files changed, 40 insertions(+), 13 deletions(-) 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)