ソースを参照

Improved presentation

Efren Yevale Varela 1 ヶ月 前
親
コミット
9fc87b8ec7

+ 12 - 0
src/stt_daemon/assistant/triage.py

@@ -468,6 +468,18 @@ class TriageAnalyzer:
         cleaned = self._unwrap_tool_call(cleaned)
         if not cleaned:
             return None
+        # Some models emit escaped newlines (literal backslash-n) instead of
+        # real line breaks; unescape only when no real newline is present so
+        # genuine backslashes in clean markdown are never mangled.
+        if "\n" not in cleaned and ("\\n" in cleaned or "\\t" in cleaned):
+            cleaned = (
+                cleaned.replace("\\r\\n", "\n")
+                .replace("\\n", "\n")
+                .replace("\\t", "\t")
+                .strip()
+            )
+            if not cleaned:
+                return None
         limit = self._brief_max_chars()
         if len(cleaned) > limit:
             logger.warning("truncating brief from %d to %d chars", len(cleaned), limit)

+ 8 - 2
src/stt_daemon/pywhispercpp_transcriber.py

@@ -9,6 +9,7 @@ stays lazy inside the constructor.
 
 import logging
 import math
+import re
 
 import numpy as np
 
@@ -131,11 +132,16 @@ class PyWhisperCppTranscriber:
             no_speech_thold=self.no_speech_prob_max,
         )
 
-        # Collect accepted segments and track their confidence
+        # Collect accepted segments and track their confidence. Segments are
+        # joined with a space and whitespace is collapsed: whisper.cpp often
+        # omits the leading spaces faster-whisper carries, and bare
+        # concatenation glues words together ("mostmatter").
         accepted_segments = [
             seg for seg in segments if accept_segment(seg, self.avg_logprob_min)
         ]
-        text = "".join(seg.text for seg in accepted_segments).strip()
+        text = re.sub(
+            r"\s+", " ", " ".join(seg.text for seg in accepted_segments)
+        ).strip()
 
         # Sticky pinning logic (only when auto-detecting with sticky strategy)
         # pywhispercpp returns numpy scalars; coerce so no np.float32 leaks into

+ 8 - 2
src/stt_daemon/transcriber.py

@@ -1,6 +1,7 @@
 """Transcription using Faster-Whisper."""
 
 import logging
+import re
 from dataclasses import dataclass
 from typing import Protocol
 
@@ -130,13 +131,18 @@ class Transcriber:
             without_timestamps=True,
         )
 
-        # Collect accepted segments and track their confidence
+        # Collect accepted segments and track their confidence. Segments are
+        # joined with a space and whitespace is collapsed: faster-whisper
+        # usually carries leading spaces but whisper.cpp often does not, and
+        # bare concatenation glues words together ("mostmatter").
         accepted_segments = [
             seg
             for seg in segments
             if accept_segment(seg, self.no_speech_prob_max, self.avg_logprob_min)
         ]
-        text = "".join(seg.text for seg in accepted_segments).strip()
+        text = re.sub(
+            r"\s+", " ", " ".join(seg.text for seg in accepted_segments)
+        ).strip()
 
         # Sticky pinning logic (only when auto-detecting with sticky strategy)
         language_probability_to_report = info.language_probability

+ 9 - 0
tests/test_pywhispercpp_transcriber.py

@@ -204,6 +204,15 @@ def test_text_is_joined_and_stripped():
     assert t.transcribe(_audio()).text == "hola mundo"
 
 
+def test_text_joins_spaceless_segments_with_space():
+    t = _transcriber()
+    _FakeModel.last.segments_to_return = [
+        _Seg(text="most", probability=0.9),
+        _Seg(text="matter", probability=0.9),
+    ]
+    assert t.transcribe(_audio()).text == "most matter"
+
+
 # --- sticky strategy --------------------------------------------------------
 
 

+ 6 - 0
tests/test_transcriber.py

@@ -104,6 +104,12 @@ def test_transcribe_disables_timestamps(fake_model):
     assert kwargs["condition_on_previous_text"] is False
 
 
+def test_text_joins_spaceless_segments_with_space(fake_model):
+    t = Transcriber("small", device="cpu", compute_type="int8", language=None)
+    fake_model.last.segments_to_return = [_Seg(text="most"), _Seg(text="matter")]
+    assert t.transcribe(_audio()).text == "most matter"
+
+
 def test_auto_language_stays_none_for_bilingual(fake_model):
     """language=auto must reach the engine as None so es/en both keep working."""
     t = Transcriber("small", device="cpu", compute_type="int8", language=None)

+ 7 - 0
tests/test_triage.py

@@ -419,6 +419,13 @@ def test_parse_brief_strips_fences_and_rejects_empty(tmp_path):
     assert analyzer.parse_brief("```") is None
 
 
+def test_parse_brief_unescapes_literal_backslash_n(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    assert analyzer.parse_brief("## Title\\n\\nBody line") == "## Title\n\nBody line"
+    assert analyzer.parse_brief("a\\tb") == "a\tb"
+    assert analyzer.parse_brief("line1\nline2\\nline3") == "line1\nline2\\nline3"
+
+
 def test_parse_brief_truncates_not_rejects(tmp_path):
     analyzer = _analyzer(tmp_path, _score_questions())
     parsed = analyzer.parse_brief("x" * 5000)