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:
+54
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 5.1.15 on 2026-07-02 07:23
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0007_team_monthly_credit_limit"),
|
||||
("ai", "0021_rename_ai_aitask_team_read_idx_ai_aitask_team_id_d668d5_idx"),
|
||||
("projects", "0006_migrate_storyboard_to_shots"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="aitask",
|
||||
name="is_deleted",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="aitask",
|
||||
name="is_favorited",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="aitask",
|
||||
name="task_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("script_generation", "Script Generation"),
|
||||
("script_optimization", "Script Optimization"),
|
||||
("entity_extraction", "Entity Extraction"),
|
||||
("product_image", "Product Image"),
|
||||
("person_image", "Person Image"),
|
||||
("scene_image", "Scene Image"),
|
||||
("storyboard", "Storyboard"),
|
||||
("video_segment", "Video Segment"),
|
||||
("voiceover", "Voiceover"),
|
||||
("export", "Export"),
|
||||
("free_video", "Free Video"),
|
||||
],
|
||||
max_length=48,
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="aitask",
|
||||
index=models.Index(
|
||||
fields=["team", "task_type", "-created_at"],
|
||||
name="ai_aitask_team_id_6a4627_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Seed 自由创作视频三档模型(Seedance 2.0 标准/Fast/Mini)+ token 定价表。
|
||||
|
||||
- 定价存 ModelConfig.metadata["pricing"](元/百万tokens,按 分辨率档 × 是否含视频参考 取价),
|
||||
与 apps/ai/catalog.py 保持一致;数字来源火山 Seedance 2.0 官方价目。
|
||||
- 标准档已存在(catalog bootstrap 种的),只 merge pricing/resolutions 进 metadata,不覆盖其它键;
|
||||
fast/mini 新建,is_default=False 且 created_at 晚于现有视频模型 →
|
||||
get_default_model(VIDEO) 仍取原默认,pipeline 视频链路零回归。
|
||||
- 幂等:可重复 apply。
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
STANDARD_PRICING = {
|
||||
"unit": "cny_per_million_tokens",
|
||||
"default": {"no_ref_video": 46, "with_ref_video": 28},
|
||||
"1080p": {"no_ref_video": 51, "with_ref_video": 31},
|
||||
"4k": {"no_ref_video": 26, "with_ref_video": 16},
|
||||
}
|
||||
|
||||
NEW_MODELS = [
|
||||
# name, display_name, pricing
|
||||
(
|
||||
"doubao-seedance-2-0-fast-260128",
|
||||
"Seedance-2.0-Fast",
|
||||
{"unit": "cny_per_million_tokens", "default": {"no_ref_video": 37, "with_ref_video": 22}},
|
||||
),
|
||||
(
|
||||
"doubao-seedance-2-0-mini-260615",
|
||||
"Seedance-2.0-Mini",
|
||||
{"unit": "cny_per_million_tokens", "default": {"no_ref_video": 23, "with_ref_video": 14}},
|
||||
),
|
||||
]
|
||||
|
||||
COMMON_METADATA = {
|
||||
"audio": "optional",
|
||||
"modes": ["text", "startFrameOptional", "imageReference:9", "videoReference:3", "audioReference:3"],
|
||||
"durations": list(range(4, 16)),
|
||||
"resolutions": ["480p", "720p"],
|
||||
"watermark": False,
|
||||
"source": "jimeng-clone/backend/utils/airdrama_client.py",
|
||||
}
|
||||
|
||||
|
||||
def seed(apps, schema_editor):
|
||||
ModelProvider = apps.get_model("ai", "ModelProvider")
|
||||
ModelConfig = apps.get_model("ai", "ModelConfig")
|
||||
|
||||
provider, _ = ModelProvider.objects.get_or_create(
|
||||
name="volcengine",
|
||||
defaults={
|
||||
"display_name": "火山引擎(豆包)",
|
||||
"status": "active",
|
||||
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
|
||||
},
|
||||
)
|
||||
|
||||
# 标准档:merge pricing + 扩分辨率,不动 status/is_default/unit_price/其它 metadata
|
||||
standard = ModelConfig.objects.filter(
|
||||
provider=provider, name="doubao-seedance-2-0-260128", capability="video"
|
||||
).first()
|
||||
if standard:
|
||||
meta = dict(standard.metadata or {})
|
||||
meta["pricing"] = STANDARD_PRICING
|
||||
meta["resolutions"] = ["480p", "720p", "1080p", "4k"]
|
||||
standard.metadata = meta
|
||||
standard.save(update_fields=["metadata"])
|
||||
else:
|
||||
meta = dict(COMMON_METADATA)
|
||||
meta["pricing"] = STANDARD_PRICING
|
||||
meta["resolutions"] = ["480p", "720p", "1080p", "4k"]
|
||||
ModelConfig.objects.create(
|
||||
provider=provider,
|
||||
name="doubao-seedance-2-0-260128",
|
||||
capability="video",
|
||||
display_name="Seedance-2.0",
|
||||
endpoint="contents/generations/tasks",
|
||||
status="active",
|
||||
metadata=meta,
|
||||
)
|
||||
|
||||
for name, display, pricing in NEW_MODELS:
|
||||
meta = dict(COMMON_METADATA)
|
||||
meta["pricing"] = pricing
|
||||
obj, created = ModelConfig.objects.get_or_create(
|
||||
provider=provider,
|
||||
name=name,
|
||||
capability="video",
|
||||
defaults={
|
||||
"display_name": display,
|
||||
"endpoint": "contents/generations/tasks",
|
||||
"status": "active",
|
||||
"metadata": meta,
|
||||
},
|
||||
)
|
||||
if not created:
|
||||
existing = dict(obj.metadata or {})
|
||||
existing.setdefault("resolutions", meta["resolutions"])
|
||||
existing.setdefault("durations", meta["durations"])
|
||||
existing["pricing"] = pricing
|
||||
obj.metadata = existing
|
||||
if obj.status != "active":
|
||||
obj.status = "active"
|
||||
obj.save(update_fields=["metadata", "status"])
|
||||
else:
|
||||
obj.save(update_fields=["metadata"])
|
||||
|
||||
|
||||
def unseed(apps, schema_editor):
|
||||
# 保守反向:只停用本迁移新建的两档,不删数据、不动标准档 metadata
|
||||
ModelConfig = apps.get_model("ai", "ModelConfig")
|
||||
ModelConfig.objects.filter(
|
||||
provider__name="volcengine",
|
||||
capability="video",
|
||||
name__in=[n for n, _, _ in NEW_MODELS],
|
||||
).update(status="disabled")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("ai", "0022_aitask_is_deleted_aitask_is_favorited_and_more")]
|
||||
operations = [migrations.RunPython(seed, unseed)]
|
||||
Reference in New Issue
Block a user