Files
xiaoxia-saas/packages/config/base.py
T
xiaoxia 5bb714f25b
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m36s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m56s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m1s
AI Code Review / AI Code Review (pull_request) Successful in 6m53s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m57s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 11m18s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 11m40s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 10m48s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 14m40s
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
fix(#2181 P0): 默认关闭VLM lite + 修复单测timeout assertion
1. 单测修复:#2180把doubao_timeout默认从30提到45后,3个单测还assert 30导致CI红,同步更新为45
   - tests/unit/test_config_base.py::test_default_doubao_config
   - tests/unit/test_api_settings.py
   - tests/unit/test_ai_client.py(两处doubao_timeout=30→45)

2. doubao_vision_use_lite 默认改为 False:
   实测方舟 doubao-seed-2-1-lite-260915 模型100%超时(45.1s read timeout,3次重试全挂),
   反而 pro (ep-20260721114705-b568m) 稳定25-38s一次成功。
   默认关闭lite,直接走pro避免每步多耗136s(lite 3次超时),整体从170s/步→35s/步。
   lite模型待方舟侧恢复或确认为模型名问题后再通过ENV开启。
2026-10-04 19:32:24 +08:00

302 lines
16 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""统一配置基类 — 所有服务共享的基础配置。
数据库/Redis/OSS/Celery/AI服务等通用配置统一定义在此。
API 和 Worker 各自的 Settings 类继承本类,只追加服务特有字段。
单例模式和 env 文件加载逻辑也统一在这里实现。
"""
import os
from typing import Optional, TypeVar
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
T = TypeVar("T", bound=BaseSettings)
class SharedSettings(BaseSettings):
"""所有服务共享的基础配置。
API 和 Worker 都继承本类,确保:
1. 数据库/Redis/OSS/Celery 等核心配置默认值一致
2. 环境变量命名统一(snake_case,pydantic-settings 自动兼容大写)
3. env 文件加载逻辑只实现一次
"""
# ── 环境 ──────────────────────────────────────────────────────────────
environment: str = "development"
debug: bool = True
auto_create_schema: bool = False
# ── 数据库 ────────────────────────────────────────────────────────────
database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas"
database_pool_size: int = 20
database_max_overflow: int = 10 # pool_size(20) + max_overflow(10) = 最大30连接
database_pool_timeout: int = 30
database_pool_recycle: int = 3600
# 测试用:使用 SQLite 内存数据库(CI 环境无需 PostgreSQL)
use_in_memory_db: bool = False
# ── Redis ────────────────────────────────────────────────────────────
redis_url: str = "redis://localhost:6379/0"
# ── Celery ───────────────────────────────────────────────────────────
celery_broker_url: str = "redis://localhost:6379/0"
celery_result_backend: str = "redis://localhost:6379/1"
# ── OSS 阿里云 ──────────────────────────────────────────────────────
oss_endpoint: str = "oss-cn-hangzhou.aliyuncs.com"
# 内网 endpoint:ECS VPC 内访问 OSS 用(千兆带宽、免公网流量费)。
# 为空时自动从 oss_endpoint 推导:若 oss_endpoint 是阿里云公网域名(形如
# oss-cn-<region>.aliyuncs.com),自动加 -internal 得到内网域名;其他情况
# (自定义域名/本地 MinIO/非阿里云)回退使用 oss_endpoint。
# 显式填同值可以覆盖自动推导、强制所有流量都走公网。
oss_internal_endpoint: str = ""
oss_access_key_id: str = ""
oss_access_key_secret: str = ""
oss_bucket_name: str = "xiaoxia-autocut"
oss_direct_upload_max_mb: int = 2000
oss_direct_upload_expire_seconds: int = 900
@property
def effective_oss_internal_endpoint(self) -> str:
"""实际用于 SDK 内网访问的 endpoint(带 -internal 自动推导)。"""
if self.oss_internal_endpoint:
return self.oss_internal_endpoint
ep = self.oss_endpoint.strip()
scheme = ""
host = ep
if ep.startswith("https://"):
scheme = "https://"
host = ep[len("https://") :]
elif ep.startswith("http://"):
scheme = "http://"
host = ep[len("http://") :]
# 阿里云公网域名自动推导:oss-cn-<region>.aliyuncs.com → oss-cn-<region>-internal.aliyuncs.com
if host.endswith(".aliyuncs.com") and "-internal" not in host and host.startswith("oss-cn-"):
host = host[: -len(".aliyuncs.com")] + "-internal.aliyuncs.com"
return f"{scheme}{host}" if scheme else host
# ── CosyVoice (阿里云百炼语音合成) ───────────────────────────────────
cosyvoice_api_key: str = ""
cosyvoice_base_url: str = "https://dashscope.aliyuncs.com/api/v1"
cosyvoice_model: str = "cosyvoice-v3-flash"
cosyvoice_voice: str = "longxiaochun_v3" # 默认音色(v3 系列系统音色带 _v3 后缀)
cosyvoice_sample_rate: int = 22050
cosyvoice_format: str = "mp3" # 输出格式:mp3/wav/pcm
# 音色克隆模型名(固定为 voice-enrollment)
cosyvoice_clone_model: str = "voice-enrollment"
# ── 豆包大模型(火山引擎方舟) ────────────────────────────────────────
doubao_api_key: str = ""
doubao_model: str = "doubao-seed-2-1-pro-260915" # 推理模型(Seed 2.1 Pro,深度思考+多模态;原 seed-1-6 已下线)
doubao_fast_model: str = (
"doubao-seed-2-1-lite-260915" # 快速模型(Seed 2.1 Lite,高 RPM,编导/审核/VLM lite;原 1-5-pro-32k 已 Retiring)
)
doubao_base_url: str = "https://ark.cn-beijing.volces.com/api/v3"
doubao_timeout: int = 45 # #2180: 方舟LLM高峰期响应6-8s,原30s太紧提到45s
doubao_max_retries: int = 1 # #2180: timeout调大后一次调用就够,1次重试防偶发抖动;避免6次重试叠加到351s
doubao_vision_model: str = (
"doubao-seed-2-1-pro-260915" # 高精度视觉(Seed 2.1 Pro 原生多模态;原 vision-pro-250328 已下线)
)
doubao_vision_lite_model: str = (
"doubao-seed-2-1-lite-260915" # 快速视觉(Seed 2.1 Lite 原生多模态;原 vision-lite-250315 不可用)
)
doubao_vision_use_lite: bool = False # #2181: lite视觉模型100%超时,默认关闭走pro(25-38s稳定返回)
doubao_embedding_model: str = "doubao-embedding-vision-251215" # 多模态向量化(原 large-text-240915 已 Retiring)
doubao_video_model: str = "doubao-seedance-2-5-260628"
doubao_video_timeout: int = 600 # 视频生成轮询总超时(秒)
doubao_video_poll_interval: int = 10 # 轮询间隔(秒)
doubao_image_model: str = (
"doubao-seedream-5-0-flash-260915" # #2173: 信任链 Seedream 改 flash 模型(实测 pro 46.5s→flash 13s;pro AI化图仍被Seedance拦截)
)
doubao_image_size: str = "1K" # #2173: 1K 已足够做 Seedance 参考图,2K 在 flash 下也 22s,1K 13s
doubao_image_timeout: int = 60 # #2173: flash+1K 通常15s内,给60s余量
doubao_trust_chain_enabled: bool = (
True # #2173: 信任链总开关;若Seedream产物仍被Seedance拦截,可配 False 关闭直接t2v降级
)
# ── DashScope (阿里云百炼 Wan 3.0 等) ─────────────────────────────────
dashscope_api_key: str = ""
dashscope_base_url: str = "https://dashscope.aliyuncs.com/api/v1"
dashscope_video_timeout: int = 900 # Wan 视频任务轮询总超时(秒)
dashscope_video_poll_interval: int = 10
# ── MediaKit (火山引擎 AI 媒体工具) ──────────────────────────────────
mediakit_api_key: str = ""
mediakit_base_url: str = "https://mediakit.cn-beijing.volces.com/api/v1"
mediakit_timeout: int = 60
mediakit_cover_enabled: bool = False # 封面抽帧是否走MediaKit(默认false走本地ffmpeg+cv2,<2s完成)
# ── 积分/会员系统 (#1895) ────────────────────────────────────────────
# 积分系统总开关(产品要求 #1895:暂停积分系统但保留全部代码/表/接口)。
# - false(默认):所有 AI 功能(生成视频/口型/数字人/AI标题/TTS/克隆音色…)
# 对全部登录用户免费放行,不扣积分、不做余额拦截;积分余额/流水/会员
# 状态等查询接口保持可用,但数据不再变动。
# - 未来恢复:只需设置环境变量 ENABLE_CREDIT_SYSTEM=true。
# 旧开关 POINTS_ENABLED 仍保留作为兼容别名(两者任一为 true 即启用)。
# 主开关(推荐环境变量名 ENABLE_CREDIT_SYSTEM)
credits_enabled: bool = Field(
default=False,
validation_alias=AliasChoices("ENABLE_CREDIT_SYSTEM", "credits_enabled"),
)
# 旧开关兼容(POINTS_ENABLED);两者任一为 true 即启用
points_enabled_compat: bool = Field(
default=False,
validation_alias=AliasChoices("POINTS_ENABLED", "points_enabled_compat"),
)
@property
def points_enabled(self) -> bool:
"""旧代码/测试使用的属性名,等价于积分系统总开关(兼容别名)。"""
return bool(self.credits_enabled or self.points_enabled_compat)
@points_enabled.setter
def points_enabled(self, value: bool) -> None:
# 支持旧测试/代码 ``settings.points_enabled = True`` 的写法
self.credits_enabled = bool(value)
self.points_enabled_compat = False
# ── GPU MuseTalk 反向轮询 Worker ────────────────────────────────────
# Worker 用这个长期 Token 鉴权(不是用户 JWT)。多 Worker 共用同一个 Token;
# worker_id 用于区分具体机器。生产必须配置;development 留空会跳过校验。
gpu_worker_token: str = ""
# GPU 任务超时(秒):processing 状态超过此时长(以任务心跳为准)才回退
# pending / failed。#1970:RTX2060 6G 推理 720p 长视频需 5 分钟以上,300→900。
# Worker 推理期间每 30s 通过 /gpu/register(task_id=...) 续心跳,
# 只有真正超时或 Worker 明确上报 failed 才会回退。
gpu_task_timeout_seconds: int = 900
# 结果预签名 URL 有效期(秒)
gpu_result_url_expires: int = 3600
# 输入预签名 URL 有效期(秒,需留出 Worker 下载时间)
gpu_input_url_expires: int = 3600
# 业务侧是否启用 GPU 口型同步(开关);关或无可用 Worker 时回退 MediaKit 云端
use_gpu_lipsync: bool = False
# 业务侧轮询 GPU 任务结果的间隔(秒)
gpu_lipsync_poll_interval: float = 5.0
# 业务侧等待 GPU 任务结果的总超时(秒);超时后回退 MediaKit。
# 应小于等于 gpu_task_timeout_seconds(默认900s)+ 冗余,留足 Worker 下载/上传时间。
gpu_lipsync_wait_timeout: int = 1200
# 判断 Worker 可用的心跳新鲜度窗口(秒)—— last_heartbeat_at 在窗口内视为在线
gpu_worker_stale_seconds: int = 300
# ── P4000 NVENC 硬件编码 ────────────────────────────────────────────
# GPU 编码总开关;关闭或 endpoint 为空时始终走本机 CPU libx264
enable_gpu_encode: bool = Field(
default=False,
validation_alias=AliasChoices("ENABLE_GPU_ENCODE", "enable_gpu_encode"),
)
# P4000 编码节点地址(Tailscale 内网),例如 http://100.105.75.67:8900
gpu_encode_endpoint: str = Field(
default="",
validation_alias=AliasChoices("GPU_ENCODE_ENDPOINT", "gpu_encode_endpoint"),
)
# GPU 回传临时文件走公网/内网 nginx(/gpu-relay/ 已加 location);
# 形如 http://100.69.73.60/gpu-relay (不带尾斜杠)
gpu_encode_relay_base_url: str = Field(
default="",
validation_alias=AliasChoices("GPU_ENCODE_RELAY_BASE_URL", "gpu_encode_relay_base_url"),
description="P4000 回传结果用的外部 URL(worker 通过该 URL 提供给 P4000 PUT),如 http://100.69.73.60:8092",
)
# Worker→API 内网直连 URL(Docker DNS),用于 worker 自己下载/清理 relay 文件。
# 未配置时回退到 relay_base_url(本地开发/单节点)。
gpu_encode_relay_internal_base_url: str = Field(
default="",
validation_alias=AliasChoices("GPU_ENCODE_RELAY_INTERNAL_BASE_URL", "gpu_encode_relay_internal_base_url"),
)
# 同步调用超时(秒):含编码+上传回传,5 分钟足够短视频
gpu_encode_sync_timeout: int = 300
# 异步轮询总超时(秒):长视频走 async + 轮询
gpu_encode_async_timeout: int = 1800
# 轮询间隔(秒)
gpu_encode_poll_interval: float = 3.0
# 启动探测超时(秒)
gpu_encode_health_timeout: float = 3.0
# NVENC 默认编码参数(可被调用方覆盖)
gpu_encode_vcodec: str = "h264_nvenc"
gpu_encode_preset: str = "p4" # NVENC preset: p1(最快)~p7(最好),p4 为均衡
gpu_encode_crf: int = 23
gpu_encode_bitrate: str = "" # 空则用 crf;非空则用 -b:v 模式
# GPU 编码失败时是否自动降级到 CPU(默认 True);设为 False 可在 CI/测试中暴露错误
gpu_encode_fallback_cpu: bool = Field(
default=True,
validation_alias=AliasChoices("GPU_ENCODE_FALLBACK_CPU", "gpu_encode_fallback_cpu"),
)
# P4000 → relay 回传鉴权 token(query 参数 token=xxx)。
# 生产环境必须设置;未设置且非 production 时自动生成随机值(写日志方便排查)。
gpu_encode_relay_secret: str = Field(
default="",
validation_alias=AliasChoices("GPU_ENCODE_RELAY_SECRET", "gpu_encode_relay_secret"),
)
# Mezzanine 传输方式:relay=走Tailscale/Docker内网relay PUT(推荐,省公网OSS往返18-20s);oss=走旧公网OSS路径
gpu_encode_mezzanine_transport: str = Field(
default="relay",
validation_alias=AliasChoices("GPU_ENCODE_MEZZANINE_TRANSPORT", "gpu_encode_mezzanine_transport"),
)
# GPU 中间片在 OSS 的临时前缀(mezzanine_transport=oss 时或 relay 失败 fallback 时使用)
gpu_encode_oss_tmp_prefix: str = Field(
default="tmp/gpu-mezzanine/",
validation_alias=AliasChoices("GPU_ENCODE_OSS_TMP_PREFIX", "gpu_encode_oss_tmp_prefix"),
)
# relay 写入目录(相对于 generated-files 根目录)
gpu_encode_relay_dir: str = Field(
default="gpu_relay",
validation_alias=AliasChoices("GPU_ENCODE_RELAY_DIR", "gpu_encode_relay_dir"),
)
# relay 文件保留时间(秒),worker 下载完成后会主动删除,此为兜底清理 TTL
gpu_encode_relay_ttl: int = 3600
@property
def effective_database_url(self) -> str:
"""返回实际使用的数据库 URL。
当 USE_IN_MEMORY_DB=True 时返回 SQLite 内存 URL,否则返回 database_url。
"""
if self.use_in_memory_db:
return "sqlite:///./test.db"
return self.database_url
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── 统一单例管理 ────────────────────────────────────────────────────────
# 所有 Settings 类的单例缓存都在这里,消除每处各自实现的重复代码
_settings_cache: dict[str, BaseSettings] = {}
def _get_env_file() -> str:
"""根据 APP_ENV 决定读取哪个 env 文件。"""
env = os.getenv("APP_ENV", "development")
env_file = f".env.{env}" if env != "development" else ".env"
return env_file if os.path.exists(env_file) else ".env"
def get_cached_settings(settings_class: type[T], cache_key: Optional[str] = None) -> T:
"""统一的 Settings 单例获取函数。
所有服务都通过这个函数获取配置,消除重复的单例实现。
按类名缓存,同一类只初始化一次。
"""
key = cache_key or settings_class.__name__
if key not in _settings_cache:
env_file = _get_env_file()
_settings_cache[key] = settings_class(_env_file=env_file)
return _settings_cache[key] # type: ignore[return-value]
def reload_settings_cache() -> None:
"""清空配置缓存,下次获取时重新加载。测试用。"""
_settings_cache.clear()
def get_shared_settings() -> SharedSettings:
"""获取共享配置单例(统一入口)。"""
return get_cached_settings(SharedSettings)