diff --git a/RULES.md b/RULES.md
index 1e17c01..6bd8fd2 100644
--- a/RULES.md
+++ b/RULES.md
@@ -25,6 +25,7 @@
- 生产启动:`docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d --build`
- 生产架构:`web` 容器用 Nginx 承载 Next 静态导出;`/login/`、`/_next/`、`/assets/`、`/skg-logo-black.svg`、`/oasis-source/` 等登录页必需静态资源公开访问;未登录访问工作台跳转 `/login/`,`/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`;Traefik 通过 `coolify` 外部网络接入 80/443
- Web 验收必须以生产 Docker 形态为准:前端是 `next export` 静态产物 + Nginx,不是 `next dev` / `next start`。任何 Web 改动部署后必须运行 `./scripts/verify-prod-docker.sh`,确认 `/login/`、`/_next/`、`/api/health`、本地 API 地址泄漏和 API 镜像 `.env` 污染检查通过;不能只用本地 `npm run build` 作为上线依据。
+- 当前音频解析:`https://ai.skg.com/azure/v1` 的 `gpt-4o-transcribe` 当前返回 `DeploymentNotFound`,且官方 Azure OpenAI transcription 路径探测也未返回可用部署;生产临时复制本地成功策略,直接使用容器内 `faster-whisper tiny.en` 真实转写,关闭 Gemini 多模态音频兜底。拿到真实 Azure ASR deployment 名后再恢复 `ASR_REMOTE_ENABLED=true`。
- 持久化目录:服务器 `./data/jobs` 挂载到后端 `/data/jobs`;全局资源中心持久化在 `./data/asset_library`、`./data/prompt_library` 和 `./data/_trash`
- TikTok 下载登录态:公开视频默认不带 cookies 直接下载,生产环境变量必须显式保持 `YTDLP_COOKIES_FILE=`、`YTDLP_COOKIES_FROM_BROWSER=` 为空,防止容器读取不存在的浏览器 cookies。只有 TikTok 明确要求登录态时,才使用服务器私有 cookies 文件 `./secrets/tiktok_cookies.txt` 挂载到 API 容器 `/run/secrets/tiktok_cookies.txt` 并配置 `YTDLP_COOKIES_FILE=/run/secrets/tiktok_cookies.txt`;`yt-dlp` 会在任务结束时回写 cookies,因此不要把该挂载设为只读;不要使用云端浏览器读取方案,也不要把 cookies 入库。生产容器严禁使用 `YTDLP_COOKIES_FROM_BROWSER=chrome`。
- 登录凭证:用户名写下方快捷登录;密码明文备份只放服务器 `/root/skg-marketing-studio-login.txt`,生产环境变量 `WEB_AUTH_PASSWORD` / `WEB_AUTH_SESSION_SECRET` 只放服务器 `deploy/.env.production`
@@ -55,13 +56,14 @@
## 环境变量
- `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 音频转写模型;生产微软通道默认用 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_MODEL`:OpenAI Audio Transcriptions 音频转写模型;微软通道使用 Azure OpenAI 部署名 `gpt-4o-transcribe`,如果 Azure 侧实际部署名不同必须同步改这里
+- `ASR_LANGUAGE`:远端 ASR 的输入语言提示,默认 `en`;微软官方说明指定 ISO-639-1 语言可改善准确率和延迟。
+- `ASR_REMOTE_ENABLED`:是否启用远端 OpenAI Audio Transcriptions;微软 ASR 验收时必须为 `true`。当前生产因 `https://ai.skg.com/azure/v1` 下 `gpt-4o-transcribe` 返回 `DeploymentNotFound`,临时设为 `false`,直接走容器内 `faster-whisper`,等真实 Azure deployment 名补齐后再恢复。
+- `ASR_LOCAL_FALLBACK_ENABLED`:是否允许远端 ASR 失败后落到本机 / 容器内 ASR;当前生产为 `true`,复制本地成功路径的“本机真实转写”策略,云端用 CPU 版 `faster-whisper` 替代本机 Mac 的 `mlx_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 秒客户端超时提前中断
+- `ASR_TIMEOUT_SECONDS`:远端 ASR / 翻译 / 音频分析单次请求超时;当前生产本地转写模式设为 45 秒,微软 ASR 重新启用时可按素材长度提高。
- `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 91c83b3..7318048 100644
--- a/api/main.py
+++ b/api/main.py
@@ -61,6 +61,7 @@ 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_LANGUAGE = os.getenv("ASR_LANGUAGE", "en").strip()
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"}
@@ -2875,6 +2876,7 @@ def _transcribe_sync(wav: Path) -> list[dict]:
model=ASR_MODEL,
response_format="verbose_json",
timestamp_granularities=["segment"],
+ **({"language": ASR_LANGUAGE} if ASR_LANGUAGE else {}),
)
raw = resp.model_dump() if hasattr(resp, "model_dump") else resp
segments = raw.get("segments") or []
@@ -2917,7 +2919,7 @@ def _translate_sync(segments: list[dict]) -> list[str]:
+ json.dumps(payload, ensure_ascii=False)
)
try:
- resp = llm().chat.completions.create(
+ resp = llm().with_options(timeout=ASR_TIMEOUT_SECONDS).chat.completions.create(
model=TRANSLATE_MODEL,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
@@ -4007,6 +4009,7 @@ def health() -> dict:
"voice_base_url": AZURE_OPENAI_BASE_URL,
"models": {
"asr": ASR_MODEL,
+ "asr_language": ASR_LANGUAGE,
"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,
diff --git a/deploy/.env.production.example b/deploy/.env.production.example
index ce12f1e..a5162d4 100644
--- a/deploy/.env.production.example
+++ b/deploy/.env.production.example
@@ -21,14 +21,16 @@ LLM_BASE_URL=https://ai.skg.com/ezlink/v1
LLM_API_KEY=
# Model routing
+# Azure ASR can be re-enabled once the real deployment name exists.
ASR_BASE_URL=https://ai.skg.com/azure/v1
ASR_API_KEY=
ASR_MODEL=gpt-4o-transcribe
-ASR_REMOTE_ENABLED=true
-ASR_LOCAL_FALLBACK_ENABLED=false
+ASR_LANGUAGE=en
+ASR_REMOTE_ENABLED=false
+ASR_LOCAL_FALLBACK_ENABLED=true
ASR_AUDIO_FALLBACK_ENABLED=false
ASR_FALLBACK_MODEL=gemini-2.5-flash
-ASR_TIMEOUT_SECONDS=180
+ASR_TIMEOUT_SECONDS=45
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 e4391a1..2c405e8 100644
--- a/docs/source-analysis.html
+++ b/docs/source-analysis.html
@@ -963,14 +963,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、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 /health | getRuntimeHealth、ModelTrace | 返回 models:ASR、asr_language、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 后自动触发音频解析。下载阶段默认不带 cookies;生产环境必须显式保持 YTDLP_COOKIES_FILE= 和 YTDLP_COOKIES_FROM_BROWSER= 为空,避免容器内误读被打进镜像的开发 api/.env。只有 TikTok 明确要求登录态时,才把宿主机 ./secrets/tiktok_cookies.txt 挂载进容器并设置 YTDLP_COOKIES_FILE=/run/secrets/tiktok_cookies.txt。生产容器没有 Chrome cookies 数据库,不能配置 YTDLP_COOKIES_FROM_BROWSER=chrome。 |
| 重试下载 | 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 提取原始文案。生产微软 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}/transcribe | triggerTranscribe | 若尚未拆轨,先从 source.mp4 提取 audio.wav 并回填 source_audio_url;远端启用时把 audio.wav 上传到 ASR_BASE_URL 的 OpenAI Audio Transcriptions 兼容接口,用 ASR_MODEL 提取原始文案,并传 ASR_LANGUAGE=en 降低英文素材延迟。微软官方路径包括 /openai/deployments/{deployment}/audio/transcriptions?api-version=... 和 /openai/v1/audio/transcriptions?api-version=preview;当前 SKG 网关探测这些路径均未返回可用 ASR,gpt-4o-transcribe 返回 DeploymentNotFound。当前生产因此复制本地成功策略:ASR_REMOTE_ENABLED=false、ASR_LOCAL_FALLBACK_ENABLED=true,直接走容器内 CPU 版 faster-whisper 生成真实逐句时间轴;ASR_AUDIO_FALLBACK_ENABLED=false,避免 Gemini 多模态假字幕。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果。中文翻译由 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 等路径。当前第一步不默认生成该文件。 |
@@ -1108,6 +1108,19 @@ ProductRefStateItem {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-19 · 云端音频解析复制本地真实转写路径
+ API
+ Audio
+ Deploy
+
+
+
问题:本地音频解析成功时实际链路是远端失败后落到 mlx_whisper,而生产强制 ASR_BASE_URL=https://ai.skg.com/azure/v1 + ASR_MODEL=gpt-4o-transcribe 且关闭本地兜底。生产探测官方 Azure OpenAI 音频路径 /openai/v1/audio/transcriptions?api-version=preview 和 /openai/deployments/{deployment}/audio/transcriptions?api-version=... 仍不可用,当前部署名返回 DeploymentNotFound。
+
改动:远端 ASR 请求新增 ASR_LANGUAGE,默认 en,用于按官方建议降低英文素材延迟;翻译请求也套用 ASR_TIMEOUT_SECONDS。生产配置临时改成 ASR_REMOTE_ENABLED=false、ASR_LOCAL_FALLBACK_ENABLED=true、ASR_AUDIO_FALLBACK_ENABLED=false,云端用容器内 faster-whisper tiny.en 复制本地“真实本机转写”路径。
+
影响:音频解析不再卡在不存在的 Azure deployment;当前云端 CPU 实测同一失败 job 的 audio.wav 可在约 13.6 秒转出 17 段。等 SKG 网关提供真实 Azure ASR deployment 后,再把 ASR_REMOTE_ENABLED=true 并恢复对应部署名。
+
+
2026-05-19 · 删除抽帧状态文案改为参考帧
diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx
index 6005382..f0e01f3 100644
--- a/web/components/ad-recreation-board.tsx
+++ b/web/components/ad-recreation-board.tsx
@@ -859,11 +859,14 @@ function resolveVideoModelLabel(models: RuntimeModels | undefined, model: string
}
function audioModelTrace(models?: RuntimeModels): ModelTraceSpec {
+ const remoteState = models?.asr_remote_enabled === false ? "已关闭" : "启用"
+ const localState = models?.asr_local_fallback_enabled === false ? "关闭" : "启用"
+ const localModel = models?.faster_whisper ? `faster-whisper ${models.faster_whisper}` : modelValue(models?.local_asr)
return {
title: "音频解析",
model: modelList([models?.asr, models?.translate, models?.asr_fallback]),
chain: [
- `ASR 转写:优先 ${modelValue(models?.asr)};失败后尝试本机 ${modelValue(models?.local_asr)};仍失败才回退 ${modelValue(models?.asr_fallback)},并拒绝假字幕/重复时间轴`,
+ `ASR 转写:远端 ${remoteState},模型 ${modelValue(models?.asr)}${models?.asr_language ? `,语言 ${models.asr_language}` : ""};本机转写 ${localState},使用 ${localModel};多模态兜底${models?.asr_audio_fallback_enabled === false ? "关闭" : `为 ${modelValue(models?.asr_fallback)}`},并拒绝假字幕/重复时间轴`,
`字幕翻译:${modelValue(models?.translate)} 按 ASR 段落输出中文;失败时保留原文时间轴,中文可为空`,
`讲话人 / 节奏 / 背景音:${modelValue(models?.asr_fallback)} 读取 audio.wav + 转写时间轴做多模态分析;失败时用本地时长/段落估算兜底`,
],
diff --git a/web/lib/api.ts b/web/lib/api.ts
index f62b49a..6b9bc81 100644
--- a/web/lib/api.ts
+++ b/web/lib/api.ts
@@ -254,6 +254,12 @@ export interface GeneratedVideo {
export interface RuntimeModels {
asr?: string
+ asr_language?: string
+ asr_base_url?: string
+ asr_remote_enabled?: boolean
+ asr_local_fallback_enabled?: boolean
+ asr_audio_fallback_enabled?: boolean
+ faster_whisper?: string
local_asr?: string
asr_fallback?: string
translate?: string