====== VoxAI Plugin Examples ====== This guide provides practical, copy-and-paste examples to help you get started with the VoxAI Plugin API. All code shown here should be placed inside the main **''register(app)''** function within your plugin's ''.py'' file. For a complete list of all available functions, please see the [[functions|API Reference]]. --- ==== Example 1: Basic Chat Command (!hello) ==== This is the "Hello World" of plugins. It registers a new chat command ''!hello'' that, when used, makes VoxAI send a chat message back to the user who typed it. This example demonstrates: * ''app.register_command()'' * ''app.send_twitch_chat()'' # This is the function that will be called when someone types !hello def say_hello(args, user): # args = any text that came after the command (we ignore it here) # user = the username of the person who sent the command app.send_twitch_chat(f"@{user}, hello there! Welcome to the stream!") # This tells VoxAI to listen for "!hello" and run our "say_hello" function app.register_command("hello", say_hello) --- ==== Example 2: Streamer-Only Command (!shoutout) ==== This example demonstrates how to create a command that only the streamer can use. It uses the **''app.channel''** attribute to verify the user's identity, which is a core "Best Practice". It also uses the ''args'' parameter to read text after the command. This example demonstrates: * Checking user permissions against **''app.channel''** * Using the ''args'' parameter from a command * ''app.register_command()'' def handle_shoutout(args, user): # BEST PRACTICE: Check if the user is the streamer (channel owner) # We always compare in .lower() case to be safe. if user.lower() != app.channel.lower(): return # Silently ignore if the user is not the streamer # 'args' will be the username, e.g., if the command was "!so some_streamer", # args will be "some_streamer" target_user = args.strip() if not target_user: app.send_twitch_chat(f"@{user}, you forgot to specify a user to shout out!") return # Clean the username just in case they added an @ target_user = target_user.replace("@", "") app.send_twitch_chat(f"=== BIG SHOUTOUT === Go check out the amazing {target_user} over at https://twitch.tv/{target_user.lower()} ! They are fantastic!") # Register the command. VoxAI handles the rest. app.register_command("so", handle_shoutout) app.register_command("shoutout", handle_shoutout) # You can register multiple aliases --- ==== Example 3: Handling a Twitch Event (Raid) ==== Plugins can listen for any Twitch event, not just chat commands. This example listens for the **''raid''** event and uses TTS to speak a custom welcome message. This example demonstrates: * ''app.register_event_handler()'' * ''app.speak_text()'' (which is the easy-to-use TTS function) * ''app.add_chat()'' (to add a log message to the UI) # This function's parameters MUST match the Event Reference guide def on_raid(raider_name, viewer_count): # This function is automatically called by VoxAI when a raid occurs welcome_message = f"Welcome in to all {viewer_count} raiders from {raider_name}'s channel! Get comfortable, we're glad you're here!" # Make VoxAI speak the message aloud using the configured TTS service app.speak_text(welcome_message) # Also send a log message to the VoxAI chat window for the streamer to see app.add_chat(f"[MyPlugin] Fired custom thank you for {raider_name}'s raid.") # Register the handler to listen for the 'raid' event app.register_event_handler("raid", on_raid) --- ==== Example 4: AI Command with Threading (!ask) ==== This is a more advanced example showing the **correct** way to use blocking or long-running tasks like **''app.ask_ai()''**. You must run these tasks in a separate thread to prevent VoxAI from freezing. **This example requires an import:** Add ''import threading'' to the very top of your plugin ''.py'' file. This example demonstrates: * Using ''import threading'' (CRITICAL BEST PRACTICE) * ''app.ask_ai()'' * Handling a function in a new thread # AT THE VERY TOP OF YOUR PLUGIN .py FILE, add this line: import threading # Then, inside your register(app) function, add the following logic: def ask_ai_question(args, user): # args = the question the user asked (e.g., "what is the capital of France?") if not args: app.send_twitch_chat(f"@{user}, please ask a question after the command!") return # We can build a more detailed prompt for the AI prompt = f"A Twitch viewer named {user} has a question for you: {args}" # CRITICAL: AI calls and other network requests can take several seconds. # We MUST run them in a separate thread so they don't block the main VoxAI application. def do_ai_work(): try: app.add_chat(f"[MyPlugin] Sending prompt to AI for {user}...") # This is the blocking call. It runs safely in the thread. response = app.ask_ai(prompt) # Send the response back to chat and speak it app.send_twitch_chat(f"@{user}, {response}") app.speak_text(response) except Exception as e: print(f"[MyPluginError] Failed to ask AI: {e}") app.send_twitch_chat(f"@{user}, I had a little brain-fart and couldn't think of an answer. Sorry!") # This is the final step: start the new thread. # setting daemon=True ensures the thread will close when VoxAI closes. threading.Thread(target=do_ai_work, daemon=True).start() app.register_command("ask", ask_ai_question) --- ==== Example 5: Self-Contained Plugin with a Sound File ==== This demonstrates the "Best Practice" for creating a self-contained plugin. The plugin creates its own ''data'' folder to store an audio file, then registers a command to play it. This example assumes you have created a folder named **''my_sound_plugin_data''** inside your ''plugins'' folder, and inside that folder, you have placed a file named **''alert.mp3''**. **File Structure:** plugins/ | +- my_sound_plugin.py | +- my_sound_plugin_data/ | +- alert.mp3 This example demonstrates: * ''app.play_sound_immediately()'' * Using ''os.path'' to create a self-contained plugin (BEST PRACTICE) # AT THE VERY TOP OF YOUR PLUGIN .py FILE, add this line: import os # --- This setup code goes at the top of your register() function --- # Best Practice: Store your assets in a dedicated subfolder. # This gets the directory where your .py file is currently located. plugin_dir = os.path.dirname(__file__) # This creates a path to your data folder (e.g., "plugins/my_sound_plugin_data") data_folder = os.path.join(plugin_dir, "my_sound_plugin_data") # This creates the folder if it doesn't already exist. os.makedirs(data_folder, exist_ok=True) # Define the full path to your sound file my_alert_sound = os.path.join(data_folder, "alert.mp3") # --- Now, we define and register our command --- def play_alert(args, user): # Always check if the file exists before trying to play it! if os.path.exists(my_alert_sound): # We use play_sound_immediately so it doesn't wait for TTS to finish app.play_sound_immediately(my_alert_sound) app.send_twitch_chat("WEE-WOO WEE-WOO! Alert has been triggered!") else: # This message helps the streamer debug if they installed it wrong. app.send_twitch_chat(f"@{user}, the streamer is missing the alert.mp3 file!") app.add_chat(f"[MyPlugin] ERROR: Could not find sound file at: {my_alert_sound}") app.register_command("alert", play_alert) --- ==== Example 6: Creating a Plugin Menu Window ==== This example shows how to add your own menu item to the main "Plugins" menu in the VoxAI app. This is perfect for opening a settings or "about" window for your plugin. This example requires imports for Tkinter. Add these to the **top of your plugin file**: import tkinter as tk import customtkinter as ctk This example demonstrates: * ''app.add_plugin_menu_command()'' * Using **''app.master''** as the parent for a new ''tk.Toplevel'' window * Using ''win.grab_set()'' to make the window modal (stay on top) # --- Place this logic inside your register() function --- def open_my_about_window(): # Create a new window. Always use app.master as the parent! win = tk.Toplevel(app.master) win.title("About My Plugin") win.geometry("350x200") # This tells the window to "grab" focus and stay on top of the main app. win.transient(app.master) win.grab_set() # We can use CustomTkinter widgets to match the style of VoxAI main_frame = ctk.CTkFrame(win, fg_color="#2b2b2b") main_frame.pack(fill="both", expand=True, padx=10, pady=10) ctk.CTkLabel(main_frame, text="My Awesome Plugin!", font=("Segoe UI", 20, "bold")).pack(pady=(10, 5)) ctk.CTkLabel(main_frame, text="Version 1.0 - Made by Modder McModface").pack(pady=5) ctk.CTkButton(main_frame, text="Awesome!", command=win.destroy).pack(pady=20, side="bottom") # This one line adds the button to the "Plugins" dropdown menu in the main VoxAI app. app.add_plugin_menu_command("About My Awesome Plugin", open_my_about_window) --- ==== Example 7: Handling Power-ups (Gigantify) ==== VoxAI supports Twitch Power-ups like Gigantify, Message Effects, and On-Screen Celebrations. This example listens for these effects and tracks how many bits were spent on them. This example demonstrates: * ''app.register_event_handler()'' for the new **"powerup"** event. * Extracting cost and type data. def on_powerup(user, powerup_type, cost): # powerup_type will be "gigantify_an_emote", "message_effect", or "celebration" # cost is the integer amount of bits used (e.g. 50) clean_type = powerup_type.replace("_", " ").title() # e.g. "Gigantify An Emote" app.add_chat(f"[MyPlugin] {user} used {clean_type} for {cost} bits!") if cost > 0: # Example logic: Give the user 100 points for every bit spent new_points = cost * 100 app.send_twitch_chat(f"Wow @{user}! Thanks for the Power-up! You earned {new_points} imaginary points!") # Register the handler app.register_event_handler("powerup", on_powerup) --- ==== Example 8: Saving & Loading Settings (Persistence) ==== Your plugin might need to remember things between sessions (like a custom API key, a toggle state, or a "High Score"). Since VoxAI's main settings file is reserved for the core app, **Best Practice** is to save a local JSON file inside your plugin's data folder. This example creates a command `!setkey [value]` that saves a value to a file, and remembers it even after VoxAI restarts. import json import os # 1. Setup paths plugin_dir = os.path.dirname(__file__) data_folder = os.path.join(plugin_dir, "my_settings_data") settings_file = os.path.join(data_folder, "config.json") os.makedirs(data_folder, exist_ok=True) # 2. Global variable to hold our settings in memory my_settings = {"api_key": "default"} # 3. Helper functions to load/save def load_settings(): global my_settings if os.path.exists(settings_file): try: with open(settings_file, "r") as f: my_settings = json.load(f) except: print("[MyPlugin] Error loading settings, using defaults.") def save_settings(): with open(settings_file, "w") as f: json.dump(my_settings, f, indent=4) # Load immediately when plugin starts load_settings() # 4. Command to change the setting def set_api_key(args, user): if not args: app.send_twitch_chat(f"@{user}, please provide a key! usage: !setkey 12345") return # Update global variable my_settings["api_key"] = args.strip() # Save to disk save_settings() app.send_twitch_chat(f"@{user}, API Key updated and saved!") # 5. Command to read the setting def check_key(args, user): current_key = my_settings.get("api_key", "None") app.send_twitch_chat(f"The current saved key is: {current_key}") app.register_command("setkey", set_api_key) app.register_command("checkkey", check_key) --- ==== Example 9: Passive Chat Listener (Hooks) ==== Sometimes you want the bot to react to normal chat messages **without** a `!` command. You can do this using **Chat Hooks**. **Warning:** Code in hooks runs for //every// single chat message. Keep it fast! If you need to do heavy work (like AI or API calls), spin up a thread. def my_chat_hook(user, message): # Ignore messages from the bot itself to prevent infinite loops if user.lower() == app.channel.lower(): return # Check for a keyword if "pizza" in message.lower(): # React naturally without a command app.send_twitch_chat(f"Did someone say pizza? I love pizza, @{user}!") # Advanced: React to "Good Bot" if "good bot" in message.lower(): app.speak_text(f"Thank you {user}, I try my best.") # Register the hook # Note: We append to the list, we don't use a register function. app.plugin_chat_hooks.append(my_chat_hook) --- ==== Example 10: Periodic Timers (Loops) ==== If you want your plugin to do something automatically every X minutes (like a "Hydrate" reminder), you need a threaded loop. **Important:** You must check `getattr(app, "_shutdown", False)` in your loop. This ensures the thread stops running when you close VoxAI. import threading import time def hydration_timer(): interval_seconds = 1800 # 30 minutes # Loop forever until VoxAI shuts down while not getattr(app, "_shutdown", False): # Sleep in small chunks so we can exit quickly if the app closes for _ in range(interval_seconds): if getattr(app, "_shutdown", False): return time.sleep(1) # Time is up, do the thing! if app.is_channel_live(): # Only spam chat if we are actually live app.send_twitch_chat("🥤 Time to hydrate! Take a sip of water!") # Start the timer in the background threading.Thread(target=hydration_timer, daemon=True).start() --- ==== Example 11: Universal Bit Tracking (Goal Bar Logic) ==== VoxAI 1.1.8 introduced the **"bits"** event. This event fires for **EVERY** bit usage: standard cheers, power-ups (Gigantify), Extension usage, and Combos. **Use this event** if you are building a Goal Bar, Leaderboard, or Total Counter. Do not use the legacy "cheer" or "powerup" events for accounting, or you might miss bits. def on_any_bits(user, amount): # amount will be an integer try: bit_val = int(amount) except: bit_val = 0 if bit_val > 0: # Example: Add to a global counter # global_bit_count += bit_val print(f"[MyPlugin] Tracking: {user} spent {bit_val} bits (Source: Universal).") # If this was a massive drop, maybe trigger a special alert? if bit_val >= 5000: app.send_twitch_chat(f"😱 HOLY MOLY! {user} just dropped {bit_val} bits!") # Register for the universal "bits" event app.register_event_handler("bits", on_any_bits) --- ==== Example 12: Audio Visualizer (Real-Time Volume) ==== VoxAI 1.1.7+ exposes the raw audio volume level while the bot is speaking. You can use this to animate avatars, create visualizers, or trigger lighting effects. **Note:** This event fires very rapidly (approx. 50 times per second). Keep the logic inside very simple! def on_audio_level(level): # level is a float, usually between 0.0 (Silent) and 1.0 (Loud) current_vol = float(level) # Example: Simple Threshold (Mouth Open/Closed) threshold = 0.05 if current_vol > threshold: # Set your avatar image to Open pass else: # Set your avatar image to Closed pass # Example: Print loudness (Only for testing, spams console!) # print(f"Volume: {current_vol:.3f}") app.register_event_handler("audio_level", on_audio_level)