Skip to content

fix(omni): 多模态听写输出纯文本并修复翻译不生效 - #1310

Open
Breadswim wants to merge 2 commits into
Open-Less:betafrom
Breadswim:fix/omni-plain-text-output
Open

Breadswim wants to merge 2 commits into
Open-Less:betafrom
Breadswim:fix/omni-plain-text-output

Conversation

@Breadswim

@Breadswim Breadswim commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

摘要

Fixes #1304
Fixes #1306

让多模态听写(OpenAI 兼容通道,实测 gpt-audio-1.5)稳定输出可直接插入的纯文本,开启翻译时只插入目标语言译文。两个 issue 改的是同一组函数(多模态提示词组装、流式上屏过滤、最终文本整理),拆开会互相冲突,因此合在一个 PR。

修复 / 新增 / 改进

  • 提示词适配音频输入
    • 安全约定改为描述录音(omni_audio_injection_defense),不再提请求里并不存在的 <raw_transcript> 信封;
    • 音频旁附上文字指令(omni_audio_user_prompt),system prompt 以标记格式收尾:无翻译为 [[OPENLESS_TEXT]],翻译复用传统模式的源文 / 译文标记;格式里写明录音没有说话内容时标记后留空;
    • 开启翻译时输出语言偏好按 Auto 处理(与 compose_translate_prompts 一致);不再附带请求里并不存在的对话历史说明。
    • 实测对照:去掉文字指令时,gpt-audio-1.5 开启翻译会产出工具调用样的 JSON;只加文字指令、不加标记格式时,仍有约 40%–75% 的回复包 JSON。
  • 没有语音不调用模型:录音里没有声音(沿用静音自动停止的电平规则:连续 3 个 10 ms 块达到阈值)时直接结束为 no speech in the recording,不插入任何内容。Gemini 专用转写与 Live 不受影响。
  • 复述提示词的回复不插入:录音只有「嗯」「啊」、呼气等没有词句的声音时,模型会把文字指令或风格模板里的示例句当作结果输出。现在记录本次请求实际发送的提示词,回复去掉标点和空白后,相邻三个字组成的片段有八成以上出现在提示词里即视为复述,不插入(过短的回复不判断);流式上屏先缓冲正文开头 12 个字(翻译时为整段源文),确认不是复述再上屏。
  • 流式上屏(OmniDictationStream):去掉回复中任意位置的标记;以 { 或代码围栏开头的回复整段不上屏;已上屏的文本就是最终文本,与传统流式一致,不会触发剪贴板兜底。
  • 最终文本(finalize_omni_output):未上屏的回复按传统润色清洗;整段 JSON 外壳按明确列出的键名还原(final_text、translated_text、final_translation 等,与键的顺序无关),嵌在 JSON 字符串里的带标记回复也能还原;按格式输出的内容里只认典型外壳键,用户口述的 JSON 原样保留;JSON 形式的翻译同时保留源文;缺少译文时不插入任何内容,不把源文当译文。
  • 多模态语音唤醒时同样清理 polish_source 中的唤醒 / 结束指令。

兼容

  • 不包含:[omni] 多模态「验证」对 OpenAI gpt-audio-1.5 必然返回 400 #1305(多模态「验证」)另提 PR;问答、选区语音、Less Computer 等其它多模态调用不变。
  • 对现有用户 / 本地环境 / 构建流程的影响:只影响多模态模式的听写。Gemini 专用转写不变;其它模型的多模态听写都会使用新的提示词和标记格式;完全没有语音的录音不再发出请求。未使用多模态模式的用户不受影响。
  • 已知限制(未在本 PR 处理):
    • 纯音频模型(gpt-audio-*)在多模态模式下的纯文本功能(打字问答、历史重新润色)仍会被接口拒绝;
    • 翻译现在同时生成源文和译文,长录音的输出约为原来的两倍;
    • Less Computer / 选区语音第一段(complete_omni_instruction_from_wav)未加同样的格式约定;
    • 传统模式「润色 + 翻译」仍带输出语言偏好那一行。

测试计划

  • 命令:cargo test --locked -p openless-core;cargo clippy --locked -p openless-core --all-targets -- -D warnings

  • 结果:全部通过(1,664 个测试,其中库测试 1,428 个)。新增测试覆盖提示词组成、语音门限、流式过滤(标记跨 delta 切分、结束标记、JSON / 代码围栏开头、翻译只上屏译文)、最终文本整理与 JSON 外壳解包,以及 5 个端到端测试:4 个对接本地 SSE 服务(JSON 回复、标记回复、翻译、翻译的 JSON 回复),1 个确认没有语音时不发请求。

  • 真实模型对照(gpt-audio-1.5,同一批 5 段合成中文语音及静音 / 底噪,经 SharedOmniDictationEngine 完整链路):

    场景 修复前(beta 85b243ab) 修复后
    轻度润色 / 清晰结构 15/15 被包成 JSON 0/10 被包裹,10/10 流式上屏且与最终文本一致
    开启翻译(English) 9/9 被包裹,0/9 输出英文 0/10 被包裹,10/10 只输出英文,源文全部保留
    静音 / 底噪(含开启翻译) 未测 12/12 不调用模型、不插入内容
    「嗯」「啊」、哼声、呼气(含开启翻译,25 段) 未测 0 次插入提示词或示例句(24 段不插入,1 段为「嗯,这个,这个,这个。」)
    短词「好」「可以」 未测 2/2 正确

    审查后的修改(8e90167d)没有改动提示词;改动后经同一链路重跑:轻度润色 7/7(含两个短词)流式上屏且与最终文本一致,翻译 5/5 只输出英文、源文全部保留。

  • 本地打包实测(macOS,第一个提交 cce70eec):多模态听写输出纯文本;开启翻译只插入英文译文,源文写入历史记录。审查后的修改尚未重新打包实测。

  • 代码审查:按审查意见修正了标记位置、流式与最终文本不一致、JSON 外壳解包、翻译缺译文、静音等问题。

  • 证据路径:对照脚本与原始结果在本地,未提交。

审查后的修改

第二个提交 8e90167d 处理了 Codex 的 4 条意见(门限过严、JSON 翻译缺译文时误用源文、键名后缀匹配误伤口述 JSON、JSON 翻译丢失源文),并修复本地实测发现的问题:没有词句的声音会让模型复述提示词。各条意见下已逐条回复。

🤖 Generated with Claude Code

多模态听写原先直接复用文本润色的 system prompt:安全约定描述的是请求里
并不存在的 `<raw_transcript>` 信封,user 消息只有音频,传统模式中的
「只输出整理后的文本正文」也被丢掉。OpenAI gpt-audio-1.5 因此把每次回复
都包成自拟的 JSON(`{"final_text": …}`、`{"raw_transcript": …}` 等),
并随流式上屏逐字打进前台 app。翻译只是在「最终输出语言偏好:简体中文」
之后追加的一行,多数时候被忽略。

- 安全约定改为描述录音;音频旁附上文字指令,system prompt 以标记格式收尾
  (无翻译为 `[[OPENLESS_TEXT]]`,翻译复用传统模式的源文 / 译文标记,并按
  Auto 处理输出语言偏好)。
- 录音里没有语音(沿用静音自动停止的电平规则)时不调用模型,否则模型会把
  文字指令当作口述内容整理,或编造内容。
- 流式上屏去掉标记,以 JSON 或代码围栏开头的回复先不上屏;已上屏的文本即
  最终文本,与传统流式一致。
- 未上屏的回复按传统润色清洗,并还原整段 JSON 外壳(只认已知字段);翻译
  格式缺少译文时不插入任何内容,不把源文当译文。
- 多模态语音唤醒时同样清理 `polish_source` 中的唤醒 / 结束指令。

真实 gpt-audio-1.5,同一批合成语音(修复前 → 修复后):
- 润色:15/15 被包成 JSON → 0/10,全部流式上屏
- 翻译为 English:0/9 输出英文 → 10/10,源文全部保留
- 静音 / 底噪:不再调用模型,不插入任何内容

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-31T07:21:18.290711Z
We couldn’t complete this review. Comment @codex review to try again.
8e90167 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cce70eec72

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +2498 to +2500
if !is_live
&& !transcription_only
&& !crate::silence_auto_stop::pcm_contains_speech(&pcm)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let short utterances reach the multimodal provider

On batch multimodal sessions, pcm_contains_speech only returns true after three 100-ms chunks exceed the threshold. Therefore every clip of 200 ms or less—and longer clips containing only a short or quiet spoken token—takes this branch and is reported as "no speech" without invoking the provider, even though it can contain valid single-word dictation. This hard gate should not reject audio solely because it cannot satisfy that duration/level heuristic.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(8e90167d):门限改为 10 ms 一块、连续 3 块(约 30 ms),与静音自动停止按录音回调计算电平的粒度一致,短词不再被拒,只拦截几乎无声的录音。

另外,真实使用中「嗯」「啊」、呼气这类能通过门限却没有词句的声音,会让模型复述提示词;同一提交加了复述检测:回复与本次请求的提示词高度重合时不插入。实测 gpt-audio-1.5:「好」「可以」正常输出;25 段无词句的声音 0 次插入提示词。

prefer_translation
.then(|| field(ENVELOPE_TRANSLATION_KEYS))
.flatten()
.or_else(|| field(ENVELOPE_TEXT_KEYS))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require a target before accepting a translation envelope

When translation is active and the model returns JSON such as {"raw_transcript":"你好"} without a translated field, prefer_translation finds no translation and this fallback accepts raw_transcript from the text keys. finalize_omni_output then treats that source value as successful output, so untranslated text is inserted despite the translation request; a missing-target envelope should yield empty/failure rather than use a source-only field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(8e90167d):JSON 形式的翻译回复改用专门的解析,只从译文字段取结果;没有译文字段时听写以失败结束、不插入任何内容,不会把 raw_transcript 等源文字段当作译文。

Comment on lines +261 to +263
let key = key.trim_matches(['[', ']']).to_ascii_lowercase();
key == *wanted || key.ends_with(&format!("_{wanted}"))
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match only explicit JSON envelope keys

In a marked dictation whose spoken content is JSON such as {"display_text":"hello"}, strict cleanup reaches this predicate with lenient=false, but display_text matches the wanted text key through the suffix test. The cleaner then replaces the dictated JSON with hello, defeating the strict path's purpose of preserving user JSON; limit suffix aliases to explicit supported keys (for example final_translation) instead of every *_text or *_final key.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(8e90167d):去掉后缀匹配,只认明确列出的键名(含 final_translation);按要求格式输出的内容里只认 final_text、translated_text 这类典型的外壳键,{"display_text": …}、{"text": …} 等口述的 JSON 原样保留,已补测试。

Comment on lines +2986 to +2987
if let Some(text) = unwrap_json_text_envelope(&cleaned, translation_active, true) {
return (strip_omni_markers(&text), None);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve source text from JSON translation envelopes

For a translation reply such as {"final_text":"你好","translated_text":"Hello"}, the helper returns only Hello and this immediate return forces the source to None. finish consequently uses Hello as raw_text and stores no polish_source, so history loses the original Chinese source (and cannot reuse it in later context), unlike the marker-form response; parse and retain the source field alongside the selected translation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(8e90167d):JSON 形式的翻译回复现在同时取出源文,写入 polish_source 和 raw_text,与标记格式的回复一致。

真实使用中,录音只有「嗯」「啊」、呼气等没有词句的声音时,能量门限会放行,
gpt-audio-1.5 则把音频旁的文字指令或风格模板里的示例句当作结果输出,
直接插入前台 app。

- 记录本次请求实际发送的提示词;回复中字母与数字的三字片段有八成以上出现在
  提示词里(不计标点、空白,过短的回复不判断)即视为复述,不插入任何内容。
  流式上屏先缓冲正文开头 12 个字(翻译时为整段源文),确认不是复述再上屏。
- 语音门限改为 10 ms 一块,与静音自动停止的实际粒度一致,短词不再被拒。
- JSON 外壳只认明确的键名,不再做后缀匹配;按格式输出的内容里只认典型的
  外壳键,口述的 JSON 原样保留。
- 翻译回复被包成 JSON 时保留源文;没有译文字段时不插入任何内容,不把源文
  当译文;嵌在 JSON 字符串里的带标记回复也能还原。

真实 gpt-audio-1.5(经完整听写引擎):
- 「嗯」「啊」、哼声、呼气等 25 段:0 次插入提示词或示例句
- 正常语音、短词、翻译共 12 段:全部正确,且流式上屏

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant