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()