修复冻结积分BUG | 拆镜状态异常BUG
This commit is contained in:
@@ -26,7 +26,6 @@ from app.services.generation.pipeline.db_lock_service import (
|
||||
DatabaseRowLockBusy,
|
||||
execute_with_lock_timeout,
|
||||
)
|
||||
from app.services.llm import optimize_prompt
|
||||
from app.services.video_url import validate_and_get_record_id, get_video_stream_url
|
||||
from app.services.private_portrait.reference_resolver import batch_resolve_private_portrait_reference_display_urls, resolve_private_portrait_reference_display_urls
|
||||
from app.services.resource_signed_url_service import build_resource_signed_url
|
||||
@@ -43,21 +42,6 @@ from app.enums.generation_status import (
|
||||
GenerationType,
|
||||
)
|
||||
from app.enums.common import LogEventStatusEnum
|
||||
from app.enums.credit_record import (
|
||||
CreditRecordBillingScene,
|
||||
CreditRecordChargeKind,
|
||||
CreditRecordSourceModule,
|
||||
)
|
||||
from app.enums.llm_billing import LlmBillingConfigKey
|
||||
from app.services.llm_billing import (
|
||||
LlmBillingContext,
|
||||
log_provider_failure,
|
||||
log_provider_start,
|
||||
log_provider_success,
|
||||
release_on_failure,
|
||||
settle_success,
|
||||
start_hold,
|
||||
)
|
||||
from app.enums.generation_record import (
|
||||
GenerationRecordConfigSourceEnum,
|
||||
GenerationRecordEventTypeEnum,
|
||||
@@ -70,12 +54,9 @@ from app.services.generation.billing_service import (
|
||||
from app.services.generation.ai.engine_service import (
|
||||
get_image_engine,
|
||||
get_video_engine,
|
||||
image_supported_sizes,
|
||||
parse_json_list,
|
||||
)
|
||||
from app.services.generation.pipeline.generation_record_config_service import (
|
||||
ensure_generation_record_config_frozen,
|
||||
freeze_generation_record_config_with_log,
|
||||
frozen_generation_record_engine_view,
|
||||
generation_record_config_fallback_hint,
|
||||
generation_record_engine_snapshot,
|
||||
@@ -87,12 +68,12 @@ from app.services.generation.media_reference_service import (
|
||||
calculate_media_reference_usage,
|
||||
validate_media_reference_usage_for_engine,
|
||||
)
|
||||
from app.services.generation.prompt_optimize_service import optimize_generation_prompt
|
||||
from app.enums.audio_reference import (
|
||||
AUDIO_ALLOWED_EXTENSIONS,
|
||||
AUDIO_ALLOWED_MIME_TYPES,
|
||||
AUDIO_MAX_FILE_SIZE_MB,
|
||||
)
|
||||
from app.utils.id_gen import generate_id
|
||||
from app.utils.exceptions import RecordNotFoundError, InvalidStatusError
|
||||
|
||||
router = APIRouter(prefix="/generation-records", tags=["generation"])
|
||||
@@ -107,43 +88,12 @@ def _record_config_complete(record: GenerationRecord) -> bool:
|
||||
return is_generation_record_config_complete(record)
|
||||
|
||||
|
||||
def _canonical_references(value: object) -> str:
|
||||
return json.dumps(value or [], ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def _idempotency_config_matches(record: GenerationRecord, req: OptimizeParams) -> bool:
|
||||
try:
|
||||
existing_references = json.loads(record.media_references) if record.media_references else []
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return False
|
||||
if (
|
||||
str(record.project_id) != str(req.project_id)
|
||||
or record.original_prompt != req.prompt
|
||||
or record.gen_type != req.gen_type.value
|
||||
or str(record.engine_id or "") != str(req.engine_id)
|
||||
or bool(record.include_media_references) != bool(req.include_media_references)
|
||||
or _canonical_references(existing_references) != _canonical_references(req.references)
|
||||
):
|
||||
return False
|
||||
if req.gen_type == GenerationType.video:
|
||||
return (
|
||||
record.duration == req.duration
|
||||
and record.aspect_ratio == req.aspect_ratio
|
||||
and record.resolution == req.resolution
|
||||
)
|
||||
return (
|
||||
record.image_size == req.image_size
|
||||
and record.image_proportion == req.image_proportion
|
||||
and record.image_px == req.image_px
|
||||
)
|
||||
|
||||
|
||||
def _record_status_view(record: GenerationRecord) -> dict[str, object]:
|
||||
config_complete = _record_config_complete(record)
|
||||
config_recoverable = is_generation_record_config_recoverable(record)
|
||||
prompt_failure = record.status == "failed" and record.resource_generation_started_at is None
|
||||
resource_failure = record.status == "failed" and record.resource_generation_started_at is not None
|
||||
if record.status in {"pending", "optimizing"}:
|
||||
if record.status in {"pending", "optimizing", "settlement_pending"}:
|
||||
client_status = "prompt_processing"
|
||||
operation_phase = "prompt"
|
||||
elif record.status == "prompt_optimized":
|
||||
@@ -164,7 +114,7 @@ def _record_status_view(record: GenerationRecord) -> dict[str, object]:
|
||||
"config_fallback_hint": generation_record_config_fallback_hint(record),
|
||||
"can_generate": record.status == "prompt_optimized" and (config_complete or config_recoverable),
|
||||
"can_retry": resource_failure and config_complete and record.pipeline_stage != GenerationRecordPipelineStage.UPSCALE_FAILED.value,
|
||||
"should_poll": record.status in {"optimizing", "generating"},
|
||||
"should_poll": record.status in {"optimizing", "settlement_pending", "generating"},
|
||||
"client_status": client_status,
|
||||
"operation_phase": operation_phase,
|
||||
}
|
||||
@@ -174,26 +124,6 @@ def _frozen_engine_view(record: GenerationRecord) -> SimpleNamespace:
|
||||
return frozen_generation_record_engine_view(record)
|
||||
|
||||
|
||||
def _validate_video_engine_selection(engine, *, aspect_ratio: str, resolution: str, duration: int) -> None:
|
||||
ratios = [str(item) for item in parse_json_list(engine.supported_ratios, [])]
|
||||
resolutions = [str(item) for item in parse_json_list(engine.supported_resolutions, [])]
|
||||
durations = [int(item) for item in parse_json_list(engine.supported_durations, []) if str(item).isdigit()]
|
||||
if ratios and aspect_ratio not in ratios:
|
||||
raise HTTPException(status_code=400, detail="当前视频引擎不支持所选画面比例")
|
||||
if resolutions and resolution not in resolutions:
|
||||
raise HTTPException(status_code=400, detail="当前视频引擎不支持所选分辨率")
|
||||
if durations and duration not in durations:
|
||||
raise HTTPException(status_code=400, detail="当前视频引擎不支持所选时长")
|
||||
if int(engine.max_duration or 0) > 0 and duration > int(engine.max_duration):
|
||||
raise HTTPException(status_code=400, detail="生成时长超过当前视频引擎上限")
|
||||
|
||||
|
||||
def _validate_image_engine_selection(engine, *, image_size: str) -> None:
|
||||
sizes = image_supported_sizes(engine)
|
||||
if sizes and image_size not in sizes:
|
||||
raise HTTPException(status_code=400, detail="当前图片引擎不支持所选画面分辨率")
|
||||
|
||||
|
||||
def _record_to_out(record: GenerationRecord, project_name: str, refs_override: list[dict] | None = None) -> GenerationRecordOut:
|
||||
refs = refs_override
|
||||
if refs is None and record.media_references:
|
||||
@@ -286,6 +216,8 @@ async def list_records(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
allowed_statuses = {
|
||||
"optimizing",
|
||||
"settlement_pending",
|
||||
"prompt_optimized",
|
||||
"generating",
|
||||
"failed",
|
||||
@@ -295,7 +227,7 @@ async def list_records(
|
||||
if status and status not in allowed_statuses:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="状态参数错误,仅支持:prompt_optimized、generating、failed、completed",
|
||||
detail="状态参数错误,仅支持:optimizing、settlement_pending、prompt_optimized、generating、failed、completed",
|
||||
)
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
@@ -356,324 +288,20 @@ async def optimize(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id_snapshot = str(current_user.id)
|
||||
# Validate project, engine and the complete generation configuration before
|
||||
# charging prompt credits or invoking the LLM.
|
||||
proj_result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == req.project_id,
|
||||
Project.user_id == user_id_snapshot,
|
||||
Project.deleted_at.is_(None),
|
||||
).limit(1)
|
||||
)
|
||||
project = proj_result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
log_generation_record_config_event(
|
||||
event_type=GenerationRecordEventTypeEnum.PROMPT_CONFIG_VALIDATE_START,
|
||||
event_status=LogEventStatusEnum.STARTED,
|
||||
source=GenerationRecordConfigSourceEnum.PROMPT_OPTIMIZE,
|
||||
record=GenerationRecord(
|
||||
id=req.idempotency_key or "pending",
|
||||
user_id=user_id_snapshot,
|
||||
project_id=req.project_id,
|
||||
original_prompt=req.prompt,
|
||||
gen_type=req.gen_type.value,
|
||||
duration=req.duration if req.gen_type == GenerationType.video else None,
|
||||
aspect_ratio=req.aspect_ratio if req.gen_type == GenerationType.video else None,
|
||||
resolution=req.resolution if req.gen_type == GenerationType.video else None,
|
||||
image_size=req.image_size if req.gen_type == GenerationType.image else None,
|
||||
image_proportion=req.image_proportion if req.gen_type == GenerationType.image else None,
|
||||
image_px=req.image_px if req.gen_type == GenerationType.image else None,
|
||||
include_media_references=bool(req.include_media_references),
|
||||
media_references=json.dumps(req.references, ensure_ascii=False) if req.references else None,
|
||||
engine_id=req.engine_id,
|
||||
),
|
||||
detail={
|
||||
"project_id": req.project_id,
|
||||
"gen_type": req.gen_type.value,
|
||||
"engine_id": req.engine_id,
|
||||
"include_media_references": bool(req.include_media_references),
|
||||
"reference_count": len(req.references or []),
|
||||
"source": GenerationRecordConfigSourceEnum.PROMPT_OPTIMIZE.value,
|
||||
},
|
||||
)
|
||||
|
||||
# Idempotency must be checked before the temporary prompt-credit hold. The
|
||||
# key is bound to one immutable generation configuration; reusing it with a
|
||||
# different engine, parameter set or attachment selection is rejected.
|
||||
if req.idempotency_key:
|
||||
existing = await db.execute(
|
||||
select(GenerationRecord, Project.name)
|
||||
.join(Project, GenerationRecord.project_id == Project.id)
|
||||
.where(
|
||||
GenerationRecord.user_id == user_id_snapshot,
|
||||
GenerationRecord.deleted_at.is_(None),
|
||||
Project.deleted_at.is_(None),
|
||||
GenerationRecord.idempotency_key == req.idempotency_key,
|
||||
)
|
||||
.order_by(GenerationRecord.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
row = existing.first()
|
||||
if row:
|
||||
existing_record, project_name = row
|
||||
if not _idempotency_config_matches(existing_record, req):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="幂等键已绑定其他生成配置,请重新提交",
|
||||
)
|
||||
if not existing_record.optimized_prompt or not _record_config_complete(existing_record):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="幂等记录配置不完整,请使用新的幂等键重新提交",
|
||||
)
|
||||
refs = await resolve_private_portrait_reference_display_urls(
|
||||
db,
|
||||
json.loads(existing_record.media_references) if existing_record.media_references else None,
|
||||
user_id=user_id_snapshot,
|
||||
)
|
||||
return OptimizeResult(
|
||||
optimized_prompt=existing_record.optimized_prompt,
|
||||
text_credits_cost=existing_record.text_credits_cost or 0.0,
|
||||
text_tokens_used=existing_record.text_tokens_used or 0,
|
||||
record=_record_to_out(existing_record, project_name, refs_override=refs),
|
||||
)
|
||||
|
||||
|
||||
if req.gen_type == GenerationType.video:
|
||||
if req.duration not in DURATIONS:
|
||||
raise HTTPException(status_code=400, detail=f"视频时长必须为{DURATIONS}秒之一")
|
||||
if req.aspect_ratio not in ASPECT_RATIOS:
|
||||
raise HTTPException(status_code=400, detail="不支持的画面比例")
|
||||
if req.resolution not in RESOLUTIONS:
|
||||
raise HTTPException(status_code=400, detail="不支持的分辨率")
|
||||
engine = await get_video_engine(db, req.engine_id)
|
||||
_validate_video_engine_selection(
|
||||
engine,
|
||||
aspect_ratio=req.aspect_ratio,
|
||||
resolution=req.resolution,
|
||||
duration=int(req.duration),
|
||||
)
|
||||
else:
|
||||
if req.image_size not in IMAGE_SIZES:
|
||||
raise HTTPException(status_code=400, detail=f"图片分辨率必须为{IMAGE_SIZES}之一")
|
||||
if not req.image_proportion or not req.image_px:
|
||||
raise HTTPException(status_code=400, detail="图片生成需要指定比例和像素尺寸")
|
||||
engine = await get_image_engine(db, req.engine_id)
|
||||
_validate_image_engine_selection(engine, image_size=req.image_size)
|
||||
|
||||
project_name_snapshot = str(project.name)
|
||||
project_industry_snapshot = str(project.industry or "")
|
||||
engine_snapshot_source = SimpleNamespace(
|
||||
**{
|
||||
key: value
|
||||
for key, value in vars(engine).items()
|
||||
if key != "_sa_instance_state"
|
||||
}
|
||||
)
|
||||
|
||||
reference_usage = calculate_media_reference_usage(
|
||||
json.dumps(req.references, ensure_ascii=False) if req.references else None,
|
||||
include=bool(req.include_media_references),
|
||||
)
|
||||
validate_media_reference_usage_for_engine(
|
||||
reference_usage,
|
||||
gen_type=req.gen_type.value,
|
||||
engine=engine,
|
||||
)
|
||||
|
||||
|
||||
log_generation_record_config_event(
|
||||
event_type=GenerationRecordEventTypeEnum.PROMPT_CONFIG_VALIDATE_SUCCESS,
|
||||
event_status=LogEventStatusEnum.SUCCESS,
|
||||
source=GenerationRecordConfigSourceEnum.PROMPT_OPTIMIZE,
|
||||
record=GenerationRecord(
|
||||
id=req.idempotency_key or "pending",
|
||||
user_id=user_id_snapshot,
|
||||
project_id=req.project_id,
|
||||
original_prompt=req.prompt,
|
||||
gen_type=req.gen_type.value,
|
||||
duration=req.duration if req.gen_type == GenerationType.video else None,
|
||||
aspect_ratio=req.aspect_ratio if req.gen_type == GenerationType.video else None,
|
||||
resolution=req.resolution if req.gen_type == GenerationType.video else None,
|
||||
image_size=req.image_size if req.gen_type == GenerationType.image else None,
|
||||
image_proportion=req.image_proportion if req.gen_type == GenerationType.image else None,
|
||||
image_px=req.image_px if req.gen_type == GenerationType.image else None,
|
||||
include_media_references=bool(req.include_media_references),
|
||||
media_references=json.dumps(req.references, ensure_ascii=False) if req.references else None,
|
||||
engine_id=req.engine_id,
|
||||
),
|
||||
detail={
|
||||
"project_id": req.project_id,
|
||||
"gen_type": req.gen_type.value,
|
||||
"engine_id": req.engine_id,
|
||||
"include_media_references": bool(req.include_media_references),
|
||||
"reference_count": len(req.references or []),
|
||||
"media_reference_usage": getattr(reference_usage, "__dict__", None) or str(reference_usage),
|
||||
"source": GenerationRecordConfigSourceEnum.PROMPT_OPTIMIZE.value,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
record_id_value = generate_id()
|
||||
prompt_attempt_no = 1
|
||||
llm_billing_context = LlmBillingContext(
|
||||
service_result = await optimize_generation_prompt(
|
||||
db,
|
||||
req=req,
|
||||
user_id=user_id_snapshot,
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
owner_id=record_id_value,
|
||||
attempt_no=prompt_attempt_no,
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=CreditRecordBillingScene.GENERATION_RECORD_TEXT_PROMPT_OPTIMIZE.value,
|
||||
source_module=CreditRecordSourceModule.GENERATION_RECORD.value,
|
||||
related_id=record_id_value,
|
||||
hold_config_key=LlmBillingConfigKey.HOLD_GENERATION_RECORD_PROMPT.value,
|
||||
description_prefix="AI创作提示词优化",
|
||||
trace_id=f"generation-optimize:{record_id_value}",
|
||||
request_id=req.idempotency_key,
|
||||
)
|
||||
await start_hold(db, llm_billing_context)
|
||||
await db.commit()
|
||||
|
||||
log_provider_start(llm_billing_context, detail={"gen_type": req.gen_type.value})
|
||||
try:
|
||||
optimized, token_usage = await optimize_prompt(
|
||||
db,
|
||||
req.prompt,
|
||||
user_id=user_id_snapshot,
|
||||
industry_key=project_industry_snapshot,
|
||||
duration=req.duration if req.gen_type == GenerationType.video else None,
|
||||
image_size=req.image_size if req.gen_type == GenerationType.image else None,
|
||||
image_proportion=req.image_proportion if req.gen_type == GenerationType.image else None,
|
||||
image_px=req.image_px if req.gen_type == GenerationType.image else None,
|
||||
references=req.references,
|
||||
gen_type=req.gen_type.value,
|
||||
log_module="generation_record",
|
||||
log_step="prompt_optimize",
|
||||
log_project_id=req.project_id,
|
||||
log_owner_type=OWNER_GENERATION_RECORD,
|
||||
log_owner_id=record_id_value,
|
||||
generation_attempt_no=prompt_attempt_no,
|
||||
)
|
||||
log_provider_success(llm_billing_context, usage=token_usage)
|
||||
except Exception as exc:
|
||||
from app.services.error_codes import extract_error_message
|
||||
|
||||
await db.rollback()
|
||||
log_provider_failure(llm_billing_context, error=str(exc))
|
||||
await release_on_failure(db, llm_billing_context, error=str(exc))
|
||||
await db.commit()
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=f"AI模型调用失败: {extract_error_message(exc, '提示词')}",
|
||||
) from exc
|
||||
|
||||
async def _persist_optimized_result() -> str:
|
||||
existing_result = await db.execute(
|
||||
select(GenerationRecord)
|
||||
.where(GenerationRecord.id == record_id_value)
|
||||
.with_for_update()
|
||||
.limit(1)
|
||||
)
|
||||
record = existing_result.scalar_one_or_none()
|
||||
if record is None:
|
||||
record = GenerationRecord(
|
||||
id=record_id_value,
|
||||
user_id=user_id_snapshot,
|
||||
project_id=req.project_id,
|
||||
original_prompt=req.prompt,
|
||||
optimized_prompt=optimized,
|
||||
gen_type=req.gen_type.value,
|
||||
duration=req.duration if req.gen_type == GenerationType.video else None,
|
||||
aspect_ratio=req.aspect_ratio if req.gen_type == GenerationType.video else None,
|
||||
resolution=req.resolution if req.gen_type == GenerationType.video else None,
|
||||
image_size=req.image_size if req.gen_type == GenerationType.image else None,
|
||||
image_proportion=req.image_proportion if req.gen_type == GenerationType.image else None,
|
||||
image_px=req.image_px if req.gen_type == GenerationType.image else None,
|
||||
status="prompt_optimized",
|
||||
pipeline_stage=None,
|
||||
credits_cost=0,
|
||||
text_credits_cost=0,
|
||||
text_tokens_used=int(token_usage.get("total_tokens", 0) or 0),
|
||||
media_references=json.dumps(req.references, ensure_ascii=False) if req.references else None,
|
||||
include_media_references=bool(req.include_media_references),
|
||||
idempotency_key=req.idempotency_key,
|
||||
)
|
||||
db.add(record)
|
||||
else:
|
||||
# commit 结果不确定或本地持久化重试时,复用同一主键和同一账务 attempt。
|
||||
record.optimized_prompt = optimized
|
||||
record.status = "prompt_optimized"
|
||||
record.pipeline_stage = None
|
||||
record.error_message = None
|
||||
record.text_credits_cost = 0
|
||||
record.text_tokens_used = int(token_usage.get("total_tokens", 0) or 0)
|
||||
|
||||
if req.gen_type == GenerationType.video:
|
||||
from app.services.video_upscale.snapshot_service import build_video_upscale_snapshot
|
||||
|
||||
provider_resolution, upscale_enabled, upscale_snapshot_json = await build_video_upscale_snapshot(
|
||||
db,
|
||||
target_resolution=req.resolution,
|
||||
aspect_ratio=req.aspect_ratio,
|
||||
supported_provider_resolutions=parse_json_list(
|
||||
engine_snapshot_source.supported_resolutions,
|
||||
[],
|
||||
),
|
||||
)
|
||||
record.provider_generation_resolution = provider_resolution
|
||||
record.video_upscale_enabled_snapshot = upscale_enabled
|
||||
record.video_upscale_snapshot_json = upscale_snapshot_json
|
||||
else:
|
||||
record.provider_generation_resolution = None
|
||||
record.video_upscale_enabled_snapshot = False
|
||||
record.video_upscale_snapshot_json = None
|
||||
|
||||
freeze_generation_record_config_with_log(
|
||||
record,
|
||||
engine=engine_snapshot_source,
|
||||
source=GenerationRecordConfigSourceEnum.PROMPT_OPTIMIZE,
|
||||
)
|
||||
await db.flush()
|
||||
billing = await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description=f"提示词优化 - {project_name_snapshot}",
|
||||
)
|
||||
charge_item = next(
|
||||
(item for item in billing.items if item.biz_key == llm_billing_context.charge_biz_key),
|
||||
None,
|
||||
)
|
||||
if charge_item:
|
||||
record.text_credits_cost = round(charge_item.amount, 2)
|
||||
record_id_snapshot = str(record.id)
|
||||
await db.commit()
|
||||
return record_id_snapshot
|
||||
|
||||
try:
|
||||
record_id_snapshot = await _persist_optimized_result()
|
||||
except Exception as first_exc:
|
||||
await db.rollback()
|
||||
logger.exception(
|
||||
"prompt optimize local persistence/settlement failed after provider success; retry once: record_id=%s",
|
||||
record_id_value,
|
||||
)
|
||||
try:
|
||||
record_id_snapshot = await _persist_optimized_result()
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
logger.exception(
|
||||
"prompt optimize idempotent persistence retry failed; active HOLD retained for repair: record_id=%s",
|
||||
record_id_value,
|
||||
)
|
||||
raise first_exc
|
||||
|
||||
|
||||
refreshed = await db.execute(
|
||||
select(GenerationRecord, Project.name)
|
||||
.join(Project, GenerationRecord.project_id == Project.id)
|
||||
.where(GenerationRecord.id == record_id_snapshot)
|
||||
.where(
|
||||
GenerationRecord.id == service_result.record_id,
|
||||
GenerationRecord.user_id == user_id_snapshot,
|
||||
GenerationRecord.deleted_at.is_(None),
|
||||
Project.deleted_at.is_(None),
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
refreshed_row = refreshed.first()
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.enums.credit_record import (
|
||||
CreditRecordBillingScene,
|
||||
CreditRecordChargeKind,
|
||||
CreditRecordOwnerType,
|
||||
CreditRecordSourceStepCode,
|
||||
)
|
||||
from app.enums.llm_billing import LlmBillingConfigKey
|
||||
from app.enums.shot_replicate import (
|
||||
@@ -274,7 +275,7 @@ def _analysis_dispatch_billing_context(
|
||||
source_module=MODULE,
|
||||
source_project_id=task_set_id,
|
||||
source_step_id=owner_id,
|
||||
source_step_code=ShotReplicateStepCodeEnum.VIDEO_ANALYSIS.value,
|
||||
source_step_code=CreditRecordSourceStepCode.VIDEO_ANALYSIS.value,
|
||||
related_id=owner_id,
|
||||
hold_config_key=LlmBillingConfigKey.HOLD_SHOT_VIDEO_ANALYSIS.value,
|
||||
description_prefix=(
|
||||
@@ -569,7 +570,7 @@ async def create_shot_task_set(
|
||||
from app.tasks.shot_replicate_tasks import analyze_original_video
|
||||
|
||||
analyze_original_video.apply_async(
|
||||
args=[task_set_id],
|
||||
args=[task_set_id, analysis_attempt_no],
|
||||
queue=CeleryQueue.GEN_SHOT_ANALYSIS.value,
|
||||
countdown=0,
|
||||
task_id=celery_task_id,
|
||||
@@ -590,6 +591,7 @@ async def create_shot_task_set(
|
||||
db,
|
||||
current_user=_user_context(current_user),
|
||||
task_set_id=task_set_id,
|
||||
expected_attempt_no=analysis_attempt_no,
|
||||
error_message=f"拆镜分析任务投递失败: {exc}",
|
||||
)
|
||||
await db.commit()
|
||||
@@ -672,7 +674,7 @@ async def get_shot_task_set(
|
||||
"/task-sets/{task_set_id}/reanalyze",
|
||||
response_model=ShotReanalyzeOut,
|
||||
summary="重新投递原视频 AI 分析任务",
|
||||
description="用于处理原视频分析失败或待处理的异常数据;重置分析状态后重新投递 analyze_original_video。",
|
||||
description="仅用于重新处理原视频分析失败的数据;处理中、待处理或已完成状态均拒绝重复投递。",
|
||||
)
|
||||
async def reanalyze_task_set(
|
||||
task_set_id: str = Path(..., description="拆镜总任务集ID,即 shot_replicate_task_sets.id"),
|
||||
@@ -686,7 +688,6 @@ async def reanalyze_task_set(
|
||||
db,
|
||||
current_user=current_user,
|
||||
task_set_id=task_set_id,
|
||||
force=req.force,
|
||||
reason=req.reason,
|
||||
)
|
||||
analysis_attempt_no = int(out.analysis_attempt_no)
|
||||
@@ -729,7 +730,7 @@ async def reanalyze_task_set(
|
||||
from app.tasks.shot_replicate_tasks import analyze_original_video
|
||||
|
||||
analyze_original_video.apply_async(
|
||||
args=[task_set_id],
|
||||
args=[task_set_id, analysis_attempt_no],
|
||||
queue=CeleryQueue.GEN_SHOT_ANALYSIS.value,
|
||||
countdown=0,
|
||||
task_id=celery_task_id,
|
||||
@@ -758,6 +759,7 @@ async def reanalyze_task_set(
|
||||
db,
|
||||
current_user=_user_context(current_user),
|
||||
task_set_id=task_set_id,
|
||||
expected_attempt_no=analysis_attempt_no,
|
||||
error_message=f"原视频再次分析任务投递失败: {exc}",
|
||||
)
|
||||
await db.commit()
|
||||
@@ -923,7 +925,7 @@ async def get_segment(
|
||||
"/segments/{segment_id}/reanalyze",
|
||||
response_model=ShotReanalyzeOut,
|
||||
summary="重新投递切片视频 AI 分析任务",
|
||||
description="用于处理自定义切片视频分析失败或待处理的异常数据;重置分析状态后重新投递 analyze_custom_segment_video。",
|
||||
description="仅用于重新处理自定义切片视频分析失败的数据;处理中、待处理或已完成状态均拒绝重复投递。",
|
||||
)
|
||||
async def reanalyze_segment(
|
||||
segment_id: str = Path(..., description="拆镜片段ID,即 shot_replicate_segments.id"),
|
||||
@@ -937,7 +939,6 @@ async def reanalyze_segment(
|
||||
db,
|
||||
current_user=current_user,
|
||||
segment_id=segment_id,
|
||||
force=req.force,
|
||||
reason=req.reason,
|
||||
)
|
||||
task_set_id = str(out.task_set_id)
|
||||
@@ -981,7 +982,7 @@ async def reanalyze_segment(
|
||||
from app.tasks.shot_replicate_tasks import analyze_custom_segment_video
|
||||
|
||||
analyze_custom_segment_video.apply_async(
|
||||
args=[segment_id],
|
||||
args=[segment_id, analysis_attempt_no],
|
||||
queue=CeleryQueue.GEN_SHOT_ANALYSIS.value,
|
||||
countdown=0,
|
||||
task_id=celery_task_id,
|
||||
@@ -1012,6 +1013,7 @@ async def reanalyze_segment(
|
||||
db,
|
||||
current_user=_user_context(current_user),
|
||||
segment_id=segment_id,
|
||||
expected_attempt_no=analysis_attempt_no,
|
||||
error_message=f"切片视频再次分析任务投递失败: {exc}",
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
Reference in New Issue
Block a user