diff --git a/.project.json b/.project.json index 0e53979..d555af2 100644 --- a/.project.json +++ b/.project.json @@ -8,6 +8,12 @@ "storage" : "api\/.env \/ deploy\/.env.production", "type" : "api_key" }, + { + "description" : "OpenAI Audio Transcriptions 兼容 ASR Key;未单独配置 ASR_API_KEY 时复用 LLM_API_KEY,本地开发只放 api\/.env,不入库", + "name" : "ASR_API_KEY", + "storage" : "api\/.env \/ deploy\/.env.production", + "type" : "api_key" + }, { "description" : "OpenAI-compatible GPT 图片模型 Key;未单独配置 IMAGE_API_KEY 时复用 LLM_API_KEY,本地开发只放 api\/.env,不入库", "name" : "IMAGE_API_KEY", diff --git a/RULES.md b/RULES.md index 019fb2f..1502ac8 100644 --- a/RULES.md +++ b/RULES.md @@ -51,7 +51,8 @@ - 能联网和鉴权时必须 `git push origin main`;如果不能推送,最终回复必须写清楚当前分支、领先/落后数量、最新未推送 commit 和失败原因 ## 环境变量 -- `LLM_BASE_URL` / `LLM_API_KEY`:OpenAI 兼容网关,用于 ASR、翻译、文案改写、音频分析等文本/音频理解模型调用 +- `LLM_BASE_URL` / `LLM_API_KEY`:OpenAI 兼容网关,用于翻译、文案改写、音频分析等文本/多模态理解模型调用 +- `ASR_BASE_URL` / `ASR_API_KEY`:OpenAI Audio Transcriptions 兼容网关,用于上传 `audio.wav` 做真实转写;未配置 `ASR_API_KEY` 时复用 `LLM_API_KEY`,生产默认指向 `https://ai.skg.com/azure/v1` - `ASR_MODEL`:OpenAI Audio Transcriptions 音频转写模型,默认 `whisper-1` - `ASR_FALLBACK_MODEL`:远端 ASR 和本机 ASR 都不可用时才尝试的多模态兜底,默认 `gemini-2.5-flash`;如果模型不能真实听到音频或返回疑似逐秒假字幕,后端必须拒绝写入时间轴 - `ASR_TIMEOUT_SECONDS`:远端 ASR / 音频分析单次请求超时,默认 45 秒,避免第一步长时间停在转录中 diff --git a/api/main.py b/api/main.py index 61dcf67..8ff7a96 100644 --- a/api/main.py +++ b/api/main.py @@ -58,6 +58,8 @@ for _library_dir in [ LLM_BASE_URL = os.getenv("LLM_BASE_URL", "").strip() LLM_API_KEY = os.getenv("LLM_API_KEY", "").strip() +ASR_BASE_URL = os.getenv("ASR_BASE_URL", LLM_BASE_URL).strip() +ASR_API_KEY = (os.getenv("ASR_API_KEY") or LLM_API_KEY).strip() ASR_MODEL = os.getenv("ASR_MODEL", "whisper-1") ASR_FALLBACK_MODEL = os.getenv("ASR_FALLBACK_MODEL", "gemini-2.5-flash").strip() or "gemini-2.5-flash" ASR_TIMEOUT_SECONDS = max(15, int(os.getenv("ASR_TIMEOUT_SECONDS", "45"))) @@ -200,6 +202,7 @@ _MEDIA_BIN_CACHE: dict[str, str] = {} # OpenAI 客户端(OpenAI 兼容网关,含 SKG ezlink) from openai import OpenAI _llm_client: OpenAI | None = None +_asr_client: OpenAI | None = None _image_client: OpenAI | None = None def ai_http_client(timeout: float = 120) -> httpx.Client: @@ -230,6 +233,20 @@ def llm() -> OpenAI: _llm_client = OpenAI(**kwargs) return _llm_client + +def asr_llm() -> OpenAI: + global _asr_client + if _asr_client is None: + if not ASR_API_KEY: + raise RuntimeError("ASR_API_KEY 或 LLM_API_KEY 未配置") + kwargs = {"base_url": ASR_BASE_URL or LLM_BASE_URL or None, "api_key": ASR_API_KEY} + http_client = openai_http_client() + if http_client: + kwargs["http_client"] = http_client + _asr_client = OpenAI(**kwargs) + return _asr_client + + def image_llm() -> OpenAI: global _image_client if _image_client is None: @@ -2813,7 +2830,7 @@ def _transcribe_sync(wav: Path) -> list[dict]: duration = media_duration(wav) try: with wav.open("rb") as f: - resp = llm().with_options(timeout=ASR_TIMEOUT_SECONDS).audio.transcriptions.create( + resp = asr_llm().with_options(timeout=ASR_TIMEOUT_SECONDS).audio.transcriptions.create( file=(wav.name, f, "audio/wav"), model=ASR_MODEL, response_format="verbose_json", @@ -3933,10 +3950,12 @@ def health() -> dict: "llm_configured": bool(LLM_API_KEY), "auth_configured": WEB_AUTH_CONFIGURED, "base_url": LLM_BASE_URL or "openai-default", + "asr_base_url": ASR_BASE_URL or LLM_BASE_URL or "openai-default", "image_base_url": IMAGE_BASE_URL or LLM_BASE_URL or "openai-default", "voice_base_url": AZURE_OPENAI_BASE_URL, "models": { "asr": ASR_MODEL, + "asr_base_url": ASR_BASE_URL or LLM_BASE_URL or "openai-default", "local_asr": LOCAL_ASR_MODEL, "asr_fallback": ASR_FALLBACK_MODEL, "translate": TRANSLATE_MODEL, diff --git a/deploy/.env.production.example b/deploy/.env.production.example index 99948ba..5ffb965 100644 --- a/deploy/.env.production.example +++ b/deploy/.env.production.example @@ -21,6 +21,8 @@ LLM_BASE_URL=https://ai.skg.com/ezlink/v1 LLM_API_KEY= # Model routing +ASR_BASE_URL=https://ai.skg.com/azure/v1 +ASR_API_KEY= ASR_MODEL=whisper-1 ASR_FALLBACK_MODEL=gemini-2.5-flash TRANSLATE_MODEL=gemini-2.5-flash diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 582260e..7426b88 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -950,14 +950,14 @@ ProductRefStateItem { 网页登录POST /auth/loginGET /auth/checkPOST /auth/logoutweb/app/login/page.tsx、Nginx auth_request登录页提交账号密码到 /api/auth/login,后端设置 HttpOnly 会话 Cookie;生产 Nginx 对工作台和 /api//auth/check 做统一校验,未登录页面跳 /login/,API 返回 JSON 401。 - 运行配置 / 模型标注GET /healthgetRuntimeHealthModelTrace返回 models:ASR、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 product_view、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 REWRITE_MODELAUDIO_REWRITE_MODELVISION_MODEL 默认使用 gpt-4o;如果旧环境变量仍写 gemini-*,后端会归一化回 GPT_TEXT_MODEL / REWRITE_MODEL。语音只走 Azure OpenAI TTS,models.voice_tts_paths 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。 + 运行配置 / 模型标注GET /healthgetRuntimeHealthModelTrace返回 models:ASR、asr_base_url、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 product_view、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 REWRITE_MODELAUDIO_REWRITE_MODELVISION_MODEL 默认使用 gpt-4o;如果旧环境变量仍写 gemini-*,后端会归一化回 GPT_TEXT_MODEL / REWRITE_MODEL。语音只走 Azure OpenAI TTS,models.voice_tts_paths 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。 历史列表GET /jobslistJobs所有 job 精简列表(id/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填全部历史;带 limit 可截断。 创建任务POST /jobscreateJob提交 TK 链接,后台开始下载;前端“开始”队列会在 downloaded 后自动触发音频解析。下载阶段优先使用 YTDLP_COOKIES_FILE,其次使用 YTDLP_COOKIES_FROM_BROWSER;生产云端固定走 /run/secrets/tiktok_cookies.txt,由宿主机 ./secrets/tiktok_cookies.txt 挂载进容器。TikTok 要求登录态时会提示上传 MP4 或配置后端 cookies。 重试下载POST /jobs/{id}/download/retryretryJobDownload用于 TK 链接下载失败且没有 video_url 的素材;清空错误、重新进入下载状态,并在后台再次执行 pipeline_download。上传视频不能重下载,需要重新上传文件。 上传视频POST /jobs/uploaduploadJob保存 source.mp4,然后同样进入下载完成状态;当前上传后也加入第一步队列,下载完成后自动解析音频。 删除输入视频DELETE /jobs/{id}deleteJob从任务队列、URL 和磁盘 jobs/<id> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。 解析视频POST /jobs/{id}/analyze?frames=&target=&mode=&quality=analyzeJob抽参考帧能力。当前开始流程会在视频下载完成后自动调用一次,默认 frames=12target=motionquality=accuratemode=replace,形成全局动作/节奏参考帧池;原版视频旁的“抽参考 12 帧”也会用同一参数显式重跑。target 仍支持透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。 - 音频文案轨POST /jobs/{id}/transcribetriggerTranscribe若尚未拆轨,先从 source.mp4 提取 audio.wav 并回填 source_audio_url;随后用 ASR 提取原始文案,翻译成中文,写入 audio_script.source_textsource_zh 和逐句 transcript。远端 ASR_MODEL 失败后先走本机 LOCAL_ASR_BIN/LOCAL_ASR_MODEL(默认 mlx_whisper),再尝试 ASR_FALLBACK_MODEL。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果,不再把不可听的多模态输出写进时间轴。中文翻译由 TRANSLATE_MODEL 按 ASR 段落补齐,失败时保留原文时间轴且中文可为空。再用 ASR_FALLBACK_MODEL 读取 audio.wav 和已有转写时间轴,多模态音频分析讲话人、语速节奏、停顿、背景音乐/环境声/音效,写入 speaker_profilerhythm_profilebackground_audio_profile;若模型分析失败,则用转写段落、时长和语速做本地估算兜底。当前第一步不默认生成 SKG 新口播和 Azure OpenAI 配音。 + 音频文案轨POST /jobs/{id}/transcribetriggerTranscribe若尚未拆轨,先从 source.mp4 提取 audio.wav 并回填 source_audio_url;随后把 audio.wav 上传到 ASR_BASE_URL 的 OpenAI Audio Transcriptions 兼容接口,用 ASR_MODEL 提取原始文案,翻译成中文,写入 audio_script.source_textsource_zh 和逐句 transcript。远端 ASR 失败后先走本机 LOCAL_ASR_BIN/LOCAL_ASR_MODEL(默认 mlx_whisper),再尝试 ASR_FALLBACK_MODEL。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果,不再把不可听的多模态输出写进时间轴。中文翻译由 TRANSLATE_MODEL 按 ASR 段落补齐,失败时保留原文时间轴且中文可为空。再用 ASR_FALLBACK_MODEL 读取 audio.wav 和已有转写时间轴,多模态音频分析讲话人、语速节奏、停顿、背景音乐/环境声/音效,写入 speaker_profilerhythm_profilebackground_audio_profile;若模型分析失败,则用转写段落、时长和语速做本地估算兜底。当前第一步不默认生成 SKG 新口播和 Azure OpenAI 配音。 分镜脚本改写POST /jobs/{id}/script/rewriterewriteStoryboardScript根据原英文参考文案、当前英文新口播、英文 role enum、时间段和作者想法改写英文口播;作者想法若含中文,后端会先经 _ensure_english 兜底翻译。mode=segment 只改一段;mode=all 一次改完整片,要求整片前后连贯。后端按 AUDIO_REWRITE_MODELASR_FALLBACK_MODELTRANSLATE_MODEL 依次尝试,全部失败时用英文本地模板保留可编辑文案。接口返回 items[index,text,text_zh],其中 text 是写入模型链路的英文主值,text_zh 只供团队审稿镜像显示;点击保存规划后写入 StoryboardScene.action。 原始音频文件GET /jobs/{id}/audio.wavsourceAudioUrl返回拆轨得到的 wav;当前主界面不再渲染底部吸附音频条,右侧复刻工作表会读取该文件生成参考图式横向响度波形,并和原视频、逐句时间轴联动;波形标题栏显示当前播放秒数、总时长和鼠标指针停点秒数。 改写配音文件GET /jobs/{id}/audio-script.mp3apiAssetUrl(job.audio_script.voice_url)后续新配音阶段保留的 TTS 产物;服务端固定走 VOICE_PROVIDER=azure_openai,通过 AZURE_OPENAI_BASE_URL 的 OpenAI 协议生成 mp3,并按 AZURE_TTS_PATHS 依次尝试 /audio/speech/v1/audio/speech 等路径。当前第一步不默认生成该文件。 @@ -1238,6 +1238,18 @@ ProductRefStateItem {

影响:只改变工作台视觉模式,不改变素材下载、音频解析、抽帧、主体模板、产品素材池、首尾帧或模型链路;web/app/page.tsx 同步移除旧全局浮动主题按钮,避免右下角出现第二套不相关的主题入口。后续新增图片/视频板块仍应复用同一套媒体悬停放大和删除逻辑。

+
+
+

2026-05-19 · 独立 ASR 上传网关配置

+ API + Ops +
+
+

问题:生产只配置 LLM_BASE_URL=https://ai.skg.com/ezlink/v1,文本网关不一定提供 /audio/transcriptions 文件上传接口,导致音频文案步骤无法真实转写。

+

改动:api/main.py 新增 ASR_BASE_URL / ASR_API_KEY 和独立 ASR OpenAI client;音频转写只通过该 client 上传 audio.wav,不再绑死 LLM_BASE_URLdeploy/.env.production.example 增加生产 ASR 网关示例。

+

影响:文本/视觉模型仍走 LLM_BASE_URL,音频文件上传可单独切换到支持 Audio Transcriptions 的网关;/health 会回传 asr_base_url 供排障。

+
+

2026-05-19 · ASR 客户端级超时硬化