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
10 changes: 9 additions & 1 deletion src/video_transcript_api/transcriber/capswriter_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ def update_server(cls, addr: str = None, port: int = None):
# 退回 max(120, duration + 60) 的小预算,宁可让长媒体走到超时也不静默掐断。
# generic 与 recorder 路径自 PR #173(#170)起同样能拿到 ffprobe 时长,因此
# 「拿不到时长」现在只出现在真的没探测到的退化输入上。
#
# 时长为 0 与「拿不到时长」同等对待(issue #190):直播录制的容器头部会把
# duration 写 0,拿它算预算等于 0*4+120=120 秒,会确定性掐断本来能跑完的任务。
# 退回 SDK 自动预算后,SDK 改用转码后实际采样数算时长,不受这个 0 影响。

DEADLINE_REALTIME_FACTOR = 4.0
DEADLINE_OVERHEAD_SECONDS = 120.0
Expand Down Expand Up @@ -251,11 +255,15 @@ def _transcription_deadline(media_duration: Optional[float] = None) -> Optional[
``media_duration`` 取自下载阶段 ffprobe 已解析出的时长(见
``downloaders/base.py::_validate_media_file``)。拿不到时长时返回
``None``,调用点据此不传 ``deadline_total``,保持 SDK 自动预算。

非正时长同样按「拿不到时长」处理(issue #190):直播录制的容器头部会把
duration 写成 0,0*4+120=120 秒的预算会确定性掐断本来能跑完的任务;而
SDK 的自动预算改用转码后的实际采样数算时长,不受这个假 0 影响。
"""
if (
media_duration is None
or not (float("-inf") < media_duration < float("inf"))
or media_duration < 0
or media_duration <= 0
):
logger.warning("transcription_deadline duration=unknown fallback=sdk_auto")
return None
Expand Down
16 changes: 16 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,22 @@ ffprobe / ffmpeg、无预置 fixture、没有 skip 分支。拒绝路径的完
LLM 处理器空输入守卫(#179):`tests/unit/test_empty_input_processors.py`。
锁的不变量:`PlainTextProcessor._calibrate_segments` 与 `SpeakerAwareProcessor._calibrate_chunks` 在输入分段/分块列表为空时直接返回与空输入同构的空结果,不启动 `ThreadPoolExecutor(max_workers=0)`,不发起 LLM 调用;`NotesProcessor.process` 在空 chapters payload 下走 `is_valid` 校验失败返回 `FAILED`,不发起 LLM 调用;端到端空文本/空对话校对返回诚实状态 `calibration_status=none` 且不抛 `ValueError`。

转录时限预算的零时长守卫(#190):`tests/unit/test_capswriter_deadline_budget.py`。
锁的不变量:`media_duration` 为 0 或负数时与「拿不到时长」完全同路——
`_transcription_deadline` 返回 `None`,且真的不把 `deadline_total` 键传给
`transcribe_file_sync`,由 SDK 改用转码后的实际采样数算时长
(`test_non_positive_duration_is_treated_as_unknown`,
覆盖 `0` / `0.0` / `-5.0`;`test_unknown_duration_omits_deadline_kwarg` 与
`test_transcription_deadline_math_equivalence` 把同一判据铺进参数化矩阵与直调断言)。
背景:直播录制的容器头部会把 `format.duration` 写 0,旧行为按公式算出
`0*4+120=120` 秒的预算,确定性掐断本来能跑完的任务。预算值本身没变(同一条
`duration*4+120` 公式),变的只是 0 的来源从本仓显式路径交回 SDK
(`test_sdk_auto_budget_is_what_covers_zero_duration`;
`tests/unit/test_youtube_api_duration.py::test_repo_budget_formula_matches_sdk_auto_budget`
锁住两侧仍相等)。同 PR 把 `test_media_duration_probe.py` 里
`test_every_admitted_duration_is_accepted_by_the_deadline_budget` 的 `"0"` 用例移出——
0 不再是「准入认可的已知时长」。

## 并发压测

`scripts/perf/concurrent_load.py` 会提交本地 API 任务,并使用真实抖音和 B 站
Expand Down
56 changes: 48 additions & 8 deletions tests/unit/test_capswriter_deadline_budget.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""转录时限预算按实测吞吐计算(issue #155)。

拿得到时长时本仓显式传 ``deadline_total = duration*4 + 120``;拿不到时长时不传该键,
回退 SDK 自动预算。SDK 的自动预算自 pin ``492fe19``(上游 PR #71)起是
``_auto_budget(duration) = duration*4 + 120``,语义是 watchdog(挂死检测)而非识别时限
SLA,由 ``test_sdk_auto_budget_*`` 单独锁住。每个用例都断言真正传给
``transcribe_file_sync`` 的实参。
回退 SDK 自动预算。时长为 0 同属「拿不到」(issue #190):直播录制的容器头部会写 0,
拿它算预算等于 120 秒,会确定性掐断本来能跑完的任务。SDK 的自动预算自 pin
``492fe19``(上游 PR #71)起是 ``_auto_budget(duration) = duration*4 + 120``,语义是
watchdog(挂死检测)而非识别时限 SLA,由 ``test_sdk_auto_budget_*`` 单独锁住。每个用例都
断言真正传给 ``transcribe_file_sync`` 的实参。
"""

import inspect
Expand Down Expand Up @@ -93,7 +94,7 @@ def test_duration_flows_through_transcriber_layer(tmp_path, sdk_config, audio):


@pytest.mark.parametrize("duration,expected", [
(0.0, 120.0), (1.0, 124.0), (30.0, 240.0), (45.0, 300.0),
(1.0, 124.0), (30.0, 240.0), (45.0, 300.0),
(45.1, 300.4), (93.08898, 492.35592), (600.0, 2520.0), (3600.0, 14520.0),
])
def test_known_duration_formula(tmp_path, sdk_config, audio, duration, expected):
Expand All @@ -102,20 +103,44 @@ def test_known_duration_formula(tmp_path, sdk_config, audio, duration, expected)
assert sdk_call.call_args.kwargs["deadline_total"] == pytest.approx(expected)


@pytest.mark.parametrize("media_duration", [None, -1.0, float("nan"), float("inf"), float("-inf")])
@pytest.mark.parametrize("media_duration", [None, 0, 0.0, -1.0, -5.0, float("nan"), float("inf"), float("-inf")])
def test_unknown_duration_omits_deadline_kwarg(tmp_path, sdk_config, audio, media_duration):
with _patch_sdk() as sdk_call:
_make_client(tmp_path).transcribe_file(str(audio), media_duration=media_duration)
assert "deadline_total" not in sdk_call.call_args.kwargs


@pytest.mark.parametrize("media_duration", [0, 0.0, -5.0])
def test_non_positive_duration_is_treated_as_unknown(tmp_path, sdk_config, audio, media_duration):
"""时长 0 / 负数一律按「拿不到时长」退回 SDK 自动预算(issue #190)。

两层都要断言:``_transcription_deadline`` 返回 None,且真的没把
``deadline_total`` 传给 SDK——只有第一层的话,调用点仍可能用别的值覆盖。

时长为 0 的现实来源是直播录制的容器头部:ffprobe 给出 ``format.duration``
为 0,按公式算出 0*4+120=120 秒的预算,会在远端还在正常识别时掐断任务。
SDK 自动预算改用转码后的实际采样数算时长,不受这个假 0 影响。
"""
assert _transcription_deadline(media_duration) is None
with _patch_sdk() as sdk_call:
success, _ = _make_client(tmp_path).transcribe_file(
str(audio), media_duration=media_duration
)
assert success is True
assert "deadline_total" not in sdk_call.call_args.kwargs


def test_transcription_deadline_math_equivalence():
"""锁住链式比较与 math.isfinite 逐值等价,覆盖 nan, ±inf, 负数与正常值。"""
"""锁住链式比较与 math.isfinite 逐值等价,覆盖 nan, ±inf, 零与负数。"""
assert _transcription_deadline(None) is None
assert _transcription_deadline(float("nan")) is None
assert _transcription_deadline(float("inf")) is None
assert _transcription_deadline(float("-inf")) is None
assert _transcription_deadline(-1.0) is None
# issue #190: 0 不是「已知时长」,与负数同样退回 SDK 自动预算。
assert _transcription_deadline(0) is None
assert _transcription_deadline(0.0) is None
assert _transcription_deadline(-5.0) is None
assert _transcription_deadline(93.08898) == pytest.approx(492.35592)
assert _transcription_deadline(600.0) == 2520.0

Expand Down Expand Up @@ -247,5 +272,20 @@ def test_sdk_auto_budget_grows_with_duration_and_matches_repo_budget():
assert auto_budget(600.0) > auto_budget(93.1) > auto_budget(0)
# 显式路径没有被上游改动的部分:两者在拿得到时长时必须给同一个秒数,
# 这正是本卡保留显式 deadline_total 的前提。
for duration in (0.0, 93.08898, 600.0, 3600.0):
for duration in (93.08898, 600.0, 3600.0):
assert auto_budget(duration) == pytest.approx(_transcription_deadline(duration))


def test_sdk_auto_budget_is_what_covers_zero_duration():
"""时长 0 改由 SDK 自动预算兜底,两条路径给出的秒数仍然相等(issue #190)。

价值不在预算值(都是 120),而在「0 不再掐断任务」:显式路径交出控制权,
SDK 转码后按实际采样数算时长,长直播拿到的是长预算。
"""
auto_budget = _sdk_auto_budget()

assert _transcription_deadline(0.0) is None
assert auto_budget(0.0) == pytest.approx(120.0)
# SDK 拿到的时长是转码后的实际采样数,不是容器头部那个 0,所以预算随真实
# 素材长度放大——这正是 0 必须退回 SDK 的理由。
assert auto_budget(17501.67) == pytest.approx(17501.67 * 4 + 120)
6 changes: 4 additions & 2 deletions tests/unit/test_media_duration_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,14 @@ def test_admission_returns_none_for_unusable_duration(tmp_path, raw):
assert _transcription_deadline(parsed) is None


@pytest.mark.parametrize("raw", ["600.0", "1286.78", "17501.67", "0"])
@pytest.mark.parametrize("raw", ["600.0", "1286.78", "17501.67"])
def test_every_admitted_duration_is_accepted_by_the_deadline_budget(tmp_path, raw):
"""准入认可的时长必须被预算公式真正采纳(不能是「算出来是 None」的哑弹)。

锁的是副作用的可观测面:时长一旦流向预算,``deadline_total`` 就是
``duration*4+120``,而不是悄悄退回 SDK 自动预算。
``duration*4+120``,而不是悄悄退回 SDK 自动预算。容器头部写 0 的直播录制
不在此列:0 按未知时长处理,由 ``test_capswriter_deadline_budget.py`` 覆盖
(issue #190)。
"""
parsed = _admission_duration(tmp_path / "a.mp3", raw)
assert parsed is not None
Expand Down
10 changes: 8 additions & 2 deletions tests/unit/test_youtube_api_duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,13 @@ def test_repo_budget_formula_matches_sdk_auto_budget():
"""
from capswriter_asr.client import _auto_budget

for duration in (0.0, 1.0, 93.0, 600.0, 1286.78, 17501.67):
for duration in (1.0, 93.0, 600.0, 1286.78, 17501.67):
explicit = _transcription_deadline(duration)
assert explicit == pytest.approx(_auto_budget(duration)), duration
assert explicit == pytest.approx(duration * 4 + 120), duration
assert explicit == pytest.approx(duration * 4 + 120), duration

# 时长 0 不再是「已知时长」(issue #190:直播录制容器头部写 0):显式路径
# 交出控制权,预算由 SDK 按转码后的实际采样数给出。同一条公式,所以 0 的
# 预算仍是 120 秒——变的是来源,不是秒数。
assert _transcription_deadline(0.0) is None
assert _auto_budget(0.0) == pytest.approx(120.0)
Loading