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/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_base_urlasr_remote_enabledasr_local_fallback_enabledasr_audio_fallback_enabledfaster_whisper、本机 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_languageasr_base_urlasr_remote_enabledasr_local_fallback_enabledasr_audio_fallback_enabledfaster_whisper、本机 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 后自动触发音频解析。下载阶段默认不带 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/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;远端启用时把 audio.wav 上传到 ASR_BASE_URL 的 OpenAI Audio Transcriptions 兼容接口,用 ASR_MODEL 提取原始文案。生产微软 ASR 验收时 ASR_BASE_URL=https://ai.skg.com/azure/v1ASR_REMOTE_ENABLED=trueASR_LOCAL_FALLBACK_ENABLED=falseASR_AUDIO_FALLBACK_ENABLED=false,Azure 失败会明确失败,不会静默切到 faster-whisper 或 Gemini。只有显式开启兜底开关时,远端不可用才会走容器内 CPU 版 faster-whisper 或多模态音频兜底。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果,不再把不可听的多模态输出写进时间轴。中文翻译由 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 提取原始文案,并传 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=falseASR_LOCAL_FALLBACK_ENABLED=true,直接走容器内 CPU 版 faster-whisper 生成真实逐句时间轴;ASR_AUDIO_FALLBACK_ENABLED=false,避免 Gemini 多模态假字幕。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果。中文翻译由 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 等路径。当前第一步不默认生成该文件。 @@ -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=falseASR_LOCAL_FALLBACK_ENABLED=trueASR_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