后端生成闸+多项修复;前端全站更新;QA 审计与报告
后端: - 新增 celery_health 生成前置闸——无 worker 在线时图片/视频生成入口 一律 503,防"提交到 ARK 后无人轮询、结果悬空+额度冻结"的数据丢失 - 拼接导出:帧率跟随源众数、字幕逐句重映射、原声/BGM 混音修复 - 资金核算审计脚本 + genesis 账目回填 + 滞留预留清理命令 - 接入 yunqi provider 与豆包 TTS 模型(catalog/migrations/bootstrap 命令) 前端:全站页面更新(pipeline/library/products/projects/team/account 等), 新增共享 pager 分页组件 QA:刷新 function-audit 全量输出,新增 full-qa 报告 文档:BP 产品介绍资料、design/CLAUDE.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from .base import AIProvider, AIProviderResult
|
||||
from .volcano import VolcanoArkProvider
|
||||
from .volcano import TtsNotConfigured, VolcanoArkProvider, VolcanoTtsProvider
|
||||
from .yunqi import YunqiProvider
|
||||
|
||||
|
||||
__all__ = ["AIProvider", "AIProviderResult", "VolcanoArkProvider"]
|
||||
|
||||
__all__ = ["AIProvider", "AIProviderResult", "TtsNotConfigured", "VolcanoArkProvider", "VolcanoTtsProvider", "YunqiProvider"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
import base64
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
@@ -171,3 +172,60 @@ class VolcanoArkProvider:
|
||||
content_type = header.split(";")[0].replace("data:", "") or "application/octet-stream"
|
||||
return BytesIO(base64.b64decode(raw)), content_type
|
||||
return BytesIO(base64.b64decode(media)), "image/png"
|
||||
|
||||
|
||||
class TtsNotConfigured(ValueError):
|
||||
"""语音合成凭证未配置(VOLC_TTS_APPID / VOLC_TTS_ACCESS_TOKEN)。"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class VolcanoTtsProvider:
|
||||
"""豆包语音合成大模型(openspeech V1 HTTP,非流式)。
|
||||
注意:与 ARK 不是同一套钥匙,要在 火山引擎控制台→语音技术→语音合成大模型 创建应用拿 APPID/Access Token。"""
|
||||
|
||||
appid: str | None = None
|
||||
access_token: str | None = None
|
||||
cluster: str | None = None
|
||||
base_url: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
cfg = getattr(settings, "VOLC_TTS", {}) or {}
|
||||
self.appid = self.appid or cfg.get("appid")
|
||||
self.access_token = self.access_token or cfg.get("access_token")
|
||||
self.cluster = self.cluster or cfg.get("cluster") or "volcano_tts"
|
||||
self.base_url = self.base_url or cfg.get("base_url") or "https://openspeech.bytedance.com/api/v1/tts"
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.appid and self.access_token)
|
||||
|
||||
def synthesize(self, *, text: str, voice_type: str, speed_ratio: float = 1.0, uid: str = "airshelf") -> tuple[bytes, int]:
|
||||
"""合成一段语音。返回 (mp3 字节, 时长毫秒;接口没回时长则为 0)。"""
|
||||
if not self.configured:
|
||||
raise TtsNotConfigured(
|
||||
"语音合成未配置:请在后端环境变量设置 VOLC_TTS_APPID 和 VOLC_TTS_ACCESS_TOKEN"
|
||||
"(火山引擎控制台 → 语音技术 → 语音合成大模型 → 创建应用)"
|
||||
)
|
||||
body = {
|
||||
"app": {"appid": self.appid, "token": self.access_token, "cluster": self.cluster},
|
||||
"user": {"uid": uid},
|
||||
"audio": {"voice_type": voice_type, "encoding": "mp3", "speed_ratio": float(speed_ratio or 1.0)},
|
||||
"request": {"reqid": str(uuid.uuid4()), "text": text, "operation": "query"},
|
||||
}
|
||||
response = requests.post(
|
||||
self.base_url,
|
||||
headers={"Authorization": f"Bearer;{self.access_token}"},
|
||||
json=body,
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("code") != 3000 or not data.get("data"):
|
||||
raise ValueError(f"语音合成失败:{data.get('message') or data.get('code')}")
|
||||
audio = base64.b64decode(data["data"])
|
||||
duration_ms = 0
|
||||
try:
|
||||
duration_ms = int(float((data.get("addition") or {}).get("duration") or 0))
|
||||
except (TypeError, ValueError):
|
||||
duration_ms = 0
|
||||
return audio, duration_ms
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from .volcano import VolcanoArkProvider
|
||||
|
||||
|
||||
class YunqiProvider(VolcanoArkProvider):
|
||||
"""YunQi AI(New API 网关)适配器:OpenAI 兼容 images/generations。
|
||||
|
||||
与火山 ARK 的差异:不接受 watermark/sequential_image_generation/response_format 私有参数;
|
||||
size 用 OpenAI 枚举(1024x1024/1024x1536/1536x1024);gpt-image-2 固定返回 b64_json,
|
||||
下游 extract_first_media_url / media_to_bytes 已兼容 base64,无需改动。
|
||||
"""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.api_key = self.api_key or settings.YUNQI.get("api_key")
|
||||
self.base_url = self.base_url or settings.YUNQI.get("base_url")
|
||||
|
||||
def image_generation(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
prompt: str,
|
||||
endpoint: str = "images/generations",
|
||||
image: str | list[str] | None = None,
|
||||
size: str = "1024x1536",
|
||||
) -> dict[str, Any]:
|
||||
if not self.api_key:
|
||||
raise ValueError("YUNQI_API_KEY is not configured")
|
||||
if image:
|
||||
raise ValueError("gpt-image-2 via YunQi only supports text-to-image; reference image is not supported")
|
||||
body: dict[str, Any] = {"model": model, "prompt": prompt, "size": size, "n": 1}
|
||||
# 实测该网关生图平均延迟 75s+,超时须显著高于火山的 180s
|
||||
response = requests.post(
|
||||
f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}",
|
||||
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
|
||||
json=body,
|
||||
timeout=300,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
Reference in New Issue
Block a user