565148e66c
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
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 / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web 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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m38s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m55s
AI Code Review / AI Code Review (pull_request) Successful in 6m27s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 13m8s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m15s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m25s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m30s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 6m22s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 26s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m42s
业务侧(LipsyncService)集成 GpuLipsyncService: - 新增开关 USE_GPU_LIPSYNC(env,默认false;staging默认true方便联调,生产默认false待稳定后开) - _submit_audio_direct 在签名提交 MediaKit 前判断: * 开关关闭 → 直接走现有 MediaKit 云端 lipsync * 开关开但无可用 Worker(last_heartbeat_at 超5分钟窗口) → 回退 MediaKit * 开关开+Worker可用 → 创建GPU任务→同步轮询等待结果 - GPU路径成功:直接标记 job.status=completed、output_video_url=签名后的结果URL - GPU超时(默认1200s)/终态failed/异常 → 全部回退 MediaKit,对用户透明 - 轮询间隔默认5s,可配 GPU_LIPSYNC_POLL_INTERVAL - Worker心跳新鲜度窗口可配 GPU_WORKER_STALE_SECONDS(默认300s) GpuLipsyncService新增方法: - has_available_worker():判断5分钟内有心跳的Worker存在 - wait_for_result(task_id, timeout, poll_interval):同步轮询DB等待终态, 期间顺手 _recover_timed_out_tasks;超时返回None让调用方兜底 env/CI配置: - .env.example 新增4个配置项注释 - deploy/configs/.env.staging USE_GPU_LIPSYNC=true(联调) - deploy/configs/.env.production USE_GPU_LIPSYNC=false(暂不开) 单元测试:tests/unit/test_lipsync_gpu_integration.py 9个用例覆盖: - 开关关不调用GPU、无Worker回退、GPU成功completed、超时回退、 failed回退、异常回退、has_available_worker三种场景 - 全部 30个GPU相关测试全绿(含此前11+10个)
158 lines
7.8 KiB
Python
Executable File
158 lines
7.8 KiB
Python
Executable File
"""统一配置基类 — 所有服务共享的基础配置。
|
||
|
||
数据库/Redis/OSS/Celery/AI服务等通用配置统一定义在此。
|
||
API 和 Worker 各自的 Settings 类继承本类,只追加服务特有字段。
|
||
单例模式和 env 文件加载逻辑也统一在这里实现。
|
||
"""
|
||
|
||
import os
|
||
from typing import Optional, TypeVar
|
||
|
||
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"
|
||
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
|
||
|
||
# ── 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-1-6-250615"
|
||
doubao_base_url: str = "https://ark.cn-beijing.volces.com/api/v3"
|
||
doubao_timeout: int = 30
|
||
doubao_max_retries: int = 2
|
||
doubao_vision_model: str = "doubao-1-5-vision-pro-250915"
|
||
|
||
# ── MediaKit (火山引擎 AI 媒体工具) ──────────────────────────────────
|
||
mediakit_api_key: str = ""
|
||
mediakit_base_url: str = "https://mediakit.cn-beijing.volces.com/api/v1"
|
||
mediakit_timeout: int = 60
|
||
|
||
# ── 积分/会员系统 (#1895) ────────────────────────────────────────────
|
||
# 总开关:默认 false(对所有用户零影响),P2 路由逐个接入时用
|
||
# `if settings.points_enabled:` 包裹,防止未完善的扣点逻辑影响现有用户。
|
||
points_enabled: bool = 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
|
||
|
||
@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)
|