Skip to content

Add Python API examples for PocketTTS - #3088

Merged
csukuangfj merged 1 commit into
k2-fsa:masterfrom
csukuangfj:pocket-tts-python-examples
Jan 27, 2026
Merged

csukuangfj merged 1 commit into
k2-fsa:masterfrom
csukuangfj:pocket-tts-python-examples

Conversation

@csukuangfj

@csukuangfj csukuangfj commented Jan 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added Python example scripts for PocketTTS voice cloning, including real-time audio playback
    • Exposed GenerationConfig and OfflineTtsPocketModelConfig to the Python API
  • Bug Fixes

    • Improved error handling and validation for voice cloning configuration

✏️ Tip: You can customize this high-level summary in your review settings.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jan 27, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @csukuangfj, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces Python API examples for the PocketTTS module, allowing users to generate speech from text. It provides two distinct examples: one for basic audio generation and saving, and another for real-time audio playback as the speech is being synthesized. Additionally, the changes include necessary configuration updates and crucial input validation within the C++ backend to ensure stable operation of the PocketTTS functionality.

Highlights

  • New Python API Examples: Two new Python API examples for PocketTTS have been added, demonstrating both basic audio generation and real-time playback during synthesis.
  • Improved Robustness: Validation checks were introduced in the C++ backend for PocketTTS to ensure that reference audio parameters are valid, preventing potential crashes.
  • Configuration Updates: The .gitignore file was updated to include new PocketTTS model directories, and Python bindings were extended to expose GenerationConfig and OfflineTtsPocketModelConfig.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai

coderabbitai Bot commented Jan 27, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR introduces PocketTTS voice cloning support for sherpa-onnx by adding example scripts demonstrating TTS usage, enhancing the C++ implementation with voice embedding validation, and expanding Python API exposure to include GenerationConfig and OfflineTtsPocketModelConfig types.

Changes

Cohort / File(s) Summary
Build Configuration
.gitignore
Added 2 ignore patterns for pocket-tts-related build artifacts (sherpa-onnx-pocket-tts-int8 and sherpa-onnx-pocket-tts variants)
Python API Exposure
sherpa-onnx/python/csrc/offline-tts.cc, sherpa-onnx/python/sherpa\_onnx/__init__.py
Added Python bindings for GenerationConfig type and exposed GenerationConfig and OfflineTtsPocketModelConfig in public package exports
Core Implementation
sherpa-onnx/csrc/offline-tts-pocket-impl.h
Added validation checks in GetVoiceEmbedding for reference sample rate and audio validity; added early return in Generate if voice embedding is invalid
Example Scripts
python-api-examples/pocket-tts.py, python-api-examples/pocket-tts-play.py
Added two demonstration scripts: pocket-tts.py for basic TTS generation with timing metrics; pocket-tts-play.py for concurrent audio generation and real-time playback with callback-driven audio buffering and multi-threaded stream management

Sequence Diagram

sequenceDiagram
    participant Main as Main Thread
    participant Gen as TTS Generator<br/>(Callback)
    participant Buffer as Audio Buffer
    participant Stream as Audio Stream<br/>(Playback)

    Main->>Gen: Start generation with callback
    activate Gen
    Gen->>Buffer: generated_audio_callback(samples)
    activate Buffer
    Buffer-->>Buffer: Append samples to buffer
    Buffer-->>Main: Signal playback ready (started=True)
    deactivate Buffer
    
    Main->>Stream: Start playback thread
    activate Stream
    Stream->>Buffer: Request audio frames (play_audio_callback)
    Buffer-->>Stream: Return buffered samples
    Stream-->>Stream: Feed to OutputStream
    
    Gen->>Buffer: Continue: generated_audio_callback(samples)
    Buffer-->>Stream: Frames available
    Stream-->>Stream: Continue playback
    
    Gen->>Buffer: Final: generated_audio_callback(last samples)
    deactivate Gen
    Buffer-->>Stream: End of buffer signal
    Stream-->>Stream: Fill silence/close stream
    deactivate Stream
    
    Main->>Main: Wait for playback completion
    Main->>Main: Write generated.wav + log metrics
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A pocket-tts bloom, so merry and bright,
With callbacks and buffers that dance through the night,
Voices clone and play—oh what a delight!
Validations ensure all embeddings are right,
New bindings exposed, Python shines with its light! ✨

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds Python API examples for PocketTTS, including one for simple generation and another for streaming playback. The changes also include necessary C++ backend updates to support this, such as adding input validation for reference audio and exposing the GenerationConfig to Python.

My review focuses on the new Python example for streaming playback. I've identified a critical race condition in the audio playback callback that could lead to application crashes. I've provided a detailed explanation and a code suggestion to fix this by using a thread-safe approach. The rest of the changes look good and are well-implemented.

Comment on lines +128 to +158
def play_audio_callback(
outdata: np.ndarray, frames: int, time, status: sd.CallbackFlags
):
if killed or (started and buffer.empty() and stopped):
event.set()

# outdata is of shape (frames, num_channels)
if buffer.empty():
outdata.fill(0)
return

n = 0
while n < frames and not buffer.empty():
remaining = frames - n
k = buffer.queue[0].shape[0]

if remaining <= k:
outdata[n:, 0] = buffer.queue[0][:remaining]
buffer.queue[0] = buffer.queue[0][remaining:]
n = frames
if buffer.queue[0].shape[0] == 0:
buffer.get()

break

outdata[n : n + k, 0] = buffer.get()
n += k

if n < frames:
outdata[n:, 0] = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Accessing buffer.queue directly is not thread-safe. The queue.Queue is designed for thread-safe communication via its public methods (put, get, etc.), but its underlying queue attribute (a collections.deque) is not protected for direct access. This can lead to race conditions between the audio generation thread (calling put) and the audio playback thread (this callback), potentially causing an IndexError or other unpredictable behavior if the queue is modified by another thread concurrently.

A safer approach is to only use the public API of queue.Queue and manage any leftover audio samples from a chunk within the callback's state. This can be achieved by attaching a state variable to the callback function itself.

def play_audio_callback(
    outdata: np.ndarray, frames: int, time, status: sd.CallbackFlags
):
    if not hasattr(play_audio_callback, "leftover"):
        play_audio_callback.leftover = np.array([], dtype=np.float32)

    if killed or (
        started
        and buffer.empty()
        and stopped
        and not play_audio_callback.leftover.any()
    ):
        event.set()
        outdata.fill(0)
        return

    outdata.fill(0)

    num_copied = 0
    if play_audio_callback.leftover.any():
        n = min(frames, play_audio_callback.leftover.shape[0])
        outdata[:n, 0] = play_audio_callback.leftover[:n]
        play_audio_callback.leftover = play_audio_callback.leftover[n:]
        num_copied = n

    while num_copied < frames:
        try:
            samples = buffer.get_nowait()
        except queue.Empty:
            break

        needed = frames - num_copied
        k = min(needed, samples.shape[0])

        outdata[num_copied : num_copied + k, 0] = samples[:k]
        num_copied += k

        if k < samples.shape[0]:
            play_audio_callback.leftover = samples[k:]
            break  # outdata is full

@csukuangfj
csukuangfj merged commit 9609160 into k2-fsa:master Jan 27, 2026
20 of 27 checks passed
@csukuangfj
csukuangfj deleted the pocket-tts-python-examples branch January 27, 2026 02:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant