From e7714d8773cd7194b67e445bb28770c47679c767 Mon Sep 17 00:00:00 2001
From: John Doe <johndoe@example.com>
Date: Thu, 4 Jun 2026 22:13:38 +0200
Subject: [PATCH 2/6] add local mode for dev compilation and startup: killall
 java, clear log, greeting, unset LD_PRELOAD; replace opencv stub with real
 lib, add play kill, slow down tts

---
 compile-local.sh                          | 29 ++++++++++++++++++
 compile.sh                                |  2 +-
 depends/src/hash/SHA256.java              | 23 ++++++++++++++
 depends/src/misc/Base64.java              | 21 +++++++++++++
 etc/pva/conf.d/01-default.conf            |  8 ++---
 etc/pva/conf.d/06-help.conf               | 12 ++++++++
 lib/PLACEHOLDER- sqlite-jdbc-3.51.1.0.jar |  3 --
 server/PVA.java                           | 37 +++++++++++++++++++++--
 start-local.sh                            | 28 +++++++++++++++++
 usr/local/sbin/pipersay                   | 20 ++++++++++++
 10 files changed, 173 insertions(+), 10 deletions(-)
 create mode 100755 compile-local.sh
 create mode 100644 depends/src/hash/SHA256.java
 create mode 100644 depends/src/misc/Base64.java
 create mode 100644 etc/pva/conf.d/06-help.conf
 delete mode 100644 lib/PLACEHOLDER- sqlite-jdbc-3.51.1.0.jar
 create mode 100755 start-local.sh
 create mode 100755 usr/local/sbin/pipersay

