完善视频复刻
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user