Files
meijiaka-zy/python-api/app/utils/audio_utils.py
T
小鱼开发 c6eba97b43 feat(points): 积分消耗系统全链路集成
后端:
- 简化积分服务: 删除 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: 积分消耗集成收尾
2026-05-09 15:42:54 +08:00

59 lines
1.7 KiB
Python
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.
"""
音频时长探测工具
================
基于 mutagen,支持从内存或文件路径探测音频时长。
不依赖 ffprobe,纯 Python 实现。
"""
from __future__ import annotations
import io
import logging
import httpx
from mutagen.mp3 import MP3
from mutagen.wave import WAVE
logger = logging.getLogger(__name__)
async def get_audio_duration(url: str, timeout: float = 10.0) -> float:
"""
探测远程音频文件的时长(秒)。
下载音频到内存后,用 mutagen 读取文件头获取时长。
不写入磁盘,全部在内存中完成。
:param url: 音频文件 URL(需可访问)
:param timeout: HTTP 下载超时(秒)
:return: 音频时长(秒,float
:raises ValueError: 无法解析时长
"""
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=timeout)
resp.raise_for_status()
data = io.BytesIO(resp.content)
# 根据文件头判断格式
header = data.read(12)
data.seek(0)
try:
if header[:3] == b"ID3" or header[:2] == b"\xff\xfb" or header[:2] == b"\xff\xf3":
audio = MP3(data)
elif header[:4] == b"RIFF":
audio = WAVE(data)
else:
# fallback:先尝试 MP3(大多数 TTS 返回 mp3
audio = MP3(data)
duration = audio.info.length
if duration is None or duration <= 0:
raise ValueError("音频时长解析失败")
return float(duration)
except Exception as e:
logger.error(f"探测音频时长失败: url={url[:60]}..., error={e}")
raise ValueError(f"无法解析音频时长: {e}") from e