完善视频复刻

This commit is contained in:
Azmat@qq.com
2026-08-27 15:56:44 +08:00
parent 3fbc2f2dab
commit d2b786a3cf
10 changed files with 722 additions and 164 deletions
+40 -54
View File
@@ -52,6 +52,11 @@ from apps.projects.models import (
logger = logging.getLogger(__name__)
# 火山官方直连(SeeDream 生图 / Seedance 视频 / 豆包文本)走 ARK SDK;其余 provider 一律
# 视为「OpenAI 兼容中转站」走通用适配器。加/换中转站 = DB 加一行 ModelProvider,零改代码。
# 注意:DB 里火山 provider 实际命名为 "volcengine"(豆包),必须包含,否则会被错路由到中转站。
OFFICIAL_DIRECT_PROVIDERS = {"volcengine", "volcano", "ark", "volcano_ark", "doubao"}
def get_default_model(capability: str) -> ModelConfig:
qs = (
@@ -62,21 +67,24 @@ def get_default_model(capability: str) -> ModelConfig:
return qs.filter(is_default=True).order_by("created_at").first() or qs.order_by("created_at").first()
def get_storyboard_image_model() -> ModelConfig:
"""故事板出图钉 YunQi gpt-image-2 多图 edits(与手工测通的 curl 同一条链路)。
找不到再回落默认图像模型,避免测试/未 seed 环境直接挂。"""
pinned = (
def get_storyboard_image_model() -> ModelConfig | None:
"""故事板出图只走 GPT 图像模型(gpt-image / gpt-image-2)。
不回落默认图像模型,也不走火山 Seedream:用户明确要求故事板无论怎样都不改成其他生图模型。
"""
qs = (
ModelConfig.objects.select_related("provider")
.filter(
capability=ModelConfig.Capability.IMAGE,
status=ModelConfig.Status.ACTIVE,
provider__status="active",
provider__name="yunqi",
name="gpt-image-2",
name__icontains="gpt-image",
)
.first()
)
return pinned or get_default_model(ModelConfig.Capability.IMAGE)
return (
qs.filter(name="gpt-image-2").order_by("created_at").first()
or qs.order_by("created_at").first()
)
def resolve_image_model(key: str | None) -> "ModelConfig | None":
@@ -101,12 +109,6 @@ def resolve_image_model(key: str | None) -> "ModelConfig | None":
return qs.filter(name=key).first()
# 火山官方直连(SeeDream 生图 / Seedance 视频 / 豆包文本)走 ARK SDK;其余 provider 一律
# 视为「OpenAI 兼容中转站」走通用适配器。加/换中转站 = DB 加一行 ModelProvider,零改代码。
# 注意:DB 里火山 provider 实际命名为 "volcengine"(豆包),必须包含,否则会被错路由到中转站。
OFFICIAL_DIRECT_PROVIDERS = {"volcengine", "volcano", "ark", "volcano_ark", "doubao"}
def public_model_name(model_config: ModelConfig) -> str:
"""普通用户公开名称保持稳定;Fallback 的真实模型只在管理员尝试链中展示。"""
@@ -2781,7 +2783,7 @@ def submit_storyboard(*, project, user, prompt: str = "", shot_ids: list | None
if adopted_script is None:
raise ValueError("script must be adopted before generating storyboard")
if get_storyboard_image_model() is None:
raise ValueError("no active image model configured")
raise ValueError("故事板只使用 GPT 图像模型,当前没有启用 gpt-image-2")
if prompt:
meta = dict(project.metadata or {})
if meta.get("storyboard_prompt") != prompt:
@@ -3071,7 +3073,6 @@ def _storyboard_shot_worker(task_id, shot_id, user_id) -> None:
user = User.objects.get(id=user_id)
project = shot.project
segment = shot.script_segment
model_config = task.model_config
reservation = task.credit_reservation
extra_prompt = (project.metadata or {}).get("storyboard_prompt", "") or ""
spec = project_output_spec(project)
@@ -3080,11 +3081,14 @@ def _storyboard_shot_worker(task_id, shot_id, user_id) -> None:
task.status = AITask.Status.SUBMITTED
task.save(update_fields=["status", "updated_at"])
try:
use_model_routing = bool((task.request_payload or {}).get("model_routing_v1"))
provider = None if use_model_routing else get_image_provider(model_config)
# 故事板无论任务上挂了什么模型、是否开了路由,都只走 GPT 图像;失败也不换 Seedream。
model_config = get_storyboard_image_model()
if model_config is None:
raise ValueError("故事板只使用 GPT 图像模型,当前没有启用 gpt-image-2")
provider = get_image_provider(model_config)
refs = _storyboard_reference_images(project, segment) if segment is not None else []
ref_urls = [r["url"] for r in refs]
if ref_urls and (use_model_routing or hasattr(provider, "image_edit")):
if ref_urls and hasattr(provider, "image_edit"):
# gpt-image-2 多图参考:必须用 refs 版提示词(点名「参考图N=角色/场景/商品」+锁脸锁商品)
frame_prompt = build_storyboard_frame_prompt_refs(project, segment, refs, extra_prompt)
else:
@@ -3093,40 +3097,24 @@ def _storyboard_shot_worker(task_id, shot_id, user_id) -> None:
else (task.request_payload.get("prompt") or "")
)
if use_model_routing:
routed = execute_routed_image_request(
task=task,
primary_model=model_config,
prompt=frame_prompt,
reference_images=ref_urls,
aspect_ratio=frame_ratio,
edit_size=frame_size,
direct_size=frame_size,
request_summary={
"storyboard_shot": str(shot.id),
"storyboard_sort_order": shot.sort_order,
},
if ref_urls and hasattr(provider, "image_edit"):
response = _call_image_with_retry(
lambda: provider.image_edit(
model=model_config.name,
prompt=frame_prompt,
images=ref_urls,
size=frame_size,
)
)
response, media = routed.value
else:
if ref_urls and hasattr(provider, "image_edit"):
response = _call_image_with_retry(
lambda: provider.image_edit(
model=model_config.name,
prompt=frame_prompt,
images=ref_urls,
size=frame_size,
)
response = _call_image_with_retry(
lambda: provider.image_generation(
model=model_config.name,
endpoint=model_config.endpoint,
prompt=frame_prompt,
)
else:
response = _call_image_with_retry(
lambda: provider.image_generation(
model=model_config.name,
endpoint=model_config.endpoint,
prompt=frame_prompt,
)
)
media = provider.extract_first_media_url(response)
)
media = provider.extract_first_media_url(response)
asset = _store_generated_media(
team=project.team, user=user, project=project, task=task, media=media,
name=f"{project.name}-storyboard-{shot.sort_order + 1}",
@@ -3206,6 +3194,8 @@ def poll_storyboard(*, project, user) -> dict:
if v
}
model_config = get_storyboard_image_model()
if model_config is None:
return {"status": "failed", "done": done, "total": total, "error": "故事板只使用 GPT 图像模型,当前没有启用 gpt-image-2"}
extra_prompt = (project.metadata or {}).get("storyboard_prompt", "") or ""
spawnable = [s for s in active if str(s.id) not in inflight_shot_ids]
slots = max(0, STORYBOARD_MAX_PARALLEL - len(inflight_shot_ids))
@@ -3217,12 +3207,8 @@ def poll_storyboard(*, project, user) -> dict:
"model": model_config.name, "endpoint": model_config.endpoint,
"prompt": build_storyboard_frame_prompt(project, segment, extra_prompt) if segment is not None else "",
"storyboard_shot": str(shot.id),
"model_routing_v1": True,
},
)
# 真实平台成本由每条 AIModelAttempt 按实际模型累加,避免 Fallback 后仍记默认模型旧成本。
task.base_cost = Decimal("0")
task.save(update_fields=["base_cost", "updated_at"])
StoryboardShot.objects.filter(id=shot.id).update(status=StoryboardShot.Status.RUNNING, updated_at=timezone.now())
threading.Thread(
target=_storyboard_shot_worker, args=(str(task.id), str(shot.id), str(user.id)), daemon=True