Files
video-gen/video-gen-api/app/services/generation_history_meta_service.py
2026-07-01 10:28:30 +08:00

284 lines
9.5 KiB
Python

from __future__ import annotations
from typing import Any, TypedDict
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.enums.generation_history import (
GenerationHistorySourceEnum,
get_generation_history_source_label,
is_generation_history_module_source,
)
from app.models.module_generation_project import ModuleGenerationProject
from app.models.module_generation_step import ModuleGenerationStep
from app.models.shot_replicate_segment import ShotReplicateSegment
class GenerationHistoryMeta(TypedDict):
"""素材云历史项模块上下文回填字段。"""
history_source: str | None
history_source_label: str | None
module_project_id: str | None
module_project_title: str | None
module_step_id: str | None
module_step_code: str | None
hot_opening_project_id: str | None
hot_opening_project_title: str | None
shot_replicate_project_id: str | None
shot_replicate_project_title: str | None
shot_task_set_id: str | None
shot_segment_id: str | None
shot_segment_index: int | None
shot_segment_label: str | None
class _StepLinkInfo(TypedDict):
module_project_id: str | None
module_step_id: str | None
module_step_code: str | None
module: str | None
class _ProjectInfo(TypedDict):
module_project_id: str
module_project_title: str | None
module: str | None
class _ShotSegmentInfo(TypedDict):
shot_task_set_id: str | None
shot_segment_id: str | None
shot_segment_index: int | None
shot_segment_label: str | None
def _unique(values: list[str] | tuple[str, ...]) -> list[str]:
return list(dict.fromkeys(str(value) for value in values if value))
def _segment_label(segment_index: int | None) -> str | None:
if segment_index is None:
return None
return f"片段{segment_index}"
def build_empty_history_meta(source: GenerationHistorySourceEnum) -> GenerationHistoryMeta:
"""构造统一历史字段,非对应模块字段保持 null。"""
return {
"history_source": source.value,
"history_source_label": get_generation_history_source_label(source),
"module_project_id": None,
"module_project_title": None,
"module_step_id": None,
"module_step_code": None,
"hot_opening_project_id": None,
"hot_opening_project_title": None,
"shot_replicate_project_id": None,
"shot_replicate_project_title": None,
"shot_task_set_id": None,
"shot_segment_id": None,
"shot_segment_index": None,
"shot_segment_label": None,
}
async def _load_step_link_map(
db: AsyncSession,
*,
chat_task_ids: list[str],
source: GenerationHistorySourceEnum,
) -> dict[str, _StepLinkInfo]:
ids = _unique(chat_task_ids)
if not ids:
return {}
stmt = (
select(
ModuleGenerationStep.chat_task_id.label("chat_task_id"),
ModuleGenerationStep.id.label("module_step_id"),
ModuleGenerationStep.project_id.label("module_project_id"),
ModuleGenerationStep.step_code.label("module_step_code"),
ModuleGenerationStep.module.label("module"),
ModuleGenerationStep.is_current.label("is_current"),
ModuleGenerationStep.updated_at.label("updated_at"),
)
.where(
ModuleGenerationStep.deleted_at.is_(None),
ModuleGenerationStep.chat_task_id.in_(ids),
ModuleGenerationStep.module == source.value,
)
.order_by(
ModuleGenerationStep.chat_task_id.asc(),
ModuleGenerationStep.is_current.desc(),
ModuleGenerationStep.updated_at.desc(),
)
)
rows = (await db.execute(stmt)).mappings().all()
link_map: dict[str, _StepLinkInfo] = {}
for row in rows:
chat_task_id = row["chat_task_id"]
if not chat_task_id or chat_task_id in link_map:
continue
link_map[chat_task_id] = {
"module_project_id": row["module_project_id"],
"module_step_id": row["module_step_id"],
"module_step_code": row["module_step_code"],
"module": row["module"],
}
return link_map
async def _load_project_map(
db: AsyncSession,
*,
module_project_ids: list[str],
) -> dict[str, _ProjectInfo]:
ids = _unique(module_project_ids)
if not ids:
return {}
stmt = (
select(
ModuleGenerationProject.id.label("module_project_id"),
ModuleGenerationProject.title.label("module_project_title"),
ModuleGenerationProject.module.label("module"),
)
.where(
ModuleGenerationProject.deleted_at.is_(None),
ModuleGenerationProject.id.in_(ids),
)
)
return {
row["module_project_id"]: {
"module_project_id": row["module_project_id"],
"module_project_title": row["module_project_title"],
"module": row["module"],
}
for row in (await db.execute(stmt)).mappings().all()
if row["module_project_id"]
}
async def _load_shot_segment_map(
db: AsyncSession,
*,
module_project_ids: list[str],
) -> dict[str, _ShotSegmentInfo]:
ids = _unique(module_project_ids)
if not ids:
return {}
stmt = (
select(
ShotReplicateSegment.module_project_id.label("module_project_id"),
ShotReplicateSegment.id.label("shot_segment_id"),
ShotReplicateSegment.task_set_id.label("shot_task_set_id"),
ShotReplicateSegment.segment_index.label("shot_segment_index"),
ShotReplicateSegment.updated_at.label("updated_at"),
)
.where(
ShotReplicateSegment.deleted_at.is_(None),
ShotReplicateSegment.module_project_id.in_(ids),
)
.order_by(
ShotReplicateSegment.module_project_id.asc(),
ShotReplicateSegment.updated_at.desc(),
)
)
segment_map: dict[str, _ShotSegmentInfo] = {}
rows = (await db.execute(stmt)).mappings().all()
for row in rows:
module_project_id = row["module_project_id"]
if not module_project_id or module_project_id in segment_map:
continue
segment_index = row["shot_segment_index"]
segment_map[module_project_id] = {
"shot_task_set_id": row["shot_task_set_id"],
"shot_segment_id": row["shot_segment_id"],
"shot_segment_index": segment_index,
"shot_segment_label": _segment_label(segment_index),
}
return segment_map
def _merge_meta(
*,
source: GenerationHistorySourceEnum,
step_info: _StepLinkInfo | None,
project_info: _ProjectInfo | None,
shot_info: _ShotSegmentInfo | None,
) -> GenerationHistoryMeta:
meta = build_empty_history_meta(source)
module_project_id = step_info["module_project_id"] if step_info else None
module_step_id = step_info["module_step_id"] if step_info else None
module_step_code = step_info["module_step_code"] if step_info else None
module_project_title = project_info["module_project_title"] if project_info else None
meta["module_project_id"] = module_project_id
meta["module_project_title"] = module_project_title
meta["module_step_id"] = module_step_id
meta["module_step_code"] = module_step_code
if source == GenerationHistorySourceEnum.HOT_OPENING_REPLICATE:
meta["hot_opening_project_id"] = module_project_id
meta["hot_opening_project_title"] = module_project_title
elif source == GenerationHistorySourceEnum.SHOT_REPLICATE:
meta["shot_replicate_project_id"] = module_project_id
meta["shot_replicate_project_title"] = module_project_title
if shot_info:
meta["shot_task_set_id"] = shot_info["shot_task_set_id"]
meta["shot_segment_id"] = shot_info["shot_segment_id"]
meta["shot_segment_index"] = shot_info["shot_segment_index"]
meta["shot_segment_label"] = shot_info["shot_segment_label"]
return meta
async def batch_load_generation_history_meta_map(
db: AsyncSession,
*,
source: GenerationHistorySourceEnum,
chat_task_ids: list[str],
) -> dict[str, GenerationHistoryMeta]:
"""批量回填历史列表模块上下文,避免逐条链式查询。"""
ids = _unique(chat_task_ids)
if not ids:
return {}
if not is_generation_history_module_source(source):
return {chat_task_id: build_empty_history_meta(source) for chat_task_id in ids}
step_link_map = await _load_step_link_map(db, chat_task_ids=ids, source=source)
module_project_ids = [
step_info["module_project_id"]
for step_info in step_link_map.values()
if step_info.get("module_project_id")
]
project_map = await _load_project_map(db, module_project_ids=module_project_ids)
shot_segment_map: dict[str, _ShotSegmentInfo] = {}
if source == GenerationHistorySourceEnum.SHOT_REPLICATE:
shot_segment_map = await _load_shot_segment_map(db, module_project_ids=module_project_ids)
meta_map: dict[str, GenerationHistoryMeta] = {}
for chat_task_id in ids:
step_info = step_link_map.get(chat_task_id)
module_project_id = step_info.get("module_project_id") if step_info else None
project_info = project_map.get(module_project_id) if module_project_id else None
shot_info = shot_segment_map.get(module_project_id) if module_project_id else None
meta_map[chat_task_id] = _merge_meta(
source=source,
step_info=step_info,
project_info=project_info,
shot_info=shot_info,
)
return meta_map