203 lines
10 KiB
Python
203 lines
10 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": "google/gemini-2.5-flash-preview",
|
|
"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")
|