dev #7

Merged
wirehack7 merged 8 commits from dev into main 2025-05-01 14:51:46 +02:00
2 changed files with 96 additions and 91 deletions

View File

@ -32,9 +32,3 @@ jobs:
- name: Push Docker Image als latest - name: Push Docker Image als latest
run: | run: |
docker push git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-bot:latest docker push git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-bot:latest
- name: Publish Container Package in Gitea
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.REGISTRY_TOKEN }}" \
"https://git.0day.agency/api/packages/${{ secrets.REGISTRY_USERNAME }}/container/novelai-discord-bot/versions/${{ gitea.ref_name }}/publish"

View File

@ -1,97 +1,115 @@
"""Discord Bot zur Bildgenerierung mit NovelAI API."""
# flake8: noqa E501
import io import io
import os
import sys
import random
import zipfile
import logging import logging
import os
import random
import sys
import time
import zipfile
import discord import discord
import requests
from discord import app_commands from discord import app_commands
from discord.ext import commands from discord.ext import commands
import requests
# ENV Variablen
NOVELAI_API_TOKEN = os.getenv("NOVELAI_API_TOKEN") NOVELAI_API_TOKEN = os.getenv("NOVELAI_API_TOKEN")
BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
ALLOWED_CHANNEL_ID = int(os.getenv("DISCORD_CHANNEL_ID", "0")) # falls nicht gesetzt → 0 ALLOWED_CHANNEL_ID = int(os.getenv("DISCORD_CHANNEL_ID", "0"))
# Logging Setup
logging.basicConfig( logging.basicConfig(
stream=sys.stdout, stream=sys.stdout,
level=logging.INFO, level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s" format="%(asctime)s - %(levelname)s - %(message)s",
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if not all([NOVELAI_API_TOKEN, BOT_TOKEN, ALLOWED_CHANNEL_ID]): 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.") raise ValueError(
"NOVELAI_API_TOKEN, DISCORD_BOT_TOKEN und DISCORD_CHANNEL_ID müssen gesetzt sein."
)
intents = discord.Intents.default() intents = discord.Intents.default()
bot = commands.Bot(command_prefix=commands.when_mentioned, intents=intents) bot = commands.Bot(command_prefix=commands.when_mentioned, intents=intents)
@bot.event @bot.event
async def on_ready(): async def on_ready():
print(f"Bot started as {bot.user}") """Bot Startup Ereignis."""
print(f"Bot gestartet als {bot.user}")
try: try:
synced = await bot.tree.sync() synced = await bot.tree.sync()
logging.info(f"{len(synced)} Slash-Commands synchronized.") logger.info("%d Slash-Commands synchronisiert.", len(synced))
except Exception as e: except Exception as err: # pylint: disable=W0718
logging.error(e) logger.error("Fehler beim Synchronisieren der Commands: %s", err)
activity = discord.Game(name="generating juicy NovelAI images 🥵")
await bot.change_presence(status=discord.Status.online, activity=activity)
@bot.tree.command(name="generate", description="Generate image with NovelAI v4 Full") @bot.tree.command(name="generate", description="Generate image with NovelAI v4 Full")
@app_commands.describe( @app_commands.describe(
prompt="What should be generated?", prompt="What should be generated?",
undesired_prompt="What should be avoided? (optional)", undesired_prompt="What should be avoided? (optional)",
orientation="portrait or landscape (Standard: portrait)", orientation="portrait or landscape (Standard: portrait)",
seed="Optional seed (integer). If empty, a random one will be generated" seed="Optional seed (integer). If empty, a random one will be generated",
) )
# pylint: disable=too-many-locals,too-many-return-statements
async def generate( async def generate(
interaction: discord.Interaction, interaction: discord.Interaction,
prompt: str, prompt: str,
undesired_prompt: str = "", undesired_prompt: str = "",
orientation: str = "portrait", orientation: str = "portrait",
seed: int = None seed: int = None,
): ):
"""Slash-Command zur Bildgenerierung."""
default_negative = ( default_negative = (
"blurry, lowres, error, film grain, scan artifacts, worst quality, bad quality, jpeg artifacts, " "blurry, lowres, error, film grain, scan artifacts, worst quality, "
"very displeasing, chromatic aberration, multiple views, logo, too many watermarks, white blank page, " "bad quality, jpeg artifacts, very displeasing, chromatic aberration, "
"blank page, watermarks, watermark, text, " "multiple views, logo, too many watermarks, white blank page, blank page, "
"watermarks, watermark, text, "
) )
if interaction.channel.id != ALLOWED_CHANNEL_ID: if interaction.channel.id != ALLOWED_CHANNEL_ID:
await interaction.response.send_message( await interaction.response.send_message(
"This command isn't allowed here.", "This command isn't allowed here.", ephemeral=True
ephemeral=True
) )
return return
MAX_PROMPT_LENGTH = 500 max_prompt_length = 500
negative_prompt = default_negative + undesired_prompt.strip()
if len(prompt) > MAX_PROMPT_LENGTH: if len(prompt) > max_prompt_length:
await interaction.followup.send(f"Prompt too long! Maximum {MAX_PROMPT_LENGTH} chars allowed.") await interaction.response.send_message(
f"Prompt too long! Max {max_prompt_length} characters.", ephemeral=True
)
return return
if len(default_negative + undesired_prompt.strip()) > MAX_PROMPT_LENGTH: if len(negative_prompt) > max_prompt_length:
await interaction.followup.send(f"Negative Prompt too long! Maximum {MAX_PROMPT_LENGTH} chars allowed.") await interaction.response.send_message(
f"Negative prompt too long! Max {max_prompt_length} characters.",
ephemeral=True,
)
return return
await interaction.response.defer(thinking=True) await interaction.response.defer(thinking=True)
logger.info( logger.info(
"User %s (%s, %s) requested image", "User %s (%s, %s) requested image",
interaction.user.display_name, interaction.user.display_name,
interaction.user.name, interaction.user.name,
interaction.user.id interaction.user.id,
) )
# Auflösung wählen width, height = (1216, 832) if orientation.lower() == "landscape" else (832, 1216)
if orientation.lower() == "landscape":
width, height = 1216, 832
else:
width, height = 832, 1216
if seed is None: if seed is None:
seed = random.randint(0, 2**32 - 1) seed = random.randint(0, 2**32 - 1)
# API-Payload zusammenbauen
payload = { payload = {
"input": prompt, "input": prompt,
"model": "nai-diffusion-4-full", "model": "nai-diffusion-4-full",
@ -121,89 +139,82 @@ async def generate(
"seed": seed, "seed": seed,
"characterPrompts": [], "characterPrompts": [],
"v4_prompt": { "v4_prompt": {
"caption": { "caption": {"base_caption": prompt, "char_captions": []},
"base_caption": prompt,
"char_captions": []
},
"use_coords": False, "use_coords": False,
"use_order": True "use_order": True,
}, },
"v4_negative_prompt": { "v4_negative_prompt": {
"caption": { "caption": {"base_caption": negative_prompt, "char_captions": []},
"base_caption": default_negative + undesired_prompt.strip(), "legacy_uc": False,
"char_captions": []
},
"legacy_uc": False
}, },
"negative_prompt": default_negative + undesired_prompt.strip(), "negative_prompt": negative_prompt,
"deliberate_euler_ancestral_bug": False, "deliberate_euler_ancestral_bug": False,
"prefer_brownian": True "prefer_brownian": True,
} },
} }
headers = { headers = {
"Authorization": f"Bearer {NOVELAI_API_TOKEN}", "Authorization": f"Bearer {NOVELAI_API_TOKEN}",
"Content-Type": "application/json" "Content-Type": "application/json",
} }
start_time = time.monotonic()
response = requests.post( response = requests.post(
"https://image.novelai.net/ai/generate-image", "https://image.novelai.net/ai/generate-image",
json=payload, json=payload,
headers=headers, headers=headers,
timeout=120 timeout=120,
) )
duration = time.monotonic() - start_time
logger.info("Bildgenerierung dauerte %.2f Sekunden", duration)
if response.status_code == 200: if response.status_code == 200:
response_bytes = response.content response_bytes = response.content
param_text = (
f"```Prompt: {prompt}\n"
f"Undesired prompt: {negative_prompt}\n"
f"Seed: {seed}\n"
f"Resolution: {width}x{height}\n"
f"Sampler: k_euler_ancestral\n"
f"Steps: 28\n"
f"Scale: 3.7\n"
f"Model: nai-diffusion-4-full```"
)
param_text = f"""```Prompt: {prompt} if response_bytes[:2] == b"PK":
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: with zipfile.ZipFile(io.BytesIO(response_bytes)) as zip_file:
namelist = zip_file.namelist() namelist = zip_file.namelist()
# Erstes PNG suchen image_name = next(
image_name = next((name for name in namelist if name.endswith(".png")), None) (name for name in namelist if name.endswith(".png")), None
)
if image_name: if image_name:
image_bytes = zip_file.read(image_name) image_bytes = zip_file.read(image_name)
filename = "novelai.png" filename = "novelai.png"
file = discord.File(io.BytesIO(image_bytes), filename=filename) file = discord.File(io.BytesIO(image_bytes), filename=filename)
await interaction.followup.send(content=param_text, file=file) await interaction.followup.send(content=param_text, file=file)
else: return
await interaction.followup.send( await interaction.followup.send("Found no PNG inside archive")
"Found no PNG inside archive" return
)
elif response_bytes[:4] == b'\x89PNG': if response_bytes[:4] == b"\x89PNG":
# Direkte PNG-Rückgabe
filename = "novelai.png" filename = "novelai.png"
file = discord.File(io.BytesIO(response_bytes), filename=filename) file = discord.File(io.BytesIO(response_bytes), filename=filename)
await interaction.followup.send(content=param_text, file=file) await interaction.followup.send(content=param_text, file=file)
return
else: await interaction.followup.send("API didn't send any file")
await interaction.followup.send( return
"API didn't send any file"
)
else: try:
try: error_data = response.json()
error_data = response.json() except Exception: # pylint: disable=W0718
except Exception: error_data = {"error": response.text}
error_data = {"error": response.text}
error_message = f"Error {response.status_code} at API-request.\n" error_message = f"Error {response.status_code} at API-request.\n"
for key, value in error_data.items(): for key, value in error_data.items():
error_message += f"**{key}**: {value}\n" error_message += f"**{key}**: {value}\n"
await interaction.followup.send(error_message)
await interaction.followup.send(error_message)
bot.run(BOT_TOKEN) bot.run(BOT_TOKEN)