From c660e192719be808c0e81d25aa1309b2397c2b4b Mon Sep 17 00:00:00 2001 From: David-Wu1119 <133224895+David-Wu1119@users.noreply.github.com> Date: Wed, 11 Nov 2026 00:03:11 -0500 Subject: [PATCH] fix(postprocess): keep a space between Latin VAD segments in rich transcription AutoModel joins the tagged SenseVoice text of each VAD segment with a single space, so a long English recording reaches rich_transcription_postprocess() as several <|lang|> blocks. The function strips every block and concatenates them with no separator, so "Hello world." and "How are you?" become "Hello world.How are you?" and the last word of one segment runs into the first word of the next. Insert a space between two blocks when both boundary characters belong to a space-separated script (ASCII or Hangul). Chinese and Japanese text and emoji boundaries are still joined without a space. The same change is applied to the funasr_onnx and funasr_torch copies so the three shipped postprocessors stay identical, and the event-segment test that pinned the glued "helloworld" output now expects "hello world". Co-Authored-By: Claude Opus 5.5 --- funasr/utils/postprocess_utils.py | 11 +++++++++- .../funasr_torch/utils/postprocess_utils.py | 11 +++++++++- .../funasr_onnx/utils/postprocess_utils.py | 11 +++++++++- .../test_rich_transcription_event_segments.py | 4 +++- tests/test_rich_transcription_postprocess.py | 21 +++++++++++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/funasr/utils/postprocess_utils.py b/funasr/utils/postprocess_utils.py index 6a3cd2336c..cd99e1fbbe 100644 --- a/funasr/utils/postprocess_utils.py +++ b/funasr/utils/postprocess_utils.py @@ -442,6 +442,11 @@ def format_str_v2(s): return s.strip() +def _is_spaced_char(ch): + """Latin and Hangul text separates words with spaces; CJK text does not.""" + return (ch.isascii() and not ch.isspace()) or "\uac00" <= ch <= "\ud7a3" + + def rich_transcription_postprocess(s): """Rich transcription postprocess. @@ -481,6 +486,10 @@ def get_event(s): cur_ent_event = get_event(s_list[i]) if get_emo(s_list[i]) != None and get_emo(s_list[i]) == get_emo(new_s): new_s = new_s[:-1] - new_s += s_list[i].strip().lstrip() + segment = s_list[i].strip() + # Each VAD segment is its own <|lang|> block; keep the word boundary between them. + if segment and _is_spaced_char(new_s[-1]) and _is_spaced_char(segment[0]): + new_s += " " + new_s += segment new_s = new_s.replace("The.", " ") return new_s.strip() diff --git a/runtime/python/libtorch/funasr_torch/utils/postprocess_utils.py b/runtime/python/libtorch/funasr_torch/utils/postprocess_utils.py index 3f8d9999e6..d9e1b06436 100644 --- a/runtime/python/libtorch/funasr_torch/utils/postprocess_utils.py +++ b/runtime/python/libtorch/funasr_torch/utils/postprocess_utils.py @@ -346,6 +346,11 @@ def format_str_v2(s): return s.strip() +def _is_spaced_char(ch): + """Latin and Hangul text separates words with spaces; CJK text does not.""" + return (ch.isascii() and not ch.isspace()) or "\uac00" <= ch <= "\ud7a3" + + def rich_transcription_postprocess(s): def get_emo(s): return s[-1] if s[-1] in emo_set else None @@ -370,6 +375,10 @@ def get_event(s): cur_ent_event = get_event(s_list[i]) if get_emo(s_list[i]) != None and get_emo(s_list[i]) == get_emo(new_s): new_s = new_s[:-1] - new_s += s_list[i].strip().lstrip() + segment = s_list[i].strip() + # Each VAD segment is its own <|lang|> block; keep the word boundary between them. + if segment and _is_spaced_char(new_s[-1]) and _is_spaced_char(segment[0]): + new_s += " " + new_s += segment new_s = new_s.replace("The.", " ") return new_s.strip() diff --git a/runtime/python/onnxruntime/funasr_onnx/utils/postprocess_utils.py b/runtime/python/onnxruntime/funasr_onnx/utils/postprocess_utils.py index 343ed383aa..84e220166c 100644 --- a/runtime/python/onnxruntime/funasr_onnx/utils/postprocess_utils.py +++ b/runtime/python/onnxruntime/funasr_onnx/utils/postprocess_utils.py @@ -400,6 +400,11 @@ def format_str_v2(s): return s.strip() +def _is_spaced_char(ch): + """Latin and Hangul text separates words with spaces; CJK text does not.""" + return (ch.isascii() and not ch.isspace()) or "\uac00" <= ch <= "\ud7a3" + + def rich_transcription_postprocess(s): def get_emo(s): return s[-1] if s[-1] in emo_set else None @@ -424,6 +429,10 @@ def get_event(s): cur_ent_event = get_event(s_list[i]) if get_emo(s_list[i]) != None and get_emo(s_list[i]) == get_emo(new_s): new_s = new_s[:-1] - new_s += s_list[i].strip().lstrip() + segment = s_list[i].strip() + # Each VAD segment is its own <|lang|> block; keep the word boundary between them. + if segment and _is_spaced_char(new_s[-1]) and _is_spaced_char(segment[0]): + new_s += " " + new_s += segment new_s = new_s.replace("The.", " ") return new_s.strip() diff --git a/tests/test_rich_transcription_event_segments.py b/tests/test_rich_transcription_event_segments.py index ed756abfd8..a565b5c3cd 100644 --- a/tests/test_rich_transcription_event_segments.py +++ b/tests/test_rich_transcription_event_segments.py @@ -34,7 +34,9 @@ def test_repeated_event_only_segments_keep_following_text(postprocess, event, fo result = postprocess.rich_transcription_postprocess(text) - assert result == postprocess.event_dict[event] + "hello" + following_text + assert result == postprocess.event_dict[event] + " ".join( + filter(None, ["hello", following_text]) + ) @pytest.mark.parametrize("event", ["<|Cough|>", "<|Sneeze|>", "<|Applause|>"]) diff --git a/tests/test_rich_transcription_postprocess.py b/tests/test_rich_transcription_postprocess.py index 8e07de09eb..2cfc708ad8 100644 --- a/tests/test_rich_transcription_postprocess.py +++ b/tests/test_rich_transcription_postprocess.py @@ -32,3 +32,24 @@ def test_cough_and_sneeze_events_render_distinct_emojis(path, event, emoji): text = module.rich_transcription_postprocess(f"<|en|><|NEUTRAL|>{event}<|woitn|>hello") assert text == f"{emoji}hello" + + +@pytest.mark.parametrize("path", POSTPROCESS_UTILS, ids=lambda path: path.parts[-3]) +@pytest.mark.parametrize( + "first, second, expected", + [ + ("Hello world.", "How are you?", "Hello world. How are you?"), + ("so I went to the", "store", "so I went to the store"), + ("안녕하세요.", "감사합니다.", "안녕하세요. 감사합니다."), + ("你好。", "再见。", "你好。再见。"), + ("你好。", "Hello.", "你好。Hello."), + ], +) +def test_vad_segments_keep_a_space_between_spaced_scripts(path, first, second, expected): + module = _load(path) + tags = "<|NEUTRAL|><|Speech|><|withitn|>" + + # AutoModel joins the tagged text of each VAD segment with a single space. + text = module.rich_transcription_postprocess(f"<|en|>{tags}{first} <|en|>{tags}{second}") + + assert text == expected