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
9 changes: 0 additions & 9 deletions src/video_transcript_api/llm/processors/notes_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`。

## 并发压测

Expand Down
42 changes: 39 additions & 3 deletions tests/unit/test_empty_input_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand Down
Loading