2972f19954
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m38s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m42s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m27s
CI/CD Pipeline / Unit Tests (push) Failing after 5m54s
CI/CD Pipeline / Integration Tests (push) Successful in 2m26s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 14s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
598 lines
21 KiB
Python
Executable File
598 lines
21 KiB
Python
Executable File
from dataclasses import FrozenInstanceError
|
|
|
|
"""domain 层剩余小模块单测 - bgm_utils/exceptions/editing_mode/recipe/template/template_version/template_clip_config/preset_bgm/preset_voices/title_library/voice_library"""
|
|
|
|
import pytest
|
|
|
|
from packages.domain.bgm_utils import merge_bgm_config
|
|
from packages.domain.editing_mode import EditingMode
|
|
from packages.domain.exceptions import (
|
|
DomainError,
|
|
NotFoundError,
|
|
QuotaExceededError,
|
|
ValidationError,
|
|
)
|
|
from packages.domain.preset_bgm import (
|
|
PRESET_BGM_LIBRARY,
|
|
PresetBGM,
|
|
get_preset_bgm,
|
|
list_preset_bgm_by_style,
|
|
search_preset_bgm,
|
|
)
|
|
from packages.domain.preset_voices import (
|
|
PRESET_VOICES,
|
|
PresetVoice,
|
|
get_preset_voice_by_id,
|
|
get_preset_voices,
|
|
is_preset_voice,
|
|
)
|
|
from packages.domain.recipe import Recipe, RecipeItem
|
|
from packages.domain.template import Template, TemplateCategory, TemplateSegment
|
|
from packages.domain.template_clip_config import (
|
|
ClipType,
|
|
TemplateClipConfig,
|
|
TransitionEffect,
|
|
)
|
|
from packages.domain.template_version import EditTemplateVersion
|
|
from packages.domain.title_library import TitleLibraryItem
|
|
from packages.domain.voice_library import VoiceLibraryItem
|
|
|
|
# ── EditingMode ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestEditingMode:
|
|
"""EditingMode 枚举测试"""
|
|
|
|
def test_all_modes_exist(self):
|
|
assert EditingMode.ONE_TAKE == "one_take"
|
|
assert EditingMode.PIP == "pip"
|
|
assert EditingMode.VOICE_OVER == "voice_over"
|
|
assert EditingMode.VOICE_PIP == "voice_pip"
|
|
|
|
def test_str_enum(self):
|
|
assert isinstance(EditingMode.ONE_TAKE, str)
|
|
|
|
def test_four_modes(self):
|
|
assert len(EditingMode) == 4
|
|
|
|
|
|
# ── Exceptions ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestDomainExceptions:
|
|
"""异常类测试"""
|
|
|
|
def test_domain_error_base(self):
|
|
err = DomainError("test")
|
|
assert isinstance(err, Exception)
|
|
assert str(err) == "test"
|
|
|
|
def test_not_found_error(self):
|
|
err = NotFoundError("not found")
|
|
assert isinstance(err, DomainError)
|
|
assert str(err) == "not found"
|
|
|
|
def test_validation_error(self):
|
|
err = ValidationError("invalid")
|
|
assert isinstance(err, DomainError)
|
|
assert str(err) == "invalid"
|
|
|
|
def test_quota_exceeded_error(self):
|
|
err = QuotaExceededError(dimension="storage", limit=100, used=150)
|
|
assert isinstance(err, DomainError)
|
|
assert err.dimension == "storage"
|
|
assert err.limit == 100
|
|
assert err.used == 150
|
|
assert "storage" in str(err)
|
|
assert "150/100" in str(err)
|
|
|
|
def test_not_found_is_domain_error(self):
|
|
assert issubclass(NotFoundError, DomainError)
|
|
|
|
def test_validation_is_domain_error(self):
|
|
assert issubclass(ValidationError, DomainError)
|
|
|
|
def test_quota_exceeded_is_domain_error(self):
|
|
assert issubclass(QuotaExceededError, DomainError)
|
|
|
|
|
|
# ── BGM Utils ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestMergeBgmConfig:
|
|
"""merge_bgm_config 函数测试"""
|
|
|
|
def test_user_empty_returns_template_copy(self):
|
|
template = {"enabled": True, "volume": 0.5, "source": "library"}
|
|
result = merge_bgm_config(template, {})
|
|
assert result == template
|
|
# 确保是拷贝不是引用
|
|
result["volume"] = 0.9
|
|
assert template["volume"] == 0.5
|
|
|
|
def test_template_empty_returns_user_copy(self):
|
|
user = {"enabled": False, "volume": 0.3}
|
|
result = merge_bgm_config({}, user)
|
|
assert result == user
|
|
|
|
def test_user_none_returns_template(self):
|
|
template = {"enabled": True, "volume": 0.5}
|
|
result = merge_bgm_config(template, None) # type: ignore
|
|
assert result == template
|
|
|
|
def test_template_none_returns_user(self):
|
|
user = {"enabled": True, "volume": 0.5}
|
|
result = merge_bgm_config(None, user) # type: ignore
|
|
assert result == user
|
|
|
|
def test_user_overrides_template(self):
|
|
template = {"volume": 0.3, "source": "library", "asset_id": "tpl-1"}
|
|
user = {"volume": 0.8, "asset_id": "user-1"}
|
|
result = merge_bgm_config(template, user)
|
|
assert result["volume"] == 0.8
|
|
assert result["asset_id"] == "user-1"
|
|
assert result["source"] == "library" # 模板保留
|
|
|
|
def test_enabled_special_handling_user_not_set(self):
|
|
"""用户没传 enabled 时保留模板的 enabled"""
|
|
template = {"enabled": True, "volume": 0.5}
|
|
user = {"volume": 0.8}
|
|
result = merge_bgm_config(template, user)
|
|
assert result["enabled"] is True # 保留模板值
|
|
|
|
def test_enabled_user_explicit_false(self):
|
|
"""用户显式传了 enabled=False 则覆盖"""
|
|
template = {"enabled": True, "volume": 0.5}
|
|
user = {"enabled": False}
|
|
result = merge_bgm_config(template, user)
|
|
assert result["enabled"] is False
|
|
|
|
def test_enabled_user_explicit_true(self):
|
|
"""用户显式传了 enabled=True 则覆盖"""
|
|
template = {"enabled": False, "volume": 0.5}
|
|
user = {"enabled": True}
|
|
result = merge_bgm_config(template, user)
|
|
assert result["enabled"] is True
|
|
|
|
def test_full_merge(self):
|
|
"""完整合并场景"""
|
|
template = {
|
|
"enabled": True,
|
|
"source": "library",
|
|
"volume": 0.3,
|
|
"fade_in": 0.0,
|
|
"fade_out": 0.0,
|
|
"loop_enabled": True,
|
|
}
|
|
user = {
|
|
"volume": 0.7,
|
|
"asset_id": "my-bgm",
|
|
"fade_in": 1.0,
|
|
}
|
|
result = merge_bgm_config(template, user)
|
|
assert result["enabled"] is True # 保留模板
|
|
assert result["volume"] == 0.7 # 用户覆盖
|
|
assert result["source"] == "library" # 模板保留
|
|
assert result["asset_id"] == "my-bgm" # 用户新增
|
|
assert result["fade_in"] == 1.0 # 用户覆盖
|
|
assert result["fade_out"] == 0.0 # 模板保留
|
|
assert result["loop_enabled"] is True # 模板保留
|
|
|
|
|
|
# ── Recipe ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestRecipe:
|
|
"""Recipe / RecipeItem 测试"""
|
|
|
|
def test_recipe_item_create(self):
|
|
item = RecipeItem(id="item-1", recipe_id="r1", item_type="asset", item_id="a1")
|
|
assert item.id == "item-1"
|
|
assert item.recipe_id == "r1"
|
|
assert item.item_type == "asset"
|
|
assert item.item_id == "a1"
|
|
assert item.position == 0
|
|
assert item.metadata_ == {}
|
|
|
|
def test_recipe_create(self):
|
|
recipe = Recipe(id="r1", user_id="u1", name="我的配方")
|
|
assert recipe.id == "r1"
|
|
assert recipe.user_id == "u1"
|
|
assert recipe.name == "我的配方"
|
|
assert recipe.description == ""
|
|
assert recipe.items == []
|
|
assert recipe.is_active is True
|
|
assert recipe.generation_params == {}
|
|
assert recipe.created_at is not None
|
|
|
|
def test_recipe_with_items(self):
|
|
items = [
|
|
RecipeItem(id="i1", recipe_id="r1", item_type="asset", item_id="a1", position=0),
|
|
RecipeItem(id="i2", recipe_id="r1", item_type="title", item_id="t1", position=1),
|
|
]
|
|
recipe = Recipe(id="r1", user_id="u1", name="test", items=items)
|
|
assert len(recipe.items) == 2
|
|
assert recipe.items[0].item_type == "asset"
|
|
assert recipe.items[1].position == 1
|
|
|
|
def test_recipe_items_independent_list(self):
|
|
"""不同 recipe 的 items 是独立列表"""
|
|
r1 = Recipe(id="r1", user_id="u1", name="r1")
|
|
r2 = Recipe(id="r2", user_id="u1", name="r2")
|
|
assert r1.items is not r2.items
|
|
|
|
|
|
# ── Template ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTemplate:
|
|
"""Template 相关实体测试"""
|
|
|
|
def test_template_segment(self):
|
|
seg = TemplateSegment(
|
|
id="seg-1",
|
|
template_id="t1",
|
|
segment_order=0,
|
|
duration_min=3.0,
|
|
duration_max=5.0,
|
|
)
|
|
assert seg.id == "seg-1"
|
|
assert seg.template_id == "t1"
|
|
assert seg.segment_order == 0
|
|
assert seg.duration_min == 3.0
|
|
assert seg.duration_max == 5.0
|
|
assert seg.material_type is None
|
|
assert seg.created_at is not None
|
|
|
|
def test_template_create(self):
|
|
tpl = Template(
|
|
id="t1",
|
|
user_id="u1",
|
|
name="口播模板",
|
|
mode="voice_over",
|
|
)
|
|
assert tpl.id == "t1"
|
|
assert tpl.user_id == "u1"
|
|
assert tpl.name == "口播模板"
|
|
assert tpl.mode == "voice_over"
|
|
assert tpl.category == ""
|
|
assert tpl.tags == []
|
|
assert tpl.segments == []
|
|
assert tpl.is_active is True
|
|
assert tpl.estimated_duration == 0.0
|
|
|
|
def test_template_with_segments(self):
|
|
segs = [
|
|
TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=2, duration_max=4),
|
|
TemplateSegment(id="s2", template_id="t1", segment_order=1, duration_min=5, duration_max=8),
|
|
]
|
|
tpl = Template(id="t1", user_id="u1", name="test", mode="one_take", segments=segs)
|
|
assert len(tpl.segments) == 2
|
|
assert tpl.segments[0].segment_order == 0
|
|
|
|
def test_template_category(self):
|
|
cat = TemplateCategory(id="cat-1", user_id="u1", name="热门")
|
|
assert cat.id == "cat-1"
|
|
assert cat.user_id == "u1"
|
|
assert cat.name == "热门"
|
|
assert cat.created_at is not None
|
|
|
|
|
|
# ── TemplateVersion ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestEditTemplateVersion:
|
|
"""EditTemplateVersion 测试"""
|
|
|
|
def test_create_basic(self):
|
|
v = EditTemplateVersion.create(template_id="t1", version=1)
|
|
assert v.id
|
|
assert len(v.id) == 32
|
|
assert v.template_id == "t1"
|
|
assert v.version == 1
|
|
assert v.name == ""
|
|
assert v.editing_mode == "one_take"
|
|
assert v.config == {}
|
|
assert v.clip_configs == []
|
|
assert v.change_note == ""
|
|
assert v.published_by == ""
|
|
|
|
def test_create_with_details(self):
|
|
config = {"title": {"size": 36}}
|
|
clips = [{"clip_type": "intro"}, {"clip_type": "outro"}]
|
|
v = EditTemplateVersion.create(
|
|
template_id="t1",
|
|
version=2,
|
|
name="V2 优化版",
|
|
editing_mode="voice_over",
|
|
config=config,
|
|
clip_configs=clips,
|
|
change_note="优化了节奏",
|
|
published_by="user-1",
|
|
)
|
|
assert v.version == 2
|
|
assert v.name == "V2 优化版"
|
|
assert v.editing_mode == "voice_over"
|
|
assert v.config == config
|
|
assert v.clip_configs == clips
|
|
assert v.change_note == "优化了节奏"
|
|
assert v.published_by == "user-1"
|
|
|
|
def test_create_none_config_defaults_empty(self):
|
|
v = EditTemplateVersion.create(template_id="t1", version=1, config=None)
|
|
assert v.config == {}
|
|
|
|
def test_create_none_clip_configs_defaults_empty(self):
|
|
v = EditTemplateVersion.create(template_id="t1", version=1, clip_configs=None)
|
|
assert v.clip_configs == []
|
|
|
|
|
|
# ── TemplateClipConfig ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTemplateClipConfig:
|
|
"""TemplateClipConfig + 枚举测试"""
|
|
|
|
def test_clip_type_values(self):
|
|
assert ClipType.INTRO == "intro"
|
|
assert ClipType.MAIN == "main"
|
|
assert ClipType.TRANSITION == "transition"
|
|
assert ClipType.OUTRO == "outro"
|
|
assert ClipType.TITLE == "title"
|
|
assert ClipType.SUBTITLE == "subtitle"
|
|
|
|
def test_clip_type_count(self):
|
|
assert len(ClipType) == 6
|
|
|
|
def test_transition_effect_values(self):
|
|
assert TransitionEffect.CUT == "cut"
|
|
assert TransitionEffect.FADE == "fade"
|
|
assert TransitionEffect.SLIDE_LEFT == "slide_left"
|
|
assert TransitionEffect.SLIDE_RIGHT == "slide_right"
|
|
assert TransitionEffect.DISSOLVE == "dissolve"
|
|
assert TransitionEffect.WIPE == "wipe"
|
|
|
|
def test_transition_effect_count(self):
|
|
assert len(TransitionEffect) == 6
|
|
|
|
def test_template_clip_config(self):
|
|
clip = TemplateClipConfig(
|
|
id="clip-1",
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=2.0,
|
|
max_duration=5.0,
|
|
)
|
|
assert clip.id == "clip-1"
|
|
assert clip.template_id == "t1"
|
|
assert clip.clip_type == ClipType.MAIN
|
|
assert clip.order == 1
|
|
assert clip.min_duration == 2.0
|
|
assert clip.max_duration == 5.0
|
|
|
|
def test_str_enum(self):
|
|
assert isinstance(ClipType.INTRO, str)
|
|
assert isinstance(TransitionEffect.FADE, str)
|
|
|
|
|
|
# ── PresetBGM ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestPresetBGM:
|
|
"""PresetBGM + 查询函数测试"""
|
|
|
|
def test_preset_bgm_create(self):
|
|
bgm = PresetBGM(
|
|
id="bgm_test_001",
|
|
name="测试音乐",
|
|
style="upbeat",
|
|
duration=120.0,
|
|
)
|
|
assert bgm.id == "bgm_test_001"
|
|
assert bgm.name == "测试音乐"
|
|
assert bgm.style == "upbeat"
|
|
assert bgm.duration == 120.0
|
|
assert bgm.artist == ""
|
|
assert bgm.tags == []
|
|
assert bgm.audio_url == ""
|
|
|
|
def test_preset_bgm_frozen(self):
|
|
bgm = PresetBGM(id="t1", name="t", style="x", duration=10.0)
|
|
with pytest.raises(FrozenInstanceError):
|
|
bgm.name = "改名"
|
|
|
|
def test_library_not_empty(self):
|
|
assert len(PRESET_BGM_LIBRARY) > 0
|
|
|
|
def test_all_presets_have_required_fields(self):
|
|
for bgm in PRESET_BGM_LIBRARY:
|
|
assert bgm.id
|
|
assert bgm.name
|
|
assert bgm.style
|
|
assert bgm.duration > 0
|
|
|
|
def test_get_preset_bgm_existing(self):
|
|
first = PRESET_BGM_LIBRARY[0]
|
|
result = get_preset_bgm(first.id)
|
|
assert result is not None
|
|
assert result.id == first.id
|
|
|
|
def test_get_preset_bgm_nonexistent(self):
|
|
assert get_preset_bgm("nonexistent_bgm") is None
|
|
|
|
def test_list_preset_bgm_by_style(self):
|
|
upbeat = list_preset_bgm_by_style("upbeat")
|
|
assert len(upbeat) > 0
|
|
assert all(b.style == "upbeat" for b in upbeat)
|
|
|
|
def test_list_preset_bgm_by_style_empty(self):
|
|
result = list_preset_bgm_by_style("nonexistent_style")
|
|
assert result == []
|
|
|
|
def test_search_preset_bgm_by_name(self):
|
|
result = search_preset_bgm("阳光")
|
|
assert len(result) >= 1
|
|
assert any("阳光" in b.name for b in result)
|
|
|
|
def test_search_preset_bgm_by_tag(self):
|
|
result = search_preset_bgm("轻快")
|
|
assert len(result) >= 1
|
|
assert any(any("轻快" in t for t in b.tags) for b in result)
|
|
|
|
def test_search_preset_bgm_empty_result(self):
|
|
result = search_preset_bgm("xyz_not_exist_keyword")
|
|
assert result == []
|
|
|
|
|
|
# ── PresetVoices ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestPresetVoices:
|
|
"""PresetVoice + 查询函数测试"""
|
|
|
|
def test_preset_voice_create(self):
|
|
v = PresetVoice(
|
|
voice_id="test_voice",
|
|
name="测试音色",
|
|
description="测试用",
|
|
gender="female",
|
|
)
|
|
assert v.voice_id == "test_voice"
|
|
assert v.name == "测试音色"
|
|
assert v.gender == "female"
|
|
assert v.language == "zh-CN"
|
|
assert v.preview_url == ""
|
|
assert v.tags is None
|
|
|
|
def test_preset_voice_to_dict(self):
|
|
v = PresetVoice(
|
|
voice_id="v1",
|
|
name="音色1",
|
|
description="desc",
|
|
gender="male",
|
|
language="zh-CN",
|
|
tags=["温柔", "男声"],
|
|
)
|
|
d = v.to_dict()
|
|
assert d["voice_id"] == "v1"
|
|
assert d["name"] == "音色1"
|
|
assert d["description"] == "desc"
|
|
assert d["gender"] == "male"
|
|
assert d["language"] == "zh-CN"
|
|
assert d["tags"] == ["温柔", "男声"]
|
|
|
|
def test_preset_voice_to_dict_no_tags(self):
|
|
v = PresetVoice(voice_id="v1", name="t", description="d", gender="female")
|
|
d = v.to_dict()
|
|
assert d["tags"] == []
|
|
|
|
def test_preset_voice_frozen(self):
|
|
v = PresetVoice(voice_id="v1", name="t", description="d", gender="female")
|
|
with pytest.raises(FrozenInstanceError):
|
|
v.name = "改名"
|
|
|
|
def test_preset_voices_list_not_empty(self):
|
|
assert len(PRESET_VOICES) > 0
|
|
|
|
def test_get_preset_voices(self):
|
|
voices = get_preset_voices()
|
|
assert len(voices) == len(PRESET_VOICES)
|
|
assert all(isinstance(v, PresetVoice) for v in voices)
|
|
|
|
def test_get_preset_voice_by_id_existing(self):
|
|
first = PRESET_VOICES[0]
|
|
result = get_preset_voice_by_id(first.voice_id)
|
|
assert result is not None
|
|
assert result.voice_id == first.voice_id
|
|
|
|
def test_get_preset_voice_by_id_nonexistent(self):
|
|
assert get_preset_voice_by_id("nonexistent_voice") is None
|
|
|
|
def test_is_preset_voice_true(self):
|
|
first = PRESET_VOICES[0]
|
|
assert is_preset_voice(first.voice_id) is True
|
|
|
|
def test_is_preset_voice_false(self):
|
|
assert is_preset_voice("fake_voice_id") is False
|
|
|
|
|
|
# ── TitleLibrary ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTitleLibraryItem:
|
|
"""TitleLibraryItem 测试"""
|
|
|
|
def test_create_basic(self):
|
|
item = TitleLibraryItem(id="t1", user_id="u1", name="标题1", text="这是标题文案")
|
|
assert item.id == "t1"
|
|
assert item.user_id == "u1"
|
|
assert item.name == "标题1"
|
|
assert item.text == "这是标题文案"
|
|
assert item.category == "default"
|
|
assert item.tags == []
|
|
assert item.usage_count == 0
|
|
assert item.is_active is True
|
|
assert item.created_at is not None
|
|
|
|
def test_create_with_details(self):
|
|
item = TitleLibraryItem(
|
|
id="t1",
|
|
user_id="u1",
|
|
name="爆款标题",
|
|
text="三个方法教你...",
|
|
category="爆款",
|
|
description="高点击率",
|
|
tags=["热门", "干货"],
|
|
usage_count=100,
|
|
)
|
|
assert item.category == "爆款"
|
|
assert item.description == "高点击率"
|
|
assert item.tags == ["热门", "干货"]
|
|
assert item.usage_count == 100
|
|
|
|
|
|
# ── VoiceLibrary ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestVoiceLibraryItem:
|
|
"""VoiceLibraryItem 测试"""
|
|
|
|
def test_create_basic(self):
|
|
item = VoiceLibraryItem(id="v1", user_id="u1", name="我的配音")
|
|
assert item.id == "v1"
|
|
assert item.user_id == "u1"
|
|
assert item.name == "我的配音"
|
|
assert item.text == ""
|
|
assert item.voice_provider == ""
|
|
assert item.duration == 0
|
|
assert item.status == "completed"
|
|
assert item.project_id is None
|
|
assert item.tags == []
|
|
assert item.created_at is not None
|
|
|
|
def test_create_with_details(self):
|
|
item = VoiceLibraryItem(
|
|
id="v1",
|
|
user_id="u1",
|
|
name="产品介绍",
|
|
text="大家好,今天给大家介绍...",
|
|
voice_provider="cosyvoice",
|
|
voice_id="voice_001",
|
|
voice_name="温柔女声",
|
|
audio_url="http://cdn/audio.mp3",
|
|
duration=30.5,
|
|
file_size=102400,
|
|
status="processing",
|
|
project_id="proj-1",
|
|
tags=["产品", "介绍"],
|
|
)
|
|
assert item.voice_provider == "cosyvoice"
|
|
assert item.voice_id == "voice_001"
|
|
assert item.audio_url == "http://cdn/audio.mp3"
|
|
assert item.duration == 30.5
|
|
assert item.file_size == 102400
|
|
assert item.status == "processing"
|
|
assert item.project_id == "proj-1"
|
|
assert item.tags == ["产品", "介绍"]
|