Compare commits

..

6 Commits

Author SHA1 Message Date
xiaoxia fcc12367f2 test: 补充 buildPayload smoke test import
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m25s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m9s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m10s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m7s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m23s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m6s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m12s
AI Code Review / AI Code Review (pull_request) Successful in 6m34s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m50s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Successful in 23s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 48s
2026-07-29 22:34:21 +08:00
xiaoxia 3fded204bb fix(lint): Prettier format useGenerateVideo
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-07-29 22:33:45 +08:00
xiaoxia 9e6b8b01e3 fix(lint): Prettier format buildPayload 2026-07-29 22:33:44 +08:00
xiaoxia e091c5beb7 test(generate): 补充 buildPayload 模块 smoke test
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 29s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m6s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m4s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 40s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m40s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m30s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 47s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 59s
AI Code Review / AI Code Review (pull_request) Successful in 6m14s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m40s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 9s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-29 22:00:17 +08:00
xiaoxia aef01b5091 refactor(generate): 简化 useGenerateVideo,抽取payload构建和校验 2026-07-29 21:59:53 +08:00
xiaoxia ee704efc0b refactor(generate): 提取 buildPayload 和 validateGenerateInputs 2026-07-29 21:59:30 +08:00
4 changed files with 85 additions and 358 deletions
@@ -0,0 +1,76 @@
import type { UseGenerateVideoProps } from "./types"
import { buildVoiceConfig } from "./voiceConfig"
/**
* 构建 updateEditPlan 的 payload
* 从 props 中提取需要的字段,组装成 API 所需的 config 结构
*/
export const buildEditPlanPayload = (props: UseGenerateVideoProps) => {
const {
titleSettings,
selectedMaterials,
materialMode,
smartSelectedIds,
voiceMode,
selectedVoice,
selectedClonedVoice,
coverSettings,
videoRatio,
style,
duration,
autoSubtitles,
bgm,
generateCount,
} = props
const voiceConfig = buildVoiceConfig({
voiceMode,
selectedVoice,
selectedClonedVoice,
})
return {
name: titleSettings.title.trim(),
config: {
asset_ids: materialMode === "auto" ? smartSelectedIds : selectedMaterials,
title_config: {
ai_auto_select: titleSettings.aiAutoSelect,
content: titleSettings.title,
position: titleSettings.position,
font_preset: titleSettings.font,
font_color: titleSettings.color,
font_size: titleSettings.size,
},
cover_config: coverSettings,
...voiceConfig,
ratio: videoRatio,
style,
duration,
auto_subtitles: autoSubtitles,
bgm,
generate_count: generateCount,
material_mode: materialMode,
},
total_duration: duration,
status: "editing" as const,
}
}
/**
* 生成前置校验
* 返回错误信息,通过则返回 null
*/
export const validateGenerateInputs = (props: UseGenerateVideoProps): string | null => {
const { titleSettings, materialMode, selectedMaterials, voiceMode, selectedClonedVoice } = props
if (!titleSettings.title.trim()) {
return "请先选择或输入标题"
}
if (materialMode === "manual" && selectedMaterials.length === 0) {
return "请至少选择一个素材"
}
if (voiceMode === "clone" && !selectedClonedVoice) {
return "请先选择一个克隆音色"
}
return null
}
@@ -9,27 +9,11 @@ import { generateEditPlan, updateEditPlan, getEditPlan } from "@/api/template-ed
import type { UseGenerateVideoProps } from "./generate-video/types"
import { getGenerationPhase } from "./generate-video/phase"
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
import { buildVoiceConfig } from "./generate-video/voiceConfig"
import { buildEditPlanPayload, validateGenerateInputs } from "./generate-video/buildPayload"
import { extractBackendError, translateError } from "./generate-video/errorUtils"
export function useGenerateVideo(props: UseGenerateVideoProps) {
const {
titleSettings,
selectedTemplate,
selectedMaterials,
materialMode,
smartSelectedIds,
voiceMode,
selectedVoice,
selectedClonedVoice,
coverSettings,
videoRatio,
style,
duration,
autoSubtitles,
bgm,
generateCount,
} = props
const { selectedTemplate } = props
/* ── 生成状态 ── */
const [generating, setGenerating] = useState(false)
@@ -58,16 +42,9 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
/* ── 生成视频 ── */
const generate = useCallback(async () => {
if (!titleSettings.title.trim()) {
message.warning("请先选择或输入标题")
return
}
if (materialMode === "manual" && selectedMaterials.length === 0) {
message.warning("请至少选择一个素材")
return
}
if (voiceMode === "clone" && !selectedClonedVoice) {
message.warning("请先选择一个克隆音色")
const errorMsg = validateGenerateInputs(props)
if (errorMsg) {
message.warning(errorMsg)
return
}
@@ -78,41 +55,13 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
clearTimer()
try {
const voiceConfig = buildVoiceConfig({
voiceMode,
selectedVoice,
selectedClonedVoice,
})
const payload = buildEditPlanPayload(props)
// 获取或创建草稿
await getEditPlan(selectedTemplate)
// 更新草稿内容 + 切换到 editing 状态
await updateEditPlan(selectedTemplate, {
name: titleSettings.title.trim(),
config: {
asset_ids: materialMode === "auto" ? smartSelectedIds : selectedMaterials,
title_config: {
ai_auto_select: titleSettings.aiAutoSelect,
content: titleSettings.title,
position: titleSettings.position,
font_preset: titleSettings.font,
font_color: titleSettings.color,
font_size: titleSettings.size,
},
cover_config: coverSettings,
...voiceConfig,
ratio: videoRatio,
style,
duration,
auto_subtitles: autoSubtitles,
bgm,
generate_count: generateCount,
material_mode: materialMode,
},
total_duration: duration,
status: "editing",
})
await updateEditPlan(selectedTemplate, payload)
await generateEditPlan(selectedTemplate)
startPolling()
@@ -125,25 +74,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
setGenerateError(finalMsg)
message.error(finalMsg)
}
}, [
titleSettings,
selectedMaterials,
selectedVoice,
voiceMode,
selectedClonedVoice,
videoRatio,
style,
duration,
autoSubtitles,
bgm,
selectedTemplate,
generateCount,
materialMode,
coverSettings,
smartSelectedIds,
clearTimer,
startPolling,
])
}, [props, selectedTemplate, clearTimer, startPolling])
/* 重新生成(失败后重试) */
const retry = useCallback(() => {
@@ -42,3 +42,4 @@ import "@/pages/generate/hooks/generate-video/types"
import "@/pages/generate/hooks/generate-video/phase"
import "@/pages/generate/hooks/generate-video/voiceConfig"
import "@/pages/generate/hooks/generate-video/errorUtils"
import "@/pages/generate/hooks/generate-video/buildPayload"
-281
View File
@@ -1,281 +0,0 @@
"""edit_template 剪辑模板实体单测."""
from datetime import datetime, timezone
import pytest
from domain.edit_template import EditTemplate, EditTemplateStatus
from domain.editing_mode import EditingMode
# ── EditTemplateStatus 枚举 ──────────────────────────────────────────────────
class TestEditTemplateStatus:
"""EditTemplateStatus 枚举"""
def test_enum_values(self):
assert EditTemplateStatus.ACTIVE.value == "active"
assert EditTemplateStatus.INACTIVE.value == "inactive"
def test_is_str_enum(self):
assert isinstance(EditTemplateStatus.ACTIVE, str)
assert EditTemplateStatus.ACTIVE == "active"
def test_from_string(self):
assert EditTemplateStatus("active") == EditTemplateStatus.ACTIVE
assert EditTemplateStatus("inactive") == EditTemplateStatus.INACTIVE
def test_invalid_raises(self):
with pytest.raises(ValueError):
EditTemplateStatus("deleted")
# ── EditTemplate.create 工厂方法 ────────────────────────────────────────────
class TestEditTemplateCreate:
"""EditTemplate.create 工厂方法"""
def test_minimal_create(self):
t = EditTemplate.create("测试模板")
assert t.id is not None
assert len(t.id) == 32 # uuid4 hex
assert t.name == "测试模板"
assert t.description == ""
assert t.template_type == "default"
assert t.editing_mode == "one_take"
assert t.config == {}
assert t.preview_url == ""
assert t.sort_weight == 0
assert t.status == EditTemplateStatus.ACTIVE
assert t.version == 1
def test_unique_ids(self):
t1 = EditTemplate.create("模板A")
t2 = EditTemplate.create("模板B")
assert t1.id != t2.id
def test_custom_fields(self):
t = EditTemplate.create(
"自定义模板",
description="这是一个自定义模板",
template_type="story",
editing_mode="one_take",
config={"key": "value"},
preview_url="https://example.com/preview.mp4",
sort_weight=100,
status=EditTemplateStatus.INACTIVE,
version=2,
)
assert t.name == "自定义模板"
assert t.description == "这是一个自定义模板"
assert t.template_type == "story"
assert t.editing_mode == "one_take"
assert t.config == {"key": "value"}
assert t.preview_url == "https://example.com/preview.mp4"
assert t.sort_weight == 100
assert t.status == EditTemplateStatus.INACTIVE
assert t.version == 2
def test_name_stripped(self):
t = EditTemplate.create(" 带空格的模板 ")
assert t.name == "带空格的模板"
def test_empty_name_raises(self):
with pytest.raises(ValueError, match="名称"):
EditTemplate.create("")
def test_whitespace_only_name_raises(self):
with pytest.raises(ValueError):
EditTemplate.create(" ")
def test_invalid_editing_mode_raises(self):
with pytest.raises(ValueError, match="editing_mode"):
EditTemplate.create("测试", editing_mode="invalid_mode")
def test_empty_editing_mode_falls_back_to_default(self):
t = EditTemplate.create("测试", editing_mode="")
assert t.editing_mode == "one_take"
def test_whitespace_editing_mode_falls_back(self):
t = EditTemplate.create("测试", editing_mode=" ")
assert t.editing_mode == "one_take"
def test_editing_mode_stripped(self):
t = EditTemplate.create("测试", editing_mode=" one_take ")
assert t.editing_mode == "one_take"
def test_description_stripped(self):
t = EditTemplate.create("测试", description=" 描述 ")
assert t.description == "描述"
def test_template_type_stripped(self):
t = EditTemplate.create("测试", template_type=" vlog ")
assert t.template_type == "vlog"
def test_empty_template_type_falls_back(self):
t = EditTemplate.create("测试", template_type="")
assert t.template_type == "default"
def test_none_config_becomes_empty_dict(self):
t = EditTemplate.create("测试", config=None)
assert t.config == {}
assert isinstance(t.config, dict)
def test_preview_url_stripped(self):
t = EditTemplate.create("测试", preview_url=" https://x.com/a.mp4 ")
assert t.preview_url == "https://x.com/a.mp4"
def test_timestamps_are_utc(self):
t = EditTemplate.create("测试")
assert t.created_at.tzinfo is not None
assert t.updated_at.tzinfo is not None
def test_created_at_equals_updated_at_on_create(self):
t = EditTemplate.create("测试")
# 创建时两个时间应该非常接近
diff = abs((t.updated_at - t.created_at).total_seconds())
assert diff < 1.0
# ── 状态操作 ─────────────────────────────────────────────────────────────────
class TestEditTemplateStatusOperations:
"""EditTemplate 状态操作"""
def test_activate_sets_active(self):
t = EditTemplate.create("测试", status=EditTemplateStatus.INACTIVE)
t.activate()
assert t.status == EditTemplateStatus.ACTIVE
assert t.is_active is True
def test_deactivate_sets_inactive(self):
t = EditTemplate.create("测试")
t.deactivate()
assert t.status == EditTemplateStatus.INACTIVE
assert t.is_active is False
def test_is_active_true(self):
t = EditTemplate.create("测试")
assert t.is_active is True
def test_is_active_false(self):
t = EditTemplate.create("测试", status=EditTemplateStatus.INACTIVE)
assert t.is_active is False
def test_activate_updates_updated_at(self):
t = EditTemplate.create("测试", status=EditTemplateStatus.INACTIVE)
old_updated = t.updated_at
t.activate()
assert t.updated_at >= old_updated
def test_deactivate_updates_updated_at(self):
t = EditTemplate.create("测试")
old_updated = t.updated_at
t.deactivate()
assert t.updated_at >= old_updated
# ── 版本操作 ─────────────────────────────────────────────────────────────────
class TestEditTemplateVersion:
"""EditTemplate 版本操作"""
def test_bump_version_increments(self):
t = EditTemplate.create("测试")
assert t.version == 1
t.bump_version()
assert t.version == 2
def test_bump_version_multiple(self):
t = EditTemplate.create("测试", version=5)
t.bump_version()
t.bump_version()
t.bump_version()
assert t.version == 8
def test_bump_version_updates_updated_at(self):
t = EditTemplate.create("测试")
old_updated = t.updated_at
t.bump_version()
assert t.updated_at >= old_updated
# ── dataclass 基础特性 ───────────────────────────────────────────────────────
class TestEditTemplateBasics:
"""EditTemplate 基础特性"""
def test_slots_no_extra_attrs(self):
t = EditTemplate.create("测试")
with pytest.raises(AttributeError):
t.nonexistent_field = "value"
def test_direct_construction_minimal(self):
# 最小构造:仅必填字段 + 状态,其余走默认值
t = EditTemplate(
id="custom_id",
name="直接构造",
status=EditTemplateStatus.ACTIVE,
)
assert t.id == "custom_id"
assert t.name == "直接构造"
assert t.status == EditTemplateStatus.ACTIVE
# 默认值检查
assert t.description == ""
assert t.config == {}
assert t.version == 1
assert t.editing_mode == EditingMode.ONE_TAKE.value
assert isinstance(t.created_at, datetime)
assert isinstance(t.updated_at, datetime)
def test_direct_construction_full(self):
# 完整构造:所有字段都传
now = datetime(2025, 1, 1, tzinfo=timezone.utc)
t = EditTemplate(
id="full_id",
name="完整构造",
description="测试描述",
template_type="custom",
editing_mode=EditingMode.PIP.value,
config={"key": "value"},
preview_url="https://example.com/preview.jpg",
sort_weight=100,
status=EditTemplateStatus.INACTIVE,
version=3,
created_at=now,
updated_at=now,
)
assert t.id == "full_id"
assert t.name == "完整构造"
assert t.description == "测试描述"
assert t.template_type == "custom"
assert t.editing_mode == EditingMode.PIP.value
assert t.config == {"key": "value"}
assert t.preview_url == "https://example.com/preview.jpg"
assert t.sort_weight == 100
assert t.status == EditTemplateStatus.INACTIVE
assert t.version == 3
assert t.created_at == now
assert t.updated_at == now
def test_config_is_independent(self):
# 不同实例的 config 应该是独立的 dict
t1 = EditTemplate.create("模板1")
t2 = EditTemplate.create("模板2")
t1.config["key"] = "value"
assert "key" not in t2.config
def test_equality(self):
# 两个不同实例即使内容相同也不等(id不同)
t1 = EditTemplate.create("同名模板")
t2 = EditTemplate.create("同名模板")
assert t1 != t2
def test_same_id_equal(self):
now = datetime.now(timezone.utc)
t1 = EditTemplate(id="same", name="同名", created_at=now, updated_at=now)
t2 = EditTemplate(id="same", name="同名", created_at=now, updated_at=now)
assert t1 == t2