initial commit #1
97
.gitea/workflows/check-code_quality.yaml
Normal file
97
.gitea/workflows/check-code_quality.yaml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
name: Code Quality Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GITHUB: ${{ secrets.GITHUB }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install -r src/requirements.txt
|
||||||
|
pip install black isort pylint flake8 mypy bandit safety pytest coverage radon
|
||||||
|
|
||||||
|
- name: Create results folder
|
||||||
|
run: mkdir -p /tmp/results
|
||||||
|
|
||||||
|
# Tools ausführen
|
||||||
|
- name: Run Black
|
||||||
|
run: black --check --diff . > /tmp/results/black.txt 2>&1 || echo "BLACK_FAILED=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Run isort
|
||||||
|
run: isort --check-only --diff . > /tmp/results/isort.txt 2>&1 || echo "ISORT_FAILED=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Run pylint with optional rcfile
|
||||||
|
run: |
|
||||||
|
if [ -f .gitea/linters/pylintrc ]; then
|
||||||
|
echo "Using custom pylintrc file."
|
||||||
|
pylint --rcfile=.gitea/linters/pylintrc . > /tmp/results/pylint.txt 2>&1 || echo "PYLINT_FAILED=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "No custom pylintrc file found. Using default settings."
|
||||||
|
pylint . > /tmp/results/pylint.txt 2>&1 || echo "PYLINT_FAILED=true" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run flake8 with optional config
|
||||||
|
run: |
|
||||||
|
if [ -f .gitea/linters/flake8 ]; then
|
||||||
|
echo "Using custom flake8 config."
|
||||||
|
flake8 --config=.gitea/linters/flake8 . > /tmp/results/flake8.txt 2>&1 || echo "FLAKE8_FAILED=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "No custom flake8 config found. Using default settings."
|
||||||
|
flake8 . > /tmp/results/flake8.txt 2>&1 || echo "FLAKE8_FAILED=true" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ergebnisse zusammenfassen
|
||||||
|
- name: Combine all results
|
||||||
|
run: |
|
||||||
|
echo "### 🛡️ Code Quality Report" > /tmp/results/summary.md
|
||||||
|
|
||||||
|
for tool in black isort pylint flake8; do
|
||||||
|
echo "#### 🔍 ${tool}" >> /tmp/results/summary.md
|
||||||
|
echo '```' >> /tmp/results/summary.md
|
||||||
|
cat /tmp/results/${tool}.txt >> /tmp/results/summary.md
|
||||||
|
echo '```' >> /tmp/results/summary.md
|
||||||
|
echo "" >> /tmp/results/summary.md
|
||||||
|
done
|
||||||
|
|
||||||
|
# PR-Kommentar hinzufügen
|
||||||
|
- name: Add comment to PR
|
||||||
|
if: ${{ env.GITHUB_EVENT_NAME == 'pull_request' }}
|
||||||
|
uses: mshick/add-pr-comment@v2
|
||||||
|
with:
|
||||||
|
message-path: /tmp/results/summary.md
|
||||||
|
|
||||||
|
- name: Create black diff patch
|
||||||
|
run: black --diff . > /tmp/results/black_diff.patch
|
||||||
|
|
||||||
|
- name: Create isort diff patch
|
||||||
|
run: isort . --diff > /tmp/results/isort.patch
|
||||||
|
|
||||||
|
# Ergebnisse als Artefakte speichern
|
||||||
|
- name: Upload results as artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: quality-reports
|
||||||
|
path: /tmp/results
|
||||||
|
|
||||||
|
- name: Check for issues
|
||||||
|
run: |
|
||||||
|
if [[ "$BLACK_FAILED" == "true" || "$ISORT_FAILED" == "true" || "$PYLINT_FAILED" == "true" || "$FLAKE8_FAILED" == "true" ]]; then
|
||||||
|
echo "❌ Code quality issues found! Please fix the issues before merging." && exit 1
|
||||||
|
fi
|
||||||
24
.gitea/workflows/check-safety.yaml
Normal file
24
.gitea/workflows/check-safety.yaml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
name: Safety Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
schedule:
|
||||||
|
- cron: "@daily"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
safety-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Create results folder
|
||||||
|
run: mkdir -p results
|
||||||
|
|
||||||
|
- name: Run Safety check
|
||||||
|
uses: pyupio/safety-action@v1
|
||||||
|
with:
|
||||||
|
api-key: ${{ secrets.SAFETY }}
|
||||||
26
.gitea/workflows/release.yaml
Normal file
26
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.0day.agency -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: |
|
||||||
|
docker build -f src/Dockerfile -t git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-discord-bot:${{ gitea.ref_name }} src/
|
||||||
|
|
||||||
|
- name: Push Docker Image
|
||||||
|
run: |
|
||||||
|
docker push git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-discord-bot:${{ gitea.ref_name }}
|
||||||
14
src/Dockerfile
Normal file
14
src/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.13.3-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
185
src/main.py
Normal file
185
src/main.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import zipfile
|
||||||
|
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
|
||||||
|
|
||||||
|
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 gestartet als {bot.user}")
|
||||||
|
try:
|
||||||
|
synced = await bot.tree.sync()
|
||||||
|
print(f"{len(synced)} Slash-Commands synchronisiert.")
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
@bot.tree.command(name="generate", description="Generiere ein Bild mit NovelAI v4")
|
||||||
|
@app_commands.describe(
|
||||||
|
prompt="Was soll dargestellt werden?",
|
||||||
|
undesired_prompt="Was soll vermieden werden? (optional)",
|
||||||
|
orientation="portrait oder landscape (Standard: portrait)"
|
||||||
|
)
|
||||||
|
async def generate(
|
||||||
|
interaction: discord.Interaction,
|
||||||
|
prompt: str,
|
||||||
|
undesired_prompt: str = "",
|
||||||
|
orientation: str = "portrait"
|
||||||
|
):
|
||||||
|
if interaction.channel.id != ALLOWED_CHANNEL_ID:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Dieser Befehl ist in diesem Kanal nicht erlaubt.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await interaction.response.defer(thinking=True)
|
||||||
|
|
||||||
|
# Auflösung wählen
|
||||||
|
if orientation.lower() == "landscape":
|
||||||
|
width, height = 1216, 832
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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}
|
||||||
|
Negativer Prompt: {negative}
|
||||||
|
Seed: {seed}
|
||||||
|
Auflösung: {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(
|
||||||
|
"ZIP erhalten, aber kein PNG-Bild gefunden."
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
"Die API hat Daten gesendet, aber es war weder ein Bild noch ein gültiges ZIP-Archiv."
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
except Exception:
|
||||||
|
error_data = {"error": response.text}
|
||||||
|
|
||||||
|
error_message = f"Fehler {response.status_code} bei der API-Anfrage.\n"
|
||||||
|
for key, value in error_data.items():
|
||||||
|
error_message += f"**{key}**: {value}\n"
|
||||||
|
|
||||||
|
await interaction.followup.send(error_message)
|
||||||
|
|
||||||
|
|
||||||
|
bot.run(BOT_TOKEN)
|
||||||
2
src/requirements.txt
Normal file
2
src/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
discord
|
||||||
|
requests
|
||||||
Loading…
x
Reference in New Issue
Block a user