Sync with main #4

Merged
wirehack7 merged 12 commits from main into dev 2025-05-01 14:07:15 +02:00
2 changed files with 76 additions and 28 deletions

View File

@ -0,0 +1,24 @@
name: Cleanup old Docker images
on:
schedule:
- cron: '0 6 * * *' # Täglich um 6 Uhr
workflow_dispatch:
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Install crane (OCI Registry tool)
run: |
curl -LO https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz
tar -xzf go-containerregistry_Linux_x86_64.tar.gz
chmod +x crane
sudo mv crane /usr/local/bin/
- name: List Docker tags
run: |
crane ls git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-discord-bot \
--username ${{ secrets.REGISTRY_USERNAME }} \
--password ${{ secrets.REGISTRY_TOKEN }}

View File

@ -1,7 +1,9 @@
import io import io
import os import os
import sys
import random import random
import zipfile import zipfile
import logging
import discord import discord
from discord import app_commands from discord import app_commands
from discord.ext import commands from discord.ext import commands
@ -11,6 +13,14 @@ 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")) # 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]): 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("Environment-Variablen NOVELAI_API_TOKEN, DISCORD_BOT_TOKEN und DISCORD_CHANNEL_ID müssen gesetzt sein.")
@ -19,33 +29,58 @@ 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 gestartet als {bot.user}") print(f"Bot started as {bot.user}")
try: try:
synced = await bot.tree.sync() synced = await bot.tree.sync()
print(f"{len(synced)} Slash-Commands synchronisiert.") logging.info(f"{len(synced)} Slash-Commands synchronized.")
except Exception as e: except Exception as e:
print(e) logging.error(e)
@bot.tree.command(name="generate", description="Generiere ein Bild mit NovelAI v4") @bot.tree.command(name="generate", description="Generate image with NovelAI v4 Full")
@app_commands.describe( @app_commands.describe(
prompt="Was soll dargestellt werden?", prompt="What should be generated?",
undesired_prompt="Was soll vermieden werden? (optional)", undesired_prompt="What should be avoided? (optional)",
orientation="portrait oder landscape (Standard: portrait)" orientation="portrait or landscape (Standard: portrait)",
seed="Optional seed (integer). If empty, a random one will be generated"
) )
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
): ):
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: if interaction.channel.id != ALLOWED_CHANNEL_ID:
await interaction.response.send_message( await interaction.response.send_message(
"Dieser Befehl ist in diesem Kanal nicht erlaubt.", "This command isn't allowed here.",
ephemeral=True ephemeral=True
) )
return 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) 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 # Auflösung wählen
if orientation.lower() == "landscape": if orientation.lower() == "landscape":
@ -53,18 +88,8 @@ async def generate(
else: else:
width, height = 832, 1216 width, height = 832, 1216
default_negative = ( if seed is None:
"blurry, lowres, error, film grain, scan artifacts, worst quality, bad quality, jpeg artifacts, " seed = random.randint(0, 2**32 - 1)
"very displeasing, chromatic aberration, multiple views, logo, too many watermarks, white blank page, "
"blank page, "
)
negative = undesired_prompt.strip() or (
"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"
)
seed = random.randint(0, 2**32 - 1)
# API-Payload zusammenbauen # API-Payload zusammenbauen
payload = { payload = {
@ -93,7 +118,7 @@ async def generate(
"use_coords": False, "use_coords": False,
"legacy_uc": False, "legacy_uc": False,
"normalize_reference_strength_multiple": True, "normalize_reference_strength_multiple": True,
#"seed": seed, "seed": seed,
"characterPrompts": [], "characterPrompts": [],
"v4_prompt": { "v4_prompt": {
"caption": { "caption": {
@ -132,9 +157,9 @@ async def generate(
response_bytes = response.content response_bytes = response.content
param_text = f"""```Prompt: {prompt} param_text = f"""```Prompt: {prompt}
Negativer Prompt: {negative} Undesired prompt: {default_negative + undesired_prompt.strip()}
Seed: {seed} Seed: {seed}
Auflösung: {width}x{height} Resolution: {width}x{height}
Sampler: k_euler_ancestral Sampler: k_euler_ancestral
Steps: 28 Steps: 28
Scale: 3.7 Scale: 3.7
@ -155,7 +180,7 @@ Model: nai-diffusion-4-full```"""
await interaction.followup.send(content=param_text, file=file) await interaction.followup.send(content=param_text, file=file)
else: else:
await interaction.followup.send( await interaction.followup.send(
"ZIP erhalten, aber kein PNG-Bild gefunden." "Found no PNG inside archive"
) )
elif response_bytes[:4] == b'\x89PNG': elif response_bytes[:4] == b'\x89PNG':
@ -166,7 +191,7 @@ Model: nai-diffusion-4-full```"""
else: else:
await interaction.followup.send( await interaction.followup.send(
"Die API hat Daten gesendet, aber es war weder ein Bild noch ein gültiges ZIP-Archiv." "API didn't send any file"
) )
else: else:
@ -175,11 +200,10 @@ Model: nai-diffusion-4-full```"""
except Exception: except Exception:
error_data = {"error": response.text} error_data = {"error": response.text}
error_message = f"Fehler {response.status_code} bei der API-Anfrage.\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)