merge main
This commit is contained in:
@@ -162,6 +162,18 @@ class AdminCreditRecordSummaryOut(BaseModel):
|
||||
total_recharge: float = 0.0
|
||||
total_consume: float = 0.0
|
||||
total_refund: float = 0.0
|
||||
# 消费类分解(仅 type=consume,不含 team_internal 团队内部转账;真实扣费 + 预扣占用 = total_consume)
|
||||
# - total_charge : 真实扣费 charge(含历史 NULL),对应"筛选明细类型=消费 且 action=charge/NULL"求和
|
||||
# - total_hold : 预扣占用 hold
|
||||
total_charge: float = 0.0
|
||||
total_hold: float = 0.0
|
||||
# 回退类分解(仅 type=refund;真实退款 + 预扣释放 = total_refund)
|
||||
# - total_refund_real : 真实退款 refund(含历史 NULL)
|
||||
# - total_hold_release: 预扣释放 hold_release
|
||||
total_refund_real: float = 0.0
|
||||
total_hold_release: float = 0.0
|
||||
# 净消耗 = max(total_consume - total_refund, 0) = 实际"用掉了"的积分
|
||||
net_consume: float = 0.0
|
||||
transaction_count: int = 0
|
||||
generation_count: int = 0
|
||||
generation_attempt_count: int = 0
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from app.schemas.admin_api.api_key import (
|
||||
ApiKeyCreateRequest,
|
||||
ApiKeyUpdateRequest,
|
||||
ApiKeyResponse,
|
||||
ApiKeyCreateResponse,
|
||||
ApiKeyListItem,
|
||||
ApiKeyListOut,
|
||||
)
|
||||
from app.schemas.admin_api.api_upscale import (
|
||||
ApiUpscaleConfigData,
|
||||
ApiUpscaleConfigSaveRequest,
|
||||
ApiUpscaleConfigResponse,
|
||||
)
|
||||
from app.schemas.admin_api.api_usage import (
|
||||
ApiUsageLogResponse,
|
||||
ApiUsageSummaryResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ApiKeyCreateRequest",
|
||||
"ApiKeyUpdateRequest",
|
||||
"ApiKeyResponse",
|
||||
"ApiKeyCreateResponse",
|
||||
"ApiKeyListItem",
|
||||
"ApiKeyListOut",
|
||||
"ApiUpscaleConfigData",
|
||||
"ApiUpscaleConfigSaveRequest",
|
||||
"ApiUpscaleConfigResponse",
|
||||
"ApiUsageLogResponse",
|
||||
"ApiUsageSummaryResponse",
|
||||
]
|
||||
@@ -0,0 +1,156 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
# 保留供其他地方使用
|
||||
def _empty_to_null(value):
|
||||
"""将空字符串转为 None,避免 Pydantic 校验失败。"""
|
||||
if value == "" or value == "null":
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
class ApiKeyCallableModel(BaseModel):
|
||||
"""API Key 可调用模型配置。支持 camelCase 和 snake_case 两种字段名。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
engine_type: str = Field(..., description="video | image", alias="engineType")
|
||||
engine_id: str = Field(..., description="引擎ID", alias="engineId")
|
||||
model_name: str = Field(..., description="模型名称", alias="modelName")
|
||||
|
||||
|
||||
class ApiKeyCreateRequest(BaseModel):
|
||||
"""创建 API Key 请求。支持 camelCase 和 snake_case 两种字段名。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
company_name: str = Field(..., max_length=128, description="公司名称", alias="companyName")
|
||||
description: str | None = Field(None, description="备注")
|
||||
callable_models: list[ApiKeyCallableModel] = Field(
|
||||
default_factory=list, description="可调用模型列表", alias="callableModels",
|
||||
)
|
||||
quota_limit: float | None = Field(None, description="配额总量,NULL=无限", alias="quotaLimit")
|
||||
quota_cycle: str | None = Field(None, description="daily | monthly | one_time | NULL=无限", alias="quotaCycle")
|
||||
valid_from: datetime | None = Field(None, description="生效时间", alias="validFrom")
|
||||
valid_until: datetime | None = Field(None, description="过期时间", alias="validUntil")
|
||||
max_concurrent_video_tasks: int | None = Field(
|
||||
None, description="最大并发视频任务数", alias="maxConcurrentVideoTasks",
|
||||
)
|
||||
|
||||
@field_validator("valid_from", "valid_until", mode="before")
|
||||
@classmethod
|
||||
def empty_str_to_none(cls, v):
|
||||
if v == "" or v == "null" or v == 0:
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
class ApiKeyUpdateRequest(BaseModel):
|
||||
"""更新 API Key 请求。支持 camelCase 和 snake_case 两种字段名。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
company_name: str | None = Field(None, max_length=128, alias="companyName")
|
||||
description: str | None = None
|
||||
callable_models: list[ApiKeyCallableModel] | None = Field(None, alias="callableModels")
|
||||
quota_limit: float | None = Field(None, alias="quotaLimit")
|
||||
quota_cycle: str | None = Field(None, alias="quotaCycle")
|
||||
valid_from: datetime | None = Field(None, alias="validFrom")
|
||||
valid_until: datetime | None = Field(None, alias="validUntil")
|
||||
max_concurrent_video_tasks: int | None = Field(None, alias="maxConcurrentVideoTasks")
|
||||
is_active: bool | None = Field(None, alias="isActive")
|
||||
|
||||
@field_validator("valid_from", "valid_until", mode="before")
|
||||
@classmethod
|
||||
def empty_str_to_none(cls, v):
|
||||
if v == "" or v == "null" or v == 0:
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
class ApiKeyResponse(BaseModel):
|
||||
"""API Key 详情响应。"""
|
||||
|
||||
id: str
|
||||
company_name: str
|
||||
api_key_prefix: str = Field(..., description="Key 前缀,如 vk_xxxx****")
|
||||
description: str | None
|
||||
callable_models: list[ApiKeyCallableModel]
|
||||
quota_limit: float | None
|
||||
quota_cycle: str | None
|
||||
quota_used: float
|
||||
valid_from: datetime | None
|
||||
valid_until: datetime | None
|
||||
max_concurrent_video_tasks: int | None
|
||||
is_active: bool
|
||||
last_used_at: datetime | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ApiKeyCreateResponse(BaseModel):
|
||||
"""创建 API Key 响应(包含完整明文 Key,仅此一次)。"""
|
||||
|
||||
id: str
|
||||
company_name: str
|
||||
api_key: str = Field(..., description="完整 API Key,仅创建时返回一次")
|
||||
api_key_prefix: str
|
||||
valid_until: datetime | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ApiKeyRevealResponse(BaseModel):
|
||||
"""揭秘 API Key 响应(随时可获取明文)。"""
|
||||
|
||||
id: str
|
||||
company_name: str
|
||||
api_key: str = Field(..., description="完整 API Key")
|
||||
api_key_prefix: str
|
||||
|
||||
|
||||
class ApiKeyListItem(BaseModel):
|
||||
"""API Key 列表项。"""
|
||||
|
||||
id: str
|
||||
company_name: str
|
||||
api_key_prefix: str
|
||||
description: str | None
|
||||
callable_models: list[ApiKeyCallableModel] = []
|
||||
quota_limit: float | None
|
||||
quota_cycle: str | None
|
||||
quota_used: float
|
||||
is_active: bool
|
||||
valid_from: datetime | None
|
||||
valid_until: datetime | None
|
||||
max_concurrent_video_tasks: int | None
|
||||
last_used_at: datetime | None
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ApiKeyQuotaAdjustRequest(BaseModel):
|
||||
"""配额调整请求。支持 camelCase 和 snake_case 两种字段名。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
action: str = Field(
|
||||
...,
|
||||
pattern=r"^(adjust|reset_usage|set_limit|change_cycle)$",
|
||||
description="adjust=增加总额 | reset_usage=重置已用 | set_limit=设置限额 | change_cycle=修改周期",
|
||||
)
|
||||
quota_limit_delta: float | None = Field(None, ge=0, description="增加总额时的增量", alias="quotaLimitDelta")
|
||||
quota_limit: float | None = Field(None, description="设置新限额时的值(NULL=无限)", alias="quotaLimit")
|
||||
quota_cycle: str | None = Field(None, description="修改周期时的值", alias="quotaCycle")
|
||||
reason: str | None = Field(None, max_length=500, description="调整原因/备注")
|
||||
|
||||
|
||||
class ApiKeyListOut(BaseModel):
|
||||
"""API Key 列表响应。"""
|
||||
|
||||
total: int
|
||||
items: list[ApiKeyListItem]
|
||||
@@ -0,0 +1,33 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.schemas.common import NaiveDatetime
|
||||
|
||||
|
||||
class ApiModelPricingCreate(BaseModel):
|
||||
"""创建 API 模型价格请求。支持 camelCase 和 snake_case 两种字段名。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
model_config_id: str = Field(
|
||||
..., max_length=32, description="引擎ID", alias="modelConfigId",
|
||||
)
|
||||
gen_type: str = Field(default="video", max_length=16, description="image | video", alias="genType")
|
||||
resolution: str = Field(..., max_length=16, description="分辨率")
|
||||
price_ratio: float = Field(default=1.0, gt=0, description="价格系数", alias="priceRatio")
|
||||
base_price: float = Field(default=0.0, ge=0, description="基础价格(元)", alias="basePrice")
|
||||
per_second_price: float = Field(default=0.0, ge=0, description="每秒价格(元)", alias="perSecondPrice")
|
||||
input_video_ratio: float = Field(default=1.0, ge=0, description="传入视频系数", alias="inputVideoRatio")
|
||||
input_video_base_price: float = Field(default=0.0, ge=0, description="传入视频基础价(元)", alias="inputVideoBasePrice")
|
||||
input_video_per_second_price: float = Field(default=0.0, ge=0, description="传入视频每秒价(元)", alias="inputVideoPerSecondPrice")
|
||||
input_image_ratio: float = Field(default=1.0, ge=0, description="传入图片系数", alias="inputImageRatio")
|
||||
input_image_base_price: float = Field(default=0.0, ge=0, description="传入图片基础价(元)", alias="inputImageBasePrice")
|
||||
input_image_per_image_price: float = Field(default=0.0, ge=0, description="传入图片每张价(元)", alias="inputImagePerImagePrice")
|
||||
|
||||
|
||||
class ApiModelPricingOut(ApiModelPricingCreate):
|
||||
"""API 模型价格响应。"""
|
||||
|
||||
id: str
|
||||
created_at: NaiveDatetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
@@ -0,0 +1,38 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ApiUpscaleRule(BaseModel):
|
||||
"""API 超分规则。支持 camelCase 和 snake_case 两种字段名。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
target_resolution: str = Field(..., description="目标分辨率: 480p | 720p | 1080p | 2K | 4K", alias="targetResolution")
|
||||
provider_generation_resolution: str = Field(..., description="供应商生成分辨率", alias="providerGenerationResolution")
|
||||
processor_key: str = Field(
|
||||
...,
|
||||
description="处理器: local_ffmpeg_crop_v1 | volc_standard_v1 | volc_professional_v1 | volc_large_model_v1",
|
||||
alias="processorKey",
|
||||
)
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class ApiUpscaleConfigData(BaseModel):
|
||||
"""API 超分配置数据。支持 camelCase 和 snake_case 两种字段名。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
enabled: bool = False
|
||||
delete_source_after_success: bool = Field(True, alias="deleteSourceAfterSuccess")
|
||||
rules: list[ApiUpscaleRule] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ApiUpscaleConfigSaveRequest(BaseModel):
|
||||
"""保存 API 超分配置请求。"""
|
||||
|
||||
data: ApiUpscaleConfigData
|
||||
|
||||
|
||||
class ApiUpscaleConfigResponse(BaseModel):
|
||||
"""API 超分配置响应。"""
|
||||
|
||||
data: ApiUpscaleConfigData
|
||||
@@ -0,0 +1,39 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ApiUsageLogResponse(BaseModel):
|
||||
"""API 使用日志响应。"""
|
||||
|
||||
id: str
|
||||
api_key_id: str
|
||||
api_generation_task_id: str | None
|
||||
request_type: str
|
||||
model_name: str
|
||||
gen_type: str
|
||||
credits_cost: float
|
||||
tokens_used: int
|
||||
request_duration_ms: int
|
||||
status: str
|
||||
error_message: str | None
|
||||
error_code: str | None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ApiUsageSummaryResponse(BaseModel):
|
||||
"""API 使用汇总响应。"""
|
||||
|
||||
total_requests: int
|
||||
total_credits_cost: float
|
||||
total_tokens_used: int
|
||||
success_count: int
|
||||
failed_count: int
|
||||
avg_duration_ms: int
|
||||
total: int = 0
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
items: list[ApiUsageLogResponse]
|
||||
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class VpV3QuotaConfigData(BaseModel):
|
||||
"""后台保存虚拟素材库配额。"""
|
||||
|
||||
project_limit: int = Field(0, ge=0, description="虚拟项目上限,0=不可创建")
|
||||
asset_limit: int = Field(0, ge=0, description="虚拟素材总数上限,0=不可上传")
|
||||
storage_mb_limit: int = Field(0, ge=0, description="存储上限 MB,0=不可上传文件")
|
||||
remark: str | None = Field(None, max_length=500, description="后台备注")
|
||||
|
||||
|
||||
class VpV3QuotaConfigResponse(BaseModel):
|
||||
"""虚拟素材库配额响应。"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
api_key_id: str = Field(description="API Key ID")
|
||||
|
||||
# 上限
|
||||
project_limit: int = Field(0, description="虚拟项目上限")
|
||||
asset_limit: int = Field(0, description="虚拟素材上限")
|
||||
storage_mb_limit: int = Field(0, description="存储上限 MB")
|
||||
remark: str | None = Field(None, description="备注")
|
||||
|
||||
# 已使用
|
||||
project_used: int = Field(0, description="已创建项目数")
|
||||
asset_used: int = Field(0, description="已上传素材数")
|
||||
storage_mb_used: float = Field(0.0, description="已使用存储 MB")
|
||||
|
||||
enabled: bool = Field(False, description="是否启用(任一上限 > 0)")
|
||||
@@ -0,0 +1,31 @@
|
||||
from app.schemas.api_v3.video import (
|
||||
ApiVideoContentPart,
|
||||
ApiVideoCreateRequest,
|
||||
ApiVideoCreateResponse,
|
||||
ApiVideoStatusResponse,
|
||||
)
|
||||
from app.schemas.api_v3.image import (
|
||||
ApiImageGenerateRequest,
|
||||
ApiImageGenerateResponse,
|
||||
)
|
||||
from app.schemas.api_v3.model import (
|
||||
ApiModelInfo,
|
||||
ApiModelsResponse,
|
||||
)
|
||||
from app.schemas.api_v3.common import (
|
||||
ApiError,
|
||||
ApiErrorResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ApiVideoContentPart",
|
||||
"ApiVideoCreateRequest",
|
||||
"ApiVideoCreateResponse",
|
||||
"ApiVideoStatusResponse",
|
||||
"ApiImageGenerateRequest",
|
||||
"ApiImageGenerateResponse",
|
||||
"ApiModelInfo",
|
||||
"ApiModelsResponse",
|
||||
"ApiError",
|
||||
"ApiErrorResponse",
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ApiError(BaseModel):
|
||||
"""API 错误详情。"""
|
||||
|
||||
code: str
|
||||
message: str
|
||||
|
||||
|
||||
class ApiErrorResponse(BaseModel):
|
||||
"""API 错误响应。"""
|
||||
|
||||
error: ApiError
|
||||
@@ -0,0 +1,34 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ApiImageGenerateRequest(BaseModel):
|
||||
"""图片生成请求。支持全量 Volcano Ark SDK 参数。"""
|
||||
|
||||
model: str = Field(..., description="模型名称, 如 doubao-seedream-5-0-260128")
|
||||
prompt: str = Field(..., description="图片描述提示词")
|
||||
size: str | None = Field("2K", description="图片尺寸: 2K | 4K 或 2048x2048")
|
||||
response_format: str | None = Field("url", description="返回格式: url | b64_json")
|
||||
watermark: bool | None = Field(False, description="是否添加水印")
|
||||
image: list[str] | None = Field(None, description="参考图片URL列表")
|
||||
output_format: str | None = Field(None, description="输出格式: jpeg | png | webp")
|
||||
sequential_image_generation: str | None = Field(
|
||||
None, description="组图模式: auto 开启"
|
||||
)
|
||||
generation_count: int | None = Field(1, ge=1, le=5, description="生成数量: 1-5")
|
||||
|
||||
|
||||
class ApiImageGenerateDataItem(BaseModel):
|
||||
"""单张图片结果。"""
|
||||
|
||||
url: str | None = None
|
||||
b64_json: str | None = None
|
||||
size: str | None = None
|
||||
output_format: str | None = None
|
||||
|
||||
|
||||
class ApiImageGenerateResponse(BaseModel):
|
||||
"""图片生成响应(同步返回)。"""
|
||||
|
||||
created: int
|
||||
data: list[ApiImageGenerateDataItem]
|
||||
model: str
|
||||
@@ -0,0 +1,19 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ApiModelInfo(BaseModel):
|
||||
"""可用模型信息。"""
|
||||
|
||||
model: str = Field(..., description="模型名称")
|
||||
engine_type: str = Field(..., description="引擎类型: video | image")
|
||||
engine_id: str = Field(..., description="引擎ID")
|
||||
supported_ratios: list[str] | None = None
|
||||
supported_resolutions: list[str] | None = None
|
||||
supported_durations: list[int] | None = None
|
||||
supported_sizes: list[str] | None = None
|
||||
|
||||
|
||||
class ApiModelsResponse(BaseModel):
|
||||
"""可用模型列表响应。"""
|
||||
|
||||
models: list[ApiModelInfo]
|
||||
@@ -0,0 +1,135 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class ApiVideoContentPart(BaseModel):
|
||||
"""视频生成内容部分:文本/图片/视频/音频参考。"""
|
||||
|
||||
type: str = Field(..., description="内容类型: text | image_url | video_url | audio_url")
|
||||
text: str | None = None
|
||||
image_url: dict | None = Field(None, description="图片URL对象: {\"url\": \"...\"}")
|
||||
video_url: dict | None = Field(None, description="视频URL对象: {\"url\": \"...\"}")
|
||||
audio_url: dict | None = Field(None, description="音频URL对象: {\"url\": \"...\"}")
|
||||
role: str | None = Field(
|
||||
None,
|
||||
description="参考角色: first_frame | last_frame | reference_image | reference_video | reference_audio",
|
||||
)
|
||||
|
||||
@field_validator("type")
|
||||
@classmethod
|
||||
def validate_type(cls, v):
|
||||
allowed = {"text", "image_url", "video_url", "audio_url"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"type 必须是 {allowed} 之一,当前值: {v}")
|
||||
return v
|
||||
|
||||
@field_validator("role")
|
||||
@classmethod
|
||||
def validate_role(cls, v, info):
|
||||
if v is None:
|
||||
return v
|
||||
type_value = info.data.get("type")
|
||||
role_map = {
|
||||
"image_url": {"first_frame", "last_frame", "reference_image"},
|
||||
"video_url": {"reference_video"},
|
||||
"audio_url": {"reference_audio"},
|
||||
}
|
||||
allowed_roles = role_map.get(type_value, set())
|
||||
if v not in allowed_roles:
|
||||
raise ValueError(
|
||||
f"type={type_value} 时 role 必须是 {allowed_roles} 之一,当前值: {v}"
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("image_url")
|
||||
@classmethod
|
||||
def validate_image_url(cls, v, info):
|
||||
if v is None:
|
||||
return v
|
||||
type_value = info.data.get("type")
|
||||
if type_value == "image_url" and (not v or not v.get("url")):
|
||||
raise ValueError("type=image_url 时 image_url.url 不能为空")
|
||||
return v
|
||||
|
||||
@field_validator("video_url")
|
||||
@classmethod
|
||||
def validate_video_url(cls, v, info):
|
||||
if v is None:
|
||||
return v
|
||||
type_value = info.data.get("type")
|
||||
if type_value == "video_url" and (not v or not v.get("url")):
|
||||
raise ValueError("type=video_url 时 video_url.url 不能为空")
|
||||
return v
|
||||
|
||||
@field_validator("audio_url")
|
||||
@classmethod
|
||||
def validate_audio_url(cls, v, info):
|
||||
if v is None:
|
||||
return v
|
||||
type_value = info.data.get("type")
|
||||
if type_value == "audio_url" and (not v or not v.get("url")):
|
||||
raise ValueError("type=audio_url 时 audio_url.url 不能为空")
|
||||
return v
|
||||
|
||||
|
||||
class ApiVideoCreateRequest(BaseModel):
|
||||
"""视频生成请求。支持全量 Volcano Ark SDK 参数。"""
|
||||
|
||||
model: str = Field(..., description="模型名称, 如 doubao-seedance-2-0-260128")
|
||||
content: list[ApiVideoContentPart] = Field(
|
||||
..., min_length=1, description="生成内容: 文本提示词 + 可选的图片/视频/音频参考"
|
||||
)
|
||||
ratio: str | None = Field("16:9", description="视频比例: 16:9 | 9:16 | 1:1 | 4:3 | 3:4 | 21:9")
|
||||
duration: int | None = Field(5, ge=3, le=30, description="视频时长(秒): 3-30")
|
||||
resolution: str | None = Field("480p", description="分辨率: 480p | 720p | 1080p")
|
||||
generate_audio: bool | None = Field(True, description="是否生成音频")
|
||||
watermark: bool | None = Field(False, description="是否添加水印")
|
||||
idempotency_key: str | None = Field(None, description="幂等键,防止重复创建")
|
||||
|
||||
@field_validator("ratio")
|
||||
@classmethod
|
||||
def validate_ratio(cls, v):
|
||||
if v is None:
|
||||
return v
|
||||
allowed = {"16:9", "9:16", "1:1", "4:3", "3:4", "21:9"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"ratio 必须是 {allowed} 之一,当前值: {v}")
|
||||
return v
|
||||
|
||||
@field_validator("resolution")
|
||||
@classmethod
|
||||
def validate_resolution(cls, v):
|
||||
if v is None:
|
||||
return v
|
||||
allowed = {"480p", "720p", "1080p"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"resolution 必须是 {allowed} 之一,当前值: {v}")
|
||||
return v
|
||||
|
||||
|
||||
class ApiVideoCreateResponse(BaseModel):
|
||||
"""视频任务创建响应。"""
|
||||
|
||||
id: str = Field(..., description="任务ID")
|
||||
|
||||
|
||||
class ApiVideoContent(BaseModel):
|
||||
"""视频内容(成功时返回)。"""
|
||||
|
||||
video_url: str = Field(..., description="视频URL")
|
||||
|
||||
|
||||
class ApiVideoStatusResponse(BaseModel):
|
||||
"""视频任务状态查询响应。"""
|
||||
|
||||
id: str = Field(..., description="任务ID")
|
||||
model: str = Field(..., description="模型名称")
|
||||
status: str = Field(..., description="任务状态: queued | running | succeeded | failed | expired")
|
||||
created_at: int = Field(..., description="创建时间戳(Unix)")
|
||||
updated_at: int = Field(..., description="更新时间戳(Unix)")
|
||||
content: ApiVideoContent | None = Field(None, description="视频内容(成功时返回)")
|
||||
duration: int | None = Field(None, description="视频时长(秒)")
|
||||
ratio: str | None = Field(None, description="视频比例")
|
||||
resolution: str | None = Field(None, description="分辨率")
|
||||
error: str | None = Field(None, description="错误信息(失败时返回)")
|
||||
@@ -0,0 +1,135 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from app.schemas.common import NaiveDatetimeOptional
|
||||
|
||||
|
||||
_EMAIL_REGEX = re.compile(r"^[\w.\-]+@[\w.\-]+\.\w+$")
|
||||
|
||||
|
||||
class InvoiceCreateRequest(BaseModel):
|
||||
"""创建发票请求。"""
|
||||
header_type: str = Field(..., pattern="^(personal|company)$", description="抬头类型")
|
||||
header_name: str = Field(..., min_length=1, max_length=128, description="抬头名称")
|
||||
header_tax_no: str | None = Field(None, max_length=32, description="税号")
|
||||
header_register_address: str | None = Field(None, max_length=256, description="注册地址")
|
||||
header_register_phone: str | None = Field(None, max_length=32, description="注册电话")
|
||||
header_bank_name: str | None = Field(None, max_length=128, description="开户行")
|
||||
header_bank_account: str | None = Field(None, max_length=64, description="银行账号")
|
||||
email: str = Field(..., max_length=128, description="电子邮箱(必填)")
|
||||
order_ids: list[str] = Field(..., min_length=1, description="订单ID列表")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_email(self) -> "InvoiceCreateRequest":
|
||||
if not _EMAIL_REGEX.match(self.email):
|
||||
raise ValueError("邮箱格式不正确")
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_company_fields(self) -> "InvoiceCreateRequest":
|
||||
if self.header_type == "company" and not self.header_tax_no:
|
||||
raise ValueError("企业抬头必须填写税号")
|
||||
return self
|
||||
|
||||
|
||||
class InvoiceStatusUpdateRequest(BaseModel):
|
||||
"""更新发票状态请求。"""
|
||||
status: str = Field(..., pattern="^(success|failed)$", description="目标状态")
|
||||
failure_reason: str | None = Field(None, max_length=500, description="失败原因")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_failure_reason(self) -> "InvoiceStatusUpdateRequest":
|
||||
if self.status == "failed" and not self.failure_reason:
|
||||
raise ValueError("开具失败时必须填写失败原因")
|
||||
return self
|
||||
|
||||
|
||||
class InvoiceOrderOut(BaseModel):
|
||||
"""发票关联订单响应。"""
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: str
|
||||
invoice_id: str
|
||||
order_id: str
|
||||
order_no: str
|
||||
amount: float
|
||||
credits: float
|
||||
|
||||
|
||||
class InvoiceOut(BaseModel):
|
||||
"""发票响应体。"""
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: str
|
||||
user_id: str
|
||||
invoice_no: str
|
||||
header_type: str
|
||||
header_name: str
|
||||
header_tax_no: str | None = None
|
||||
header_register_address: str | None = None
|
||||
header_register_phone: str | None = None
|
||||
header_bank_name: str | None = None
|
||||
header_bank_account: str | None = None
|
||||
email: str
|
||||
total_amount: float
|
||||
total_credits: float
|
||||
status: str
|
||||
failure_reason: str | None = None
|
||||
issued_at: NaiveDatetimeOptional = None
|
||||
created_at: NaiveDatetimeOptional = None
|
||||
updated_at: NaiveDatetimeOptional = None
|
||||
orders: list[InvoiceOrderOut] = []
|
||||
|
||||
|
||||
# ── 发票抬头 ──────────────────────────────────────────────
|
||||
|
||||
class InvoiceHeaderCreate(BaseModel):
|
||||
"""创建发票抬头请求。"""
|
||||
type: str = Field(..., pattern="^(personal|company)$", description="抬头类型")
|
||||
name: str = Field(..., min_length=1, max_length=128, description="抬头名称")
|
||||
tax_no: str | None = Field(None, max_length=32, description="税号")
|
||||
register_address: str | None = Field(None, max_length=256, description="注册地址")
|
||||
register_phone: str | None = Field(None, max_length=32, description="注册电话")
|
||||
bank_name: str | None = Field(None, max_length=128, description="开户行")
|
||||
bank_account: str | None = Field(None, max_length=64, description="银行账号")
|
||||
email: str | None = Field(None, max_length=128, description="接收邮箱")
|
||||
is_default: bool = Field(False, description="是否设为默认")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_company_fields(self) -> "InvoiceHeaderCreate":
|
||||
if self.type == "company" and not self.tax_no:
|
||||
raise ValueError("企业抬头必须填写税号")
|
||||
return self
|
||||
|
||||
|
||||
class InvoiceHeaderUpdate(BaseModel):
|
||||
"""更新发票抬头请求。"""
|
||||
name: str | None = Field(None, min_length=1, max_length=128, description="抬头名称")
|
||||
tax_no: str | None = Field(None, max_length=32, description="税号")
|
||||
register_address: str | None = Field(None, max_length=256, description="注册地址")
|
||||
register_phone: str | None = Field(None, max_length=32, description="注册电话")
|
||||
bank_name: str | None = Field(None, max_length=128, description="开户行")
|
||||
bank_account: str | None = Field(None, max_length=64, description="银行账号")
|
||||
email: str | None = Field(None, max_length=128, description="接收邮箱")
|
||||
is_default: bool | None = Field(None, description="是否设为默认")
|
||||
|
||||
|
||||
class InvoiceHeaderOut(BaseModel):
|
||||
"""发票抬头响应体。"""
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: str
|
||||
user_id: str
|
||||
type: str
|
||||
name: str
|
||||
tax_no: str | None = None
|
||||
register_address: str | None = None
|
||||
register_phone: str | None = None
|
||||
bank_name: str | None = None
|
||||
bank_account: str | None = None
|
||||
email: str | None = None
|
||||
is_default: bool = False
|
||||
created_at: NaiveDatetimeOptional = None
|
||||
updated_at: NaiveDatetimeOptional = None
|
||||
@@ -11,11 +11,11 @@ class VideoEngineCreate(BaseModel):
|
||||
model_name: str = Field(default="", max_length=128)
|
||||
supported_ratios: str = Field(default='["16:9","4:3","1:1","3:4","9:16","21:9"]')
|
||||
supported_resolutions: str = Field(default='["480p","720p","1080p"]')
|
||||
supported_durations: str = Field(default='[4,5,6,7,8,9,10,11,12,13,14,15]')
|
||||
supported_durations: str = Field(default='[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]')
|
||||
max_duration: int = Field(default=15)
|
||||
max_image_count: int = Field(default=2)
|
||||
max_video_count: int = Field(default=0)
|
||||
max_audio_count: int = Field(default=0, ge=0, le=3, description="最大参考音频数量,0 表示不支持音频参考")
|
||||
max_audio_count: int = Field(default=0, description="最大参考音频数量,0 表示不支持音频参考")
|
||||
multi_generation_enabled: bool = Field(
|
||||
default=False,
|
||||
description="是否允许客户端选择生成多个视频;关闭时客户端只能选择 1 份",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
from app.schemas.virtual_portrait_v3.common import VpV3EnumMeta
|
||||
from app.schemas.virtual_portrait_v3.quota import VpV3QuotaConfigOut
|
||||
from app.schemas.virtual_portrait_v3.project import (
|
||||
VpV3IdOut,
|
||||
VpV3ProjectCreate,
|
||||
VpV3ProjectDeleteOut,
|
||||
VpV3ProjectListOut,
|
||||
VpV3ProjectOut,
|
||||
VpV3ProjectUpdate,
|
||||
)
|
||||
from app.schemas.virtual_portrait_v3.asset import (
|
||||
VpV3AssetCreate,
|
||||
VpV3AssetDeleteOut,
|
||||
VpV3AssetListOut,
|
||||
VpV3AssetOut,
|
||||
VpV3SelectableAssetListOut,
|
||||
VpV3SelectableAssetOut,
|
||||
)
|
||||
from app.schemas.virtual_portrait_v3.upload import VpV3UploadOut
|
||||
|
||||
__all__ = [
|
||||
"VpV3EnumMeta",
|
||||
"VpV3QuotaConfigOut",
|
||||
"VpV3IdOut",
|
||||
"VpV3ProjectCreate",
|
||||
"VpV3ProjectUpdate",
|
||||
"VpV3ProjectOut",
|
||||
"VpV3ProjectListOut",
|
||||
"VpV3ProjectDeleteOut",
|
||||
"VpV3AssetCreate",
|
||||
"VpV3AssetOut",
|
||||
"VpV3AssetListOut",
|
||||
"VpV3AssetDeleteOut",
|
||||
"VpV3SelectableAssetOut",
|
||||
"VpV3SelectableAssetListOut",
|
||||
"VpV3UploadOut",
|
||||
]
|
||||
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class VpV3AssetCreate(BaseModel):
|
||||
"""在项目下创建虚拟素材请求(一步到位:接收远程 URL 先下载到本地,再同步火山)。"""
|
||||
|
||||
source_url: str = Field(
|
||||
...,
|
||||
min_length=8,
|
||||
max_length=2000,
|
||||
description=(
|
||||
"素材源 URL(必须 http/https 公网可访问的图片/视频直链,"
|
||||
"系统先将其下载保存到本地存储并占用存储配额,再同步到火山)"
|
||||
),
|
||||
)
|
||||
name: str | None = Field(
|
||||
None, max_length=100, description="素材名称(可选;不传则自动从 URL 文件名 / Content-Disposition 推断)",
|
||||
)
|
||||
asset_type: str = Field(
|
||||
..., pattern=r"^(Image|Video)$", description="素材类型:Image=图片 / Video=视频",
|
||||
)
|
||||
video_duration: float | None = Field(
|
||||
None, description="视频时长,秒(Video 可选;不传时系统自动用 ffprobe 探测;最大 60 秒)",
|
||||
)
|
||||
video_cover_url: str | None = Field(
|
||||
None, description="视频封面图 URL(可选,仅 Video 用,建议 16:9)",
|
||||
)
|
||||
|
||||
|
||||
class VpV3AssetOut(BaseModel):
|
||||
"""虚拟素材详情响应。"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
asset_id: str = Field(description="素材 ID")
|
||||
project_id: str = Field(description="所属项目 ID")
|
||||
name: str | None = Field(None, description="素材名称")
|
||||
asset_type: str = Field(description="素材类型:Image/Video")
|
||||
status: str = Field(description="素材状态")
|
||||
|
||||
# 显示 URL
|
||||
source_url: str = Field(description="原始上传 URL")
|
||||
preview_url: str | None = Field(None, description="显示用预览 URL(可能带签名过期)")
|
||||
remote_url: str | None = Field(None, description="火山返回的资源访问 URL(可能带签名过期)")
|
||||
remote_url_expired_at: datetime | None = Field(None, description="remote_url 过期时间")
|
||||
|
||||
video_duration: float | None = Field(None, description="视频时长秒")
|
||||
video_cover_url: str | None = Field(None, description="视频封面")
|
||||
file_size_bytes: int | None = Field(None, description="文件大小字节")
|
||||
mime_type: str | None = Field(None, description="MIME 类型")
|
||||
|
||||
moderation_json: dict | None = Field(None, description="火山审核 JSON(失败时可查看原因)")
|
||||
error_message: str | None = Field(None, description="失败原因")
|
||||
remote_delete_status: str = Field("none", description="远端删除状态")
|
||||
|
||||
created_at: datetime = Field(description="创建时间")
|
||||
updated_at: datetime = Field(description="最后更新时间")
|
||||
|
||||
|
||||
class VpV3AssetListOut(BaseModel):
|
||||
"""虚拟素材列表响应。"""
|
||||
|
||||
items: list[VpV3AssetOut] = Field(default_factory=list)
|
||||
total: int = Field(0, description="总数")
|
||||
page: int = Field(1, description="当前页码")
|
||||
page_size: int = Field(20, description="每页数量")
|
||||
|
||||
|
||||
class VpV3AssetDeleteOut(BaseModel):
|
||||
"""删除响应。"""
|
||||
|
||||
success: bool = Field(True)
|
||||
remote_delete_status: str = Field(description="远端删除状态")
|
||||
|
||||
|
||||
class VpV3SelectableAssetOut(BaseModel):
|
||||
"""AI 创作选择器使用的素材条目。"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
asset_id: str = Field(description="素材 ID(带入生成用 source=vp_v3_asset + asset_id)")
|
||||
project_id: str = Field(description="项目 ID")
|
||||
name: str | None = Field(None)
|
||||
asset_type: str = Field(description="Image/Video")
|
||||
status: str = Field(description="状态=Active")
|
||||
|
||||
source_url: str = Field(description="原始上传 URL")
|
||||
preview_url: str | None = Field(None, description="预览 URL(直接显示用)")
|
||||
video_duration: float | None = Field(None)
|
||||
video_cover_url: str | None = Field(None)
|
||||
file_size_bytes: int | None = Field(None)
|
||||
|
||||
created_at: datetime = Field(description="创建时间")
|
||||
|
||||
|
||||
class VpV3SelectableAssetListOut(BaseModel):
|
||||
"""AI 创作选择器素材列表。"""
|
||||
|
||||
items: list[VpV3SelectableAssetOut] = Field(default_factory=list)
|
||||
total: int = Field(0)
|
||||
page: int = Field(1)
|
||||
page_size: int = Field(20)
|
||||
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class VpV3EnumMeta(BaseModel):
|
||||
"""虚拟素材库枚举元数据。"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
asset_type: dict[str, str] = Field(description="素材类型:Image=图片 / Video=视频")
|
||||
asset_status: dict[str, str] = Field(description="素材状态:creating/审核中 active/可用 failed/失败")
|
||||
project_status: dict[str, str] = Field(description="项目状态:active/creating_remote_group/create_group_failed/deleting")
|
||||
remote_delete_status: dict[str, str] = Field(description="远端删除状态:none/pending/processing/deleted/failed")
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class VpV3ProjectCreate(BaseModel):
|
||||
"""创建虚拟素材项目请求。"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="项目名称,1-100 字符")
|
||||
description: str | None = Field(None, max_length=500, description="项目描述,最多 500 字符")
|
||||
|
||||
|
||||
class VpV3ProjectUpdate(BaseModel):
|
||||
"""更新虚拟素材项目请求。"""
|
||||
|
||||
name: str | None = Field(None, min_length=1, max_length=100, description="项目名称,1-100 字符")
|
||||
description: str | None = Field(None, max_length=500, description="项目描述,最多 500 字符")
|
||||
|
||||
|
||||
class VpV3ProjectOut(BaseModel):
|
||||
"""虚拟素材项目详情响应。"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
project_id: str = Field(description="项目 ID")
|
||||
name: str = Field(description="项目名称")
|
||||
description: str | None = Field(None, description="项目描述")
|
||||
status: str = Field(description="项目状态")
|
||||
|
||||
# 计数
|
||||
asset_count: int = Field(0, description="素材总数(含失败、删除中)")
|
||||
active_asset_count: int = Field(0, description="可用素材数(status=active)")
|
||||
image_asset_count: int = Field(0, description="图片素材数")
|
||||
video_asset_count: int = Field(0, description="视频素材数")
|
||||
storage_mb_used: float = Field(0, description="已占用存储 MB")
|
||||
|
||||
remote_delete_status: str = Field("none", description="远端删除状态")
|
||||
error_message: str | None = Field(None, description="最近一次错误信息")
|
||||
|
||||
created_at: datetime = Field(description="创建时间")
|
||||
updated_at: datetime = Field(description="最后更新时间")
|
||||
|
||||
|
||||
class VpV3ProjectListOut(BaseModel):
|
||||
"""虚拟素材项目列表响应。"""
|
||||
|
||||
items: list[VpV3ProjectOut] = Field(default_factory=list)
|
||||
total: int = Field(0, description="总数")
|
||||
page: int = Field(1, ge=1, description="当前页码")
|
||||
page_size: int = Field(20, ge=1, description="每页数量")
|
||||
|
||||
|
||||
class VpV3ProjectDeleteOut(BaseModel):
|
||||
"""删除响应。"""
|
||||
|
||||
success: bool = Field(True)
|
||||
remote_delete_status: str = Field(description="远端删除状态:none/pending/...")
|
||||
|
||||
|
||||
class VpV3IdOut(BaseModel):
|
||||
"""创建接口的简单 ID 响应。"""
|
||||
|
||||
Id: str = Field(description="资源 ID")
|
||||
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class VpV3QuotaConfigOut(BaseModel):
|
||||
"""当前 API Key 的虚拟素材配额&已使用量。"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
# 上限
|
||||
project_limit: int = Field(0, description="虚拟项目上限,0=不可创建")
|
||||
asset_limit: int = Field(0, description="虚拟素材总数上限,0=不可上传")
|
||||
storage_mb_limit: int = Field(0, description="上传存储上限 MB,0=不可上传文件")
|
||||
|
||||
# 已使用
|
||||
project_used: int = Field(0, description="已创建项目数(未删除)")
|
||||
asset_used: int = Field(0, description="已上传素材数(未删除,图片+视频)")
|
||||
storage_mb_used: float = Field(0, description="已占用存储 MB(未删除文件大小合计)")
|
||||
|
||||
enabled: bool = Field(False, description="该 API Key 是否可使用虚拟素材库功能(任一上限 > 0 即可)")
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class VpV3UploadOut(BaseModel):
|
||||
"""上传文件响应(写入 UploadResource 账本后返回)。"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
url: str = Field(description="上传后的访问 URL,创建素材时作为 source_url 传入")
|
||||
filename: str = Field(description="文件名")
|
||||
type: str = Field(description="资源类型:Image/Video")
|
||||
resource_id: str = Field(description="UploadResource 的 resource_id,创建素材时请回传 upload_resource_id")
|
||||
file_size_bytes: int = Field(0, description="文件大小字节")
|
||||
duration_seconds: float | None = Field(None, description="视频时长秒(Video 上传返回)")
|
||||
Reference in New Issue
Block a user