Compare commits

...

42 Commits
v0.1.0 ... main

Author SHA1 Message Date
c8bb6681c7 Merge pull request 'dev' (#24) from dev into main
Some checks failed
Code Quality Check / quality-check (push) Failing after -40s
Safety Check / safety-check (push) Successful in -41s
Cleanup old Docker images / cleanup (push) Failing after -54s
Reviewed-on: #24
2025-12-11 09:41:20 +01:00
c0d5bff873 Merge pull request 'Update actions/setup-python action to v6' (#20) from renovate/actions-setup-python-6.x into dev
Some checks failed
Safety Check / safety-check (pull_request) Successful in -53s
Code Quality Check / quality-check (pull_request) Failing after 1m19s
Reviewed-on: #20
2025-12-11 09:35:51 +01:00
ff7b33b952 Merge pull request 'Update python Docker tag to v3.14.2' (#23) from renovate/python-3.x into dev
Reviewed-on: #23
2025-12-11 09:35:39 +01:00
702e62973a
Update python Docker tag to v3.14.2 2025-12-09 00:00:11 +00:00
c3dd3fa7b0 Merge pull request 'Update dependency python to v3.14.0' (#18) from renovate/python-3.x into dev
Reviewed-on: #18
2025-11-24 09:43:48 +01:00
b4954b082e Merge pull request 'Update actions/upload-artifact action to v5' (#21) from renovate/major-github-artifact-actions into dev
Reviewed-on: #21
2025-11-24 09:43:35 +01:00
54713ed346 Merge pull request 'Update actions/checkout action to v6' (#22) from renovate/actions-checkout-6.x into dev
Reviewed-on: #22
2025-11-24 09:43:14 +01:00
673fc0deaf
Update actions/checkout action to v6 2025-11-21 00:00:23 +00:00
f80cc18294
Update actions/upload-artifact action to v5 2025-10-25 00:15:07 +00:00
e03b26da0f
Update dependency python to v3.14.0 2025-10-08 00:13:55 +00:00
9eef597c8d
Update actions/setup-python action to v6 2025-09-05 00:11:19 +00:00
57acaef1b2 Merge pull request 'dev' (#17) from dev into main
Some checks failed
Code Quality Check / quality-check (push) Successful in 4m58s
Safety Check / safety-check (push) Successful in -52s
Cleanup old Docker images / cleanup (push) Failing after -1m5s
Reviewed-on: #17
2025-06-25 16:42:29 +02:00
aa2382f3b4 Merge pull request 'Update python Docker tag to v3.13.5' (#16) from renovate/python-3.x into dev
All checks were successful
Safety Check / safety-check (pull_request) Successful in 4m41s
Code Quality Check / quality-check (pull_request) Successful in 5m5s
Reviewed-on: #16
2025-06-25 16:41:15 +02:00
a8d83c8768
Update python Docker tag to v3.13.5 2025-06-14 00:04:03 +00:00
e7be5553cc Merge pull request 'Update python Docker tag to v3.13.4' (#15) from renovate/python-3.x into dev
Reviewed-on: #15
2025-06-10 17:35:31 +02:00
895b17390d
Update python Docker tag to v3.13.4 2025-06-04 22:04:02 +00:00
9145ac1bb4
pylint, fuck you! 🖕
All checks were successful
Safety Check / safety-check (pull_request) Successful in -2s
Code Quality Check / quality-check (pull_request) Successful in 13s
2025-05-01 19:30:45 +02:00
44d0315059 Merge pull request 'better timeout handling' (#14) from dev into main
Some checks failed
Code Quality Check / quality-check (push) Successful in 13s
Safety Check / safety-check (push) Successful in 4m38s
Cleanup old Docker images / cleanup (push) Failing after 4m27s
Reviewed-on: #14
2025-05-01 19:29:54 +02:00
b3045bf724
reformatted
Some checks failed
Safety Check / safety-check (pull_request) Successful in -1s
Code Quality Check / quality-check (pull_request) Failing after 13s
2025-05-01 19:28:14 +02:00
90c97d3f54
better timeout handling
Some checks failed
Safety Check / safety-check (pull_request) Successful in -1s
Code Quality Check / quality-check (pull_request) Failing after 14s
2025-05-01 19:25:48 +02:00
6bf61fdbf0
removed requests
All checks were successful
Safety Check / safety-check (pull_request) Successful in -3s
Code Quality Check / quality-check (pull_request) Successful in 10s
2025-05-01 15:56:57 +02:00
b2c2d4bcc2 Merge pull request 'switched to atiohttp' (#13) from dev into main
All checks were successful
Safety Check / safety-check (push) Successful in -2s
Code Quality Check / quality-check (push) Successful in 11s
Build and Push Docker Image / build (push) Successful in -9s
Reviewed-on: #13
2025-05-01 15:56:10 +02:00
1ef60193a0
more fixes
Some checks failed
Safety Check / safety-check (pull_request) Successful in -2s
Code Quality Check / quality-check (pull_request) Failing after 10s
2025-05-01 15:55:54 +02:00
3fbc996de1
switched to aiohttp
Some checks failed
Safety Check / safety-check (pull_request) Successful in -1s
Code Quality Check / quality-check (pull_request) Failing after 57s
2025-05-01 15:52:18 +02:00
8819489a7d Merge pull request 'Update dependency python to 3.13' (#10) from renovate/python-3.x into dev
Reviewed-on: #10
2025-05-01 15:18:11 +02:00
3c979d855e
Update dependency python to 3.13 2025-05-01 13:16:48 +00:00
5d42ab65e6
Added renovate
All checks were successful
Safety Check / safety-check (pull_request) Successful in -1s
Code Quality Check / quality-check (pull_request) Successful in 15s
2025-05-01 15:15:21 +02:00
fa70c064cf Merge pull request 'Added renovate' (#9) from dev into main
All checks were successful
Safety Check / safety-check (push) Successful in -2s
Code Quality Check / quality-check (push) Successful in 13s
Reviewed-on: #9
2025-05-01 15:14:56 +02:00
dfaf4ee8f3
added readme
All checks were successful
Safety Check / safety-check (pull_request) Successful in -1s
Code Quality Check / quality-check (pull_request) Successful in 14s
2025-05-01 15:09:40 +02:00
ed3cd1df57 Merge pull request 'Created README' (#8) from dev into main
All checks were successful
Safety Check / safety-check (push) Successful in -4s
Code Quality Check / quality-check (push) Successful in 14s
Build and Push Docker Image / build (push) Successful in -10s
Reviewed-on: #8
2025-05-01 15:09:18 +02:00
059761fb29
translated 2025-05-01 14:56:12 +02:00
05f4454ace
added newline
All checks were successful
Safety Check / safety-check (pull_request) Successful in 2s
Code Quality Check / quality-check (pull_request) Successful in 15s
2025-05-01 14:52:17 +02:00
afb1ac797f Merge pull request 'dev' (#7) from dev into main
All checks were successful
Safety Check / safety-check (push) Successful in -1s
Code Quality Check / quality-check (push) Successful in 14s
Build and Push Docker Image / build (push) Successful in -10s
Reviewed-on: #7
2025-05-01 14:51:45 +02:00
8e5d4dd250
flake ignores
Some checks failed
Safety Check / safety-check (pull_request) Successful in -2s
Code Quality Check / quality-check (pull_request) Failing after 15s
2025-05-01 14:51:22 +02:00
c2f7acb30d
more pylint ignores
Some checks failed
Safety Check / safety-check (pull_request) Successful in -2s
Code Quality Check / quality-check (pull_request) Failing after 14s
2025-05-01 14:49:47 +02:00
7b8021de41
beautified
Some checks failed
Safety Check / safety-check (pull_request) Successful in -1s
Code Quality Check / quality-check (pull_request) Failing after 14s
2025-05-01 14:47:29 +02:00
2312e00cdc
some fixes
Some checks failed
Safety Check / safety-check (pull_request) Successful in -2s
Code Quality Check / quality-check (pull_request) Failing after 13s
2025-05-01 14:41:45 +02:00
8f1c68aa04
more fixes
Some checks failed
Safety Check / safety-check (pull_request) Successful in 0s
Code Quality Check / quality-check (pull_request) Failing after 15s
2025-05-01 14:38:57 +02:00
1a76246f53
added activity 2025-05-01 14:32:47 +02:00
656d8e21e3
removed publish 2025-05-01 14:24:38 +02:00
7461571cec
added publish
Some checks failed
Safety Check / safety-check (pull_request) Successful in -1s
Code Quality Check / quality-check (pull_request) Failing after 15s
2025-05-01 14:21:25 +02:00
198cdd1af3 Merge pull request 'added publish' (#6) from dev into main
Some checks failed
Safety Check / safety-check (push) Successful in -2s
Code Quality Check / quality-check (push) Failing after 14s
Build and Push Docker Image / build (push) Successful in -9s
Reviewed-on: #6
2025-05-01 14:20:20 +02:00
7 changed files with 187 additions and 98 deletions

View File

@ -15,12 +15,12 @@ jobs:
GITHUB: ${{ secrets.GITHUB }}
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.10'
python-version: '3.14'
- name: Install dependencies
run: |
@ -85,7 +85,7 @@ jobs:
# Ergebnisse als Artefakte speichern
- name: Upload results as artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v5
with:
name: quality-reports
path: /tmp/results

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Verify Docker installation
run: docker --version
@ -31,4 +31,4 @@ jobs:
- name: Push Docker Image als latest
run: |
docker push git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-bot:latest
docker push git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-bot:latest

20
.github/renovate.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"baseBranches": [
"dev"
],
"packageRules": [
{
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest"
],
"automerge": false
}
]
}

View File

@ -1,2 +1,52 @@
# novelai-bot
# NovelAI Discord Bot
![Code Quality](https://git.0day.agency/wirehack7/novelai-bot/actions/workflows/check-code_quality.yaml/badge.svg)
![Safety Check](https://git.0day.agency/wirehack7/novelai-bot/actions/workflows/check-safety.yaml/badge.svg)
![Release](https://git.0day.agency/wirehack7/novelai-bot/actions/workflows/release.yaml/badge.svg)
Simple bot which registers a command to generate images via the NovelAI API.
## Usage
Just enter `/generate` in the desired channel.
```text
/generate
*prompt: string
undesired_prompt: string
orientation: landscape or portrait
seed: integer
```
`*: required`
## Run
You might create environvent vars:
- **NOVELAI_API_TOKEN**: your NovelAI user API key
- **DISCORD_BOT_TOKEN**: token of your Discord bot
- **DISCORD_CHANNEL_ID**: ID of the channel to enable this bot
Don't forget to install the requirements in [requirements.txt](src/requirements.txt)
## Docker Compose
Or simply run it via **docker-compose**:
```yaml
services:
bot:
restart: unless-stopped
image: git.0day.agency/wirehack7/novelai-bot:latest
environment:
- NOVELAI_API_TOKEN=
- DISCORD_BOT_TOKEN=
- DISCORD_CHANNEL_ID=
networks: {}
```
## Notes
To create a Discord bot go to [Discord Developers](https://discord.com/developers/).
You should make the bot private.

View File

@ -1,4 +1,4 @@
FROM python:3.13.3-slim
FROM python:3.14.2-slim
WORKDIR /app

View File

@ -1,97 +1,115 @@
"""Discord Bot zur Bildgenerierung mit NovelAI API."""
# flake8: noqa E501
import io
import os
import sys
import random
import zipfile
import logging
import os
import random
import sys
import time
import zipfile
import aiohttp
import discord
from discord import app_commands
from discord.ext import commands
import requests
# ENV Variablen
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
ALLOWED_CHANNEL_ID = int(os.getenv("DISCORD_CHANNEL_ID", "0"))
# Logging Setup
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
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.")
raise ValueError(
"NOVELAI_API_TOKEN, DISCORD_BOT_TOKEN and DISCORD_CHANNEL_ID have to be set."
)
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}")
"""Bot Startup Ereignis."""
print(f"Bot gestartet als {bot.user}")
try:
synced = await bot.tree.sync()
logging.info(f"{len(synced)} Slash-Commands synchronized.")
except Exception as e:
logging.error(e)
logger.info("%d Slash-Commands synchronized.", len(synced))
except Exception as err: # pylint: disable=W0718
logger.error("error while syncing the command: %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")
@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"
seed="Optional seed (integer). If empty, a random one will be generated",
)
# pylint: disable=too-many-locals,too-many-return-statements,too-many-statements
async def generate(
interaction: discord.Interaction,
prompt: str,
undesired_prompt: str = "",
orientation: str = "portrait",
seed: int = None
seed: int = None,
):
"""Slash-Command zur Bildgenerierung."""
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, "
"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
"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.")
max_prompt_length = 500
negative_prompt = default_negative + undesired_prompt.strip()
if len(prompt) > max_prompt_length:
await interaction.response.send_message(
f"Prompt too long! Max {max_prompt_length} characters.", ephemeral=True
)
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.")
if len(negative_prompt) > max_prompt_length:
await interaction.response.send_message(
f"Negative prompt too long! Max {max_prompt_length} characters.",
ephemeral=True,
)
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
interaction.user.id,
)
# Auflösung wählen
if orientation.lower() == "landscape":
width, height = 1216, 832
else:
width, height = 832, 1216
width, height = (1216, 832) if orientation.lower() == "landscape" else (832, 1216)
if seed is None:
seed = random.randint(0, 2**32 - 1)
# API-Payload zusammenbauen
payload = {
"input": prompt,
"model": "nai-diffusion-4-full",
@ -121,89 +139,90 @@ async def generate(
"seed": seed,
"characterPrompts": [],
"v4_prompt": {
"caption": {
"base_caption": prompt,
"char_captions": []
},
"caption": {"base_caption": prompt, "char_captions": []},
"use_coords": False,
"use_order": True
"use_order": True,
},
"v4_negative_prompt": {
"caption": {
"base_caption": default_negative + undesired_prompt.strip(),
"char_captions": []
},
"legacy_uc": False
"caption": {"base_caption": negative_prompt, "char_captions": []},
"legacy_uc": False,
},
"negative_prompt": default_negative + undesired_prompt.strip(),
"negative_prompt": negative_prompt,
"deliberate_euler_ancestral_bug": False,
"prefer_brownian": True
}
"prefer_brownian": True,
},
}
headers = {
"Authorization": f"Bearer {NOVELAI_API_TOKEN}",
"Content-Type": "application/json"
"Content-Type": "application/json",
}
response = requests.post(
"https://image.novelai.net/ai/generate-image",
json=payload,
headers=headers,
timeout=120
)
start_time = time.monotonic()
try:
async with aiohttp.ClientSession() as session:
async with session.post(
"https://image.novelai.net/ai/generate-image",
json=payload,
headers=headers,
timeout=aiohttp.ClientTimeout(total=120),
) as response:
response_bytes = await response.read()
status_code = response.status
except aiohttp.TimeoutError:
await interaction.followup.send("Error: the request timed out.")
except aiohttp.ClientError as e:
await interaction.followup.send(f"Error: {e}")
if response.status_code == 200:
response_bytes = response.content
duration = time.monotonic() - start_time
logger.info("Image creation took %.2f seconds", duration)
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```"""
if status_code == 200:
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```"
)
# Prüfen ob ZIP (PK am Anfang)
if response_bytes[:2] == b'PK':
# ZIP-Archiv entpacken
if response_bytes[:2] == b"PK":
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)
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"
)
return
await interaction.followup.send("Found no PNG inside archive")
return
elif response_bytes[:4] == b'\x89PNG':
# Direkte PNG-Rückgabe
if response_bytes[:4] == b"\x89PNG":
filename = "novelai.png"
file = discord.File(io.BytesIO(response_bytes), filename=filename)
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("API didn't send any file")
return
else:
try:
error_data = response.json()
except Exception:
error_data = {"error": response.text}
try:
error_data = response_bytes.decode("utf-8")
except Exception: # pylint: disable=W0718
error_data = "error message unreadable"
error_message = f"Error {response.status_code} at API-request.\n"
for key, value in error_data.items():
error_message += f"**{key}**: {value}\n"
error_message = (
f"Error {status_code} at API-request.\n" f"Answer: {error_data}\n"
)
await interaction.followup.send(error_message)
bot.run(BOT_TOKEN)

View File

@ -1,2 +1,2 @@
discord
requests
aiohttp