Repository navigation
Qwen3-TTS PR 3: sentence chunking and error status for /v1/audio/speech #300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
9dd6e98
api: sentence splitter for long text-to-speech inputs
merceod 6d1e734
api: per-adapter sentence-chunking thresholds for /v1/audio/speech
merceod 474ef23
api: synthesize long speech inputs as ordered sentence chunks
merceod d99c95a
test: sentence chunking on /v1/audio/speech
merceod 45eaef1
api: streaming speech surfaces an up-front engine error as its HTTP s…
merceod 20d5de1
test: streaming speech returns the engine error status
merceod d5cb7f4
Merge remote-tracking branch 'origin/main' into engine/speech-sentenc…
NSagan271 0260eb5
respond to PR comments
NSagan271 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| """Sentence chunking for ``/v1/audio/speech``. | ||
|
|
||
| Long inputs are split into sentence groups that are synthesized as separate | ||
| requests and played back in order. This keeps every autoregressive | ||
| text-to-speech request inside the length its model was trained for, lets the | ||
| engine batch the pieces of one long input like independent requests, and | ||
| starts the second piece while the first is still streaming (see | ||
| ``serving_speech``). Splitting is purely textual and model-agnostic. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
|
|
||
| # One sentence: a lazy body up to a Latin terminator (with any closing quotes | ||
| # or brackets) that precedes whitespace or the end, a CJK terminator, a | ||
| # paragraph break, or the end of the text. | ||
| _SENTENCE = re.compile( | ||
| r""".+?(?: | ||
| [.!?;]["'”’)\]]*(?=\s|\Z) | ||
| | [。!?;] | ||
| | (?=\n[ \t]*\n) | ||
| | \Z | ||
| )""", | ||
| re.VERBOSE | re.DOTALL, | ||
| ) | ||
| # Soft break points inside an over-long sentence, most preferred first. | ||
| _SOFT_BREAKS = ( | ||
| re.compile(r"(?<=[,;:,;:])\s*"), | ||
| re.compile(r"\s+"), | ||
| ) | ||
|
|
||
|
|
||
| def _hard_wrap(sentence: str, max_chars: int) -> list[str]: | ||
| """Split one over-long sentence at clause boundaries, then at spaces.""" | ||
| pieces = [sentence] | ||
| for pattern in _SOFT_BREAKS: | ||
| wrapped: list[str] = [] | ||
| for piece in pieces: | ||
| if len(piece) <= max_chars: | ||
| wrapped.append(piece) | ||
| continue | ||
| current = "" | ||
| for part in (p.strip() for p in pattern.split(piece)): | ||
| if not part: | ||
| continue | ||
| candidate = f"{current} {part}" if current else part | ||
| if current and len(candidate) > max_chars: | ||
| wrapped.append(current) | ||
| current = part | ||
| else: | ||
| current = candidate | ||
| if current: | ||
| wrapped.append(current) | ||
| pieces = wrapped | ||
| return pieces | ||
|
|
||
|
|
||
| def split_sentences(text: str, max_chars: int = 400, min_chars: int = 24) -> list[str]: | ||
| """Group ``text`` into sentence chunks of at most ``max_chars`` characters. | ||
|
|
||
| Sentences are never cut unless one alone exceeds ``max_chars`` (then it is | ||
| wrapped at clause boundaries, or spaces as a last resort). A trailing | ||
| fragment shorter than ``min_chars`` is merged into its predecessor so the | ||
| model is not asked to voice a lone "Okay." Returns ``[text]`` when nothing | ||
| needs splitting and ``[]`` for blank input. | ||
| """ | ||
| if max_chars <= 0: | ||
| raise ValueError("max_chars must be positive") | ||
| text = text.strip() | ||
| if not text: | ||
| return [] | ||
| if len(text) <= max_chars: | ||
| return [text] | ||
|
|
||
| sentences: list[str] = [] | ||
| for match in _SENTENCE.finditer(text): | ||
| piece = " ".join(match.group(0).split()) | ||
| if not piece: | ||
| continue | ||
| sentences.extend(_hard_wrap(piece, max_chars) if len(piece) > max_chars else [piece]) | ||
|
|
||
| chunks: list[str] = [] | ||
| current = "" | ||
| for sentence in sentences: | ||
| if current and len(current) + 1 + len(sentence) > max_chars: | ||
| chunks.append(current) | ||
| current = sentence | ||
| else: | ||
| current = f"{current} {sentence}".strip() | ||
| if current: | ||
| chunks.append(current) | ||
|
|
||
| if ( | ||
| len(chunks) > 1 | ||
| and len(chunks[-1]) < min_chars | ||
| and len(chunks[-2]) + 1 + len(chunks[-1]) <= max_chars * 5 // 4 | ||
| ): | ||
| chunks[-2:] = [f"{chunks[-2]} {chunks[-1]}"] | ||
| return chunks |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any client can send
sentence_chunking: trueand nothing boundsinput, so a 1.2 MB input becomes over 3,000 engine requests where the unchunked request stops at the model's max length. I think the override needs a cap.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a
speech_chunk_max_piecesto the adapter, which bounds the number of chunks and raises a 400 if it's exceeded.