diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 595fc38..92ab099 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -617,6 +617,7 @@ jobs: docker-build: name: Build and Push Docker Image runs-on: ubuntu-latest + if: false # Disabled - Docker build step is commented out; re-enable when build-push-action is restored continue-on-error: true needs: build # Run after Windows build succeeds diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ca3f0bb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog + +All notable changes to Chat Yapper are documented here. + +## v1.3.3 (Latest) +- twitch redeem fix +- two new voice effects +- new filter for @user messages + +## v1.3.2 +- Fix to edge-tts + +## v1.3.1 +- **New Features:** + - Twitch fix + - Allow random avatar assignment + - More fonts to select from + +## 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 +- Quick status view +- Limit concurrent TTS messages +- Some more twitch fixes and improved notifications + +## v1.2.1 +- Usernames for chat bubbles +- Text size adjustment +- Toggle for only allowing redeem messages for Twitch +- Twitch token refresh (should fix auth issues) + +## v1.2.0 +- Chat bubbles above avatars +- Pop-up mode for avatars +- Linux standalone build (x64) +- Improved audio quality (reduced crackling) +- Docker multi-architecture support (amd64, arm64) +- Better audio preloading and buffering +- High-quality ffmpeg audio processing +- GitHub Container Registry (GHCR) for Docker images +- Automated cross-platform builds via GitHub Actions + +## v1.1.2 +- Stability fixes +- MSI installation for Windows +- Light mode theme +- Cleaned up settings UI + +## v1.0.0 +- GIF and WebP support for animated avatars +- Customizable speaking glow effects (color, opacity, size, enable/disable) +- Message history and replay system (stores 100 recent messages) +- Export/Import configuration system (backup/restore settings, voices, and avatars) +- Persistent voice caching for all TTS providers (MonsterTTS, Google Cloud, Amazon Polly, Edge TTS) +- Audio filters +- Docker Support + +## v0.1.0 +- Initial release +- Avatars page +- Settings page +- TTS selection +- Avatar positioning +- Basic Twitch integration diff --git a/README.md b/README.md index 717d3b4..58e1827 100644 --- a/README.md +++ b/README.md @@ -225,64 +225,7 @@ chat-yapper/ ## Changelog -### 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 - - 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 - - Quick status view - - Limit concurrent TTS messages - - Some more twitch fixes and improved notifcations - -### v1.2.1 - - usernames for chatbubbles - - text size adjustment - - Toggle for only allowing redeem messages for twitch - - twitch token refresh (should fix auth issues) - -### v1.2.0 -- Chat bubbles above avatars -- Pop-up mode for avatars -- Linux standalone build (x64) -- Improved audio quality (reduced crackling) -- Docker multi-architecture support (amd64, arm64) -- Better audio preloading and buffering -- High-quality ffmpeg audio processing -- GitHub Container Registry (GHCR) for Docker images -- Automated cross-platform builds via GitHub Actions - -### v1.1.2 -- Stability fixes -- MSI installation for Windows -- Light mode theme -- Cleaned up settings UI - -### v1.0.0 -- GIF and WebP support for animated avatars -- Customizable speaking glow effects (color, opacity, size, enable/disable) -- Message history and replay system (stores 100 recent messages) -- Export/Import configuration system (backup/restore settings, voices, and avatars) -- Persistent voice caching for all TTS providers (MonsterTTS, Google Cloud, Amazon Polly, Edge TTS) -- Audio filters -- Docker Support - -### v0.1.0 -- Initial release -- Avatars page -- Settings page -- TTS selection -- Avatar positioning -- Basic Twitch integration +See [CHANGELOG.md](CHANGELOG.md) for the full version history. --- @@ -319,6 +262,7 @@ This application was inspired by the work done by [shindigs](https://x.com/shind Special thanks these streamers that helped test the early prototype: - [**Inislein**](https://x.com/iniskein) - [**Kirana**](https://x.com/KiranaYonome) +- [**Miaelou**](https://x.com/Miaelou_VT) --- diff --git a/backend/modules/audio_filters.py b/backend/modules/audio_filters.py index baf2719..87c5425 100644 --- a/backend/modules/audio_filters.py +++ b/backend/modules/audio_filters.py @@ -178,6 +178,8 @@ def _has_enabled_filters(self, settings: Dict[str, Any]) -> bool: 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]: @@ -212,7 +214,27 @@ def _build_filters(self, settings: Dict[str, Any]) -> List[str]: 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}") + return filters def _build_random_filters(self, settings: Dict[str, Any]) -> List[str]: @@ -228,6 +250,11 @@ def _build_random_filters(self, settings: Dict[str, Any]) -> List[str]: if settings.get("speed", {}).get("randomEnabled", True): available_filters.append("speed") + if settings.get("underwater", {}).get("randomEnabled", True): + available_filters.append("underwater") + if settings.get("vibrato", {}).get("randomEnabled", True): + available_filters.append("vibrato") + if not available_filters: logger.warning("No effects enabled for random mode") return filters @@ -239,7 +266,23 @@ def _build_random_filters(self, settings: Dict[str, Any]) -> List[str]: selected = random.sample(available_filters, num_filters) for filter_type in selected: - if filter_type == "reverb": + if filter_type == "underwater": + intensity = random.randint(30, 80) + 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" + ) + logger.debug(f"Random underwater: intensity={intensity}% (cutoff={freq} Hz)") + + elif filter_type == "vibrato": + rate = round(random.uniform(6.0, 15.0), 1) + depth = round(random.uniform(0.5, 1.0), 2) + filters.append(f"vibrato=f={rate}:d={depth}") + logger.debug(f"Random vibrato: rate={rate} Hz, depth={depth}") + + elif filter_type == "reverb": # Get custom range or use defaults reverb_config = settings.get("reverb", {}) random_range = reverb_config.get("randomRange", {"min": 20, "max": 80}) diff --git a/backend/modules/message_filter.py b/backend/modules/message_filter.py index a18f35b..17eb630 100644 --- a/backend/modules/message_filter.py +++ b/backend/modules/message_filter.py @@ -138,33 +138,44 @@ def should_process_message( Returns: (should_process, filtered_text) - tuple indicating if message should be processed and the filtered text """ - filtering = settings.get("messageFiltering", {}) - - if not filtering.get("enabled", True): - return True, text - - # Check Twitch channel point redeem filter + # Check Twitch channel point redeem filter first — this applies regardless of + # whether general message filtering is enabled or disabled. twitch_settings = settings.get("twitch", {}) redeem_filter = twitch_settings.get("redeemFilter", {}) if redeem_filter.get("enabled", False): - allowed_redeem_names = redeem_filter.get("allowedRedeemNames", []) - if allowed_redeem_names: - # Check if message has a msg-param-reward-name tag (the redeem name) - # Also check custom-reward-id to confirm it's a redeem - custom_reward_id = tags.get("custom-reward-id", "") if tags else "" - reward_name = tags.get("msg-param-reward-name", "") if tags else "" - - if not custom_reward_id: - # No redeem ID means this is a regular message, not a channel point redeem - logger.info(f"Skipping message from {username} - not from a channel point redeem") - return False, text - - # Check if the redeem name is in the allowed list (case-insensitive) - if not any(reward_name.lower() == allowed_name.lower() for allowed_name in allowed_redeem_names): - logger.info(f"Skipping message from {username} - redeem name '{reward_name}' not in allowed list") - return False, text - - logger.info(f"Processing message from {username} - redeem name '{reward_name}' is allowed") + # Twitch IRC PRIVMSG tags include custom-reward-id (UUID) for channel point redeems, + # but do NOT include the reward title/name. + # 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 = { + str(r).strip().lower() + for r in allowed_redeem_names + if str(r).strip() + } + if normalized_allowed and redeem_identifier.lower() not in normalized_allowed: + logger.info( + f"Skipping channel point redeem from {username} - reward ID not in allowlist: {redeem_identifier}" + ) + return False, text + + logger.info(f"Processing channel point redeem from {username} (reward-id: {redeem_identifier})") + + filtering = settings.get("messageFiltering", {}) + + if not filtering.get("enabled", True): + return True, text # Skip ignored users if username and filtering.get("ignoredUsers"): @@ -180,6 +191,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 # Start with original text, apply filters progressively filtered_text = text diff --git a/backend/modules/persistent_data.py b/backend/modules/persistent_data.py index cfd6b93..b4c4312 100644 --- a/backend/modules/persistent_data.py +++ b/backend/modules/persistent_data.py @@ -51,7 +51,7 @@ def get_user_data_dir(): TWITCH_CLIENT_ID = get_env_var("TWITCH_CLIENT_ID", "pker88pnps6l8ku90u7ggwvt9dmz2f") TWITCH_CLIENT_SECRET = get_env_var("TWITCH_CLIENT_SECRET", "") TWITCH_REDIRECT_URI = f"http://localhost:{os.environ.get('PORT', 8000)}/auth/twitch/callback" -TWITCH_SCOPE = "chat:read" +TWITCH_SCOPE = "chat:read channel:read:redemptions" # YouTube OAuth Configuration YOUTUBE_CLIENT_ID = get_env_var("YOUTUBE_CLIENT_ID", "") diff --git a/backend/modules/settings_defaults.json b/backend/modules/settings_defaults.json index 82aaff3..df68ae2 100644 --- a/backend/modules/settings_defaults.json +++ b/backend/modules/settings_defaults.json @@ -73,6 +73,7 @@ "minLength": 1, "maxLength": 500, "skipCommands": true, +"skipMentions": false, "skipEmotes": false, "removeUrls": true, "ignoredUsers": [], @@ -118,6 +119,17 @@ "min": 0.75, "max": 1.3 } +}, +"underwater": { +"enabled": false, +"intensity": 50, +"randomEnabled": true +}, +"vibrato": { +"enabled": false, +"rate": 10.0, +"depth": 75, +"randomEnabled": true } } } \ No newline at end of file diff --git a/backend/modules/tts.py b/backend/modules/tts.py index 5ce7700..78a560c 100644 --- a/backend/modules/tts.py +++ b/backend/modules/tts.py @@ -22,54 +22,25 @@ def reset_fallback_stats(): fallback_voice_stats.clear() fallback_selection_count = 0 -# Track edge-tts update attempts to avoid repeated updates +# Track edge-tts update attempts to avoid repeated update instructions _edge_tts_update_attempted = False +_edge_tts_update_lock = asyncio.Lock() async def try_update_edge_tts(): - """Attempt to update edge-tts package when API compatibility issues occur""" + """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 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()}") + + async with _edge_tts_update_lock: + if _edge_tts_update_attempted: + logger.info("edge-tts update already flagged this session, skipping") return False - except Exception as e: - logger.error(f"Failed to update edge-tts: {e}") + + _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 # Provider 1: MonsterAPI TTS (async, great quality) diff --git a/backend/routers/auth.py b/backend/routers/auth.py index c158cb9..dc8672f 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -138,6 +138,53 @@ async def twitch_auth_status(): logger.error(f"Error checking Twitch status: {e}") return {"connected": False, "error": str(e)} +@router.get("/api/twitch/redeems") +async def get_twitch_channel_redeems(): + """Fetch channel point rewards for the authenticated broadcaster from Twitch Helix API""" + try: + token_info = await get_twitch_token_for_bot() + if not token_info: + raise HTTPException(status_code=401, detail="No Twitch account connected. Please connect your account first.") + + broadcaster_id = token_info["user_id"] + headers = { + "Authorization": f"Bearer {token_info['token']}", + "Client-Id": TWITCH_CLIENT_ID + } + + async with aiohttp.ClientSession() as session: + async with session.get( + f"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}", + headers=headers + ) as response: + if response.status == 200: + result = await response.json() + rewards = [ + {"id": r["id"], "title": r["title"], "cost": r["cost"]} + for r in result.get("data", []) + ] + return {"rewards": rewards} + elif response.status == 403: + raise HTTPException( + status_code=403, + detail="Channel points are not available. The channel may not be a Partner or Affiliate." + ) + elif response.status == 401: + raise HTTPException( + status_code=401, + detail="Twitch token missing required permissions. Please disconnect and reconnect your Twitch account to grant the 'channel:read:redemptions' scope." + ) + else: + error_text = await response.text() + logger.error(f"Twitch redeems request failed: {response.status} - {error_text}") + raise HTTPException(status_code=response.status, detail="Failed to fetch channel point rewards from Twitch.") + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching Twitch redeems: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + + @router.delete("/api/twitch/disconnect") async def twitch_disconnect(): """Disconnect Twitch account""" diff --git a/backend/routers/config_backup.py b/backend/routers/config_backup.py index 0de0ba7..eb0d129 100644 --- a/backend/routers/config_backup.py +++ b/backend/routers/config_backup.py @@ -19,9 +19,10 @@ from modules import logger from modules.persistent_data import ( get_settings, save_settings, get_all_avatars, get_voices, - add_avatar, add_voice, PERSISTENT_AVATARS_DIR, DB_PATH, USER_DATA_DIR + add_avatar, add_voice, PERSISTENT_AVATARS_DIR, DB_PATH, USER_DATA_DIR, + engine ) -from modules.models import AvatarImage, Voice +from modules.models import AvatarImage, Voice, Setting router = APIRouter() @@ -390,16 +391,20 @@ async def factory_reset(): logger.warning(f"Failed to delete avatar file {filename}: {e}") logger.info(f"✓ Deleted {avatar_files_deleted} avatar files") - # Delete database file (this removes all settings, voices, avatars metadata, auth tokens, etc.) - if os.path.exists(DB_PATH): - os.unlink(DB_PATH) - logger.info(f"✓ Deleted database: {DB_PATH}") - - # Reinitialize database with empty tables - from sqlmodel import SQLModel, create_engine - engine = create_engine(f"sqlite:///{DB_PATH}") - SQLModel.metadata.create_all(engine) - logger.info("✓ Reinitialized empty database") + # Clear all data from database tables using the existing engine connection. + # We deliberately avoid deleting the .db file because on Windows the process + # holds an open file handle (SQLAlchemy connection pool), which causes + # WinError 32 "file in use". Truncating via SQL achieves the same reset + # without touching the file itself. + from sqlmodel import Session, SQLModel, delete + 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)) + session.commit() + logger.info("✓ Cleared all database tables") logger.warning(f"✅ FACTORY RESET COMPLETE - Deleted: {settings_count} settings, {voices_count} voices, {avatars_count} avatars, {avatar_files_deleted} files") diff --git a/backend/tests/test_audio_filters.py b/backend/tests/test_audio_filters.py index bb77c0b..1be2c3b 100644 --- a/backend/tests/test_audio_filters.py +++ b/backend/tests/test_audio_filters.py @@ -141,7 +141,9 @@ def test_build_random_filters_no_chance(self, audio_processor): settings = { 'reverb': {'randomEnabled': False}, 'pitch': {'randomEnabled': False}, - 'speed': {'randomEnabled': False} + 'speed': {'randomEnabled': False}, + 'underwater': {'randomEnabled': False}, + 'vibrato': {'randomEnabled': False} } filters = audio_processor._build_random_filters(settings) diff --git a/backend/version.py b/backend/version.py index 70824b9..c349705 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.2" +__version__ = "1.3.3" diff --git a/deployment/ChatYapper.wxs b/deployment/ChatYapper.wxs index 8816cc8..5562aa3 100644 --- a/deployment/ChatYapper.wxs +++ b/deployment/ChatYapper.wxs @@ -5,7 +5,7 @@ { @@ -203,6 +205,110 @@ export default function AudioFiltersSettings({ settings, updateSettings }) { )} + + + + {/* Underwater Filter */} +
+
+
+ +

