chore(release): bump version to 1.9.1 and apply pending changes

This commit is contained in:
小鱼开发
2026-06-16 15:17:30 +08:00
parent 9a71584d6c
commit c6a40331d4
152 changed files with 9396 additions and 10267 deletions
+537
View File
@@ -0,0 +1,537 @@
diff --git a/python-api/app/api/v1/caption.py b/python-api/app/api/v1/caption.py
index afae831..99a771a 100644
--- a/python-api/app/api/v1/caption.py
+++ b/python-api/app/api/v1/caption.py
@@ -24,19 +24,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/caption", tags=["Caption"])
-
-
-
-
-
-
-
-
-
-
-
-
-
@router.post("/ata/align")
async def auto_align_caption(
request_body: AutoAlignSubmitRequest,
@@ -88,9 +75,3 @@ async def auto_align_caption(
except Exception as e:
logger.error(f"自动打轴异常: {e}")
raise HTTPException(status_code=500, detail="字幕打轴失败,请稍后重试")
-
-
-
-
-
-
diff --git a/python-api/app/api/v1/points.py b/python-api/app/api/v1/points.py
index b2c7208..23bafa9 100644
--- a/python-api/app/api/v1/points.py
+++ b/python-api/app/api/v1/points.py
@@ -6,7 +6,7 @@
"""
import logging
-from datetime import UTC, datetime
+from datetime import UTC, datetime, timedelta
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request
@@ -14,6 +14,7 @@ from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, get_db
+from app.core.exceptions import InsufficientPointsException
from app.crud.point_recharge_order import point_recharge_order
from app.crud.point_transaction import point_transaction
from app.models.user import User
@@ -35,6 +36,7 @@ router = APIRouter(prefix="/points", tags=["Points"])
# ── 余额查询 ──────────────────────────────────────────
+
@router.get("/balance", response_model=ApiResponse[PointBalanceResponse])
async def get_balance(
db: AsyncSession = Depends(get_db),
@@ -47,6 +49,7 @@ async def get_balance(
# ── 流水查询 ──────────────────────────────────────────
+
@router.get("/transactions", response_model=ApiResponse[PointTransactionListResponse])
async def list_transactions(
pagination: PaginationParams = Depends(),
@@ -127,12 +130,13 @@ async def list_transactions(
# ── 充值 ──────────────────────────────────────────────
+
@router.post("/recharge", response_model=ApiResponse[RechargeResponse])
async def create_recharge_order(
request: RechargeRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
- http_request: Request = None,
+ http_request: Request = None, # type: ignore[assignment]
):
"""
创建积分充值订单(微信支付 Native 扫码)
@@ -303,9 +307,7 @@ async def handle_wxpay_notify(
return _wx_response()
# 查找订单
- order = await point_recharge_order.get_by_out_trade_no(
- db, out_trade_no=out_trade_no
- )
+ order = await point_recharge_order.get_by_out_trade_no(db, out_trade_no=out_trade_no)
if not order:
logger.error(f"[WechatPay] 回调订单不存在: {out_trade_no}")
return _wx_response()
@@ -400,9 +402,7 @@ async def query_recharge_status(
wxpay = get_wxpay_service()
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=10.0)) as client:
- wx_result = await wxpay.query_order(
- client, out_trade_no=order.out_trade_no
- )
+ wx_result = await wxpay.query_order(client, out_trade_no=order.out_trade_no)
order.query_result = str(wx_result)
trade_state = wx_result.get("trade_state", "")
@@ -465,6 +465,7 @@ async def query_recharge_status(
# ── 充值档位查询 ──────────────────────────────────────
+
@router.get("/recharge-options", response_model=ApiResponse[list[dict]])
async def get_recharge_options(
current_user: User = Depends(get_current_user),
@@ -480,6 +481,7 @@ async def get_recharge_options(
# ── 扣费业务类型查询 ──────────────────────────────────
+
@router.get("/chargeable-types", response_model=ApiResponse[list[str]])
async def get_chargeable_types(
current_user: User = Depends(get_current_user),
@@ -496,6 +498,7 @@ async def get_chargeable_types(
# ── 积分规则查询 ──────────────────────────────────────
+
@router.get("/rules", response_model=ApiResponse[list[dict]])
async def get_points_rules(
current_user: User = Depends(get_current_user),
@@ -530,9 +533,9 @@ async def get_points_rules(
# ── 积分预估查询 ──────────────────────────────────────
-
# ── 今日消费统计 ──────────────────────────────────────
+
@router.get("/today-consumed", response_model=ApiResponse[dict])
async def get_today_consumed(
db: AsyncSession = Depends(get_db),
@@ -545,6 +548,7 @@ async def get_today_consumed(
# ── 直接消费扣费(前端/Rust 层调用)───────────────────
+
@router.post("/consume", response_model=ApiResponse[dict])
async def consume_points(
request: ConsumeRequest,
@@ -569,12 +573,12 @@ async def consume_points(
source_type=request.source_type,
source_id=request.source_id,
description=f"【{request.description or request.source_type}】",
- allow_negative=False,
+ allow_negative=request.allow_negative,
)
- except ValueError as e:
+ except InsufficientPointsException:
# 余额不足(在同一事务内判断,避免竞态)
- if "积分不足" in str(e):
- raise HTTPException(status_code=402, detail=str(e))
+ raise
+ except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
await db.commit()
@@ -587,5 +591,3 @@ async def consume_points(
},
message="消费成功",
)
-
-
diff --git a/python-api/app/api/v1/script.py b/python-api/app/api/v1/script.py
index 5e7b18f..26a11a3 100644
--- a/python-api/app/api/v1/script.py
+++ b/python-api/app/api/v1/script.py
@@ -17,6 +17,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.model_router import get_model_router
from app.ai.prompts import list_categories, list_prompt_files, load_prompt, render_template
from app.api.deps import get_current_user
+from app.core.exceptions import AITimeoutException, InsufficientPointsException
from app.db.session import get_db
from app.models.user import User
from app.schemas.common import ApiResponse, success_response
@@ -71,9 +72,8 @@ async def polish_content(
required_points = ps._calculate_cost("polish")
check = await ps.check_balance(db, current_user.id, required_points)
if not check["sufficient"]:
- raise HTTPException(
- status_code=402,
- detail=f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}",
+ raise InsufficientPointsException(
+ f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}"
)
try:
@@ -99,11 +99,11 @@ async def polish_content(
data=polished,
message=f"{type_name}润色完成",
)
+ except InsufficientPointsException:
+ raise
except HTTPException:
raise
except ValueError as e:
- if "积分不足" in str(e):
- raise HTTPException(status_code=402, detail=str(e))
logger.warning(f"[Polish] 润色失败: {e}")
raise HTTPException(status_code=500, detail="润色失败,请检查输入内容后重试")
except Exception as e:
@@ -111,9 +111,6 @@ async def polish_content(
raise HTTPException(status_code=500, detail=f"{type_name}润色失败,请稍后重试")
-
-
-
@router.post("/generate-title", response_model=ApiResponse[GenerateTitleResponse])
async def generate_title(
request: GenerateTitleRequest,
@@ -146,7 +143,11 @@ async def generate_title(
usage_note = "- 视频画面上的标题需要精炼,聚焦核心关键词\n- 副标题与主标题形成呼应,补充说明但不喧宾夺主"
# 渲染用户提示词
- title_type_desc = "大标题(主标题,提炼核心卖点,吸睛)" if request.title_type == "main" else "小标题(副标题,补充说明或制造悬念)"
+ title_type_desc = (
+ "大标题(主标题,提炼核心卖点,吸睛)"
+ if request.title_type == "main"
+ else "小标题(副标题,补充说明或制造悬念)"
+ )
user_prompt = render_template(
user_template,
title_type=request.title_type,
@@ -163,9 +164,8 @@ async def generate_title(
required_points = ps._calculate_cost("title")
check = await ps.check_balance(db, current_user.id, required_points)
if not check["sufficient"]:
- raise HTTPException(
- status_code=402,
- detail=f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}",
+ raise InsufficientPointsException(
+ f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}"
)
try:
@@ -179,10 +179,10 @@ async def generate_title(
title = result.content.strip() if result.content else ""
# 去除可能的引号
- title = title.strip('"').strip("'").strip('「」').strip('『』').strip('《》')
+ title = title.strip('"').strip("'").strip("「」").strip("『』").strip("《》")
# 截断到最大长度
if len(title) > request.max_length:
- title = title[:request.max_length]
+ title = title[: request.max_length]
# 扣费
points = ps._calculate_cost("title")
@@ -200,15 +200,13 @@ async def generate_title(
data=GenerateTitleResponse(title=title),
message="标题生成成功",
)
+ except InsufficientPointsException:
+ raise
except HTTPException:
raise
except TimeoutError:
logger.warning("[generate_title] 标题生成超时")
- raise HTTPException(status_code=500, detail="标题生成超时,请重试")
- except ValueError as e:
- if "积分不足" in str(e):
- raise HTTPException(status_code=402, detail=str(e))
- raise HTTPException(status_code=500, detail=f"标题生成失败: {str(e)}")
+ raise AITimeoutException("标题生成超时,请稍后重试")
except Exception as e:
logger.error(f"[generate_title] 标题生成失败: {e}")
raise HTTPException(status_code=500, detail=f"标题生成失败: {str(e)}")
diff --git a/python-api/app/api/v1/tasks.py b/python-api/app/api/v1/tasks.py
index ae17fe3..55965ca 100644
--- a/python-api/app/api/v1/tasks.py
+++ b/python-api/app/api/v1/tasks.py
@@ -18,6 +18,7 @@ from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, field_validator
from app.api.deps import get_current_user
+from app.core.exceptions import InsufficientPointsException
from app.core.redis_client import get_redis_client
from app.db.session import AsyncSessionLocal
from app.models.user import User
@@ -38,7 +39,6 @@ class ScriptParams(BaseModel):
category: str = Field(..., min_length=1, description="大类代码")
filename: str = Field(..., min_length=1, description="提示词文件名")
-
@field_validator("category")
@classmethod
def validate_category(cls, v: str) -> str:
@@ -96,7 +96,9 @@ class VideoParams(BaseModel):
volume: int = Field(default=0, ge=0, le=10, description="音量")
ref_photo_url: str | None = Field(default=None, description="人脸参考图 URL")
planned_duration: float = Field(..., gt=0, description="该分镜脚本规划时长(秒),用于余额预检")
- total_planned_duration: float = Field(..., gt=0, description="所有分镜规划时长之和(秒),用于预检")
+ total_planned_duration: float = Field(
+ ..., gt=0, description="所有分镜规划时长之和(秒),用于预检"
+ )
batch_id: str | None = Field(default=None, description="批次ID(可选)")
@field_validator("video_url")
@@ -134,6 +136,7 @@ class TaskStatusResponse(BaseModel):
total: int = Field(0, description="总子任务数")
result: dict | None = Field(None, description="任务结果(完成时)")
error: str | None = Field(None, description="错误信息(失败时)")
+ error_code: str | None = Field(None, description="错误码(失败时,如 content_violation")
created_at: str = Field("", description="任务创建时间(ISO格式)")
@@ -222,9 +225,8 @@ async def create_task(
f"[API] 积分不足: user={user_id}, type={task_type}, "
f"required={required_points}, balance={check['balance']}"
)
- raise HTTPException(
- status_code=402,
- detail=f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}",
+ raise InsufficientPointsException(
+ f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}"
)
# ── 3. 写入 Redis ──────────────────────────────────
@@ -246,18 +248,41 @@ async def create_task(
params=validated_params,
)
await registry.add_running(task_id)
-
- logger.info(f"[API] Task created: {task_id}, type={task_type}, user={user_id}")
- return TaskCreateResponse(
- task_id=task_id,
- status="running",
- message=f"{task_type} 任务已创建",
- )
-
except Exception as e:
logger.error(f"[API] Failed to update registry: {e}")
raise HTTPException(status_code=500, detail="创建任务失败:Redis写入错误")
+ # ── 4. 脚本生成:Redis 写入成功后再扣费 ─────────────
+ if task_type == "script" and required_points > 0:
+ try:
+ async with AsyncSessionLocal() as db:
+ await ps.consume(
+ db,
+ user_id=user_id,
+ points=required_points,
+ source_type="script",
+ source_id=task_id,
+ description="【脚本生成】",
+ )
+ await db.commit()
+ except InsufficientPointsException:
+ # 余额不足:将任务标记为失败,避免无费执行
+ await registry.update(task_id, status="failed", message="积分不足")
+ await registry.remove_running(task_id)
+ raise
+ except Exception as e:
+ logger.error(f"[API] 脚本任务扣费失败: {e}")
+ await registry.update(task_id, status="failed", message="扣费失败")
+ await registry.remove_running(task_id)
+ raise HTTPException(status_code=500, detail="扣费失败,请稍后重试")
+
+ logger.info(f"[API] Task created: {task_id}, type={task_type}, user={user_id}")
+ return TaskCreateResponse(
+ task_id=task_id,
+ status="running",
+ message=f"{task_type} 任务已创建",
+ )
+
@router.get("", response_model=list[TaskStatusResponse])
async def list_tasks(
@@ -294,6 +319,7 @@ async def list_tasks(
total=task.total,
result=None, # 列表查询不返回 result,避免数据过大
error=task.error,
+ error_code=task.error_code,
created_at=task.created_at,
)
)
@@ -337,6 +363,7 @@ async def get_task_status(
total=task.total,
result=task.result,
error=task.error,
+ error_code=task.error_code,
created_at=task.created_at,
)
diff --git a/python-api/app/api/v1/vidu.py b/python-api/app/api/v1/vidu.py
index c0fb04a..401d8da 100644
--- a/python-api/app/api/v1/vidu.py
+++ b/python-api/app/api/v1/vidu.py
@@ -44,10 +44,12 @@ async def vidu_callback(request: Request):
headers_dict = dict(request.headers)
# 使用 APP_BASE_URL 构建 callback_url,确保与提交任务时传给 Vidu 的一致
- #Nginx 反向代理可能导致 request.url 的 scheme 为 http,与 Vidu 签名时的 https 不一致)
+ # Nginx 反向代理可能导致 request.url 的 scheme 为 http,与 Vidu 签名时的 https 不一致)
app_base_url = get_settings().app_base_url
callback_url = f"{app_base_url}/api/v1/vidu/callback" if app_base_url else str(request.url)
- logger.info(f"[Vidu] 收到回调: request_url={request.url}, callback_url={callback_url}, body={body_bytes.decode('utf-8', errors='replace')[:500]}")
+ logger.info(
+ f"[Vidu] 收到回调: request_url={request.url}, callback_url={callback_url}, body={body_bytes.decode('utf-8', errors='replace')[:500]}"
+ )
try:
task_status = await gateway.handle_webhook(
@@ -64,15 +66,13 @@ async def vidu_callback(request: Request):
logger.error(f"[Vidu] 回调处理失败: {e}")
raise HTTPException(status_code=500, detail="回调处理失败,请稍后重试")
- logger.info(f"[Vidu] 回调解析完成: state={task_status.state}, result={task_status.result}, error={task_status.error_message}")
+ logger.info(
+ f"[Vidu] 回调解析完成: state={task_status.state}, result={task_status.result}, error={task_status.error_message}"
+ )
# 2. 通过 platform_task_id 反查 Async Engine 内部 task_id,更新 TaskRegistry
- platform_task_id = (
- task_status.result.get("task_id") if task_status.result else None
- )
- video_url = (
- task_status.result.get("video_url") if task_status.result else None
- )
+ platform_task_id = task_status.result.get("task_id") if task_status.result else None
+ video_url = task_status.result.get("video_url") if task_status.result else None
logger.info(f"[Vidu] 准备反查 internal_task_id: platform_task_id={platform_task_id}")
@@ -121,8 +121,6 @@ async def vidu_callback(request: Request):
f"platform={platform_task_id}"
)
else:
- logger.warning(
- f"[Vidu] 回调无法反查内部 task_id: platform={platform_task_id}"
- )
+ logger.warning(f"[Vidu] 回调无法反查内部 task_id: platform={platform_task_id}")
return success_response(message="回调已接收")
diff --git a/python-api/app/api/v1/voice.py b/python-api/app/api/v1/voice.py
index ba8cac9..dad2b7b 100644
--- a/python-api/app/api/v1/voice.py
+++ b/python-api/app/api/v1/voice.py
@@ -10,12 +10,13 @@ import logging
import re
import time
import uuid
+
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
-from app.core.exceptions import PlatformError
+from app.core.exceptions import InsufficientPointsException, PlatformError
from app.db.session import get_db
from app.models.user import User
from app.schemas.common import ApiResponse, success_response
@@ -49,7 +50,9 @@ class TTSSynthesizeRequest(BaseModel):
class VoiceCloneSubmitRequest(BaseModel):
"""声音复刻提交请求"""
- source_audio_url: str | None = Field(None, description="源音频 URL5-30秒,mp3/wav,需公开可访问)")
+ source_audio_url: str | None = Field(
+ None, description="源音频 URL5-30秒,mp3/wav,需公开可访问)"
+ )
source_video_url: str | None = Field(None, description="源视频 URL(可选)")
video_id: str | None = Field(None, description="历史作品ID(可选)")
voice_name: str | None = Field(None, description="自定义音色名称(≤20字符)")
@@ -111,7 +114,7 @@ async def synthesize_speech(
# 宽松预检:余额为负或零时阻止,避免浪费第三方资源
balance_info = await ps.get_user_balance(db, current_user.id)
if balance_info["balance"] <= 0:
- raise HTTPException(status_code=402, detail="余额不足,请先充值")
+ raise InsufficientPointsException("余额不足,请先充值")
try:
audio_url = await service.synthesize(
@@ -137,10 +140,8 @@ async def synthesize_speech(
allow_negative=True,
)
await db.commit()
- except ValueError as e:
- if "积分不足" in str(e):
- raise HTTPException(status_code=402, detail=str(e))
- logger.error(f"[Voice] TTS 扣费失败: {e}")
+ except InsufficientPointsException:
+ raise
except Exception as e:
logger.error(f"[Voice] TTS 扣费失败: {e}")
@@ -165,7 +166,6 @@ async def synthesize_speech(
raise HTTPException(status_code=500, detail="语音合成失败,请稍后重试")
-
def _normalize_voice_id(name: str | None) -> str:
"""
将用户输入的名称规范化为 Vidu 合法的 voice_id。
@@ -220,9 +220,8 @@ async def submit_clone_task(
required_points = ps._calculate_cost("voice_clone")
check = await ps.check_balance(db, current_user.id, required_points)
if not check["sufficient"]:
- raise HTTPException(
- status_code=402,
- detail=f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}",
+ raise InsufficientPointsException(
+ f"积分不足,需要 {required_points} 积分,当前余额 {check['balance']}"
)
try:
@@ -244,10 +243,8 @@ async def submit_clone_task(
description="【声音复刻】",
)
await db.commit()
- except ValueError as e:
- if "积分不足" in str(e):
- raise HTTPException(status_code=402, detail=str(e))
- logger.error(f"[Voice] 克隆扣费失败: {e}")
+ except InsufficientPointsException:
+ raise
except Exception as e:
logger.error(f"[Voice] 克隆扣费失败: {e}")
@@ -292,5 +289,3 @@ async def query_clone_task(
),
message="克隆已完成",
)
-
-
+146
View File
@@ -0,0 +1,146 @@
diff --git a/python-api/app/core/exceptions.py b/python-api/app/core/exceptions.py
index d9970d5..837f8d3 100644
--- a/python-api/app/core/exceptions.py
+++ b/python-api/app/core/exceptions.py
@@ -24,9 +24,16 @@ class AppException(HTTPException):
status_code: int,
message: str = "操作失败",
detail: dict | None = None,
+ *,
+ error_code: str | None = None,
):
- super().__init__(status_code=status_code, detail=detail or {})
+ body = detail or {}
+ body["message"] = message
+ if error_code:
+ body["error_code"] = error_code
+ super().__init__(status_code=status_code, detail=body)
self.message = message
+ self.error_code = error_code
class NotFoundException(AppException):
@@ -44,7 +51,7 @@ class ValidationException(AppException):
def __init__(self, message: str = "参数验证失败"):
super().__init__(
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
message=message,
)
@@ -79,6 +86,17 @@ class BusinessException(AppException):
)
+class InsufficientPointsException(AppException):
+ """积分不足"""
+
+ def __init__(self, message: str = "积分不足"):
+ super().__init__(
+ status_code=status.HTTP_402_PAYMENT_REQUIRED,
+ message=message,
+ error_code="insufficient_points",
+ )
+
+
class ModelUnavailableException(AppException):
"""AI 模型不可用"""
@@ -99,6 +117,50 @@ class TaskFailedException(AppException):
)
+class PromptNotFoundException(AppException):
+ """提示词文件不存在"""
+
+ def __init__(self, message: str = "未找到提示词"):
+ super().__init__(
+ status_code=status.HTTP_404_NOT_FOUND,
+ message=message,
+ error_code="prompt_not_found",
+ )
+
+
+class AIEmptyResponseException(AppException):
+ """AI 返回内容为空"""
+
+ def __init__(self, message: str = "AI 返回内容为空"):
+ super().__init__(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ message=message,
+ error_code="empty_result",
+ )
+
+
+class AIParseErrorException(AppException):
+ """AI 返回内容解析失败"""
+
+ def __init__(self, message: str = "AI 返回格式解析失败"):
+ super().__init__(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ message=message,
+ error_code="parse_error",
+ )
+
+
+class AITimeoutException(AppException):
+ """AI 调用超时"""
+
+ def __init__(self, message: str = "AI 请求超时,请稍后重试"):
+ super().__init__(
+ status_code=status.HTTP_504_GATEWAY_TIMEOUT,
+ message=message,
+ error_code="timeout",
+ )
+
+
# ═══════════════════════════════════════════════════════════════
# 第三方平台异常(PlatformError 体系)
# ═══════════════════════════════════════════════════════════════
@@ -111,14 +173,15 @@ class PlatformErrorType:
确保前端和网关能够统一处理。
"""
- RATE_LIMIT = "rate_limit" # 429,可重试
- AUTH_FAILED = "auth_failed" # 401/403,不可重试
- TIMEOUT = "timeout" # 连接/读取超时,可重试
- SERVER_ERROR = "server_error" # 第三方 5xx,可重试
- BAD_REQUEST = "bad_request" # 参数错误,不可重试
+ RATE_LIMIT = "rate_limit" # 429,可重试
+ AUTH_FAILED = "auth_failed" # 401/403,不可重试
+ TIMEOUT = "timeout" # 连接/读取超时,可重试
+ SERVER_ERROR = "server_error" # 第三方 5xx,可重试
+ BAD_REQUEST = "bad_request" # 参数错误,不可重试
QUOTA_EXHAUSTED = "quota_exhausted" # 额度用完,不可重试(或延迟重试)
- NOT_FOUND = "not_found" # 资源不存在,不可重试
- UNKNOWN = "unknown" # 兜底
+ NOT_FOUND = "not_found" # 资源不存在,不可重试
+ CONTENT_VIOLATION = "content_violation" # 内容安全/审核不通过,不可重试
+ UNKNOWN = "unknown" # 兜底
class PlatformError(Exception):
@@ -145,12 +208,14 @@ class PlatformError(Exception):
retryable: bool = False,
error_type: str = PlatformErrorType.UNKNOWN,
status_code: int | None = None,
+ raw_code: str | None = None,
):
super().__init__(message)
self.platform = platform
self.retryable = retryable
self.error_type = error_type
self.status_code = status_code
+ self.raw_code = raw_code
def to_http_status(self) -> int:
"""根据 error_type 和 retryable 返回标准 HTTP 状态码"""
@@ -161,6 +226,7 @@ class PlatformError(Exception):
PlatformErrorType.AUTH_FAILED: 401,
PlatformErrorType.BAD_REQUEST: 400,
PlatformErrorType.NOT_FOUND: 404,
+ PlatformErrorType.CONTENT_VIOLATION: 400,
}
if self.error_type in mapping:
return mapping[self.error_type]
+345
View File
@@ -0,0 +1,345 @@
diff --git a/python-api/app/ai/providers/vidu_provider.py b/python-api/app/ai/providers/vidu_provider.py
index cab5902..fccfbbf 100644
--- a/python-api/app/ai/providers/vidu_provider.py
+++ b/python-api/app/ai/providers/vidu_provider.py
@@ -24,8 +24,90 @@ from app.core.exceptions import PlatformError, PlatformErrorType
logger = logging.getLogger(__name__)
-def _map_vidu_error(status: int, message: str) -> PlatformError:
- """把 Vidu HTTP 错误映射为标准 PlatformError"""
+# Vidu 错误码分类
+_VIDU_AUDIT_ERROR_CODES = {
+ "TaskPromptPolicyViolation",
+ "AuditSubmitIllegal",
+ "CreationPolicyViolation",
+ "PhotoAuditNotPass",
+ "AuditFailed",
+ "ImageCheckBodyJointsFailed",
+ "ImageCheckFaceFailed",
+ "ImageObjectsUndetected",
+ "FaceDetectFailure",
+ "FaceDetectNotPass",
+ "NoFaceDetected",
+ "MultiFaceDetected",
+}
+
+_VIDU_RETRYABLE_ERROR_CODES = {
+ "InternalServiceFailure",
+ "ModelUnavailable",
+ "Unknown",
+}
+
+_VIDU_RATE_LIMIT_ERROR_CODES = {
+ "QuotaExceeded",
+ "TooManyRequests",
+ "SystemThrottling",
+ "OperationInProcess",
+}
+
+
+def _extract_vidu_error_code(message: str | None) -> str | None:
+ """从 Vidu 错误信息中提取错误码"""
+ if not message:
+ return None
+ # Vidu 错误码格式:"ErrorCode: 中文描述"
+ return message.split(":")[0].strip() or None
+
+
+def _map_vidu_error(
+ status: int,
+ message: str,
+ *,
+ err_code: str | None = None,
+) -> PlatformError:
+ """把 Vidu HTTP 错误映射为标准 PlatformError
+
+ 优先根据 Vidu 业务错误码(err_code)判断类型,HTTP status 仅作为兜底。
+ """
+ raw_code = err_code or _extract_vidu_error_code(message)
+
+ # 1. 内容安全/审核类:不可重试
+ if raw_code in _VIDU_AUDIT_ERROR_CODES:
+ return PlatformError(
+ message=message,
+ platform="vidu",
+ retryable=False,
+ error_type=PlatformErrorType.CONTENT_VIOLATION,
+ status_code=status,
+ raw_code=raw_code,
+ )
+
+ # 2. 平台内部/模型不可用:可重试
+ if raw_code in _VIDU_RETRYABLE_ERROR_CODES:
+ return PlatformError(
+ message=message,
+ platform="vidu",
+ retryable=True,
+ error_type=PlatformErrorType.SERVER_ERROR,
+ status_code=status,
+ raw_code=raw_code,
+ )
+
+ # 3. 限流类:可重试
+ if raw_code in _VIDU_RATE_LIMIT_ERROR_CODES:
+ return PlatformError(
+ message=message,
+ platform="vidu",
+ retryable=True,
+ error_type=PlatformErrorType.RATE_LIMIT,
+ status_code=status,
+ raw_code=raw_code,
+ )
+
+ # 4. HTTP status 兜底
mapping = {
429: (PlatformErrorType.RATE_LIMIT, True),
401: (PlatformErrorType.AUTH_FAILED, False),
@@ -43,6 +125,7 @@ def _map_vidu_error(status: int, message: str) -> PlatformError:
retryable=retryable,
error_type=error_type,
status_code=status,
+ raw_code=raw_code,
)
@@ -66,7 +149,9 @@ class ViduProvider:
from app.core.platform_config import get_platform_config_loader
platform_config = get_platform_config_loader().get_platform("vidu")
- self.base_url = (platform_config.base_url if platform_config else "https://api.vidu.cn").rstrip("/")
+ self.base_url = (
+ platform_config.base_url if platform_config else "https://api.vidu.cn"
+ ).rstrip("/")
if not self.api_key:
raise ValueError("Vidu API Key 未配置,请在 .env 中设置 VIDU_API_KEY")
@@ -135,9 +220,12 @@ class ViduProvider:
resp = await self.client.post(url, json=body, timeout=httpx.Timeout(120.0, connect=5.0))
data = resp.json()
if resp.status_code != 200 or data.get("state") == "failed":
- msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}"
- logger.error(f"[Vidu TTS] 请求失败: url={url}, status={resp.status_code}, response={data}")
- raise _map_vidu_error(resp.status_code, f"Vidu TTS error: {msg}")
+ err_code = data.get("err_code") or _extract_vidu_error_code(data.get("message"))
+ msg = err_code or data.get("message") or f"HTTP {resp.status_code}"
+ logger.error(
+ f"[Vidu TTS] 请求失败: url={url}, status={resp.status_code}, response={data}"
+ )
+ raise _map_vidu_error(resp.status_code, f"Vidu TTS error: {msg}", err_code=err_code)
return data
except (httpx.NetworkError, httpx.TimeoutException) as e:
logger.error(f"[Vidu TTS] 网络错误: {e}")
@@ -182,9 +270,14 @@ class ViduProvider:
resp = await self.client.post(url, json=body, timeout=httpx.Timeout(120.0, connect=5.0))
data = resp.json()
if resp.status_code != 200 or data.get("state") == "failed":
- msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}"
- logger.error(f"[Vidu Clone] 请求失败: url={url}, status={resp.status_code}, response={data}")
- raise _map_vidu_error(resp.status_code, f"Vidu clone error: {msg}")
+ err_code = data.get("err_code") or _extract_vidu_error_code(data.get("message"))
+ msg = err_code or data.get("message") or f"HTTP {resp.status_code}"
+ logger.error(
+ f"[Vidu Clone] 请求失败: url={url}, status={resp.status_code}, response={data}"
+ )
+ raise _map_vidu_error(
+ resp.status_code, f"Vidu clone error: {msg}", err_code=err_code
+ )
return data
except (httpx.NetworkError, httpx.TimeoutException) as e:
logger.error(f"[Vidu Clone] 网络错误: {e}")
@@ -238,9 +331,14 @@ class ViduProvider:
resp = await self.client.post(url, json=body)
data = resp.json()
if resp.status_code != 200 or data.get("state") == "failed":
- msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}"
- logger.error(f"[Vidu LipSync] 请求失败: url={url}, status={resp.status_code}, response={data}")
- raise _map_vidu_error(resp.status_code, f"Vidu lip-sync error: {msg}")
+ err_code = data.get("err_code") or _extract_vidu_error_code(data.get("message"))
+ msg = err_code or data.get("message") or f"HTTP {resp.status_code}"
+ logger.error(
+ f"[Vidu LipSync] 请求失败: url={url}, status={resp.status_code}, response={data}"
+ )
+ raise _map_vidu_error(
+ resp.status_code, f"Vidu lip-sync error: {msg}", err_code=err_code
+ )
return data
except (httpx.NetworkError, httpx.TimeoutException) as e:
logger.error(f"[Vidu LipSync] 网络错误: {e}")
@@ -264,9 +362,14 @@ class ViduProvider:
resp = await self.client.get(url)
data = resp.json()
if resp.status_code != 200:
- msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}"
- logger.error(f"[Vidu Query] 请求失败: url={url}, status={resp.status_code}, response={data}")
- raise _map_vidu_error(resp.status_code, f"Vidu query task error: {msg}")
+ err_code = data.get("err_code") or _extract_vidu_error_code(data.get("message"))
+ msg = err_code or data.get("message") or f"HTTP {resp.status_code}"
+ logger.error(
+ f"[Vidu Query] 请求失败: url={url}, status={resp.status_code}, response={data}"
+ )
+ raise _map_vidu_error(
+ resp.status_code, f"Vidu query task error: {msg}", err_code=err_code
+ )
return data
except (httpx.NetworkError, httpx.TimeoutException) as e:
logger.error(f"[Vidu Query] 网络错误: {e}")
diff --git a/python-api/app/ai/providers/volcengine_caption_provider.py b/python-api/app/ai/providers/volcengine_caption_provider.py
index 0f2f271..09ddcc7 100644
--- a/python-api/app/ai/providers/volcengine_caption_provider.py
+++ b/python-api/app/ai/providers/volcengine_caption_provider.py
@@ -37,8 +37,10 @@ def _map_caption_error(status: int, message: str, code: int | None = None) -> Pl
if code is not None and code in error_mapping:
error_type, retryable = error_mapping[code]
return PlatformError(
- message, platform="volcengine_caption",
- retryable=retryable, error_type=error_type,
+ message,
+ platform="volcengine_caption",
+ retryable=retryable,
+ error_type=error_type,
status_code=status,
)
@@ -53,8 +55,10 @@ def _map_caption_error(status: int, message: str, code: int | None = None) -> Pl
}
error_type, retryable = http_mapping.get(status, (PlatformErrorType.UNKNOWN, False))
return PlatformError(
- message, platform="volcengine_caption",
- retryable=retryable, error_type=error_type,
+ message,
+ platform="volcengine_caption",
+ retryable=retryable,
+ error_type=error_type,
status_code=status,
)
@@ -124,7 +128,7 @@ class VolcengineCaptionProvider:
max_lines: int = 1,
) -> dict[str, Any]:
"""提交字幕生成任务,返回 {id: task_id}"""
- params = {
+ params: dict[str, str | int] = {
"appid": self.appid,
"language": language,
"caption_type": caption_type,
@@ -150,11 +154,15 @@ class VolcengineCaptionProvider:
except PlatformError:
raise
except httpx.HTTPStatusError as e:
- raise _map_caption_error(e.response.status_code, f"HTTP错误: {e.response.status_code}") from e
+ raise _map_caption_error(
+ e.response.status_code, f"HTTP错误: {e.response.status_code}"
+ ) from e
except (httpx.NetworkError, httpx.TimeoutException) as e:
raise PlatformError(
- f"字幕服务网络错误: {e}", platform="volcengine_caption",
- retryable=True, error_type=PlatformErrorType.TIMEOUT,
+ f"字幕服务网络错误: {e}",
+ platform="volcengine_caption",
+ retryable=True,
+ error_type=PlatformErrorType.TIMEOUT,
) from e
except Exception as e:
raise _map_caption_error(500, f"提交任务失败: {str(e)}") from e
@@ -165,7 +173,7 @@ class VolcengineCaptionProvider:
blocking: bool = False,
) -> dict[str, Any]:
"""查询字幕任务结果,返回原始 JSON"""
- params = {
+ params: dict[str, str | int] = {
"appid": self.appid,
"id": task_id,
"blocking": 1 if blocking else 0,
@@ -182,11 +190,15 @@ class VolcengineCaptionProvider:
except PlatformError:
raise
except httpx.HTTPStatusError as e:
- raise _map_caption_error(e.response.status_code, f"HTTP错误: {e.response.status_code}") from e
+ raise _map_caption_error(
+ e.response.status_code, f"HTTP错误: {e.response.status_code}"
+ ) from e
except (httpx.NetworkError, httpx.TimeoutException) as e:
raise PlatformError(
- f"字幕服务网络错误: {e}", platform="volcengine_caption",
- retryable=True, error_type=PlatformErrorType.TIMEOUT,
+ f"字幕服务网络错误: {e}",
+ platform="volcengine_caption",
+ retryable=True,
+ error_type=PlatformErrorType.TIMEOUT,
) from e
except Exception as e:
raise _map_caption_error(500, f"查询任务失败: {str(e)}") from e
@@ -201,7 +213,7 @@ class VolcengineCaptionProvider:
sta_punc_mode: int = 3,
) -> dict[str, Any]:
"""提交自动字幕打轴任务,返回 {id: task_id}"""
- params = {
+ params: dict[str, str | int] = {
"appid": self.appid,
"caption_type": caption_type,
"sta_punc_mode": sta_punc_mode,
@@ -218,7 +230,9 @@ class VolcengineCaptionProvider:
response.raise_for_status()
data = response.json()
if "id" not in data:
- raise _map_caption_error(500, f"提交打轴任务失败: {data.get('message', '未知错误')}")
+ raise _map_caption_error(
+ 500, f"提交打轴任务失败: {data.get('message', '未知错误')}"
+ )
return data
except PlatformError:
raise
diff --git a/python-api/app/ai/providers/volcengine_provider.py b/python-api/app/ai/providers/volcengine_provider.py
index 0e2a5d5..9f029a0 100644
--- a/python-api/app/ai/providers/volcengine_provider.py
+++ b/python-api/app/ai/providers/volcengine_provider.py
@@ -291,27 +291,40 @@ class VolcengineProvider(LLMProvider):
if status == 429 or "rate limit" in message.lower():
return PlatformError(
- message, platform="volcengine_ark", retryable=True,
- error_type=PlatformErrorType.RATE_LIMIT, status_code=status,
+ message,
+ platform="volcengine_ark",
+ retryable=True,
+ error_type=PlatformErrorType.RATE_LIMIT,
+ status_code=status,
)
elif status in (401, 403) or "authentication" in message.lower():
return PlatformError(
- message, platform="volcengine_ark", retryable=False,
- error_type=PlatformErrorType.AUTH_FAILED, status_code=status,
+ message,
+ platform="volcengine_ark",
+ retryable=False,
+ error_type=PlatformErrorType.AUTH_FAILED,
+ status_code=status,
)
elif status and status >= 500:
return PlatformError(
- message, platform="volcengine_ark", retryable=True,
- error_type=PlatformErrorType.SERVER_ERROR, status_code=status,
+ message,
+ platform="volcengine_ark",
+ retryable=True,
+ error_type=PlatformErrorType.SERVER_ERROR,
+ status_code=status,
)
elif "timeout" in message.lower() or isinstance(e, TimeoutError):
return PlatformError(
- message, platform="volcengine_ark", retryable=True,
+ message,
+ platform="volcengine_ark",
+ retryable=True,
error_type=PlatformErrorType.TIMEOUT,
)
else:
return PlatformError(
- message, platform="volcengine_ark", retryable=False,
+ message,
+ platform="volcengine_ark",
+ retryable=False,
error_type=PlatformErrorType.UNKNOWN,
)
+331
View File
@@ -0,0 +1,331 @@
diff --git a/python-api/app/services/point_service.py b/python-api/app/services/point_service.py
index 0e5d79e..709f78d 100644
--- a/python-api/app/services/point_service.py
+++ b/python-api/app/services/point_service.py
@@ -25,7 +25,7 @@ import logging
import math
from datetime import UTC, datetime, timedelta
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
import yaml
from sqlalchemy import select
@@ -33,6 +33,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
+from app.core.exceptions import InsufficientPointsException
from app.models.point_batch import PointBatch
from app.models.point_transaction import PointTransaction
from app.models.user_point import UserPoint
@@ -46,11 +47,11 @@ if TYPE_CHECKING:
_CONFIG_PATH = Path(__file__).resolve().parents[2] / "config" / "points-config.yaml"
-def _load_points_config() -> dict:
+def _load_points_config() -> dict[str, Any]:
"""加载积分计费配置。服务启动时读取一次,后续内存中使用。"""
if not _CONFIG_PATH.exists():
raise FileNotFoundError(f"积分配置文件不存在: {_CONFIG_PATH}")
- with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
+ with open(_CONFIG_PATH, encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
# 合并为统一的查询字典:source_type -> {"mode": "fixed|duration|free", ...}
merged: dict[str, dict] = {}
@@ -65,18 +66,22 @@ def _load_points_config() -> dict:
return merged
-POINTS_CONFIG: dict[str, dict] = _load_points_config()
+POINTS_CONFIG: dict[str, Any] = _load_points_config()
def get_recharge_options() -> list[dict]:
"""获取充值档位配置(由后端控制,支持积分赠送)"""
- return POINTS_CONFIG.get("_recharge_options", [])
+ options = POINTS_CONFIG.get("_recharge_options", [])
+ if isinstance(options, list):
+ return options
+ return []
def get_chargeable_source_types() -> list[str]:
"""获取所有需要扣费的业务类型列表(排除免费业务)"""
return [
- key for key, cfg in POINTS_CONFIG.items()
+ key
+ for key, cfg in POINTS_CONFIG.items()
if not key.startswith("_") and cfg.get("mode") != "free"
]
@@ -163,11 +168,10 @@ def _estimate_max_cost(source_type: str, param: dict | None = None) -> int:
# ── 余额查询 ──────────────────────────────────────────
+
async def get_user_balance(db: AsyncSession, user_id: UUID | str) -> dict:
"""获取用户积分余额快照(实时计算,排除已过期批次)。"""
- result = await db.execute(
- select(UserPoint).where(UserPoint.user_id == user_id)
- )
+ result = await db.execute(select(UserPoint).where(UserPoint.user_id == user_id))
up = result.scalar_one_or_none()
if not up:
@@ -182,8 +186,7 @@ async def get_user_balance(db: AsyncSession, user_id: UUID | str) -> dict:
from sqlalchemy import func as _func
available_result = await db.execute(
- select(_func.coalesce(_func.sum(PointBatch.remaining), 0))
- .where(
+ select(_func.coalesce(_func.sum(PointBatch.remaining), 0)).where(
PointBatch.user_id == user_id,
PointBatch.remaining > 0,
PointBatch.expired_at > _now(),
@@ -221,6 +224,7 @@ async def check_balance(
# ── 充值 ──────────────────────────────────────────────
+
async def recharge(
db: AsyncSession,
*,
@@ -247,8 +251,7 @@ async def recharge(
# 幂等保护:同一笔订单(order_id)只能充值一次
if order_id:
existing_result = await db.execute(
- select(PointTransaction)
- .where(
+ select(PointTransaction).where(
PointTransaction.source_id == str(order_id),
PointTransaction.type == "recharge",
)
@@ -259,9 +262,7 @@ async def recharge(
return existing_tx
# 1. 获取或创建用户积分账户
- result = await db.execute(
- select(UserPoint).where(UserPoint.user_id == user_id)
- )
+ result = await db.execute(select(UserPoint).where(UserPoint.user_id == user_id))
up = result.scalar_one_or_none()
if not up:
@@ -353,7 +354,7 @@ async def consume(
直接扣费(后置计费)。
业务执行成功后调用,按实际消耗直接扣除余额。
- 默认不允许欠费(余额不足时抛出 ValueError)。
+ 默认不允许欠费(余额不足时抛出 InsufficientPointsException)。
Scheduler 后置扣费等场景可设置 allow_negative=True,允许余额变负。
:param points: 实际消耗积分(正整数)
@@ -383,9 +384,7 @@ async def consume(
# 2. 获取用户积分账户(加锁)
result = await db.execute(
- select(UserPoint)
- .where(UserPoint.user_id == user_id)
- .with_for_update()
+ select(UserPoint).where(UserPoint.user_id == user_id).with_for_update()
)
up = result.scalar_one_or_none()
@@ -404,7 +403,7 @@ async def consume(
# 3. 余额检查:用实时可用余额(未过期批次 remaining 总和),避免 expire_batches 延迟导致超扣
available = sum(b.remaining for b in batches)
if not allow_negative and available < points:
- raise ValueError(f"积分不足,当前可用余额 {available},需要 {points} 积分")
+ raise InsufficientPointsException(f"积分不足,当前可用余额 {available},需要 {points} 积分")
remaining_to_deduct = points
for batch in batches:
@@ -440,6 +439,7 @@ async def consume(
# ── 过期回收 ──────────────────────────────────────────
+
async def expire_batches(db: AsyncSession) -> int:
"""
回收过期积分批次。返回过期积分总数。
@@ -468,9 +468,7 @@ async def expire_batches(db: AsyncSession) -> int:
# 获取用户积分账户(加锁)
up_result = await db.execute(
- select(UserPoint)
- .where(UserPoint.user_id == batch.user_id)
- .with_for_update()
+ select(UserPoint).where(UserPoint.user_id == batch.user_id).with_for_update()
)
up = up_result.scalar_one_or_none()
if not up:
diff --git a/python-api/app/services/script_service.py b/python-api/app/services/script_service.py
index 49aa4b1..60f58d5 100644
--- a/python-api/app/services/script_service.py
+++ b/python-api/app/services/script_service.py
@@ -7,9 +7,16 @@ import asyncio
import logging
import time
from pathlib import Path
+from typing import Any
from app.ai.model_router import get_model_router
from app.ai.prompts import load_prompt_file, load_script_user_prompt
+from app.core.exceptions import (
+ AIEmptyResponseException,
+ AIParseErrorException,
+ AITimeoutException,
+ PromptNotFoundException,
+)
from app.schemas.script import ScriptShot
from app.services.ai_response_utils import (
safe_parse_ai_json_response,
@@ -22,12 +29,9 @@ logger = logging.getLogger(__name__)
class ScriptService:
"""脚本生成服务"""
-
def __init__(self):
self.prompts_dir = Path(__file__).parent.parent / "ai" / "prompts"
-
-
def _load_prompt(self, name: str) -> str:
"""加载 Prompt 模板"""
prompt_file = self.prompts_dir / f"{name}.txt"
@@ -58,7 +62,7 @@ class ScriptService:
# 加载 Prompt
system_prompt = load_prompt_file(category, filename)
if not system_prompt:
- raise ValueError(f"未找到提示词: category={category}, filename={filename}")
+ raise PromptNotFoundException(f"未找到提示词: category={category}, filename={filename}")
# 用户提示词
user_prompt = load_script_user_prompt(
@@ -75,24 +79,26 @@ class ScriptService:
)
if not result.content or not result.content.strip():
- raise ValueError("AI 返回内容为空,请检查模型配置或重试")
+ raise AIEmptyResponseException("AI 返回内容为空,请检查模型配置或重试")
success, parsed_data, error_msg = safe_parse_ai_json_response(result.content)
if not success:
- raise ValueError(error_msg or "AI 返回格式错误,无法解析为 JSON")
+ raise AIParseErrorException(error_msg or "AI 返回格式错误,无法解析为 JSON")
try:
shots_data = validate_and_normalize_shots(parsed_data)
if not shots_data:
- raise ValueError("AI 返回的分镜数据为空或格式不正确")
+ raise AIEmptyResponseException("AI 返回的分镜数据为空或格式不正确")
shots = [ScriptShot(**shot) for shot in shots_data]
return shots
+ except (AIEmptyResponseException, AIParseErrorException):
+ raise
except Exception as e:
- raise ValueError(f"分镜数据处理失败: {str(e)}")
+ raise AIParseErrorException(f"分镜数据处理失败: {str(e)}")
async def polish_content(
self,
@@ -144,21 +150,23 @@ class ScriptService:
)
return result.content.strip()
except TimeoutError:
- raise ValueError("润色请求超时,请重试")
+ raise AITimeoutException("润色请求超时,请重试")
+ except (AIEmptyResponseException, AIParseErrorException, AITimeoutException):
+ raise
except Exception as e:
- raise ValueError(f"润色失败: {str(e)}")
+ raise AIParseErrorException(f"润色失败: {str(e)}")
async def check_model_health(self) -> dict:
"""检查模型健康状态"""
model_router = await get_model_router()
health_results = await model_router.health_check()
- models = []
+ models: list[dict[str, Any]] = []
available_count = 0
- recommended = None
+ recommended: dict[str, Any] | None = None
for _provider_id, health in health_results.items():
- model_info = {
+ model_info: dict[str, Any] = {
"id": health.id,
"name": health.name,
"is_available": health.is_available,
@@ -169,9 +177,12 @@ class ScriptService:
if health.is_available:
available_count += 1
- if recommended is None or health.response_time < recommended.get(
- "response_time", float("inf")
- ):
+ current_best = (
+ float("inf")
+ if recommended is None
+ else float(recommended.get("response_time") or float("inf"))
+ )
+ if health.response_time < current_best:
recommended = model_info
total = len(models)
@@ -188,7 +199,6 @@ class ScriptService:
"""测试指定模型连接"""
model_router = await get_model_router()
-
start_time = time.time()
try:
diff --git a/python-api/app/services/vidu_service.py b/python-api/app/services/vidu_service.py
index 054823f..61bbdcd 100644
--- a/python-api/app/services/vidu_service.py
+++ b/python-api/app/services/vidu_service.py
@@ -207,8 +207,9 @@ class ViduService:
error_type=PlatformErrorType.BAD_REQUEST,
)
- logger.info(f"[Vidu Clone] 复刻成功: voice_id={result.data.get('voice_id')}")
- return result.data or {}
+ clone_data = result.data or {}
+ logger.info(f"[Vidu Clone] 复刻成功: voice_id={clone_data.get('voice_id')}")
+ return clone_data
async def query_clone_task(self, voice_id: str) -> dict[str, Any]:
"""Vidu 声音复刻是同步接口,无独立查询。
@@ -270,6 +271,8 @@ class ViduService:
result_data = status.result or {}
return {
"state": ViduAdapter.denormalize_state(status.state),
- "creations": [{"url": result_data.get("video_url")}] if result_data.get("video_url") else [],
+ "creations": (
+ [{"url": result_data.get("video_url")}] if result_data.get("video_url") else []
+ ),
"message": status.error_message,
}
diff --git a/python-api/app/services/volcengine_caption_service.py b/python-api/app/services/volcengine_caption_service.py
index 83f59b4..9a565a5 100644
--- a/python-api/app/services/volcengine_caption_service.py
+++ b/python-api/app/services/volcengine_caption_service.py
@@ -155,10 +155,7 @@ class VolcengineCaptionService:
error_type=PlatformErrorType.BAD_REQUEST,
)
- logger.warning(
- f"{task_name}超过最大轮询次数: task_id={task_id}, "
- f"retries={retries}"
- )
+ logger.warning(f"{task_name}超过最大轮询次数: task_id={task_id}, " f"retries={retries}")
raise PlatformError(
f"{task_name}超时,请稍后重试",
platform="volcengine_caption",