diff --git a/sentence_api/.env.example b/sentence_api/.env.example index 851c5e8..02b8baa 100644 --- a/sentence_api/.env.example +++ b/sentence_api/.env.example @@ -14,3 +14,5 @@ MOSS_TRANSCRIBE_URL=http://127.0.0.1:8001/v1/audio/transcriptions MOSS_MODEL=OpenMOSS-Team/MOSS-Transcribe-Diarize MOSS_TIMEOUT_SECONDS=1800 MOSS_MAX_NEW_TOKENS=65536 +# 每句结束时间向后顺延的毫秒数,避免句子末尾发音被切掉;按需在 200-500 之间调整 +MOSS_END_PADDING_MS=300 diff --git a/sentence_api/DEPLOYMENT.md b/sentence_api/DEPLOYMENT.md index ae68726..38077cf 100644 --- a/sentence_api/DEPLOYMENT.md +++ b/sentence_api/DEPLOYMENT.md @@ -158,6 +158,8 @@ segment 会自动合并成同一句。若转写服务支持词级时间戳 (`timestamp_granularities[]=word`,Whisper/Speaches 支持),句子的结束时间用 标点所在单词的时间戳;不支持时自动按文字长度在 segment 内估算,请求失败会回退为 不带词级时间戳的普通请求。MOSS 不支持该参数时同样自动回退,不影响转写。 +每句结束时间默认再向后顺延 300ms(`MOSS_END_PADDING_MS`,可在 200–500 之间调整), +避免句子末尾发音被切掉;顺延不会越过下一句的开头。 ## 3. 部署 API diff --git a/sentence_api/config.py b/sentence_api/config.py index 6544424..120802d 100644 --- a/sentence_api/config.py +++ b/sentence_api/config.py @@ -24,6 +24,7 @@ class Settings: moss_model: str moss_timeout_seconds: float moss_max_new_tokens: int + moss_end_padding_ms: int pass_score: float @classmethod @@ -51,6 +52,7 @@ class Settings: ), moss_timeout_seconds=float(os.getenv("MOSS_TIMEOUT_SECONDS", "1800")), moss_max_new_tokens=int(os.getenv("MOSS_MAX_NEW_TOKENS", "65536")), + moss_end_padding_ms=int(os.getenv("MOSS_END_PADDING_MS", "300")), pass_score=float(os.getenv("ASSESSMENT_PASS_SCORE", "70")), ) diff --git a/sentence_api/processing.py b/sentence_api/processing.py index 7ebfe9c..3b83c38 100644 --- a/sentence_api/processing.py +++ b/sentence_api/processing.py @@ -47,6 +47,7 @@ class VideoProcessor: transcript=transcript, language=video.get("language"), audio_path=work_path, + end_padding_ms=self.settings.moss_end_padding_ms, ) if not document.sentences: raise RuntimeError("MOSS returned no timestamped speech segments.") @@ -103,13 +104,23 @@ def document_from_transcript( transcript: Transcript, language: Optional[str], audio_path: Path, + end_padding_ms: int = 300, ) -> SentenceBoundaryDocument: samples, sample_rate = decode_audio_mono(audio_path) sentences: List[SentenceBoundary] = [] previous_end = 0 - for sentence_segment in split_sentences_at_punctuation(transcript.segments): + sentence_segments = split_sentences_at_punctuation(transcript.segments) + for index, sentence_segment in enumerate(sentence_segments): start_ms = max(previous_end, int(round(sentence_segment.start_seconds * 1000))) - end_ms = min(duration_ms, int(round(sentence_segment.end_seconds * 1000))) + end_ms = min( + duration_ms, + int(round(sentence_segment.end_seconds * 1000)) + end_padding_ms, + ) + if index + 1 < len(sentence_segments): + next_start_ms = int( + round(sentence_segments[index + 1].start_seconds * 1000) + ) + end_ms = min(end_ms, next_start_ms) if not sentence_segment.text.strip() or end_ms <= start_ms: continue start_sample = max(0, int(start_ms / 1000 * sample_rate))