爆款开头API开发完成文件追加
This commit is contained in:
@@ -15,6 +15,7 @@ from app.api.v1.recharge_packages import router as recharge_packages_router
|
||||
from app.api.v1.video_engines import router as video_engines_router
|
||||
from app.api.v1.image_engines import router as image_engines_router
|
||||
from app.api.v1.generation_ai import router as generation_ai_router
|
||||
from app.api.v1.hot_opening_replicate import router as hot_opening_replicate_router
|
||||
from app.api.v1.test import router as test_router
|
||||
from app.api.v1.user_oauth import router as user_oauth_router
|
||||
from app.api.v1.user_oauth_app import router as user_oauth_app_router
|
||||
@@ -35,6 +36,7 @@ api_router.include_router(recharge_packages_router)
|
||||
api_router.include_router(video_engines_router)
|
||||
api_router.include_router(image_engines_router)
|
||||
api_router.include_router(generation_ai_router)
|
||||
api_router.include_router(hot_opening_replicate_router)
|
||||
api_router.include_router(test_router)
|
||||
api_router.include_router(user_oauth_router)
|
||||
api_router.include_router(user_oauth_app_router)
|
||||
|
||||
@@ -87,7 +87,7 @@ class Settings(BaseSettings):
|
||||
# ChatAPI async generation pipeline settings
|
||||
CELERY_BROKER_URL: str = ""
|
||||
CELERY_RESULT_BACKEND: str = ""
|
||||
CHATAPI_REQUEST_TIMEOUT_SECONDS: int = 120
|
||||
CHATAPI_REQUEST_TIMEOUT_SECONDS: int = 180
|
||||
CHATAPI_VIDEO_FPS: float = 0.5
|
||||
CHATAPI_ASYNC_MAX_RETRIES: int = 3
|
||||
CHATAPI_ASYNC_RETRY_BACKOFF_SECONDS: int = 30
|
||||
@@ -128,5 +128,11 @@ class Settings(BaseSettings):
|
||||
RESOURCE_SIGN_ARG_EXPIRE: str = "exp"
|
||||
RESOURCE_SIGN_ARG_SIGNATURE: str = "sign"
|
||||
|
||||
# 爆款开头复刻默认配置。素材校验由前端完成,后端只接收已有上传接口返回的链接。
|
||||
HOT_OPENING_DEFAULT_VIDEO_DURATION: int = 4
|
||||
HOT_OPENING_DEFAULT_VIDEO_RATIO: str = "9:16"
|
||||
HOT_OPENING_DEFAULT_VIDEO_RESOLUTION: str = "480p"
|
||||
HOT_OPENING_DEFAULT_TARGET_PLATFORM: str = "抖音"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -21,6 +21,8 @@ from app.models.chat_provider_call_log import ChatProviderCallLog
|
||||
from app.models.generated_resource import GeneratedResource
|
||||
from app.models.user_resource_month_stat import UserResourceMonthStat
|
||||
from app.models.user_resource_total_stat import UserResourceTotalStat
|
||||
from app.models.module_generation_project import ModuleGenerationProject
|
||||
from app.models.module_generation_step import ModuleGenerationStep
|
||||
from app.models.user_oauth import UserOAuth
|
||||
from app.models.user_oauth_account import UserOAuthAccount
|
||||
from app.models.user_oauth_app import UserOAuthApp
|
||||
@@ -34,5 +36,6 @@ __all__ = [
|
||||
"MenuConfig", "RechargePackage", "OperationLog",
|
||||
"ChatGenerationTask", "ChatGenerationTaskEvent", "ChatProviderCallLog",
|
||||
"GeneratedResource", "UserResourceMonthStat", "UserResourceTotalStat",
|
||||
"ModuleGenerationProject", "ModuleGenerationStep",
|
||||
"UserOAuth", "UserOAuthAccount", "UserOAuthApp",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
|
||||
@@ -18,7 +18,14 @@ class ChatGenerationTask(Base, TimestampMixin, SoftDeleteMixin):
|
||||
__table_args__ = (
|
||||
# 防止前端按钮连点/网络重试时同一个 idempotency_key 并发创建多条任务。
|
||||
# nullable unique 兼容不传 idempotency_key 的普通请求。
|
||||
Index("uq_chat_generation_tasks_user_mode_idempotency", "user_id", "generation_mode", "idempotency_key", unique=True),
|
||||
Index(
|
||||
"uq_chat_generation_tasks_user_mode_idempotency",
|
||||
"user_id",
|
||||
"generation_mode",
|
||||
"idempotency_key",
|
||||
unique=True,
|
||||
postgresql_where=text("deleted_at IS NULL AND idempotency_key IS NOT NULL"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ CHARGE_MEDIA = "media"
|
||||
|
||||
OWNER_GENERATION_RECORD = "generation_record"
|
||||
OWNER_CHAT_GENERATION_TASK = "chat_generation_task"
|
||||
OWNER_MODULE_GENERATION_STEP = "module_generation_step"
|
||||
|
||||
_BIZ_KEY_PATTERN = re.compile(
|
||||
r"^(?P<owner_type>[^:]+):(?P<owner_id>[^:]+):attempt:(?P<attempt_no>\d+):(?P<charge_kind>[^:]+):(?P<action>charge|refund)$"
|
||||
@@ -287,6 +288,45 @@ async def charge_chatapi_prompt_usage(
|
||||
return BillingSummary(record_id=record.id, user_id=record.user_id, items=items)
|
||||
|
||||
|
||||
async def charge_module_prompt_usage(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
user_id: str,
|
||||
step_id: str,
|
||||
usage: Mapping[str, Any],
|
||||
description: str,
|
||||
attempt_no: int = 1,
|
||||
) -> BillingSummary:
|
||||
"""爆款开头复刻模块图片/视频 AI 提词扣文本积分。
|
||||
|
||||
文本提词属于已经发生的 LLM 消费:
|
||||
- 调用成功后按 input_tokens + output_tokens 扣费。
|
||||
- 不参与后续图片/视频媒体生成失败退款。
|
||||
- 通过 module_generation_step:{step_id}:attempt:1:text_prompt:charge 幂等。
|
||||
"""
|
||||
input_tokens = _safe_int(usage.get("input_tokens"))
|
||||
output_tokens = _safe_int(usage.get("output_tokens"))
|
||||
text_credits = await calc_text_credits(db, input_tokens, output_tokens)
|
||||
biz_key = build_credit_biz_key(
|
||||
owner_type=OWNER_MODULE_GENERATION_STEP,
|
||||
owner_id=step_id,
|
||||
attempt_no=attempt_no,
|
||||
charge_kind=CHARGE_TEXT_PROMPT,
|
||||
action="charge",
|
||||
)
|
||||
item = await deduct_credits_locked_once(
|
||||
db,
|
||||
user_id=user_id,
|
||||
amount=text_credits,
|
||||
description=description,
|
||||
related_id=step_id,
|
||||
charge_key=CHARGE_TEXT_PROMPT,
|
||||
biz_key=biz_key,
|
||||
attempt_no=attempt_no,
|
||||
)
|
||||
return BillingSummary(record_id=step_id, user_id=user_id, items=[item])
|
||||
|
||||
|
||||
async def charge_generation_media_by_params(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
|
||||
@@ -17,10 +17,13 @@ from app.services.celery_download_recovery_service import (
|
||||
remove_download_active,
|
||||
)
|
||||
from app.services.generation_log_service import log_task_event
|
||||
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
|
||||
from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once
|
||||
|
||||
logger = logging.getLogger("video_gen")
|
||||
|
||||
ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate"}
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
@@ -73,7 +76,7 @@ async def recover_one_download_task(
|
||||
|
||||
if not task:
|
||||
return "skip_missing_task"
|
||||
if task.generation_mode != "chatapi_async":
|
||||
if task.generation_mode not in ALLOWED_GENERATION_MODES:
|
||||
await remove_download_active(task.id)
|
||||
return "clean_invalid_mode"
|
||||
if _is_final_task_state(task):
|
||||
@@ -218,7 +221,7 @@ async def recover_download_tasks_once(db: AsyncSession) -> dict[str, Any]:
|
||||
select(ChatGenerationTask)
|
||||
.where(
|
||||
ChatGenerationTask.deleted_at.is_(None),
|
||||
ChatGenerationTask.generation_mode == "chatapi_async",
|
||||
ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate"]),
|
||||
ChatGenerationTask.status == "generating",
|
||||
ChatGenerationTask.remote_result_url.is_not(None),
|
||||
ChatGenerationTask.pipeline_stage.in_(
|
||||
@@ -263,7 +266,7 @@ async def recover_generation_tasks_once(db: AsyncSession) -> dict[str, Any]:
|
||||
select(ChatGenerationTask)
|
||||
.where(
|
||||
ChatGenerationTask.deleted_at.is_(None),
|
||||
ChatGenerationTask.generation_mode == "chatapi_async",
|
||||
ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate"]),
|
||||
ChatGenerationTask.status == "generating",
|
||||
ChatGenerationTask.pipeline_stage.in_(
|
||||
[
|
||||
@@ -290,6 +293,7 @@ async def recover_generation_tasks_once(db: AsyncSession) -> dict[str, Any]:
|
||||
error_message="任务超时",
|
||||
pipeline_stage="timeout",
|
||||
)
|
||||
await notify_chat_generation_task_finished(db, task)
|
||||
await db.commit()
|
||||
await log_task_event(
|
||||
task,
|
||||
|
||||
@@ -180,7 +180,7 @@ async def mark_chat_generation_task_failed_and_refund_once(
|
||||
select(ChatGenerationTask)
|
||||
.where(
|
||||
ChatGenerationTask.id == task_id,
|
||||
ChatGenerationTask.generation_mode == "chatapi_async",
|
||||
ChatGenerationTask.generation_mode.in_(["chatapi_async", "hot_opening_replicate"]),
|
||||
ChatGenerationTask.deleted_at.is_(None),
|
||||
)
|
||||
.with_for_update()
|
||||
|
||||
@@ -10,6 +10,7 @@ try:
|
||||
generation_poll_tasks,
|
||||
generation_download_tasks,
|
||||
generation_recovery_tasks,
|
||||
hot_opening_replicate_tasks
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -49,6 +49,8 @@ if broker_url:
|
||||
"generation.chatapi_create_generation_task": {"queue": "gen_chatapi_create"},
|
||||
"generation.poll_generation_task": {"queue": "gen_provider_poll"},
|
||||
"generation.download_generation_result_task": {"queue": "gen_result_download"},
|
||||
"hot_opening.start_image_prompt_optimize": {"queue": "gen_chatapi_create"},
|
||||
"hot_opening.start_video_prompt_optimize": {"queue": "gen_chatapi_create"},
|
||||
"generation.recover_download_tasks_once": {"queue": "gen_result_download"},
|
||||
"generation.recover_generation_tasks_once": {"queue": "gen_result_download"},
|
||||
"app.tasks.cleanup.*": {"queue": "default"},
|
||||
|
||||
@@ -13,6 +13,8 @@ from app.services.generation_refund_service import mark_chat_generation_task_fai
|
||||
from app.services.generation_provider_service import create_provider_task
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate"}
|
||||
|
||||
|
||||
def _get_first_value(obj: Any, *field_names: str) -> Optional[Any]:
|
||||
"""
|
||||
@@ -63,6 +65,14 @@ def _build_optimized_prompt_by_params(task: ChatGenerationTask) -> str:
|
||||
base_prompt = original_prompt.rstrip(",,。;; \n\t")
|
||||
|
||||
gen_type = (_to_clean_str(getattr(task, "gen_type", None)) or "").lower()
|
||||
generation_mode = _to_clean_str(getattr(task, "generation_mode", None)) or ""
|
||||
|
||||
# 爆款开头复刻第5步的视频生成,original_prompt 已经是视频提词 JSON schema。
|
||||
# 不能再追加“时长/比例/分辨率”中文参数,否则会污染 schema。
|
||||
if generation_mode == "hot_opening_replicate" and gen_type == "video":
|
||||
stripped = base_prompt.strip()
|
||||
if stripped.startswith("{") or stripped.startswith("["):
|
||||
return base_prompt
|
||||
|
||||
duration = _get_first_value(task, "duration")
|
||||
aspect_ratio = _get_first_value(task, "aspect_ratio")
|
||||
@@ -113,7 +123,7 @@ async def _run(task_id: str):
|
||||
).with_for_update().limit(1))
|
||||
task = result.scalar_one_or_none()
|
||||
|
||||
if not task or task.generation_mode != "chatapi_async":
|
||||
if not task or task.generation_mode not in ALLOWED_GENERATION_MODES:
|
||||
return
|
||||
|
||||
if task.status != "generating":
|
||||
@@ -128,6 +138,9 @@ async def _run(task_id: str):
|
||||
)
|
||||
await db.commit()
|
||||
await log_task_event(task, event_type="TASK_TIMEOUT", to_status="failed", to_stage="timeout")
|
||||
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
|
||||
await notify_chat_generation_task_finished(db, task)
|
||||
await db.commit()
|
||||
return
|
||||
|
||||
if task.pipeline_stage not in ("queued", "preparing", "creating_provider_task"):
|
||||
@@ -253,6 +266,9 @@ async def _run(task_id: str):
|
||||
)
|
||||
await db.commit()
|
||||
await log_task_event(task, event_type="TASK_FAILED", message=task.error_message)
|
||||
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
|
||||
await notify_chat_generation_task_finished(db, task)
|
||||
await db.commit()
|
||||
|
||||
|
||||
if celery_app:
|
||||
|
||||
@@ -21,6 +21,8 @@ from app.services.generation_refund_service import mark_chat_generation_task_fai
|
||||
from app.services.resource_accounting_service import record_chat_task_generated_resource
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate"}
|
||||
|
||||
DOWNLOAD_QUEUE = "gen_result_download"
|
||||
DOWNLOAD_STAGE_QUEUED = "download_queued"
|
||||
DOWNLOAD_STAGE_DOWNLOADING = "downloading"
|
||||
@@ -108,7 +110,7 @@ async def enqueue_download_task(
|
||||
countdown: int | None = None,
|
||||
) -> str | None:
|
||||
"""统一投递图片/视频下载任务,并同步 DB + Redis active 注册表。"""
|
||||
if not task or task.generation_mode != "chatapi_async":
|
||||
if not task or task.generation_mode not in ALLOWED_GENERATION_MODES:
|
||||
return None
|
||||
if task.status != "generating":
|
||||
return None
|
||||
@@ -159,7 +161,7 @@ async def _reload_task(db: AsyncSession, task_id: str) -> ChatGenerationTask | N
|
||||
async def _claim_download_lease(db: AsyncSession, task: ChatGenerationTask) -> bool:
|
||||
now = _now()
|
||||
|
||||
if not task or task.generation_mode != "chatapi_async":
|
||||
if not task or task.generation_mode not in ALLOWED_GENERATION_MODES:
|
||||
return False
|
||||
if task.status != "generating":
|
||||
return False
|
||||
@@ -310,6 +312,10 @@ async def _run(task_id: str):
|
||||
await db.commit()
|
||||
await remove_download_active(task.id)
|
||||
|
||||
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
|
||||
await notify_chat_generation_task_finished(db, task)
|
||||
await db.commit()
|
||||
|
||||
await log_task_event(
|
||||
task,
|
||||
event_type="DOWNLOAD_SUCCESS",
|
||||
@@ -348,6 +354,10 @@ async def _run(task_id: str):
|
||||
|
||||
await remove_download_active(task.id)
|
||||
|
||||
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
|
||||
await notify_chat_generation_task_finished(db, task)
|
||||
await db.commit()
|
||||
|
||||
await log_task_event(
|
||||
task,
|
||||
event_type="DOWNLOAD_FAILED",
|
||||
|
||||
@@ -13,6 +13,8 @@ from app.services.generation_refund_service import mark_chat_generation_task_fai
|
||||
from app.services.generation_provider_service import poll_provider_task
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate"}
|
||||
|
||||
|
||||
def _is_success(status: str) -> bool:
|
||||
return status in ("succeeded", "success", "completed", "done")
|
||||
@@ -29,6 +31,12 @@ def _engine_snapshot(task: ChatGenerationTask) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
async def _notify_finished(db, task: ChatGenerationTask) -> None:
|
||||
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
|
||||
|
||||
await notify_chat_generation_task_finished(db, task)
|
||||
|
||||
|
||||
async def _reload_task(db, task_id: str) -> ChatGenerationTask | None:
|
||||
"""
|
||||
rollback 后重新查询任务对象。
|
||||
@@ -54,7 +62,7 @@ async def _run(task_id: str):
|
||||
ChatGenerationTask.deleted_at.is_(None),
|
||||
).with_for_update().limit(1))
|
||||
task = result.scalar_one_or_none()
|
||||
if not task or task.generation_mode != "chatapi_async":
|
||||
if not task or task.generation_mode not in ALLOWED_GENERATION_MODES:
|
||||
return
|
||||
|
||||
# 只处理正在生成,且处于远程等待/轮询中的任务。
|
||||
@@ -68,6 +76,7 @@ async def _run(task_id: str):
|
||||
error_message="任务轮询超时",
|
||||
pipeline_stage="timeout",
|
||||
)
|
||||
await _notify_finished(db, task)
|
||||
await db.commit()
|
||||
await log_task_event(task, event_type="TASK_TIMEOUT", to_status="failed", to_stage="timeout")
|
||||
return
|
||||
@@ -79,6 +88,7 @@ async def _run(task_id: str):
|
||||
error_message="缺少外部任务ID",
|
||||
pipeline_stage="failed",
|
||||
)
|
||||
await _notify_finished(db, task)
|
||||
await db.commit()
|
||||
await log_task_event(task, event_type="POLL_FAILED", message=task.error_message)
|
||||
return
|
||||
@@ -130,6 +140,7 @@ async def _run(task_id: str):
|
||||
error_message="供应商任务成功但未返回结果URL",
|
||||
pipeline_stage="failed",
|
||||
)
|
||||
await _notify_finished(db, task)
|
||||
await db.commit()
|
||||
await log_task_event(task, event_type="POLL_FAILED", message=task.error_message)
|
||||
return
|
||||
@@ -153,6 +164,7 @@ async def _run(task_id: str):
|
||||
error_message=poll_result.get("error") or f"供应商任务失败: {status}",
|
||||
pipeline_stage="failed",
|
||||
)
|
||||
await _notify_finished(db, task)
|
||||
await db.commit()
|
||||
await log_task_event(task, event_type="POLL_FAILED", message=task.error_message, detail=poll_result)
|
||||
return
|
||||
@@ -194,6 +206,7 @@ async def _run(task_id: str):
|
||||
error_message=error_message,
|
||||
pipeline_stage="failed",
|
||||
)
|
||||
await _notify_finished(db, task)
|
||||
await db.commit()
|
||||
await log_task_event(task, event_type="POLL_FAILED", message=task.error_message)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user