Qxotic jinfer Text-to-Speech (TTS)
Introduction
jinfer is a pure-JVM inference engine from the Qxotic AI project.
JinferSpeechModel implements Spring AI’s TextToSpeechModel and StreamingTextToSpeechModel interfaces.
It synthesizes speech locally from a GGUF model inside your application’s JVM, without an API key or external service.
The jinfer Spring AI integration is an external module maintained by the Qxotic AI project, not part of the Spring AI repository.
Its artifacts are published to Maven Central under the com.qxotic group.
|
Prerequisites
-
The same Java 25, JVM flag and Spring Boot requirements as the jinfer chat model.
-
A speech model family on the classpath:
Model
Artifact
Companions
Inflect (Nano, Micro)
jinfer-inflect2lexicon, the pronunciation lexiconjinfer-kokorovoice, a voice file. Also requiresespeak-ngon thePATH.
Auto-configuration
Add the jinfer Spring Boot starter and a speech model family. Import both BOMs as shown in the jinfer chat model documentation.
<dependency>
<groupId>com.qxotic</groupId>
<artifactId>jinfer-spring-ai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.qxotic</groupId>
<artifactId>jinfer-inflect2</artifactId>
</dependency>
or, for Gradle:
dependencies {
implementation 'com.qxotic:jinfer-spring-ai-spring-boot-starter'
implementation 'com.qxotic:jinfer-inflect2'
}
Speech Properties
The JinferSpeechModel bean is created when spring.ai.jinfer.speech.model is set.
It does not use the spring.ai.model.audio.speech selection property.
Property |
Description |
Default |
spring.ai.jinfer.speech.model |
A local GGUF path or a model reference for a speech model. Setting it creates the bean. |
- |
spring.ai.jinfer.speech.companions.<capability> |
An auxiliary file for the model, given as a path or a model reference: |
- |
spring.ai.jinfer.speech.speed |
A playback speed multiplier. When unset, the model’s own pace is used. |
- |
spring.ai.jinfer.speech.max-input-chars |
Rejects input text longer than this many characters before synthesis starts. Synthesis cost grows with the input length, so an unbounded limit would let one request tie up the CPU. |
|
spring:
ai:
jinfer:
speech:
model: remixerdec/Inflect-Nano-v2-GGUF:Q8_0
companions:
lexicon: remixerdec/Inflect-Nano-v2-GGUF/lexicon.bin
Runtime Options
Requests use the portable TextToSpeechOptions.
Of those options, JinferSpeechModel supports only speed.
The voice, model and format options are rejected with an UnsupportedOperationException, rather than being silently ignored:
-
The voice is fixed by the loaded model. To use another voice, load a different model or voice companion.
-
Each instance is bound to the GGUF file it loaded.
-
The output format is fixed:
callreturns WAV, andstreamreturns raw PCM16.
TextToSpeechResponse response = textToSpeechModel.call(new TextToSpeechPrompt(
"Hello from local Java inference.",
TextToSpeechOptions.builder().speed(1.1).build()));
Output Format
-
callreturns a complete WAV file. -
streamemits successive chunks of headerless, little-endian PCM16 audio. Chunks are emitted in order, can contain silence, and do not necessarily line up with sentences. Cancelling the subscription stops the synthesis.
Every response carries the playback parameters as integer metadata under the JinferSpeechModel.SAMPLE_RATE (Hz) and JinferSpeechModel.CHANNELS keys.
byte[] wav = textToSpeechModel.call("Hello from local Java inference.");
Files.write(Path.of("hello.wav"), wav);
textToSpeechModel.stream(new TextToSpeechPrompt("Hello again."))
.doOnNext(response -> System.out.printf("%d PCM bytes at %s Hz%n",
response.getResult().getOutput().length,
response.getMetadata().get(JinferSpeechModel.SAMPLE_RATE)))
.blockLast();
In a WebFlux application, return the Flux from the handler.
Only block at an imperative boundary, such as the standalone example above, and never on an event-loop thread.
JinferSpeechModel is thread-safe. Concurrent requests use independent synthesis states and can run in parallel.
Manual Configuration
Depend on com.qxotic:jinfer-spring-ai and a speech model family, then build the model:
try (var speech = JinferSpeechModel.builder()
.model("remixerdec/Inflect-Nano-v2-GGUF:Q8_0")
.companion("lexicon", "remixerdec/Inflect-Nano-v2-GGUF/lexicon.bin")
.build()) {
Files.write(Path.of("hello.wav"), speech.call("Hello from local Java inference."));
}
Kokoro takes its voice as a companion in the same way:
try (var speech = JinferSpeechModel.builder()
.model("simonfxr/kokoro.cpp-GGUF:Q8_0")
.companion("voice", "simonfxr/kokoro.cpp-GGUF/voices/kokoro-voice-af_heart.gguf")
.build()) {
Files.write(Path.of("kokoro.wav"), speech.call("Hello from Kokoro."));
}
close() blocks until in-flight syntheses have finished, then frees the model weights.