Files
sherpa-onnx/java-api-examples/PocketTts.java
T
Fangjun Kuang 8dc6244e9d Refactor JNI (#3107)
This pull request significantly refactors the Java Native Interface (JNI) layer for the `sherpa-onnx` library, primarily enhancing the `OfflineTts` component. The changes introduce more robust error handling in various API constructors by validating native object initialization. Furthermore, the callback mechanism for Text-to-Speech operations has been streamlined, offering more convenient `java.util.function.Consumer` based interfaces in Java while simplifying the underlying C++ JNI implementation.

### Highlights

* **Improved Error Handling in Constructors**: Added `IllegalArgumentException` throws in constructors of several `sherpa-onnx` Java API classes (e.g., `AudioTagging`, `KeywordSpotter`, `OfflineTts`) if the native object creation (`newFromFile`) returns a null pointer (0). This makes the API more robust by failing fast on invalid configurations.
* **Streamlined Offline TTS Callbacks**: The `OfflineTts` class now provides new `generateWithCallback` and `generateWithConfigAndCallback` overloads that accept `java.util.function.Consumer<float[]>` for simpler, more idiomatic Java callback usage. This reduces boilerplate for users who only need to consume audio samples.
* **Simplified JNI Callback Implementation**: The native C++ code (`offline-tts.cc`) has been refactored to remove separate handling for `Consumer` and `Function` interfaces. All callbacks are now handled uniformly through the `OfflineTtsCallback` interface, simplifying the JNI bridge logic.
* **Removed Unused Imports and Code**: The `PocketTts.java` example file was cleaned up by removing unused `Consumer` and `Function` imports and simplifying lambda expressions, reflecting the changes in the `OfflineTts` API.
* **Added `getNumSpeakers()` to `OfflineTts`**: A new method `getNumSpeakers()` was added to the `OfflineTts` class, providing access to the number of speakers supported by the TTS engine.
2026-01-29 10:32:46 +08:00

133 lines
4.8 KiB
Java

// Copyright 2026 Xiaomi Corporation
// This file shows how to use a PocketTTS English model
// for voice cloning.
import com.k2fsa.sherpa.onnx.*;
import java.util.HashMap;
import java.util.Map;
public class PocketTts {
public static void main(String[] args) {
LibraryUtils.enableDebug();
// please visit
// https://k2-fsa.github.io/sherpa/onnx/tts/pocket.html
// to download model files
String lmFlow = "./sherpa-onnx-pocket-tts-int8-2026-01-26/lm_flow.int8.onnx";
String lmMain = "./sherpa-onnx-pocket-tts-int8-2026-01-26/lm_main.int8.onnx";
String encoder = "./sherpa-onnx-pocket-tts-int8-2026-01-26/encoder.onnx";
String decoder = "./sherpa-onnx-pocket-tts-int8-2026-01-26/decoder.int8.onnx";
String textConditioner = "./sherpa-onnx-pocket-tts-int8-2026-01-26/text_conditioner.onnx";
String vocabJson = "./sherpa-onnx-pocket-tts-int8-2026-01-26/vocab.json";
String tokenScoresJson = "./sherpa-onnx-pocket-tts-int8-2026-01-26/token_scores.json";
String text =
"Today as always, men fall into two groups: slaves and free men. Whoever does not have"
+ " two-thirds of his day for himself, is a slave, whatever he may be: a statesman, a"
+ " businessman, an official, or a scholar.";
OfflineTtsPocketModelConfig pocketModelConfig =
OfflineTtsPocketModelConfig.builder()
.setLmMain(lmMain)
.setLmFlow(lmFlow)
.setEncoder(encoder)
.setDecoder(decoder)
.setTextConditioner(textConditioner)
.setVocabJson(vocabJson)
.setTokenScoresJson(tokenScoresJson)
.build();
OfflineTtsModelConfig modelConfig =
OfflineTtsModelConfig.builder()
.setPocket(pocketModelConfig)
.setNumThreads(2)
.setDebug(true)
.build();
OfflineTtsConfig config = OfflineTtsConfig.builder().setModel(modelConfig).build();
OfflineTts tts = new OfflineTts(config);
String referenceAudioFilename = "./sherpa-onnx-pocket-tts-int8-2026-01-26/test_wavs/bria.wav";
WaveReader reader = new WaveReader(referenceAudioFilename);
GenerationConfig genConfig = new GenerationConfig();
genConfig.setReferenceAudio(reader.getSamples());
genConfig.setReferenceSampleRate(reader.getSampleRate());
genConfig.setNumSteps(5);
Map<String, String> extra = new HashMap<>();
extra.put("temperature", "0.7");
extra.put("chunk_size", "15");
genConfig.setExtra(extra);
long start = System.currentTimeMillis();
GeneratedAudio audio = null;
// You can choose one of the following callback style
// ---------------------------------------------------
// 1. Anonymous class implementing OfflineTtsCallback
// ---------------------------------------------------
if (true) {
audio =
tts.generateWithConfigAndCallback(
text,
genConfig,
new OfflineTtsCallback() {
@Override
public Integer invoke(float[] samples) {
// you can play the generated samples in a separate thread
System.out.println("callback got called with " + samples.length + " samples");
// 1 = continue, 0 = stop
return 1;
}
});
}
// -------------------------------
// 2. Lambda implementing OfflineTtsCallback
// -------------------------------
if (false) {
audio =
tts.generateWithConfigAndCallback(
text,
genConfig,
samples -> {
System.out.println("Lambda Integer callback: " + samples.length);
return 1; // continue
});
}
if (false) {
audio =
tts.generateWithConfigAndCallback(
text,
genConfig,
samples -> {
System.out.println("Consumer: " + samples.length);
// implicitly, it returns 1 internally
});
}
if (audio == null) {
System.err.println("No audio was generated. Please enable at least one callback branch.");
return;
}
long stop = System.currentTimeMillis();
float timeElapsedSeconds = (stop - start) / 1000.0f;
float audioDuration = audio.getSamples().length / (float) audio.getSampleRate();
float real_time_factor = timeElapsedSeconds / audioDuration;
String waveFilename = "pocket-tts-bria.wav";
audio.save(waveFilename);
System.out.printf("-- elapsed : %.3f seconds\n", timeElapsedSeconds);
System.out.printf("-- audio duration: %.3f seconds\n", audioDuration);
System.out.printf("-- real-time factor (RTF): %.3f\n", real_time_factor);
System.out.printf("-- text: %s\n", text);
System.out.printf("-- Saved to %s\n", waveFilename);
tts.release();
}
}