+ Makes the voice sound like it's coming from another room +

+
+ updateFilter('underwater', { enabled: checked })} + /> +
+ + {audioFilters.underwater?.enabled && ( +
+ + updateFilter('underwater', { intensity: value })} + className="w-full" + /> +

+ Low = slightly muffled, High = deep underwater +

+
+ )} +
+ + + + {/* Vibrato Filter */} +
+
+
+ +

+ Periodic pitch wobble — wavy, singing vibrato effect +

+
+ updateFilter('vibrato', { enabled: checked })} + /> +
+ + {audioFilters.vibrato?.enabled && ( +
+
+ + updateFilter('vibrato', { rate: value / 10 })} + className="w-full" + /> +

+ Speed of pitch oscillations (6 Hz = subtle, 15 Hz = intense) +

+
+
+ + updateFilter('vibrato', { depth: value })} + className="w-full" + /> +

+ How much the pitch varies (50% = subtle, 100% = extreme) +

+
+
+ )} +
)} @@ -385,6 +491,48 @@ export default function AudioFiltersSettings({ settings, updateSettings }) { )} + + + + {/* Random Muffled / Another Room Settings */} +
+
+
+ +

+ Enable muffled effect for random mode +

+
+ updateFilter('underwater', { randomEnabled: checked })} + /> +
+
+ + + + {/* Random Vibrato Settings */} +
+
+
+ +

