diff --git a/README.md b/README.md index 39fc9ca..635600b 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ systemctl --user status voice-input.service voice-input-hud.service ## Safe support diagnostics -Use `voice-input diagnostics [--format text|json]` as the canonical output for support reports. Schema 4 contains bounded stage statuses and failure categories; aggregate streaming delivery/result timings and counts (ready, partial, nonempty partial, segment-final, audio packets and sent duration, queue delays, finish, task completion/failure, and finalization); bounded Audio3 timestamp aggregates (timestamp-bearing results, accepted timed units, truncated units, results with rejected timestamp metadata, and the latest event-reported valid numeric audio-relative end in milliseconds); the Audio3 native-pass mode/decision/reason; and a safe configuration summary for one session only: the active or most recently completed session. Each normal result contributes at most one rejected-timestamp-metadata count when its timestamp block contains any invalid scalar, relationship, words shape, or processed unit; truncation alone contributes zero. The latest valid end is overwritten by a later event that supplies one, with no cross-event monotonicity assumption. Timestamp diagnostics never include sentence IDs, timed-unit text or punctuation, transcripts, or provider messages. The safe summary reports the Audio3 endpoint mode and region, only whether a Workspace ID is configured, whether language hints and heartbeat are enabled, the effective maximum sentence silence and semantic-punctuation state after preset resolution, and the dynamic-vocabulary entry count. It never includes the Workspace ID, endpoint/host, model, key, or vocabulary terms. A failed provider task may include an optional strictly bounded provider error identifier. That session summary remains available after completion and is reset when the next recording starts. It contains no audio, credentials, endpoints, model names, provider messages, window/application data, prompt context, tooltips, session history, or normal recognized/refined transcript text. +Use `voice-input diagnostics [--format text|json]` as the canonical output for support reports. Schema 4 contains bounded stage statuses and failure categories; aggregate streaming delivery/result timings and counts (ready, partial, nonempty partial, segment-final, audio packets and sent duration, queue delays, finish, task completion/failure, and finalization); bounded Audio3 timestamp aggregates (timestamp-bearing results, accepted timed units, truncated units, results with rejected timestamp metadata, and the latest event-reported valid numeric audio-relative end in milliseconds); the Audio3 native-pass mode/decision/reason; and a safe configuration summary for one session only: the active or most recently completed session. Each normal result contributes at most one rejected-timestamp-metadata count when its timestamp block contains any invalid scalar, relationship, words shape, or processed unit; truncation alone contributes zero. The latest valid end is overwritten by a later event that supplies one, with no cross-event monotonicity assumption. Timestamp diagnostics never include sentence IDs, timed-unit text or punctuation, transcripts, or provider messages. The safe summary reports the Audio3 endpoint mode and region, whether language hints and heartbeat are enabled, the effective maximum sentence silence and semantic-punctuation state after preset resolution, and the dynamic-vocabulary entry count. It never includes route identifiers, endpoint/host values, model, key, or vocabulary terms. A failed provider task may include an optional strictly bounded provider error identifier. That session summary remains available after completion and is reset when the next recording starts. It contains no audio, credentials, endpoints, model names, provider messages, window/application data, prompt context, tooltips, session history, or normal recognized/refined transcript text. Do not paste `voice-input status` output into reports, with or without `--extended`. Status output is intended for local UI integration and can include the current or most recent transcript and tooltip text. @@ -89,11 +89,11 @@ Do not paste `voice-input status` output into reports, with or without `--extend Qwen-Audio-3 is available as an explicit experimental provider. It is disabled by default and is not offered by the stable setup wizard. The explicit experimental gate and setup-wizard omission remain intentional while the beta is prepared. To try it, open Settings, choose **Qwen-Audio-3 (experimental)**, acknowledge the experimental-provider warning, and save. The existing encrypted Alibaba credential is shared with this provider. -**Endpoint mode** defaults to **Regional**, with **Beijing** as the default region; **Singapore** is also selectable. In Regional mode, an empty **Workspace ID** uses Alibaba's reviewed legacy host for the selected region, while a nonempty value routes both Streaming and Native through that workspace's reviewed regional hostname. Workspace ID is treated as an opaque provider value. Because it occupies one hostname label, Voice Input enforces only the transport constraint of 1–63 ASCII letters, digits, or hyphens with no leading or trailing hyphen; this is not Alibaba business-ID validation. Voice Input does not add a workspace header, query field, or request-body field. **Custom** mode uses the configured raw Streaming and Native URLs exactly and ignores the dormant Workspace ID. +**Endpoint mode** defaults to **Regional**, with **Beijing** as the default region; **Singapore** is also selectable. Regional mode selects the fixed reviewed legacy Streaming and Native hosts for the chosen region. **Custom** mode uses the configured Streaming and Native URLs exactly, including path, port, and query bytes. -Migration is presence-aware. A configuration without `endpoint_mode` migrates to Regional Beijing only when both old URLs exactly equal the canonical Beijing pair, or to Regional Singapore only when both exactly equal the canonical Singapore pair. Either migration preserves an explicitly configured Workspace ID exactly; an absent or empty value remains empty. Mixed pairs, current workspace hosts, loopback endpoints, proxies, and any custom path, port, or query migrate to Custom with both URL strings preserved byte-for-byte; Voice Input never infers a Workspace ID from a host. An explicit endpoint mode takes precedence, and an explicit region takes precedence with Beijing as its missing-field default. Dormant raw URLs remain preserved. +Migration is presence-aware. A configuration without `endpoint_mode` migrates to Regional Beijing only when both old URLs exactly equal the canonical Beijing pair, or to Regional Singapore only when both exactly equal the canonical Singapore pair. Mixed pairs, noncanonical hosts, loopback endpoints, proxies, and any custom path, port, or query migrate to Custom with both URL strings preserved byte-for-byte. An explicit endpoint mode takes precedence, and an explicit region takes precedence with Beijing as its missing-field default. Dormant raw URLs remain preserved. -Alibaba API keys are region- and workspace-scoped. Changing either setting may require replacing the single encrypted Alibaba credential. Voice Input never probes another region/workspace and never migrates a key automatically. This phase intentionally does not add separate credentials per region. Singapore availability does not establish feature parity: each model, control combination, language/vocabulary scenario, and workspace route still requires authorized live validation. +Alibaba API keys are region-scoped. Changing the region may require replacing the encrypted Alibaba credential. Voice Input never probes another region and never migrates a key automatically. Singapore availability does not establish feature parity: each model, control combination, and language/vocabulary scenario still requires authorized live validation. The streaming model supplies realtime text. **Language hints** and **streaming heartbeat** are independent opt-in settings and are disabled by default. Enabling language hints sends the existing language selection to Audio3: English uses `en`; Simplified and Traditional Chinese use `zh,en`; Japanese uses `ja,en`; and Korean uses `ko,en`. The extra English hint retains mixed-English recognition for Chinese, Japanese, and Korean; leaving the switch disabled preserves the provider's automatic detection. Enabling streaming heartbeat keeps long silent push-to-talk sessions alive while correctly formatted audio frames continue. diff --git a/README.zh-CN.md b/README.zh-CN.md index e22fa58..9ba4b18 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -81,7 +81,7 @@ systemctl --user status voice-input.service voice-input-hud.service ## 安全的支持诊断信息 -提交支持报告时,请使用 `voice-input diagnostics [--format text|json]` 作为标准诊断输出。Schema 4 只提供当前 session 或最近完成的 session,并使用固定字段记录各阶段状态和失败类别、流式投递与结果的汇总耗时和计数(ready、partial、非空 partial、segment-final、音频包及已发送音频时长、队列延迟、finish、任务完成或失败以及 finalization)、有明确数量上限的 Audio3 时间戳汇总数据(包含时间戳的结果数、接受的计时单元数、因超过上限而截断的单元数、时间戳元数据被拒绝的结果数,以及最近一个事件报告的有效音频相对结束毫秒数)、Audio3 原生最终处理的模式/决定/原因,以及安全的配置摘要。如果一个普通结果的时间戳块包含任何无效标量、关系、`words` 结构或已处理单元,该结果最多使“时间戳元数据被拒绝的结果数”增加 1;仅发生截断时不会增加该计数。如果后续事件提供有效结束时间,诊断信息会直接覆盖此前的值,不假设不同事件之间单调递增。时间戳诊断信息不会包含句子 ID、计时单元文本或标点、transcript 或提供商消息。安全摘要说明 Audio3 的端点模式和区域、是否配置了工作空间 ID、语言提示和 heartbeat 是否启用、解析预设后生效的最大句末静音时长和语义标点状态,以及动态词汇表的词条数量。它不会包含工作空间 ID、端点或主机、模型、key 或词条内容。提供商任务失败时,诊断信息可能包含一个可选且长度和字符范围受到严格限制的提供商错误标识符。完成后的摘要会保留到下一次录音开始,并在新录音开始时重置。输出不包含音频、凭据、端点、模型名称、提供商消息、窗口或应用信息、prompt context、tooltip、session 历史记录,也不包含正常的识别或整理后 transcript 文本。 +提交支持报告时,请使用 `voice-input diagnostics [--format text|json]` 作为标准诊断输出。Schema 4 只提供当前 session 或最近完成的 session,并使用固定字段记录各阶段状态和失败类别、流式投递与结果的汇总耗时和计数(ready、partial、非空 partial、segment-final、音频包及已发送音频时长、队列延迟、finish、任务完成或失败以及 finalization)、有明确数量上限的 Audio3 时间戳汇总数据(包含时间戳的结果数、接受的计时单元数、因超过上限而截断的单元数、时间戳元数据被拒绝的结果数,以及最近一个事件报告的有效音频相对结束毫秒数)、Audio3 原生最终处理的模式/决定/原因,以及安全的配置摘要。如果一个普通结果的时间戳块包含任何无效标量、关系、`words` 结构或已处理单元,该结果最多使“时间戳元数据被拒绝的结果数”增加 1;仅发生截断时不会增加该计数。如果后续事件提供有效结束时间,诊断信息会直接覆盖此前的值,不假设不同事件之间单调递增。时间戳诊断信息不会包含句子 ID、计时单元文本或标点、transcript 或提供商消息。安全摘要说明 Audio3 的端点模式和区域、语言提示和 heartbeat 是否启用、解析预设后生效的最大句末静音时长和语义标点状态,以及动态词汇表的词条数量。它不会包含路由标识、端点或主机值、模型、key 或词条内容。提供商任务失败时,诊断信息可能包含一个可选且长度和字符范围受到严格限制的提供商错误标识符。完成后的摘要会保留到下一次录音开始,并在新录音开始时重置。输出不包含音频、凭据、端点、模型名称、提供商消息、窗口或应用信息、prompt context、tooltip、session 历史记录,也不包含正常的识别或整理后 transcript 文本。 请勿把 `voice-input status` 的输出粘贴到报告中,无论是否使用 `--extended`。状态输出用于本地 UI 集成,可能包含当前或最近一次 transcript 和 tooltip 文本。 @@ -89,11 +89,11 @@ systemctl --user status voice-input.service voice-input-hud.service Qwen-Audio-3 目前作为需要明确启用的实验性提供商使用。该选项默认关闭,稳定安装向导也不会提供它。在 beta 准备期间,明确的实验功能开关以及安装向导不提供该选项都是有意保留的设计。如需试用,请打开 Settings,选择 **Qwen-Audio-3(实验性)**,确认实验功能警告后保存。该提供商与现有 Alibaba 实时识别共用同一份加密凭据。 -**端点模式**默认使用**区域路由**,默认区域为**北京**,也可以选择**新加坡**。在区域路由模式下,**工作空间 ID** 留空时会使用所选区域经过审核的 Alibaba 旧版主机;填写该值后,流式请求和原生请求都会通过该工作空间对应且经过审核的区域主机名发送。程序把工作空间 ID 视为不透明的提供商值。因为该值会占用一个主机名 label,Voice Input 只验证传输约束:必须包含 1–63 个 ASCII 字母、数字或连字符,且首尾不能是连字符。这项检查不代表 Alibaba 的业务 ID 规则。Voice Input 不会添加工作空间 header、query 字段或请求正文(request body)字段。**自定义**模式会原样使用已配置的流式 URL 和原生 URL,并忽略处于非活动状态的工作空间 ID。 +**端点模式**默认使用**区域路由**,默认区域为**北京**,也可以选择**新加坡**。区域路由会为所选区域使用固定且经过审核的旧版流式主机和原生主机。**自定义**模式会原样使用已配置的流式 URL 和原生 URL,包括路径、端口和 query 字节。 -迁移过程会区分字段是否存在。未包含 `endpoint_mode` 的配置只有在两个旧 URL 与北京标准组合完全相同时,才会迁移到北京区域路由;只有在两个 URL 与新加坡标准组合完全相同时,才会迁移到新加坡区域路由。两种迁移都会原样保留明确配置的工作空间 ID;该字段缺失或为空时仍保持为空。混合组合、当前工作空间主机、回环端点、代理,以及带有自定义路径、端口或 query 的配置都会迁移到自定义模式,并逐字节保留两个 URL 字符串;Voice Input 绝不会根据主机推断工作空间 ID。明确配置的端点模式具有优先级;明确配置的区域也具有优先级,缺少区域字段时默认使用北京。程序还会保留处于非活动状态的原始 URL。 +迁移过程会区分字段是否存在。未包含 `endpoint_mode` 的配置只有在两个旧 URL 与北京标准组合完全相同时,才会迁移到北京区域路由;只有在两个 URL 与新加坡标准组合完全相同时,才会迁移到新加坡区域路由。混合组合、非标准主机、回环端点、代理,以及带有自定义路径、端口或 query 的配置都会迁移到自定义模式,并逐字节保留两个 URL 字符串。明确配置的端点模式具有优先级;明确配置的区域也具有优先级,缺少区域字段时默认使用北京。程序还会保留处于非活动状态的原始 URL。 -Alibaba API key 受区域和工作空间范围约束。更改其中任何一项后,用户可能需要替换同一份加密 Alibaba 凭据。Voice Input 绝不会探测其他区域或工作空间,也不会自动迁移 key。本阶段有意不增加按区域分别保存的凭据。支持选择新加坡区域并不表示已经实现完整功能一致性;每个模型、控制项组合、语言或词汇表场景以及工作空间路由仍需完成经过授权的在线验证。 +Alibaba API key 受区域范围约束。更改区域后,用户可能需要替换加密的 Alibaba 凭据。Voice Input 绝不会探测其他区域,也不会自动迁移 key。支持选择新加坡区域并不表示已经实现完整功能一致性;每个模型、控制项组合以及语言或词汇表场景仍需完成经过授权的在线验证。 流式模型负责提供实时文本。**语言提示**和**流式 heartbeat** 是两个相互独立的选用设置,默认均为关闭。启用语言提示后,程序会把现有语言选项发送给 Audio3:英语使用 `en`;简体中文和繁体中文使用 `zh,en`;日语使用 `ja,en`;韩语使用 `ko,en`。中文、日语和韩语的额外英语提示用于保留英语混合识别;关闭该开关会保留服务商的自动检测行为。启用流式 heartbeat 后,只要程序继续发送格式正确的音频帧,它就能使长时间静音的按键说话 session 保持连接。 diff --git a/assets/config.toml b/assets/config.toml index 58f241e..48395e8 100644 --- a/assets/config.toml +++ b/assets/config.toml @@ -38,7 +38,6 @@ final_pass_enable_itn = false experimental_enabled = false endpoint_mode = "regional" region = "beijing" -workspace_id = "" endpoint = "wss://dashscope.aliyuncs.com/api-ws/v1/inference" model = "qwen-audio-3.0-asr-flash-streaming" language_hints_enabled = false diff --git a/assets/quickshell-settings/I18n.qml b/assets/quickshell-settings/I18n.qml index 8dfdd0a..3929691 100644 --- a/assets/quickshell-settings/I18n.qml +++ b/assets/quickshell-settings/I18n.qml @@ -106,7 +106,7 @@ QtObject { "Realtime recognition behavior.": "设置实时识别行为。", "Replace Alibaba API key": "替换 Alibaba API key", "Enter a new credential": "输入新凭据", - "API keys are region- and workspace-scoped; Voice Input never probes another route or migrates a key automatically.": "API key 受区域和工作空间范围约束;语音输入绝不会探测其他路由,也不会自动迁移 key。", + "API keys are region-scoped; Voice Input never probes another region or migrates a key automatically.": "API key 受区域范围约束;语音输入绝不会探测其他区域,也不会自动迁移 key。", "Realtime model": "实时模型", "Turn mode": "分段模式", "Server VAD": "服务端 VAD", @@ -116,14 +116,11 @@ QtObject { "Selecting the provider does not enable this acknowledgement.": "仅选择此提供商不会自动确认并启用实验功能。", "Endpoint mode": "端点模式", "Regional": "区域路由", - "Regional routing uses reviewed Alibaba hosts. Custom keeps the raw streaming and native endpoints in Advanced settings.": "区域路由使用经过审核的 Alibaba 主机。自定义模式会在高级设置中保留原始流式端点和原生端点。", + "Regional routing uses the fixed reviewed Alibaba host for the selected region. Custom keeps the exact streaming and native endpoints in Advanced settings.": "区域路由使用所选区域固定且经过审核的 Alibaba 主机。自定义模式会在高级设置中保留原样的流式端点和原生端点。", "Region": "区域", "Beijing": "北京", "Singapore": "新加坡", "Select the region that owns the configured Alibaba API key. Singapore controls still require scenario-specific live validation.": "请选择当前 Alibaba API key 所属的区域。新加坡区域的各项控制仍需按具体场景完成在线验证。", - "Workspace ID": "工作空间 ID", - "Optional": "可选", - "Optional. Empty uses the regional legacy route. A nonempty value must satisfy the DNS-label transport constraint (1–63 ASCII letters, digits, or hyphens; no leading or trailing hyphen). This is not provider business-ID validation. API keys are region- and workspace-scoped; Voice Input never probes or migrates them.": "可选。留空时使用对应区域的旧版路由。非空值必须满足 DNS label 传输约束:包含 1–63 个 ASCII 字母、数字或连字符,且首尾不能是连字符。这不是对提供商业务 ID 规则的验证。API key 受区域和工作空间范围约束;语音输入绝不会探测或迁移 key。", "Enable language hints": "启用语言提示", "Opt in to sending the selected language as an Audio3 recognition hint.": "选择将所选语言作为 Audio3 识别提示发送。", "Enable streaming heartbeat": "启用流式 heartbeat", @@ -375,9 +372,9 @@ QtObject { if (match) return "已通过 " + match[1] + " 配置"; - match = source.match(/^(.*)\. Blank keeps it unchanged\. API keys are region- and workspace-scoped; Voice Input never probes another route or migrates a key automatically\.$/); + match = source.match(/^(.*)\. Blank keeps it unchanged\. API keys are region-scoped; Voice Input never probes another region or migrates a key automatically\.$/); if (match) - return root.tr(match[1]) + "。留空将保持不变。API key 受区域和工作空间范围约束;语音输入绝不会探测其他路由,也不会自动迁移 key。"; + return root.tr(match[1]) + "。留空将保持不变。API key 受区域范围约束;语音输入绝不会探测其他区域,也不会自动迁移 key。"; match = source.match(/^(.*)\. Blank keeps it unchanged\.$/); if (match) diff --git a/assets/quickshell-settings/SettingsController.qml b/assets/quickshell-settings/SettingsController.qml index 0eccf6f..3050e2b 100644 --- a/assets/quickshell-settings/SettingsController.qml +++ b/assets/quickshell-settings/SettingsController.qml @@ -90,7 +90,6 @@ QtObject { "experimental_enabled": false, "endpoint_mode": "regional", "region": "beijing", - "workspace_id": "", "endpoint": "wss://dashscope.aliyuncs.com/api-ws/v1/inference", "model": "qwen-audio-3.0-asr-flash-streaming", "language_hints_enabled": false, @@ -305,7 +304,7 @@ QtObject { } function fieldNeedsAdvanced(path) { - const audio3Advanced = path.indexOf("asr.alibaba_audio3.") === 0 && path !== "asr.alibaba_audio3.experimental_enabled" && path !== "asr.alibaba_audio3.endpoint_mode" && path !== "asr.alibaba_audio3.region" && path !== "asr.alibaba_audio3.workspace_id" && path !== "asr.alibaba_audio3.language_hints_enabled" && path !== "asr.alibaba_audio3.heartbeat_enabled" && path !== "asr.alibaba_audio3.recognition_preset" && path !== "asr.alibaba_audio3.vocabulary" && path !== "asr.alibaba_audio3.native_final_pass_mode"; + const audio3Advanced = path.indexOf("asr.alibaba_audio3.") === 0 && path !== "asr.alibaba_audio3.experimental_enabled" && path !== "asr.alibaba_audio3.endpoint_mode" && path !== "asr.alibaba_audio3.region" && path !== "asr.alibaba_audio3.language_hints_enabled" && path !== "asr.alibaba_audio3.heartbeat_enabled" && path !== "asr.alibaba_audio3.recognition_preset" && path !== "asr.alibaba_audio3.vocabulary" && path !== "asr.alibaba_audio3.native_final_pass_mode"; return path === "audio.sample_rate" || path === "audio.partial_interval_ms" || path === "audio.pre_roll_ms" || path === "asr.connect_timeout_ms" || path === "asr.finalize_timeout_ms" || path.indexOf("asr.alibaba.endpoint") === 0 || path.indexOf("asr.alibaba.vad_") === 0 || path.indexOf("asr.alibaba.silence_") === 0 || path.indexOf("asr.alibaba.final_pass_") === 0 || audio3Advanced || path === "llm.api_base_url" || path === "llm.timeout_ms" || path === "llm.provider_sort" || path === "llm.agent_context_max_chars" || path === "output.type_delay_ms" || path === "output.pre_type_delay_ms" || path === "output.paste_keys" || path === "output.prefer_paste_for_xwayland" || path === "output.xwayland_paste_keys" || path === "hud.margin_bottom" || path === "hud.height" || path === "hud.offset_x" || path === "hud.offset_y" || path === "hud.nudge_step"; } diff --git a/assets/quickshell-settings/SpeechPage.qml b/assets/quickshell-settings/SpeechPage.qml index a649f71..c33dc54 100644 --- a/assets/quickshell-settings/SpeechPage.qml +++ b/assets/quickshell-settings/SpeechPage.qml @@ -153,7 +153,7 @@ SettingsPage { theme: root.theme label: "Replace Alibaba API key" value: root.controller.alibabaCredential - help: root.controller.credentialLabel("alibaba-api-key") + ". Blank keeps it unchanged. API keys are region- and workspace-scoped; Voice Input never probes another route or migrates a key automatically." + help: root.controller.credentialLabel("alibaba-api-key") + ". Blank keeps it unchanged. API keys are region-scoped; Voice Input never probes another region or migrates a key automatically." password: true placeholderText: "Enter a new credential" error: root.controller.errorFor("credentials.alibaba-api-key") @@ -223,7 +223,7 @@ SettingsPage { value: root.controller.value("asr.alibaba_audio3.endpoint_mode", "regional") labels: ["Regional", "Custom"] values: ["regional", "custom"] - help: "Regional routing uses reviewed Alibaba hosts. Custom keeps the raw streaming and native endpoints in Advanced settings." + help: "Regional routing uses the fixed reviewed Alibaba host for the selected region. Custom keeps the exact streaming and native endpoints in Advanced settings." error: root.controller.errorFor("asr.alibaba_audio3.endpoint_mode") enabled: !root.controller.busy onSelected: (value) => { @@ -246,20 +246,6 @@ SettingsPage { } } - SettingTextField { - visible: root.controller.value("asr.alibaba_audio3.endpoint_mode", "regional") === "regional" || root.controller.errorFor("asr.alibaba_audio3.workspace_id").length > 0 - theme: root.theme - label: "Workspace ID" - value: root.controller.value("asr.alibaba_audio3.workspace_id", "") - placeholderText: "Optional" - help: "Optional. Empty uses the regional legacy route. A nonempty value must satisfy the DNS-label transport constraint (1–63 ASCII letters, digits, or hyphens; no leading or trailing hyphen). This is not provider business-ID validation. API keys are region- and workspace-scoped; Voice Input never probes or migrates them." - error: root.controller.errorFor("asr.alibaba_audio3.workspace_id") - enabled: !root.controller.busy - onEdited: (value) => { - return root.controller.setValue("asr.alibaba_audio3.workspace_id", value); - } - } - SettingSwitch { theme: root.theme label: "Enable language hints" diff --git a/docs/qwen-audio3-milestone2-api-decisions.md b/docs/qwen-audio3-milestone2-api-decisions.md index 43b91ab..7fe8403 100644 --- a/docs/qwen-audio3-milestone2-api-decisions.md +++ b/docs/qwen-audio3-milestone2-api-decisions.md @@ -10,7 +10,7 @@ Milestone 2 is limited to: 1. Audio3 streaming VAD and sentence-boundary controls with named presets; 2. bounded timestamp parsing and timestamp-aware assembly foundations; -3. Beijing/Singapore regional and workspace endpoint routing. +3. Beijing/Singapore regional and custom endpoint routing. It does not add reconnect/replay, `continue-task` context, application/window collection, Filetrans, diarization, or new credential-discovery behavior. @@ -89,51 +89,44 @@ The parser borrows transcript text only long enough to apply the existing 16 KiB Duplicate-final suppression and correction replacement remain blocked unless Alibaba publishes a stable revision-identity contract or an implementation is proven correct even when IDs and ranges are reused or revised. A finite live capture can validate parser compatibility and reveal counterexamples, but it cannot establish an undocumented identity guarantee. Reconnect and replay remain Milestone 3 and will not be implemented here. -## Regional and workspace endpoints +## Regional and custom endpoints -Both Audio3 model variants are officially available in Beijing and Singapore with unchanged model IDs. +Both Audio3 model variants are available in Beijing and Singapore with unchanged model IDs. Voice Input intentionally exposes only two routing modes: Regional and Custom. -### Canonical endpoint matrix +### Fixed reviewed endpoint matrix -| API | Region | Legacy endpoint | Current workspace endpoint template | -| --- | --- | --- | --- | -| Streaming WebSocket | Beijing | `wss://dashscope.aliyuncs.com/api-ws/v1/inference` | `wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference` | -| Streaming WebSocket | Singapore | `wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference` | `wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference` | -| Native HTTP | Beijing | `https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` | `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` | -| Native HTTP | Singapore | `https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` | `https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` | - -Alibaba recommends workspace-specific hosts and states that legacy hosts remain usable. Current workspace routing places Workspace ID in the hostname's leftmost label. It is not a query parameter, path component, or request-body field. - -The legacy WebSocket documentation also describes an optional `X-DashScope-WorkSpace` header. The Native HTTP reference does not describe that header and instead documents workspace-specific hosts. Voice Input will use one consistent hostname-derived mechanism for both APIs rather than inventing Native header behavior. +| API | Beijing | Singapore | +| --- | --- | --- | +| Streaming WebSocket | `wss://dashscope.aliyuncs.com/api-ws/v1/inference` | `wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference` | +| Native HTTP | `https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` | `https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` | ### Configuration and migration decisions -- Add `regional` and `custom` endpoint modes. -- Add explicit `beijing` and `singapore` regions. -- In regional mode, an empty Workspace ID uses the official legacy endpoint; a configured Workspace ID derives the reviewed workspace hostname. -- Existing exact Beijing legacy endpoint pairs migrate to regional Beijing with an empty Workspace ID. -- Existing exact Singapore legacy endpoint pairs migrate to regional Singapore with an empty Workspace ID. -- Any noncanonical, mixed, proxied, loopback, query-bearing, or otherwise overridden pair migrates to custom and remains byte-for-byte preserved. -- Regional hosts are selected from constants. Region input is never concatenated into a host. -- Workspace ID has no published provider grammar or length. Voice Input treats it as opaque and applies only the RFC-compatible DNS-label constraints required to place it safely in a hostname. These constraints are transport validation, not a claim about Alibaba's business identifier format. +- Regional selects only the fixed reviewed Streaming and Native pair for Beijing or Singapore. +- Custom uses the configured Streaming and Native URL strings exactly. +- Existing exact Beijing pairs migrate to Regional Beijing. +- Existing exact Singapore pairs migrate to Regional Singapore. +- Any mixed pair, noncanonical host, loopback endpoint, proxy, custom path, port, query, or otherwise changed pair migrates to Custom and remains byte-for-byte preserved. +- Region input is matched to constants and is never concatenated into a host. +- Explicit endpoint mode and region values take precedence over inferred migration values. +- Dormant custom URLs remain preserved in Regional mode. -API keys are region-specific and workspace-scoped. A key cannot be assumed to work across regions or workspaces. Voice Input will retain one encrypted Alibaba credential, warn that changing region/workspace may require replacing it, and never probe another region automatically. +API keys are region-scoped. Voice Input retains one encrypted Alibaba credential, warns that changing region may require replacing it, and never probes another region or migrates a key automatically. ### Deterministic implementation state Milestone 2 item 7 is implemented and validated locally without network access: - kebab-case `regional`/`custom` endpoint modes and `beijing`/`singapore` regions use Regional Beijing for new configurations; -- presence-aware migration recognizes only the two exact canonical legacy pairs and preserves any explicitly configured Workspace ID exactly (absent or empty stays empty); all mixed, workspace-host, loopback, proxy, custom path/port/query, and otherwise changed pairs remain Custom byte-for-byte, without inferring a Workspace ID; -- one pure typed resolver selects reviewed constants for both Streaming and Native, uses legacy hosts for empty workspaces, and constructs the reviewed regional workspace host only after value-free DNS-label transport validation; -- Custom mode ignores its dormant Workspace ID, while Regional mode ignores and preserves dormant custom URLs; neither mode sends a workspace header, query field, or request-body field; +- presence-aware migration recognizes only the two exact canonical pairs; all other pairs remain Custom byte-for-byte; +- one pure, infallible typed resolver selects the fixed reviewed constants for both APIs or returns the exact Custom values; - production WebSocket and Native requests use the resolved target, while authorization, request bodies, response sanitization, redirect behavior, models, and controls remain unchanged; -- normal Settings displays mode, region, optional Workspace ID, and the credential-scope warning without displaying a derived URL; raw URLs are shown only for Custom routing or a routed validation error; -- schema 4 diagnostics add only endpoint mode, region, and a Workspace-ID-configured boolean. They never include the Workspace ID, endpoint/host, model, or key, and schema-3/schema-4 compatibility defaults remain readable. +- normal Settings displays endpoint mode and region; raw URLs are shown only for Custom routing or a routed validation error; +- diagnostics schema 4 retains endpoint mode and region while excluding endpoint values, model, key, and route identifiers. Older schema 4 snapshots with removed fields remain readable through unknown-field tolerance. -Authorized live canaries subsequently succeeded for the Beijing Regional empty-workspace Streaming and Native routes. Singapore and workspace-specific routes remain untested because no matching scoped credential is configured. The implementation and Beijing canaries do not establish Singapore feature parity or workspace-route success. +Authorized live canaries succeeded for the Beijing Regional Streaming and Native routes. Singapore remains untested because no matching region credential is configured. The implementation and Beijing canaries do not establish Singapore feature parity. -Official pages do not provide a complete per-region matrix for language hints, heartbeat, vocabulary, sentence controls, thresholds, and timestamp behavior. The shared API reference documents these controls without a regional exclusion. Each model, field combination, and scenario used in Singapore still requires its own authorized live validation; the result will not be presented as complete feature parity. +Official pages do not provide a complete per-region matrix for language hints, heartbeat, vocabulary, sentence controls, thresholds, and timestamp behavior. Each model, field combination, and scenario used in Singapore still requires its own authorized live validation; the result will not be presented as complete feature parity. ## Implementation and live-test gates @@ -143,7 +136,6 @@ Official pages do not provide a complete per-region matrix for language hints, h - exact request-envelope fields for confirmed VAD controls; - bounded timestamp parsing and aggregate-only diagnostics; - regional/custom endpoint migration and pure endpoint resolution; -- hostname-based workspace routing; - safe settings controls and privacy regression tests. ### Block pending evidence @@ -151,7 +143,6 @@ Official pages do not provide a complete per-region matrix for language hints, h - separate speech/noise threshold fields; - guaranteed character-level timing; - identity-dependent timestamp duplicate suppression or correction replacement without an official contract or an algorithm that remains correct under revisions; -- inferred Workspace ID business grammar; - automatic credential-region probing; - claims of complete regional feature parity. @@ -161,12 +152,11 @@ Completed within the bounded scope documented in [`qwen-audio3-milestone2-evalua - Low-latency and Long-form field combinations and observable segmentation effects across a private pause/noise corpus; - aggregate-only parser compatibility observation for live partial/final timestamp metadata, without retaining sentence IDs or inferring a revision identity contract; -- Beijing Regional empty-workspace Streaming and Native canaries. +- Beijing Regional Streaming and Native canaries. Still requires a matching scoped credential and separate authorization: - Singapore model/control scenarios, including dynamic vocabulary; -- workspace-specific Streaming and Native routing; - any broader regional feature-parity claim. ## Official sources @@ -179,6 +169,4 @@ Still requires a matching scoped credential and separate authorization: - [Native recognition guide](https://help.aliyun.com/zh/model-studio/non-realtime-speech-recognition-user-guide) - [ASR model specifications](https://help.aliyun.com/zh/model-studio/asr-model/) - [Model Studio regional Base URLs](https://help.aliyun.com/zh/model-studio/base-url) -- [Obtain Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id) - [API key acquisition](https://help.aliyun.com/zh/model-studio/get-api-key) -- [Workspace permission management](https://help.aliyun.com/zh/model-studio/permission-management-overview) diff --git a/docs/qwen-audio3-milestone2-evaluation.md b/docs/qwen-audio3-milestone2-evaluation.md index 285f236..d6e1a07 100644 --- a/docs/qwen-audio3-milestone2-evaluation.md +++ b/docs/qwen-audio3-milestone2-evaluation.md @@ -4,7 +4,7 @@ Evaluation date: 2026-08-05 Candidate commit: `8c11b9e` -This report contains aggregate results only. It does not include audio, reference text, normal or refined transcripts, provider messages, credentials, endpoints, Workspace IDs, model names, window/application data, or prompt context. Source recordings, generated WAV files, per-request transcripts, errors, detailed per-file construction metadata, and raw per-session diagnostics remain in a private local directory excluded from Git. +This report contains aggregate results only. It does not include audio, reference text, normal or refined transcripts, provider messages, credentials, endpoints, route identifiers, model names, window/application data, or prompt context. Source recordings, generated WAV files, per-request transcripts, errors, detailed per-file construction metadata, and raw per-session diagnostics remain in a private local directory excluded from Git. ## Scope @@ -15,9 +15,9 @@ The authorized evaluation covered: - one real microphone session per preset; - one live cancellation canary; - bounded timestamp parser compatibility; -- Beijing Regional routing with an empty Workspace ID for Streaming and Native. +- Beijing Regional routing through the fixed reviewed hosts for Streaming and Native. -Singapore and workspace-specific routing were not attempted because no matching region/workspace credential was configured. Their live status remains unknown. +Singapore routing was not attempted because no matching region credential was configured. Its live status remains unknown. ## Fixed private corpus @@ -64,14 +64,14 @@ A live Long-form session was cancelled with F8 while toggle-mode recording was a ## Regional route canaries -With Regional Beijing selected and Workspace ID empty, one private WAV completed successfully through each API: +With Regional Beijing selected, one private WAV completed successfully through each fixed reviewed host: | Route | Result | Command elapsed | | --- | --- | ---: | | Beijing Streaming legacy host | Success | 1,364 ms | | Beijing Native legacy host | Success | 984 ms | -The existing encrypted credential was not probed against Singapore or a workspace host. No automatic fallback or cross-region attempt was made. +The existing encrypted credential was not probed against Singapore. No automatic fallback or cross-region attempt was made. ## Approximate request cost @@ -84,4 +84,4 @@ Using the Beijing rates already recorded for Milestone 1—¥0.00033 per Streami 3. **Keep Long-form as an explicit option.** Its field combination was accepted, semantic punctuation produced valid results, and it preserved all pause clauses. This corpus does not show a general accuracy advantage. 4. **Keep Custom controls and validation unchanged.** The provider documentation still exposes one optional combined speech/noise threshold; separate threshold fields remain unsupported. 5. **Retain bounded timestamp parsing without identity-based transcript changes.** Live events were parser-compatible, but finite captures cannot prove stable revision identity semantics. -6. **Mark Beijing empty-workspace routing live-validated for the exercised Streaming and Native calls.** Singapore and workspace-specific routes remain pending a credential scoped to those routes; no feature-parity claim is made. +6. **Mark Beijing fixed regional routing live-validated for the exercised Streaming and Native calls.** Singapore remains pending a credential scoped to that region; no feature-parity claim is made. diff --git a/docs/qwen-audio3-optimization-checklist.md b/docs/qwen-audio3-optimization-checklist.md index 13995d1..3d3d75e 100644 --- a/docs/qwen-audio3-optimization-checklist.md +++ b/docs/qwen-audio3-optimization-checklist.md @@ -42,7 +42,7 @@ Complete items 1–4, then stop and evaluate their combined effect before starti ### 3. Dynamic hotwords (`vocabulary`) -- [x] Confirm term limits, weight range, super-hotword behavior, precedence, and request schema. Streaming and short Flash accept an object mapping term text to integer weight in their `parameters` object. Each request permits at most 2,000 unique terms; weights are `1–5` or exactly `50`, with at most 50 weight-50 terms. A term containing non-ASCII characters is limited to 15 total characters; a pure-ASCII term is limited to 7 space-separated segments. Dynamic `vocabulary` takes precedence over `vocabulary_id` when both are present. Singapore child-workspace support is officially ambiguous, so Milestone 1 treats hotwords as Beijing-confirmed only. +- [x] Confirm term limits, weight range, super-hotword behavior, precedence, and request schema. Streaming and short Flash accept an object mapping term text to integer weight in their `parameters` object. Each request permits at most 2,000 unique terms; weights are `1–5` or exactly `50`, with at most 50 weight-50 terms. A term containing non-ASCII characters is limited to 15 total characters; a pure-ASCII term is limited to 7 space-separated segments. Dynamic `vocabulary` takes precedence over `vocabulary_id` when both are present. Singapore support is not live-validated, so Milestone 1 treats hotwords as Beijing-confirmed only. - [!] Design bounded global and optional per-application configuration without automatically collecting private text. Milestone 1 implements only bounded global entries. Any future profile must exact-match a locally captured application class, override same-named global terms locally, and never send or log the class or title. Focused-window capture is deliberately deferred. - [x] Validate and trim terms locally; define duplicate-term and invalid-weight behavior. No normalization beyond trimming is performed. - [x] Add Settings UI controls that make all remotely sent terms visible to the user. @@ -90,13 +90,14 @@ Provider-facing decisions, confirmed fields, endpoint constants, ambiguities, an - [x] Keep timestamp diagnostics aggregate-only and transcript-free. Schema 4 records timestamp-bearing result, accepted-unit and truncated-unit counts, plus one rejected-result count for each normal result containing any timestamp metadata defect. Truncation alone is not rejection. The latest valid numeric end is overwritten when a later event supplies one; no cross-event monotonicity is assumed. - [x] Observe authorized live Audio3 partial/final timestamp metadata for parser compatibility and counterexample discovery only. Aggregate diagnostics showed no parser rejection in the bounded sample; no stable revision identity contract is inferred from finite captures, and sentence IDs/text were not retained. -### 7. Workspace regional endpoints +### 7. Regional endpoints -- [~] Deterministic implementation, migration, Settings, diagnostics, privacy coverage, and offline validation are complete. Authorized Beijing Regional empty-workspace Streaming and Native canaries succeeded. Item 7 remains in progress because Singapore and workspace-specific routes require a matching scoped credential; no Singapore feature-parity or workspace live-success claim is made. -- [x] Add explicit Beijing/Singapore region selection and derive official Streaming and Native workspace endpoints from fixed reviewed constants. -- [x] Validate the optional Workspace ID only as a DNS-label hostname transport constraint and document regional/workspace credential expectations without logging or diagnosing endpoint, host, Workspace ID, model, or credential values. -- [x] Preserve custom Streaming and Native endpoints byte-for-byte for testing and compatibility; Custom ignores its dormant Workspace ID and sends no workspace header/query/body field. -- [x] Document presence-aware migration from exact canonical legacy DashScope endpoint pairs while preserving an explicitly configured Workspace ID exactly; an absent or empty value remains empty, and mixed, workspace-host, loopback, proxy, and custom path/port/query pairs remain Custom without Workspace ID inference. +- [x] Deterministic implementation, migration, Settings, diagnostics, privacy coverage, and offline validation are complete. Authorized Beijing Regional Streaming and Native canaries succeeded. +- [~] Singapore live validation remains pending because no matching region credential is configured; no Singapore feature-parity claim is made. +- [x] Add explicit Beijing/Singapore region selection and select fixed reviewed Streaming and Native endpoints from constants. +- [x] Preserve custom Streaming and Native endpoints byte-for-byte for testing and compatibility. +- [x] Document presence-aware migration from exact canonical DashScope endpoint pairs; mixed pairs, noncanonical hosts, loopback endpoints, proxies, and custom path/port/query pairs remain Custom without host inference. +- [x] Keep diagnostics schema 4 while reporting only endpoint mode and region; endpoint values and route identifiers remain private. ## Milestone 3 — Context, resilience, and separate workflows @@ -126,11 +127,11 @@ Provider-facing decisions, confirmed fields, endpoint constants, ambiguities, an | 2026-08-02 | Milestone 1 live evaluation | Completed with documented limits | Real streaming/native APIs accepted the new controls. Seven controlled, transcript-free diagnostic samples covered short Chinese/English/mixed speech, configured technical terms, silence, repeated noise, long speech, and silence beyond 60 seconds. Healthy short streams skipped native; empty and long cases invoked it; no fallback or ASR failure occurred. One of two noise attempts produced a false positive. Aggregate timings, invocation rate, approximate Beijing pricing, decisions, and limitations are in `docs/qwen-audio3-milestone1-evaluation.md`. | | 2026-08-05 | Milestone 1 closeout | Completed locally; no new live evaluation | Added deterministic production-used Audio3 lifecycle coverage for 65 simulated heartbeat-enabled seconds and cancellation, corrected terminal/committed-only streaming telemetry, made malformed persisted provider error codes degrade to unavailable, and advanced diagnostics to schema v3. Included validated `max_sentence_silence` and semantic-punctuation configuration foundations at user direction; Milestone 2 item 5 remains in progress because presets, multi-threshold/noise controls, and full pause/noise evaluation are still outstanding. | | 2026-08-05 | Fixed-corpus vocabulary A/B | Completed | Retained a private local Git-excluded corpus with 30 clips and replayed identical audio under no vocabulary, weight 5, and weight 50 in Streaming and Native. All 180 requests succeeded. Weight 50 produced 10/10 recall for each tested term in both modes; weight 5 showed no recall uplift. Streaming had no negative-control insertion, while Native weight 50 inserted a configured term for one deliberately sound-alike control. Only aggregate results are committed. | -| 2026-08-05 | Milestone 2 Phase 0 API verification | Completed; implementation not started | Confirmed the Audio3 VAD field schema, timestamp paths and units, Beijing/Singapore endpoint matrix, hostname-based workspace routing, and region/workspace credential scope from official documentation. Separate speech/noise thresholds and guaranteed character timestamps are unsupported. Identity-dependent timestamp deduplication remains blocked without an official revision contract or a revision-safe algorithm; finite captures are observational only. Workspace ID has no published business grammar, so only hostname transport safety can be validated. Decisions and live-test gates are in `docs/qwen-audio3-milestone2-api-decisions.md`. | +| 2026-08-05 | Milestone 2 Phase 0 API verification | Completed; implementation not started | Confirmed the Audio3 VAD field schema, timestamp paths and units, and Beijing/Singapore fixed endpoint matrix from official documentation. Separate speech/noise thresholds and guaranteed character timestamps are unsupported. Identity-dependent timestamp deduplication remains blocked without an official revision contract or a revision-safe algorithm; finite captures are observational only. Decisions and live-test gates are in `docs/qwen-audio3-milestone2-api-decisions.md`. | | 2026-08-05 | Milestone 2 item 5 deterministic implementation | Completed | Added presence-aware preset migration, pure effective-control resolution, universal finite-threshold validation for Settings JSON serialization, active-custom range/combination validation, exact streaming request omission behavior, custom-only advanced Settings controls, effective diagnostics summaries, deterministic migration/request/settings/privacy tests, sample configuration, and bilingual documentation. Standard remains unchanged and is the default. | | 2026-08-05 | Milestone 2 item 6 bounded timestamp implementation | Parsing, aggregate diagnostics, and bounded live compatibility observation completed; identity use blocked | Added borrowed typed Audio3 sentence/timed-unit parsing with a 512-candidate per-result bound, a 1 MiB WebSocket transport cap, required-bound and event-local range validation, per-malformed-result rejection counting, event-latest overwrite semantics, saturating aggregate-only telemetry, best-effort daemon persistence, and diagnostics schema 4 with schema-3 compatibility. Transcript event and authoritative task-finished assembly remain unchanged. Timed-unit text, punctuation and sentence IDs are discarded before telemetry. Three controlled live sessions accepted 73 timestamp-bearing results and 106 timed units without rejection or truncation; finite captures do not establish an identity contract. | | 2026-08-05 | Milestone 2 bounded live evaluation | Items 5 and 6 completed within documented limits; Beijing portion of item 7 validated | Replayed 17 fixed private WAV files under three presets for 51/51 successful Streaming requests; all pause clauses were retained, silence/noise-only stayed empty, and one realtime session per preset plus one cancellation canary completed. Aggregate results, one-sample latency limits, parser observations, cost estimate, and decisions are in `docs/qwen-audio3-milestone2-evaluation.md`. | -| 2026-08-05 | Milestone 2 item 7 deterministic regional/workspace routing | Implemented; Beijing empty-workspace live canaries passed; other scoped routes pending | Added presence-aware exact-pair migration, typed Regional/Custom and Beijing/Singapore configuration, fixed-constant Streaming/Native endpoint resolution, hostname-only workspace routing with value-free DNS-label transport errors, Custom byte preservation, active-provider isolation, normal Settings controls and credential warnings, schema-4-safe routing summaries, bilingual documentation, and offline resolver/request/privacy tests. Authorized Beijing Regional empty-workspace Streaming and Native calls succeeded. Singapore and workspace-specific calls were not attempted without a matching credential. | +| 2026-08-05 | Milestone 2 item 7 deterministic regional routing | Implemented; Beijing live canaries passed; Singapore pending | Added presence-aware exact-pair migration, typed Regional/Custom and Beijing/Singapore configuration, fixed-constant Streaming/Native endpoint resolution, Custom byte preservation, active-provider isolation, normal Settings controls and region-scoped credential warnings, schema-4-safe routing summaries, bilingual documentation, and offline resolver/request/privacy tests. Authorized Beijing Regional Streaming and Native calls succeeded. Singapore was not attempted without a matching region credential. | ## Official references diff --git a/src/app.rs b/src/app.rs index 02e2663..928103c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -507,7 +507,6 @@ mod tests { config.asr.alibaba_audio3.endpoint = "https://SENTINEL_ENDPOINT.example".into(); config.asr.alibaba_audio3.native_endpoint = "https://SENTINEL_NATIVE_ENDPOINT.example".into(); - config.asr.alibaba_audio3.workspace_id = "SENTINEL_WORKSPACE".into(); config.asr.alibaba_audio3.api_key = "SENTINEL_CREDENTIAL".into(); let mut snapshot = Snapshot::idle(&config); @@ -529,7 +528,6 @@ mod tests { "SENTINEL_NATIVE_MODEL", "SENTINEL_ENDPOINT", "SENTINEL_NATIVE_ENDPOINT", - "SENTINEL_WORKSPACE", "SENTINEL_CREDENTIAL", "SENTINEL_TOOLTIP", "SENTINEL_TRANSCRIPT", diff --git a/src/backend/qwen_audio3/native.rs b/src/backend/qwen_audio3/native.rs index 82cffe9..3f6e02e 100644 --- a/src/backend/qwen_audio3/native.rs +++ b/src/backend/qwen_audio3/native.rs @@ -20,9 +20,7 @@ pub(crate) fn transcribe_full_audio(config: &Config, wav_path: &Path) -> Result< bail!("Qwen-Audio-3 native ASR requires a configured credential"); } - let endpoints = audio3 - .resolve_endpoints() - .context("failed to resolve Qwen-Audio-3 routing")?; + let endpoints = audio3.resolve_endpoints(); let wav_bytes = read_bounded_wav(wav_path)?; let body = request_body( &audio3.native_model, @@ -386,7 +384,6 @@ mod tests { weight: 5, }]; config.asr.alibaba_audio3.endpoint_mode = Audio3EndpointMode::Custom; - config.asr.alibaba_audio3.workspace_id = "dormant-workspace".into(); config.asr.alibaba_audio3.native_endpoint = endpoint; config.asr.alibaba_audio3.native_model = "test-native-model".into(); @@ -404,12 +401,6 @@ mod tests { .lines() .any(|line| line.eq_ignore_ascii_case("authorization: Bearer test-bearer-token")) ); - assert!( - !headers - .to_ascii_lowercase() - .contains("x-dashscope-workspace") - ); - assert!(!headers.contains("dormant-workspace")); let body: Value = serde_json::from_str(body).unwrap(); assert_eq!(body["parameters"]["language_hints"], json!(["ko", "en"])); assert_eq!(body["parameters"]["vocabulary"], json!({"Voice Input": 5})); diff --git a/src/backend/qwen_audio3/streaming.rs b/src/backend/qwen_audio3/streaming.rs index 291bada..0dae625 100644 --- a/src/backend/qwen_audio3/streaming.rs +++ b/src/backend/qwen_audio3/streaming.rs @@ -416,9 +416,7 @@ fn websocket_request_for_config( audio3: &AlibabaAudio3Config, api_key: &str, ) -> Result> { - let endpoints = audio3 - .resolve_endpoints() - .map_err(|_| anyhow!("failed to resolve Qwen-Audio-3 routing"))?; + let endpoints = audio3.resolve_endpoints(); websocket_request(endpoints.streaming(), api_key) } @@ -2047,32 +2045,27 @@ mod tests { request.headers()[tungstenite::http::header::AUTHORIZATION], "Bearer test-key" ); - assert!(request.headers().get("X-DashScope-WorkSpace").is_none()); } #[test] - fn production_websocket_request_seam_uses_resolved_target_without_workspace_header() { + fn production_websocket_request_seam_uses_resolved_target() { let mut audio3 = AlibabaAudio3Config { region: Audio3Region::Singapore, - workspace_id: "workspace-7".into(), ..AlibabaAudio3Config::default() }; let request = websocket_request_for_config(&audio3, "test-key").unwrap(); assert_eq!( request.uri().to_string(), - "wss://workspace-7.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference" + crate::config::AUDIO3_SINGAPORE_STREAMING_ENDPOINT ); - assert!(request.headers().get("X-DashScope-WorkSpace").is_none()); audio3.endpoint_mode = Audio3EndpointMode::Custom; - audio3.workspace_id = "dormant-invalid-workspace".into(); audio3.endpoint = "ws://127.0.0.1:1234/custom?opaque=a%2Fb&x=1".into(); let custom = websocket_request_for_config(&audio3, "test-key").unwrap(); assert_eq!( custom.uri().to_string(), "ws://127.0.0.1:1234/custom?opaque=a%2Fb&x=1" ); - assert!(custom.headers().get("X-DashScope-WorkSpace").is_none()); const ENDPOINT_SENTINEL: &str = "private endpoint construction sentinel"; audio3.endpoint = ENDPOINT_SENTINEL.into(); @@ -2087,7 +2080,7 @@ mod tests { #[test] fn websocket_handshake_failure_discards_raw_error_and_source_chain() { - const SENTINEL: &str = "private TLS certificate host and workspace sentinel"; + const SENTINEL: &str = "private TLS certificate host and route sentinel"; let error = sanitize_websocket_handshake_failure(io::Error::other(SENTINEL)); assert_eq!(error.to_string(), "Qwen-Audio-3 websocket handshake failed"); diff --git a/src/config.rs b/src/config.rs index 2323b20..74a2fcb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -102,7 +102,6 @@ pub struct AlibabaAudio3Config { pub experimental_enabled: bool, pub endpoint_mode: Audio3EndpointMode, pub region: Audio3Region, - pub workspace_id: String, /// Dormant in regional mode; retained for custom routing and migration. pub endpoint: String, #[serde(default, skip_serializing)] @@ -166,11 +165,6 @@ pub const AUDIO3_BEIJING_NATIVE_ENDPOINT: &str = pub const AUDIO3_SINGAPORE_NATIVE_ENDPOINT: &str = "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"; -const AUDIO3_BEIJING_WORKSPACE_SUFFIX: &str = "cn-beijing.maas.aliyuncs.com"; -const AUDIO3_SINGAPORE_WORKSPACE_SUFFIX: &str = "ap-southeast-1.maas.aliyuncs.com"; -const AUDIO3_STREAMING_PATH: &str = "/api-ws/v1/inference"; -const AUDIO3_NATIVE_PATH: &str = "/api/v1/services/aigc/multimodal-generation/generation"; - #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedAudio3Endpoints { streaming: String, @@ -187,17 +181,6 @@ impl ResolvedAudio3Endpoints { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Audio3EndpointResolutionError; - -impl fmt::Display for Audio3EndpointResolutionError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("invalid Audio3 regional endpoint configuration") - } -} - -impl Error for Audio3EndpointResolutionError {} - #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum Audio3RecognitionPreset { @@ -217,47 +200,28 @@ pub struct EffectiveAudio3RecognitionControls { } impl AlibabaAudio3Config { - pub fn resolve_endpoints( - &self, - ) -> std::result::Result { + pub fn resolve_endpoints(&self) -> ResolvedAudio3Endpoints { if self.endpoint_mode == Audio3EndpointMode::Custom { - return Ok(ResolvedAudio3Endpoints { + return ResolvedAudio3Endpoints { streaming: self.endpoint.clone(), native: self.native_endpoint.clone(), - }); - } - - if self.workspace_id.is_empty() { - let (streaming, native) = match self.region { - Audio3Region::Beijing => ( - AUDIO3_BEIJING_STREAMING_ENDPOINT, - AUDIO3_BEIJING_NATIVE_ENDPOINT, - ), - Audio3Region::Singapore => ( - AUDIO3_SINGAPORE_STREAMING_ENDPOINT, - AUDIO3_SINGAPORE_NATIVE_ENDPOINT, - ), }; - return Ok(ResolvedAudio3Endpoints { - streaming: streaming.into(), - native: native.into(), - }); } - if !audio3_workspace_is_transport_safe(&self.workspace_id) { - return Err(Audio3EndpointResolutionError); - } - let suffix = match self.region { - Audio3Region::Beijing => AUDIO3_BEIJING_WORKSPACE_SUFFIX, - Audio3Region::Singapore => AUDIO3_SINGAPORE_WORKSPACE_SUFFIX, - }; - Ok(ResolvedAudio3Endpoints { - streaming: format!( - "wss://{}.{suffix}{AUDIO3_STREAMING_PATH}", - self.workspace_id + let (streaming, native) = match self.region { + Audio3Region::Beijing => ( + AUDIO3_BEIJING_STREAMING_ENDPOINT, + AUDIO3_BEIJING_NATIVE_ENDPOINT, ), - native: format!("https://{}.{suffix}{AUDIO3_NATIVE_PATH}", self.workspace_id), - }) + Audio3Region::Singapore => ( + AUDIO3_SINGAPORE_STREAMING_ENDPOINT, + AUDIO3_SINGAPORE_NATIVE_ENDPOINT, + ), + }; + ResolvedAudio3Endpoints { + streaming: streaming.into(), + native: native.into(), + } } pub fn effective_recognition_controls(&self) -> EffectiveAudio3RecognitionControls { @@ -315,7 +279,6 @@ struct RawAlibabaAudio3Config { experimental_enabled: bool, endpoint_mode: Option, region: Option, - workspace_id: String, endpoint: String, api_key: String, model: String, @@ -341,7 +304,6 @@ impl Default for RawAlibabaAudio3Config { experimental_enabled: defaults.experimental_enabled, endpoint_mode: None, region: None, - workspace_id: defaults.workspace_id, endpoint: defaults.endpoint, api_key: defaults.api_key, model: defaults.model, @@ -385,19 +347,14 @@ impl<'de> Deserialize<'de> for AlibabaAudio3Config { (None, Some(mode)) => mode, (None, None) => NativeFinalPassMode::StreamingOnly, }; - let (endpoint_mode, region, workspace_id) = match raw.endpoint_mode { - Some(mode) => ( - mode, - raw.region.unwrap_or_default(), - raw.workspace_id.clone(), - ), + let (endpoint_mode, region) = match raw.endpoint_mode { + Some(mode) => (mode, raw.region.unwrap_or_default()), None if raw.endpoint == AUDIO3_BEIJING_STREAMING_ENDPOINT && raw.native_endpoint == AUDIO3_BEIJING_NATIVE_ENDPOINT => { ( Audio3EndpointMode::Regional, raw.region.unwrap_or(Audio3Region::Beijing), - raw.workspace_id.clone(), ) } None if raw.endpoint == AUDIO3_SINGAPORE_STREAMING_ENDPOINT @@ -406,14 +363,9 @@ impl<'de> Deserialize<'de> for AlibabaAudio3Config { ( Audio3EndpointMode::Regional, raw.region.unwrap_or(Audio3Region::Singapore), - raw.workspace_id.clone(), ) } - None => ( - Audio3EndpointMode::Custom, - raw.region.unwrap_or_default(), - raw.workspace_id.clone(), - ), + None => (Audio3EndpointMode::Custom, raw.region.unwrap_or_default()), }; let multi_threshold_mode_enabled = raw.multi_threshold_mode_enabled.unwrap_or(false); let recognition_preset = raw.recognition_preset.unwrap_or_else(|| { @@ -431,7 +383,6 @@ impl<'de> Deserialize<'de> for AlibabaAudio3Config { experimental_enabled: raw.experimental_enabled, endpoint_mode, region, - workspace_id, endpoint: raw.endpoint, api_key: raw.api_key, model: raw.model, @@ -656,7 +607,6 @@ impl Default for Config { experimental_enabled: false, endpoint_mode: Audio3EndpointMode::Regional, region: Audio3Region::Beijing, - workspace_id: String::new(), endpoint: AUDIO3_BEIJING_STREAMING_ENDPOINT.into(), api_key: String::new(), model: "qwen-audio-3.0-asr-flash-streaming".into(), @@ -911,18 +861,7 @@ impl Config { false, ); } - Audio3EndpointMode::Regional => { - if !self.asr.alibaba_audio3.workspace_id.is_empty() - && !audio3_workspace_is_transport_safe( - &self.asr.alibaba_audio3.workspace_id, - ) - { - fields.insert( - "asr.alibaba_audio3.workspace_id".into(), - "must be a valid DNS label for hostname transport".into(), - ); - } - } + Audio3EndpointMode::Regional => {} } validate_text( @@ -1311,16 +1250,6 @@ fn missing_revision() -> String { "missing:fnv1a64:cbf29ce484222325".into() } -fn audio3_workspace_is_transport_safe(workspace_id: &str) -> bool { - let bytes = workspace_id.as_bytes(); - (1..=63).contains(&bytes.len()) - && bytes.first() != Some(&b'-') - && bytes.last() != Some(&b'-') - && bytes - .iter() - .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-') -} - fn validate_audio3_vocabulary(vocabulary: &[Audio3VocabularyTerm]) -> Option { if vocabulary.len() > MAX_AUDIO3_VOCABULARY_TERMS { return Some(format!( @@ -1706,7 +1635,6 @@ mod tests { Audio3EndpointMode::Regional ); assert_eq!(config.asr.alibaba_audio3.region, Audio3Region::Beijing); - assert!(config.asr.alibaba_audio3.workspace_id.is_empty()); assert_eq!( config.asr.alibaba_audio3.endpoint, "wss://dashscope.aliyuncs.com/api-ws/v1/inference" @@ -2435,35 +2363,20 @@ native_timeout_ms = 20000 ); let mut audio3 = super::AlibabaAudio3Config::default(); - for (region, workspace, streaming, native) in [ + for (region, streaming, native) in [ ( Audio3Region::Beijing, - "", - AUDIO3_BEIJING_STREAMING_ENDPOINT.to_string(), - AUDIO3_BEIJING_NATIVE_ENDPOINT.to_string(), - ), - ( - Audio3Region::Singapore, - "", - AUDIO3_SINGAPORE_STREAMING_ENDPOINT.to_string(), - AUDIO3_SINGAPORE_NATIVE_ENDPOINT.to_string(), - ), - ( - Audio3Region::Beijing, - "Workspace-9", - "wss://Workspace-9.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference".into(), - "https://Workspace-9.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation".into(), + AUDIO3_BEIJING_STREAMING_ENDPOINT, + AUDIO3_BEIJING_NATIVE_ENDPOINT, ), ( Audio3Region::Singapore, - "Workspace-9", - "wss://Workspace-9.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference".into(), - "https://Workspace-9.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation".into(), + AUDIO3_SINGAPORE_STREAMING_ENDPOINT, + AUDIO3_SINGAPORE_NATIVE_ENDPOINT, ), ] { audio3.region = region; - audio3.workspace_id = workspace.into(); - let resolved = audio3.resolve_endpoints().unwrap(); + let resolved = audio3.resolve_endpoints(); assert_eq!(resolved.streaming(), streaming); assert_eq!(resolved.native(), native); } @@ -2471,15 +2384,13 @@ native_timeout_ms = 20000 #[test] fn legacy_audio3_endpoint_pairs_migrate_only_when_both_are_exact() { - let parse = - |streaming: &str, native: &str, workspace_id: &str| -> super::AlibabaAudio3Config { - serde_json::from_value(serde_json::json!({ - "workspace_id": workspace_id, - "endpoint": streaming, - "native_endpoint": native - })) - .unwrap() - }; + let parse = |streaming: &str, native: &str| -> super::AlibabaAudio3Config { + serde_json::from_value(serde_json::json!({ + "endpoint": streaming, + "native_endpoint": native + })) + .unwrap() + }; for (streaming, native, region) in [ ( AUDIO3_BEIJING_STREAMING_ENDPOINT, @@ -2492,20 +2403,12 @@ native_timeout_ms = 20000 Audio3Region::Singapore, ), ] { - let migrated = parse(streaming, native, "explicit-workspace"); + let migrated = parse(streaming, native); assert_eq!(migrated.endpoint_mode, Audio3EndpointMode::Regional); assert_eq!(migrated.region, region); - assert_eq!(migrated.workspace_id, "explicit-workspace"); assert_eq!(migrated.endpoint, streaming); assert_eq!(migrated.native_endpoint, native); } - let migrated_empty = parse( - AUDIO3_BEIJING_STREAMING_ENDPOINT, - AUDIO3_BEIJING_NATIVE_ENDPOINT, - "", - ); - assert_eq!(migrated_empty.endpoint_mode, Audio3EndpointMode::Regional); - assert!(migrated_empty.workspace_id.is_empty()); let custom_pairs = [ ( @@ -2517,12 +2420,8 @@ native_timeout_ms = 20000 AUDIO3_BEIJING_NATIVE_ENDPOINT, ), ( - "wss://legacy-workspace.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference", - "https://legacy-workspace.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", - ), - ( - "wss://legacy-workspace.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference", - "https://legacy-workspace.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", + "wss://tenant.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference", + "https://tenant.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", ), ("ws://127.0.0.1:8123/stream", "http://localhost:8124/native"), ( @@ -2543,146 +2442,60 @@ native_timeout_ms = 20000 ), ]; for (streaming, native) in custom_pairs { - let migrated = parse(streaming, native, ""); + let migrated = parse(streaming, native); assert_eq!(migrated.endpoint_mode, Audio3EndpointMode::Custom); assert_eq!(migrated.region, Audio3Region::Beijing); - assert!(migrated.workspace_id.is_empty()); assert_eq!(migrated.endpoint, streaming); assert_eq!(migrated.native_endpoint, native); - let resolved = migrated.resolve_endpoints().unwrap(); + let resolved = migrated.resolve_endpoints(); assert_eq!(resolved.streaming(), streaming); assert_eq!(resolved.native(), native); } } #[test] - fn explicit_audio3_endpoint_mode_and_region_win_and_preserve_dormant_values() { + fn explicit_audio3_mode_and_region_take_precedence() { let custom_streaming = "wss://proxy.example/ws?opaque=a%2Fb&x=1"; let custom_native = "https://proxy.example/native?opaque=%20"; - let inferred_custom: super::AlibabaAudio3Config = - serde_json::from_value(serde_json::json!({ - "workspace_id": "opaque dormant value", - "endpoint": custom_streaming, - "native_endpoint": custom_native - })) - .unwrap(); - assert_eq!(inferred_custom.endpoint_mode, Audio3EndpointMode::Custom); - assert_eq!(inferred_custom.workspace_id, "opaque dormant value"); - assert_eq!(inferred_custom.endpoint, custom_streaming); - assert_eq!(inferred_custom.native_endpoint, custom_native); - let custom: super::AlibabaAudio3Config = serde_json::from_value(serde_json::json!({ "endpoint_mode": "custom", "region": "singapore", - "workspace_id": "dormant workspace value", "endpoint": custom_streaming, "native_endpoint": custom_native })) .unwrap(); - assert_eq!(custom.endpoint_mode, Audio3EndpointMode::Custom); + let resolved = custom.resolve_endpoints(); assert_eq!(custom.region, Audio3Region::Singapore); - assert_eq!(custom.workspace_id, "dormant workspace value"); - let resolved = custom.resolve_endpoints().unwrap(); assert_eq!(resolved.streaming(), custom_streaming); assert_eq!(resolved.native(), custom_native); - let inferred_mode_with_explicit_region: super::AlibabaAudio3Config = - serde_json::from_value(serde_json::json!({ - "region": "singapore", - "workspace_id": "retained-for-legacy-pair", - "endpoint": AUDIO3_BEIJING_STREAMING_ENDPOINT, - "native_endpoint": AUDIO3_BEIJING_NATIVE_ENDPOINT - })) - .unwrap(); - assert_eq!( - inferred_mode_with_explicit_region.endpoint_mode, - Audio3EndpointMode::Regional - ); - assert_eq!( - inferred_mode_with_explicit_region.region, - Audio3Region::Singapore - ); - assert_eq!( - inferred_mode_with_explicit_region.workspace_id, - "retained-for-legacy-pair" - ); - let regional: super::AlibabaAudio3Config = serde_json::from_value(serde_json::json!({ - "endpoint_mode": "regional", "region": "singapore", - "workspace_id": "workspace-1", - "endpoint": "dormant streaming bytes", - "native_endpoint": "dormant native bytes" + "endpoint": AUDIO3_BEIJING_STREAMING_ENDPOINT, + "native_endpoint": AUDIO3_BEIJING_NATIVE_ENDPOINT })) .unwrap(); - assert_eq!(regional.endpoint, "dormant streaming bytes"); - assert_eq!(regional.native_endpoint, "dormant native bytes"); + assert_eq!(regional.endpoint_mode, Audio3EndpointMode::Regional); + assert_eq!(regional.region, Audio3Region::Singapore); assert_eq!( - regional.resolve_endpoints().unwrap().streaming(), - "wss://workspace-1.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference" + regional.resolve_endpoints().streaming(), + AUDIO3_SINGAPORE_STREAMING_ENDPOINT ); } #[test] - fn regional_workspace_transport_validation_is_bounded_private_and_active_only() { - for valid in ["a", "A-9", &"a".repeat(63)] { - let mut config = Config::default(); - config.asr.provider = AsrProvider::AlibabaQwenAudio3; - config.asr.alibaba_audio3.experimental_enabled = true; - config.asr.alibaba_audio3.workspace_id = valid.into(); - config.validate().expect("transport-safe DNS label"); - config.asr.alibaba_audio3.resolve_endpoints().unwrap(); - } - - const SENTINEL: &str = "private_workspace_value"; - for invalid in [ - "-leading", - "trailing-", - "under_score", - "contains.dot", - "工作区", - &"a".repeat(64), - SENTINEL, - ] { - let mut config = Config::default(); - config.asr.provider = AsrProvider::AlibabaQwenAudio3; - config.asr.alibaba_audio3.experimental_enabled = true; - config.asr.alibaba_audio3.workspace_id = invalid.into(); - let error = config.validate().expect_err("unsafe hostname label"); - assert_eq!( - error.fields["asr.alibaba_audio3.workspace_id"], - "must be a valid DNS label for hostname transport" - ); - assert!(!format!("{error}: {:?}", error.fields).contains(invalid)); - let resolver_error = config - .asr - .alibaba_audio3 - .resolve_endpoints() - .expect_err("resolver applies the same transport boundary"); - assert_eq!( - resolver_error.to_string(), - "invalid Audio3 regional endpoint configuration" - ); - assert!(!resolver_error.to_string().contains(invalid)); - } - - let mut inactive = Config::default(); - inactive.asr.alibaba_audio3.workspace_id = SENTINEL.into(); - inactive.asr.alibaba_audio3.endpoint = "dormant invalid streaming".into(); - inactive.asr.alibaba_audio3.native_endpoint = "dormant invalid native".into(); - inactive - .validate() - .expect("inactive Audio3 routing is isolated"); - - let mut custom = inactive; - custom.asr.provider = AsrProvider::AlibabaQwenAudio3; - custom.asr.alibaba_audio3.experimental_enabled = true; - custom.asr.alibaba_audio3.endpoint_mode = Audio3EndpointMode::Custom; - custom.asr.alibaba_audio3.endpoint = "wss://proxy.example/ws".into(); - custom.asr.alibaba_audio3.native_endpoint = "https://proxy.example/native".into(); - custom - .validate() - .expect("custom mode ignores its dormant workspace value"); + fn removed_legacy_route_identifier_is_ignored_omitted_and_has_no_request_effect() { + const OLD_FIELD: &str = "workspace_id"; + let source = format!( + "endpoint_mode = \"regional\"\nregion = \"singapore\"\n{OLD_FIELD} = \"private-route-sentinel\"\nendpoint = \"dormant bytes\"\nnative_endpoint = \"dormant bytes\"\n" + ); + let audio3: super::AlibabaAudio3Config = toml::from_str(&source).unwrap(); + let serialized = toml::to_string(&audio3).unwrap(); + assert!(!serialized.contains(OLD_FIELD)); + assert!(!serialized.contains("private-route-sentinel")); + let resolved = audio3.resolve_endpoints(); + assert_eq!(resolved.streaming(), AUDIO3_SINGAPORE_STREAMING_ENDPOINT); + assert_eq!(resolved.native(), AUDIO3_SINGAPORE_NATIVE_ENDPOINT); } #[test] diff --git a/src/diagnostics.rs b/src/diagnostics.rs index e0b9fc7..eaae396 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -767,13 +767,12 @@ impl SupportPayload { } let _ = writeln!( output, - "Configuration: provider={}, final-pass={}, fallback={}, audio3-endpoint-mode={}, audio3-region={}, audio3-workspace-configured={}, audio3-native-mode={}, audio3-language-hints={}, audio3-heartbeat={}, audio3-max-sentence-silence-ms={}, audio3-semantic-punctuation={}, audio3-vocabulary-count={}", + "Configuration: provider={}, final-pass={}, fallback={}, audio3-endpoint-mode={}, audio3-region={}, audio3-native-mode={}, audio3-language-hints={}, audio3-heartbeat={}, audio3-max-sentence-silence-ms={}, audio3-semantic-punctuation={}, audio3-vocabulary-count={}", self.config.provider.as_str(), enabled_name(self.config.final_pass_enabled), enabled_name(self.config.fallback_enabled), self.config.endpoint_mode.as_str(), self.config.region.as_str(), - enabled_name(self.config.workspace_configured), self.config.audio3_native_final_pass_mode.as_str(), enabled_name(self.config.audio3_language_hints_enabled), enabled_name(self.config.audio3_heartbeat_enabled), @@ -823,7 +822,6 @@ pub struct SafeConfigSummary { pub fallback_enabled: bool, pub endpoint_mode: Audio3EndpointMode, pub region: Audio3Region, - pub workspace_configured: bool, pub audio3_native_final_pass_mode: NativeFinalPassMode, pub audio3_language_hints_enabled: bool, pub audio3_heartbeat_enabled: bool, @@ -849,7 +847,6 @@ impl SafeConfigSummary { fallback_enabled: config.asr.fallback_to_local, endpoint_mode: config.asr.alibaba_audio3.endpoint_mode, region: config.asr.alibaba_audio3.region, - workspace_configured: !config.asr.alibaba_audio3.workspace_id.is_empty(), audio3_native_final_pass_mode: config.asr.alibaba_audio3.native_final_pass_mode, audio3_language_hints_enabled: config.asr.alibaba_audio3.language_hints_enabled, audio3_heartbeat_enabled: config.asr.alibaba_audio3.heartbeat_enabled, @@ -1367,14 +1364,12 @@ mod tests { #[test] fn audio3_mode_and_safe_controls_are_serialized_without_private_values() { const TERM: &str = "private-diagnostics-vocabulary-sentinel"; - const WORKSPACE: &str = "private-workspace-diagnostics-sentinel"; const STREAMING_ENDPOINT: &str = "wss://private-streaming-host.invalid/private-path"; const NATIVE_ENDPOINT: &str = "https://private-native-host.invalid/private-path"; let mut config = Config::default(); config.asr.provider = AsrProvider::AlibabaQwenAudio3; config.asr.alibaba_audio3.endpoint_mode = Audio3EndpointMode::Custom; config.asr.alibaba_audio3.region = Audio3Region::Singapore; - config.asr.alibaba_audio3.workspace_id = WORKSPACE.into(); config.asr.alibaba_audio3.endpoint = STREAMING_ENDPOINT.into(); config.asr.alibaba_audio3.native_endpoint = NATIVE_ENDPOINT.into(); config.asr.alibaba_audio3.native_final_pass_mode = NativeFinalPassMode::Adaptive; @@ -1396,7 +1391,6 @@ mod tests { assert_eq!(json["schema_version"], 4); assert_eq!(json["config"]["endpoint_mode"], "custom"); assert_eq!(json["config"]["region"], "singapore"); - assert_eq!(json["config"]["workspace_configured"], true); assert_eq!(json["config"]["audio3_native_final_pass_mode"], "adaptive"); assert_eq!(json["config"]["audio3_language_hints_enabled"], true); assert_eq!(json["config"]["audio3_heartbeat_enabled"], true); @@ -1406,7 +1400,6 @@ mod tests { let encoded = json.to_string(); for forbidden in [ TERM, - WORKSPACE, STREAMING_ENDPOINT, NATIVE_ENDPOINT, "private-streaming-host", @@ -1416,9 +1409,8 @@ mod tests { assert!(!payload.format_text().contains(forbidden)); } assert!(!encoded.contains("speech_noise_threshold")); - assert!(!encoded.contains("workspace_id")); assert!(!encoded.contains("endpoint\"")); - assert_eq!(json["config"].as_object().unwrap().len(), 12); + assert_eq!(json["config"].as_object().unwrap().len(), 11); let mut diagnostics = Diagnostics::inactive(); diagnostics.start_session( @@ -1445,11 +1437,12 @@ mod tests { } #[test] - fn older_safe_config_summaries_default_new_routing_fields() { + fn older_safe_config_summaries_default_new_routing_fields_and_ignore_removed_fields() { let summary: SafeConfigSummary = serde_json::from_value(json!({ "provider": "alibaba-qwen-audio3", "final_pass_enabled": false, "fallback_enabled": true, + "workspace_configured": true, "audio3_native_final_pass_mode": "streaming-only", "audio3_language_hints_enabled": false, "audio3_heartbeat_enabled": false, @@ -1460,7 +1453,11 @@ mod tests { .expect("older schema 4/3 safe summaries remain readable"); assert_eq!(summary.endpoint_mode, Audio3EndpointMode::Regional); assert_eq!(summary.region, Audio3Region::Beijing); - assert!(!summary.workspace_configured); + assert!( + !serde_json::to_string(&summary) + .unwrap() + .contains("workspace_configured") + ); } #[test] diff --git a/src/settings_backend.rs b/src/settings_backend.rs index 9b0ff2b..bd3abc2 100644 --- a/src/settings_backend.rs +++ b/src/settings_backend.rs @@ -834,7 +834,6 @@ mod tests { "experimental_enabled": false, "endpoint_mode": "regional", "region": "beijing", - "workspace_id": "", "endpoint": "wss://dashscope.aliyuncs.com/api-ws/v1/inference", "model": "qwen-audio-3.0-asr-flash-streaming", "language_hints_enabled": false, @@ -854,76 +853,54 @@ mod tests { } #[test] - fn settings_get_applies_presence_aware_audio3_endpoint_migration() { - for (streaming, native, workspace_id, mode, region) in [ - ( + fn settings_get_migrates_exact_pairs_and_omits_removed_route_identifier() { + let temp = tempfile::tempdir().unwrap(); + let store = ConfigStore::new(temp.path().join("config.toml")); + let source = toml::to_string_pretty(&Config::default()) + .unwrap() + .replace( + "endpoint_mode = \"regional\"\nregion = \"beijing\"\n", + "workspace_id = \"ignored-route-sentinel\"\n", + ) + .replace( + "wss://dashscope.aliyuncs.com/api-ws/v1/inference", "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference", + ) + .replace( + "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", - "legacy-regional-workspace", - "regional", - "singapore", - ), - ( - "wss://proxy.example/custom?opaque=one", - "https://proxy.example/custom?opaque=two", - "dormant-custom-workspace", - "custom", - "beijing", - ), - ] { - let temp = tempfile::tempdir().unwrap(); - let store = ConfigStore::new(temp.path().join("config.toml")); - let source = toml::to_string_pretty(&Config::default()) - .unwrap() - .replace( - "endpoint_mode = \"regional\"\nregion = \"beijing\"\n", - "", - ) - .replace("workspace_id = \"\"", &format!("workspace_id = \"{workspace_id}\"")) - .replace( - "wss://dashscope.aliyuncs.com/api-ws/v1/inference", - streaming, - ) - .replace( - "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", - native, - ); - fs::write(store.path(), source).unwrap(); - - let get = json!({"version": 1, "id": 37, "method": "settings.get", "params": {}}); - let response: Value = - serde_json::from_slice(&handle_line(get.to_string().as_bytes(), &store)).unwrap(); - assert_eq!(response["ok"], true); - let audio3 = &response["result"]["config"]["asr"]["alibaba_audio3"]; - assert_eq!(audio3["endpoint_mode"], mode); - assert_eq!(audio3["region"], region); - assert_eq!(audio3["workspace_id"], workspace_id); - assert_eq!(audio3["endpoint"], streaming); - assert_eq!(audio3["native_endpoint"], native); - - let save = json!({ - "version": 1, - "id": 38, - "method": "settings.save", - "params": { - "revision": response["result"]["revision"], - "config": response["result"]["config"], - "credentials": {}, - "restart": false - } - }); - let saved: Value = - serde_json::from_slice(&handle_line(save.to_string().as_bytes(), &store)).unwrap(); - assert_eq!(saved["ok"], true); - assert_eq!( - saved["result"]["config"]["asr"]["alibaba_audio3"]["workspace_id"], - workspace_id ); - assert_eq!( - store.load().unwrap().config.asr.alibaba_audio3.workspace_id, - workspace_id - ); - } + fs::write(store.path(), source).unwrap(); + + let get = json!({"version": 1, "id": 37, "method": "settings.get", "params": {}}); + let response: Value = + serde_json::from_slice(&handle_line(get.to_string().as_bytes(), &store)).unwrap(); + assert_eq!(response["ok"], true); + let audio3 = &response["result"]["config"]["asr"]["alibaba_audio3"]; + assert_eq!(audio3["endpoint_mode"], "regional"); + assert_eq!(audio3["region"], "singapore"); + assert!(audio3.get("workspace_id").is_none()); + assert!(!response.to_string().contains("ignored-route-sentinel")); + + let save = json!({ + "version": 1, + "id": 38, + "method": "settings.save", + "params": { + "revision": response["result"]["revision"], + "config": response["result"]["config"], + "credentials": {}, + "restart": false + } + }); + let saved: Value = + serde_json::from_slice(&handle_line(save.to_string().as_bytes(), &store)).unwrap(); + assert_eq!(saved["ok"], true); + assert!( + !fs::read_to_string(store.path()) + .unwrap() + .contains("workspace_id") + ); } #[test] @@ -937,7 +914,6 @@ mod tests { "experimental_enabled": true, "endpoint_mode": "custom", "region": "singapore", - "workspace_id": "dormant-workspace", "endpoint": "wss://audio3.example.test/stream?opaque=one", "model": "audio3-stream-test", "language_hints_enabled": true, @@ -993,23 +969,12 @@ mod tests { saved.asr.alibaba_audio3.region, crate::config::Audio3Region::Singapore ); - assert_eq!(saved.asr.alibaba_audio3.workspace_id, "dormant-workspace"); assert_eq!( - saved - .asr - .alibaba_audio3 - .resolve_endpoints() - .unwrap() - .streaming(), + saved.asr.alibaba_audio3.resolve_endpoints().streaming(), "wss://audio3.example.test/stream?opaque=one" ); assert_eq!( - saved - .asr - .alibaba_audio3 - .resolve_endpoints() - .unwrap() - .native(), + saved.asr.alibaba_audio3.resolve_endpoints().native(), "https://audio3.example.test/native?opaque=two" ); assert!(saved.asr.alibaba_audio3.language_hints_enabled); @@ -1031,7 +996,7 @@ mod tests { } #[test] - fn regional_audio3_settings_round_trip_preserves_workspace_and_dormant_urls() { + fn regional_audio3_settings_round_trip_preserves_region_and_dormant_urls() { let temp = tempfile::tempdir().unwrap(); let store = ConfigStore::new(temp.path().join("config.toml")); let loaded = store.load().unwrap(); @@ -1040,7 +1005,7 @@ mod tests { config["asr"]["alibaba_audio3"]["experimental_enabled"] = json!(true); config["asr"]["alibaba_audio3"]["endpoint_mode"] = json!("regional"); config["asr"]["alibaba_audio3"]["region"] = json!("singapore"); - config["asr"]["alibaba_audio3"]["workspace_id"] = json!("workspace-9"); + config["asr"]["alibaba_audio3"]["workspace_id"] = json!("ignored-json-route-sentinel"); config["asr"]["alibaba_audio3"]["endpoint"] = json!("dormant streaming bytes"); config["asr"]["alibaba_audio3"]["native_endpoint"] = json!("dormant native bytes"); let save = json!({ @@ -1061,27 +1026,26 @@ mod tests { saved["result"]["config"]["asr"]["alibaba_audio3"]["region"], "singapore" ); - assert_eq!( - saved["result"]["config"]["asr"]["alibaba_audio3"]["workspace_id"], - "workspace-9" - ); assert_eq!( saved["result"]["config"]["asr"]["alibaba_audio3"]["endpoint"], "dormant streaming bytes" ); - - let get = json!({"version": 1, "id": 36, "method": "settings.get", "params": {}}); - let fetched: Value = - serde_json::from_slice(&handle_line(get.to_string().as_bytes(), &store)).unwrap(); - assert_eq!(fetched["ok"], true); - assert_eq!( - fetched["result"]["config"]["asr"]["alibaba_audio3"], + assert!( saved["result"]["config"]["asr"]["alibaba_audio3"] + .get("workspace_id") + .is_none() + ); + assert!(!saved.to_string().contains("ignored-json-route-sentinel")); + assert!( + !fs::read_to_string(store.path()) + .unwrap() + .contains("ignored-json-route-sentinel") ); + let loaded = store.load().unwrap().config.asr.alibaba_audio3; assert_eq!( - loaded.resolve_endpoints().unwrap().native(), - "https://workspace-9.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation" + loaded.resolve_endpoints().native(), + crate::config::AUDIO3_SINGAPORE_NATIVE_ENDPOINT ); }