Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Timestamps are expressed in seconds, and adjacent segments are concatenated into
[0.48][S01]Welcome everyone[1.66][12.26][S02]The new transcription pipeline is ready for evaluation[13.81][14.36][S01]Great, include the diarization results in the report[18.76]
```

When a backend returns an otherwise valid segment without a speaker token, the local `parse_transcript` helper assigns the default speaker `S01` instead of dropping the segment. This only affects local parsing; the raw model text is preserved unchanged. Pass `default_speaker=None` when strict rejection of unlabelled segments is required.

## Model Architecture

<p align="center">
Expand Down
2 changes: 2 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ MOSS-Transcribe-Diarize 面向会议、通话、播客、访谈、讲座和视
[0.48][S01]Welcome everyone[1.66][12.26][S02]The new transcription pipeline is ready for evaluation[13.81][14.36][S01]Great, include the diarization results in the report[18.76]
```

当后端返回的片段格式有效、但缺少说话人标签时,本地 `parse_transcript` 辅助函数会使用默认说话人 `S01`,而不是丢弃整段内容。此行为只影响本地解析,原始模型文本会保持不变;如果需要严格拒绝缺少标签的片段,可传入 `default_speaker=None`。

## 模型架构

<p align="center">
Expand Down
22 changes: 21 additions & 1 deletion moss_transcribe_diarize/transcript_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class TranscriptStreamParser:

[start][Sxx]text[end]

If the model omits the speaker token, ``default_speaker`` is used so that
otherwise valid transcript text is not discarded. Pass ``None`` to keep
strict parsing and reject unlabelled segments.

The parser deliberately avoids regular expressions. It scans characters
once, keeps only the active token/text buffers, and emits a segment after
an end timestamp is confirmed by the next segment start or by ``close()``.
Expand All @@ -36,9 +40,20 @@ class TranscriptStreamParser:
_READ_END = 5
_AFTER_END = 6

def __init__(self, *, strip_text: bool = True, skip_empty: bool = True):
def __init__(
self,
*,
strip_text: bool = True,
skip_empty: bool = True,
default_speaker: str | None = "S01",
):
if default_speaker is not None and _parse_speaker(list(default_speaker)) is None:
raise TranscriptParseError(
f"default_speaker must look like [Sxx], got {default_speaker!r}"
)
self.strip_text = strip_text
self.skip_empty = skip_empty
self.default_speaker = default_speaker
self._state = self._SEEK_START
self._token: list[str] = []
self._text: list[str] = []
Expand Down Expand Up @@ -127,6 +142,11 @@ def _expect_speaker_open(self, ch: str) -> None:
if ch == "[":
self._token.clear()
self._state = self._READ_SPEAKER
elif not ch.isspace() and self.default_speaker is not None:
self._speaker = self.default_speaker
self._text.clear()
self._state = self._READ_TEXT
self._read_text(ch)
elif not ch.isspace():
self.reset()

Expand Down
14 changes: 14 additions & 0 deletions tests/test_subtitle_postprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ def test_builds_subtitle_segments_from_transcript(self):
self.assertEqual([segment.text for segment in segments], ["你好", "开始"])
self.assertEqual(segments[0].id, "seg_0001")

def test_recovers_unlabelled_single_speaker_segments(self):
segments = subtitle_segments_from_transcript(
"[0.0]連續單一說話者內容[2.0][2.0]下一段[3.0]",
postprocess=False,
)

self.assertEqual(
[(segment.start, segment.end, segment.speaker, segment.text) for segment in segments],
[
(0.0, 2.0, "S01", "連續單一說話者內容"),
(2.0, 3.0, "S01", "下一段"),
],
)

def test_can_build_raw_subtitle_segments_without_postprocess(self):
segments = subtitle_segments_from_transcript(
"[0][S01]短[0.4][0.2][S01]重叠但保留[0.8]",
Expand Down
30 changes: 30 additions & 0 deletions tests/test_transcript_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import unittest

from moss_transcribe_diarize.transcript_parser import (
TranscriptParseError,
TranscriptSegment,
TranscriptStreamParser,
iter_transcript_segments,
Expand Down Expand Up @@ -58,6 +59,35 @@ def test_numeric_brackets_inside_text_are_preserved(self):
[TranscriptSegment(0.0, 4.0, "S01", "第[2024]年,编号[001]继续")],
)

def test_missing_speaker_uses_default_speaker(self):
text = "[0.0]單一說話者片段[2.0][2.0]下一段[3.0]"

self.assertEqual(
parse_transcript(text),
[
TranscriptSegment(0.0, 2.0, "S01", "單一說話者片段"),
TranscriptSegment(2.0, 3.0, "S01", "下一段"),
],
)

def test_missing_speaker_can_be_parsed_from_arbitrary_chunks(self):
chunks = ["[0.0]hel", "lo[1.0]", "[1.0]world[2.0]"]

self.assertEqual(
list(iter_transcript_segments(chunks)),
[
TranscriptSegment(0.0, 1.0, "S01", "hello"),
TranscriptSegment(1.0, 2.0, "S01", "world"),
],
)

def test_strict_mode_keeps_unlabelled_segments_ignored(self):
self.assertEqual(parse_transcript("[0]unlabelled[1]", default_speaker=None), [])

def test_default_speaker_must_be_valid(self):
with self.assertRaises(TranscriptParseError):
TranscriptStreamParser(default_speaker="speaker-1")

def test_noise_before_first_segment_is_ignored(self):
text = "noise [bad][0.1][S01]hello[0.9]"

Expand Down