Supertonic is a Text-to-Speech (TTS) engine available across multiple platforms. This repository is for the Systemwide TTS implementation on Android specifically, supporting most/all ARM ABIs on Play Store and GitHub releases APKs. On F-Droid release currently only arm64-v8a is implemented.
Please note that functionality and bug fixes may vary slightly between the F-Droid and Play Store builds as there can be version difference as they get submitted and approved following different timetables.
- Stable Release: Available now on F-Droid
- Stable Release: Available on Google Play Store
Note: This repository currently tracks both versions of the application.
We welcome community contributions to support new languages or improve existing ones. The text-processing pipeline works as follows:
- Kotlin Text Normalizer: Expands shorthand notations (numbers, currency, percentages, ranges) into full spoken words (in the target language).
- JNI / Rust Chunker: Receives the normalized text and splits it into optimal sentences and audio chunks (maximum 300 characters, or 120 for CJK languages).
To add or improve support for a language, follow these steps:
Text normalization prevents the engine from reading symbols literally (e.g. pronouncing "3" as English "three" instead of Hindi "तीन").
- Path:
app/src/main/java/com/brahmadeo/supertonic/tts/utils/TextNormalizer.kt - Action: Add a routing check for your language prefix inside the
normalizefunction, and implement your language-specific normalization logic:
// Inside TextNormalizer.normalize()
if (lowerLang.startsWith("hi")) {
return normalizeHindi(processedText)
}// Example: Implementation of normalizeHindi
private fun normalizeHindi(text: String): String {
// 1. Convert native scripts digits to latin digits (e.g. ०-९ -> 0-9)
var normalized = convertDevanagariDigits(text)
// 2. Format ranges (e.g. "10-15" -> "10 से 15")
val rangePattern = Pattern.compile("\\b(\\d+)\\s*[-–—]\\s*(\\d+)\\b")
// Replace logic ...
// 3. Format currency (e.g. "₹500" -> "500 रुपये")
val currencyPattern = Pattern.compile("(?:\\bINR|₹)\\s*(\\d+(?:\\.\\d+)?)\\b")
// Replace logic ...
// 4. Convert remaining digits to words using NumberUtils
val numberPattern = Pattern.compile("\\b(\\d+(?:\\.\\d+)?)\\b")
// Replace each match using NumberUtils.convertHindi or NumberUtils.convertHindiDouble
return normalized
}- Path:
app/src/main/java/com/brahmadeo/supertonic/tts/utils/NumberUtils.kt - Action: Implement utility functions to convert integer and decimal values into spoken words:
// Example signature:
fun convertHindi(n: Long): String
fun convertHindiDouble(d: Double): StringThe JNI layer delegates text chunking to Rust, ensuring the display UI in PlaybackActivity matches the underlying audio chunks.
- Paths:
rust/src/lang/mod.rs(Language Normalizer Registry)rust/src/lang/<lang_code>.rs(Language normalizer implementation)rust/src/lang/configs/<lang_code>.json(JSON config for abbreviations)
- Action:
- Add your language configuration JSON containing punctuation splits, abbreviations, and rules.
- Implement the
LanguageNormalizertrait for your language:pub struct HindiNormalizer; impl LanguageNormalizer for HindiNormalizer { fn preprocess(&self, text: &str) -> String { ... } fn split_sentences(&self, text: &str) -> Vec<String> { ... } fn max_chunk_len(&self) -> usize { 300 } fn should_wrap_tags(&self) -> bool { false } }
- Register your normalizer in the
get_normalizerfactory function inrust/src/lang/mod.rs.
- Paths:
- Action:
- Declare the language display string in
strings.xml:<string name="lang_hindi">Hindi</string>
- Add the string ID and language code mapping to the
languagesmap inMainActivity.kt:R.string.lang_hindi to "hi"
- Declare the language display string in
Always write tests for the normalization expansions and chunking rules to prevent regressions.
- Kotlin Tests Path:
app/src/test/java/com/brahmadeo/supertonic/tts/utils/TextNormalizerTest.kt - Run Rust Tests:
cd rust cargo test
- Run Kotlin Tests:
.\gradlew.bat testDebugUnitTest
To easily share or modify pronunciation rules, you can import and export them as a JSON file.
The file must be a JSON array containing objects with the following fields:
term(orword): The source text or word pattern you wish to replace. (Required)replacement(orpronunciation/ipa): The target text to substitute. (Required)- Note:
replacementis a plain-text substitution applied before synthesis, not phonetic IPA notation.
- Note:
ignoreCase(optional, default:true): Boolean specifying whether matching is case-insensitive.isRegex(optional, default:false): Boolean specifying whether to treat the matching term as a regular expression pattern.
[
{
"term": "LLMs",
"replacement": "L L Ems",
"ignoreCase": true,
"isRegex": false
},
{
"word": "read",
"pronunciation": "red",
"ignoreCase": false
}
]- Supertonic - For creating lightweight, high-performance, and great-sounding TTS models optimized for edge compute.
- Readium Kotlin Toolkit (BSD 3-Clause) - For EPUB and PDF parsing, manifest structures, and book rendering navigation.
- PdfBox-Android by Tom Roush (Apache 2.0) - For parsing and extracting text from PDF documents.
- jsoup (MIT) - For cleaning, parsing, and normalising HTML content.
- ONNX Runtime (MIT) - For running cross-platform neural model inference on device.