fix: native Android TTS bridge in kiosk — bypass Web Speech API voice issues
On Android WebView, window.speechSynthesis.getVoices() often returns empty because the Web Speech API cannot enumerate the device's TTS voices. This caused the kiosk to show 'nessuna voce offline è supportata'. Changes: - KioskActivity.kt: initialise Android TextToSpeech engine on startup; expose speak(text, rate, pitch), stopSpeech() and isTtsReady() via the existing _kioskBridge JavascriptInterface; release TTS in onDestroy. - app.js (_speakBrowser): when _kioskBridge.speak is available, delegate to it instead of using speechSynthesis — works even without offline voice packs installed. - app.js (_initBrowserTtsVoices): show 'Voce nativa Android (kiosk)' in the voice dropdown when running inside the kiosk WebView. - app.js (testTTS): use the bridge path when testing TTS inside the kiosk.
This commit is contained in:
+33
-4
@@ -9368,6 +9368,13 @@ function _initBrowserTtsVoices(selectedVoice) {
|
||||
const sel = document.getElementById('setting-tts-voice');
|
||||
if (!sel) return;
|
||||
|
||||
// Inside the EverShelf Kiosk Android app the native TTS bridge handles
|
||||
// speech — no Web Speech API voice list needed.
|
||||
if (typeof _kioskBridge !== 'undefined' && typeof _kioskBridge.speak === 'function') {
|
||||
sel.innerHTML = '<option value="">— Voce nativa Android (kiosk) —</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.speechSynthesis) {
|
||||
sel.innerHTML = '<option value="">— Voce non supportata dal browser —</option>';
|
||||
return;
|
||||
@@ -9417,14 +9424,26 @@ function _initBrowserTtsVoices(selectedVoice) {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/** Speak text using the browser Web Speech API (offline). */
|
||||
/** Speak text using the browser Web Speech API (offline).
|
||||
* When running inside the EverShelf Kiosk Android app the native TTS bridge
|
||||
* is preferred — it bypasses Web Speech API voice limitations on Android. */
|
||||
function _speakBrowser(text) {
|
||||
const s = getSettings();
|
||||
const rate = parseFloat(s.tts_rate) || 1;
|
||||
const pitch = parseFloat(s.tts_pitch) || 1;
|
||||
|
||||
// ── Native Android TTS bridge (kiosk WebView) ──────────────────────
|
||||
if (typeof _kioskBridge !== 'undefined' && typeof _kioskBridge.speak === 'function') {
|
||||
try { _kioskBridge.speak(text, rate, pitch); } catch(_e) { /* silent */ }
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Web Speech API (desktop / mobile browser) ──────────────────────
|
||||
if (!window.speechSynthesis) return;
|
||||
window.speechSynthesis.cancel();
|
||||
const s = getSettings();
|
||||
const utt = new SpeechSynthesisUtterance(text);
|
||||
utt.rate = parseFloat(s.tts_rate) || 1;
|
||||
utt.pitch = parseFloat(s.tts_pitch) || 1;
|
||||
utt.rate = rate;
|
||||
utt.pitch = pitch;
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
const preferred = voices.find(v => v.name === s.tts_voice);
|
||||
if (preferred) {
|
||||
@@ -9445,6 +9464,16 @@ async function testTTS() {
|
||||
}
|
||||
const engine = document.getElementById('setting-tts-engine')?.value || 'browser';
|
||||
if (engine === 'browser') {
|
||||
// Kiosk native TTS bridge takes priority over Web Speech API
|
||||
if (typeof _kioskBridge !== 'undefined' && typeof _kioskBridge.speak === 'function') {
|
||||
const s = getSettings();
|
||||
s.tts_rate = parseFloat(document.getElementById('setting-tts-rate')?.value) || 1;
|
||||
s.tts_pitch = parseFloat(document.getElementById('setting-tts-pitch')?.value) || 1;
|
||||
saveSettingsToStorage(s);
|
||||
_speakBrowser('Test vocale EverShelf. La sintesi vocale funziona correttamente.');
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status success'; statusEl.textContent = '✅ Riproduzione in corso — controlla l\'audio del dispositivo.'; }
|
||||
return;
|
||||
}
|
||||
if (!window.speechSynthesis) {
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '❌ Web Speech API non supportata da questo browser.'; }
|
||||
return;
|
||||
|
||||
@@ -14,11 +14,13 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.ValueCallback
|
||||
@@ -39,6 +41,7 @@ import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.json.JSONObject
|
||||
import java.net.URL
|
||||
import java.util.Locale
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
@@ -49,6 +52,11 @@ class KioskActivity : AppCompatActivity() {
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private var currentStep = 1
|
||||
|
||||
// Native TTS engine (Android) — used by the JS bridge so the WebView
|
||||
// doesn't depend on Web Speech API voices being installed.
|
||||
private var tts: TextToSpeech? = null
|
||||
private var ttsReady = false
|
||||
|
||||
// Views
|
||||
private lateinit var splashContainer: LinearLayout
|
||||
private lateinit var wizardContainer: ScrollView
|
||||
@@ -98,6 +106,19 @@ class KioskActivity : AppCompatActivity() {
|
||||
enableKioskLock()
|
||||
requestAllPermissions()
|
||||
|
||||
// Initialise native TTS engine so the JS bridge works even when
|
||||
// Web Speech API voices are unavailable in the Android WebView.
|
||||
tts = TextToSpeech(this) { status ->
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
val it = tts?.setLanguage(Locale.ITALIAN)
|
||||
if (it == TextToSpeech.LANG_MISSING_DATA || it == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
// Italian data missing — fall back to device default
|
||||
tts?.language = Locale.getDefault()
|
||||
}
|
||||
ttsReady = true
|
||||
}
|
||||
}
|
||||
|
||||
// Show splash then proceed
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
splashContainer.visibility = View.GONE
|
||||
@@ -506,7 +527,7 @@ class KioskActivity : AppCompatActivity() {
|
||||
|
||||
// Add JS interface ONCE before loading
|
||||
webView.addJavascriptInterface(object {
|
||||
@android.webkit.JavascriptInterface
|
||||
@JavascriptInterface
|
||||
fun exit() {
|
||||
runOnUiThread {
|
||||
disableKioskLock()
|
||||
@@ -514,13 +535,35 @@ class KioskActivity : AppCompatActivity() {
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
@android.webkit.JavascriptInterface
|
||||
@JavascriptInterface
|
||||
fun hardReload() {
|
||||
runOnUiThread {
|
||||
webView.clearCache(true)
|
||||
webView.reload()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Speak [text] via Android native TTS.
|
||||
* Called by app.js when running inside the kiosk WebView so that
|
||||
* speech synthesis works even without Web Speech API offline voices.
|
||||
* [rate] and [pitch] are floats (default 1.0).
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun speak(text: String, rate: Float, pitch: Float) {
|
||||
val engine = tts ?: return
|
||||
if (!ttsReady) return
|
||||
engine.setSpeechRate(rate.coerceIn(0.1f, 4f))
|
||||
engine.setPitch(pitch.coerceIn(0.1f, 4f))
|
||||
engine.speak(text, TextToSpeech.QUEUE_FLUSH, null, "kiosk_tts")
|
||||
}
|
||||
/** Cancel any ongoing speech. */
|
||||
@JavascriptInterface
|
||||
fun stopSpeech() {
|
||||
tts?.stop()
|
||||
}
|
||||
/** Returns "true" when the TTS engine is ready. */
|
||||
@JavascriptInterface
|
||||
fun isTtsReady(): String = if (ttsReady) "true" else "false"
|
||||
}, "_kioskBridge")
|
||||
|
||||
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
|
||||
@@ -729,6 +772,13 @@ class KioskActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
tts?.stop()
|
||||
tts?.shutdown()
|
||||
tts = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (webView.visibility == View.VISIBLE && webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
|
||||
Reference in New Issue
Block a user