修复chat生成参数提交错误BUG|模型引擎积分设置关联开发优化成功|视频/图片生成引擎API开发完成

This commit is contained in:
2026-05-28 13:00:27 +08:00
parent 5021cfd35b
commit edef601f7c
17 changed files with 890 additions and 483 deletions
+34 -2
View File
@@ -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
+66
View File
@@ -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
View File
@@ -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,
+12 -6
View File
@@ -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)
+43 -10
View File
@@ -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生成任务详情响应体。"""
+71 -9
View File
@@ -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)