feat: add resource library and real image workflow
This commit is contained in:
@@ -1,20 +1,82 @@
|
||||
"""Export mock activity."""
|
||||
"""Export activity."""
|
||||
|
||||
from app.domain.enums import AssetType, OrderStatus, StepStatus
|
||||
from app.infra.db.models.asset import AssetORM
|
||||
from app.infra.db.session import get_session_factory
|
||||
from temporalio import activity
|
||||
|
||||
from app.domain.enums import AssetType
|
||||
from app.workers.activities.tryon_activities import execute_asset_step
|
||||
from app.workers.activities.tryon_activities import create_step_record, jsonable, load_order_and_run, utc_now
|
||||
from app.workers.workflows.types import MockActivityResult, StepActivityInput
|
||||
|
||||
|
||||
@activity.defn
|
||||
async def run_export_activity(payload: StepActivityInput) -> MockActivityResult:
|
||||
"""Mock final asset export."""
|
||||
"""Finalize the chosen source asset as the order's exported deliverable."""
|
||||
|
||||
return await execute_asset_step(
|
||||
payload,
|
||||
AssetType.FINAL,
|
||||
filename="final.png",
|
||||
finalize=True,
|
||||
)
|
||||
if payload.source_asset_id is None:
|
||||
raise ValueError("run_export_activity requires source_asset_id")
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
order, workflow_run = await load_order_and_run(session, payload.order_id, payload.workflow_run_id)
|
||||
step = create_step_record(payload)
|
||||
session.add(step)
|
||||
|
||||
order.status = OrderStatus.RUNNING
|
||||
workflow_run.status = OrderStatus.RUNNING
|
||||
workflow_run.current_step = payload.step_name
|
||||
await session.flush()
|
||||
|
||||
try:
|
||||
source_asset = await session.get(AssetORM, payload.source_asset_id)
|
||||
if source_asset is None:
|
||||
raise ValueError(f"Source asset {payload.source_asset_id} not found")
|
||||
if source_asset.order_id != payload.order_id:
|
||||
raise ValueError(
|
||||
f"Source asset {payload.source_asset_id} does not belong to order {payload.order_id}"
|
||||
)
|
||||
|
||||
metadata = {
|
||||
**payload.metadata,
|
||||
"source_asset_id": payload.source_asset_id,
|
||||
"selected_asset_id": payload.selected_asset_id,
|
||||
}
|
||||
metadata = {key: value for key, value in metadata.items() if value is not None}
|
||||
|
||||
asset = AssetORM(
|
||||
order_id=payload.order_id,
|
||||
asset_type=AssetType.FINAL,
|
||||
step_name=payload.step_name,
|
||||
uri=source_asset.uri,
|
||||
metadata_json=jsonable(metadata),
|
||||
)
|
||||
session.add(asset)
|
||||
await session.flush()
|
||||
|
||||
result = MockActivityResult(
|
||||
step_name=payload.step_name,
|
||||
success=True,
|
||||
asset_id=asset.id,
|
||||
uri=asset.uri,
|
||||
score=0.95,
|
||||
passed=True,
|
||||
message="mock success",
|
||||
metadata=jsonable(metadata) or {},
|
||||
)
|
||||
|
||||
order.final_asset_id = asset.id
|
||||
order.status = OrderStatus.SUCCEEDED
|
||||
workflow_run.status = OrderStatus.SUCCEEDED
|
||||
|
||||
step.step_status = StepStatus.SUCCEEDED
|
||||
step.output_json = jsonable(result)
|
||||
step.ended_at = utc_now()
|
||||
await session.commit()
|
||||
return result
|
||||
except Exception as exc:
|
||||
step.step_status = StepStatus.FAILED
|
||||
step.error_message = str(exc)
|
||||
step.ended_at = utc_now()
|
||||
order.status = OrderStatus.FAILED
|
||||
workflow_run.status = OrderStatus.FAILED
|
||||
await session.commit()
|
||||
raise
|
||||
|
||||
@@ -29,11 +29,22 @@ async def run_qc_activity(payload: StepActivityInput) -> MockActivityResult:
|
||||
candidate_uri: str | None = None
|
||||
|
||||
if passed:
|
||||
if payload.source_asset_id is None:
|
||||
raise ValueError("run_qc_activity requires source_asset_id")
|
||||
|
||||
source_asset = await session.get(AssetORM, payload.source_asset_id)
|
||||
if source_asset is None:
|
||||
raise ValueError(f"Source asset {payload.source_asset_id} not found")
|
||||
if source_asset.order_id != payload.order_id:
|
||||
raise ValueError(
|
||||
f"Source asset {payload.source_asset_id} does not belong to order {payload.order_id}"
|
||||
)
|
||||
|
||||
candidate = AssetORM(
|
||||
order_id=payload.order_id,
|
||||
asset_type=AssetType.QC_CANDIDATE,
|
||||
step_name=payload.step_name,
|
||||
uri=mock_uri(payload.order_id, payload.step_name.value, "candidate.png"),
|
||||
uri=source_asset.uri,
|
||||
metadata_json=jsonable({"source_asset_id": payload.source_asset_id}),
|
||||
)
|
||||
session.add(candidate)
|
||||
|
||||
@@ -1,19 +1,122 @@
|
||||
"""Scene mock activity."""
|
||||
"""Scene activities."""
|
||||
|
||||
from temporalio import activity
|
||||
|
||||
from app.domain.enums import AssetType
|
||||
from app.workers.activities.tryon_activities import execute_asset_step
|
||||
from app.domain.enums import AssetType, LibraryResourceType, OrderStatus, StepStatus
|
||||
from app.infra.db.models.asset import AssetORM
|
||||
from app.infra.db.session import get_session_factory
|
||||
from app.workers.activities.tryon_activities import (
|
||||
create_step_record,
|
||||
execute_asset_step,
|
||||
get_image_generation_service,
|
||||
get_order_artifact_storage_service,
|
||||
jsonable,
|
||||
load_active_library_resource,
|
||||
load_order_and_run,
|
||||
utc_now,
|
||||
)
|
||||
from app.workers.workflows.types import MockActivityResult, StepActivityInput
|
||||
|
||||
|
||||
@activity.defn
|
||||
async def run_scene_activity(payload: StepActivityInput) -> MockActivityResult:
|
||||
"""Mock scene replacement."""
|
||||
"""Generate a scene-composited asset, or fall back to mock mode when configured."""
|
||||
|
||||
return await execute_asset_step(
|
||||
payload,
|
||||
AssetType.SCENE,
|
||||
extra_metadata={"scene_ref_asset_id": payload.scene_ref_asset_id},
|
||||
)
|
||||
service = get_image_generation_service()
|
||||
if service.__class__.__name__ == "MockImageGenerationService":
|
||||
return await execute_asset_step(
|
||||
payload,
|
||||
AssetType.SCENE,
|
||||
extra_metadata={"scene_ref_asset_id": payload.scene_ref_asset_id},
|
||||
)
|
||||
|
||||
if payload.source_asset_id is None:
|
||||
raise ValueError("run_scene_activity requires source_asset_id")
|
||||
if payload.scene_ref_asset_id is None:
|
||||
raise ValueError("run_scene_activity requires scene_ref_asset_id")
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
order, workflow_run = await load_order_and_run(session, payload.order_id, payload.workflow_run_id)
|
||||
step = create_step_record(payload)
|
||||
session.add(step)
|
||||
|
||||
order.status = OrderStatus.RUNNING
|
||||
workflow_run.status = OrderStatus.RUNNING
|
||||
workflow_run.current_step = payload.step_name
|
||||
await session.flush()
|
||||
|
||||
try:
|
||||
source_asset = await session.get(AssetORM, payload.source_asset_id)
|
||||
if source_asset is None:
|
||||
raise ValueError(f"Source asset {payload.source_asset_id} not found")
|
||||
if source_asset.order_id != payload.order_id:
|
||||
raise ValueError(
|
||||
f"Source asset {payload.source_asset_id} does not belong to order {payload.order_id}"
|
||||
)
|
||||
|
||||
scene_resource, scene_original = await load_active_library_resource(
|
||||
session,
|
||||
payload.scene_ref_asset_id,
|
||||
resource_type=LibraryResourceType.SCENE,
|
||||
)
|
||||
|
||||
generated = await service.generate_scene_image(
|
||||
source_image_url=source_asset.uri,
|
||||
scene_image_url=scene_original.public_url,
|
||||
)
|
||||
storage_key, public_url = await get_order_artifact_storage_service().upload_generated_image(
|
||||
order_id=payload.order_id,
|
||||
step_name=payload.step_name,
|
||||
image_bytes=generated.image_bytes,
|
||||
mime_type=generated.mime_type,
|
||||
)
|
||||
|
||||
metadata = {
|
||||
**payload.metadata,
|
||||
"source_asset_id": source_asset.id,
|
||||
"scene_ref_asset_id": payload.scene_ref_asset_id,
|
||||
"scene_resource_id": scene_resource.id,
|
||||
"scene_original_file_id": scene_original.id,
|
||||
"scene_original_url": scene_original.public_url,
|
||||
"provider": generated.provider,
|
||||
"model": generated.model,
|
||||
"storage_key": storage_key,
|
||||
"mime_type": generated.mime_type,
|
||||
"prompt": generated.prompt,
|
||||
}
|
||||
metadata = {key: value for key, value in metadata.items() if value is not None}
|
||||
|
||||
asset = AssetORM(
|
||||
order_id=payload.order_id,
|
||||
asset_type=AssetType.SCENE,
|
||||
step_name=payload.step_name,
|
||||
uri=public_url,
|
||||
metadata_json=jsonable(metadata),
|
||||
)
|
||||
session.add(asset)
|
||||
await session.flush()
|
||||
|
||||
result = MockActivityResult(
|
||||
step_name=payload.step_name,
|
||||
success=True,
|
||||
asset_id=asset.id,
|
||||
uri=asset.uri,
|
||||
score=1.0,
|
||||
passed=True,
|
||||
message="scene generated",
|
||||
metadata=jsonable(metadata) or {},
|
||||
)
|
||||
|
||||
step.step_status = StepStatus.SUCCEEDED
|
||||
step.output_json = jsonable(result)
|
||||
step.ended_at = utc_now()
|
||||
await session.commit()
|
||||
return result
|
||||
except Exception as exc:
|
||||
step.step_status = StepStatus.FAILED
|
||||
step.error_message = str(exc)
|
||||
step.ended_at = utc_now()
|
||||
order.status = OrderStatus.FAILED
|
||||
workflow_run.status = OrderStatus.FAILED
|
||||
await session.commit()
|
||||
raise
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Prepare-model and try-on mock activities plus shared helpers."""
|
||||
"""Prepare-model and try-on activities plus shared helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -8,14 +8,20 @@ from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from temporalio import activity
|
||||
|
||||
from app.domain.enums import AssetType, OrderStatus, StepStatus
|
||||
from app.application.services.image_generation_service import build_image_generation_service
|
||||
from app.domain.enums import AssetType, LibraryResourceStatus, LibraryResourceType, OrderStatus, StepStatus
|
||||
from app.infra.db.models.asset import AssetORM
|
||||
from app.infra.db.models.library_resource import LibraryResourceORM
|
||||
from app.infra.db.models.library_resource_file import LibraryResourceFileORM
|
||||
from app.infra.db.models.order import OrderORM
|
||||
from app.infra.db.models.workflow_run import WorkflowRunORM
|
||||
from app.infra.db.models.workflow_step import WorkflowStepORM
|
||||
from app.infra.db.session import get_session_factory
|
||||
from app.infra.storage.s3 import S3ObjectStorageService
|
||||
from app.workers.workflows.types import MockActivityResult, StepActivityInput
|
||||
|
||||
|
||||
@@ -71,6 +77,64 @@ def create_step_record(payload: StepActivityInput) -> WorkflowStepORM:
|
||||
)
|
||||
|
||||
|
||||
async def load_active_library_resource(
|
||||
session,
|
||||
resource_id: int,
|
||||
*,
|
||||
resource_type: LibraryResourceType,
|
||||
) -> tuple[LibraryResourceORM, LibraryResourceFileORM]:
|
||||
"""Load an active library resource and its original file from the library."""
|
||||
|
||||
result = await session.execute(
|
||||
select(LibraryResourceORM)
|
||||
.options(selectinload(LibraryResourceORM.files))
|
||||
.where(LibraryResourceORM.id == resource_id)
|
||||
)
|
||||
resource = result.scalar_one_or_none()
|
||||
if resource is None:
|
||||
raise ValueError(f"Library resource {resource_id} not found")
|
||||
if resource.resource_type != resource_type:
|
||||
raise ValueError(f"Resource {resource_id} is not a {resource_type.value} resource")
|
||||
if resource.status != LibraryResourceStatus.ACTIVE:
|
||||
raise ValueError(f"Library resource {resource_id} is not active")
|
||||
if resource.original_file_id is None:
|
||||
raise ValueError(f"Library resource {resource_id} is missing an original file")
|
||||
|
||||
original_file = next((item for item in resource.files if item.id == resource.original_file_id), None)
|
||||
if original_file is None:
|
||||
raise ValueError(f"Library resource {resource_id} original file record not found")
|
||||
return resource, original_file
|
||||
|
||||
|
||||
def get_image_generation_service():
|
||||
"""Return the configured image-generation service."""
|
||||
|
||||
return build_image_generation_service()
|
||||
|
||||
|
||||
def get_order_artifact_storage_service() -> S3ObjectStorageService:
|
||||
"""Return the object-storage service for workflow-generated images."""
|
||||
|
||||
return S3ObjectStorageService()
|
||||
|
||||
|
||||
def build_resource_input_snapshot(
|
||||
resource: LibraryResourceORM,
|
||||
original_file: LibraryResourceFileORM,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a frontend-friendly snapshot of one library input resource."""
|
||||
|
||||
return {
|
||||
"resource_id": resource.id,
|
||||
"resource_name": resource.name,
|
||||
"original_file_id": original_file.id,
|
||||
"original_url": original_file.public_url,
|
||||
"mime_type": original_file.mime_type,
|
||||
"width": original_file.width,
|
||||
"height": original_file.height,
|
||||
}
|
||||
|
||||
|
||||
async def execute_asset_step(
|
||||
payload: StepActivityInput,
|
||||
asset_type: AssetType,
|
||||
@@ -145,26 +209,213 @@ async def execute_asset_step(
|
||||
|
||||
@activity.defn
|
||||
async def prepare_model_activity(payload: StepActivityInput) -> MockActivityResult:
|
||||
"""Mock model preparation for the pipeline."""
|
||||
"""Resolve a model resource into an order-scoped prepared-model asset."""
|
||||
|
||||
return await execute_asset_step(
|
||||
payload,
|
||||
AssetType.PREPARED_MODEL,
|
||||
extra_metadata={
|
||||
"model_id": payload.model_id,
|
||||
"pose_id": payload.pose_id,
|
||||
"garment_asset_id": payload.garment_asset_id,
|
||||
"scene_ref_asset_id": payload.scene_ref_asset_id,
|
||||
},
|
||||
)
|
||||
if payload.model_id is None:
|
||||
raise ValueError("prepare_model_activity requires model_id")
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
order, workflow_run = await load_order_and_run(session, payload.order_id, payload.workflow_run_id)
|
||||
step = create_step_record(payload)
|
||||
session.add(step)
|
||||
|
||||
order.status = OrderStatus.RUNNING
|
||||
workflow_run.status = OrderStatus.RUNNING
|
||||
workflow_run.current_step = payload.step_name
|
||||
await session.flush()
|
||||
|
||||
try:
|
||||
resource, original_file = await load_active_library_resource(
|
||||
session,
|
||||
payload.model_id,
|
||||
resource_type=LibraryResourceType.MODEL,
|
||||
)
|
||||
garment_snapshot = None
|
||||
if payload.garment_asset_id is not None:
|
||||
garment_resource, garment_original = await load_active_library_resource(
|
||||
session,
|
||||
payload.garment_asset_id,
|
||||
resource_type=LibraryResourceType.GARMENT,
|
||||
)
|
||||
garment_snapshot = build_resource_input_snapshot(garment_resource, garment_original)
|
||||
|
||||
scene_snapshot = None
|
||||
if payload.scene_ref_asset_id is not None:
|
||||
scene_resource, scene_original = await load_active_library_resource(
|
||||
session,
|
||||
payload.scene_ref_asset_id,
|
||||
resource_type=LibraryResourceType.SCENE,
|
||||
)
|
||||
scene_snapshot = build_resource_input_snapshot(scene_resource, scene_original)
|
||||
|
||||
metadata = {
|
||||
**payload.metadata,
|
||||
"model_id": payload.model_id,
|
||||
"pose_id": payload.pose_id,
|
||||
"garment_asset_id": payload.garment_asset_id,
|
||||
"scene_ref_asset_id": payload.scene_ref_asset_id,
|
||||
"library_resource_id": resource.id,
|
||||
"library_original_file_id": original_file.id,
|
||||
"library_original_url": original_file.public_url,
|
||||
"library_original_mime_type": original_file.mime_type,
|
||||
"library_original_width": original_file.width,
|
||||
"library_original_height": original_file.height,
|
||||
"model_input": build_resource_input_snapshot(resource, original_file),
|
||||
"garment_input": garment_snapshot,
|
||||
"scene_input": scene_snapshot,
|
||||
"pose_input": {"pose_id": payload.pose_id} if payload.pose_id is not None else None,
|
||||
"normalized": False,
|
||||
}
|
||||
metadata = {key: value for key, value in metadata.items() if value is not None}
|
||||
|
||||
asset = AssetORM(
|
||||
order_id=payload.order_id,
|
||||
asset_type=AssetType.PREPARED_MODEL,
|
||||
step_name=payload.step_name,
|
||||
uri=original_file.public_url,
|
||||
metadata_json=jsonable(metadata),
|
||||
)
|
||||
session.add(asset)
|
||||
await session.flush()
|
||||
|
||||
result = MockActivityResult(
|
||||
step_name=payload.step_name,
|
||||
success=True,
|
||||
asset_id=asset.id,
|
||||
uri=asset.uri,
|
||||
score=1.0,
|
||||
passed=True,
|
||||
message="prepared model ready",
|
||||
metadata=jsonable(metadata) or {},
|
||||
)
|
||||
|
||||
step.step_status = StepStatus.SUCCEEDED
|
||||
step.output_json = jsonable(result)
|
||||
step.ended_at = utc_now()
|
||||
await session.commit()
|
||||
return result
|
||||
except Exception as exc:
|
||||
step.step_status = StepStatus.FAILED
|
||||
step.error_message = str(exc)
|
||||
step.ended_at = utc_now()
|
||||
order.status = OrderStatus.FAILED
|
||||
workflow_run.status = OrderStatus.FAILED
|
||||
await session.commit()
|
||||
raise
|
||||
|
||||
|
||||
@activity.defn
|
||||
async def run_tryon_activity(payload: StepActivityInput) -> MockActivityResult:
|
||||
"""Mock try-on rendering."""
|
||||
"""执行试衣渲染步骤,或在未接真实能力时走 mock 分支。
|
||||
|
||||
return await execute_asset_step(
|
||||
payload,
|
||||
AssetType.TRYON,
|
||||
extra_metadata={"prepared_asset_id": payload.source_asset_id},
|
||||
)
|
||||
流程:
|
||||
1. 读取当前配置的图片生成 service。
|
||||
2. 如果当前仍是 mock service,就退回通用的 mock 资产产出逻辑。
|
||||
3. 如果是真实模式,先读取 prepare_model_activity 产出的 prepared_model 资产。
|
||||
4. 再读取本次选中的服装资源,并解析出它的原图 URL。
|
||||
5. 把“模特准备图 + 服装原图”一起发给 provider 做试衣生成。
|
||||
6. 把生成结果上传到 S3,并落成订单内的一条 TRYON 资产。
|
||||
7. 在 metadata 里记录 provider、model、prompt 和输入来源,方便追踪排查。
|
||||
"""
|
||||
|
||||
service = get_image_generation_service()
|
||||
# 保留旧的 mock 分支,这样在没有接入真实 provider 时 workflow 也还能跑通。
|
||||
if service.__class__.__name__ == "MockImageGenerationService":
|
||||
return await execute_asset_step(
|
||||
payload,
|
||||
AssetType.TRYON,
|
||||
extra_metadata={"prepared_asset_id": payload.source_asset_id},
|
||||
)
|
||||
|
||||
if payload.source_asset_id is None:
|
||||
raise ValueError("run_tryon_activity requires source_asset_id")
|
||||
if payload.garment_asset_id is None:
|
||||
raise ValueError("run_tryon_activity requires garment_asset_id")
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
order, workflow_run = await load_order_and_run(session, payload.order_id, payload.workflow_run_id)
|
||||
step = create_step_record(payload)
|
||||
session.add(step)
|
||||
|
||||
order.status = OrderStatus.RUNNING
|
||||
workflow_run.status = OrderStatus.RUNNING
|
||||
workflow_run.current_step = payload.step_name
|
||||
await session.flush()
|
||||
|
||||
try:
|
||||
# 试衣步骤的输入起点固定是上一阶段产出的 prepared_model 资产。
|
||||
prepared_asset = await session.get(AssetORM, payload.source_asset_id)
|
||||
if prepared_asset is None or prepared_asset.order_id != payload.order_id:
|
||||
raise ValueError(f"Prepared asset {payload.source_asset_id} not found for order {payload.order_id}")
|
||||
if prepared_asset.asset_type != AssetType.PREPARED_MODEL:
|
||||
raise ValueError(f"Asset {payload.source_asset_id} is not a prepared_model asset")
|
||||
|
||||
# 服装素材来自共享资源库,workflow 实际消费的是资源库里的原图。
|
||||
garment_resource, garment_original = await load_active_library_resource(
|
||||
session,
|
||||
payload.garment_asset_id,
|
||||
resource_type=LibraryResourceType.GARMENT,
|
||||
)
|
||||
|
||||
# provider 接收两张真实输入图:模特准备图 + 服装参考图。
|
||||
generated = await service.generate_tryon_image(
|
||||
person_image_url=prepared_asset.uri,
|
||||
garment_image_url=garment_original.public_url,
|
||||
)
|
||||
# workflow 生成出的结果图先上传到 S3,再登记成订单资产。
|
||||
storage_key, public_url = await get_order_artifact_storage_service().upload_generated_image(
|
||||
order_id=payload.order_id,
|
||||
step_name=payload.step_name,
|
||||
image_bytes=generated.image_bytes,
|
||||
mime_type=generated.mime_type,
|
||||
)
|
||||
|
||||
# 记录足够的追踪信息,方便回溯这张试衣图由哪些输入和哪个 provider 产出。
|
||||
metadata = {
|
||||
**payload.metadata,
|
||||
"prepared_asset_id": prepared_asset.id,
|
||||
"garment_resource_id": garment_resource.id,
|
||||
"garment_original_file_id": garment_original.id,
|
||||
"garment_original_url": garment_original.public_url,
|
||||
"provider": generated.provider,
|
||||
"model": generated.model,
|
||||
"storage_key": storage_key,
|
||||
"mime_type": generated.mime_type,
|
||||
"prompt": generated.prompt,
|
||||
}
|
||||
metadata = {key: value for key, value in metadata.items() if value is not None}
|
||||
|
||||
asset = AssetORM(
|
||||
order_id=payload.order_id,
|
||||
asset_type=AssetType.TRYON,
|
||||
step_name=payload.step_name,
|
||||
uri=public_url,
|
||||
metadata_json=jsonable(metadata),
|
||||
)
|
||||
session.add(asset)
|
||||
await session.flush()
|
||||
|
||||
result = MockActivityResult(
|
||||
step_name=payload.step_name,
|
||||
success=True,
|
||||
asset_id=asset.id,
|
||||
uri=asset.uri,
|
||||
score=1.0,
|
||||
passed=True,
|
||||
message="try-on generated",
|
||||
metadata=jsonable(metadata) or {},
|
||||
)
|
||||
|
||||
step.step_status = StepStatus.SUCCEEDED
|
||||
step.output_json = jsonable(result)
|
||||
step.ended_at = utc_now()
|
||||
await session.commit()
|
||||
return result
|
||||
except Exception as exc:
|
||||
step.step_status = StepStatus.FAILED
|
||||
step.error_message = str(exc)
|
||||
step.ended_at = utc_now()
|
||||
order.status = OrderStatus.FAILED
|
||||
workflow_run.status = OrderStatus.FAILED
|
||||
await session.commit()
|
||||
raise
|
||||
|
||||
@@ -26,14 +26,13 @@ with workflow.unsafe.imports_passed_through():
|
||||
from app.workers.activities.review_activities import mark_workflow_failed_activity
|
||||
from app.workers.activities.scene_activities import run_scene_activity
|
||||
from app.workers.activities.tryon_activities import prepare_model_activity, run_tryon_activity
|
||||
from app.workers.workflows.timeout_policy import DEFAULT_ACTIVITY_TIMEOUT, activity_timeout_for_task_queue
|
||||
from app.workers.workflows.types import (
|
||||
PipelineWorkflowInput,
|
||||
StepActivityInput,
|
||||
WorkflowFailureActivityInput,
|
||||
)
|
||||
|
||||
|
||||
ACTIVITY_TIMEOUT = timedelta(seconds=30)
|
||||
ACTIVITY_RETRY_POLICY = RetryPolicy(
|
||||
initial_interval=timedelta(seconds=1),
|
||||
backoff_coefficient=2.0,
|
||||
@@ -64,6 +63,8 @@ class LowEndPipelineWorkflow:
|
||||
try:
|
||||
# 每个步骤都通过 execute_activity 触发真正执行。
|
||||
# workflow 自己不做计算,只负责调度。
|
||||
# prepare_model 的职责是把“模特资源 + 订单上下文”整理成后续可消费的人物底图。
|
||||
# 这一步产出的 prepared.asset_id 会作为 tryon 的 source_asset_id。
|
||||
prepared = await workflow.execute_activity(
|
||||
prepare_model_activity,
|
||||
StepActivityInput(
|
||||
@@ -76,12 +77,14 @@ class LowEndPipelineWorkflow:
|
||||
scene_ref_asset_id=payload.scene_ref_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_CONTROL_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_CONTROL_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
current_step = WorkflowStepName.TRYON
|
||||
# 下游步骤通过 source_asset_id 引用上一步生成的资产。
|
||||
# tryon 是换装主步骤:把 prepared 模特图和 garment_asset_id 组合成试衣结果。
|
||||
# 它产出的 tryon_result.asset_id 是后续 scene / qc 的基础输入。
|
||||
tryon_result = await workflow.execute_activity(
|
||||
run_tryon_activity,
|
||||
StepActivityInput(
|
||||
@@ -92,38 +95,46 @@ class LowEndPipelineWorkflow:
|
||||
garment_asset_id=payload.garment_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
current_step = WorkflowStepName.SCENE
|
||||
scene_result = await workflow.execute_activity(
|
||||
run_scene_activity,
|
||||
StepActivityInput(
|
||||
order_id=payload.order_id,
|
||||
workflow_run_id=payload.workflow_run_id,
|
||||
step_name=WorkflowStepName.SCENE,
|
||||
source_asset_id=tryon_result.asset_id,
|
||||
scene_ref_asset_id=payload.scene_ref_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
scene_source_asset_id = tryon_result.asset_id
|
||||
if payload.scene_ref_asset_id is not None:
|
||||
current_step = WorkflowStepName.SCENE
|
||||
# scene 是可选步骤。
|
||||
# 有场景图时,用 scene_ref_asset_id 把 tryon 结果合成到目标背景;
|
||||
# 没有场景图时,直接沿用 tryon 结果继续往下走。
|
||||
scene_result = await workflow.execute_activity(
|
||||
run_scene_activity,
|
||||
StepActivityInput(
|
||||
order_id=payload.order_id,
|
||||
workflow_run_id=payload.workflow_run_id,
|
||||
step_name=WorkflowStepName.SCENE,
|
||||
source_asset_id=tryon_result.asset_id,
|
||||
scene_ref_asset_id=payload.scene_ref_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
scene_source_asset_id = scene_result.asset_id
|
||||
|
||||
current_step = WorkflowStepName.QC
|
||||
# QC 是流程里的“闸门”。
|
||||
# 如果这里不通过,低端流程直接失败,不会再 export。
|
||||
# source_asset_id 指向当前链路里“最接近最终成品”的那张图:
|
||||
# 有场景时是 scene 结果,没有场景时就是 tryon 结果。
|
||||
qc_result = await workflow.execute_activity(
|
||||
run_qc_activity,
|
||||
StepActivityInput(
|
||||
order_id=payload.order_id,
|
||||
workflow_run_id=payload.workflow_run_id,
|
||||
step_name=WorkflowStepName.QC,
|
||||
source_asset_id=scene_result.asset_id,
|
||||
source_asset_id=scene_source_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_QC_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_QC_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -134,16 +145,17 @@ class LowEndPipelineWorkflow:
|
||||
current_step = WorkflowStepName.EXPORT
|
||||
# candidate_asset_ids 是 QC 推荐可导出的候选资产。
|
||||
# 当前 MVP 只会返回一个候选;如果没有,就退回 scene 结果导出。
|
||||
# export 是最后的收口步骤:生成最终交付资产,并把订单状态写成 succeeded。
|
||||
final_result = await workflow.execute_activity(
|
||||
run_export_activity,
|
||||
StepActivityInput(
|
||||
order_id=payload.order_id,
|
||||
workflow_run_id=payload.workflow_run_id,
|
||||
step_name=WorkflowStepName.EXPORT,
|
||||
source_asset_id=(qc_result.candidate_asset_ids or [scene_result.asset_id])[0],
|
||||
source_asset_id=(qc_result.candidate_asset_ids or [scene_source_asset_id])[0],
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_EXPORT_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_EXPORT_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
return {
|
||||
@@ -177,6 +189,6 @@ class LowEndPipelineWorkflow:
|
||||
message=message,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_CONTROL_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=DEFAULT_ACTIVITY_TIMEOUT,
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ with workflow.unsafe.imports_passed_through():
|
||||
from app.workers.activities.scene_activities import run_scene_activity
|
||||
from app.workers.activities.texture_activities import run_texture_activity
|
||||
from app.workers.activities.tryon_activities import prepare_model_activity, run_tryon_activity
|
||||
from app.workers.workflows.timeout_policy import DEFAULT_ACTIVITY_TIMEOUT, activity_timeout_for_task_queue
|
||||
from app.workers.workflows.types import (
|
||||
MockActivityResult,
|
||||
PipelineWorkflowInput,
|
||||
@@ -44,8 +45,6 @@ with workflow.unsafe.imports_passed_through():
|
||||
WorkflowFailureActivityInput,
|
||||
)
|
||||
|
||||
|
||||
ACTIVITY_TIMEOUT = timedelta(seconds=30)
|
||||
ACTIVITY_RETRY_POLICY = RetryPolicy(
|
||||
initial_interval=timedelta(seconds=1),
|
||||
backoff_coefficient=2.0,
|
||||
@@ -87,6 +86,8 @@ class MidEndPipelineWorkflow:
|
||||
# current_step 用于失败时记录“最后跑到哪一步”。
|
||||
current_step = WorkflowStepName.PREPARE_MODEL
|
||||
try:
|
||||
# prepare_model / tryon / scene 这三段和低端流程共享同一套 activity,
|
||||
# 区别只在于中端流程后面还会继续进入 texture / face / fusion / review。
|
||||
prepared = await workflow.execute_activity(
|
||||
prepare_model_activity,
|
||||
StepActivityInput(
|
||||
@@ -99,11 +100,12 @@ class MidEndPipelineWorkflow:
|
||||
scene_ref_asset_id=payload.scene_ref_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_CONTROL_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_CONTROL_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
current_step = WorkflowStepName.TRYON
|
||||
# tryon 产出基础换装图,后面的所有增强步骤都以它为起点。
|
||||
tryon_result = await workflow.execute_activity(
|
||||
run_tryon_activity,
|
||||
StepActivityInput(
|
||||
@@ -114,23 +116,37 @@ class MidEndPipelineWorkflow:
|
||||
garment_asset_id=payload.garment_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
current_step = WorkflowStepName.SCENE
|
||||
scene_result = await self._run_scene(payload, tryon_result.asset_id)
|
||||
scene_source_asset_id = tryon_result.asset_id
|
||||
if payload.scene_ref_asset_id is not None:
|
||||
current_step = WorkflowStepName.SCENE
|
||||
# scene 仍然是可选步骤。
|
||||
# 如果存在场景图,就先完成换背景,再把结果交给 texture / fusion;
|
||||
# 如果没有场景图,就直接把 tryon 结果当成“场景基底”继续流程。
|
||||
scene_result = await self._run_scene(payload, tryon_result.asset_id)
|
||||
scene_source_asset_id = scene_result.asset_id
|
||||
else:
|
||||
scene_result = tryon_result
|
||||
|
||||
current_step = WorkflowStepName.TEXTURE
|
||||
texture_result = await self._run_texture(payload, scene_result.asset_id)
|
||||
# texture 是复杂流独有步骤。
|
||||
# 它通常负责衣物纹理、材质细节或局部增强,输入是当前场景基底图。
|
||||
texture_result = await self._run_texture(payload, scene_source_asset_id)
|
||||
|
||||
current_step = WorkflowStepName.FACE
|
||||
# face 也是复杂流独有步骤。
|
||||
# 它在 texture 结果基础上做脸部修复/增强,产出供 fusion 合成的人像版本。
|
||||
face_result = await self._run_face(payload, texture_result.asset_id)
|
||||
|
||||
current_step = WorkflowStepName.FUSION
|
||||
fusion_result = await self._run_fusion(payload, scene_result.asset_id, face_result.asset_id)
|
||||
# fusion 负责把“场景基底”和“脸部增强结果”合成成候选成品。
|
||||
fusion_result = await self._run_fusion(payload, scene_source_asset_id, face_result.asset_id)
|
||||
|
||||
current_step = WorkflowStepName.QC
|
||||
# QC 和低端流程复用同一套 activity,但这里检查的是 fusion 产物。
|
||||
qc_result = await self._run_qc(payload, fusion_result.asset_id)
|
||||
if not qc_result.passed:
|
||||
await self._mark_failed(payload, current_step, qc_result.message)
|
||||
@@ -144,6 +160,8 @@ class MidEndPipelineWorkflow:
|
||||
current_step = WorkflowStepName.REVIEW
|
||||
# 这里通过 activity 把数据库里的订单状态更新成 waiting_review,
|
||||
# 同时创建 review_task,供 API 查询待审核列表。
|
||||
# review 是复杂流真正区别于低端流程的核心:
|
||||
# 在 export 前插入人工决策点,并支持按意见回流重跑。
|
||||
await workflow.execute_activity(
|
||||
mark_waiting_for_review_activity,
|
||||
ReviewWaitActivityInput(
|
||||
@@ -152,7 +170,7 @@ class MidEndPipelineWorkflow:
|
||||
candidate_asset_ids=qc_result.candidate_asset_ids,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_CONTROL_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_CONTROL_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -172,7 +190,7 @@ class MidEndPipelineWorkflow:
|
||||
comment=review_payload.comment,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_CONTROL_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_CONTROL_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -180,6 +198,7 @@ class MidEndPipelineWorkflow:
|
||||
current_step = WorkflowStepName.EXPORT
|
||||
# 如果审核人显式选了资产,就导出该资产;
|
||||
# 否则默认导出 QC 候选资产。
|
||||
# export 本身仍然和低端流程是同一个 activity。
|
||||
export_source_id = review_payload.selected_asset_id
|
||||
if export_source_id is None:
|
||||
export_source_id = (qc_result.candidate_asset_ids or [fusion_result.asset_id])[0]
|
||||
@@ -192,7 +211,7 @@ class MidEndPipelineWorkflow:
|
||||
source_asset_id=export_source_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_EXPORT_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_EXPORT_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
return {
|
||||
@@ -208,22 +227,30 @@ class MidEndPipelineWorkflow:
|
||||
# rerun 的核心思想是:
|
||||
# 把指定节点后的链路重新跑一遍,然后再次进入 QC 和 waiting_review。
|
||||
if review_payload.decision == ReviewDecision.RERUN_SCENE:
|
||||
current_step = WorkflowStepName.SCENE
|
||||
scene_result = await self._run_scene(payload, tryon_result.asset_id)
|
||||
# 从 scene 开始重跑意味着 scene / texture / face / fusion 全部重算。
|
||||
if payload.scene_ref_asset_id is not None:
|
||||
current_step = WorkflowStepName.SCENE
|
||||
scene_result = await self._run_scene(payload, tryon_result.asset_id)
|
||||
scene_source_asset_id = scene_result.asset_id
|
||||
else:
|
||||
scene_result = tryon_result
|
||||
scene_source_asset_id = tryon_result.asset_id
|
||||
current_step = WorkflowStepName.TEXTURE
|
||||
texture_result = await self._run_texture(payload, scene_result.asset_id)
|
||||
texture_result = await self._run_texture(payload, scene_source_asset_id)
|
||||
current_step = WorkflowStepName.FACE
|
||||
face_result = await self._run_face(payload, texture_result.asset_id)
|
||||
current_step = WorkflowStepName.FUSION
|
||||
fusion_result = await self._run_fusion(payload, scene_result.asset_id, face_result.asset_id)
|
||||
fusion_result = await self._run_fusion(payload, scene_source_asset_id, face_result.asset_id)
|
||||
elif review_payload.decision == ReviewDecision.RERUN_FACE:
|
||||
# 从 face 开始重跑时,scene / texture 结果保持不变。
|
||||
current_step = WorkflowStepName.FACE
|
||||
face_result = await self._run_face(payload, texture_result.asset_id)
|
||||
current_step = WorkflowStepName.FUSION
|
||||
fusion_result = await self._run_fusion(payload, scene_result.asset_id, face_result.asset_id)
|
||||
fusion_result = await self._run_fusion(payload, scene_source_asset_id, face_result.asset_id)
|
||||
elif review_payload.decision == ReviewDecision.RERUN_FUSION:
|
||||
# 从 fusion 重跑是最小范围回流,只重做最终合成。
|
||||
current_step = WorkflowStepName.FUSION
|
||||
fusion_result = await self._run_fusion(payload, scene_result.asset_id, face_result.asset_id)
|
||||
fusion_result = await self._run_fusion(payload, scene_source_asset_id, face_result.asset_id)
|
||||
|
||||
current_step = WorkflowStepName.QC
|
||||
qc_result = await self._run_qc(payload, fusion_result.asset_id)
|
||||
@@ -264,7 +291,7 @@ class MidEndPipelineWorkflow:
|
||||
scene_ref_asset_id=payload.scene_ref_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -280,7 +307,7 @@ class MidEndPipelineWorkflow:
|
||||
source_asset_id=source_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -296,7 +323,7 @@ class MidEndPipelineWorkflow:
|
||||
source_asset_id=source_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -318,7 +345,7 @@ class MidEndPipelineWorkflow:
|
||||
selected_asset_id=face_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -334,7 +361,7 @@ class MidEndPipelineWorkflow:
|
||||
source_asset_id=source_asset_id,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_QC_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=activity_timeout_for_task_queue(IMAGE_PIPELINE_QC_TASK_QUEUE),
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
@@ -355,6 +382,6 @@ class MidEndPipelineWorkflow:
|
||||
message=message,
|
||||
),
|
||||
task_queue=IMAGE_PIPELINE_CONTROL_TASK_QUEUE,
|
||||
start_to_close_timeout=ACTIVITY_TIMEOUT,
|
||||
start_to_close_timeout=DEFAULT_ACTIVITY_TIMEOUT,
|
||||
retry_policy=ACTIVITY_RETRY_POLICY,
|
||||
)
|
||||
|
||||
24
app/workers/workflows/timeout_policy.py
Normal file
24
app/workers/workflows/timeout_policy.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Shared activity timeout policy for Temporal workflows."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from app.infra.temporal.task_queues import (
|
||||
IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE,
|
||||
IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE,
|
||||
)
|
||||
|
||||
DEFAULT_ACTIVITY_TIMEOUT = timedelta(seconds=30)
|
||||
LONG_RUNNING_ACTIVITY_TIMEOUT = timedelta(minutes=5)
|
||||
|
||||
LONG_RUNNING_TASK_QUEUES = {
|
||||
IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE,
|
||||
IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE,
|
||||
}
|
||||
|
||||
|
||||
def activity_timeout_for_task_queue(task_queue: str) -> timedelta:
|
||||
"""Return the timeout that matches the workload behind the given task queue."""
|
||||
|
||||
if task_queue in LONG_RUNNING_TASK_QUEUES:
|
||||
return LONG_RUNNING_ACTIVITY_TIMEOUT
|
||||
return DEFAULT_ACTIVITY_TIMEOUT
|
||||
@@ -32,14 +32,22 @@ class PipelineWorkflowInput:
|
||||
这是一张订单进入 workflow 时携带的最小上下文。
|
||||
"""
|
||||
|
||||
# 订单主键。整个 workflow 的所有步骤都会围绕这张订单落库、查状态。
|
||||
order_id: int
|
||||
# workflow_run 主键。用于把每一步 step 记录关联到同一次运行。
|
||||
workflow_run_id: int
|
||||
# 客户层级。决定业务侧订单归属,也会影响后续统计和展示。
|
||||
customer_level: CustomerLevel
|
||||
# 服务模式。这里直接决定启动简单流程还是复杂流程。
|
||||
service_mode: ServiceMode
|
||||
# 模特资源 ID。prepare_model 会基于它准备人物素材。
|
||||
model_id: int
|
||||
pose_id: int
|
||||
# 服装资源/资产 ID。tryon 会把它作为换装输入。
|
||||
garment_asset_id: int
|
||||
scene_ref_asset_id: int
|
||||
# 场景参考图 ID。可为空;为空时跳过 scene 步骤。
|
||||
scene_ref_asset_id: int | None = None
|
||||
# 模特姿势 ID。当前 MVP 已经放宽为可空,后续需要姿势控制时再启用。
|
||||
pose_id: int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""在反序列化后把枚举字段修正回来。"""
|
||||
@@ -59,15 +67,25 @@ class StepActivityInput:
|
||||
- 上一步产出的 asset_id
|
||||
"""
|
||||
|
||||
# 当前步骤属于哪张订单。
|
||||
order_id: int
|
||||
# 当前步骤属于哪次 workflow 运行。
|
||||
workflow_run_id: int
|
||||
# 当前执行的步骤名,例如 tryon / qc / export。
|
||||
step_name: WorkflowStepName
|
||||
# 模特资源 ID。只有 prepare_model 等少数步骤会直接消费它。
|
||||
model_id: int | None = None
|
||||
# 姿势 ID。当前大多为透传预留字段。
|
||||
pose_id: int | None = None
|
||||
# 服装资源/资产 ID。tryon 是主要消费者。
|
||||
garment_asset_id: int | None = None
|
||||
# 场景参考图 ID。scene 步骤会用它完成换背景。
|
||||
scene_ref_asset_id: int | None = None
|
||||
# 上一步产出的资产 ID。绝大多数步骤都靠它串起资产链路。
|
||||
source_asset_id: int | None = None
|
||||
# 人工选中的资产 ID。review / fusion / export 等需要显式选图时使用。
|
||||
selected_asset_id: int | None = None
|
||||
# 额外扩展参数。给特殊步骤塞一些不值得单独建字段的临时上下文。
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -80,14 +98,23 @@ class StepActivityInput:
|
||||
class MockActivityResult:
|
||||
"""通用 mock activity 返回结构。"""
|
||||
|
||||
# 这是哪一步返回的结果,方便 workflow 和落库代码识别来源。
|
||||
step_name: WorkflowStepName
|
||||
# activity 是否执行成功。一般表示“代码执行成功”,不等于业务一定通过。
|
||||
success: bool
|
||||
# 这一步新生成的资产 ID;如果步骤不产出资产,可以为空。
|
||||
asset_id: int | None
|
||||
# 产出资产的访问地址;当前 mock 阶段通常是 mock:// URI。
|
||||
uri: str | None
|
||||
# 模型分数/质量分数之类的数值结果,非所有步骤都有。
|
||||
score: float | None = None
|
||||
# 业务是否通过。典型场景是 QC:activity 成功执行,但 passed 可能为 False。
|
||||
passed: bool | None = None
|
||||
# 给 workflow 或 API 展示的简短结果消息。
|
||||
message: str = "mock success"
|
||||
# 候选资产列表。主要给 QC / review 这类“多候选图”步骤使用。
|
||||
candidate_asset_ids: list[int] = field(default_factory=list)
|
||||
# 补充元数据。用于把步骤内部的一些上下文回传给调用方。
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -100,9 +127,13 @@ class MockActivityResult:
|
||||
class ReviewSignalPayload:
|
||||
"""API 发给中端 workflow 的审核 signal 载荷。"""
|
||||
|
||||
# 审核动作:通过 / 拒绝 / 从某一步重跑。
|
||||
decision: ReviewDecision
|
||||
# 操作审核的用户 ID,便于审计和任务归属。
|
||||
reviewer_id: int
|
||||
# 审核人最终选中的候选资产 ID;approve 时最常见。
|
||||
selected_asset_id: int | None = None
|
||||
# 审核备注,例如“脸部不自然,重跑融合”。
|
||||
comment: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -115,9 +146,13 @@ class ReviewSignalPayload:
|
||||
class ReviewWaitActivityInput:
|
||||
"""把流程切到 waiting_review 时传给 activity 的输入。"""
|
||||
|
||||
# 当前审核任务属于哪张订单。
|
||||
order_id: int
|
||||
# 当前审核任务属于哪次 workflow 运行。
|
||||
workflow_run_id: int
|
||||
# 提交给审核端看的候选资产列表。
|
||||
candidate_asset_ids: list[int] = field(default_factory=list)
|
||||
# 进入审核态时附带的说明文案,当前大多留空。
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
@@ -125,11 +160,17 @@ class ReviewWaitActivityInput:
|
||||
class ReviewResolutionActivityInput:
|
||||
"""审核结果到达后,用于结束 waiting_review 的输入。"""
|
||||
|
||||
# 当前审核结果属于哪张订单。
|
||||
order_id: int
|
||||
# 当前审核结果属于哪次 workflow 运行。
|
||||
workflow_run_id: int
|
||||
# 审核最终决策。
|
||||
decision: ReviewDecision
|
||||
# 处理这次审核的审核人 ID。
|
||||
reviewer_id: int
|
||||
# 如果审核人明确挑了一张图,这里记录最终选择的资产 ID。
|
||||
selected_asset_id: int | None = None
|
||||
# 审核备注或重跑原因。
|
||||
comment: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -142,10 +183,15 @@ class ReviewResolutionActivityInput:
|
||||
class WorkflowFailureActivityInput:
|
||||
"""流程失败收尾 activity 的输入。"""
|
||||
|
||||
# 失败发生在哪张订单。
|
||||
order_id: int
|
||||
# 失败发生在哪次 workflow 运行。
|
||||
workflow_run_id: int
|
||||
# 失败停留在哪个步骤,用于更新 workflow_run.current_step。
|
||||
current_step: WorkflowStepName
|
||||
# 失败原因文本,通常来自异常或 QC 驳回消息。
|
||||
message: str
|
||||
# 要写回数据库的最终状态,默认就是 failed。
|
||||
status: OrderStatus = OrderStatus.FAILED
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user