diff --git a/apps/mofa-asr/src/screen/mod.rs b/apps/mofa-asr/src/screen/mod.rs
index 8a97d507..827e8a72 100644
--- a/apps/mofa-asr/src/screen/mod.rs
+++ b/apps/mofa-asr/src/screen/mod.rs
@@ -262,7 +262,13 @@ impl Widget for MoFaASRScreen {
if self.paraformer_chat_controller.is_none() {
let controller = ChatController::new_arc();
{
- let mut guard = controller.lock().expect("ChatController mutex poisoned");
+ let mut guard = match controller.lock() {
+ Ok(g) => g,
+ Err(poisoned) => {
+ ::log::warn!("ChatController mutex poisoned; recovering inner state");
+ poisoned.into_inner()
+ }
+ };
guard.dangerous_state_mut().bots.push(Bot {
id: BotId::new("asr"),
name: "Paraformer".to_string(),
@@ -270,7 +276,13 @@ impl Widget for MoFaASRScreen {
capabilities: BotCapabilities::new(),
});
}
- self.paraformer_chat_controller = Some(controller.clone());
+ self.paraformer_chatmatch controller.lock() {
+ Ok(g) => g,
+ Err(poisoned) => {
+ ::log::warn!("ChatController mutex poisoned; recovering inner state");
+ poisoned.into_inner()
+ }
+ }
self.view.messages(ids!(paraformer_messages)).write().chat_controller = Some(controller);
}
if self.qwen3_chat_controller.is_none() {
@@ -423,7 +435,13 @@ impl MoFaASRScreen {
};
let count = {
- let mut guard = controller.lock().expect("ChatController mutex poisoned");
+ let mut guard = match controller.lock() {
+ Ok(g) => g,
+ Err(poisoned) => {
+ ::log::warn!("ChatController mutex poisoned; recovering inner state");
+ poisoned.into_inner()
+ }
+ };
let state = guard.dangerous_state_mut();
state.messages.clear();
for msg in messages {
@@ -465,7 +483,14 @@ impl MoFaASRScreen {
fn handle_start(&mut self, cx: &mut Cx) {
// Clear per-engine chat controllers
if let Some(ref controller) = self.paraformer_chat_controller {
- controller.lock().expect("ChatController mutex poisoned").dangerous_state_mut().messages.clear();
+ let mut guard = match controller.lock() {
+ Ok(g) => g,
+ Err(poisoned) => {
+ ::log::warn!("ChatController mutex poisoned; recovering inner state");
+ poisoned.into_inner()
+ }
+ };
+ guard.dangerous_state_mut().messages.clear();
}
if let Some(ref controller) = self.qwen3_chat_controller {
controller.lock().expect("ChatController mutex poisoned").dangerous_state_mut().messages.clear();
diff --git a/doc/CHECKLIST.md b/doc/CHECKLIST.md
index 1f8760bb..793c273e 100644
--- a/doc/CHECKLIST.md
+++ b/doc/CHECKLIST.md
@@ -543,7 +543,7 @@ pub enum TabId {
- [x] Simplified code: `contains(&TabId::Profile)` instead of `iter().any(|t| t == "profile")`
**Benefits**:
-- Compile-time checking prevents typos like `"profiel"` or `"setings"`
+- Compile-time checking prevents typos like `"profiel"` or `"settgs"`
- IDE autocomplete works with enum variants
- Exhaustive match ensures all cases handled
- `Copy` trait allows efficient passing without `.clone()` or `.to_string()`
diff --git a/mofa-dora-bridge/src/parser.rs b/mofa-dora-bridge/src/parser.rs
index 271a469e..d5f5f976 100644
--- a/mofa-dora-bridge/src/parser.rs
+++ b/mofa-dora-bridge/src/parser.rs
@@ -430,7 +430,8 @@ nodes:
tts_log: tts/log
"#;
- let parsed = DataflowParser::parse_string(yaml, PathBuf::from("test.yml")).unwrap();
+ let parsed = DataflowParser::parse_string(yaml, PathBuf::from("test.yml"))
+ .expect("DataflowParser failed to parse a valid test YAML; check parser changes");
assert_eq!(parsed.mofa_nodes.len(), 2);
assert_eq!(parsed.mofa_nodes[0].id, "mofa-audio-player");
diff --git a/node-hub/dora-funasr-nano-mlx/src/main.rs b/node-hub/dora-funasr-nano-mlx/src/main.rs
index 462302f8..775befac 100644
--- a/node-hub/dora-funasr-nano-mlx/src/main.rs
+++ b/node-hub/dora-funasr-nano-mlx/src/main.rs
@@ -109,7 +109,14 @@ fn main() -> Result<()> {
}
}
- let engine = engine.as_mut().unwrap();
+ let engine = match engine.as_mut() {
+ Some(e) => e,
+ None => {
+ log::error!("Engine unexpectedly missing after attempted initialization");
+ send_log(&mut node, "ERROR", "Engine not available")?;
+ continue;
+ }
+ };
// Extract metadata
let question_id = metadata
diff --git a/node-hub/dora-gpt-sovits-mlx/src/ssml.rs b/node-hub/dora-gpt-sovits-mlx/src/ssml.rs
index 0c70d72a..8c56539f 100644
--- a/node-hub/dora-gpt-sovits-mlx/src/ssml.rs
+++ b/node-hub/dora-gpt-sovits-mlx/src/ssml.rs
@@ -354,7 +354,8 @@ mod tests {
#[test]
fn test_plain_text() {
- let result = parse_ssml("hello world").unwrap();
+ let result = parse_ssml("hello world")
+ .expect("parse_ssml should parse simple content");
assert_eq!(result, vec![SsmlSegment::Text {
text: "hello world".to_string(),
speed: 1.0,
diff --git a/node-hub/dora-primespeech/dora_primespeech/moyoyo_tts/text/english.py b/node-hub/dora-primespeech/dora_primespeech/moyoyo_tts/text/english.py
index 57f12fc2..4713b131 100644
--- a/node-hub/dora-primespeech/dora_primespeech/moyoyo_tts/text/english.py
+++ b/node-hub/dora-primespeech/dora_primespeech/moyoyo_tts/text/english.py
@@ -219,11 +219,20 @@ def get_namedict():
def text_normalize(text):
- # todo: eng text normalize
# 适配中文及 g2p_en 标点
+ if not text:
+ return ""
+
+ # ensure string
+ text = str(text)
+
+ # punctuation compatibility (smart quotes, dashes, ellipsis, CJK punctuation)
rep_map = {
"[;::,;]": ",",
- '["’]': "'",
+ "[\\u2018\\u2019`‘’]": "'",
+ "[\\u201c\\u201d\\u00ab\\u00bb\\u201e\\\"]": '"',
+ "\\u2026": "...",
+ "[\\u2013\\u2014]": "-",
"。": ".",
"!": "!",
"?": "?",
@@ -233,16 +242,36 @@ def text_normalize(text):
# 来自 g2p_en 文本格式化处理
# 增加大写兼容
- text = unicode(text)
- text = normalize_numbers(text)
- text = ''.join(char for char in unicodedata.normalize('NFD', text)
- if unicodedata.category(char) != 'Mn') # Strip accents
- text = re.sub("[^ A-Za-z'.,?!\-]", "", text)
- text = re.sub(r"(?i)i\.e\.", "that is", text)
- text = re.sub(r"(?i)e\.g\.", "for example", text)
+ try:
+ text = normalize_numbers(text)
+ except Exception:
+ pass
+
+ # strip accents
+ text = "".join(
+ ch for ch in unicodedata.normalize('NFD', text)
+ if unicodedata.category(ch) != 'Mn'
+ )
+
+ # keep letters, digits, quotes/apostrophes, basic punctuation and hyphen
+ text = re.sub(r"[^ 0-9a-z'\".,?!\-]", "", text)
+
+ # expand abbreviations (word boundaries, case-insensitive)
+ text = re.sub(r"\bi\.e\.\b", "that is", text, flags=re.IGNORECASE)
+ text = re.sub(r"\be\.g\.\b", "for example", text, flags=re.IGNORECASE)
# 避免重复标点引起的参考泄露
- text = replace_consecutive_punctuation(text)
+ try:
+ text = replace_consecutive_punctuation(text)
+ except Exception:
+ text = re.sub(r"([.,?!\-])\1+", r"\1", text)
+
+ # normalize whitespace
+ text = re.sub(r"\s+", " ", text).strip()
+
+ # ensure terminal punctuation for TTS stability
+ if text and text[-1] not in ".?!":
+ text += "."
return text