From 4d27f1ed2c12f6826547948f00cba18616205016 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 9 Aug 2026 13:57:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20Worker=20=E9=85=8D=E9=9F=B3=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=20fallback=20=E5=88=B0=20voice=5Fids=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=A2=84=E8=A7=88=E6=97=A0=E5=A3=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当前端选择上传音频时,asset_id 放在 voice_ids 数组里, voice_library_id 为空。Worker 的 _download_all_assets() 之前 只检查 voice_library_id,导致音频永远不会被下载。 修复:当 voice_library_id 为空但 voice_ids 非空时, 使用 voice_ids[0] 作为音频 asset_id 进行下载。 优先级:voice_library_id > voice_ids[0]。 新增 6 个单元测试覆盖 fallback 逻辑。 --- apps/worker/worker_app/tasks/generation.py | 24 ++- tests/unit/test_preview_voice_ids_fallback.py | 161 ++++++++++++++++++ 2 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_preview_voice_ids_fallback.py diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index fa8fee9c5..96fced8fc 100644 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -1058,6 +1058,7 @@ def _download_all_assets( task_asset_ids: list[str], voice_library_id: str, task_id: str, + voice_ids: list[str] | None = None, ) -> tuple[list[Path], str | None]: """下载视频素材和配音素材。 @@ -1066,6 +1067,9 @@ def _download_all_assets( Note: gen_task 不传入下载函数(session 已关闭), 主函数在下载前后已有汇总日志。 + + 配音下载逻辑:优先使用 voice_library_id(配音素材库资产); + 若为空则 fallback 到 voice_ids[0](前端选择的音频 asset_id)。 """ logger.info("[task_id=%s] [下载素材] 开始下载视频素材", task_id) download_start = time.monotonic() @@ -1085,11 +1089,24 @@ def _download_all_assets( ) audio_path: str | None = None - if voice_library_id: + # 配音下载:优先 voice_library_id,fallback 到 voice_ids[0] + effective_voice_id = voice_library_id + if not effective_voice_id and voice_ids: + effective_voice_id = voice_ids[0] + logger.info( + "[task_id=%s] [下载配音] voice_library_id 为空,fallback 到 voice_ids[0]=%s", + task_id, + effective_voice_id, + ) + if effective_voice_id: local_audio = temp_path / "voice.mp3" - if _download_voice_asset(voice_library_id, local_audio): + if _download_voice_asset(effective_voice_id, local_audio): audio_path = str(local_audio) - logger.info("[task_id=%s] [下载配音] 配音下载成功", task_id) + logger.info( + "[task_id=%s] [下载配音] 配音下载成功 (source=%s)", + task_id, + "voice_library_id" if voice_library_id else "voice_ids", + ) return downloaded_videos, audio_path @@ -1422,6 +1439,7 @@ def generate_video(self, task_id: str) -> dict: task_asset_ids=task_asset_ids, voice_library_id=voice_library_id, task_id=task_id, + voice_ids=task_info.get("voice_ids", []), ) if gen_task: diff --git a/tests/unit/test_preview_voice_ids_fallback.py b/tests/unit/test_preview_voice_ids_fallback.py new file mode 100644 index 000000000..8d95fc672 --- /dev/null +++ b/tests/unit/test_preview_voice_ids_fallback.py @@ -0,0 +1,161 @@ +"""Tests for voice_ids fallback in _download_all_assets. + +When voice_library_id is empty but voice_ids is non-empty, the Worker +should fallback to voice_ids[0] as the audio asset_id. +""" +import pytest +from pathlib import Path +from unittest.mock import patch, MagicMock + + +class TestDownloadAllAssetsVoiceIdsFallback: + """_download_all_assets 配音下载 fallback 逻辑测试。""" + + @patch("worker_app.tasks.generation._download_voice_asset") + @patch("worker_app.tasks.generation._download_library_assets") + def test_voice_library_id_takes_priority( + self, mock_download_videos, mock_download_voice, tmp_path + ): + """voice_library_id 存在时优先使用,不 fallback 到 voice_ids。""" + from worker_app.tasks.generation import _download_all_assets + + mock_download_videos.return_value = [tmp_path / "v1.mp4"] + mock_download_voice.return_value = True + + videos, audio = _download_all_assets( + temp_path=tmp_path, + asset_library_id="lib-1", + project_id="proj-1", + task_asset_ids=["a1"], + voice_library_id="voice-lib-123", + task_id="task-1", + voice_ids=["voice-ids-456"], + ) + + assert audio is not None + mock_download_voice.assert_called_once() + call_args = mock_download_voice.call_args + assert call_args[0][0] == "voice-lib-123" # first positional arg + + @patch("worker_app.tasks.generation._download_voice_asset") + @patch("worker_app.tasks.generation._download_library_assets") + def test_fallback_to_voice_ids_when_voice_library_id_empty( + self, mock_download_videos, mock_download_voice, tmp_path + ): + """voice_library_id 为空时 fallback 到 voice_ids[0]。""" + from worker_app.tasks.generation import _download_all_assets + + mock_download_videos.return_value = [tmp_path / "v1.mp4"] + mock_download_voice.return_value = True + + videos, audio = _download_all_assets( + temp_path=tmp_path, + asset_library_id="lib-1", + project_id="proj-1", + task_asset_ids=["a1"], + voice_library_id="", # 空字符串 + task_id="task-2", + voice_ids=["voice-asset-789"], + ) + + assert audio is not None + mock_download_voice.assert_called_once() + call_args = mock_download_voice.call_args + assert call_args[0][0] == "voice-asset-789" + + @patch("worker_app.tasks.generation._download_voice_asset") + @patch("worker_app.tasks.generation._download_library_assets") + def test_no_audio_when_both_empty( + self, mock_download_videos, mock_download_voice, tmp_path + ): + """voice_library_id 和 voice_ids 都为空时,不下载音频。""" + from worker_app.tasks.generation import _download_all_assets + + mock_download_videos.return_value = [tmp_path / "v1.mp4"] + + videos, audio = _download_all_assets( + temp_path=tmp_path, + asset_library_id="lib-1", + project_id="proj-1", + task_asset_ids=["a1"], + voice_library_id="", + task_id="task-3", + voice_ids=[], + ) + + assert audio is None + mock_download_voice.assert_not_called() + + @patch("worker_app.tasks.generation._download_voice_asset") + @patch("worker_app.tasks.generation._download_library_assets") + def test_no_audio_when_voice_ids_none( + self, mock_download_videos, mock_download_voice, tmp_path + ): + """voice_ids 为 None 时,不触发 fallback。""" + from worker_app.tasks.generation import _download_all_assets + + mock_download_videos.return_value = [tmp_path / "v1.mp4"] + + videos, audio = _download_all_assets( + temp_path=tmp_path, + asset_library_id="lib-1", + project_id="proj-1", + task_asset_ids=["a1"], + voice_library_id="", + task_id="task-4", + voice_ids=None, + ) + + assert audio is None + mock_download_voice.assert_not_called() + + @patch("worker_app.tasks.generation._download_voice_asset") + @patch("worker_app.tasks.generation._download_library_assets") + def test_voice_library_id_empty_string_fallback( + self, mock_download_videos, mock_download_voice, tmp_path + ): + """voice_library_id 为空字符串且 voice_ids 有多个元素时,取第一个。""" + from worker_app.tasks.generation import _download_all_assets + + mock_download_videos.return_value = [tmp_path / "v1.mp4"] + mock_download_voice.return_value = True + + videos, audio = _download_all_assets( + temp_path=tmp_path, + asset_library_id="lib-1", + project_id="proj-1", + task_asset_ids=["a1"], + voice_library_id="", + task_id="task-5", + voice_ids=["first-id", "second-id", "third-id"], + ) + + assert audio is not None + call_args = mock_download_voice.call_args + assert call_args[0][0] == "first-id" + + @patch("worker_app.tasks.generation._download_voice_asset") + @patch("worker_app.tasks.generation._download_library_assets") + def test_backward_compat_no_voice_ids_param( + self, mock_download_videos, mock_download_voice, tmp_path + ): + """不传 voice_ids 参数时,行为与之前一致(向后兼容)。""" + from worker_app.tasks.generation import _download_all_assets + + mock_download_videos.return_value = [tmp_path / "v1.mp4"] + mock_download_voice.return_value = True + + # 不传 voice_ids + videos, audio = _download_all_assets( + temp_path=tmp_path, + asset_library_id="lib-1", + project_id="proj-1", + task_asset_ids=["a1"], + voice_library_id="voice-lib-999", + task_id="task-6", + ) + + assert audio is not None + mock_download_voice.assert_called_once_with( + "voice-lib-999", tmp_path / "voice.mp3" + ) -- 2.54.0 From 16af9ce31666d98c459d51d78feeee98613af785 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 9 Aug 2026 05:59:50 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/test_preview_voice_ids_fallback.py | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_preview_voice_ids_fallback.py b/tests/unit/test_preview_voice_ids_fallback.py index 8d95fc672..acac09d1a 100644 --- a/tests/unit/test_preview_voice_ids_fallback.py +++ b/tests/unit/test_preview_voice_ids_fallback.py @@ -3,9 +3,11 @@ When voice_library_id is empty but voice_ids is non-empty, the Worker should fallback to voice_ids[0] as the audio asset_id. """ -import pytest + from pathlib import Path -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + +import pytest class TestDownloadAllAssetsVoiceIdsFallback: @@ -13,9 +15,7 @@ class TestDownloadAllAssetsVoiceIdsFallback: @patch("worker_app.tasks.generation._download_voice_asset") @patch("worker_app.tasks.generation._download_library_assets") - def test_voice_library_id_takes_priority( - self, mock_download_videos, mock_download_voice, tmp_path - ): + def test_voice_library_id_takes_priority(self, mock_download_videos, mock_download_voice, tmp_path): """voice_library_id 存在时优先使用,不 fallback 到 voice_ids。""" from worker_app.tasks.generation import _download_all_assets @@ -65,9 +65,7 @@ class TestDownloadAllAssetsVoiceIdsFallback: @patch("worker_app.tasks.generation._download_voice_asset") @patch("worker_app.tasks.generation._download_library_assets") - def test_no_audio_when_both_empty( - self, mock_download_videos, mock_download_voice, tmp_path - ): + def test_no_audio_when_both_empty(self, mock_download_videos, mock_download_voice, tmp_path): """voice_library_id 和 voice_ids 都为空时,不下载音频。""" from worker_app.tasks.generation import _download_all_assets @@ -88,9 +86,7 @@ class TestDownloadAllAssetsVoiceIdsFallback: @patch("worker_app.tasks.generation._download_voice_asset") @patch("worker_app.tasks.generation._download_library_assets") - def test_no_audio_when_voice_ids_none( - self, mock_download_videos, mock_download_voice, tmp_path - ): + def test_no_audio_when_voice_ids_none(self, mock_download_videos, mock_download_voice, tmp_path): """voice_ids 为 None 时,不触发 fallback。""" from worker_app.tasks.generation import _download_all_assets @@ -111,9 +107,7 @@ class TestDownloadAllAssetsVoiceIdsFallback: @patch("worker_app.tasks.generation._download_voice_asset") @patch("worker_app.tasks.generation._download_library_assets") - def test_voice_library_id_empty_string_fallback( - self, mock_download_videos, mock_download_voice, tmp_path - ): + def test_voice_library_id_empty_string_fallback(self, mock_download_videos, mock_download_voice, tmp_path): """voice_library_id 为空字符串且 voice_ids 有多个元素时,取第一个。""" from worker_app.tasks.generation import _download_all_assets @@ -136,9 +130,7 @@ class TestDownloadAllAssetsVoiceIdsFallback: @patch("worker_app.tasks.generation._download_voice_asset") @patch("worker_app.tasks.generation._download_library_assets") - def test_backward_compat_no_voice_ids_param( - self, mock_download_videos, mock_download_voice, tmp_path - ): + def test_backward_compat_no_voice_ids_param(self, mock_download_videos, mock_download_voice, tmp_path): """不传 voice_ids 参数时,行为与之前一致(向后兼容)。""" from worker_app.tasks.generation import _download_all_assets @@ -156,6 +148,4 @@ class TestDownloadAllAssetsVoiceIdsFallback: ) assert audio is not None - mock_download_voice.assert_called_once_with( - "voice-lib-999", tmp_path / "voice.mp3" - ) + mock_download_voice.assert_called_once_with("voice-lib-999", tmp_path / "voice.mp3") -- 2.54.0