diff --git a/DEVELOPMENT_LOG.md b/DEVELOPMENT_LOG.md index 3dc0e1c..a9b4173 100644 --- a/DEVELOPMENT_LOG.md +++ b/DEVELOPMENT_LOG.md @@ -1348,3 +1348,9 @@ - 最终安全交叉复审把静态媒体 CORS 与管理 API 写入 Origin 拆开;抖音/B站创作者中心仍可按白名单读取受控媒体,但不能据此跨站写本地管理 API。 - 新增人工 Job 去重、Worker 完成、事务回滚、旧 lease fencing、DB 已提交后接管、单元 checkpoint 复用/计费不确定、自动 checkpoint 共存、坏长直播结构、质量门禁和父进程收尾测试。最终独立全量验收 `785 passed, 3 deselected`;Ruff、Compileall、`app.js`/`publish-center.js` 语法、三套 Compose 合并配置和 `git diff --check` 全部通过。 - Pytest 使用进程级 `niuma-pytest-*\data\test_workflow.sqlite3`,未触碰活动库。活动服务继续由 `127.0.0.1:8001` 的 Uvicorn PID `56576` 持有;只读数据库检查为 `integrity_check=ok`、`foreign_key_check=0`。活动库哈希随常驻服务 WAL 写入发生变化,未为取得静态哈希而停止正式服务。 + +## 2026-08-26 修复中文路径视频预检误判 + +- 修复 Windows 默认 GBK 解码 FFprobe/FFmpeg UTF-8 输出时,中文文件路径触发解码异常并被误报为“源文件没有视频轨”的问题。 +- 媒体创建预检与首尾解码抽样统一显式使用 UTF-8,并以替换非法字节的方式保留可诊断输出;真正缺少视频轨或音轨的素材仍继续拒绝创建。 +- 增加中文文件名编码回归和真正无视频轨回归测试;不修改上传目录、任务数据库、页面结构或原始视频文件。 diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index b09e27f..c85a254 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -1104,3 +1104,10 @@ 2. 先用一条短测试文字稿执行 Codex 分析,核对候选片段 JSON、时间范围和封面时间;这一步会消耗 Codex 额度,需人工发起。 3. 在发送中心只对一条未发布草稿点击 AI 补齐,确认标题、话题、简介通过现有内容安全规则;不要点击真实发布。 4. 若 Codex 不可用,可在系统状态页手动选择远程 AI 或本地模型;旧 API 配置和历史任务保持不变。 + +## 2026-08-26 中文路径视频创建验收 + +1. 打开 `http://127.0.0.1:8001/tasks/new`,选择包含中文目录或中文文件名的正常 MP4 视频。 +2. 创建任务时不应再出现“源文件没有视频轨”;系统应继续完成视频轨、音轨、时长、解码能力和磁盘空间预检。 +3. 纯音频文件、真正没有视频轨或音轨的素材仍应显示对应拦截原因,不会被当作有效视频任务创建。 +4. 本次验收只创建处理任务,不会自动绕过登录、验证码、平台风控或执行真实投稿。 diff --git a/app/services/media_preflight_service.py b/app/services/media_preflight_service.py index 0a1ab1d..82bdc5d 100644 --- a/app/services/media_preflight_service.py +++ b/app/services/media_preflight_service.py @@ -63,6 +63,8 @@ def _run_decode_sample(path: Path, start_seconds: float) -> None: command, capture_output=True, text=True, + encoding="utf-8", + errors="replace", timeout=settings.ffprobe_timeout, check=False, ) @@ -94,6 +96,8 @@ def probe_media(path_value: str | Path) -> dict: ], capture_output=True, text=True, + encoding="utf-8", + errors="replace", timeout=settings.ffprobe_timeout, check=False, ) diff --git a/tests/test_long_live_foundation.py b/tests/test_long_live_foundation.py index f2ead27..682b63b 100644 --- a/tests/test_long_live_foundation.py +++ b/tests/test_long_live_foundation.py @@ -18,13 +18,33 @@ from app.services.transcription_checkpoint_service import TranscriptionCheckpoint -def _ffprobe_payload(*, duration: float = 3600, include_audio: bool = True) -> str: - streams = [ - {"codec_type": "video", "codec_name": "h264", "width": 1920, "height": 1080, "avg_frame_rate": "30/1"}, - ] +def _ffprobe_payload( + *, + duration: float = 3600, + include_video: bool = True, + include_audio: bool = True, + filename: str = "", +) -> str: + streams = [] + if include_video: + streams.append( + { + "codec_type": "video", + "codec_name": "h264", + "width": 1920, + "height": 1080, + "avg_frame_rate": "30/1", + } + ) if include_audio: streams.append({"codec_type": "audio", "codec_name": "aac", "channels": 2, "sample_rate": "48000"}) - return json.dumps({"streams": streams, "format": {"duration": str(duration), "size": "1048576"}}) + return json.dumps( + { + "streams": streams, + "format": {"duration": str(duration), "size": "1048576", "filename": filename}, + }, + ensure_ascii=False, + ) def test_media_preflight_collects_streams_and_six_hour_warning(monkeypatch, tmp_path): @@ -62,6 +82,53 @@ def test_media_preflight_rejects_missing_audio(monkeypatch, tmp_path): probe_media(source) +def test_media_preflight_uses_utf8_for_chinese_filename(monkeypatch, tmp_path): + source = tmp_path / "康熙来了.mp4" + source.write_bytes(b"video") + calls = [] + + def fake_run(command, **kwargs): + calls.append((command, kwargs)) + if "-show_streams" in command: + return SimpleNamespace( + returncode=0, + stdout=_ffprobe_payload(duration=20, filename=str(source)), + stderr="", + ) + return SimpleNamespace(returncode=0, stdout="", stderr="") + + monkeypatch.setattr("app.services.media_preflight_service.shutil.which", lambda name: name) + monkeypatch.setattr("app.services.media_preflight_service.subprocess.run", fake_run) + monkeypatch.setattr( + "app.services.media_preflight_service.shutil.disk_usage", + lambda _path: SimpleNamespace(free=100 * 1024 ** 3), + ) + + result = preflight_media(source, total_output_limit=12) + + assert result.video_codec == "h264" + assert len(calls) == 3 + assert all(kwargs["encoding"] == "utf-8" for _command, kwargs in calls) + assert all(kwargs["errors"] == "replace" for _command, kwargs in calls) + + +def test_media_preflight_rejects_missing_video(monkeypatch, tmp_path): + source = tmp_path / "audio-only.mp4" + source.write_bytes(b"audio") + monkeypatch.setattr("app.services.media_preflight_service.shutil.which", lambda name: name) + monkeypatch.setattr( + "app.services.media_preflight_service.subprocess.run", + lambda *_args, **_kwargs: SimpleNamespace( + returncode=0, + stdout=_ffprobe_payload(include_video=False), + stderr="", + ), + ) + + with pytest.raises(ValueError, match="没有视频轨"): + probe_media(source) + + def test_transcription_checkpoint_resumes_and_invalidates_on_source_change(tmp_path): init_db() task_id = "checkpoint-foundation-test"