diff --git a/compile-local.sh b/compile-local.sh
new file mode 100755
index 0000000..dde9153
--- /dev/null
+++ b/compile-local.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+rm -f lib/PLACEHOLDER-*
+
+if [ "$1" = "clean" ]; then
+	find . -iname "*.class" -delete
+	shift
+fi
+
+CP=""
+if [ "$1" = "-withcustomlibs" ]; then
+	CP="$2"
+elif [ "$1" = "-withjnapackaged" ]; then
+	CP=""
+else
+	for file in lib/*.jar; do
+		CP="${CP:+$CP:}$file"
+	done
+
+	if [ -e plugins/files/WatchdogPlugin.java ]; then
+		CP="/usr/share/java/opencv4/opencv-4100.jar:$CP"
+	fi
+fi
+
+echo "Classpath: $CP"
+
+find . -name "*.java" > java_files.txt
+javac --release 11 -implicit:none -cp "$CP:." @java_files.txt
+rm -f java_files.txt
diff --git a/compile.sh b/compile.sh
index 990253b..de0e544 100644
--- a/compile.sh
+++ b/compile.sh
@@ -35,7 +35,7 @@ for file in lib/*jar;do  CP="$CP:./$file"; done
 
 echo "Classpath: $CP"
 
-javac --release 11 server/PVA.java */*.java */*/*java -cp "$CP:."
+javac --release 11 -implicit:none server/PVA.java */*.java */*/*java -cp "$CP:."
 
 # if you need Java 8 (legacy) change 11 back to 8
 
diff --git a/depends/src/hash/SHA256.java b/depends/src/hash/SHA256.java
new file mode 100644
index 0000000..d18d50c
--- /dev/null
+++ b/depends/src/hash/SHA256.java
@@ -0,0 +1,23 @@
+package hash;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class SHA256 {
+
+    public static String sha256(String input) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = digest.digest(input.getBytes("UTF-8"));
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hash) {
+                String hex = Integer.toHexString(0xff & b);
+                if (hex.length() == 1) hexString.append('0');
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (Exception e) {
+            return "";
+        }
+    }
+}
diff --git a/depends/src/misc/Base64.java b/depends/src/misc/Base64.java
new file mode 100644
index 0000000..dddceab
--- /dev/null
+++ b/depends/src/misc/Base64.java
@@ -0,0 +1,21 @@
+package misc;
+
+public class Base64 {
+
+    private static final String BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+    public static String encodeBytes(byte[] data) {
+        StringBuilder sb = new StringBuilder();
+        int i = 0;
+        while (i < data.length) {
+            int b0 = data[i++] & 0xff;
+            int b1 = i < data.length ? data[i++] & 0xff : 0;
+            int b2 = i < data.length ? data[i++] & 0xff : 0;
+            sb.append(BASE64_CHARS.charAt(b0 >>> 2));
+            sb.append(BASE64_CHARS.charAt(((b0 & 3) << 4) | (b1 >>> 4)));
+            sb.append(i > data.length + 0 ? '=' : BASE64_CHARS.charAt(((b1 & 15) << 2) | (b2 >>> 6)));
+            sb.append(i > data.length + 1 ? '=' : BASE64_CHARS.charAt(b2 & 63));
+        }
+        return sb.toString();
+    }
+}
diff --git a/etc/pva/conf.d/01-default.conf b/etc/pva/conf.d/01-default.conf
index 8631d3f..1b59968 100644
--- a/etc/pva/conf.d/01-default.conf
+++ b/etc/pva/conf.d/01-default.conf
@@ -131,7 +131,7 @@ app:"gfx","gnome-open"
 app:"mail","thunderbird"
 app:"web","firefox"
 app:"phone","/usr/bin/jitsix:xtel:"
-app:"say","/usr/bin/envx:xVOICE=%VOICEx:x/usr/local/sbin/say"
+app:"say","/usr/local/sbin/pipersay"
 app:"searchengine","duckduckgo"
 app:"screenshot","gnome-screenshot"
 app:"bildbetrachter","eogx:x-nx:x-sx:x%F"
@@ -154,15 +154,15 @@ code:"beta","beta default"
 # alternatives are prefered apps to use in case we offer alternatives . PVA command to change "%NAME benutze %KEYWORD"
 # alternatives:"%KEYWORD","%FOR-APP","%CMD-TO-USE"
 
-alternatives:"normale sprachausgabe","say","envx:xVOICE=%VOICEx:x/usr/local/sbin/say"
+alternatives:"normale sprachausgabe","say","/usr/local/sbin/pipersay"
 alternatives:"google sprachausgabe","say","envx:xVOICE=%VOICEx:xGTTS=1x:x/usr/local/sbin/say"
 alternatives:"samsung sprachausgabe","say","/usr/local/sbin/samsungtts"
 alternatives:"piko sprachausgabe","say","envx:xVOICE=%VOICEx:x/usr/local/sbin/psay"
 alternatives:"pico sprachausgabe","say","envx:xVOICE=%VOICEx:x/usr/local/sbin/psay"
 alternatives:"marie sprachausgabe","say","envx:xVOICE=%VOICEx:x/usr/local/sbin/msay"
 alternatives:"roboter sprachausgabe","say","envx:xVOICE=%VOICEx:x/usr/local/sbin/robotsay"
-alternatives:"piper sprachausgabe","say","envx:xVOICE=%VOICEx:x/usr/local/sbin/piper"
-alternatives:"pipa sprachausgabe","say","envx:xVOICE=%VOICEx:x/usr/local/sbin/piper"
+alternatives:"piper sprachausgabe","say","/usr/local/sbin/pipersay"
+alternatives:"pipa sprachausgabe","say","/usr/local/sbin/pipersay"
 alternatives:"firefox","web","firefox"
 alternatives:"google chrome","web","google-chrome"
 alternatives:"chromium free","web"," chromium-freeworld"
diff --git a/etc/pva/conf.d/06-help.conf b/etc/pva/conf.d/06-help.conf
new file mode 100644
index 0000000..66e52d9
--- /dev/null
+++ b/etc/pva/conf.d/06-help.conf
@@ -0,0 +1,12 @@
+text:"de","HELPGENERAL","Verfügbare Befehle: Musik, Video, Wetter, Uhrzeit, Apps, Suche, Email, Telefon, Timer, Sprachausgabe, und mehr. Sage 'Hilfe zu Musik' für Details."
+text:"de","HELPMUSIC","Musik Befehle: spiele musik, pausiere musik, musik stop, nächster titel, letzter titel, leiser, lauter, viel leiser, viel lauter, füge titel hinzu"
+text:"de","HELPVIDEO","Video Befehle: video start, video stop, video pause, nächstes video, letztes video, fullscreen an, fullscreen aus"
+text:"de","HELPWEATHER","Wetter Befehle: wie ist das wetter, wie wird das wetter, wie wird das wetter morgen"
+text:"de","HELPTIME","Zeit Befehle: wie spät ist es, sag mir die uhrzeit, wie hoch ist die last"
+text:"de","HELPAPPS","App Befehle: start <app>, öffne <app>, beende <app>"
+text:"de","HELPSEARCH","Suche Befehle: suche web <begriff>, suche bild <begriff>, suche dokument <begriff>, lies dokument"
+text:"de","HELPEMAIL","Email Befehle: schreibe email an <name> betreff <text> inhalt <text>, suche email von <name>"
+text:"de","HELPPHONE","Telefon Befehle: rufe <name> an, suche telefon von <name>"
+text:"de","HELPTIMER","Timer Befehle: erinnere mich um <zeit> an <was>, erinnere mich in <zeit> an <was>, meine termine, lies termine vor"
+text:"de","HELPSYSTEM","System Befehle: beende dich, schalte dich aus, kompiliere dich neu, öffne sourcecode, öffne config, mache screenshot, erstelle cache"
+text:"de","HELPVOICE","Sprachausgabe Befehle: benutze normale sprachausgabe, benutze piper sprachausgabe, benutze google sprachausgabe"
diff --git a/lib/PLACEHOLDER- sqlite-jdbc-3.51.1.0.jar b/lib/PLACEHOLDER- sqlite-jdbc-3.51.1.0.jar
deleted file mode 100644
index fa245dd..0000000
--- a/lib/PLACEHOLDER- sqlite-jdbc-3.51.1.0.jar	
+++ /dev/null
@@ -1,3 +0,0 @@
-you can get it i.e. from here:
-
-https://github.com/xerial/sqlite-jdbc/releases
diff --git a/server/PVA.java b/server/PVA.java
index 745ffe0..7868589 100644
--- a/server/PVA.java
+++ b/server/PVA.java
@@ -23,7 +23,9 @@ import plugins.Plugins;
 import plugins.Plugin;
 import utils.Tools;
 import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter; 
+import java.time.format.DateTimeFormatter;
+import java.net.HttpURLConnection;
+import java.net.URL; 
 
 public class PVA {
 
@@ -690,6 +692,8 @@ public class PVA {
 	}
 
 	public static String getHome() {
+		String pvaHome = System.getProperty("pva.home");
+		if (pvaHome != null) return pvaHome.replaceAll("/$","");
 		return System.getenv("HOME").trim();
 	}
 
@@ -1243,9 +1247,34 @@ public class PVA {
 			if ( vcards.isEmpty() ) {
 				StringHash adb = config.get("addressbook");
 				if ( adb != null ) {
+					boolean reachable = false;
+					Enumeration en = adb.keys();
+					while ( en.hasMoreElements() ) {
+						String key = (String) en.nextElement();
+						try {
+							URL url = new URL(key);
+							HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+							conn.setConnectTimeout(5000);
+							conn.setReadTimeout(5000);
+							conn.setRequestMethod("GET");
+							conn.connect();
+							int code = conn.getResponseCode();
+							if ( code >= 200 && code < 400 ) {
+								reachable = true;
+								conn.disconnect();
+								break;
+							}
+							conn.disconnect();
+						} catch ( Exception e ) {
+							log("addressbook not reachable: "+ key);
+						}
+					}
+					if ( !reachable ) {
+						log("addressbooks configured but none reachable, skipping");
+					} else {
 					say( texte.get( config.get("conf","lang_short"), "READPHONEBOOK") );
 
-					Enumeration en = adb.keys();
+					en = adb.keys();
 					while ( en.hasMoreElements() ) {
 						String key = (String) en.nextElement();
 						String value = adb.get(key);
@@ -1275,6 +1304,7 @@ public class PVA {
 						}
 				
 					}
+					}
 				}
 	
 				vcards = "";
@@ -1619,6 +1649,8 @@ public class PVA {
 
 			if ( wort(keyword) ) {
 			
+				log("keyword found, text="+text);
+				
 				// remove everything BEFORE the keyword, because the keyword can be at any position in a recording
 				// also remove the keyword itself
 				
@@ -3328,6 +3360,7 @@ public class PVA {
 						dos.writeFile(getHome()+"/.cache/pva/cmd.last", text_raw);
 				}
 			} else {
+				log("keyword NOT found in '"+text+"', keyword='"+keyword+"'");
 				if ( ! reaction ) log("Nicht für mich gedacht:" + text);
 			}
 	}
diff --git a/start-local.sh b/start-local.sh
new file mode 100755
index 0000000..e582b6f
--- /dev/null
+++ b/start-local.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+killall -9 java play 2>/dev/null
+> local/.var/log/pva.log
+CP=""
+for file in lib/*.jar; do
+	CP="${CP:+$CP:}$file"
+done
+
+CP="$CP:depends/src"
+
+NATIVE_PATH="$(cd "$(dirname "$0")" && pwd)/local/native"
+
+if [ -e plugins/files/WatchdogPlugin.java ]; then
+	CP="/usr/share/java/opencv4/opencv-4100.jar:$CP"
+	NATIVE_PATH="$NATIVE_PATH:/usr/lib/jni"
+fi
+
+mkdir -p local/.cache/pva local/.var/log
+
+BASE="$(cd "$(dirname "$0")" && pwd)"
+GREETING=$(grep 'conf:.*greeting' $BASE/local/.config/pva/conf.d/* 2>/dev/null | tail -n 1 | sed -e 's/^.*,//' -e 's/"//g')
+if [ -n "$GREETING" ]; then
+	PVA_CACHE="$BASE/local/.cache/pva" PVA_PIPER="$BASE/venv/bin/piper" PVA_PIPER_VOICES="$BASE/local/piper-voices" \
+		"$BASE/usr/local/sbin/pipersay" "$GREETING" &
+fi
+
+PVA_CACHE="$BASE/local/.cache/pva" PVA_PIPER="$BASE/venv/bin/piper" PVA_PIPER_VOICES="$BASE/local/piper-voices" \
+PATH="$BASE/usr/local/sbin:$PATH" LD_PRELOAD="" java -Djna.library.path="$NATIVE_PATH" -Djava.library.path="$NATIVE_PATH" -Dpva.home=./local -cp "$CP:." server.PVA "startserver" >> local/.var/log/pva.log 2>> local/.var/log/pva.log
diff --git a/usr/local/sbin/pipersay b/usr/local/sbin/pipersay
new file mode 100755
index 0000000..ec3803c
--- /dev/null
+++ b/usr/local/sbin/pipersay
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+CACHE="${PVA_CACHE:-$HOME/.cache/pva}/audio"
+mkdir -p "$CACHE"
+HASH=$(printf '%s' "$1" | sha256sum | cut -d' ' -f1)
+CACHED="$CACHE/$HASH.wav"
+
+if [ -f "$CACHED" ]; then
+        play "$CACHED" tempo "${PVA_TEMPO:-1.0}" 2>/dev/null
+else
+    NAME=$(mktemp /tmp/piper.XXXXXX.wav)
+    if [ -n "$NAME" ]; then
+        printf '%s' "$1" | "${PVA_PIPER:-piper}" \
+            -m "${PVA_PIPER_VOICES}/de_DE-kerstin-low.onnx" \
+            -c "${PVA_PIPER_VOICES}/de_DE-kerstin-low.onnx.json" \
+            -f "$NAME" 2>/dev/null
+        mv "$NAME" "$CACHED"
+        play "$CACHED" tempo "${PVA_TEMPO:-1.0}" 2>/dev/null
+    fi
+fi
-- 
2.47.3

