diff --git a/.includes/bottle.sh b/.includes/bottle.sh index c57a171..7482036 100644 --- a/.includes/bottle.sh +++ b/.includes/bottle.sh @@ -69,6 +69,77 @@ winetricks() { fi } +install_winespeak() { + local installerName="winespeak.exe" + local installerPath="${cache}/${installerName}" + local installerSha256="187d4db69f3af7c1bca1c100a04489b76732048be7c38449781da360ed7b2d66" + local wrapperPath="${WINEPREFIX}/drive_c/Program Files (x86)/espeak-ng-sapi/EspeakSAPI.dll" + local wrapperSha256="8009750dad82ca6665871814033dcee6844bb424f033130c109f47e188c5364b" + local cscriptPath="${WINEPREFIX}/drive_c/windows/syswow64/cscript.exe" + local verifyScriptPath="${WINEPREFIX}/drive_c/windows/temp/verify_winespeak.vbs" + local actualSha256 + local registryOutput + local voiceOutput + local defaultToken='HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\TokenEnums\eSpeak-NG\English (America)' + + if [[ -z "${wineSpeakInstaller:-}" ]]; then + echo "WineSpeak download URL is not configured." + return 1 + fi + + download "$wineSpeakInstaller" + actualSha256="$(sha256sum "$installerPath" | awk '{print $1}')" + if [[ "$actualSha256" != "$installerSha256" ]]; then + echo "WineSpeak installer failed its SHA-256 check." + rm -f "$installerPath" + return 1 + fi + + if ! wine "$installerPath" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-; then + echo "WineSpeak installer failed." + return 1 + fi + + if [[ ! -f "$wrapperPath" ]]; then + echo "WineSpeak did not install its 32-bit SAPI wrapper." + return 1 + fi + actualSha256="$(sha256sum "$wrapperPath" | awk '{print $1}')" + if [[ "$actualSha256" != "$wrapperSha256" ]]; then + echo "WineSpeak installed an unexpected 32-bit SAPI wrapper." + return 1 + fi + + registryOutput="$(wine reg query 'HKCU\Software\Microsoft\Speech\Voices' /v DefaultTokenId 2>/dev/null)" + if ! grep -Fq "$defaultToken" <<< "$registryOutput"; then + echo "WineSpeak did not become the default SAPI voice." + return 1 + fi + + if [[ ! -f "$cscriptPath" ]]; then + echo "Wine's 32-bit cscript.exe was not found." + return 1 + fi + mkdir -p "${verifyScriptPath%/*}" + cat > "$verifyScriptPath" <<'EOF' +dim speechobject +set speechobject=createobject("sapi.spvoice") +wscript.echo speechobject.voice.getdescription +EOF + if ! voiceOutput="$(wine "$cscriptPath" //nologo 'c:\windows\temp\verify_winespeak.vbs' 2>&1)"; then + echo "WineSpeak's 32-bit SAPI voice could not be created." + echo "$voiceOutput" + return 1 + fi + if ! grep -Fq "English (America)" <<< "$voiceOutput"; then + echo "WineSpeak's 32-bit SAPI verification returned an unexpected voice." + echo "$voiceOutput" + return 1 + fi + + echo "WineSpeak English (America) is the default SAPI voice." +} + install_rhvoice() { if [[ -d "${WINEPREFIX}/drive_c/Program Files/Olga Yakovleva/" ]]; then return diff --git a/.includes/ipfs.sh b/.includes/ipfs.sh index 6b9b90e..f70e4eb 100644 --- a/.includes/ipfs.sh +++ b/.includes/ipfs.sh @@ -7,6 +7,7 @@ declare -Ag ipfs=( [nvdaControllerClient32]="${ipfsGateway}/ipfs/QmTrRrT4QFKSkZ8ivfUawA6iJ6adEyyogccE3nLDTfSK8u?filename=nvdaControllerClient32.dll" [nvdaControllerClient64]="${ipfsGateway}/ipfs/QmaYE7RFDtwHCiXCVLcuA3esfFx6E7koidtvrck9AwPuuN?filename=nvdaControllerClient64.dll" [nvda2speechd]="${ipfsGateway}/ipfs/QmPxhoNsoFoJC7bCfioBBCcK8tEoSoYpm342z6u7KjFsVz?filename=nvda2speechd" + [winespeak]="https://stormgames.wolfe.casa/downloads/winespeak.exe" # Games (alphabetical order) [BG 15 Puzzle]="${ipfsGateway}/ipfs/QmQiocMpMXoxejDftKKvmrR5xxpj1qcWcgkhBBwTcyijXg?filename=FPB32Setup10a.exe" diff --git a/audiogame-manager.sh b/audiogame-manager.sh index 569f262..42f26bb 100755 --- a/audiogame-manager.sh +++ b/audiogame-manager.sh @@ -285,11 +285,14 @@ check_wine32() { # Ensure wine64 bottle exists with all dependencies (wine32 no longer needed!) ensure_wine_bottles() { local wine64Bottle="$HOME/.local/wine64" + local setupMarker="${wine64Bottle}/.agm-winespeak-setup-pending" # Create wine64 bottle if missing - now includes SAPI support via WINETRICKS_FORCE=1 - if [[ ! -d "$wine64Bottle" ]] || [[ ! -f "$wine64Bottle/system.reg" ]]; then + if [[ ! -d "$wine64Bottle" ]] || [[ ! -f "$wine64Bottle/system.reg" ]] || [[ -f "$setupMarker" ]]; then { echo "# Creating wine64 bottle for modern games..." + mkdir -p "$wine64Bottle" + touch "$setupMarker" # Set up environment for wine64 export WINEPREFIX="$wine64Bottle" @@ -329,19 +332,12 @@ ensure_wine_bottles() { # Restore win10 after speechsdk (speechsdk sets winxp) winetricks -q win10 - # Initialize SAPI and set Microsoft Mike as default voice - echo "# Setting Microsoft Mike as default voice..." - mkdir -p "${WINEPREFIX}/drive_c/windows/temp" - cat << "EOF" > "${WINEPREFIX}/drive_c/windows/temp/init_sapi.vbs" -dim speechobject -set speechobject=createobject("sapi.spvoice") -speechobject.speak "" -EOF - wine cscript "c:\\windows\\temp\\init_sapi.vbs" - - wine reg add "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTokenId" /t REG_SZ /d "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\MSMike" /f - wine reg add "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTTSRate" /t REG_DWORD /d "7" /f - echo "Set Microsoft Mike as default voice for wine64" + # Install and verify the default 32-bit SAPI voice. + echo "# Installing WineSpeak English (America)..." + if ! install_winespeak; then + echo "# WineSpeak installation or SAPI verification failed." + return 1 + fi # Setup nvda2speechd for accessibility if needed if ! ss -ltnp | grep 3457 | grep -q 'cthulhu'; then @@ -353,6 +349,7 @@ EOF fi fi + rm -f "$setupMarker" echo "# Wine64 bottle creation complete." } > >(agm_progressbox "Wine Bottle Setup" "Creating unified wine64 bottle with SAPI support (this may take several minutes)...") fi @@ -945,13 +942,14 @@ source "${scriptDir}/.includes/update.sh" export nvdaControllerClient32Dll="${ipfs[nvdaControllerClient32]}" export nvdaControllerClient64Dll="${ipfs[nvdaControllerClient64]}" export nvda2speechdBinary="${ipfs[nvda2speechd]}" +export wineSpeakInstaller="${ipfs[winespeak]}" # Check minimum requirements check_requirements || exit 1 # Wine32 no longer needed - all games use wine64 with SAPI support via WINETRICKS_FORCE=1 # check_wine32 # Disabled - wine32 eliminated 2025-12-06 # Ensure wine bottles exist with dependencies -ensure_wine_bottles +ensure_wine_bottles || exit 1 # Check for updates update # Get latest news if available diff --git a/speech/install_espeak_ng.sh b/speech/install_espeak_ng.sh new file mode 100755 index 0000000..70d995f --- /dev/null +++ b/speech/install_espeak_ng.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +export WINEPREFIX="${WINEPREFIX:-${HOME}/.local/wine64}" +export WINEDEBUG="${WINEDEBUG:--all}" + +installerUrl="https://stormgames.wolfe.casa/downloads/winespeak.exe" +installerName="winespeak.exe" +installerSha256="187d4db69f3af7c1bca1c100a04489b76732048be7c38449781da360ed7b2d66" +cacheDir="${XDG_CACHE_HOME:-${HOME}/.cache}/audiogame-manager" +installerPath="${cacheDir}/${installerName}" +partialPath="${installerPath}.part" +wrapperPath="${WINEPREFIX}/drive_c/Program Files (x86)/espeak-ng-sapi/EspeakSAPI.dll" +wrapperSha256="8009750dad82ca6665871814033dcee6844bb424f033130c109f47e188c5364b" +voiceToken="HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\TokenEnums\\eSpeak-NG\\English (America)" + +fail() { + printf 'Error: %s\n' "$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" > /dev/null 2>&1 || fail "Required command not found: $1" +} + +verify_sha256() { + local file="$1" + local expected="$2" + local actual + + actual="$(sha256sum "$file" | awk '{print $1}')" + [[ "$actual" == "$expected" ]] +} + +download_installer() { + mkdir -p "$cacheDir" + + if [[ -s "$installerPath" ]] && verify_sha256 "$installerPath" "$installerSha256"; then + printf 'Using cached WineSpeak installer.\n' + return + fi + + rm -f "$installerPath" "$partialPath" + printf 'Downloading WineSpeak...\n' + if ! curl -L4 --fail --retry 3 --output "$partialPath" "$installerUrl"; then + rm -f "$partialPath" + fail 'Could not download WineSpeak.' + fi + + if ! verify_sha256 "$partialPath" "$installerSha256"; then + rm -f "$partialPath" + fail 'The downloaded WineSpeak installer failed its SHA-256 check.' + fi + + mv "$partialPath" "$installerPath" +} + +install_winespeak() { + local registryOutput + + download_installer + + printf 'Installing WineSpeak into %s...\n' "$WINEPREFIX" + if ! wine "$installerPath" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-; then + fail 'WineSpeak installation failed.' + fi + + if [[ ! -f "$wrapperPath" ]]; then + fail 'WineSpeak did not install its 32-bit SAPI wrapper.' + fi + if ! verify_sha256 "$wrapperPath" "$wrapperSha256"; then + fail 'WineSpeak installed an unexpected 32-bit SAPI wrapper.' + fi + + printf 'Setting English (America) as the default SAPI voice...\n' + if ! wine reg add 'HKCU\Software\Microsoft\Speech\Voices' \ + /v DefaultTokenId /t REG_SZ /d "$voiceToken" /f > /dev/null; then + fail 'Could not set the default SAPI voice.' + fi + + registryOutput="$(wine reg query 'HKCU\Software\Microsoft\Speech\Voices' /v DefaultTokenId 2>/dev/null)" + if ! grep -Fq "$voiceToken" <<< "$registryOutput"; then + fail 'Could not verify the default SAPI voice.' + fi + + printf 'WineSpeak is installed and English (America) is the default SAPI voice.\n' +} + +for commandName in curl wine sha256sum awk grep; do + require_command "$commandName" +done + +install_winespeak