diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index e888bb8..595fc38 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -116,10 +116,10 @@ jobs: Write-Host "Executable size: $([math]::Round($size, 2)) MB" shell: powershell - - name: Install WiX Toolset v4 + - name: Install WiX Toolset v5 run: | - Write-Host "Installing WiX Toolset v4..." - dotnet tool install --global wix + Write-Host "Installing WiX Toolset v5..." + dotnet tool install --global wix --version 5.0.1 # Ensure WiX is in PATH $env:PATH = "$env:USERPROFILE\.dotnet\tools;$env:PATH" @@ -128,10 +128,9 @@ jobs: wix --version Write-Host "Installing WiX UI extension globally..." - wix extension add --global WixToolset.UI.wixext + wix extension add --global WixToolset.UI.wixext/5.0.1 Write-Host "Verifying UI extension..." - wix extension list wix extension list --global Write-Host "WiX installation complete" @@ -145,7 +144,16 @@ jobs: shell: powershell - name: Build MSI Installer - run: python deployment/build_msi.py + run: | + # Ensure WiX is in PATH + $env:PATH = "$env:USERPROFILE\.dotnet\tools;$env:PATH" + + # Verify WiX is accessible + wix --version + + # Build MSI + python deployment/build_msi.py + shell: powershell env: VITE_APP_VERSION: ${{ steps.get_version.outputs.app_version }} @@ -256,7 +264,7 @@ jobs: create-release: name: Create GitHub Release runs-on: ubuntu-latest - needs: [build, linux-build, docker-build] + needs: [build, linux-build] if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: @@ -286,14 +294,6 @@ jobs: name: linux-build path: ./linux-build - - name: Download Docker artifacts - if: needs.docker-build.result == 'success' - uses: actions/download-artifact@v4 - with: - name: docker-release-files - path: ./docker-release - continue-on-error: true - - name: Get MSI filename id: msi_info run: | @@ -308,17 +308,6 @@ jobs: echo "MSI file not found, using default name" fi - - name: Check Docker build status - id: docker_status - run: | - if [ "${{ needs.docker-build.result }}" == "success" ]; then - echo "docker_success=true" >> $GITHUB_OUTPUT - echo "Docker build succeeded" - else - echo "docker_success=false" >> $GITHUB_OUTPUT - echo "Docker build failed or was skipped - release will not include Docker files" - fi - - name: Create Release uses: softprops/action-gh-release@v1 with: @@ -680,19 +669,20 @@ jobs: type=raw,value=${{ steps.docker_version.outputs.docker_tag }} type=raw,value=latest,enable=${{ steps.docker_version.outputs.is_release == 'true' }} - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - APP_VERSION=${{ steps.docker_version.outputs.app_version }} + # Commented out to speed up CI - Docker build takes too long + # - name: Build and push Docker image + # uses: docker/build-push-action@v5 + # with: + # context: . + # file: ./docker/Dockerfile + # push: true + # tags: ${{ steps.meta.outputs.tags }} + # labels: ${{ steps.meta.outputs.labels }} + # platforms: linux/amd64,linux/arm64 + # cache-from: type=gha + # cache-to: type=gha,mode=max + # build-args: | + # APP_VERSION=${{ steps.docker_version.outputs.app_version }} - name: Generate docker-compose.yml for release if: steps.docker_version.outputs.is_release == 'true' @@ -979,7 +969,7 @@ jobs: build-status-check: name: Build Status Check runs-on: ubuntu-latest - needs: [build, linux-build, docker-build] + needs: [build, linux-build] if: always() steps: @@ -995,9 +985,4 @@ jobs: echo "Cannot merge to main until build succeeds" exit 1 fi - if [ "${{ needs.docker-build.result }}" == "success" ]; then - echo "[PASS] Docker build succeeded" - elif [ "${{ needs.docker-build.result }}" == "failure" ]; then - echo "[WARN] Docker build failed - continuing anyway (Docker build is optional)" - fi echo "[PASS] Required builds succeeded - safe to merge" \ No newline at end of file diff --git a/README.md b/README.md index 27c6b0e..717d3b4 100644 --- a/README.md +++ b/README.md @@ -225,18 +225,21 @@ chat-yapper/ ## Changelog -### v1.3.1 (Latest) +### v1.3.2 (Latest) + - Fix to edge-tts + +### v1.3.1 - **New Features:** - Twitch fix - Allow random avatar assignment - More fonts to select from -### v1.3.0 (Latest) +### v1.3.0 - Better control of avatar placement in Avatar Layout Editor - Select and Adjust speaking animations for crowd mode - Added idle animations for crowd mode -### v1.2.2 (Latest) +### v1.2.2 - Quick status view - Limit concurrent TTS messages - Some more twitch fixes and improved notifcations diff --git a/backend/modules/tts.py b/backend/modules/tts.py index 3293379..5ce7700 100644 --- a/backend/modules/tts.py +++ b/backend/modules/tts.py @@ -2,6 +2,8 @@ import os import uuid import time +import sys +import subprocess from dataclasses import dataclass import aiohttp import random @@ -20,6 +22,56 @@ def reset_fallback_stats(): fallback_voice_stats.clear() fallback_selection_count = 0 +# Track edge-tts update attempts to avoid repeated updates +_edge_tts_update_attempted = False + +async def try_update_edge_tts(): + """Attempt to update edge-tts package when API compatibility issues occur""" + global _edge_tts_update_attempted + + if _edge_tts_update_attempted: + logger.info("edge-tts update already attempted this session, skipping") + return False + + _edge_tts_update_attempted = True + logger.info("Attempting to update edge-tts package to fix API compatibility...") + + try: + # Run pip upgrade in subprocess + python_exe = sys.executable + result = await asyncio.create_subprocess_exec( + python_exe, "-m", "pip", "install", "--upgrade", "edge-tts", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await result.communicate() + + if result.returncode == 0: + logger.info(f"edge-tts successfully updated: {stdout.decode()}") + + # Reload the edge_tts module + try: + import importlib + global edge_tts + if edge_tts: + importlib.reload(edge_tts) + logger.info("edge-tts module reloaded successfully") + else: + import edge_tts as new_edge_tts + edge_tts = new_edge_tts + logger.info("edge-tts module imported successfully") + return True + except Exception as e: + logger.warning(f"edge-tts updated but module reload failed: {e}") + logger.info("Restart the application to use the updated edge-tts") + return False + else: + logger.error(f"edge-tts update failed: {stderr.decode()}") + return False + except Exception as e: + logger.error(f"Failed to update edge-tts: {e}") + return False + # Provider 1: MonsterAPI TTS (async, great quality) try: import aiohttp @@ -341,9 +393,28 @@ async def synth(self, job: TTSJob) -> str: outpath = os.path.join(AUDIO_DIR, f"{uuid.uuid4()}.{job.audio_format}") + # Helper function to attempt synthesis with retry logic + async def attempt_synthesis(voice_id: str, retry_on_403: bool = True): + try: + communicate = edge_tts.Communicate(job.text, voice_id) + await communicate.save(outpath) + return True + except Exception as e: + # Check if it's a 403 error (API compatibility issue) + error_str = str(e) + if "403" in error_str and "Invalid response status" in error_str and retry_on_403: + logger.warning(f"Edge TTS returned 403 error - API may need updating") + # Attempt to update edge-tts + if await try_update_edge_tts(): + logger.info("Retrying synthesis after edge-tts update...") + # Retry once after update (without further retries) + return await attempt_synthesis(voice_id, retry_on_403=False) + else: + logger.error("edge-tts update failed or requires restart. Please restart the application.") + raise + try: - communicate = edge_tts.Communicate(job.text, job.voice) - await communicate.save(outpath) + await attempt_synthesis(job.voice) except edge_tts.exceptions.NoAudioReceived as e: logger.error(f"Edge TTS NoAudioReceived error - Voice: {job.voice}, Text: '{job.text[:50]}...'") @@ -358,8 +429,7 @@ async def synth(self, job: TTSJob) -> str: if job.voice != self.voice_id: logger.warning(f"Voice '{job.voice}' appears to be invalid or deprecated. Retrying with default voice: {self.voice_id}") try: - communicate = edge_tts.Communicate(job.text, self.voice_id) - await communicate.save(outpath) + await attempt_synthesis(self.voice_id) logger.info(f"Successfully synthesized with fallback voice: {self.voice_id}") except edge_tts.exceptions.NoAudioReceived: raise RuntimeError(f"Edge TTS failed with both '{job.voice}' and fallback '{self.voice_id}'. The voices may be invalid or Edge TTS service is unavailable. Please refresh your voice list in Settings → TTS → Edge TTS.") diff --git a/backend/version.py b/backend/version.py index 31f37c3..70824b9 100644 --- a/backend/version.py +++ b/backend/version.py @@ -3,4 +3,4 @@ This file is automatically updated during CI/CD builds """ -__version__ = "1.3.1" +__version__ = "1.3.2" diff --git a/deployment/ChatYapper.wxs b/deployment/ChatYapper.wxs index 5739850..8816cc8 100644 --- a/deployment/ChatYapper.wxs +++ b/deployment/ChatYapper.wxs @@ -5,7 +5,7 @@ =6.0.0" } }, "node_modules/bidi-js": { @@ -2742,9 +2745,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001745", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", - "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "version": "1.0.30001785", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", + "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", "dev": true, "funding": [ { diff --git a/frontend/src/app.jsx b/frontend/src/app.jsx index 4ffb4d0..7f376a3 100644 --- a/frontend/src/app.jsx +++ b/frontend/src/app.jsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' import { WebSocketProvider } from './WebSocketContext' import YappersPage from './pages/YappersPage' import SettingsPage from './pages/SettingsPage' @@ -19,9 +19,9 @@ export default function App() { + } /> } /> } /> - {/* No default route - accessing root shows nothing */} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 3504497..8e2a75f 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,6 +1,9 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' +import { fileURLToPath } from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) // Load environment variables const backendPort = process.env.PORT || 8008 diff --git a/requirements.txt b/requirements.txt index 38673d3..fc5407b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ python-multipart>=0.0.6 python-dotenv>=1.0.0 # TTS and audio -edge-tts==7.2.7 +edge-tts==7.2.8 mutagen>=1.45.0 # Streaming platforms