修复chat生成参数提交错误BUG|模型引擎积分设置关联开发优化成功|视频/图片生成引擎API开发完成
This commit is contained in:
@@ -651,6 +651,30 @@ async def delete_image_engine(
|
||||
return {"message": "ok"}
|
||||
|
||||
|
||||
|
||||
|
||||
async def _validate_credit_ratio_engine(db: AsyncSession, req: CreditRatioCreate) -> None:
|
||||
"""校验积分规则绑定的引擎是否存在。
|
||||
|
||||
CreditRatio.model_config_id 为兼容旧字段名,当前实际保存引擎ID:
|
||||
- gen_type=image 时对应 image_engines.id
|
||||
- gen_type=video 时对应 video_engines.id
|
||||
"""
|
||||
gen_type = (req.gen_type or "").lower().strip()
|
||||
engine_id = (req.model_config_id or "").strip()
|
||||
if gen_type not in ("image", "video"):
|
||||
raise HTTPException(status_code=400, detail="gen_type 仅支持 image 或 video")
|
||||
if not engine_id:
|
||||
raise HTTPException(status_code=400, detail="model_config_id 不能为空,当前字段用于保存图片/视频引擎ID")
|
||||
|
||||
model = ImageEngine if gen_type == "image" else VideoEngine
|
||||
result = await db.execute(select(model).where(model.id == engine_id).limit(1))
|
||||
engine = result.scalar_one_or_none()
|
||||
if not engine:
|
||||
detail = "图片积分规则绑定的图片引擎不存在" if gen_type == "image" else "视频积分规则绑定的视频引擎不存在"
|
||||
raise HTTPException(status_code=400, detail=detail)
|
||||
|
||||
|
||||
# ── Credit Ratio ─────────────────────────────────────────
|
||||
|
||||
@router.get("/credit-ratios", response_model=list[CreditRatioOut])
|
||||
@@ -668,7 +692,11 @@ async def create_credit_ratio(
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
ratio = CreditRatio(id=generate_id(), **req.model_dump())
|
||||
await _validate_credit_ratio_engine(db, req)
|
||||
data = req.model_dump()
|
||||
data["gen_type"] = data["gen_type"].lower().strip()
|
||||
data["model_config_id"] = data["model_config_id"].strip()
|
||||
ratio = CreditRatio(id=generate_id(), **data)
|
||||
db.add(ratio)
|
||||
await db.flush()
|
||||
return ratio
|
||||
@@ -687,7 +715,11 @@ async def update_credit_ratio(
|
||||
ratio = result.scalar_one_or_none()
|
||||
if not ratio:
|
||||
raise HTTPException(status_code=404, detail="积分比例不存在")
|
||||
for k, v in req.model_dump().items():
|
||||
await _validate_credit_ratio_engine(db, req)
|
||||
data = req.model_dump()
|
||||
data["gen_type"] = data["gen_type"].lower().strip()
|
||||
data["model_config_id"] = data["model_config_id"].strip()
|
||||
for k, v in data.items():
|
||||
setattr(ratio, k, v)
|
||||
await db.flush()
|
||||
return ratio
|
||||
|
||||
@@ -6,6 +6,7 @@ from app.dependencies import get_current_user, get_db
|
||||
from app.models.chat_generation_task import ChatGenerationTask
|
||||
from app.models.user import User
|
||||
from app.schemas.generation_ai import (
|
||||
GenerationAIEngineOptionsOut,
|
||||
GenerationAIHistoryDayItemsOut,
|
||||
GenerationAIHistoryGroupedOut,
|
||||
GenerationAIRetryOut,
|
||||
@@ -15,6 +16,7 @@ from app.schemas.generation_ai import (
|
||||
)
|
||||
from app.services.generation_ai_service import (
|
||||
create_async_generation_task,
|
||||
list_generation_ai_engine_options,
|
||||
list_async_generation_tasks,
|
||||
list_generation_history_day_items,
|
||||
list_generation_history_grouped_days,
|
||||
@@ -29,6 +31,70 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/engines",
|
||||
response_model=GenerationAIEngineOptionsOut,
|
||||
summary="获取AI图片/视频可用引擎列表",
|
||||
description=(
|
||||
"获取当前启用状态的图片生成引擎和视频生成引擎。"
|
||||
"返回格式为 engine.image 和 engine.video 两个数组。"
|
||||
"前端创建 /generation-ai/tasks 任务时,可以把对应引擎 id 作为 engine_id 传入。"
|
||||
"该接口只返回前端需要展示和选择的模型能力信息,不返回 api_key 等敏感配置。"
|
||||
),
|
||||
responses={
|
||||
200: {
|
||||
"description": "查询成功,返回当前启用的图片/视频生成引擎列表",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"engine": {
|
||||
"image": [
|
||||
{
|
||||
"id": "image_engine_xxx",
|
||||
"name": "豆包文生图",
|
||||
"provider": "ark",
|
||||
"model_name": "doubao-seedream-5-0-260128",
|
||||
"supported_models": ["doubao-seedream-5-0-260128"],
|
||||
"supported_sizes": {
|
||||
"2K": {
|
||||
"1:1": "2048x2048",
|
||||
"16:9": "2560x1440",
|
||||
}
|
||||
},
|
||||
"default_size": "2K",
|
||||
"priority": 10,
|
||||
}
|
||||
],
|
||||
"video": [
|
||||
{
|
||||
"id": "video_engine_xxx",
|
||||
"name": "Seedance 2.0",
|
||||
"provider": "ark",
|
||||
"model_name": "doubao-seedance-2-0-260128",
|
||||
"supported_ratios": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"],
|
||||
"supported_resolutions": ["480p", "720p", "1080p"],
|
||||
"supported_durations": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
"max_duration": 15,
|
||||
"priority": 10,
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
401: {
|
||||
"description": "未登录或 Token 无效",
|
||||
},
|
||||
},
|
||||
)
|
||||
async def list_engines(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await list_generation_ai_engine_options(db)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/tasks",
|
||||
response_model=GenerationAITaskOut,
|
||||
|
||||
+61
-17
@@ -208,26 +208,70 @@ async def _seed_data():
|
||||
)
|
||||
)
|
||||
|
||||
# Seed credit ratios - use first model config if available
|
||||
model_result = await db.execute(select(ModelConfig).limit(1))
|
||||
model = model_result.scalar_one_or_none()
|
||||
if model:
|
||||
existing_ratio = await db.execute(
|
||||
select(CreditRatio).where(CreditRatio.model_config_id == model.id).limit(1)
|
||||
)
|
||||
if not existing_ratio.scalars().first():
|
||||
for gen_type, resolution, ratio_val, base, per_sec in [
|
||||
("video", "480p", 1.0, 60, 2),
|
||||
("video", "720p", 1.0, 80, 2),
|
||||
("video", "1080p", 1.5, 120, 3),
|
||||
("image", "2K", 1.0, 4, 0),
|
||||
("image", "4K", 1.0, 6, 0),
|
||||
]:
|
||||
# Seed credit ratios - model_config_id is kept as a compatible field name,
|
||||
# but now stores the actual engine id:
|
||||
# - gen_type=video -> video_engines.id
|
||||
# - gen_type=image -> image_engines.id
|
||||
await db.flush()
|
||||
|
||||
default_video_engine_result = await db.execute(
|
||||
select(VideoEngine)
|
||||
.where(VideoEngine.is_active == True)
|
||||
.order_by(VideoEngine.priority.desc(), VideoEngine.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
default_video_engine = default_video_engine_result.scalar_one_or_none()
|
||||
if default_video_engine:
|
||||
for resolution, ratio_val, base, per_sec in [
|
||||
("480p", 1.0, 60, 2),
|
||||
("720p", 1.0, 80, 2),
|
||||
("1080p", 1.5, 120, 3),
|
||||
]:
|
||||
existing_ratio = await db.execute(
|
||||
select(CreditRatio)
|
||||
.where(CreditRatio.model_config_id == default_video_engine.id)
|
||||
.where(CreditRatio.gen_type == "video")
|
||||
.where(CreditRatio.resolution == resolution)
|
||||
.limit(1)
|
||||
)
|
||||
if not existing_ratio.scalar_one_or_none():
|
||||
db.add(
|
||||
CreditRatio(
|
||||
id=generate_id(),
|
||||
model_config_id=model.id,
|
||||
gen_type=gen_type,
|
||||
model_config_id=default_video_engine.id,
|
||||
gen_type="video",
|
||||
resolution=resolution,
|
||||
ratio=ratio_val,
|
||||
base_credits=base,
|
||||
per_second_credits=per_sec,
|
||||
)
|
||||
)
|
||||
|
||||
default_image_engine_result = await db.execute(
|
||||
select(ImageEngine)
|
||||
.where(ImageEngine.is_active == True)
|
||||
.order_by(ImageEngine.priority.desc(), ImageEngine.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
default_image_engine = default_image_engine_result.scalar_one_or_none()
|
||||
if default_image_engine:
|
||||
for resolution, ratio_val, base, per_sec in [
|
||||
("2K", 1.0, 4, 0),
|
||||
("4K", 1.0, 6, 0),
|
||||
]:
|
||||
existing_ratio = await db.execute(
|
||||
select(CreditRatio)
|
||||
.where(CreditRatio.model_config_id == default_image_engine.id)
|
||||
.where(CreditRatio.gen_type == "image")
|
||||
.where(CreditRatio.resolution == resolution)
|
||||
.limit(1)
|
||||
)
|
||||
if not existing_ratio.scalar_one_or_none():
|
||||
db.add(
|
||||
CreditRatio(
|
||||
id=generate_id(),
|
||||
model_config_id=default_image_engine.id,
|
||||
gen_type="image",
|
||||
resolution=resolution,
|
||||
ratio=ratio_val,
|
||||
base_credits=base,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Float, ForeignKey, Integer, String
|
||||
from sqlalchemy import Float, Index, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
@@ -6,13 +6,19 @@ from app.models.base import Base, TimestampMixin
|
||||
|
||||
class CreditRatio(Base, TimestampMixin):
|
||||
__tablename__ = "credit_ratios"
|
||||
__table_args__ = (
|
||||
Index("ix_credit_ratios_gen_type_engine_resolution", "gen_type", "model_config_id", "resolution"),
|
||||
Index("ix_credit_ratios_gen_type_resolution", "gen_type", "resolution"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
model_config_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("model_configs.id")
|
||||
)
|
||||
gen_type: Mapped[str] = mapped_column(String(16), default="video")
|
||||
resolution: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
# 兼容旧字段名:
|
||||
# gen_type=image 时,该字段保存 image_engines.id;
|
||||
# gen_type=video 时,该字段保存 video_engines.id。
|
||||
# 不再通过数据库外键绑定 model_configs.id,避免同一字段无法同时关联图片/视频引擎表。
|
||||
model_config_id: Mapped[str] = mapped_column(String(32), index=True)
|
||||
gen_type: Mapped[str] = mapped_column(String(16), default="video", index=True)
|
||||
resolution: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
|
||||
ratio: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
base_credits: Mapped[float] = mapped_column(Float, default=80.0)
|
||||
per_second_credits: Mapped[float] = mapped_column(Float, default=2.0)
|
||||
|
||||
@@ -1,19 +1,52 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.schemas.common import NaiveDatetime
|
||||
|
||||
|
||||
class CreditRatioCreate(BaseModel):
|
||||
model_config_id: str = Field(..., max_length=32)
|
||||
gen_type: str = Field(default="video", max_length=16)
|
||||
resolution: str = Field(..., max_length=16)
|
||||
ratio: float = Field(..., gt=0)
|
||||
base_credits: float = Field(default=80.0, ge=0)
|
||||
per_second_credits: float = Field(default=2.0, ge=0)
|
||||
model_config_id: str = Field(
|
||||
...,
|
||||
max_length=32,
|
||||
description=(
|
||||
"引擎ID,兼容旧字段名。gen_type=image 时填写 image_engines.id;"
|
||||
"gen_type=video 时填写 video_engines.id。当前不再绑定 model_configs.id 外键"
|
||||
),
|
||||
examples=["engine_xxx"],
|
||||
)
|
||||
gen_type: str = Field(
|
||||
default="video",
|
||||
max_length=16,
|
||||
description="生成类型:image=图片积分规则,video=视频积分规则",
|
||||
examples=["video"],
|
||||
)
|
||||
resolution: str = Field(
|
||||
...,
|
||||
max_length=16,
|
||||
description="计费参数。视频为分辨率,例如 480p/720p/1080p;图片为分辨率档位,例如 2K/4K",
|
||||
examples=["720p"],
|
||||
)
|
||||
ratio: float = Field(
|
||||
...,
|
||||
gt=0,
|
||||
description="积分倍率。最终扣费会乘以该倍率",
|
||||
examples=[1.0],
|
||||
)
|
||||
base_credits: float = Field(
|
||||
default=80.0,
|
||||
ge=0,
|
||||
description="基础积分。视频按 base_credits + per_second_credits * duration 后再乘 ratio;图片按 base_credits * ratio",
|
||||
examples=[80.0],
|
||||
)
|
||||
per_second_credits: float = Field(
|
||||
default=2.0,
|
||||
ge=0,
|
||||
description="视频每秒积分。图片规则通常为 0",
|
||||
examples=[2.0],
|
||||
)
|
||||
|
||||
|
||||
class CreditRatioOut(CreditRatioCreate):
|
||||
id: str
|
||||
created_at: NaiveDatetime
|
||||
id: str = Field(..., description="积分规则ID")
|
||||
created_at: NaiveDatetime = Field(..., description="创建时间")
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -152,6 +152,96 @@ class GenerationAITaskCreate(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
class GenerationAIImageEngineOptionOut(BaseModel):
|
||||
"""AI图片生成可用引擎响应项。"""
|
||||
|
||||
id: str = Field(..., description="图片引擎ID,创建图片任务时传入 engine_id")
|
||||
name: str = Field(..., description="图片引擎名称,前端展示用")
|
||||
provider: str = Field(..., description="服务商标识,例如 ark")
|
||||
model_name: str | None = Field(None, description="服务商模型名称")
|
||||
supported_models: list[str] = Field(default_factory=list, description="该图片引擎支持的模型名称列表")
|
||||
supported_sizes: dict = Field(
|
||||
default_factory=dict,
|
||||
description="支持的图片尺寸映射。第一层为分辨率档位,例如 2K/4K;第二层为画布比例,例如 1:1/16:9;值为像素尺寸",
|
||||
)
|
||||
default_size: str | None = Field(None, description="默认图片分辨率档位,例如 2K")
|
||||
priority: int = Field(0, description="引擎优先级,数值越大越优先")
|
||||
|
||||
|
||||
class GenerationAIVideoEngineOptionOut(BaseModel):
|
||||
"""AI视频生成可用引擎响应项。"""
|
||||
|
||||
id: str = Field(..., description="视频引擎ID,创建视频任务时传入 engine_id")
|
||||
name: str = Field(..., description="视频引擎名称,前端展示用")
|
||||
provider: str = Field(..., description="服务商标识,例如 ark")
|
||||
model_name: str | None = Field(None, description="服务商模型名称")
|
||||
supported_ratios: list[str] = Field(default_factory=list, description="支持的视频画面比例,例如 16:9、9:16、1:1")
|
||||
supported_resolutions: list[str] = Field(default_factory=list, description="支持的视频分辨率,例如 480p、720p、1080p")
|
||||
supported_durations: list[int] = Field(default_factory=list, description="支持的视频时长列表,单位秒")
|
||||
max_duration: int | None = Field(None, description="最大视频时长,单位秒")
|
||||
priority: int = Field(0, description="引擎优先级,数值越大越优先")
|
||||
|
||||
|
||||
class GenerationAIEngineGroupOut(BaseModel):
|
||||
"""AI图片/视频引擎分组。"""
|
||||
|
||||
image: list[GenerationAIImageEngineOptionOut] = Field(
|
||||
default_factory=list,
|
||||
description="当前启用的图片生成引擎列表",
|
||||
)
|
||||
video: list[GenerationAIVideoEngineOptionOut] = Field(
|
||||
default_factory=list,
|
||||
description="当前启用的视频生成引擎列表",
|
||||
)
|
||||
|
||||
|
||||
class GenerationAIEngineOptionsOut(BaseModel):
|
||||
"""AI图片/视频生成引擎列表响应体。"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"engine": {
|
||||
"image": [
|
||||
{
|
||||
"id": "image_engine_xxx",
|
||||
"name": "豆包文生图",
|
||||
"provider": "ark",
|
||||
"model_name": "doubao-seedream-5-0-260128",
|
||||
"supported_models": ["doubao-seedream-5-0-260128"],
|
||||
"supported_sizes": {
|
||||
"2K": {
|
||||
"1:1": "2048x2048",
|
||||
"16:9": "2560x1440",
|
||||
}
|
||||
},
|
||||
"default_size": "2K",
|
||||
"priority": 10,
|
||||
}
|
||||
],
|
||||
"video": [
|
||||
{
|
||||
"id": "video_engine_xxx",
|
||||
"name": "Seedance 2.0",
|
||||
"provider": "ark",
|
||||
"model_name": "doubao-seedance-2-0-260128",
|
||||
"supported_ratios": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"],
|
||||
"supported_resolutions": ["480p", "720p", "1080p"],
|
||||
"supported_durations": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
"max_duration": 15,
|
||||
"priority": 10,
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
engine: GenerationAIEngineGroupOut = Field(..., description="图片/视频可用引擎分组")
|
||||
|
||||
|
||||
class GenerationAITaskOut(BaseModel):
|
||||
"""AI生成任务详情响应体。"""
|
||||
|
||||
|
||||
@@ -22,14 +22,63 @@ async def calc_text_credits(db: AsyncSession, input_tokens: int, output_tokens:
|
||||
return round(total_tokens * rate / 1000, 2)
|
||||
|
||||
|
||||
async def calc_video_credits(db: AsyncSession, duration: int, resolution: str) -> float:
|
||||
"""Calculate video credits using CreditRatio table, with fallback to hardcoded."""
|
||||
async def _get_credit_ratio(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
gen_type: str,
|
||||
resolution: str,
|
||||
engine_id: str | None = None,
|
||||
) -> CreditRatio | None:
|
||||
"""按引擎精确规则优先获取积分规则;找不到时回退到同类型同参数最高规则。"""
|
||||
gen_type = (gen_type or "").lower().strip()
|
||||
resolution = (resolution or "").strip()
|
||||
engine_id = (engine_id or "").strip() or None
|
||||
|
||||
if engine_id:
|
||||
result = await db.execute(
|
||||
select(CreditRatio)
|
||||
.where(CreditRatio.gen_type == gen_type)
|
||||
.where(CreditRatio.model_config_id == engine_id)
|
||||
.where(CreditRatio.resolution == resolution)
|
||||
.order_by(CreditRatio.base_credits.desc(), CreditRatio.per_second_credits.desc())
|
||||
.limit(1)
|
||||
)
|
||||
ratio = result.scalar_one_or_none()
|
||||
if ratio:
|
||||
return ratio
|
||||
|
||||
result = await db.execute(
|
||||
select(CreditRatio).where(CreditRatio.resolution == resolution).limit(1)
|
||||
select(CreditRatio)
|
||||
.where(CreditRatio.gen_type == gen_type)
|
||||
.where(CreditRatio.resolution == resolution)
|
||||
.order_by(CreditRatio.base_credits.desc(), CreditRatio.per_second_credits.desc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def calc_video_credits(
|
||||
db: AsyncSession,
|
||||
duration: int,
|
||||
resolution: str,
|
||||
engine_id: str | None = None,
|
||||
) -> float:
|
||||
"""Calculate video credits using CreditRatio table, with fallback to hardcoded.
|
||||
|
||||
查询优先级:
|
||||
1. gen_type=video + engine_id + resolution 精确规则;
|
||||
2. gen_type=video + resolution 下 base_credits/per_second_credits 最高规则;
|
||||
3. 原硬编码默认算法。
|
||||
"""
|
||||
ratio = await _get_credit_ratio(
|
||||
db,
|
||||
gen_type="video",
|
||||
resolution=resolution,
|
||||
engine_id=engine_id,
|
||||
)
|
||||
ratio = result.scalar_one_or_none()
|
||||
if ratio:
|
||||
return round((ratio.base_credits + ratio.per_second_credits * duration) * ratio.ratio, 2)
|
||||
|
||||
# Fallback
|
||||
base = 60.0
|
||||
duration_cost = duration * 2.0
|
||||
@@ -45,14 +94,27 @@ def calc_credits(duration: int, resolution: str) -> float:
|
||||
return round((base + duration_cost) * multiplier, 2)
|
||||
|
||||
|
||||
async def calc_image_credits(db: AsyncSession, image_size: str) -> float:
|
||||
"""Calculate image credits using CreditRatio table, with fallback to hardcoded."""
|
||||
result = await db.execute(
|
||||
select(CreditRatio).where(CreditRatio.gen_type == "image").where(CreditRatio.resolution == image_size).limit(1)
|
||||
async def calc_image_credits(
|
||||
db: AsyncSession,
|
||||
image_size: str,
|
||||
engine_id: str | None = None,
|
||||
) -> float:
|
||||
"""Calculate image credits using CreditRatio table, with fallback to hardcoded.
|
||||
|
||||
查询优先级:
|
||||
1. gen_type=image + engine_id + image_size 精确规则;
|
||||
2. gen_type=image + image_size 下 base_credits/per_second_credits 最高规则;
|
||||
3. 原硬编码默认算法。
|
||||
"""
|
||||
ratio = await _get_credit_ratio(
|
||||
db,
|
||||
gen_type="image",
|
||||
resolution=image_size,
|
||||
engine_id=engine_id,
|
||||
)
|
||||
ratio = result.scalar_one_or_none()
|
||||
if ratio:
|
||||
return round(ratio.base_credits * ratio.ratio, 2)
|
||||
|
||||
# Fallback
|
||||
multiplier = {"4K": 2.0, "2K": 1.0}.get(image_size, 1.0)
|
||||
base_cost = 4.0
|
||||
|
||||
@@ -16,9 +16,13 @@ from app.models.image_engine import ImageEngine
|
||||
from app.models.user import User
|
||||
from app.models.video_engine import VideoEngine
|
||||
from app.schemas.generation_ai import (
|
||||
GenerationAIEngineGroupOut,
|
||||
GenerationAIEngineOptionsOut,
|
||||
GenerationAIImageEngineOptionOut,
|
||||
GenerationAIRecordHistoryItemOut,
|
||||
GenerationAITaskCreate,
|
||||
GenerationAITaskOut,
|
||||
GenerationAIVideoEngineOptionOut,
|
||||
)
|
||||
from app.services.generation_billing_service import charge_generation_media_by_params
|
||||
from app.utils.id_gen import generate_id
|
||||
@@ -135,6 +139,52 @@ def _build_video_snapshot(engine: VideoEngine, ratio: str, resolution: str, dura
|
||||
}
|
||||
|
||||
|
||||
async def list_generation_ai_engine_options(db: AsyncSession) -> GenerationAIEngineOptionsOut:
|
||||
"""获取当前启用的图片/视频生成引擎,供前端创建任务时选择 engine_id。"""
|
||||
image_result = await db.execute(
|
||||
select(ImageEngine)
|
||||
.where(ImageEngine.is_active == True)
|
||||
.order_by(ImageEngine.priority.desc())
|
||||
)
|
||||
video_result = await db.execute(
|
||||
select(VideoEngine)
|
||||
.where(VideoEngine.is_active == True)
|
||||
.order_by(VideoEngine.priority.desc())
|
||||
)
|
||||
|
||||
image_items = [
|
||||
GenerationAIImageEngineOptionOut(
|
||||
id=engine.id,
|
||||
name=engine.name,
|
||||
provider=engine.provider,
|
||||
model_name=engine.model_name,
|
||||
supported_models=_parse_list(engine.supported_models, []),
|
||||
supported_sizes=_image_supported_sizes(engine),
|
||||
default_size=engine.default_size,
|
||||
priority=engine.priority or 0,
|
||||
)
|
||||
for engine in image_result.scalars().all()
|
||||
]
|
||||
video_items = [
|
||||
GenerationAIVideoEngineOptionOut(
|
||||
id=engine.id,
|
||||
name=engine.name,
|
||||
provider=engine.provider,
|
||||
model_name=engine.model_name,
|
||||
supported_ratios=_parse_list(engine.supported_ratios, []),
|
||||
supported_resolutions=_parse_list(engine.supported_resolutions, []),
|
||||
supported_durations=_parse_list(engine.supported_durations, []),
|
||||
max_duration=engine.max_duration,
|
||||
priority=engine.priority or 0,
|
||||
)
|
||||
for engine in video_result.scalars().all()
|
||||
]
|
||||
|
||||
return GenerationAIEngineOptionsOut(
|
||||
engine=GenerationAIEngineGroupOut(image=image_items, video=video_items)
|
||||
)
|
||||
|
||||
|
||||
async def create_async_generation_task(db: AsyncSession, current_user: User, req: GenerationAITaskCreate) -> ChatGenerationTask:
|
||||
"""Create a project-independent chat generation task.
|
||||
|
||||
@@ -180,6 +230,7 @@ async def create_async_generation_task(db: AsyncSession, current_user: User, req
|
||||
record_id=task_id,
|
||||
gen_type="image",
|
||||
image_size=size,
|
||||
engine_id=engine.id,
|
||||
project_name="AI生成任务",
|
||||
description_prefix="Chat任务",
|
||||
)
|
||||
@@ -225,6 +276,7 @@ async def create_async_generation_task(db: AsyncSession, current_user: User, req
|
||||
gen_type="video",
|
||||
duration=duration,
|
||||
resolution=resolution,
|
||||
engine_id=engine.id,
|
||||
project_name="AI生成任务",
|
||||
description_prefix="Chat任务",
|
||||
)
|
||||
|
||||
@@ -262,6 +262,7 @@ async def charge_generation_media_by_params(
|
||||
image_size: str | None = None,
|
||||
duration: int | None = None,
|
||||
resolution: str | None = None,
|
||||
engine_id: str | None = None,
|
||||
project_name: str | None = None,
|
||||
description_prefix: str = "ChatAPI异步",
|
||||
) -> BillingSummary:
|
||||
@@ -272,7 +273,7 @@ async def charge_generation_media_by_params(
|
||||
|
||||
if gen_type == "image":
|
||||
size = image_size or "2K"
|
||||
amount = await calc_image_credits(db, size)
|
||||
amount = await calc_image_credits(db, size, engine_id=engine_id)
|
||||
items.append(
|
||||
await deduct_credits_locked_once(
|
||||
db,
|
||||
@@ -284,7 +285,7 @@ async def charge_generation_media_by_params(
|
||||
)
|
||||
)
|
||||
elif gen_type == "video":
|
||||
amount = await calc_video_credits(db, duration or 5, resolution or "720p")
|
||||
amount = await calc_video_credits(db, duration or 5, resolution or "720p", engine_id=engine_id)
|
||||
items.append(
|
||||
await deduct_credits_locked_once(
|
||||
db,
|
||||
|
||||
@@ -63,68 +63,37 @@ def _build_optimized_prompt_by_params(task: ChatGenerationTask) -> str:
|
||||
|
||||
gen_type = (_to_clean_str(getattr(task, "gen_type", None)) or "").lower()
|
||||
|
||||
# 常见字段名兼容:
|
||||
# duration / duration_seconds / video_duration
|
||||
# aspect_ratio / ratio
|
||||
# resolution
|
||||
# image_size / size / pixel_size
|
||||
duration = _get_first_value(task, "duration", "duration_seconds", "video_duration")
|
||||
aspect_ratio = _get_first_value(task, "aspect_ratio", "ratio")
|
||||
duration = _get_first_value(task, "duration")
|
||||
aspect_ratio = _get_first_value(task, "aspect_ratio")
|
||||
resolution = _get_first_value(task, "resolution")
|
||||
image_size = _get_first_value(task, "image_size", "size", "pixel_size")
|
||||
image_size = _get_first_value(task, "image_size")
|
||||
image_px = _get_first_value(task, "image_px")
|
||||
image_proportion = _get_first_value(task, "image_proportion")
|
||||
|
||||
parts = []
|
||||
|
||||
if gen_type == "video":
|
||||
duration_text = _format_duration(duration)
|
||||
aspect_ratio_text = _to_clean_str(aspect_ratio)
|
||||
resolution_text = _to_clean_str(resolution)
|
||||
|
||||
if duration_text:
|
||||
parts.append(f"时长:{duration_text}")
|
||||
if aspect_ratio_text:
|
||||
parts.append(f"画面比例:{aspect_ratio_text}")
|
||||
if resolution_text:
|
||||
parts.append(f"分辨率:{resolution_text}")
|
||||
|
||||
# 时长:4秒,画面比例:16:9,分辨率:480p
|
||||
if duration:
|
||||
parts.append(f"时长:{duration}秒")
|
||||
parts.append(f"画面比例:{aspect_ratio}")
|
||||
parts.append(f"分辨率:{resolution}")
|
||||
else:
|
||||
parts.append(f"时长:4秒")
|
||||
parts.append(f"画面比例:16:9")
|
||||
parts.append(f"分辨率:480p")
|
||||
elif gen_type == "image":
|
||||
resolution_text = _to_clean_str(resolution)
|
||||
aspect_ratio_text = _to_clean_str(aspect_ratio)
|
||||
image_size_text = _to_clean_str(image_size)
|
||||
|
||||
if resolution_text:
|
||||
if resolution_text.startswith("分辨率"):
|
||||
parts.append(resolution_text)
|
||||
else:
|
||||
parts.append(f"分辨率{resolution_text}")
|
||||
|
||||
if aspect_ratio_text:
|
||||
if aspect_ratio_text.startswith("画布比例"):
|
||||
parts.append(aspect_ratio_text)
|
||||
else:
|
||||
parts.append(f"画布比例{aspect_ratio_text}")
|
||||
|
||||
if image_size_text:
|
||||
if image_size_text.startswith("像素尺寸"):
|
||||
parts.append(image_size_text)
|
||||
else:
|
||||
parts.append(f"像素尺寸{image_size_text}")
|
||||
|
||||
if image_size :
|
||||
parts.append(f"分辨率:{image_size}")
|
||||
parts.append(f"画布比例:{image_proportion}")
|
||||
parts.append(f"像素尺寸:{image_px}")
|
||||
else:
|
||||
parts.append(f"分辨率:2K")
|
||||
parts.append(f"画布比例:1:1")
|
||||
parts.append(f"像素尺寸:2048x2048")
|
||||
else:
|
||||
# 未知类型时尽量保守拼接已有参数,避免直接丢失生成参数。
|
||||
duration_text = _format_duration(duration)
|
||||
aspect_ratio_text = _to_clean_str(aspect_ratio)
|
||||
resolution_text = _to_clean_str(resolution)
|
||||
image_size_text = _to_clean_str(image_size)
|
||||
|
||||
if duration_text:
|
||||
parts.append(f"时长:{duration_text}")
|
||||
if aspect_ratio_text:
|
||||
parts.append(f"画面比例:{aspect_ratio_text}")
|
||||
if resolution_text:
|
||||
parts.append(f"分辨率:{resolution_text}")
|
||||
if image_size_text:
|
||||
parts.append(f"像素尺寸{image_size_text}")
|
||||
# 未知类型时返回原始字符
|
||||
return base_prompt
|
||||
|
||||
suffix = ",".join(parts)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user