回退链从「一条主路由 + 一串替补」改成两级:先把同能力(有说话人分离)的路由 试完,全挂才降级到不分离的路由。降级是**本次事实**而不是配置事实,写进第 2 步 产物(capability_degraded / capability_zh),第 3 步与第 5/6 步据此判为不可用。 - transcribe.go:Result 加 CapabilityDegraded,判据 chain[0] 能分离而实际这条 不能;与 HasSpeakers 分开记(后者可能是「配了却没输出」那种异常) - audio_handlers.go:闸门从「读路由声明的能力」改成「读稿子里实际有没有标签」 (audioTranscriptSpeakerKeys),与第 3 步共用同一句 SpeakerKeysOf;本次没分离 → 哨兵错误 errAudioSpeakersUnavailableThisRun,不再放行去写一份看不出残缺的纪要 - 闸门**不看** capability_degraded:改动前落库的老产物没有这个字段,看它就 fail-open - 第 1 步「转写要求」提前把降级的后果说清;第 2 步产物带完整措辞与「⚠」日志 - 前端两处(SpecialistPanel.vue / audioSkill.js)判断顺序改为先读实际结果 has_speakers、再退回声明 speakers —— 顺序反了会在降级那一次照旧显示第 3 步 - ai_config.json:补 3 条云端无分离路由与各自的回退链 验证:三处变异(产物 key 拼错、标记写死 true、闸门 fail-closed)都验过会红; 新增两个用例文件走真实 gin 路由 + 真实鉴权中间件,断言拦下来的**理由**而不只是 「拦下来了」;go test ./... 全绿、gofmt 干净、前端构建通过。 同期把本地 ASR 装成 systemd 常驻服务(deploy/install_asr_local.sh 七步全过, 开机自启,实测 26.7 分钟录音 → 3.1 分钟)。装的过程挖出两个只在服务化时才暴露的坑: - E12 转写堵住事件循环 → 探活超时 → 本地被判不健康 → auto 静默退云端、音频出网, 全程没有任何报错。修法 run_in_threadpool(deploy/asr/serve.py) - E13 服务账号的 ~ 不可写,pyannote 写不了 ~/.pyannote/database.yml,每次转写 500。 修法 asr.env 加 HOME=<cache 目录>(该目录在 unit 的 ReadWritePaths 里) 顺带收口一处交付缺口:服务源码原先只有 ~/asr-poc 一份,而 DELIVERY.md 的清理计划 要 rm -rf 它 —— 那会让唯一副本变成 /opt 下 root 所有、不在任何版本库里的文件。 现在 deploy/asr/ 是唯一事实源,装机脚本与文档同步改。 已知偏离 / 未做(记在案): - 界面那句「本次没有说话人分离,后续步骤不可用」只验到后端接口层,没有造出真实 降级场景渲染出来看过 - deploy/asr/ 的引入改变了装机来源:原型目录 $SRC_DIR 从此只提供 venv 与模型, 服务代码一律从仓库取 Co-Authored-By: Claude Code <noreply@anthropic.com>
312 lines
14 KiB
JSON
312 lines
14 KiB
JSON
{
|
||
"agent_routes": {
|
||
"embed_gen": "embed_route_llamacpp_nomic",
|
||
"image_gen": "image_route_openrouter_gpt_image_2",
|
||
"video_gen": "video_route_openrouter_veo_3",
|
||
"path_coach": "chat_route_lmuai_deepseek_v4_flash",
|
||
"title_gen": "chat_route_lmuai_deepseek_v4_flash",
|
||
"audio_transcribe": "audio_route_auto",
|
||
"audio_transcribe_llm": "chat_route_lmuai_deepseek_v4_flash_doc"
|
||
},
|
||
"audio_routes": {
|
||
"audio_route_local_whisper": {
|
||
"base_url": "http://127.0.0.1:8090/v1",
|
||
"description": "语音转写 · 本机 faster-whisper large-v3 + pyannote 3.1(音频不出本机)。由 deploy/eai_agentplatform-asr.service 常驻在 127.0.0.1:8090;服务不在时 audio_route_auto 会改走 fallback_routes 里的云端路由,并在转写产物里标注「音频已出本机」。timeout 定在 840s 而不是一小时:前端 api/audioSkill.js 给这一步的上限是 15 分钟,后端必须在它之前自己收手(否则用户看到的是 axios 超时,而后端还在跑),同时 840s 里还留得下云端那 600s 的回退预算。回退链分两级:先把同能力(有说话人分离)的路由试完,最后才降级到不分离的路由;一旦降级,转写产物里 capability_degraded=true,第 3 步「识别说话人身份」与第 5/6 步「整理/纪要」判为不可用,只交付逐字稿",
|
||
"endpoint": "/audio/transcriptions",
|
||
"model": "large-v3",
|
||
"provider": "local_asr",
|
||
"short_model_name": "whisper large-v3",
|
||
"short_route_name": "本地",
|
||
"supports_speakers": true,
|
||
"timeout_seconds": 840
|
||
},
|
||
"audio_route_siliconflow_diarize": {
|
||
"description": "语音转写 · SiliconFlow / XingChen ASR Diarize(云端,音频会出本机;本地服务不可用时的回退,也可由管理员手动指定)。回退链分两级:先把同能力(有说话人分离)的路由试完,最后才降级到不分离的路由;一旦降级,转写产物里 capability_degraded=true,第 3 步「识别说话人身份」与第 5/6 步「整理/纪要」判为不可用,只交付逐字稿",
|
||
"endpoint": "/audio/transcriptions",
|
||
"model": "XingChenAGI/XingChenASR-Diarize-V3.0",
|
||
"provider": "siliconflow",
|
||
"short_model_name": "XingChen ASR Diarize",
|
||
"short_route_name": "SiliconFlow",
|
||
"supports_speakers": true,
|
||
"timeout_seconds": 600
|
||
},
|
||
"audio_route_siliconflow_qwen3": {
|
||
"description": "语音转写 · SiliconFlow / Qwen3-ASR(云端,音频会出公网;无说话人分离)。实测最快(45 秒片段约 1.4 秒),是降级兜底的首选",
|
||
"endpoint": "/audio/transcriptions",
|
||
"model": "Qwen/Qwen3-ASR-1.7B",
|
||
"provider": "siliconflow",
|
||
"short_model_name": "Qwen3 ASR",
|
||
"short_route_name": "SiliconFlow",
|
||
"supports_speakers": false,
|
||
"timeout_seconds": 600
|
||
},
|
||
"audio_route_openrouter_whisper": {
|
||
"description": "语音转写 · OpenRouter / OpenAI whisper-1(云端,音频会出公网;无说话人分离)。实测是几条云端路线里最慢的(45 秒片段约 9 秒),排在同能力链的末位",
|
||
"endpoint": "/audio/transcriptions",
|
||
"model": "openai/whisper-1",
|
||
"provider": "openrouter",
|
||
"base_url": "https://openrouter.ai/api/v1",
|
||
"short_model_name": "whisper-1",
|
||
"short_route_name": "OpenRouter",
|
||
"supports_speakers": false,
|
||
"timeout_seconds": 600
|
||
},
|
||
"audio_route_openrouter_gpt4o_transcribe": {
|
||
"description": "语音转写 · OpenRouter / OpenAI gpt-4o-transcribe(云端,音频会出公网;无说话人分离)。实测比 whisper-1 快一倍且断句更整,无分离路由里的首选",
|
||
"endpoint": "/audio/transcriptions",
|
||
"model": "openai/gpt-4o-transcribe",
|
||
"provider": "openrouter",
|
||
"base_url": "https://openrouter.ai/api/v1",
|
||
"short_model_name": "gpt-4o-transcribe",
|
||
"short_route_name": "OpenRouter",
|
||
"supports_speakers": false,
|
||
"timeout_seconds": 600
|
||
},
|
||
"audio_route_siliconflow_asr_ultra": {
|
||
"description": "语音转写 · SiliconFlow / XingChen ASR V3.2 Ultra(云端,音频会出公网;无说话人分离)。同门的 V3.2(非 Ultra)实测同样可用、约快一倍,但效果略差,没有单独建路由",
|
||
"endpoint": "/audio/transcriptions",
|
||
"model": "XingChenAGI/XingChenASR-V3.2-Ultra",
|
||
"provider": "siliconflow",
|
||
"short_model_name": "XingChen V3.2 Ultra",
|
||
"short_route_name": "SiliconFlow",
|
||
"supports_speakers": false,
|
||
"timeout_seconds": 600
|
||
}
|
||
},
|
||
"chat_routes": {
|
||
"chat_route_lmuai_deepseek_v4_flash": {
|
||
"description": "LMUAI / DeepSeek Flash。max_tokens 从 4096 提到 32768(2026-09-26):4096 是当初只按「聊天」估的,但这条路由是 default_route,用户在对话框里选过模型后这个选择会被记进 localStorage 并**一路带到技能内部的加工步骤**(全仓库只有语音转写的第 3、4 步消费 ai_route_id,见 skills/api/audio_handlers.go 的 resolveAudioChatRoute)。于是它会顶掉技能自带的 _doc 路由,让「整理段落」在推理模型思考完之前就把预算耗光、正文 0 字。上限是天花板不是配额,模型写完就停,抬高不会让聊天变慢变贵(实测见 _doc 那条的说明)",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 32768,
|
||
"model": "deepseek-v4-flash",
|
||
"provider": "lmuai",
|
||
"short_model_name": "DeepSeek V4 Flash",
|
||
"short_route_name": "LMUAI",
|
||
"temperature": 0.7
|
||
},
|
||
"chat_route_lmuai_deepseek_v4_flash_doc": {
|
||
"description": "LMUAI / DeepSeek Flash(长文输出档,max_tokens=32768)· 供逐字稿整理这类要产出一整篇文档的技能使用。为什么不复用上面那条 4096 的:这是推理模型,思考与正文共用 max_tokens,而思考的长度跟输入几乎不成比例(实测 1200 字的输入,思考照样写到 7169 字)。4096 下正文一个字都写不出来(六次实测只有一次成功)。再说为什么是 32768 而不是 8192:纪要步骤最后要把各段结果**归并**成一份,那是全流程最长的一次输入(实测 8628 字),8192 下 finish_reason=length、正文 0 字,16384 才出稿、32768 又快又稳。上限提高**不会**让逐块调用变慢或变贵——上限是天花板不是配额,模型写完就停:实测同一批 11 段,8192 与 32768 两档的用时(18.6s / 19.2s)与 completion token(均 4928 / 5094)基本一致。调小这个值之前请先看 audio_transcribe/prompts.go 里的实测表",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 32768,
|
||
"model": "deepseek-v4-flash",
|
||
"provider": "lmuai",
|
||
"short_model_name": "DeepSeek V4 Flash(长文档)",
|
||
"short_route_name": "LMUAI",
|
||
"temperature": 0.7
|
||
},
|
||
"chat_route_ollama_qwen": {
|
||
"description": "本地对话 · llama.cpp / Qwen3.5 4B",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 512,
|
||
"model": "Qwen3.5-4B-Q4_K_M.gguf",
|
||
"provider": "llamacpp",
|
||
"short_model_name": "Qwen3.5 4B",
|
||
"short_route_name": "本地对话",
|
||
"temperature": 0.7,
|
||
"timeout_seconds": 90
|
||
},
|
||
"chat_route_ollama_qwen_fast": {
|
||
"description": "本地快聊 · llama.cpp / Qwen3.5 4B",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 512,
|
||
"model": "Qwen3.5-4B-Q4_K_M.gguf",
|
||
"provider": "llamacpp",
|
||
"short_model_name": "Qwen3.5 4B",
|
||
"short_route_name": "本地快聊",
|
||
"temperature": 0.3
|
||
},
|
||
"chat_route_openrouter_deepseek_v3": {
|
||
"description": "OpenRouter / DeepSeek V3.2",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 4096,
|
||
"model": "deepseek/deepseek-v3.2",
|
||
"provider": "openrouter",
|
||
"short_model_name": "DeepSeek V3.2",
|
||
"short_route_name": "OpenRouter",
|
||
"temperature": 0.6
|
||
},
|
||
"chat_route_openrouter_deepseek_v4_flash": {
|
||
"description": "OpenRouter / DeepSeek V4 Flash 0731",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 4096,
|
||
"model": "deepseek/deepseek-v4-flash-0731",
|
||
"provider": "openrouter",
|
||
"short_model_name": "DeepSeek V4 Flash 0731",
|
||
"short_route_name": "OpenRouter",
|
||
"temperature": 0.7
|
||
},
|
||
"chat_route_openrouter_gpt_5": {
|
||
"description": "OpenRouter / GPT 5.4",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 8192,
|
||
"model": "openai/gpt-5.4",
|
||
"provider": "openrouter",
|
||
"short_model_name": "GPT 5.4",
|
||
"short_route_name": "OpenRouter",
|
||
"temperature": 0.7
|
||
},
|
||
"chat_route_openrouter_grok_4": {
|
||
"description": "OpenRouter / Grok 4.20",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 4096,
|
||
"model": "x-ai/grok-4.20",
|
||
"provider": "openrouter",
|
||
"short_model_name": "Grok 4.20",
|
||
"short_route_name": "OpenRouter",
|
||
"temperature": 0.6
|
||
},
|
||
"chat_route_siliconflow_deepseek_v4_flash": {
|
||
"description": "SiliconFlow / DeepSeek V4 Flash",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 4096,
|
||
"model": "deepseek-ai/DeepSeek-V4-Flash",
|
||
"provider": "siliconflow",
|
||
"short_model_name": "DeepSeek V4 Flash",
|
||
"short_route_name": "SiliconFlow",
|
||
"temperature": 0.7
|
||
}
|
||
},
|
||
"default_audio_route": "audio_route_local_whisper",
|
||
"default_embed_route": "embed_route_llamacpp_nomic",
|
||
"default_route": "chat_route_lmuai_deepseek_v4_flash",
|
||
"description": "eai_agentplatform AI 路由配置 — 按场景分类定义多条路由,支持 provider/模型切换",
|
||
"embed_routes": {
|
||
"embed_route_llamacpp_nomic": {
|
||
"base_url": "http://127.0.0.1:8081/v1",
|
||
"description": "本地向量 · llama.cpp / Nomic Embed v1.5",
|
||
"endpoint": "/embeddings",
|
||
"model": "nomic-embed-text-v1.5.Q4_K_M.gguf",
|
||
"provider": "llamacpp",
|
||
"short_model_name": "Nomic Embed v1.5",
|
||
"short_route_name": "本地向量",
|
||
"timeout_seconds": 30
|
||
},
|
||
"embed_route_ollama_bge_m3": {
|
||
"description": "本地向量 · Ollama / BGE-M3",
|
||
"endpoint": "/embeddings",
|
||
"model": "bge-m3",
|
||
"provider": "ollama",
|
||
"short_model_name": "BGE-M3",
|
||
"short_route_name": "本地向量"
|
||
},
|
||
"embed_route_openrouter_text_v3": {
|
||
"description": "OpenRouter / text-embedding-3-small",
|
||
"endpoint": "/embeddings",
|
||
"model": "openai/text-embedding-3-small",
|
||
"provider": "openrouter",
|
||
"short_model_name": "text-embedding-3-small",
|
||
"short_route_name": "OpenRouter"
|
||
}
|
||
},
|
||
"fallback_routes": {
|
||
"chat_route_lmuai_deepseek_flash": [
|
||
"chat_route_ollama_qwen_fast",
|
||
"chat_route_ollama_qwen"
|
||
],
|
||
"chat_route_openrouter_deepseek_v3": [
|
||
"chat_route_openrouter_grok_4",
|
||
"chat_route_ollama_qwen"
|
||
],
|
||
"chat_route_openrouter_deepseek_v4_flash": [
|
||
"chat_route_openrouter_deepseek_v3",
|
||
"chat_route_openrouter_grok_4",
|
||
"chat_route_ollama_qwen"
|
||
],
|
||
"chat_route_openrouter_grok_4": [
|
||
"chat_route_ollama_qwen"
|
||
],
|
||
"chat_route_siliconflow_deepseek_v4_flash": [
|
||
"chat_route_ollama_qwen_fast",
|
||
"chat_route_ollama_qwen"
|
||
],
|
||
"audio_route_local_whisper": [
|
||
"audio_route_siliconflow_diarize",
|
||
"audio_route_siliconflow_qwen3",
|
||
"audio_route_siliconflow_asr_ultra",
|
||
"audio_route_openrouter_gpt4o_transcribe",
|
||
"audio_route_openrouter_whisper"
|
||
],
|
||
"embed_route_llamacpp_nomic": [
|
||
"embed_route_openrouter_text_v3"
|
||
],
|
||
"embed_route_ollama_bge_m3": [
|
||
"embed_route_openrouter_text_v3"
|
||
],
|
||
"embed_route_openrouter_text_v3": [
|
||
"embed_route_ollama_bge_m3"
|
||
],
|
||
"audio_route_openrouter_gpt4o_transcribe": [
|
||
"audio_route_siliconflow_asr_ultra",
|
||
"audio_route_openrouter_whisper",
|
||
"audio_route_siliconflow_qwen3"
|
||
],
|
||
"audio_route_siliconflow_asr_ultra": [
|
||
"audio_route_openrouter_gpt4o_transcribe",
|
||
"audio_route_openrouter_whisper",
|
||
"audio_route_siliconflow_qwen3"
|
||
],
|
||
"audio_route_openrouter_whisper": [
|
||
"audio_route_openrouter_gpt4o_transcribe",
|
||
"audio_route_siliconflow_asr_ultra",
|
||
"audio_route_siliconflow_qwen3"
|
||
],
|
||
"audio_route_siliconflow_qwen3": [
|
||
"audio_route_openrouter_gpt4o_transcribe",
|
||
"audio_route_siliconflow_asr_ultra",
|
||
"audio_route_openrouter_whisper"
|
||
],
|
||
"audio_route_siliconflow_diarize": [
|
||
"audio_route_local_whisper",
|
||
"audio_route_siliconflow_qwen3",
|
||
"audio_route_siliconflow_asr_ultra",
|
||
"audio_route_openrouter_gpt4o_transcribe",
|
||
"audio_route_openrouter_whisper"
|
||
]
|
||
},
|
||
"image_routes": {
|
||
"image_route_ollama_llava": {
|
||
"description": "本地图像 · Ollama / LLaVA",
|
||
"endpoint": "/chat/completions",
|
||
"max_tokens": 2048,
|
||
"model": "llava",
|
||
"provider": "ollama",
|
||
"short_model_name": "LLaVA",
|
||
"short_route_name": "本地图像",
|
||
"temperature": 0.7
|
||
},
|
||
"image_route_openrouter_flux": {
|
||
"description": "OpenRouter / FLUX 1.1 Pro",
|
||
"endpoint": "/images/generations",
|
||
"max_tokens": 1024,
|
||
"model": "black-forest-labs/flux-1.1-pro",
|
||
"provider": "openrouter",
|
||
"short_model_name": "FLUX 1.1 Pro",
|
||
"short_route_name": "OpenRouter",
|
||
"temperature": 1
|
||
},
|
||
"image_route_openrouter_gpt_image_2": {
|
||
"description": "OpenRouter / GPT 5.4 Image 2",
|
||
"endpoint": "/images/generations",
|
||
"max_tokens": 4096,
|
||
"model": "openai/gpt-5.4-image-2",
|
||
"provider": "openrouter",
|
||
"short_model_name": "GPT 5.4 Image 2",
|
||
"short_route_name": "OpenRouter",
|
||
"temperature": 1
|
||
}
|
||
},
|
||
"video_routes": {
|
||
"video_route_openrouter_veo_3": {
|
||
"description": "OpenRouter / Google Veo 3",
|
||
"endpoint": "/videos/generations",
|
||
"model": "google/veo-3",
|
||
"max_tokens": 4096,
|
||
"provider": "openrouter",
|
||
"short_model_name": "Veo 3",
|
||
"short_route_name": "OpenRouter",
|
||
"temperature": 0.7
|
||
}
|
||
},
|
||
"version": "2.1.0"
|
||
}
|