爆款/拆镜生成简化3个步骤 | 项目生成可携带附件控制
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""爆款复刻/拆镜复刻 V2 三步骤公共流程。"""
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.enums.hot_opening_replicate import (
|
||||
HotOpeningGenerationModeEnum,
|
||||
HotOpeningStepIOSchemaVersionEnum,
|
||||
ModuleCodeEnum as HotModuleCodeEnum,
|
||||
)
|
||||
from app.enums.shot_replicate import (
|
||||
ModuleCodeEnum as ShotModuleCodeEnum,
|
||||
ShotReplicateGenerationModeEnum,
|
||||
ShotReplicateStepIOSchemaVersionEnum,
|
||||
)
|
||||
from app.enums.module_generation_flow import ModuleGenerationFlowConfig
|
||||
|
||||
MATERIAL_INPUT = "material_input"
|
||||
VIDEO_PROMPT_OPTIMIZE = "video_prompt_optimize"
|
||||
VIDEO_GENERATE = "video_generate"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ModuleGenerationV2Config:
|
||||
module: str
|
||||
generation_mode: str
|
||||
io_schema_version: str
|
||||
display_name: str
|
||||
project_not_found_message: str
|
||||
material_video_locked: bool
|
||||
|
||||
@property
|
||||
def flow_config(self) -> ModuleGenerationFlowConfig:
|
||||
return ModuleGenerationFlowConfig(
|
||||
module=self.module,
|
||||
step_index_map={
|
||||
MATERIAL_INPUT: 1,
|
||||
VIDEO_PROMPT_OPTIMIZE: 2,
|
||||
VIDEO_GENERATE: 3,
|
||||
},
|
||||
material_step_code=MATERIAL_INPUT,
|
||||
image_prompt_step_code="__v2_no_image_prompt__",
|
||||
image_generate_step_code="__v2_no_image_generate__",
|
||||
video_prompt_step_code=VIDEO_PROMPT_OPTIMIZE,
|
||||
video_generate_step_code=VIDEO_GENERATE,
|
||||
project_not_found_message=self.project_not_found_message,
|
||||
step_not_found_message="V2 子任务不存在",
|
||||
cancel_chat_task_error_message=f"{self.display_name}步骤被重建或删除,旧生成任务已取消",
|
||||
material_video_url_editable=not self.material_video_locked,
|
||||
step_io_schema_version=self.io_schema_version,
|
||||
expected_flow_version="v2",
|
||||
)
|
||||
|
||||
|
||||
HOT_OPENING_V2 = ModuleGenerationV2Config(
|
||||
module=HotModuleCodeEnum.HOT_OPENING_REPLICATE.value,
|
||||
generation_mode=HotOpeningGenerationModeEnum.HOT_OPENING_REPLICATE.value,
|
||||
io_schema_version=HotOpeningStepIOSchemaVersionEnum.V2.value,
|
||||
display_name="爆款开头复刻",
|
||||
project_not_found_message="爆款开头复刻 V2 项目不存在",
|
||||
material_video_locked=True,
|
||||
)
|
||||
|
||||
SHOT_REPLICATE_V2 = ModuleGenerationV2Config(
|
||||
module=ShotModuleCodeEnum.SHOT_REPLICATE.value,
|
||||
generation_mode=ShotReplicateGenerationModeEnum.SHOT_REPLICATE.value,
|
||||
io_schema_version=ShotReplicateStepIOSchemaVersionEnum.V2.value,
|
||||
display_name="拆镜复刻",
|
||||
project_not_found_message="拆镜复刻 V2 项目不存在",
|
||||
material_video_locked=True,
|
||||
)
|
||||
|
||||
CONFIG_BY_MODULE = {
|
||||
HOT_OPENING_V2.module: HOT_OPENING_V2,
|
||||
SHOT_REPLICATE_V2.module: SHOT_REPLICATE_V2,
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.enums.celery_queue import CeleryQueue
|
||||
from app.enums.common import ModuleEventTypeEnum
|
||||
from app.services.module_async_recovery_service import (
|
||||
TASK_MODULE_V2_VIDEO_PROMPT,
|
||||
register_module_step_task,
|
||||
)
|
||||
from app.services.module_generation_log_service import log_module_error, log_module_event_file
|
||||
from app.services.module_generation_v2.config import VIDEO_PROMPT_OPTIMIZE, ModuleGenerationV2Config
|
||||
from app.tasks.celery_app import celery_app
|
||||
from app.tasks.module_generation_v2_tasks import start_video_prompt_optimize_v2
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VideoPromptDispatchResult:
|
||||
registry_success: bool
|
||||
celery_success: bool
|
||||
registry_error: str | None = None
|
||||
celery_error: str | None = None
|
||||
|
||||
@property
|
||||
def recoverable(self) -> bool:
|
||||
return self.registry_success or self.celery_success
|
||||
|
||||
|
||||
def ensure_v2_celery_enabled() -> None:
|
||||
if celery_app is None:
|
||||
raise HTTPException(status_code=503, detail="Celery未启用")
|
||||
|
||||
|
||||
async def dispatch_video_prompt_v2(
|
||||
*,
|
||||
config: ModuleGenerationV2Config,
|
||||
project_id: str,
|
||||
step_id: str,
|
||||
) -> VideoPromptDispatchResult:
|
||||
"""注册并投递 V2 视频提词任务。
|
||||
|
||||
Redis 注册成功但 Celery 直投失败时,由周期恢复任务补投;Celery 成功但
|
||||
Redis 注册失败时任务仍可正常执行。只有两个通道都失败时由 API 补偿落库为失败。
|
||||
"""
|
||||
registry_error: Exception | None = None
|
||||
try:
|
||||
await register_module_step_task(
|
||||
module=config.module,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
step_code=VIDEO_PROMPT_OPTIMIZE,
|
||||
task_name=TASK_MODULE_V2_VIDEO_PROMPT,
|
||||
)
|
||||
except Exception as exc:
|
||||
registry_error = exc
|
||||
log_module_error(
|
||||
module=config.module,
|
||||
event_type=ModuleEventTypeEnum.V2_VIDEO_PROMPT_REGISTRY_FAILED.value,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
message="V2 视频提词 Redis 活跃注册失败,将继续尝试 Celery 直投",
|
||||
exc=exc,
|
||||
)
|
||||
|
||||
celery_error: Exception | None = None
|
||||
try:
|
||||
start_video_prompt_optimize_v2.apply_async(
|
||||
args=[project_id, step_id],
|
||||
queue=CeleryQueue.GEN_CHATAPI_CREATE.value,
|
||||
countdown=0,
|
||||
task_id=f"module-v2-video-prompt:{step_id}",
|
||||
)
|
||||
except Exception as exc:
|
||||
celery_error = exc
|
||||
log_module_error(
|
||||
module=config.module,
|
||||
event_type=ModuleEventTypeEnum.V2_VIDEO_PROMPT_DISPATCH_FAILED.value,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
message="V2 视频提词 Celery 投递失败",
|
||||
detail={"redis_registry_available": registry_error is None},
|
||||
exc=exc,
|
||||
)
|
||||
|
||||
result = VideoPromptDispatchResult(
|
||||
registry_success=registry_error is None,
|
||||
celery_success=celery_error is None,
|
||||
registry_error=str(registry_error) if registry_error else None,
|
||||
celery_error=str(celery_error) if celery_error else None,
|
||||
)
|
||||
if result.celery_success:
|
||||
log_module_event_file(
|
||||
module=config.module,
|
||||
event_type=ModuleEventTypeEnum.V2_VIDEO_PROMPT_DISPATCHED.value,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
message="V2 视频提词任务已投递",
|
||||
detail={
|
||||
"queue": CeleryQueue.GEN_CHATAPI_CREATE.value,
|
||||
"redis_registry_available": result.registry_success,
|
||||
},
|
||||
)
|
||||
return result
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.services.module_generation_step_common_service import build_file_url_or_data_uri
|
||||
|
||||
|
||||
def build_v2_video_generation_references(material_payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""V2 最终视频只能携带可选素材图片,绝不携带素材视频/音频。"""
|
||||
image_url = str(material_payload.get("material_image_url") or "").strip()
|
||||
if not image_url:
|
||||
return []
|
||||
return [
|
||||
{
|
||||
"type": "image",
|
||||
"url": build_file_url_or_data_uri(image_url),
|
||||
"name": "素材参考图片",
|
||||
"upload_resource_id": material_payload.get("material_image_resource_id"),
|
||||
"role": "reference_image",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def assert_v2_video_generation_references(references: list[dict[str, Any]]) -> None:
|
||||
image_count = 0
|
||||
for item in references:
|
||||
media_type = str(item.get("type") or item.get("media_type") or "").lower()
|
||||
if media_type == "image":
|
||||
image_count += 1
|
||||
continue
|
||||
raise HTTPException(status_code=400, detail="V2 视频生成只允许携带一张素材图片,禁止视频或音频附件")
|
||||
if image_count > 1:
|
||||
raise HTTPException(status_code=400, detail="V2 视频生成最多携带一张素材图片")
|
||||
Reference in New Issue
Block a user