"""Channel Points runtime — viewers redeem points to make Byte speak. The 2026 flagship engagement: a viewer redeems a custom reward and Byte voices their message in persona. Redemptions to the dedicated "voice" reward are spoken (sanitized + length-capped); anything else is filed as a streamer suggestion in the risk queue. Pure/​testable core; the EventSub subscription that feeds it activates in eventsub mode with the channel:read:redemptions scope. """ from __future__ import annotations from typing import Any from bot.control_plane import control_plane from bot.control_plane_constants import RISK_SUGGEST_STREAMER from bot.voice_runtime import voice_runtime MAX_REDEMPTION_CHARS = 200 _VOICE_REWARD_MARKERS = ("byte fala", "byte fale", "fala byte", "byte tts", "byte speak") def is_voice_reward(reward_title: str) -> bool: lowered = (reward_title or "").strip().lower() return any(marker in lowered for marker in _VOICE_REWARD_MARKERS) def _sanitize(text: str) -> str: return " ".join((text or "").split())[:MAX_REDEMPTION_CHARS] def process_redemption(user: str, reward_title: str, user_input: str = "") -> dict[str, Any]: """Turn a Channel Points redemption into voice (voice reward) or a suggestion.""" safe_user = (user or "viewer").strip() or "viewer" message = _sanitize(user_input) if is_voice_reward(reward_title): if not message: return {"ok": False, "reason": "empty_message"} spoken = voice_runtime.say(f"{safe_user} diz: {message}", source="channel_points") return {"ok": True, "action": "voice", "spoke": spoken.get("spoke", False)} # Non-voice rewards go to the risk queue for the streamer to handle. item = control_plane.enqueue_action( kind="channel_points", risk=RISK_SUGGEST_STREAMER, title=f"Resgate de {safe_user}: {(_sanitize(reward_title) or 'recompensa')}", body=message or "(sem mensagem)", payload={"user": safe_user, "reward": _sanitize(reward_title)}, created_by="channel_points", ) return {"ok": True, "action": "queued", "item": item} __all__ = ["is_voice_reward", "process_redemption"]