hotfix: resolve compatibility issues with Android Text-to-Speech API (#1394)

This commit is contained in:
Huang Xin
2025-06-14 21:39:12 +08:00
committed by GitHub
parent 89d48c72b0
commit 84328dcfb2
7 changed files with 51 additions and 30 deletions
@@ -1,3 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
</manifest>
@@ -3,6 +3,7 @@ package com.readest.native_tts
import android.os.Bundle
import android.app.Activity
import android.content.Context
import android.provider.Settings
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.speech.tts.Voice
@@ -97,7 +98,11 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
private suspend fun initializeTTS(): Boolean = suspendCancellableCoroutine { continuation ->
try {
textToSpeech = TextToSpeech(activity) { status ->
val preferredEngine = Settings.Secure.getString(
activity.contentResolver,
Settings.Secure.TTS_DEFAULT_SYNTH
)
textToSpeech = TextToSpeech(activity, { status ->
when (status) {
TextToSpeech.SUCCESS -> {
setupTTSListener()
@@ -111,7 +116,7 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
continuation.resume(false) {}
}
}
}
}, preferredEngine)
} catch (e: Exception) {
Log.e(TAG, "Exception during TTS initialization", e)
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@@ -264,7 +264,7 @@ export class EdgeTTSClient implements TTSClient {
const voicesGroup: TTSVoicesGroup = {
id: 'edge-tts',
name: 'Edge TTS',
voices: filteredVoices,
voices: filteredVoices.sort(TTSUtils.sortVoicesFunc),
disabled: !this.initialized || filteredVoices.length === 0,
};
@@ -14,12 +14,25 @@ type TTSEventPayload = {
const TTSEngines = {
default: 'System TTS',
msctts: 'Msc TTS',
mstrans: 'MSTrans TTS',
microsoft: 'Microsoft TTS',
bytedance: 'ByteDance TTS',
peiyinya: 'PeiYinYa TTS',
huoshan: 'HuoShan TTS',
sougou: 'Sougou TTS',
xiaomi: 'XiaoMi TTS',
bdetts: 'BDeTTS',
bdotts: 'BDoTTS',
vcstts: 'VcsTTS',
isstts: 'IssTTS',
xfpeiyin: 'XFPeiYin',
azure: 'Azure TTS',
edgetts: 'Edge TTS',
google: 'Google TTS',
gemini: 'Gemini TTS',
weread: 'WeRead TTS',
aispeech: 'Aispeech',
} as Record<string, string>;
export class NativeTTSClient implements TTSClient {
@@ -189,7 +202,7 @@ export class NativeTTSClient implements TTSClient {
const defaultVoice = preferredVoice
? preferredVoice
: (await this.getVoices(lang))[0]?.voices[0] || null;
return defaultVoice?.id || 'NOT_SET';
return defaultVoice?.id || '';
}
async pause() {
@@ -244,7 +257,6 @@ export class NativeTTSClient implements TTSClient {
const filteredVoices = voices.filter(
(v) => v.lang.startsWith(locale) || (lang === 'en' && ['en-US', 'en-GB'].includes(v.lang)),
);
const voiceGroups = new Map<string, TTSVoice[]>();
filteredVoices.forEach((voice) => {
const { name, lang } = voice;
@@ -271,7 +283,7 @@ export class NativeTTSClient implements TTSClient {
({
id: groupId,
name: TTSEngines[groupId] || groupId,
voices: voices,
voices: voices.sort(TTSUtils.sortVoicesFunc),
disabled: !this.initialized || voices.length === 0,
}) as TTSVoicesGroup,
)
@@ -61,6 +61,7 @@ export class TTSController extends EventTarget {
if (await this.ttsWebClient.init()) {
availableClients.push(this.ttsWebClient);
}
this.ttsClient = availableClients[0] || this.ttsWebClient;
const preferredClientName = TTSUtils.getPreferredClient();
if (preferredClientName) {
const preferredClient = availableClients.find(
@@ -70,9 +71,6 @@ export class TTSController extends EventTarget {
this.ttsClient = preferredClient;
}
}
if (!this.ttsClient) {
this.ttsClient = availableClients[0] || this.ttsWebClient;
}
this.ttsWebVoices = await this.ttsWebClient.getAllVoices();
this.ttsEdgeVoices = await this.ttsEdgeClient.getAllVoices();
}
@@ -288,26 +286,6 @@ export class TTSController extends EventTarget {
const ttsNativeVoices = (await this.ttsNativeClient?.getVoices(lang)) ?? [];
const voicesGroups = [...ttsNativeVoices, ...ttsEdgeVoices, ...ttsWebVoices];
voicesGroups.forEach((group) => {
group.voices = group.voices.sort((a, b) => {
const aRegion = a.lang.split('-')[1] || '';
const bRegion = b.lang.split('-')[1] || '';
if (aRegion === bRegion) {
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
}
if (aRegion === 'CN') return -1;
if (bRegion === 'CN') return 1;
if (aRegion === 'TW') return -1;
if (bRegion === 'TW') return 1;
if (aRegion === 'HK') return -1;
if (bRegion === 'HK') return 1;
if (aRegion === 'US') return -1;
if (bRegion === 'US') return 1;
if (aRegion === 'GB') return -1;
if (bRegion === 'GB') return 1;
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
});
});
return voicesGroups;
}
@@ -1,3 +1,5 @@
import { TTSVoice } from './types';
export class TTSUtils {
private static readonly LOCAL_STORAGE_KEY = 'ttsPreferredVoices';
private static readonly PREFERRED_CLIENT_KEY = 'preferredClient';
@@ -37,4 +39,23 @@ export class TTSUtils {
const storedPreferences = localStorage.getItem(this.LOCAL_STORAGE_KEY);
return storedPreferences ? JSON.parse(storedPreferences) : {};
}
static sortVoicesFunc(a: TTSVoice, b: TTSVoice): number {
const aRegion = a.lang.split('-')[1] || '';
const bRegion = b.lang.split('-')[1] || '';
if (aRegion === bRegion) {
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
}
if (aRegion === 'CN') return -1;
if (bRegion === 'CN') return 1;
if (aRegion === 'TW') return -1;
if (bRegion === 'TW') return 1;
if (aRegion === 'HK') return -1;
if (bRegion === 'HK') return 1;
if (aRegion === 'US') return -1;
if (bRegion === 'US') return 1;
if (aRegion === 'GB') return -1;
if (bRegion === 'GB') return 1;
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
}
}
@@ -270,7 +270,7 @@ export class WebSpeechClient implements TTSClient {
const voicesGroup: TTSVoicesGroup = {
id: 'web-speech-api',
name: 'Web TTS',
voices,
voices: voices.sort(TTSUtils.sortVoicesFunc),
disabled: !this.initialized || voices.length === 0,
};
return [voicesGroup];