feat(ai): 自由创作视频生成全量移植(/free-create)——Seedance 文/图生视频+按时长计费+人物素材库

后端:
- free_video.py: 提交/轮询/收藏/软删全链路,复用 AITask(新增 is_deleted/is_favorited, migration 0022)
- video_pricing.py: 按时长 token 计费(×1.10 buffer+clamp); video_errors.py 错误归一; media_probe.py 时长探测
- catalog/volcano: Seedance free-video 模型接入+seed(migration 0023)
- 素材库: FreeAssetGroup/FreeAsset(火山 Assets API 引用登记, migration 0009)+ 上传/轮询/删除接口
- settings: FREE_VIDEO_MAX_CONCURRENT 团队并发闸(默认3); CELERY_TASK_ALWAYS_EAGER 本地联调开关(生产恒关)
- 测试: test_free_video.py 新增; billing/products/projects tests 配套调整

前端:
- /free-create 页面+components/free-create/ 全套(输入栏/@mention 素材引用/生成卡/视频详情弹窗/素材库弹窗)
- api.ts/types.ts 扩展 free-video 与 free-assets 接口; 路由/侧边栏入口接入

bug/: 测试清单 (11)(12) 与截图归档

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-07-06 10:58:36 +08:00
co-authored by Claude Fable 5
parent 94202e7bce
commit c5f7eb2acc
45 changed files with 5186 additions and 21 deletions
+51 -1
View File
@@ -59,7 +59,11 @@ def _get_service():
Credentials(ak, sk, SERVICE, REGION),
10, 30,
)
actions = ["CreateAssetGroup", "CreateAsset", "ListAssetGroups", "ListAssets", "GetAsset", "DeleteAsset"]
actions = [
"CreateAssetGroup", "CreateAsset", "ListAssetGroups", "ListAssets", "GetAsset", "DeleteAsset",
# 自由创作素材库(组/素材全生命周期)
"GetAssetGroup", "UpdateAssetGroup", "UpdateAsset", "DeleteAssetGroup",
]
api_info = {a: ApiInfo("POST", "/", {"Action": a, "Version": API_VERSION}, {}, {}) for a in actions}
return Service(service_info, api_info)
@@ -117,3 +121,49 @@ def list_asset_groups(page: int = 1, page_size: int = 20, name: str | None = Non
{"Filter": filter_dict, "PageNumber": page, "PageSize": page_size, "ProjectName": _project()},
)
return result.get("Items", []), result.get("TotalCount", 0)
def list_assets(group_ids: list | None = None, status: str | None = None,
name: str | None = None, page: int = 1, page_size: int = 20) -> tuple:
"""列组内素材。返回 (items, total_count)。"""
filter_dict: dict = {"GroupType": "AIGC"}
if group_ids:
filter_dict["GroupIds"] = group_ids
if status:
filter_dict["Statuses"] = [status]
if name:
filter_dict["Name"] = name
result = _do_request(
"ListAssets",
{"Filter": filter_dict, "PageNumber": page, "PageSize": page_size, "ProjectName": _project()},
)
return result.get("Items", []), result.get("TotalCount", 0)
def get_asset_group(group_id: str) -> dict:
return _do_request("GetAssetGroup", {"Id": group_id, "ProjectName": _project()})
def update_asset_group(group_id: str, name: str | None = None, description: str | None = None) -> None:
body: dict = {"Id": group_id, "ProjectName": _project()}
if name is not None:
body["Name"] = name
if description is not None:
body["Description"] = description
_do_request("UpdateAssetGroup", body)
def update_asset(asset_id: str, name: str | None = None) -> None:
body: dict = {"Id": asset_id, "ProjectName": _project()}
if name is not None:
body["Name"] = name
_do_request("UpdateAsset", body)
def delete_asset(asset_id: str) -> None:
_do_request("DeleteAsset", {"Id": asset_id, "ProjectName": _project()})
def delete_asset_group(group_id: str) -> None:
"""删组(远程级联删组内素材)。"""
_do_request("DeleteAssetGroup", {"Id": group_id, "ProjectName": _project()})