novelai-bot/src/main.py
wirehack7 413bc52594
Some checks failed
Safety Check / safety-check (push) Successful in -1s
Build and Push Docker Image / build (push) Successful in -9s
Code Quality Check / quality-check (push) Failing after 15s
removed antlas request
2025-05-01 14:04:39 +02:00

210 lines
6.6 KiB
Python

import io
import os
import sys
import random
import zipfile
import logging
import discord
from discord import app_commands
from discord.ext import commands
import requests
NOVELAI_API_TOKEN = os.getenv("NOVELAI_API_TOKEN")
BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
ALLOWED_CHANNEL_ID = int(os.getenv("DISCORD_CHANNEL_ID", "0")) # falls nicht gesetzt → 0
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
if not all([NOVELAI_API_TOKEN, BOT_TOKEN, ALLOWED_CHANNEL_ID]):
raise ValueError("Environment-Variablen NOVELAI_API_TOKEN, DISCORD_BOT_TOKEN und DISCORD_CHANNEL_ID müssen gesetzt sein.")
intents = discord.Intents.default()
bot = commands.Bot(command_prefix=commands.when_mentioned, intents=intents)
@bot.event
async def on_ready():
print(f"Bot started as {bot.user}")
try:
synced = await bot.tree.sync()
logging.info(f"{len(synced)} Slash-Commands synchronized.")
except Exception as e:
logging.error(e)
@bot.tree.command(name="generate", description="Generate image with NovelAI v4 Full")
@app_commands.describe(
prompt="What should be generated?",
undesired_prompt="What should be avoided? (optional)",
orientation="portrait or landscape (Standard: portrait)",
seed="Optional seed (integer). If empty, a random one will be generated"
)
async def generate(
interaction: discord.Interaction,
prompt: str,
undesired_prompt: str = "",
orientation: str = "portrait",
seed: int = None
):
default_negative = (
"blurry, lowres, error, film grain, scan artifacts, worst quality, bad quality, jpeg artifacts, "
"very displeasing, chromatic aberration, multiple views, logo, too many watermarks, white blank page, "
"blank page, watermarks, watermark, text, "
)
if interaction.channel.id != ALLOWED_CHANNEL_ID:
await interaction.response.send_message(
"This command isn't allowed here.",
ephemeral=True
)
return
MAX_PROMPT_LENGTH = 500
if len(prompt) > MAX_PROMPT_LENGTH:
await interaction.followup.send(f"Prompt too long! Maximum {MAX_PROMPT_LENGTH} chars allowed.")
return
if len(default_negative + undesired_prompt.strip()) > MAX_PROMPT_LENGTH:
await interaction.followup.send(f"Negative Prompt too long! Maximum {MAX_PROMPT_LENGTH} chars allowed.")
return
await interaction.response.defer(thinking=True)
logger.info(
"User %s (%s, %s) requested image",
interaction.user.display_name,
interaction.user.name,
interaction.user.id
)
# Auflösung wählen
if orientation.lower() == "landscape":
width, height = 1216, 832
else:
width, height = 832, 1216
if seed is None:
seed = random.randint(0, 2**32 - 1)
# API-Payload zusammenbauen
payload = {
"input": prompt,
"model": "nai-diffusion-4-full",
"action": "generate",
"parameters": {
"params_version": 3,
"width": width,
"height": height,
"scale": 3.7,
"sampler": "k_euler_ancestral",
"steps": 28,
"n_samples": 1,
"ucPreset": 0,
"qualityToggle": True,
"autoSmea": False,
"dynamic_thresholding": False,
"controlnet_strength": 1,
"legacy": False,
"add_original_image": True,
"cfg_rescale": 0,
"noise_schedule": "karras",
"legacy_v3_extend": False,
"skip_cfg_above_sigma": None,
"use_coords": False,
"legacy_uc": False,
"normalize_reference_strength_multiple": True,
"seed": seed,
"characterPrompts": [],
"v4_prompt": {
"caption": {
"base_caption": prompt,
"char_captions": []
},
"use_coords": False,
"use_order": True
},
"v4_negative_prompt": {
"caption": {
"base_caption": default_negative + undesired_prompt.strip(),
"char_captions": []
},
"legacy_uc": False
},
"negative_prompt": default_negative + undesired_prompt.strip(),
"deliberate_euler_ancestral_bug": False,
"prefer_brownian": True
}
}
headers = {
"Authorization": f"Bearer {NOVELAI_API_TOKEN}",
"Content-Type": "application/json"
}
response = requests.post(
"https://image.novelai.net/ai/generate-image",
json=payload,
headers=headers,
timeout=120
)
if response.status_code == 200:
response_bytes = response.content
param_text = f"""```Prompt: {prompt}
Undesired prompt: {default_negative + undesired_prompt.strip()}
Seed: {seed}
Resolution: {width}x{height}
Sampler: k_euler_ancestral
Steps: 28
Scale: 3.7
Model: nai-diffusion-4-full```"""
# Prüfen ob ZIP (PK am Anfang)
if response_bytes[:2] == b'PK':
# ZIP-Archiv entpacken
with zipfile.ZipFile(io.BytesIO(response_bytes)) as zip_file:
namelist = zip_file.namelist()
# Erstes PNG suchen
image_name = next((name for name in namelist if name.endswith(".png")), None)
if image_name:
image_bytes = zip_file.read(image_name)
filename = "novelai.png"
file = discord.File(io.BytesIO(image_bytes), filename=filename)
await interaction.followup.send(content=param_text, file=file)
else:
await interaction.followup.send(
"Found no PNG inside archive"
)
elif response_bytes[:4] == b'\x89PNG':
# Direkte PNG-Rückgabe
filename = "novelai.png"
file = discord.File(io.BytesIO(response_bytes), filename=filename)
await interaction.followup.send(content=param_text, file=file)
else:
await interaction.followup.send(
"API didn't send any file"
)
else:
try:
error_data = response.json()
except Exception:
error_data = {"error": response.text}
error_message = f"Error {response.status_code} at API-request.\n"
for key, value in error_data.items():
error_message += f"**{key}**: {value}\n"
await interaction.followup.send(error_message)
bot.run(BOT_TOKEN)