change sentence cut method

This commit is contained in:
2026-08-18 20:34:58 +08:00
parent a36e776343
commit f5c6717186
7 changed files with 440 additions and 29 deletions

View File

@@ -31,6 +31,14 @@ class TranscriptionSegment:
end_seconds: float
text: str
speaker: Optional[str] = None
words: Optional[List["WordTimestamp"]] = None
@dataclass(frozen=True)
class WordTimestamp:
start_seconds: float
end_seconds: float
text: str
@dataclass(frozen=True)
@@ -114,12 +122,23 @@ class MossTranscriber:
if not chunk_segments:
chunk_segments = _parse_compact_segments(str(payload.get("text") or "").strip())
for segment in chunk_segments:
words = None
if segment.words:
words = [
WordTimestamp(
start_seconds=word.start_seconds + start_seconds,
end_seconds=word.end_seconds + start_seconds,
text=word.text,
)
for word in segment.words
]
segments.append(
TranscriptionSegment(
start_seconds=segment.start_seconds + start_seconds,
end_seconds=segment.end_seconds + start_seconds,
text=segment.text,
speaker=segment.speaker,
words=words,
)
)
segments.sort(key=lambda item: (item.start_seconds, item.end_seconds))
@@ -144,17 +163,27 @@ class MossTranscriber:
data["language"] = language
try:
response = httpx.post(
self.endpoint,
data=data,
files={"file": (filename, audio_file, content_type)},
timeout=httpx.Timeout(self.timeout_seconds, connect=30),
word_payload = dict(data)
word_payload["timestamp_granularities[]"] = "word"
response = self._post_once(
word_payload, audio_file, filename, content_type
)
if response.status_code in (400, 422):
audio_file.seek(0)
response = self._post_once(data, audio_file, filename, content_type)
response.raise_for_status()
return response.json()
except (httpx.HTTPError, ValueError) as exc:
raise RuntimeError(f"MOSS transcription request failed: {exc}") from exc
def _post_once(self, data: dict, audio_file, filename: str, content_type: str):
return httpx.post(
self.endpoint,
data=data,
files={"file": (filename, audio_file, content_type)},
timeout=httpx.Timeout(self.timeout_seconds, connect=30),
)
def _parse_json_segments(
raw_segments, compression_limit: Optional[float] = None
@@ -186,11 +215,126 @@ def _parse_json_segments(
end_seconds=end,
text=text,
speaker=str(speaker) if speaker is not None else None,
words=_parse_word_timestamps(item.get("words")),
)
)
return segments
def _parse_word_timestamps(raw_words) -> Optional[List[WordTimestamp]]:
if not isinstance(raw_words, list):
return None
words = []
for item in raw_words:
if not isinstance(item, dict):
continue
try:
start = float(item["start"])
end = float(item["end"])
except (KeyError, TypeError, ValueError):
continue
text = str(item.get("word") or "").strip()
if text and end > start:
words.append(WordTimestamp(start_seconds=start, end_seconds=end, text=text))
return words or None
_PERIOD_BOUNDARY = re.compile(r"\.+")
def split_segment_by_periods(
segment: TranscriptionSegment,
) -> List[TranscriptionSegment]:
"""Split one whisper segment into sentences at periods (".")."""
text = segment.text.strip()
if not text:
return []
pieces: List[str] = []
cursor = 0
for match in _PERIOD_BOUNDARY.finditer(text):
piece = text[cursor:match.end()].strip()
if piece.strip(".").strip():
pieces.append(piece)
cursor = match.end()
tail = text[cursor:].strip()
if tail:
pieces.append(tail)
if len(pieces) <= 1:
return [segment] if pieces else []
ends = _sentence_end_seconds(segment, pieces)
sub_segments: List[TranscriptionSegment] = []
start = segment.start_seconds
for piece, end in zip(pieces, ends):
if end > start:
sub_segments.append(
TranscriptionSegment(
start_seconds=start,
end_seconds=end,
text=piece,
speaker=segment.speaker,
)
)
start = end
return sub_segments
def _sentence_end_seconds(
segment: TranscriptionSegment, pieces: List[str]
) -> List[float]:
lengths = [len(piece) for piece in pieces]
total = sum(lengths)
if segment.words and len(segment.words) >= len(pieces):
word_ends = _word_boundary_ends(segment, lengths)
if _valid_boundaries(segment, word_ends):
return word_ends + [segment.end_seconds]
return _proportional_ends(segment, lengths) + [segment.end_seconds]
def _word_boundary_ends(
segment: TranscriptionSegment, lengths: List[int]
) -> List[float]:
targets = [sum(lengths[:count]) for count in range(1, len(lengths))]
buffer = ""
word_indexes: List[int] = []
target_index = 0
for word_index, word in enumerate(segment.words):
buffer += (" " if buffer else "") + word.text
while target_index < len(targets) and len(buffer) >= targets[target_index]:
word_indexes.append(word_index)
target_index += 1
if len(word_indexes) != len(targets):
return []
return [segment.words[index].end_seconds for index in word_indexes]
def _valid_boundaries(
segment: TranscriptionSegment, ends: List[float]
) -> bool:
previous = segment.start_seconds
for end in ends:
if not segment.start_seconds < end < segment.end_seconds:
return False
if end <= previous:
return False
previous = end
return True
def _proportional_ends(
segment: TranscriptionSegment, lengths: List[int]
) -> List[float]:
total = sum(lengths)
ends = []
cumulative = 0
for length in lengths[:-1]:
cumulative += length
ends.append(
segment.start_seconds
+ (cumulative / total) * (segment.end_seconds - segment.start_seconds)
)
return ends
def _wav_duration_seconds(path: Path) -> Optional[float]:
try:
with wave.open(str(path), "rb") as wav: