c6eba97b43
后端: - 简化积分服务: 删除 freeze/settle/refund, 保留 consume/recharge/expire - 计费配置化: config/points-config.yaml 驱动 fixed/duration/free 三种模式 - TTS 时长探测: app/utils/audio_utils.py (httpx + mutagen 纯 Python) - Python 层扣费: script(5)/polish(1)/title(1)/voice_clone(200)/tts(按秒)/video(按秒) - 字幕 free_services: caption/auto_align 不扣费 - 新增 POST /points/consume 端点(402余额预检) - 新增 check_balance + /points/cost 返回 sufficient/balance/required - 新增 expire_batches 定时回收, 接入 scheduler main(每5分钟) - 删除废弃 tts_handler.py - Alembic 迁移: 删除 frozen/total_refunded 字段 - 同步 requirements.lock 添加 mutagen 前端: - Rust/IPC 层扣费: compose(5)/subtitle_burn(2)/cover_design(2) - 字幕打轴改异步: 走 scheduler subtitle handler - 对口型传 duration: VideoGeneration 传 actualDuration - 创建 pointStore: 全局余额 + fetchBalance + 充值弹窗控制 - 402 欠费弹 RechargeModal: VideoGeneration/SubtitleBurning/CoverDesign - 修复 VoiceDubbing.tsx 类型错误 (alignResult never) - 同步 PointBalance 类型(删除 frozen/available/totalRefunded) Refs: 积分消耗集成收尾
137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
"""
|
||
Async Engine 独立进程入口
|
||
=========================
|
||
|
||
usage: python -m app.scheduler.main
|
||
"""
|
||
|
||
import asyncio
|
||
import logging
|
||
import sys
|
||
|
||
from app.scheduler.engine import AsyncEngine
|
||
from app.scheduler.handlers.script_handler import ScriptHandler
|
||
from app.scheduler.handlers.subtitle_handler import SubtitleHandler
|
||
from app.scheduler.handlers.video_handler import VideoHandler
|
||
|
||
logger = logging.getLogger("scheduler")
|
||
|
||
|
||
def setup_logging() -> None:
|
||
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format=log_format,
|
||
handlers=[logging.StreamHandler(sys.stdout)],
|
||
)
|
||
|
||
|
||
async def _expire_loop(interval_seconds: float = 300.0) -> None:
|
||
"""
|
||
定时回收过期积分批次。
|
||
|
||
作为独立 task 运行,每 5 分钟执行一次。
|
||
"""
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.services import point_service
|
||
|
||
while True:
|
||
await asyncio.sleep(interval_seconds)
|
||
try:
|
||
async with AsyncSessionLocal() as db:
|
||
total = await point_service.expire_batches(db)
|
||
await db.commit()
|
||
if total > 0:
|
||
logger.info(f"[Expire] 回收过期积分 {total} 分")
|
||
except Exception:
|
||
logger.exception("[Expire] 过期积分回收失败")
|
||
|
||
|
||
async def main() -> None:
|
||
setup_logging()
|
||
|
||
# 启动过期积分回收定时任务
|
||
asyncio.create_task(_expire_loop(interval_seconds=300.0))
|
||
logger.info("过期积分回收定时任务已启动(间隔 300s)")
|
||
|
||
# 初始化共享 HTTP Client
|
||
import httpx
|
||
|
||
vidu_client = httpx.AsyncClient(
|
||
timeout=httpx.Timeout(30.0, connect=5.0),
|
||
limits=httpx.Limits(max_connections=20, max_keepalive_connections=20),
|
||
)
|
||
caption_client = httpx.AsyncClient(
|
||
timeout=httpx.Timeout(60.0, connect=5.0),
|
||
limits=httpx.Limits(max_connections=10, max_keepalive_connections=10),
|
||
)
|
||
|
||
# 初始化 PlatformGateway(统一调用入口)
|
||
from app.ai.adapters.vidu_adapter import ViduAdapter
|
||
from app.ai.adapters.volcengine_ark_adapter import VolcengineArkAdapter
|
||
from app.ai.adapters.volcengine_caption_adapter import VolcengineCaptionAdapter
|
||
from app.ai.providers.vidu_provider import ViduProvider
|
||
from app.ai.providers.volcengine_caption_provider import VolcengineCaptionProvider
|
||
from app.ai.providers.volcengine_provider import VolcengineProvider
|
||
from app.platform_gateway import PlatformGateway
|
||
from app.services.script_service import get_script_service
|
||
from app.services.vidu_service import ViduService
|
||
from app.services.volcengine_caption_service import VolcengineCaptionService
|
||
|
||
platform_gateway = PlatformGateway()
|
||
|
||
# Vidu 链路
|
||
vidu_provider = ViduProvider(client=vidu_client)
|
||
vidu_adapter = ViduAdapter(vidu_provider)
|
||
platform_gateway.register("vidu", vidu_adapter)
|
||
vidu_service = ViduService(platform_gateway)
|
||
logger.info("Vidu 链路初始化完成")
|
||
|
||
# 火山字幕链路
|
||
caption_service = None
|
||
try:
|
||
caption_provider = VolcengineCaptionProvider(client=caption_client)
|
||
caption_adapter = VolcengineCaptionAdapter(caption_provider)
|
||
platform_gateway.register("volcengine_caption", caption_adapter)
|
||
caption_service = VolcengineCaptionService(platform_gateway)
|
||
logger.info("火山字幕链路初始化完成")
|
||
except Exception as e:
|
||
logger.warning(f"火山字幕链路初始化失败: {e}")
|
||
|
||
# 火山方舟链路(脚本生成依赖)
|
||
try:
|
||
volcengine_provider = VolcengineProvider()
|
||
volcengine_ark_adapter = VolcengineArkAdapter(volcengine_provider)
|
||
platform_gateway.register("volcengine_ark", volcengine_ark_adapter)
|
||
logger.info("火山方舟链路初始化完成")
|
||
except Exception as e:
|
||
logger.warning(f"火山方舟链路初始化失败: {e}")
|
||
|
||
# 初始化 ModelRouter(传入 Gateway,确保 ScriptHandler 能调用 LLM)
|
||
try:
|
||
from app.ai.model_router import get_model_router
|
||
|
||
await get_model_router(gateway=platform_gateway)
|
||
logger.info("ModelRouter 初始化完成")
|
||
except Exception as e:
|
||
logger.warning(f"ModelRouter 初始化失败: {e}")
|
||
|
||
engine = AsyncEngine()
|
||
engine.register(SubtitleHandler(service=caption_service))
|
||
engine.register(VideoHandler(service=vidu_service))
|
||
engine.register(ScriptHandler(service=get_script_service()))
|
||
|
||
try:
|
||
await engine.run_forever(interval=5.0, min_interval=1.0)
|
||
finally:
|
||
await platform_gateway.close_all()
|
||
await vidu_client.aclose()
|
||
await caption_client.aclose()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
asyncio.run(main())
|
||
except KeyboardInterrupt:
|
||
logger.info("Scheduler stopped by user")
|