package echomeet import ( "fmt" "strings" ) const ( MaxShortAudioProcess = 10 MaxLongAudioProcess = 10 MaxAIAwaitProcess = 10 TaskTypeShortAudioAwait = "Echomeet:short_audio_await" TaskTypeShortAudioProcess = "Echomeet:short_audio_process" TaskTypeAIAwait = "Echomeet:ai_await" TaskTypeAIAwaitProcess = "Echomeet:ai_process" // TranscribeQueryMinInterval 主动查询第三方转写服务的最小间隔(秒) TranscribeQueryMinInterval int64 = 10 // TranscribePollConcurrency 轮询第三方查询的并发度,避免 for 串行被单条慢请求拖住整轮 TranscribePollConcurrency = 10 // ===== 卡单兜底扫描 ===== // // ⚠️ 在这之前,一条记录能从 Transcribing 往前走只有两条路:第三方异步回调、 // 客户端拉取时驱动的主动查询(api_getrecord/api_getrecords)。**没有任何 // 服务端自愈**——两条同时断掉时记录就永远停在「转写中」。 // // 2026-09-06 真机上正好凑齐了:阿里的回调一次没到,而客户端的轮询循环因为 // 一个未捕获异常死掉且再也起不来(_isTask 卡在 true),于是记录卡了 8 分钟, // 直到用户手动杀掉 App 重开才动。业务是服务端在跑的,不该由客户端在不在线 // 决定它推不推进。下面这套 cron 就是补这个缺口。 StuckSweepCron = "0 */1 * * * ?" // 每分钟扫一次 // StuckTranscribeAfter 转写中的记录距上次查询超过这么久(秒)就兜底查一次。 // 取 60s:比 TranscribeQueryMinInterval(10s) 大得多,正常有客户端在轮询时 // 根本轮不到兜底出手,只在真的没人管时才介入。 StuckTranscribeAfter int64 = 60 // StuckTranscribeGiveUp 提交转写后超过这么久还没完成就判失败。 // 不设上限的话,第三方把任务弄丢的记录会被每分钟白查一次、直到天荒地老。 StuckTranscribeGiveUp int64 = 2 * 60 * 60 // StuckSummarizeAfter 停在「待总结/总结中」超过这么久就重新入队。 // AI 一轮通常 10s 内,取 5 分钟留足余量,避免把正在跑的重复提交。 StuckSummarizeAfter int64 = 5 * 60 // StuckSweepLimit 每轮最多处理多少条。积压时不要一次性把第三方打爆。 StuckSweepLimit = 10 ) // toByteDanceLang 将内部 BCP-47 语言码转换为字节跳动 audomodel 支持的格式。 // 不在支持列表内返回空字符串,调用方应路由到其他引擎。 var bcp47ToByteDance = map[string]string{ "": "", "zh-CN": "zh", "zh-TW": "zh", "zh-HK": "zh", "en-US": "en-US", "en-GB": "en-US", "ja-JP": "ja-JP", "ko-KR": "ko-KR", "id-ID": "id-ID", "es-MX": "es-MX", "es-ES": "es-MX", "pt-BR": "pt-BR", "pt-PT": "pt-BR", "de-DE": "de-DE", "fr-FR": "fr-FR", "fil-PH": "fil-PH", "ms-MY": "ms-MY", "th-TH": "th-TH", "ar-SA": "ar-SA", } func toByteDanceLang(bcp47 string) (lang string, supported bool) { lang, supported = bcp47ToByteDance[bcp47] return } // toDashScopeLang 将内部 BCP-47 语言码转换为百炼 language_hints 的短码格式。 // 返回空字符串表示该语言不传 hints,由引擎自动检测。 // // ⚠️ 这张表只做「格式转换」,不代表模型支持该语种——阿里两套模型支持范围不同 // (qwen-audio-3.0-asr-flash-filetrans 27 种 / paraformer-v2 仅 8 种), // 真正的支持性校验在 sys/aliyun/filetrans 的 modelLangs,选路过滤则由后台 languages 白名单负责。 var bcp47ToDashScope = map[string]string{ "zh-CN": "zh", "zh-TW": "zh", "zh-HK": "zh", "yue-HK": "yue", "en-US": "en", "en-GB": "en", "ja-JP": "ja", "ko-KR": "ko", "ru-RU": "ru", "ru-UA": "ru", "vi-VN": "vi", "tr-TR": "tr", "pl-PL": "pl", "nl-NL": "nl", "it-IT": "it", "uk-UA": "uk", "de-DE": "de", "fr-FR": "fr", "es-ES": "es", "es-MX": "es", "pt-BR": "pt", "pt-PT": "pt", "ar-SA": "ar", "id-ID": "id", "ms-MY": "ms", "th-TH": "th", "fil-PH": "fil", } // toDashScopeLang 返回 DashScope language_hints 值,空字符串表示自动检测 func toDashScopeLang(bcp47 string) string { return bcp47ToDashScope[bcp47] // 不在 map 中自动返回 "" } // ============== 翻译平台语言码转换 ============== // 输入统一是客户端的 BCP-47 码(如 zh-CN / en-US / ja-JP); // 每个平台有自己的接受格式,逐家做映射。未识别一律返回原值,让平台自己决定是否报错, // 绝对不要 fallback 成 "zh"——那会让"未知语言→中文"出现在调用栈深处难以发现。 // 火山翻译 / 阿里 MT:短码 zh / en / ja / ... var bcp47ToShortLang = map[string]string{ "zh-CN": "zh", "zh-TW": "zh", "zh-HK": "zh", "en-US": "en", "en-GB": "en", "ja-JP": "ja", "ko-KR": "ko", "id-ID": "id", "es-MX": "es", "es-ES": "es", "pt-BR": "pt", "pt-PT": "pt", "de-DE": "de", "fr-FR": "fr", "fil-PH": "fil", "ms-MY": "ms", "th-TH": "th", "ar-SA": "ar", "ru-RU": "ru", "ru-UA": "ru", "vi-VN": "vi", "tr-TR": "tr", "pl-PL": "pl", "nl-NL": "nl", "it-IT": "it", "uk-UA": "uk", } // 火山翻译:BCP-47 → 短码(zh / en / ja ...) func toByteDanceTranslateLang(bcp47 string) string { if v, ok := bcp47ToShortLang[bcp47]; ok { return v } return bcp47 } // 阿里 MT:与火山相同的短码体系 func toAliTranslateLang(bcp47 string) string { if v, ok := bcp47ToShortLang[bcp47]; ok { return v } return bcp47 } // 说明:实时翻译接口(modules/user/api_translate.go)用的是 comm.BCP47ToShortLang, // 与本表同源。本函数保持原样不动——echomeet 在跑生产,不为共用去改它的行为。 // Google Cloud Translation:直接接受 BCP-47(en / en-US / zh-CN 都支持),原样透传 func toGoogleTranslateLang(bcp47 string) string { return bcp47 } // Azure Translator:中文要 zh-Hans / zh-Hant,其他用短 ISO(en / ja / ko ...) var bcp47ToMicrosoft = map[string]string{ "zh-CN": "zh-Hans", "zh-TW": "zh-Hant", "zh-HK": "zh-Hant", "en-US": "en", "en-GB": "en", "ja-JP": "ja", "ko-KR": "ko", "id-ID": "id", "es-MX": "es", "es-ES": "es", "pt-BR": "pt", "pt-PT": "pt", "de-DE": "de", "fr-FR": "fr", "fil-PH": "fil", "ms-MY": "ms", "th-TH": "th", "ar-SA": "ar", "ru-RU": "ru", "ru-UA": "ru", "vi-VN": "vi", "tr-TR": "tr", "pl-PL": "pl", "nl-NL": "nl", "it-IT": "it", "uk-UA": "uk", } func toMicrosoftTranslateLang(bcp47 string) string { if v, ok := bcp47ToMicrosoft[bcp47]; ok { return v } return bcp47 } // isByteDanceSupported 判断 BCP-47 语言是否被字节跳动支持 func isByteDanceSupported(bcp47 string) bool { _, ok := bcp47ToByteDance[bcp47] return ok } // baseLang 取 BCP-47 的基础语言子标签(首个 '-' 之前),如 es-MX→es、zh-TW→zh。 // 无区域后缀(如 "es")或空串则原样返回。仅用于模板查找的区域回退,不参与各平台语言码映射。 func baseLang(bcp47 string) string { for i := 0; i < len(bcp47); i++ { if bcp47[i] == '-' { return bcp47[:i] } } return bcp47 } // ============== 会议纪要:自动语种 / 总结输出语言 ============== // normalizeAutoLang 客户端表达「自动 / 不翻译」的两种写法(空串、"auto")统一成空串。 func normalizeAutoLang(lang string) string { lang = strings.TrimSpace(lang) if strings.EqualFold(lang, "auto") { return "" } return lang } // summaryLangNames 总结输出语言的英文名,拼进给大模型的指令里(模型对语种名比对 BCP-47 码更稳)。 // 不在表里的按原码写,模型一般也认得。 var summaryLangNames = map[string]string{ "zh-CN": "Simplified Chinese", "zh-TW": "Traditional Chinese", "zh-HK": "Traditional Chinese", "en-US": "English", "en-GB": "English", "ja-JP": "Japanese", "ko-KR": "Korean", "fr-FR": "French", "de-DE": "German", "es-ES": "Spanish", "es-MX": "Spanish", "pt-PT": "Portuguese", "pt-BR": "Portuguese", "it-IT": "Italian", "ru-RU": "Russian", "ar-SA": "Arabic", "th-TH": "Thai", "vi-VN": "Vietnamese", "id-ID": "Indonesian", "ms-MY": "Malay", "tr-TR": "Turkish", "nl-NL": "Dutch", "pl-PL": "Polish", "hi-IN": "Hindi", "uk-UA": "Ukrainian", } // summaryLanguageInstruction 追加在总结/概览模板 system prompt 末尾的输出语言要求。 // // 用英文写是刻意的:它是给模型的元指令,不是展示给用户的文字,与模板本身的语种无关; // 写成某一种自然语言的「请用中文回答」反而会和模板语言打架。 // - to 为空 = 不翻译:跟随转写内容的语言(录音是英文就出英文纪要); // - to 有值 = 翻译:整篇用目标语言输出,转写原文是什么语言都一样。 func summaryLanguageInstruction(to string) string { to = normalizeAutoLang(to) if to == "" { return "\n\n[Output language] Write the entire output in the same language as the transcript. Do not translate it into another language." } name := summaryLangNames[to] if name == "" { name = to } return fmt.Sprintf("\n\n[Output language] Write the entire output in %s (%s), regardless of the language used in the transcript.", name, to) }