discordbot/cogs/owoify_cog.py
2025-06-05 21:31:06 -06:00

287 lines
11 KiB
Python

import discord
from discord.ext import commands
from discord import app_commands
import re
import random
import os
import aiohttp
# In-memory conversation history for owo AI (keyed by channel id)
_owo_conversations = {}
def _owoify_text(text: str) -> str:
"""Improved owoification with more rules and randomness."""
# Basic substitutions
text = re.sub(r"[rl]", "w", text)
text = re.sub(r"[RL]", "W", text)
text = re.sub(r"n([aeiou])", r"ny\1", text)
text = re.sub(r"N([aeiou])", r"Ny\1", text)
text = re.sub(r"N([AEIOU])", r"NY\1", text)
text = re.sub(r"ove", "uv", text)
text = re.sub(r"OVE", "UV", text)
# Extra substitutions
text = re.sub(
r"\bth",
lambda m: "d" if random.random() < 0.5 else "f",
text,
flags=re.IGNORECASE,
)
text = re.sub(r"\bthe\b", "da", text, flags=re.IGNORECASE)
text = re.sub(r"\bthat\b", "dat", text, flags=re.IGNORECASE)
text = re.sub(r"\bthis\b", "dis", text, flags=re.IGNORECASE)
text = re.sub(r"\bthose\b", "dose", text, flags=re.IGNORECASE)
text = re.sub(r"\bthere\b", "dere", text, flags=re.IGNORECASE)
text = re.sub(
r"\bhere\b", "here", text, flags=re.IGNORECASE
) # Intentionally no change, for variety
text = re.sub(r"\bwhat\b", "whut", text, flags=re.IGNORECASE)
text = re.sub(r"\bwhen\b", "wen", text, flags=re.IGNORECASE)
text = re.sub(r"\bwhere\b", "whewe", text, flags=re.IGNORECASE)
text = re.sub(r"\bwhy\b", "wai", text, flags=re.IGNORECASE)
text = re.sub(r"\bhow\b", "hau", text, flags=re.IGNORECASE)
text = re.sub(r"\bno\b", "nu", text, flags=re.IGNORECASE)
text = re.sub(r"\bhas\b", "haz", text, flags=re.IGNORECASE)
text = re.sub(r"\bhave\b", "haz", text, flags=re.IGNORECASE)
text = re.sub(
r"\byou\b",
lambda m: "u" if random.random() < 0.5 else "yu",
text,
flags=re.IGNORECASE,
)
text = re.sub(r"\byour\b", "ur", text, flags=re.IGNORECASE)
text = re.sub(r"tion\b", "shun", text, flags=re.IGNORECASE)
text = re.sub(r"ing\b", "in", text, flags=re.IGNORECASE)
# Playful punctuation
text = re.sub(
r"!",
lambda m: random.choice(["!!1!", "! UwU", "! owo", "!! >w<", "! >//<", "!!?!"]),
text,
)
text = re.sub(r"\?", lambda m: random.choice(["?? OwO", "? uwu", "?"]), text)
text = re.sub(
r"\.", lambda m: random.choice(["~", ".", " ^w^", " o.o", " ._."]), text
)
# Stutter (probabilistic, only for words with at least 2 letters)
def stutter_word(match):
word = match.group(0)
if (
len(word) > 2 and random.random() < 0.33 and word[0].isalpha()
): # Increased probability
return f"{word[0]}-{word}"
return word
text = re.sub(r"\b\w+\b", stutter_word, text)
# Random interjection insertion (after commas or randomly)
interjections = [
" owo",
" uwu",
" >w<",
" ^w^",
" OwO",
" UwU",
" >.<",
" XD",
" nyaa~",
":3",
"(^///^)",
"(ᵘʷᵘ)",
"(・`ω´・)",
";;w;;",
" teehee",
" hehe",
" x3",
" rawr",
"*nuzzles*",
"*pounces*",
]
parts = re.split(r"([,])", text)
for i in range(len(parts)):
if parts[i] == "," or (
random.random() < 0.15 and parts[i].strip()
): # Increased probability
parts[i] += random.choice(interjections)
text = "".join(parts)
# Suffix
text += random.choice(interjections)
return text
async def _owoify_text_ai(text: str) -> str:
"""Owoify text using AI via OpenRouter (google/gemini-2.0-flash-exp:free)."""
return await _owoify_text_ai_with_messages(
[{"role": "user", "content": text}], system_mode="transform"
)
async def _owoify_text_ai_with_messages(messages, system_mode="transform"):
"""
Use OpenRouter AI to generate an owoified response.
system_mode: "transform" for just transforming, "reply" for replying as an owo AI.
"""
api_key = os.getenv("AI_API_KEY")
if not api_key:
raise RuntimeError("AI_API_KEY environment variable not set.")
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
if system_mode == "transform":
system_prompt = (
"You are a text transformer. Your ONLY job is to convert the user's input into an uwu/owo style of speech. "
"Do NOT reply, greet, or comment. Do NOT add any extra words, context, or explanation. "
"Return ONLY the transformed version of the input text, preserving punctuation and structure. "
"Make the output playful, creative, and expressive, using a variety of cute interjections, stuttering, and playful punctuation. "
"Never act as if you are replying to a message—just output the transformed text."
)
else:
system_prompt = (
"You are an uwu/owo style AI chatbot. Reply to the user in a playful, expressive, and cute uwu/owo style. "
"Stay in character, use lots of interjections, stuttering, and playful punctuation. "
"You are having a conversation, so respond naturally and keep the conversation going in uwu/owo style."
)
payload = {
"model": "deepseek/deepseek-chat-v3-0324:free",
"messages": [{"role": "system", "content": system_prompt}] + messages,
}
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=payload) as resp:
if resp.content_type == "application/json":
data = await resp.json()
return data["choices"][0]["message"]["content"]
else:
text = await resp.text()
raise RuntimeError(
f"OpenRouter API returned non-JSON response (status {resp.status}): {text[:500]}"
)
class OwoifyCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@app_commands.command(name="owoify", description="Owoifies your message!")
@app_commands.describe(message_to_owoify="The message to owoify")
async def owoify_slash_command(
self, interaction: discord.Interaction, message_to_owoify: str
):
"""Owoifies the provided message via a slash command."""
if not message_to_owoify.strip():
await interaction.response.send_message(
"You nyeed to pwovide some text to owoify! >w<", ephemeral=True
)
return
owo_text = _owoify_text(message_to_owoify)
await interaction.response.send_message(owo_text)
@app_commands.command(name="owoify_ai", description="Owoify your message with AI!")
@app_commands.describe(message_to_owoify="The message to owoify using AI")
async def owoify_ai_slash_command(
self, interaction: discord.Interaction, message_to_owoify: str
):
"""Owoifies the provided message via the OpenRouter AI."""
if not message_to_owoify.strip():
await interaction.response.send_message(
"You nyeed to pwovide some text to owoify! >w<", ephemeral=True
)
return
try:
owo_text = await _owoify_text_ai(message_to_owoify)
await interaction.response.send_message(owo_text)
except Exception as e:
await interaction.response.send_message(
f"AI owoification failed: {e} >w<", ephemeral=True
)
# Context menu command must be defined at module level
@app_commands.context_menu(name="Owoify Message")
async def owoify_context_menu(
interaction: discord.Interaction, message: discord.Message
):
"""Owoifies the content of the selected message and replies."""
if not message.content:
await interaction.response.send_message(
"The sewected message has no text content to owoify! >.<", ephemeral=True
)
return
original_content = message.content
owo_text = _owoify_text(original_content)
try:
await message.reply(owo_text)
await interaction.response.send_message(
"Message owoified and wepwied! uwu", ephemeral=True
)
except discord.Forbidden:
await interaction.response.send_message(
f"I couwdn't wepwy to the message (nyi Pwermissions? owo).\n"
f"But hewe's the owoified text fow you: {owo_text}",
ephemeral=True,
)
except discord.HTTPException as e:
await interaction.response.send_message(
f"Oopsie! A tiny ewwow occuwwed: {e} >w<", ephemeral=True
)
@app_commands.context_menu(name="Owoify Message (AI)")
async def owoify_context_menu_ai(
interaction: discord.Interaction, message: discord.Message
):
"""Owoifies the content of the selected message using AI and replies."""
if not message.content:
await interaction.response.send_message(
"The sewected message has no text content to owoify! >.<", ephemeral=True
)
return
original_content = message.content
try:
await interaction.response.defer(ephemeral=True)
owo_text = await _owoify_text_ai(original_content)
await message.reply(owo_text)
await interaction.followup.send(
"Message AI-owoified and wepwied! uwu", ephemeral=True
)
except discord.Forbidden:
await interaction.followup.send(
f"I couwdn't wepwy to the message (nyi Pwermissions? owo).\n"
f"But hewe's the AI owoified text fow you: {owo_text}",
ephemeral=True,
)
except Exception as e:
await interaction.followup.send(
f"AI owoification failed: {e} >w<", ephemeral=True
)
@app_commands.context_menu(name="Owo AI Reply")
async def owoify_context_menu_ai_reply(
interaction: discord.Interaction, message: discord.Message
):
"""Replies to the selected message as an owo AI."""
if not message.content:
await interaction.response.send_message(
"The sewected message has no text content to reply to! >.<", ephemeral=True
)
return
await interaction.response.defer(ephemeral=True)
convo_key = message.channel.id
convo = _owo_conversations.get(convo_key, [])
convo.append({"role": "user", "content": message.content})
try:
ai_reply = await _owoify_text_ai_with_messages(convo, system_mode="reply")
await message.reply(ai_reply)
await interaction.followup.send("AI replied in owo style! uwu", ephemeral=True)
convo.append({"role": "assistant", "content": ai_reply})
_owo_conversations[convo_key] = convo[-10:] # Keep last 10 messages
except Exception as e:
await interaction.followup.send(f"AI owo reply failed: {e} >w<", ephemeral=True)
async def setup(bot: commands.Bot):
cog = OwoifyCog(bot)
await bot.add_cog(cog)
bot.tree.add_command(owoify_context_menu)
bot.tree.add_command(owoify_context_menu_ai)
bot.tree.add_command(owoify_context_menu_ai_reply)
print("OwoifyCog loaded! uwu")