Skip to content

redeem fix#41

Merged
pladisdev merged 87 commits intomainfrom
development
Apr 7, 2026
Merged

redeem fix#41
pladisdev merged 87 commits intomainfrom
development

Conversation

@pladisdev
Copy link
Copy Markdown
Owner

No description provided.

pladisdev and others added 30 commits November 2, 2025 09:42
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
Copilot AI review requested due to automatic review settings April 7, 2026 22:31
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

✅ Windows Build Successful

Executable: ChatYapper.exe (67.02 MB)
MSI Installer: ChatYapper-1.3.3.msi (66.34 MB)
App Version: 1.3.3
Build ID: build-v1.3.3-2026.04.07-cb27367
Commit: cb27367

Build Status

  • ✅ Windows executable & MSI installer
  • 🔄 Linux build (check separate job)
  • 🔄 Docker image (check separate job)

Download the artifacts from the workflow run to test before merging.

Once merged to main, an official release will be created automatically with tag v1.3.3.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @username mentions.
  • 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 returns False, so the “retry after update” path in attempt_synthesis is 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 into try_update_edge_tts() so it’s truly one-time (right now logger.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.

Comment on lines +400 to +405
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))
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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))

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +161
# 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'})")

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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})"
)

Copilot uses AI. Check for mistakes.
Comment on lines 141 to +186
@@ -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
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 175 to +236
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}")
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

✅ Windows Build Successful

Executable: ChatYapper.exe (67.02 MB)
MSI Installer: ChatYapper-1.3.3.msi (66.34 MB)
App Version: 1.3.3
Build ID: build-v1.3.3-2026.04.07-8803028
Commit: 8803028

Build Status

  • ✅ Windows executable & MSI installer
  • 🔄 Linux build (check separate job)
  • 🔄 Docker image (check separate job)

Download the artifacts from the workflow run to test before merging.

Once merged to main, an official release will be created automatically with tag v1.3.3.

@pladisdev pladisdev merged commit bad620c into main Apr 7, 2026
8 checks passed
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

✅ Windows Build Successful

Executable: ChatYapper.exe (67.02 MB)
MSI Installer: ChatYapper-1.3.3.msi (66.34 MB)
App Version: 1.3.3
Build ID: build-v1.3.3-2026.04.07-eca4d1b
Commit: eca4d1b

Build Status

  • ✅ Windows executable & MSI installer
  • 🔄 Linux build (check separate job)
  • 🔄 Docker image (check separate job)

Download the artifacts from the workflow run to test before merging.

Once merged to main, an official release will be created automatically with tag v1.3.3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants