diff --git a/assets/js/app.js b/assets/js/app.js index caa46d3..0097657 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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 = ''; + return; + } + if (!window.speechSynthesis) { sel.innerHTML = ''; return; @@ -9417,19 +9424,31 @@ 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) { utt.voice = preferred; - utt.lang = preferred.lang; + utt.lang = preferred.lang; } else { utt.lang = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-US' : 'it-IT'; } @@ -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; diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt index d6ccb1a..04d81c6 100644 --- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt +++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt @@ -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()