Add Mistral API TTS support with provider selection and API key configuration

- Add TTS provider selection (Gleez vs Mistral) in Settings
- Add Mistral API key configuration option in Settings
- Implement Mistral TTS API integration in MainActivity
- Add provider-specific voice options for both Gleez and Mistral
- Update voice selection UI to be provider-aware
- Add proper error handling for missing Mistral API key
- Maintain backward compatibility with existing Gleez TTS

Co-authored-by: jahruz67 <jahruz67@users.noreply.github.com>
This commit is contained in:
Vibe Nuage Agent
2026-08-01 22:42:19 +00:00
co-authored by jahruz67
parent 1cd8b82b72
commit df2333825d
3 changed files with 338 additions and 32 deletions
@@ -86,8 +86,9 @@ public class MainActivity extends Activity {
private static final int RANK_FEAST = 2;
private static final int RANK_SOLEMNITY = 3;
private static final String TTS_SPEECH_URL = "https://ttsapi.host2.gleeze.com/v1/audio/speech";
private static final String TTS_MODEL = "kokoro";
private static final String TTS_SPEECH_URL_GLEEZE = "https://ttsapi.host2.gleeze.com/v1/audio/speech";
private static final String TTS_MODEL_GLEEZE = "kokoro";
private static final String TTS_SPEECH_URL_MISTRAL = "https://api.mistral.ai/v1/text-to-speech";
private static final String READING_CACHE_DIR = "reading-cache";
private static final String PREFETCH_MONTH_PREFIX = "prefetch_month_";
private static final int NEXT_MONTH_PREFETCH_DAY = 26;
@@ -1490,7 +1491,16 @@ public class MainActivity extends Activity {
}
private File downloadTtsAudio(String text) throws Exception {
HttpURLConnection connection = (HttpURLConnection) new URL(TTS_SPEECH_URL).openConnection();
String provider = UpdateManager.getTtsProvider(this);
if (provider.equals(UpdateManager.TTS_PROVIDER_MISTRAL)) {
return downloadMistralTtsAudio(text);
} else {
return downloadGleezTtsAudio(text);
}
}
private File downloadGleezTtsAudio(String text) throws Exception {
HttpURLConnection connection = (HttpURLConnection) new URL(TTS_SPEECH_URL_GLEEZE).openConnection();
connection.setConnectTimeout(15000);
connection.setReadTimeout(120000);
connection.setRequestMethod("POST");
@@ -1500,8 +1510,8 @@ public class MainActivity extends Activity {
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
JSONObject body = new JSONObject();
body.put("model", TTS_MODEL);
body.put("voice", UpdateManager.getTtsVoice(this, isEnglish));
body.put("model", TTS_MODEL_GLEEZE);
body.put("voice", UpdateManager.getTtsVoice(this, isEnglish, UpdateManager.TTS_PROVIDER_GLEEZE));
body.put("input", text);
body.put("response_format", "mp3");
body.put("speed", 1.0);
@@ -1539,6 +1549,59 @@ public class MainActivity extends Activity {
return outputFile;
}
private File downloadMistralTtsAudio(String text) throws Exception {
String apiKey = UpdateManager.getMistralApiKey(this);
if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IOException(isEnglish ? "Mistral API key is not configured. Please set it in Settings." : "La clave API de Mistral no está configurada. Configúrela en Ajustes.");
}
HttpURLConnection connection = (HttpURLConnection) new URL(TTS_SPEECH_URL_MISTRAL).openConnection();
connection.setConnectTimeout(15000);
connection.setReadTimeout(120000);
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("User-Agent", "BibliaDiaria/1.0 Android");
connection.setRequestProperty("Accept", "audio/mpeg");
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
connection.setRequestProperty("Authorization", "Bearer " + apiKey);
JSONObject body = new JSONObject();
body.put("text", text);
body.put("voice", UpdateManager.getTtsVoice(this, isEnglish, UpdateManager.TTS_PROVIDER_MISTRAL));
byte[] payload = body.toString().getBytes(StandardCharsets.UTF_8);
connection.setFixedLengthStreamingMode(payload.length);
try (OutputStream stream = connection.getOutputStream()) {
stream.write(payload);
}
int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
String details = "";
InputStream errorStream = connection.getErrorStream();
if (errorStream != null) {
details = readStream(errorStream);
}
connection.disconnect();
throw new IOException((isEnglish ? "Mistral TTS API responded with code " : "La API TTS de Mistral respondió con código ")
+ responseCode
+ (details.isEmpty() ? "." : ". " + details));
}
File outputFile = File.createTempFile("daily-reading-tts-", ".mp3", getCacheDir());
try (InputStream input = connection.getInputStream();
FileOutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[8192];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
} finally {
connection.disconnect();
}
return outputFile;
}
private void showTtsAudioReady(File audioFile) {
try {
releaseTtsPlayer();
@@ -7,9 +7,11 @@ import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -45,6 +47,8 @@ public class SettingsActivity extends Activity {
private TextView downloadButton;
private TextView voiceSubtitleText;
private TextView voiceButton;
private TextView ttsProviderButton;
private TextView mistralApiKeyButton;
private UpdateManager.UpdateInfo latestInfo;
private boolean isEnglish;
@@ -100,7 +104,9 @@ public class SettingsActivity extends Activity {
content.addView(title, titleParams);
content.addView(createLanguageCard());
content.addView(createTtsProviderCard());
content.addView(createVoiceCard());
content.addView(createMistralApiKeyCard());
content.addView(createVersionCard());
checkButton = actionButton(isEnglish ? "Check now" : "Comprobar", COLOR_ACCENT, Color.WHITE);
@@ -200,6 +206,8 @@ public class SettingsActivity extends Activity {
boolean current = UpdateManager.isEnglish(this);
UpdateManager.setEnglish(this, !current);
langToggle.setText(!current ? "English" : "Español");
isEnglish = !current;
recreate();
updateVoiceCard();
});
card.addView(langToggle, new LinearLayout.LayoutParams(dp(100), dp(40)));
@@ -207,6 +215,39 @@ public class SettingsActivity extends Activity {
return card;
}
private View createTtsProviderCard() {
LinearLayout card = createCard();
card.setOrientation(LinearLayout.HORIZONTAL);
card.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout copy = new LinearLayout(this);
copy.setOrientation(LinearLayout.VERTICAL);
TextView title = textView("TTS Provider", 20, ink(), Typeface.BOLD);
title.setIncludeFontPadding(false);
copy.addView(title);
TextView subtitle = textView("Choose between Gleez and Mistral TTS", 14, muted(), Typeface.NORMAL);
LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
subtitleParams.setMargins(0, dp(6), 0, 0);
copy.addView(subtitle, subtitleParams);
card.addView(copy, new LinearLayout.LayoutParams(
0,
ViewGroup.LayoutParams.WRAP_CONTENT,
1f
));
ttsProviderButton = actionButton(getTtsProviderLabel(), COLOR_ACCENT, Color.WHITE);
ttsProviderButton.setOnClickListener(view -> showTtsProviderPicker());
card.addView(ttsProviderButton, new LinearLayout.LayoutParams(dp(140), dp(40)));
return card;
}
private View createVoiceCard() {
LinearLayout card = createCard();
card.setOrientation(LinearLayout.VERTICAL);
@@ -244,6 +285,42 @@ public class SettingsActivity extends Activity {
return card;
}
private View createMistralApiKeyCard() {
LinearLayout card = createCard();
card.setOrientation(LinearLayout.VERTICAL);
LinearLayout copy = new LinearLayout(this);
copy.setOrientation(LinearLayout.VERTICAL);
TextView title = textView("Mistral API Key", 20, ink(), Typeface.BOLD);
title.setIncludeFontPadding(false);
copy.addView(title);
TextView subtitle = textView("Required for Mistral TTS (get from mistral.ai)", 14, muted(), Typeface.NORMAL);
LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
subtitleParams.setMargins(0, dp(6), 0, 0);
copy.addView(subtitle, subtitleParams);
card.addView(copy, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
mistralApiKeyButton = actionButton(getMistralApiKeyLabel(), COLOR_ACCENT, Color.WHITE);
mistralApiKeyButton.setOnClickListener(view -> showMistralApiKeyDialog());
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
dp(42)
);
buttonParams.setMargins(0, dp(12), 0, 0);
card.addView(mistralApiKeyButton, buttonParams);
return card;
}
private View createVersionCard() {
LinearLayout card = createCard();
card.setOrientation(LinearLayout.VERTICAL);
@@ -319,7 +396,8 @@ public class SettingsActivity extends Activity {
private void showVoicePicker() {
boolean english = UpdateManager.isEnglish(this);
UpdateManager.VoiceOption[] options = UpdateManager.getTtsVoiceOptions(english);
String provider = UpdateManager.getTtsProvider(this);
UpdateManager.VoiceOption[] options = UpdateManager.getTtsVoiceOptions(english, provider);
String[] labels = new String[options.length];
for (int index = 0; index < options.length; index++) {
labels[index] = options[index].label;
@@ -327,13 +405,14 @@ public class SettingsActivity extends Activity {
int selectedIndex = UpdateManager.getTtsVoiceIndex(
english,
UpdateManager.getTtsVoice(this, english)
provider,
UpdateManager.getTtsVoice(this, english, provider)
);
new AlertDialog.Builder(this)
.setTitle(english ? "English voice" : "Spanish voice")
.setSingleChoiceItems(labels, selectedIndex, (dialog, which) -> {
UpdateManager.setTtsVoice(this, english, options[which].id);
UpdateManager.setTtsVoice(this, english, provider, options[which].id);
updateVoiceCard();
dialog.dismiss();
})
@@ -341,17 +420,94 @@ public class SettingsActivity extends Activity {
.show();
}
private void showTtsProviderPicker() {
String[] providers = {"Gleez", "Mistral"};
String currentProvider = UpdateManager.getTtsProvider(this);
int selectedIndex = currentProvider.equals(UpdateManager.TTS_PROVIDER_MISTRAL) ? 1 : 0;
new AlertDialog.Builder(this)
.setTitle("Select TTS Provider")
.setSingleChoiceItems(providers, selectedIndex, (dialog, which) -> {
String provider = which == 1 ? UpdateManager.TTS_PROVIDER_MISTRAL : UpdateManager.TTS_PROVIDER_GLEEZE;
UpdateManager.setTtsProvider(this, provider);
ttsProviderButton.setText(getTtsProviderLabel());
updateVoiceCard();
dialog.dismiss();
})
.setNegativeButton("Cancel", null)
.show();
}
private void showMistralApiKeyDialog() {
String currentKey = UpdateManager.getMistralApiKey(this);
String displayKey = currentKey.isEmpty() ? "" : "••••••••";
EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
input.setHint("Enter your Mistral API key");
input.setText(currentKey);
input.setSelection(currentKey.length());
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(dp(20), dp(20), dp(20), dp(20));
layout.addView(input, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
new AlertDialog.Builder(this)
.setTitle("Mistral API Key")
.setView(layout)
.setPositiveButton("Save", (dialog, which) -> {
UpdateManager.setMistralApiKey(this, input.getText().toString().trim());
mistralApiKeyButton.setText(getMistralApiKeyLabel());
})
.setNegativeButton("Cancel", null)
.show();
}
private String getTtsProviderLabel() {
String provider = UpdateManager.getTtsProvider(this);
return provider.equals(UpdateManager.TTS_PROVIDER_MISTRAL) ? "Mistral" : "Gleez";
}
private String getMistralApiKeyLabel() {
String apiKey = UpdateManager.getMistralApiKey(this);
if (apiKey.isEmpty()) {
return "Not configured";
}
return "••••••••";
}
private void updateVoiceCard() {
if (voiceSubtitleText == null || voiceButton == null) {
return;
}
boolean english = UpdateManager.isEnglish(this);
String provider = UpdateManager.getTtsProvider(this);
String voiceId = UpdateManager.getTtsVoice(this, english);
// Update subtitle based on provider
if (provider.equals(UpdateManager.TTS_PROVIDER_MISTRAL)) {
voiceSubtitleText.setText(english
? "English voices are shown while English is selected"
: "Las voces en español se muestran mientras el español esté seleccionado");
voiceButton.setText(UpdateManager.getTtsVoiceLabel(english, voiceId));
? "Mistral voices (requires API key)"
: "Voces de Mistral (requiere clave API)");
} else {
voiceSubtitleText.setText(english
? "Gleez voices are shown while English is selected"
: "Las voces de Gleez se muestran mientras el idioma esta seleccionado");
}
voiceButton.setText(UpdateManager.getTtsVoiceLabel(english, provider, voiceId));
// If Mistral is selected but API key is not configured, show a warning
if (provider.equals(UpdateManager.TTS_PROVIDER_MISTRAL) && !UpdateManager.isMistralConfigured(this)) {
voiceSubtitleText.setText(english
? "Mistral voices (requires API key - not configured!)"
: "Voces de Mistral (requiere clave API - ¡no configurada!)");
}
}
private void updateInstalledVersionText() {
@@ -29,12 +29,19 @@ final class UpdateManager {
static final String KEY_TTS_VOICE_SPANISH = "tts_voice_spanish";
static final String DEFAULT_TTS_VOICE_ENGLISH = "af_heart";
static final String DEFAULT_TTS_VOICE_SPANISH = "ef_dora";
static final String DEFAULT_TTS_VOICE_ENGLISH_MISTRAL = "v2/en_us-florence";
static final String DEFAULT_TTS_VOICE_SPANISH_MISTRAL = "v2/es_es-carlota";
static final String KEY_TTS_PROVIDER = "tts_provider";
static final String KEY_MISTRAL_API_KEY = "mistral_api_key";
static final String TTS_PROVIDER_GLEEZE = "gleeze";
static final String TTS_PROVIDER_MISTRAL = "mistral";
static final String MISTRAL_TTS_URL = "https://api.mistral.ai/v1/text-to-speech";
static final String LATEST_JSON_URL =
"https://github.com/jahruz67/Bible-Daily/releases/download/latest/latest.json";
static final String APK_URL =
"https://github.com/jahruz67/Bible-Daily/releases/download/latest/app-debug.apk";
private static final VoiceOption[] ENGLISH_VOICES = {
private static final VoiceOption[] ENGLISH_VOICES_GLEEZE = {
new VoiceOption("af_heart", "Heart (US female)"),
new VoiceOption("af_alloy", "Alloy (US female)"),
new VoiceOption("af_aoede", "Aoede (US female)"),
@@ -65,12 +72,27 @@ final class UpdateManager {
new VoiceOption("bm_lewis", "Lewis (UK male)")
};
private static final VoiceOption[] SPANISH_VOICES = {
private static final VoiceOption[] ENGLISH_VOICES_MISTRAL = {
new VoiceOption("v2/en_us-florence", "Florence (US female)"),
new VoiceOption("v2/en_us-dave", "Dave (US male)"),
new VoiceOption("v2/en_us-libby", "Libby (US female)"),
new VoiceOption("v2/en_us-matt", "Matt (US male)"),
new VoiceOption("v2/en_us-serena", "Serena (US female)"),
new VoiceOption("v2/en_us-andrew", "Andrew (US male)")
};
private static final VoiceOption[] SPANISH_VOICES_GLEEZE = {
new VoiceOption("ef_dora", "Dora (Spanish female)"),
new VoiceOption("em_alex", "Alex (Spanish male)"),
new VoiceOption("em_santa", "Santa (Spanish male)")
};
private static final VoiceOption[] SPANISH_VOICES_MISTRAL = {
new VoiceOption("v2/es_es-alvaro", "Alvaro (Spanish male)"),
new VoiceOption("v2/es_es-carlota", "Carlota (Spanish female)"),
new VoiceOption("v2/es_es-gerardo", "Gerardo (Spanish male)")
};
private UpdateManager() {
}
@@ -98,35 +120,76 @@ final class UpdateManager {
preferences(context).edit().putBoolean(KEY_DARK_MODE, enabled).apply();
}
static VoiceOption[] getTtsVoiceOptions(boolean isEnglish) {
return isEnglish ? ENGLISH_VOICES : SPANISH_VOICES;
static VoiceOption[] getTtsVoiceOptions(boolean isEnglish, String provider) {
if (provider == null || provider.equals(TTS_PROVIDER_GLEEZE)) {
return isEnglish ? ENGLISH_VOICES_GLEEZE : SPANISH_VOICES_GLEEZE;
} else {
return isEnglish ? ENGLISH_VOICES_MISTRAL : SPANISH_VOICES_MISTRAL;
}
}
static String getTtsVoice(Context context, boolean isEnglish) {
String defaultVoice = getDefaultTtsVoice(isEnglish);
String voice = preferences(context).getString(getTtsVoiceKey(isEnglish), defaultVoice);
if (isValidTtsVoice(isEnglish, voice)) {
static VoiceOption[] getTtsVoiceOptions(boolean isEnglish) {
return getTtsVoiceOptions(isEnglish, TTS_PROVIDER_GLEEZE);
}
static String getTtsVoice(Context context, boolean isEnglish, String provider) {
String defaultVoice = getDefaultTtsVoice(isEnglish, provider);
String voice = preferences(context).getString(getTtsVoiceKey(isEnglish, provider), defaultVoice);
if (isValidTtsVoice(isEnglish, provider, voice)) {
return voice;
}
return defaultVoice;
}
static void setTtsVoice(Context context, boolean isEnglish, String voiceId) {
String voice = isValidTtsVoice(isEnglish, voiceId) ? voiceId : getDefaultTtsVoice(isEnglish);
preferences(context).edit().putString(getTtsVoiceKey(isEnglish), voice).apply();
static String getTtsVoice(Context context, boolean isEnglish) {
return getTtsVoice(context, isEnglish, TTS_PROVIDER_GLEEZE);
}
static String getTtsVoiceLabel(boolean isEnglish, String voiceId) {
for (VoiceOption option : getTtsVoiceOptions(isEnglish)) {
static void setTtsVoice(Context context, boolean isEnglish, String provider, String voiceId) {
String voice = isValidTtsVoice(isEnglish, provider, voiceId) ? voiceId : getDefaultTtsVoice(isEnglish, provider);
preferences(context).edit().putString(getTtsVoiceKey(isEnglish, provider), voice).apply();
}
static void setTtsVoice(Context context, boolean isEnglish, String voiceId) {
setTtsVoice(context, isEnglish, TTS_PROVIDER_GLEEZE, voiceId);
}
static String getTtsProvider(Context context) {
return preferences(context).getString(KEY_TTS_PROVIDER, TTS_PROVIDER_GLEEZE);
}
static void setTtsProvider(Context context, String provider) {
preferences(context).edit().putString(KEY_TTS_PROVIDER, provider).apply();
}
static String getMistralApiKey(Context context) {
return preferences(context).getString(KEY_MISTRAL_API_KEY, "");
}
static void setMistralApiKey(Context context, String apiKey) {
preferences(context).edit().putString(KEY_MISTRAL_API_KEY, apiKey).apply();
}
static boolean isMistralConfigured(Context context) {
String apiKey = getMistralApiKey(context);
return apiKey != null && !apiKey.trim().isEmpty();
}
static String getTtsVoiceLabel(boolean isEnglish, String provider, String voiceId) {
for (VoiceOption option : getTtsVoiceOptions(isEnglish, provider)) {
if (option.id.equals(voiceId)) {
return option.label;
}
}
return getTtsVoiceLabel(isEnglish, getDefaultTtsVoice(isEnglish));
return getTtsVoiceLabel(isEnglish, provider, getDefaultTtsVoice(isEnglish));
}
static int getTtsVoiceIndex(boolean isEnglish, String voiceId) {
VoiceOption[] options = getTtsVoiceOptions(isEnglish);
static String getTtsVoiceLabel(boolean isEnglish, String voiceId) {
return getTtsVoiceLabel(isEnglish, TTS_PROVIDER_GLEEZE, voiceId);
}
static int getTtsVoiceIndex(boolean isEnglish, String provider, String voiceId) {
VoiceOption[] options = getTtsVoiceOptions(isEnglish, provider);
for (int index = 0; index < options.length; index++) {
if (options[index].id.equals(voiceId)) {
return index;
@@ -135,19 +198,39 @@ final class UpdateManager {
return 0;
}
private static String getDefaultTtsVoice(boolean isEnglish) {
static int getTtsVoiceIndex(boolean isEnglish, String voiceId) {
return getTtsVoiceIndex(isEnglish, TTS_PROVIDER_GLEEZE, voiceId);
}
private static String getDefaultTtsVoice(boolean isEnglish, String provider) {
if (provider == null || provider.equals(TTS_PROVIDER_GLEEZE)) {
return isEnglish ? DEFAULT_TTS_VOICE_ENGLISH : DEFAULT_TTS_VOICE_SPANISH;
} else {
return isEnglish ? DEFAULT_TTS_VOICE_ENGLISH_MISTRAL : DEFAULT_TTS_VOICE_SPANISH_MISTRAL;
}
}
private static String getDefaultTtsVoice(boolean isEnglish) {
return getDefaultTtsVoice(isEnglish, TTS_PROVIDER_GLEEZE);
}
private static String getTtsVoiceKey(boolean isEnglish, String provider) {
if (provider == null || provider.equals(TTS_PROVIDER_GLEEZE)) {
return isEnglish ? KEY_TTS_VOICE_ENGLISH : KEY_TTS_VOICE_SPANISH;
} else {
return isEnglish ? KEY_TTS_VOICE_ENGLISH + "_mistral" : KEY_TTS_VOICE_SPANISH + "_mistral";
}
}
private static String getTtsVoiceKey(boolean isEnglish) {
return isEnglish ? KEY_TTS_VOICE_ENGLISH : KEY_TTS_VOICE_SPANISH;
return getTtsVoiceKey(isEnglish, TTS_PROVIDER_GLEEZE);
}
private static boolean isValidTtsVoice(boolean isEnglish, String voiceId) {
private static boolean isValidTtsVoice(boolean isEnglish, String provider, String voiceId) {
if (voiceId == null) {
return false;
}
for (VoiceOption option : getTtsVoiceOptions(isEnglish)) {
for (VoiceOption option : getTtsVoiceOptions(isEnglish, provider)) {
if (option.id.equals(voiceId)) {
return true;
}
@@ -155,6 +238,10 @@ final class UpdateManager {
return false;
}
private static boolean isValidTtsVoice(boolean isEnglish, String voiceId) {
return isValidTtsVoice(isEnglish, TTS_PROVIDER_GLEEZE, voiceId);
}
static UpdateInfo fetchLatestUpdate(Context context) throws Exception {
String jsonText = download(LATEST_JSON_URL, "application/json");
JSONObject json = new JSONObject(jsonText);