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
✅ Windows Build SuccessfulExecutable: Build Status
Download the artifacts from the workflow run to test before merging. Once merged to |
There was a problem hiding this comment.
Pull request overview
This PR updates Chat Yapper to v1.3.3 with a Twitch channel-point “redeem filter” fix, adds new message/audio filtering options, and performs related UI/docs/build pipeline cleanup.
Changes:
- Fix Twitch redeem filtering logic and add message filtering option to skip
@usernamementions. - Add two new audio effects (underwater/muffled + vibrato) across frontend settings, backend defaults, and ffmpeg filter generation.
- Version/docs/build updates (new
CHANGELOG.md, README link, WiX v5 in CI, Docker build disabled, dependency bumps).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| requirements.txt | Bumps edge-tts pin to 7.2.8. |
| README.md | Replaces embedded changelog with link to CHANGELOG.md; adds tester credit. |
| CHANGELOG.md | Adds standalone changelog file (v1.3.0–v1.3.3). |
| backend/version.py | Bumps backend version to 1.3.3. |
| backend/modules/message_filter.py | Redeem filter logic change + new skipMentions filter. |
| backend/modules/settings_defaults.json | Adds defaults for skipMentions, underwater, vibrato. |
| backend/modules/audio_filters.py | Implements underwater and vibrato ffmpeg filter strings + random mode support. |
| backend/modules/tts.py | Adds one-time edge-tts update instruction logic and 403 handling wrapper. |
| backend/routers/config_backup.py | Changes factory reset to truncate tables instead of deleting DB file. |
| frontend/src/components/settings/MessageFiltering.jsx | Adds UI toggle for skipping @username mentions. |
| frontend/src/components/settings/AudioFiltersSettings.jsx | Adds UI for underwater/muffled + vibrato (and random-mode toggles). |
| frontend/src/app.jsx | Redirects / to /settings. |
| frontend/vite.config.js | Adds ESM-compatible __dirname handling for path aliases/build output. |
| frontend/src/pages/SettingsPage.jsx | Removes subtitle line in header. |
| frontend/src/components/settings/YouTubeIntegration.jsx | Removes card description. |
| frontend/src/components/settings/TwitchIntegration.jsx | Removes card description. |
| frontend/src/components/settings/TTSProviderTabs.jsx | Removes card description. |
| frontend/src/components/settings/PlatformIntegration.jsx | Removes card description. |
| frontend/src/components/settings/GeneralSettings.jsx | Removes card description. |
| frontend/src/components/settings/CrowdAnimationSettings.jsx | Removes card description. |
| frontend/src/components/settings/AvatarPlacementSettings.jsx | Removes card description. |
| frontend/src/components/settings/AvatarConfigurationTabs.jsx | Removes card description. |
| frontend/package-lock.json | Updates lockfile dependencies (e.g., baseline-browser-mapping, caniuse-lite). |
| deployment/ChatYapper.wxs | Bumps MSI version to 1.3.3. |
| deployment/build_msi.py | Extends WiX extension detection to include --global list. |
| .github/workflows/build-and-release.yml | Updates WiX install to v5.0.1, disables docker-build, adjusts release dependencies. |
Comments suppressed due to low confidence (1)
backend/modules/tts.py:383
try_update_edge_tts()always returnsFalse, so the “retry after update” path inattempt_synthesisis unreachable and every 403 will immediately fall through to raising. If the intent is only to emit a one-time upgrade instruction, consider removing the retry branch and moving the restart/error messaging intotry_update_edge_tts()so it’s truly one-time (right nowlogger.error("...requires restart")will still run on every 403).
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:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from modules.models import Setting, Voice, AvatarImage, TwitchAuth | ||
| with Session(engine) as session: | ||
| session.exec(delete(Setting)) | ||
| session.exec(delete(Voice)) | ||
| session.exec(delete(AvatarImage)) | ||
| session.exec(delete(TwitchAuth)) |
There was a problem hiding this comment.
factory_reset now clears only Setting/Voice/AvatarImage/TwitchAuth rows. The DB schema also includes YouTubeAuth, ProviderVoiceCache, and AvatarSlot (see backend/modules/models.py), so a “factory reset” will leave YouTube tokens, cached provider voices, and avatar slot layout behind. Consider clearing all SQLModel tables (or at least the remaining auth/cache/layout tables) to match the endpoint’s promise of deleting all user data.
| from modules.models import Setting, Voice, AvatarImage, TwitchAuth | |
| with Session(engine) as session: | |
| session.exec(delete(Setting)) | |
| session.exec(delete(Voice)) | |
| session.exec(delete(AvatarImage)) | |
| session.exec(delete(TwitchAuth)) | |
| from modules.models import ( | |
| Setting, Voice, AvatarImage, TwitchAuth, | |
| YouTubeAuth, ProviderVoiceCache, AvatarSlot, | |
| ) | |
| with Session(engine) as session: | |
| session.exec(delete(Setting)) | |
| session.exec(delete(Voice)) | |
| session.exec(delete(AvatarImage)) | |
| session.exec(delete(TwitchAuth)) | |
| session.exec(delete(YouTubeAuth)) | |
| session.exec(delete(ProviderVoiceCache)) | |
| session.exec(delete(AvatarSlot)) |
backend/modules/message_filter.py
Outdated
| # but do NOT include the reward title/name. Name-based filtering is not possible | ||
| # from IRC events alone, so we filter only on the presence of custom-reward-id. | ||
| # Exception: the built-in "Highlight My Message" reward uses msg-id=highlighted-message | ||
| # instead of custom-reward-id, so we treat that as a valid redeem too. | ||
| custom_reward_id = tags.get("custom-reward-id", "") if tags else "" | ||
| msg_id = (tags.get("msg-id", "") or "") if tags else "" | ||
| is_highlight = msg_id.lower() == "highlighted-message" | ||
|
|
||
| if not custom_reward_id and not is_highlight: | ||
| # No redeem ID means this is a regular chat message, not a channel point redeem | ||
| logger.info(f"Skipping message from {username} - not from a channel point redeem") | ||
| return False, text | ||
|
|
||
| logger.info(f"Processing channel point redeem from {username} (reward-id: {custom_reward_id or 'highlighted-message'})") | ||
|
|
There was a problem hiding this comment.
The redeem filter logic was changed to ignore allowedRedeemNames entirely and only check for the presence of custom-reward-id (plus the highlighted-message special case). The frontend/settings defaults still expose and persist twitch.redeemFilter.allowedRedeemNames, so users can configure a list that the backend will never honor. Either remove/deprecate the name allowlist in settings/UI, or switch the allowlist to something available in IRC tags (e.g., reward IDs) and enforce it here.
| # but do NOT include the reward title/name. Name-based filtering is not possible | |
| # from IRC events alone, so we filter only on the presence of custom-reward-id. | |
| # Exception: the built-in "Highlight My Message" reward uses msg-id=highlighted-message | |
| # instead of custom-reward-id, so we treat that as a valid redeem too. | |
| custom_reward_id = tags.get("custom-reward-id", "") if tags else "" | |
| msg_id = (tags.get("msg-id", "") or "") if tags else "" | |
| is_highlight = msg_id.lower() == "highlighted-message" | |
| if not custom_reward_id and not is_highlight: | |
| # No redeem ID means this is a regular chat message, not a channel point redeem | |
| logger.info(f"Skipping message from {username} - not from a channel point redeem") | |
| return False, text | |
| logger.info(f"Processing channel point redeem from {username} (reward-id: {custom_reward_id or 'highlighted-message'})") | |
| # but do NOT include the reward title/name. To preserve backend filtering support | |
| # with the existing settings shape, we enforce the allowlist using redeem | |
| # identifiers available in IRC tags rather than reward titles. | |
| # Exception: the built-in "Highlight My Message" reward uses | |
| # msg-id=highlighted-message instead of custom-reward-id, so we treat that as | |
| # a valid redeem with the identifier "highlighted-message". | |
| custom_reward_id = (tags.get("custom-reward-id", "") or "") if tags else "" | |
| msg_id = (tags.get("msg-id", "") or "") if tags else "" | |
| is_highlight = msg_id.lower() == "highlighted-message" | |
| redeem_identifier = custom_reward_id or ("highlighted-message" if is_highlight else "") | |
| if not redeem_identifier: | |
| # No redeem ID means this is a regular chat message, not a channel point redeem | |
| logger.info(f"Skipping message from {username} - not from a channel point redeem") | |
| return False, text | |
| allowed_redeem_names = redeem_filter.get("allowedRedeemNames", []) or [] | |
| normalized_allowed_redeems = { | |
| str(allowed_redeem).strip().lower() | |
| for allowed_redeem in allowed_redeem_names | |
| if str(allowed_redeem).strip() | |
| } | |
| if normalized_allowed_redeems and redeem_identifier.lower() not in normalized_allowed_redeems: | |
| logger.info( | |
| f"Skipping channel point redeem from {username} - reward-id not allowed: {redeem_identifier}" | |
| ) | |
| return False, text | |
| logger.info( | |
| f"Processing channel point redeem from {username} (reward-id: {redeem_identifier})" | |
| ) |
| @@ -180,6 +178,12 @@ def should_process_message( | |||
| if stripped.startswith('!') or stripped.startswith('/'): | |||
| logger.info(f"Skipping command message: {text[:50]}...") | |||
| return False, text | |||
|
|
|||
| # Skip messages that @mention someone | |||
| if filtering.get("skipMentions", False): | |||
| if re.search(r'@\w+', text): | |||
| logger.info(f"Skipping mention message from {username}: {text[:50]}...") | |||
| return False, text | |||
There was a problem hiding this comment.
New skipMentions filtering and the updated Twitch redeem handling are not covered by unit tests. There are existing pytest tests for this module, but none exercise should_process_message(). Adding tests for (a) redeemFilter enabled/disabled with/without custom-reward-id/highlighted-message, and (b) skipMentions on/off would help prevent regressions.
| def _has_enabled_filters(self, settings: Dict[str, Any]) -> bool: | ||
| """Check if any filters are enabled""" | ||
| return any([ | ||
| settings.get("reverb", {}).get("enabled", False), | ||
| settings.get("pitch", {}).get("enabled", False), | ||
| settings.get("speed", {}).get("enabled", False), | ||
| settings.get("underwater", {}).get("enabled", False), | ||
| settings.get("vibrato", {}).get("enabled", False), | ||
| ]) | ||
|
|
||
| def _build_filters(self, settings: Dict[str, Any]) -> List[str]: | ||
| """Build ffmpeg filter chain from settings""" | ||
| filters = [] | ||
|
|
||
| # Reverb filter | ||
| if settings.get("reverb", {}).get("enabled", False): | ||
| reverb_amount = settings.get("reverb", {}).get("amount", 50) / 100.0 # 0.0 to 1.0 | ||
| # Use freeverb for reverb effect | ||
| filters.append(f"afreqshift=shift=0,aecho=0.8:0.88:60:0.4,volume={1 + reverb_amount * 0.3}") | ||
|
|
||
| # Pitch shift | ||
| if settings.get("pitch", {}).get("enabled", False): | ||
| semitones = settings.get("pitch", {}).get("semitones", 0) # -12 to +12 | ||
| if semitones != 0: | ||
| # Use rubberband for high-quality pitch shifting (if available) | ||
| # Otherwise use asetrate for simple pitch shift | ||
| cents = semitones * 100 | ||
| filters.append(f"asetrate=44100*2^({semitones}/12),aresample=44100") | ||
|
|
||
| # Speed change (affects duration) | ||
| if settings.get("speed", {}).get("enabled", False): | ||
| speed = settings.get("speed", {}).get("multiplier", 1.0) # 0.5 to 2.0 | ||
| if speed != 1.0: | ||
| # atempo can only do 0.5 to 2.0, chain multiple for larger changes | ||
| if 0.5 <= speed <= 2.0: | ||
| filters.append(f"atempo={speed}") | ||
| elif speed < 0.5: | ||
| # Chain multiple atempo for very slow speeds | ||
| filters.append(f"atempo=0.5,atempo={speed/0.5}") | ||
| else: # speed > 2.0 | ||
| # Chain multiple atempo for very fast speeds | ||
| filters.append(f"atempo=2.0,atempo={speed/2.0}") | ||
|
|
||
|
|
||
| # Underwater effect | ||
| # Technique: low-pass filter (water absorbs high freqs) + echo (watery reverb) + chorus (wobbly sub-surface) | ||
| if settings.get("underwater", {}).get("enabled", False): | ||
| intensity = settings.get("underwater", {}).get("intensity", 50) # 0-100 | ||
| # Map intensity to low-pass cutoff: 0% = 1200 Hz (shallow), 100% = 500 Hz (deep) | ||
| freq = int(1200 - (intensity / 100.0) * 700) | ||
| filters.append( | ||
| f"lowpass=f={freq}," | ||
| f"aecho=0.8:0.88:60|80:0.5|0.4," | ||
| f"chorus=0.7:0.9:55|60:0.4|0.35:0.25|0.4:2|1.3" | ||
| ) | ||
|
|
||
| # Vibrato effect: periodic pitch modulation | ||
| if settings.get("vibrato", {}).get("enabled", False): | ||
| rate = settings.get("vibrato", {}).get("rate", 10.0) # Hz, 6.0-15.0 | ||
| depth = settings.get("vibrato", {}).get("depth", 75) / 100.0 # 0.0-1.0 | ||
| rate = max(0.1, min(20.0, float(rate))) | ||
| depth = max(0.0, min(1.0, depth)) | ||
| filters.append(f"vibrato=f={rate:.1f}:d={depth:.2f}") |
There was a problem hiding this comment.
underwater and vibrato filters were added to _has_enabled_filters(), _build_filters(), and _build_random_filters(), but the existing backend/tests/test_audio_filters.py suite doesn’t cover these new filter types (it currently asserts only reverb/pitch/speed behavior). Adding tests that assert the expected ffmpeg filter strings are produced for both deterministic and random modes would help prevent regressions.
✅ Windows Build SuccessfulExecutable: Build Status
Download the artifacts from the workflow run to test before merging. Once merged to |
✅ Windows Build SuccessfulExecutable: Build Status
Download the artifacts from the workflow run to test before merging. Once merged to |
No description provided.