Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pladisdev <127021507+pladisdev@users.noreply.github.com>
Co-authored-by: pladisdev <127021507+pladisdev@users.noreply.github.com>
Co-authored-by: pladisdev <127021507+pladisdev@users.noreply.github.com>
Co-authored-by: pladisdev <127021507+pladisdev@users.noreply.github.com>
Co-authored-by: pladisdev <127021507+pladisdev@users.noreply.github.com>
…4b50-93c4-0e11b13594f4 Clarify error handler ordering is correct - no changes needed
Co-authored-by: pladisdev <127021507+pladisdev@users.noreply.github.com>
Co-authored-by: pladisdev <127021507+pladisdev@users.noreply.github.com>
…47b6-b204-5bf8892748d3 Extract magic numbers to named constants in audio playback code
…4606-8149-97ceaa22873d Use platform.system() instead of sys.platform for OS detection
Extract duplicated hex opacity calculation to utility function
Extract avatar active offset magic number into configurable setting
Fix audio reference cleanup on play() failure in popup mode
Fix race condition in popup avatar lifecycle
…into development
…into development
There was a problem hiding this comment.
Pull request overview
Updates dependencies and build/config behavior across the backend, frontend, and CI to address Edge TTS compatibility and release pipeline changes.
Changes:
- Bump
edge-ttsversion and add backend logic to retry synthesis by attempting a runtimeedge-ttsupgrade on certain 403 errors. - Frontend: fix Vite ESM
__dirnamehandling and add a default/route redirect to/settings. - CI/Release: move Windows MSI build to WiX v5 and reduce Docker involvement in releases.
Reviewed changes
Copilot reviewed 5 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| requirements.txt | Bumps edge-tts pinned version. |
| frontend/vite.config.js | Defines __dirname for ESM-based Vite config path resolution. |
| frontend/src/app.jsx | Adds root route redirect to /settings. |
| frontend/package-lock.json | Updates lockfile entries from dependency refresh. |
| deployment/build_msi.py | Adjusts WiX extension detection to include global installs. |
| backend/modules/tts.py | Adds 403-triggered retry + runtime edge-tts upgrade attempt. |
| .github/workflows/build-and-release.yml | Updates WiX install to v5 and reduces Docker usage in release flow. |
Files not reviewed (1)
- frontend/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)
deployment/build_msi.py:56
check_wix_extensions()now checks global extensions, but the auto-install path still runswix extension add WixToolset.UI.wixextwithout--global(and without a version), which can diverge from the CI setup (--global WixToolset.UI.wixext/5.0.1). Align the install command with the expected scope/version so local builds match CI.
# Check both local and global extensions
result = subprocess.run("wix extension list", shell=True, capture_output=True, text=True)
global_result = subprocess.run("wix extension list --global", shell=True, capture_output=True, text=True)
if "WixToolset.UI.wixext" in result.stdout or "WixToolset.UI.wixext" in global_result.stdout:
print("WiX UI extension found")
return True
else:
print("\nWARNING: WiX UI extension not found!")
print("Installing WixToolset.UI.wixext...")
install_result = subprocess.run("wix extension add WixToolset.UI.wixext",
shell=True, capture_output=True, text=True)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import uuid | ||
| import time | ||
| import sys | ||
| import subprocess |
There was a problem hiding this comment.
subprocess is imported but not used in this module; please remove it to avoid lint/packaging noise.
| import subprocess |
| # 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 |
There was a problem hiding this comment.
This introduces a runtime pip install --upgrade edge-tts inside the server process. That can break reproducible deployments (conflicts with the pinned edge-tts==... in requirements), fail in read-only/container environments, and expands the attack surface by fetching code at runtime. Consider removing the auto-upgrade and instead surface a clear error/instruction (or gate it behind an explicit admin setting/env var and pin to an expected version).
| # 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 | |
| # Track edge-tts update attempts to avoid repeated update instructions | |
| _edge_tts_update_attempted = False | |
| async def try_update_edge_tts(): | |
| """Log a one-time instruction to update edge-tts outside the running process.""" | |
| global _edge_tts_update_attempted | |
| if _edge_tts_update_attempted: | |
| logger.info("edge-tts update already flagged this session, skipping") | |
| return False | |
| _edge_tts_update_attempted = True | |
| logger.error( | |
| "edge-tts appears incompatible, but automatic runtime upgrades are disabled. " | |
| "Please update the pinned 'edge-tts' dependency through your normal build/deploy " | |
| "process and restart the application." | |
| ) | |
| return 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...") | ||
|
|
There was a problem hiding this comment.
The _edge_tts_update_attempted guard is a shared global without synchronization. With concurrent synth requests, multiple tasks can still race into try_update_edge_tts() and run upgrades simultaneously. Consider guarding with an asyncio.Lock (or similar) to ensure only one update attempt runs at a time.
| 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...") | |
| _edge_tts_update_lock = asyncio.Lock() | |
| async def try_update_edge_tts(): | |
| """Attempt to update edge-tts package when API compatibility issues occur""" | |
| global _edge_tts_update_attempted | |
| async with _edge_tts_update_lock: | |
| 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...") | |
| # 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 |
There was a problem hiding this comment.
New 403-detection + auto-update retry logic isn’t covered by existing unit tests. Add a unit test that mocks edge_tts.Communicate.save to raise the 403 error once, asserts try_update_edge_tts() is invoked at most once per session, and verifies synthesis is retried (or that a clear error is surfaced when update fails).
| # 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 }} |
There was a problem hiding this comment.
The Docker build step is commented out, but the job still logs into GHCR, computes metadata, and later posts a PR comment claiming an image was built/published. Either disable the whole docker-build job, or gate the downstream steps/comments/artifacts on actually building/pushing the image to avoid misleading CI output.
| @@ -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 | |||
|
|
|||
There was a problem hiding this comment.
PR title indicates an edge-tts update, but this workflow change also upgrades WiX to v5 and changes release/build dependencies (including de-emphasizing Docker). If these are intentional, consider splitting them into a separate PR or updating the PR title/description so reviewers can assess CI/release changes explicitly.
✅ Windows Build SuccessfulExecutable: Build Status
Download the artifacts from the workflow run to test before merging. Once merged to |
🐳 Docker Image Built SuccessfullyImage: Test this PR with Docker:docker pull ghcr.io/pladisdev/chat-yapper:pr-5b4c76f
docker run -d \
--name chat-yapper-pr \
-p 8069:8008 \
-e TWITCH_CLIENT_ID=your_id \
-e TWITCH_CLIENT_SECRET=your_secret \
ghcr.io/pladisdev/chat-yapper:pr-5b4c76fAccess at: http://localhost:8069 The Docker image will be published to the GitHub Container Registry when merged to |
No description provided.