Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4158,11 +4158,35 @@ mod tests {
}

#[test]
fn build_polish_translate_prompt_contains_markers_and_target() {
let p = build_polish_translate_system_prompt("日本語");
assert!(p.contains(POLISH_TRANSLATE_SRC_MARKER));
assert!(p.contains(POLISH_TRANSLATE_TGT_MARKER));
assert!(p.contains("日本語"));
fn translation_prompt_inherits_active_style_and_preserves_structure() {
let style_prompt = "# STYLE_PACK_976\n按主题整理为编号列表。\n\n{{HOTWORDS}}";
let combined = build_polish_translate_system_prompt(style_prompt, "English");
let (system_prompt, _) = crate::polish::compose_polish_prompts(
"原始转写",
PolishMode::Structured,
&["OpenLess".to_string()],
&combined,
&["简体中文".to_string(), "English".to_string()],
ChineseScriptPreference::Auto,
crate::types::OutputLanguagePreference::Auto,
Some("GitHub"),
Some("已有上下文<|OPENLESS_CURSOR|>"),
true,
);

assert!(system_prompt.contains("STYLE_PACK_976"));
assert!(system_prompt.contains("OpenLess"));
assert!(!system_prompt.contains("{{HOTWORDS}}"));
assert!(system_prompt.contains("English"));
assert!(system_prompt.contains(POLISH_TRANSLATE_SRC_MARKER));
assert!(system_prompt.contains(POLISH_TRANSLATE_TGT_MARKER));
assert!(system_prompt.contains("# ASR 纠错"));
assert!(system_prompt.contains("Token"));
assert!(!system_prompt.contains("只输出最终英文译文"));
assert!(!system_prompt.contains("不得输出中文"));
assert!(system_prompt.contains("列表、编号、段落和 Markdown 结构"));
assert!(system_prompt.contains("<cursor_context>"));
assert!(system_prompt.contains("# 多轮上下文使用规则"));
}

#[tokio::test]
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src-tauri/src/coordinator/dictation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4546,6 +4546,7 @@ pub(super) async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
&translation_target,
mode,
&hotword_strs,
&style_system_prompt,
&working_languages,
chinese_script_preference,
output_language_preference,
Expand Down
40 changes: 26 additions & 14 deletions openless-all/app/src-tauri/src/coordinator/polish_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,23 +331,34 @@ pub(super) async fn translate_text(
pub(super) const POLISH_TRANSLATE_SRC_MARKER: &str = "[[OPENLESS_POLISHED_SOURCE]]";
pub(super) const POLISH_TRANSLATE_TGT_MARKER: &str = "[[OPENLESS_TRANSLATION]]";

/// 合成"先润色源文、再翻译"的系统提示词:在原翻译 prompt 之上追加"额外输出润色后源文"
/// 与严格两段格式(覆盖原 prompt 末尾的"只输出译文")。译文仍是要插入用户光标的主产物,
/// 故完整保留原翻译规则;润色后的源文只作对话上下文用,轻量清理即可。
pub(super) fn build_polish_translate_system_prompt(target_language: &str) -> String {
let base = crate::polish::prompts::translate_system_prompt(target_language);
/// 合成"按当前风格润色源文、再翻译"的系统提示词。当前风格包决定源文的结构与语气,
/// 翻译规则负责把该结果忠实转换为目标语言;末尾的严格两段格式覆盖两套 prompt 各自的
/// "只输出正文"约束。译文用于插入,风格化源文只写入历史供后续上下文复用。
pub(super) fn build_polish_translate_system_prompt(
style_system_prompt: &str,
target_language: &str,
) -> String {
let translation_rules = crate::polish::prompts::translate_system_prompt_rules(target_language);
format!(
"{base}\n\n\
# 额外输出:润色后的源文(仅用于对话上下文,不展示给用户)\n\
在译文之前,先把上面的原始转写**按它本来的语言**润色一遍:去掉口癖(嗯 / 那个 / um)、\
补必要标点、纠正明显的识别错误,但**不翻译、不改写风格、不增删意思**。\n\n\
# 输出格式(覆盖上面\u{201C}只输出译文\u{201D}的说明,严格遵守)\n\
"# 任务(按当前风格润色并翻译)\n\
先完整执行下方的当前风格包规则,把原始 ASR 转写整理为同语言的风格化源文;\
再把该风格化源文翻译成\u{300C}{lang}\u{300D}。翻译对象是风格化源文,不是原始转写。\n\n\
# 当前风格包规则\n\
{style}\n\n\
# 翻译规则\n\
{translation_rules}\n\n\
# 两阶段约束\n\
- 风格包决定内容的组织方式、语气和信息密度;翻译不得把它还原成普通连续段落。\n\
- 译文必须保留风格化源文的列表、编号、段落和 Markdown 结构,并忠实保留原意。\n\
- 风格化源文保持原语言;最终译文只使用\u{300C}{lang}\u{300D}表达需要翻译的正文。\n\n\
# 输出格式(优先级最高,覆盖上面所有\u{201C}只输出正文\u{201D}的说明)\n\
严格按下面两段输出,两个标记必须原样出现、各占一行,标记之外不要有任何多余文字:\n\
{src}\n\
这里放润色后的源文,保持原语言)\n\
这里放按当前风格包完整润色后的源文,保持原语言)\n\
{tgt}\n\
(这里放翻译成\u{300C}{lang}\u{300D}的译文)",
base = base,
(这里放保留相同风格与结构的\u{300C}{lang}\u{300D}译文)",
style = style_system_prompt.trim(),
translation_rules = translation_rules,
src = POLISH_TRANSLATE_SRC_MARKER,
tgt = POLISH_TRANSLATE_TGT_MARKER,
lang = target_language,
Expand Down Expand Up @@ -386,6 +397,7 @@ pub(super) async fn polish_and_translate_or_passthrough(
target_language: &str,
mode: PolishMode,
hotwords: &[String],
style_system_prompt: &str,
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
Expand All @@ -397,7 +409,7 @@ pub(super) async fn polish_and_translate_or_passthrough(
llm_elapsed_ms: &mut Option<u64>,
multimodal: bool,
) -> (String, Option<String>, Option<String>) {
let system_prompt = build_polish_translate_system_prompt(target_language);
let system_prompt = build_polish_translate_system_prompt(style_system_prompt, target_language);
match polish_text(
&raw.text,
mode,
Expand Down
72 changes: 48 additions & 24 deletions openless-all/app/src-tauri/src/polish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2155,28 +2155,53 @@ pub mod prompts {
/// 此 prompt 之上还有 working_languages_premise 拼出的"# 上下文"前提。
///
/// target_language == "English"(含 "美式英文" / "英文" / "english" 等别名)时整段切到
/// EN_TRANSLATE_SYSTEM_PROMPT —— 不再走通用 base,避免通用规则与 EN 专属的「ASR 纠错优先
/// EN_TRANSLATE_SYSTEM_RULES —— 不再走通用 base,避免通用规则与 EN 专属的「ASR 纠错优先
/// + 中→英技术词规范化」相互稀释。来源:社区「重写为英文」prompt,精简整合后整体注入。
pub fn translate_system_prompt(target_language: &str) -> String {
// issue #609 F-02:翻译路径与 polish 路径对齐——在系统提示末尾追加对抗式注入防御措辞。
// 本函数是所有翻译路径(OpenAI 兼容 / Gemini 的 compose_translate_prompts、Codex
// translate_to、润色+翻译合一的 build_polish_translate_system_prompt)写给模型的唯一
// base,把防御嵌在这里令每个调用方自动覆盖,杜绝调用点遗漏。LLM 不是安全边界,纵深防御。
// translate_to)写给模型的唯一 base,把防御嵌在这里令每个调用方自动覆盖,杜绝调用点遗漏。
// LLM 不是安全边界,纵深防御。
let base = translate_system_prompt_base(target_language);
format!("{}\n\n{}", base, polish_injection_defense())
}

/// 可嵌入其它工作流的翻译规则,不包含单段翻译的输出格式约束。
///
/// 润色+翻译流程需要同时输出原语言风格化源文和目标语言译文;复用
/// translate_system_prompt 会把“只输出译文 / 不得输出中文”等单段输出规则一并带入,
/// 与两段格式冲突。因此这里只复用 ASR 纠错、术语和忠实翻译规则。
pub fn translate_system_prompt_rules(target_language: &str) -> String {
translate_system_prompt_rules_base(target_language)
}

fn translate_system_prompt_base(target_language: &str) -> String {
let rules = translate_system_prompt_rules_base(target_language);
if is_english_target(target_language) {
return EN_TRANSLATE_SYSTEM_PROMPT.to_string();
return format!(
"{rules}\n\n{output}",
output = EN_TRANSLATE_OUTPUT_INSTRUCTIONS
);
}
format!(
"# 任务(翻译输出)\n\
把下面收到的一段语音转写翻译成 \u{300C}{lang}\u{300D}。\n\
这是用户对着语音输入工具说的话——他正在某个 app 的输入框前,\
转译结果会直接被插入到光标位置。\n\
\n\
# 翻译规则\n\
转译结果会直接被插入到光标位置。\n\n\
{rules}\n\n\
{output}",
lang = target_language,
rules = rules,
output = COMMON_TRANSLATE_OUTPUT_INSTRUCTIONS,
)
}

fn translate_system_prompt_rules_base(target_language: &str) -> String {
if is_english_target(target_language) {
return EN_TRANSLATE_SYSTEM_RULES.to_string();
}
format!(
"# 翻译规则\n\
## 必须保留原文(不要翻译)\n\
- 人名、地名、品牌名(OpenAI、Tauri、字节跳动、张三 等)。\n\
- 代码标识符、技术术语(useState、async/await、HTTP、Rust crate 名 等)。\n\
Expand All @@ -2196,15 +2221,15 @@ pub mod prompts {
## 边界 case\n\
- 转写非常短(一两个字)也照译,\u{4E0D}因为短就硬补内容。\n\
- 转写是命令式(\"加个空格 / 删除最后一行\")时,照原意翻译,\u{4E0D}改成陈述句。\n\
- 转写全是 fillers(\"嗯嗯啊那个\")时,输出空字符串。\n\
\n\
# 输出\n\
只输出翻译后的正文,\u{4E0D}带 \u{300C}翻译:\u{300D}\u{300C}译文:\u{300D}\u{300C}Translation:\u{300D}之类前缀,\
\u{4E0D}加引号、\u{4E0D}加 markdown 围栏。",
lang = target_language
- 转写全是 fillers(\"嗯嗯啊那个\")时,输出空字符串。",
lang = target_language,
)
}

const COMMON_TRANSLATE_OUTPUT_INSTRUCTIONS: &str = "# 输出\n\
只输出翻译后的正文,\u{4E0D}带 \u{300C}翻译:\u{300D}\u{300C}译文:\u{300D}\u{300C}Translation:\u{300D}之类前缀,\
\u{4E0D}加引号、\u{4E0D}加 markdown 围栏。";

/// target_language 是否指向英语 —— 容忍用户在偏好里写 "English" / "english" / "美式英文" /
/// "英文" / "British English" 等几种写法。匹配松一点没坏处:误命中只会让模型走 EN 专属
/// prompt,对纯中文 / 日文等目标本来就不会被选中。
Expand All @@ -2227,7 +2252,7 @@ pub mod prompts {
/// - 比通用翻译 prompt 更窄、更强:ASR 纠错优先于逐字翻译;英文要求自然 idiomatic,
/// 不接受 Chinglish 直译。
/// - 来源:社区「重写为英文」prompt(imported.573e86a1bcf44dbb...),整合精简后注入。
const EN_TRANSLATE_SYSTEM_PROMPT: &str = "# 任务(中文转写 → 英文翻译)\n\
const EN_TRANSLATE_SYSTEM_RULES: &str = "# 任务(中文转写 → 英文翻译)\n\
你是一名中译英助手,专门处理语音识别(ASR)后的中文技术文本。\n\
用户的转写不是可靠原文:可能有错别字、同音字、近音字、断句缺失、术语误识别、\
英文术语被中文音译。**你的任务不是逐字翻译,而是先理解用户真实意图,纠正显然的识别错误,\
Expand All @@ -2240,7 +2265,6 @@ pub mod prompts {
3. 把中文音译还原为标准英文技术术语。\n\
4. 整理混乱、口语化或重复的表达。\n\
5. 在不改变用户真实意图的前提下,翻译成自然、专业的英文。\n\
6. **只输出最终英文译文**。\n\
\n\
# ASR 纠错(按置信度分级)\n\
- 高置信度(错误明显、正确写法唯一)→ 直接替换,不保留原词、不加说明。\n\
Expand Down Expand Up @@ -2287,13 +2311,13 @@ pub mod prompts {
\n\
# 禁止\n\
1. \u{4E0D}得逐字翻译明显错误的 ASR 文本。\n\
2. \u{4E0D}得输出中文(不要给出中文润色稿、对比表、原文回显)。\n\
3. \u{4E0D}得输出解释、修改说明、change log、思路过程。\n\
4. \u{4E0D}得为了流畅而删减重要信息,也\u{4E0D}得添加用户未表达过的新事实、链接、路径、字段、步骤。\n\
5. \u{4E0D}得改变用户真实意图。\n\
\n\
# 输出\n\
只输出最终英文译文。\u{4E0D}带 \u{300C}翻译:\u{300D}\u{300C}译文:\u{300D}\u{300C}Translation:\u{300D}\
2. \u{4E0D}得输出解释、修改说明、change log、思路过程。\n\
3. \u{4E0D}得为了流畅而删减重要信息,也\u{4E0D}得添加用户未表达过的新事实、链接、路径、字段、步骤。\n\
4. \u{4E0D}得改变用户真实意图。";

const EN_TRANSLATE_OUTPUT_INSTRUCTIONS: &str = "# 输出\n\
只输出最终英文译文。\u{4E0D}得输出中文(不要给出中文润色稿、对比表、原文回显)。\
\u{4E0D}带 \u{300C}翻译:\u{300D}\u{300C}译文:\u{300D}\u{300C}Translation:\u{300D}\
\u{4E4B}\u{7C7B}前缀,\u{4E0D}加引号、\u{4E0D}加 markdown 围栏、\u{4E0D}加代码 fence。";
}

Expand Down Expand Up @@ -3843,7 +3867,7 @@ mod tests {
#[test]
fn injection_defense_present_in_translate_system_prompt() {
// issue #609 F-02:翻译路径(EN 专用 / 通用 base)必须与 polish 路径一样带对抗式注入防御。
// 覆盖英文目标(走 EN_TRANSLATE_SYSTEM_PROMPT)与非英文目标(走通用 base)两条分支。
// 覆盖英文目标(走 EN_TRANSLATE_SYSTEM_RULES)与非英文目标(走通用 base)两条分支。
for target in ["English", "繁体中文", "日本語"] {
let p = prompts::translate_system_prompt(target);
assert!(
Expand Down Expand Up @@ -3932,7 +3956,7 @@ mod tests {

#[test]
fn translate_prompt_swaps_to_en_dedicated_when_target_is_english() {
// 英文目标:整段切到 EN_TRANSLATE_SYSTEM_PROMPT,不再带通用 base 的 \"# 任务(翻译输出)\" 标题。
// 英文目标:整段切到 EN_TRANSLATE_SYSTEM_RULES,不再带通用 base 的 \"# 任务(翻译输出)\" 标题。
let en = prompts::translate_system_prompt("English");
assert!(
en.contains("# 任务(中文转写 → 英文翻译)"),
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,11 @@ export const en: typeof zhCN = {
disabled: 'Disabled (Shift does nothing)',
sameAsWorking: 'The target matches your only working language, so translation cannot take effect — Shift will just run a normal polish. Pick a different target, or add another working language above.',
},
style: {
title: 'Translation style',
desc: 'Automatically inherits the active style pack from the Style page.',
unavailable: 'Unavailable',
},
save: {
workingFailed: 'Failed to save working languages. Please try again.',
targetFailed: 'Failed to save translation target. Please try again.',
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,11 @@ export const ja: typeof zhCN = {
disabled: '無効(Shift で翻訳を発動しない)',
sameAsWorking: 'ターゲット言語が唯一の作業言語と同じため、翻訳は発動しません(Shift を押しても通常の整文になります)。別のターゲットを選ぶか、上で作業言語を追加してください。',
},
style: {
title: '翻訳スタイル',
desc: '「スタイル」ページで現在有効なスタイルパックを自動的に引き継ぎます。',
unavailable: '取得できません',
},
save: {
workingFailed: '作業言語の保存に失敗しました。もう一度お試しください。',
targetFailed: '翻訳ターゲット言語の保存に失敗しました。もう一度お試しください。',
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,11 @@ export const ko: typeof zhCN = {
disabled: '비활성화 (Shift 로 번역 발동 안 함)',
sameAsWorking: '대상 언어가 유일한 작업 언어와 같아 번역이 실행되지 않습니다. Shift 를 눌러도 일반 정리로 처리됩니다. 다른 대상 언어를 고르거나 위에서 작업 언어를 추가하세요.',
},
style: {
title: '번역 스타일',
desc: '「스타일」 페이지에서 현재 활성화된 스타일 팩을 자동으로 사용합니다.',
unavailable: '사용할 수 없음',
},
save: {
workingFailed: '작업 언어 저장에 실패했습니다. 다시 시도하세요.',
targetFailed: '번역 대상 언어 저장에 실패했습니다. 다시 시도하세요.',
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,11 @@ export const zhCN = {
disabled: '不启用(Shift 按下不触发翻译)',
sameAsWorking: '目标语言与你唯一的工作语言相同,翻译不会生效:按 Shift 仍按普通润色处理。换一个目标语言,或在上方多勾选一个工作语言。',
},
style: {
title: '翻译风格',
desc: '自动继承「风格」页当前激活的风格包。',
unavailable: '暂不可用',
},
save: {
workingFailed: '工作语言保存失败,请重试。',
targetFailed: '翻译目标语言保存失败,请重试。',
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,11 @@ export const zhTW: typeof zhCN = {
disabled: '不啓用(Shift 按下不觸發翻譯)',
sameAsWorking: '目標語言與你唯一的工作語言相同,翻譯不會生效:按 Shift 仍按普通潤色處理。換一個目標語言,或在上方多勾選一個工作語言。',
},
style: {
title: '翻譯風格',
desc: '自動沿用「風格」頁目前啓用的風格包。',
unavailable: '暫時無法取得',
},
save: {
workingFailed: '工作語言保存失敗,請重試。',
targetFailed: '翻譯目標語言保存失敗,請重試。',
Expand Down
Loading
Loading