diff --git a/RULES.md b/RULES.md
index 08e851f..b886e74 100644
--- a/RULES.md
+++ b/RULES.md
@@ -53,11 +53,13 @@
## 环境变量
- `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_REMOTE_ENABLED`:是否启用远端 OpenAI Audio Transcriptions;云端音频网关不可用时可设为 `false`,直接走容器内 CPU 版 `faster-whisper`
-- `FASTER_WHISPER_MODEL` / `FASTER_WHISPER_DEVICE` / `FASTER_WHISPER_COMPUTE_TYPE`:容器内本地 ASR 兜底,生产可用 `tiny.en` / `cpu` / `int8`
-- `ASR_FALLBACK_MODEL`:远端 ASR 和本机 ASR 都不可用时才尝试的多模态兜底,默认 `gemini-2.5-flash`;如果模型不能真实听到音频或返回疑似逐秒假字幕,后端必须拒绝写入时间轴
-- `ASR_TIMEOUT_SECONDS`:远端 ASR / 音频分析单次请求超时,默认 45 秒,避免第一步长时间停在转录中
+- `ASR_MODEL`:OpenAI Audio Transcriptions 音频转写模型;生产微软通道默认用 Azure OpenAI 部署名 `gpt-4o-transcribe`,如果 Azure 侧实际部署名不同必须同步改这里
+- `ASR_REMOTE_ENABLED`:是否启用远端 OpenAI Audio Transcriptions;微软 ASR 验收时必须为 `true`
+- `ASR_LOCAL_FALLBACK_ENABLED`:是否允许远端 ASR 失败后落到本机 / 容器内 ASR;生产微软 ASR 验收设为 `false`,避免静默使用 `faster-whisper`
+- `ASR_AUDIO_FALLBACK_ENABLED`:是否允许远端和本机 ASR 失败后落到多模态音频兜底;生产微软 ASR 验收设为 `false`,避免静默使用 Gemini 音频
+- `FASTER_WHISPER_MODEL` / `FASTER_WHISPER_DEVICE` / `FASTER_WHISPER_COMPUTE_TYPE`:容器内本地 ASR 兜底,仅在 `ASR_LOCAL_FALLBACK_ENABLED=true` 时启用
+- `ASR_FALLBACK_MODEL`:多模态音频兜底模型,仅在 `ASR_AUDIO_FALLBACK_ENABLED=true` 时用于兜底或音频画像,默认 `gemini-2.5-flash`;如果模型不能真实听到音频或返回疑似逐秒假字幕,后端必须拒绝写入时间轴
+- `ASR_TIMEOUT_SECONDS`:远端 ASR / 音频分析单次请求超时,生产微软 ASR 默认 180 秒,避免 60 秒左右音频被 45 秒客户端超时提前中断
- `LOCAL_ASR_BIN` / `LOCAL_ASR_MODEL` / `LOCAL_ASR_TIMEOUT_SECONDS`:本机 ASR 兜底,默认使用 `/opt/homebrew/bin/mlx_whisper` + `mlx-community/whisper-tiny`,用于当前 SKG 网关 `/audio/transcriptions` 不可用时生成真实逐句时间轴
- `TRANSLATE_MODEL`:字幕翻译模型,默认 `gemini-2.5-flash`
- `GPT_TEXT_MODEL`:GPT 文本 / 视觉默认模型,默认 `gpt-4o`;用于兜底修正旧 Gemini 覆盖值
diff --git a/api/main.py b/api/main.py
index b0640bb..8336097 100644
--- a/api/main.py
+++ b/api/main.py
@@ -62,6 +62,8 @@ 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_REMOTE_ENABLED = os.getenv("ASR_REMOTE_ENABLED", "true").strip().lower() not in {"0", "false", "no", "off"}
+ASR_LOCAL_FALLBACK_ENABLED = os.getenv("ASR_LOCAL_FALLBACK_ENABLED", "true").strip().lower() not in {"0", "false", "no", "off"}
+ASR_AUDIO_FALLBACK_ENABLED = os.getenv("ASR_AUDIO_FALLBACK_ENABLED", "true").strip().lower() not in {"0", "false", "no", "off"}
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")))
FASTER_WHISPER_MODEL = os.getenv("FASTER_WHISPER_MODEL", "tiny.en").strip() or "tiny.en"
@@ -2855,7 +2857,7 @@ def _transcribe_gemini_sync(wav: Path) -> list[dict]:
def _transcribe_sync(wav: Path) -> list[dict]:
- """Remote ASR first, local mlx_whisper second. Gemini fallback is guarded against fake timelines."""
+ """Remote ASR first; local/multimodal fallbacks are explicit runtime switches."""
errors: list[str] = []
duration = media_duration(wav)
if ASR_REMOTE_ENABLED:
@@ -2877,18 +2879,24 @@ def _transcribe_sync(wav: Path) -> list[dict]:
errors.append(f"{ASR_MODEL}: {e}")
else:
errors.append(f"{ASR_MODEL}: remote disabled")
- try:
- return _transcribe_faster_whisper_sync(wav)
- except Exception as e:
- errors.append(f"faster-whisper: {e}")
- try:
- return _transcribe_mlx_sync(wav)
- except Exception as e:
- errors.append(f"mlx_whisper: {e}")
- try:
- return _transcribe_gemini_sync(wav)
- except Exception as e:
- errors.append(f"{ASR_FALLBACK_MODEL}: {e}")
+ if ASR_LOCAL_FALLBACK_ENABLED:
+ try:
+ return _transcribe_faster_whisper_sync(wav)
+ except Exception as e:
+ errors.append(f"faster-whisper: {e}")
+ try:
+ return _transcribe_mlx_sync(wav)
+ except Exception as e:
+ errors.append(f"mlx_whisper: {e}")
+ else:
+ errors.append("local ASR fallback disabled")
+ if ASR_AUDIO_FALLBACK_ENABLED:
+ try:
+ return _transcribe_gemini_sync(wav)
+ except Exception as e:
+ errors.append(f"{ASR_FALLBACK_MODEL}: {e}")
+ else:
+ errors.append("multimodal audio fallback disabled")
raise TranscriptionUnavailable(";".join(errors))
@@ -3994,6 +4002,8 @@ def health() -> dict:
"asr": ASR_MODEL,
"asr_base_url": ASR_BASE_URL or LLM_BASE_URL or "openai-default",
"asr_remote_enabled": ASR_REMOTE_ENABLED,
+ "asr_local_fallback_enabled": ASR_LOCAL_FALLBACK_ENABLED,
+ "asr_audio_fallback_enabled": ASR_AUDIO_FALLBACK_ENABLED,
"faster_whisper": FASTER_WHISPER_MODEL,
"local_asr": LOCAL_ASR_MODEL,
"asr_fallback": ASR_FALLBACK_MODEL,
diff --git a/api/requirements.txt b/api/requirements.txt
index 3c632a1..89ff262 100644
--- a/api/requirements.txt
+++ b/api/requirements.txt
@@ -6,6 +6,7 @@ python-dotenv==1.0.1
yt-dlp==2026.3.17
openai==1.55.3
httpx==0.27.2
+requests==2.32.5
imagehash==4.3.1
Pillow>=11.0
numpy>=2.0
diff --git a/deploy/.env.production.example b/deploy/.env.production.example
index 81c518b..e37f62c 100644
--- a/deploy/.env.production.example
+++ b/deploy/.env.production.example
@@ -23,10 +23,12 @@ LLM_API_KEY=
# Model routing
ASR_BASE_URL=https://ai.skg.com/azure/v1
ASR_API_KEY=
-ASR_MODEL=whisper-1
+ASR_MODEL=gpt-4o-transcribe
ASR_REMOTE_ENABLED=true
+ASR_LOCAL_FALLBACK_ENABLED=false
+ASR_AUDIO_FALLBACK_ENABLED=false
ASR_FALLBACK_MODEL=gemini-2.5-flash
-ASR_TIMEOUT_SECONDS=45
+ASR_TIMEOUT_SECONDS=180
FASTER_WHISPER_MODEL=tiny.en
FASTER_WHISPER_DEVICE=cpu
FASTER_WHISPER_COMPUTE_TYPE=int8
diff --git a/docs/source-analysis.html b/docs/source-analysis.html
index ff34cdf..f7fcbfc 100644
--- a/docs/source-analysis.html
+++ b/docs/source-analysis.html
@@ -950,14 +950,14 @@ ProductRefStateItem {
| 网页登录 | POST /auth/login、GET /auth/check、POST /auth/logout | web/app/login/page.tsx、Nginx auth_request | 登录页提交账号密码到 /api/auth/login,后端设置 HttpOnly 会话 Cookie;生产 Nginx 对工作台和 /api/ 调 /auth/check 做统一校验,未登录页面跳 /login/,API 返回 JSON 401。 |
- | 运行配置 / 模型标注 | GET /health | getRuntimeHealth、ModelTrace | 返回 models:ASR、asr_base_url、asr_remote_enabled、faster_whisper、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 product_view、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 REWRITE_MODEL、AUDIO_REWRITE_MODEL 和 VISION_MODEL 默认使用 gpt-4o;如果旧环境变量仍写 gemini-*,后端会归一化回 GPT_TEXT_MODEL / REWRITE_MODEL。语音只走 Azure OpenAI TTS,models.voice_tts_paths 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。 |
+ | 运行配置 / 模型标注 | GET /health | getRuntimeHealth、ModelTrace | 返回 models:ASR、asr_base_url、asr_remote_enabled、asr_local_fallback_enabled、asr_audio_fallback_enabled、faster_whisper、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 product_view、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 REWRITE_MODEL、AUDIO_REWRITE_MODEL 和 VISION_MODEL 默认使用 gpt-4o;如果旧环境变量仍写 gemini-*,后端会归一化回 GPT_TEXT_MODEL / REWRITE_MODEL。语音只走 Azure OpenAI TTS,models.voice_tts_paths 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。 |
| 历史列表 | GET /jobs | listJobs | 所有 job 精简列表(id/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填全部历史;带 limit 可截断。 |
| 创建任务 | POST /jobs | createJob | 提交 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/retry | retryJobDownload | 用于 TK 链接下载失败且没有 video_url 的素材;清空错误、重新进入下载状态,并在后台再次执行 pipeline_download。上传视频不能重下载,需要重新上传文件。 |
| 上传视频 | POST /jobs/upload | uploadJob | 保存 source.mp4,然后同样进入下载完成状态;当前上传后也加入第一步队列,下载完成后自动解析音频。 |
| 删除输入视频 | DELETE /jobs/{id} | deleteJob | 从任务队列、URL 和磁盘 jobs/<id> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。 |
| 解析视频 | POST /jobs/{id}/analyze?frames=&target=&mode=&quality= | analyzeJob | 抽参考帧能力。当前开始流程会在视频下载完成后自动调用一次,默认 frames=12、target=motion、quality=accurate、mode=replace,形成全局动作/节奏参考帧池;原版视频旁的“抽参考 12 帧”也会用同一参数显式重跑。target 仍支持透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。 |
- | 音频文案轨 | POST /jobs/{id}/transcribe | triggerTranscribe | 若尚未拆轨,先从 source.mp4 提取 audio.wav 并回填 source_audio_url;远端启用时把 audio.wav 上传到 ASR_BASE_URL 的 OpenAI Audio Transcriptions 兼容接口,用 ASR_MODEL 提取原始文案;远端不可用或关闭时走容器内 CPU 版 faster-whisper,再补中文翻译并写入 audio_script.source_text、source_zh 和逐句 transcript。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果,不再把不可听的多模态输出写进时间轴。中文翻译由 TRANSLATE_MODEL 按 ASR 段落补齐,失败时保留原文时间轴且中文可为空。再用 ASR_FALLBACK_MODEL 读取 audio.wav 和已有转写时间轴,多模态音频分析讲话人、语速节奏、停顿、背景音乐/环境声/音效,写入 speaker_profile、rhythm_profile、background_audio_profile;若模型分析失败,则用转写段落、时长和语速做本地估算兜底。当前第一步不默认生成 SKG 新口播和 Azure OpenAI 配音。 |
+ | 音频文案轨 | POST /jobs/{id}/transcribe | triggerTranscribe | 若尚未拆轨,先从 source.mp4 提取 audio.wav 并回填 source_audio_url;远端启用时把 audio.wav 上传到 ASR_BASE_URL 的 OpenAI Audio Transcriptions 兼容接口,用 ASR_MODEL 提取原始文案。生产微软 ASR 验收时 ASR_BASE_URL=https://ai.skg.com/azure/v1、ASR_REMOTE_ENABLED=true、ASR_LOCAL_FALLBACK_ENABLED=false、ASR_AUDIO_FALLBACK_ENABLED=false,Azure 失败会明确失败,不会静默切到 faster-whisper 或 Gemini。只有显式开启兜底开关时,远端不可用才会走容器内 CPU 版 faster-whisper 或多模态音频兜底。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果,不再把不可听的多模态输出写进时间轴。中文翻译由 TRANSLATE_MODEL 按 ASR 段落补齐,失败时保留原文时间轴且中文可为空。再用 ASR_FALLBACK_MODEL 读取 audio.wav 和已有转写时间轴,多模态音频分析讲话人、语速节奏、停顿、背景音乐/环境声/音效,写入 speaker_profile、rhythm_profile、background_audio_profile;若模型分析失败,则用转写段落、时长和语速做本地估算兜底。当前第一步不默认生成 SKG 新口播和 Azure OpenAI 配音。 |
| 分镜脚本改写 | POST /jobs/{id}/script/rewrite | rewriteStoryboardScript | 根据原英文参考文案、当前英文新口播、英文 role enum、时间段和作者想法改写英文口播;作者想法若含中文,后端会先经 _ensure_english 兜底翻译。mode=segment 只改一段;mode=all 一次改完整片,要求整片前后连贯。后端按 AUDIO_REWRITE_MODEL、ASR_FALLBACK_MODEL、TRANSLATE_MODEL 依次尝试,全部失败时用英文本地模板保留可编辑文案。接口返回 items[index,text,text_zh],其中 text 是写入模型链路的英文主值,text_zh 只供团队审稿镜像显示;点击保存规划后写入 StoryboardScene.action。 |
| 原始音频文件 | GET /jobs/{id}/audio.wav | sourceAudioUrl | 返回拆轨得到的 wav;当前主界面不再渲染底部吸附音频条,右侧复刻工作表会读取该文件生成参考图式横向响度波形,并和原视频、逐句时间轴联动;波形标题栏显示当前播放秒数、总时长和鼠标指针停点秒数。 |
| 改写配音文件 | GET /jobs/{id}/audio-script.mp3 | apiAssetUrl(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
+
+
+
问题:生产验收要求音频解析走微软 Azure OpenAI,而不是远端失败后静默落到容器内 faster-whisper 或 Gemini 多模态兜底。
+
改动:api/main.py 增加 ASR_LOCAL_FALLBACK_ENABLED 和 ASR_AUDIO_FALLBACK_ENABLED 两个运行期开关;GET /health 回传这两个值。生产示例把 ASR_BASE_URL 固定为 https://ai.skg.com/azure/v1,ASR_MODEL 固定为 gpt-4o-transcribe,并关闭本地 / 多模态兜底。
+
影响:微软 ASR 未部署或部署名不匹配时,音频步骤会直接暴露 Azure 的错误,例如 DeploymentNotFound;不会再用其它通道生成看似成功的逐句时间轴。
+
+
2026-05-19 · 独立 ASR 上传网关配置