diff --git a/.gitea/workflows/cleanup_registry.yaml b/.gitea/workflows/cleanup_registry.yaml new file mode 100644 index 0000000..4719169 --- /dev/null +++ b/.gitea/workflows/cleanup_registry.yaml @@ -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 }} \ No newline at end of file diff --git a/src/main.py b/src/main.py index 99f6b99..fe60f91 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,9 @@ import io import os +import sys import random import zipfile +import logging import discord from discord import app_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") 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.") @@ -19,33 +29,58 @@ bot = commands.Bot(command_prefix=commands.when_mentioned, intents=intents) @bot.event async def on_ready(): - print(f"Bot gestartet als {bot.user}") + print(f"Bot started as {bot.user}") try: synced = await bot.tree.sync() - print(f"{len(synced)} Slash-Commands synchronisiert.") + logging.info(f"{len(synced)} Slash-Commands synchronized.") 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( - prompt="Was soll dargestellt werden?", - undesired_prompt="Was soll vermieden werden? (optional)", - orientation="portrait oder landscape (Standard: portrait)" + 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" + 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( - "Dieser Befehl ist in diesem Kanal nicht erlaubt.", + "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": @@ -53,18 +88,8 @@ async def generate( else: width, height = 832, 1216 - 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, " - ) - 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) + if seed is None: + seed = random.randint(0, 2**32 - 1) # API-Payload zusammenbauen payload = { @@ -93,7 +118,7 @@ async def generate( "use_coords": False, "legacy_uc": False, "normalize_reference_strength_multiple": True, - #"seed": seed, + "seed": seed, "characterPrompts": [], "v4_prompt": { "caption": { @@ -132,9 +157,9 @@ async def generate( response_bytes = response.content param_text = f"""```Prompt: {prompt} -Negativer Prompt: {negative} +Undesired prompt: {default_negative + undesired_prompt.strip()} Seed: {seed} -Auflösung: {width}x{height} +Resolution: {width}x{height} Sampler: k_euler_ancestral Steps: 28 Scale: 3.7 @@ -155,7 +180,7 @@ Model: nai-diffusion-4-full```""" await interaction.followup.send(content=param_text, file=file) else: await interaction.followup.send( - "ZIP erhalten, aber kein PNG-Bild gefunden." + "Found no PNG inside archive" ) elif response_bytes[:4] == b'\x89PNG': @@ -166,7 +191,7 @@ Model: nai-diffusion-4-full```""" else: 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: @@ -175,11 +200,10 @@ Model: nai-diffusion-4-full```""" except Exception: 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(): error_message += f"**{key}**: {value}\n" await interaction.followup.send(error_message) - bot.run(BOT_TOKEN)