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:
@@ -192,12 +192,19 @@ class VolcanoArkProvider:
|
||||
resolution: str = "720p",
|
||||
reference_images: list[str] | None = None,
|
||||
generate_audio: bool = True,
|
||||
content_items: list[dict[str, Any]] | None = None,
|
||||
seed: int | None = None,
|
||||
search_mode: str = "off",
|
||||
) -> dict[str, Any]:
|
||||
if not self.api_key:
|
||||
raise ValueError("VOLCANO_ARK_API_KEY is not configured")
|
||||
content: list[dict[str, Any]] = [{"type": "text", "text": prompt}]
|
||||
for image_url in reference_images or []:
|
||||
content.append({"type": "image_url", "image_url": {"url": image_url}, "role": "reference_image"})
|
||||
if content_items is not None:
|
||||
# 自由创作:调用方整段接管参考素材(混合 image/video/audio + role first_frame/last_frame 等)
|
||||
content.extend(content_items)
|
||||
else:
|
||||
for image_url in reference_images or []:
|
||||
content.append({"type": "image_url", "image_url": {"url": image_url}, "role": "reference_image"})
|
||||
body = {
|
||||
"model": model,
|
||||
"content": content,
|
||||
@@ -208,6 +215,10 @@ class VolcanoArkProvider:
|
||||
# Seedance 直接出音效 + 人物声音(参考生视频);关掉则是哑片。默认开。
|
||||
"generate_audio": generate_audio,
|
||||
}
|
||||
if seed is not None and seed != -1:
|
||||
body["seed"] = seed
|
||||
if search_mode == "smart":
|
||||
body["tools"] = [{"type": "web_search"}]
|
||||
response = requests.post(
|
||||
f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}",
|
||||
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
|
||||
@@ -236,9 +247,24 @@ class VolcanoArkProvider:
|
||||
return item["url"]
|
||||
if item.get("b64_json"):
|
||||
return item["b64_json"]
|
||||
content = data.get("content") or {}
|
||||
if content.get("video_url"):
|
||||
return content["video_url"]
|
||||
# 视频任务响应的 content 有两种形态:dict {video_url: "..."} 或
|
||||
# list [{type:"video_url", video_url:{url:"..."}}](Seedance 2.0 多模态响应)。
|
||||
content = data.get("content")
|
||||
if isinstance(content, dict):
|
||||
video_url = content.get("video_url")
|
||||
if isinstance(video_url, str) and video_url:
|
||||
return video_url
|
||||
if isinstance(video_url, dict) and video_url.get("url"):
|
||||
return video_url["url"]
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
video_url = item.get("video_url")
|
||||
if isinstance(video_url, str) and video_url:
|
||||
return video_url
|
||||
if isinstance(video_url, dict) and video_url.get("url"):
|
||||
return video_url["url"]
|
||||
raise ValueError("Volcano response does not contain media url")
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user