+ Enable vibrato for random mode +

+
+ updateFilter('vibrato', { randomEnabled: checked })} + /> +
+
)} diff --git a/frontend/src/components/settings/AvatarConfigurationTabs.jsx b/frontend/src/components/settings/AvatarConfigurationTabs.jsx index f1ded2b..e655340 100644 --- a/frontend/src/components/settings/AvatarConfigurationTabs.jsx +++ b/frontend/src/components/settings/AvatarConfigurationTabs.jsx @@ -18,7 +18,6 @@ function AvatarConfigurationTabs({ settings, updateSettings, apiUrl, managedAvat Avatar Configuration - Configure avatar placement and visual effects diff --git a/frontend/src/components/settings/AvatarPlacementSettings.jsx b/frontend/src/components/settings/AvatarPlacementSettings.jsx index e2f32f5..2999469 100644 --- a/frontend/src/components/settings/AvatarPlacementSettings.jsx +++ b/frontend/src/components/settings/AvatarPlacementSettings.jsx @@ -16,9 +16,6 @@ function AvatarPlacementSettings({ settings, updateSettings, apiUrl }) { Avatar Display Mode - - Choose how avatars appear and configure mode-specific settings -
diff --git a/frontend/src/components/settings/CrowdAnimationSettings.jsx b/frontend/src/components/settings/CrowdAnimationSettings.jsx index 222e7df..e1cfcf3 100644 --- a/frontend/src/components/settings/CrowdAnimationSettings.jsx +++ b/frontend/src/components/settings/CrowdAnimationSettings.jsx @@ -27,9 +27,6 @@ function CrowdAnimationSettings({ settings, onUpdate }) { Crowd Mode Animations - - Customize how avatars animate in crowd mode - diff --git a/frontend/src/components/settings/GeneralSettings.jsx b/frontend/src/components/settings/GeneralSettings.jsx index 74b5830..9b41ccf 100644 --- a/frontend/src/components/settings/GeneralSettings.jsx +++ b/frontend/src/components/settings/GeneralSettings.jsx @@ -52,7 +52,6 @@ function GeneralSettings({ settings, setSettings, updateSettings, apiUrl }) { General Settings - Configure TTS control, audio volume, and message limits
diff --git a/frontend/src/components/settings/MessageFiltering.jsx b/frontend/src/components/settings/MessageFiltering.jsx index da90b89..636bcae 100644 --- a/frontend/src/components/settings/MessageFiltering.jsx +++ b/frontend/src/components/settings/MessageFiltering.jsx @@ -293,7 +293,6 @@ function MessageFiltering({ settings, updateSettings, apiUrl }) { Message Filtering - Control which messages get processed for TTS {/* Enable Message Filtering Toggle - Always Visible */} @@ -410,6 +409,22 @@ function MessageFiltering({ settings, updateSettings, apiUrl }) {
+
+ updateSettings({ + messageFiltering: { + ...settings.messageFiltering, + skipMentions: checked + } + })} + /> + +
+
Platform Integrations - Connect to streaming platforms to enable chat TTS diff --git a/frontend/src/components/settings/TTSProviderTabs.jsx b/frontend/src/components/settings/TTSProviderTabs.jsx index baf9061..4cd8d6e 100644 --- a/frontend/src/components/settings/TTSProviderTabs.jsx +++ b/frontend/src/components/settings/TTSProviderTabs.jsx @@ -157,7 +157,6 @@ function TTSProviderTabs({ settings, updateSettings, apiUrl = '' }) { TTS Provider Settings - Configure your text-to-speech providers diff --git a/frontend/src/components/settings/TwitchIntegration.jsx b/frontend/src/components/settings/TwitchIntegration.jsx index 001393e..962056e 100644 --- a/frontend/src/components/settings/TwitchIntegration.jsx +++ b/frontend/src/components/settings/TwitchIntegration.jsx @@ -12,76 +12,104 @@ import { AlertTriangle } from 'lucide-react' -function RedeemNamesManager({ redeemNames, onUpdate }) { - const [newRedeem, setNewRedeem] = useState('') +function RedeemNamesManager({ redeemNames, onUpdate, apiUrl = '' }) { + const [rewards, setRewards] = useState([]) + const [loadingRewards, setLoadingRewards] = useState(false) + const [rewardsError, setRewardsError] = useState(null) - const addRedeem = () => { - const redeemName = newRedeem.trim() - if (!redeemName) return - - if (redeemNames.some(name => name.toLowerCase() === redeemName.toLowerCase())) { - alert('This redeem name is already in the list') - return + const fetchRewards = async () => { + setLoadingRewards(true) + setRewardsError(null) + try { + const response = await fetch(`${apiUrl}/api/twitch/redeems`) + if (!response.ok) { + const err = await response.json().catch(() => ({})) + throw new Error(err.detail || `HTTP ${response.status}`) + } + const data = await response.json() + setRewards(data.rewards || []) + } catch (e) { + setRewardsError(e.message) + } finally { + setLoadingRewards(false) } - - onUpdate([...redeemNames, redeemName]) - setNewRedeem('') } - const removeRedeem = (redeemToRemove) => { - onUpdate(redeemNames.filter(name => name !== redeemToRemove)) + const toggleReward = (id) => { + if (redeemNames.includes(id)) { + onUpdate(redeemNames.filter(r => r !== id)) + } else { + onUpdate([...redeemNames, id]) + } } - const clearAllRedeems = () => { - if (redeemNames.length === 0) return - if (confirm(`Are you sure you want to remove all ${redeemNames.length} redeem names?`)) { - onUpdate([]) - } + const labelFor = (id) => { + if (id === 'highlighted-message') return 'Highlight My Message (built-in)' + const match = rewards.find(r => r.id === id) + return match ? match.title : id } return (
-
- setNewRedeem(e.target.value)} - onKeyPress={e => e.key === 'Enter' && addRedeem()} - /> +
+ {rewardsError && ( + {rewardsError} + )}
+ {rewards.length > 0 && ( +
+ + {rewards.map(reward => ( + + ))} +
+ )} + {redeemNames.length > 0 && ( -
+
- - Allowed Redeems ({redeemNames.length}) - + Filtering to {redeemNames.length} reward{redeemNames.length !== 1 ? 's' : ''} + >Allow All
-
- {redeemNames.map((name, index) => ( -
- {name} + {redeemNames.map((id) => ( +
+ {labelFor(id)} @@ -91,11 +119,7 @@ function RedeemNamesManager({ redeemNames, onUpdate }) {
)} - {redeemNames.length === 0 && ( -

- No redeem names added yet -

- )} +
) } @@ -258,11 +282,18 @@ function TwitchIntegration({ settings, updateSettings, apiUrl = '' }) { }; const updateChannel = () => { - updateSettings({ - twitch: { - ...settings.twitch, - channel: channelInput.trim() - } + const newChannel = channelInput.trim() + const isOwnChannel = newChannel.toLowerCase() === (twitchStatus?.username || '').toLowerCase() + updateSettings({ + twitch: { + ...settings.twitch, + channel: newChannel, + // Disable redeem filter when switching to a channel that isn't the connected account + redeemFilter: { + ...settings.twitch?.redeemFilter, + enabled: isOwnChannel ? (settings.twitch?.redeemFilter?.enabled ?? false) : false + } + } }); }; @@ -273,9 +304,6 @@ function TwitchIntegration({ settings, updateSettings, apiUrl = '' }) { Twitch Integration - - Connect to your Twitch account to enable chat TTS - {/* Authentication Error Display */} @@ -428,52 +456,61 @@ function TwitchIntegration({ settings, updateSettings, apiUrl = '' }) {
{/* Channel Point Redeem Filter */} -
-
-
- -
- updateSettings({ - twitch: { - ...settings.twitch, - redeemFilter: { - ...settings.twitch?.redeemFilter, - enabled: checked - } - } - })} - /> -
- - {settings.twitch?.redeemFilter?.enabled && ( -
- - updateSettings({ - twitch: { - ...settings.twitch, - redeemFilter: { - ...settings.twitch?.redeemFilter, - allowedRedeemNames: names + {(() => { + const isOwnChannel = (settings.twitch?.channel || '').toLowerCase() === (twitchStatus?.username || '').toLowerCase() + return ( +
+
+
+ + {!isOwnChannel && ( +

+ Only available when monitoring your own channel (@{twitchStatus?.username}) +

+ )} +
+ updateSettings({ + twitch: { + ...settings.twitch, + redeemFilter: { + ...settings.twitch?.redeemFilter, + enabled: checked + } } - } - })} - /> -

- Enter the exact names of your channel point rewards. Names are case-insensitive. - You can find reward names in your Twitch Dashboard under Channel Points. -

+ })} + /> +
+ + {settings.twitch?.redeemFilter?.enabled && ( +
+ + updateSettings({ + twitch: { + ...settings.twitch, + redeemFilter: { + ...settings.twitch?.redeemFilter, + allowedRedeemNames: names + } + } + })} + apiUrl={apiUrl} + /> +

+ Leave empty to allow all channel point redeems, or select specific rewards to restrict to. +

+
+ )}
- )} -
+ ) + })()}
)}
diff --git a/frontend/src/components/settings/YouTubeIntegration.jsx b/frontend/src/components/settings/YouTubeIntegration.jsx index 5c6df2d..d711d38 100644 --- a/frontend/src/components/settings/YouTubeIntegration.jsx +++ b/frontend/src/components/settings/YouTubeIntegration.jsx @@ -100,9 +100,6 @@ function YouTubeIntegration({ settings, updateSettings, apiUrl = '' }) { YouTube Integration - - Connect to your YouTube account to enable live chat TTS - {!youtubeStatus?.connected ? ( diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx index 3f9f8f7..dd5e3df 100644 --- a/frontend/src/pages/SettingsPage.jsx +++ b/frontend/src/pages/SettingsPage.jsx @@ -186,7 +186,6 @@ export default function SettingsPage() { Chat Yapper Chat Yapper Settings -

Configure your voice avatar TTS system

{/* Global Stop TTS Button */} @@ -666,6 +665,25 @@ function AboutSection({ apiUrl }) {
+
+
+
+

Website

+

+ Check out other projects and updates +

+ + pladis.dev + +
+
+
+