Skip to content

Commit 55cbd02

Browse files
committed
feat(asr): REQ-265 端点参数档案化骨架(asr-params.json,默认零变更)
1 parent 9a9678a commit 55cbd02

5 files changed

Lines changed: 112 additions & 21 deletions

File tree

‎app/src-tauri/src/live_session_lifecycle.rs‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,9 @@ fn run_session(
346346
let epoch = Instant::now();
347347

348348
// 1) 流式 ASR(SenseVoice 重打分接离线引擎池;M5 热词经共享词表注入)
349-
// ADR-012 F3-1:rule3 最长句 env 可覆盖(默认 8s——5s 过短致句中硬切)
350-
let rule3_secs = std::env::var("ENTROPY_ASR_RULE3_SECS")
351-
.ok()
352-
.and_then(|v| v.parse::<f32>().ok())
353-
.unwrap_or(8.0);
354-
let asr_config = StreamingAsrConfig { rule3_min_utterance_secs: rule3_secs };
349+
// v0.20.1 REQ-265:端点配置档案化(asr-params.json + 遗留 env 覆盖;
350+
// 默认值即原常量零行为变更——与 run_prepared 同口径防漂移)
351+
let asr_config = StreamingAsrConfig::from_env();
355352
let asr_engine = match StreamingAsrEngine::load(
356353
&params.streaming_models,
357354
&asr_config,

‎app/src-tauri/src/live_session_prepare.rs‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,9 @@ impl PreparedSession {
102102

103103
/// 预备线程入口:加载引擎 → 置 Ready → park 等 Start/Cancel/TTL。
104104
pub fn run_prepared(rx: mpsc::Receiver<PrepareMsg>, env: PrepareEnv, status: Arc<Mutex<PrepareStatus>>) {
105-
// 与 run_session 同口径的 rule3 配置(env 可覆盖,防两处漂移)
106-
let rule3_secs = std::env::var("ENTROPY_ASR_RULE3_SECS")
107-
.ok()
108-
.and_then(|v| v.parse::<f32>().ok())
109-
.unwrap_or(8.0);
110-
let asr_config = StreamingAsrConfig { rule3_min_utterance_secs: rule3_secs };
105+
// 与 run_session 同口径的端点配置(v0.20.1 REQ-265:档案 asr-params.json +
106+
// 遗留 env 覆盖,防两处漂移;默认值即原常量零变更)
107+
let asr_config = StreamingAsrConfig::from_env();
111108
let engine = match StreamingAsrEngine::load(
112109
&env.streaming_models,
113110
&asr_config,

‎app/src-tauri/src/streaming_asr.rs‎

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,83 @@ pub struct StreamingAsrModels {
4040
pub tokens: String,
4141
}
4242

43-
/// 流式引擎配置(ADR-012 F3-1:rule3 可配置)。
44-
#[derive(Debug, Clone, Copy)]
43+
/// 流式引擎配置(ADR-012 F3-1:rule3 可配置;v0.20.1 REQ-265:rule1/2 一并入档——
44+
/// 默认值即原常量,零行为变更;档案化来源:`asr-params.json`(可选,经
45+
/// ENTROPY_ASR_PARAMS_JSON 指定)+ 遗留 env ENTROPY_ASR_RULE3_SECS 覆盖)。
46+
#[derive(Debug, Clone, PartialEq)]
4547
pub struct StreamingAsrConfig {
46-
/// rule3 最长句强制断句(秒;默认 8s——5s 过短致句中硬切,20s 过长)
48+
/// 端点 rule1:尾静音断句秒数(默认 2.4——sherpa-onnx 惯例)。
49+
pub rule1_min_trailing_silence: f32,
50+
/// 端点 rule2:短停顿断句秒数(默认 1.2)。
51+
pub rule2_min_trailing_silence: f32,
52+
/// rule3 最长句强制断句(秒;默认 8s——5s 过短致句中硬切,20s 过长)。
4753
pub rule3_min_utterance_secs: f32,
4854
}
4955

5056
impl Default for StreamingAsrConfig {
5157
fn default() -> Self {
52-
Self { rule3_min_utterance_secs: 8.0 }
58+
Self {
59+
rule1_min_trailing_silence: 2.4,
60+
rule2_min_trailing_silence: 1.2,
61+
rule3_min_utterance_secs: 8.0,
62+
}
63+
}
64+
}
65+
66+
impl StreamingAsrConfig {
67+
/// 从 asr-params.json(可选)加载:缺失/损坏 → 默认 + 提示(不阻断启动,
68+
/// 同 audio-preproc.json 先例);字段可缺省。
69+
///
70+
/// @ai-context: JSON 形态 `{"rule1S": 2.4, "rule2S": 1.2, "rule3S": 8.0}`——
71+
/// 字段名沿用 harness 参数注入面的短名,标定结论可直接落档。
72+
pub fn load_from_file(path: &std::path::Path) -> Self {
73+
let mut cfg = Self::default();
74+
if let Ok(raw) = std::fs::read_to_string(path) {
75+
match serde_json::from_str::<AsrParamsFile>(&raw) {
76+
Ok(f) => {
77+
if let Some(v) = f.rule1_s {
78+
cfg.rule1_min_trailing_silence = v;
79+
}
80+
if let Some(v) = f.rule2_s {
81+
cfg.rule2_min_trailing_silence = v;
82+
}
83+
if let Some(v) = f.rule3_s {
84+
cfg.rule3_min_utterance_secs = v;
85+
}
86+
}
87+
Err(e) => eprintln!("[AsrParams] asr-params.json 解析失败,使用默认: {}", e),
88+
}
89+
}
90+
cfg
91+
}
92+
93+
/// 档案 + 遗留 env 覆盖(生产装配入口:文件优先、env 最高优先级保持旧行为)。
94+
pub fn from_env() -> Self {
95+
let mut cfg = Self::default();
96+
if let Ok(p) = std::env::var("ENTROPY_ASR_PARAMS_JSON") {
97+
cfg = Self::load_from_file(std::path::Path::new(&p));
98+
}
99+
if let Ok(v) = std::env::var("ENTROPY_ASR_RULE3_SECS") {
100+
if let Ok(s) = v.parse::<f32>() {
101+
cfg.rule3_min_utterance_secs = s;
102+
}
103+
}
104+
cfg
53105
}
54106
}
55107

108+
/// asr-params.json 的 serde 形态(字段可缺省)。
109+
#[derive(Debug, Clone, serde::Deserialize)]
110+
#[serde(rename_all = "camelCase")]
111+
struct AsrParamsFile {
112+
#[serde(default)]
113+
rule1_s: Option<f32>,
114+
#[serde(default)]
115+
rule2_s: Option<f32>,
116+
#[serde(default)]
117+
rule3_s: Option<f32>,
118+
}
119+
56120
/// 流式 ASR 事件(文本事件,时间戳由编排层标注)。
57121
#[derive(Debug, Clone, PartialEq)]
58122
pub enum StreamingAsrEvent {
@@ -127,10 +191,10 @@ impl StreamingAsrEngine {
127191
// 带 graph 的流,断言 abort(exit 0xffffffff,用户真机日志实证 Decode:101);
128192
// 无热词时同样兼容(beam 搜索对普通流无副作用,识别质量相当或更优)。
129193
recognizer_config.decoding_method = Some("modified_beam_search".into());
130-
// 端点规则:尾静音 2.4s / 1.2s 断句(sherpa-onnx 默认),
131-
// rule3 强制断句 = 可配置(ADR-012 F3-1:默认 8s,env 可覆盖)
132-
recognizer_config.rule1_min_trailing_silence = 2.4;
133-
recognizer_config.rule2_min_trailing_silence = 1.2;
194+
// 端点规则:rule1/2/3 来自配置(默认 2.4/1.2/8.0 即原常量,零行为变更;
195+
// 档案/覆盖来源见 StreamingAsrConfig::from_env/load_from_file)
196+
recognizer_config.rule1_min_trailing_silence = config.rule1_min_trailing_silence;
197+
recognizer_config.rule2_min_trailing_silence = config.rule2_min_trailing_silence;
134198
recognizer_config.rule3_min_utterance_length = config.rule3_min_utterance_secs;
135199

136200
let recognizer = OnlineRecognizer::create(&recognizer_config)

‎app/src-tauri/src/streaming_asr_tests.rs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,36 @@ fn token_chars_load_single_char_columns_only() {
426426
assert!(set.contains(&'a') && set.contains(&'b') && set.contains(&'焦') && set.contains(&'▁'));
427427
assert_eq!(set.len(), 4);
428428
}
429+
430+
// ── REQ-265(v0.20.1):端点参数档案化(asr-params.json)──
431+
432+
#[test]
433+
fn params_file_overrides_and_partial_fields_ok() {
434+
// Arrange:临时 asr-params.json——只给 rule3(rule1/2 缺省走默认)
435+
let raw = r#"{"rule3S": 6.5}"#;
436+
let path = std::env::temp_dir().join(format!("asr_params_{}.json", std::process::id()));
437+
std::fs::write(&path, raw).expect("写临时 params");
438+
// Act
439+
let cfg = StreamingAsrConfig::load_from_file(&path);
440+
let _ = std::fs::remove_file(&path);
441+
// Assert:覆盖生效;缺省字段保持默认(默认值即原常量,零行为变更)
442+
assert_eq!(cfg.rule3_min_utterance_secs, 6.5);
443+
assert_eq!(cfg.rule1_min_trailing_silence, 2.4);
444+
assert_eq!(cfg.rule2_min_trailing_silence, 1.2);
445+
}
446+
447+
#[test]
448+
fn params_file_bad_or_missing_falls_back_defaults() {
449+
// Arrange:损坏 JSON 与缺失文件
450+
let path = std::env::temp_dir().join(format!("asr_params_bad_{}.json", std::process::id()));
451+
std::fs::write(&path, "{not json").expect("写坏 params");
452+
// Act
453+
let bad = StreamingAsrConfig::load_from_file(&path);
454+
let missing = StreamingAsrConfig::load_from_file(
455+
std::path::Path::new(&format!("{}.nope", path.to_string_lossy())),
456+
);
457+
let _ = std::fs::remove_file(&path);
458+
// Assert:默认兜底(不阻断启动,同 audio-preproc.json 先例)
459+
assert_eq!(bad, StreamingAsrConfig::default());
460+
assert_eq!(missing, StreamingAsrConfig::default());
461+
}

‎app/src-tauri/src/title_rules.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub fn title_hotword_candidates(raw: &str) -> Vec<String> {
4949
if tok.chars().all(|c| c.is_ascii_digit()) {
5050
continue; // 纯数字(年份/计数)无术语价值
5151
}
52-
if BRAND_SUFFIXES.iter().any(|b| tok == *b) {
52+
if BRAND_SUFFIXES.contains(&tok) {
5353
continue; // 平台品牌名不入词表
5454
}
5555
if !out.iter().any(|t| t == tok) {

0 commit comments

Comments
 (0)