AI-Edit-Ebene via OpenRouter (Gemini Image): Studio-Hintergrund, Retusche, Restaurierung + freier Prompt
- app/ai_edit.py: OpenRouter chat/completions (image in/out), Presets + Prompt, Retry - bot: AI-Preset-Buttons + Bildunterschrift = freier AI-Prompt; _process refaktoriert - Freistellen/Upscaling bleiben lokal (originaltreu); AI-Edit fuer generative Edits - httpx pin, config OPENROUTER_* Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
+95
-53
@@ -23,7 +23,7 @@ from telegram.ext import (
|
||||
filters,
|
||||
)
|
||||
|
||||
from . import config, enhance, picdrop
|
||||
from . import ai_edit, config, enhance, picdrop
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s %(levelname)s %(name)s | %(message)s",
|
||||
@@ -63,13 +63,21 @@ async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
uid = update.effective_user.id if update.effective_user else "?"
|
||||
if not _allowed(update):
|
||||
return await _deny(update)
|
||||
ai_block = ""
|
||||
if config.AI_EDIT_ENABLED:
|
||||
ai_block = (
|
||||
"• 🎨 *Studio-Hintergrund* / 🪄 *AI-Retusche* / 🖼 *Restaurierung*\n"
|
||||
"• 💬 *Eigener AI-Prompt*: Bild mit Bildunterschrift senden "
|
||||
"(z. B. 'Hintergrund zu Strand', 'mach es schwarz-weiß')\n"
|
||||
)
|
||||
await update.effective_message.reply_text(
|
||||
"👋 *Klarbildbot* – AI-Bildaufbereitung\n\n"
|
||||
"Schick mir ein Bild, dann kannst du wählen:\n"
|
||||
"• 🔍 *Klarbild ×2 / ×4* – AI-Upscaling + Schärfen\n"
|
||||
"• ✂️ *Freistellen* – Hintergrund per AI entfernen (PNG)\n"
|
||||
"• ✨ *Freistellen + Klarbild* – beides\n\n"
|
||||
"💡 Für beste Qualität das Bild als *Datei* senden "
|
||||
"• 🔍 *Klarbild ×2 / ×4* – Upscaling + Schärfen (lokal, originaltreu)\n"
|
||||
"• ✂️ *Freistellen* – Hintergrund entfernen, echtes transparentes PNG (lokal)\n"
|
||||
"• ✨ *Freistellen + Klarbild* – beides\n"
|
||||
+ ai_block +
|
||||
"\n💡 Für beste Qualität das Bild als *Datei* senden "
|
||||
"(Büroklammer → Datei), nicht als komprimiertes Foto.\n\n"
|
||||
f"Deine Telegram-ID: `{uid}`",
|
||||
parse_mode="Markdown",
|
||||
@@ -97,12 +105,81 @@ async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
# Bild empfangen -> Aktionsmenue
|
||||
# ---------------------------------------------------------------------------
|
||||
def _menu() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup([
|
||||
rows = [
|
||||
[InlineKeyboardButton("🔍 Klarbild ×2", callback_data="up:2"),
|
||||
InlineKeyboardButton("🔍 Klarbild ×4", callback_data="up:4")],
|
||||
[InlineKeyboardButton("✂️ Freistellen", callback_data="cut:0")],
|
||||
[InlineKeyboardButton("✨ Freistellen + Klarbild ×2", callback_data="cutup:2")],
|
||||
])
|
||||
]
|
||||
if config.AI_EDIT_ENABLED:
|
||||
rows += [
|
||||
[InlineKeyboardButton("🎨 Studio-Hintergrund", callback_data="ai:studio"),
|
||||
InlineKeyboardButton("🪄 AI-Retusche", callback_data="ai:retouch")],
|
||||
[InlineKeyboardButton("🖼 Foto-Restaurierung", callback_data="ai:restore")],
|
||||
]
|
||||
return InlineKeyboardMarkup(rows)
|
||||
|
||||
|
||||
def _label(action: str, scale: int = 0, prompt: str = "") -> str:
|
||||
return {
|
||||
"up": f"Klarbild ×{scale}",
|
||||
"cut": "Freistellen",
|
||||
"cutup": f"Freistellen + Klarbild ×{scale}",
|
||||
"ai": ai_edit.PRESET_LABELS.get(prompt, prompt),
|
||||
"aiprompt": f"AI-Edit: {prompt[:40]}",
|
||||
}.get(action, action)
|
||||
|
||||
|
||||
def _do_job(action: str, scale: int, prompt: str, image_bytes: bytes) -> enhance.Result:
|
||||
if action == "up":
|
||||
return enhance.upscale(image_bytes, scale)
|
||||
if action == "cut":
|
||||
return enhance.cutout(image_bytes)
|
||||
if action == "cutup":
|
||||
return enhance.cutout_then_upscale(image_bytes, scale)
|
||||
if action == "ai":
|
||||
return ai_edit.edit(image_bytes, ai_edit.PRESETS[prompt])
|
||||
if action == "aiprompt":
|
||||
return ai_edit.edit(image_bytes, prompt)
|
||||
raise ValueError(action)
|
||||
|
||||
|
||||
async def _process(ctx: ContextTypes.DEFAULT_TYPE, chat_id: int, status,
|
||||
file_id: str, action: str, scale: int, prompt: str) -> None:
|
||||
"""Bild laden, Job im Executor ausfuehren, Ergebnis als Dokument senden.
|
||||
`status` ist eine Telegram-Message, deren Text als Fortschritt editiert wird."""
|
||||
label = _label(action, scale, prompt)
|
||||
await status.edit_text(f"⏳ {label} läuft … (kann bei großen Bildern dauern)")
|
||||
try:
|
||||
tg_file = await ctx.bot.get_file(file_id)
|
||||
image_bytes = bytes(await tg_file.download_as_bytearray())
|
||||
except Exception as exc:
|
||||
log.exception("Download fehlgeschlagen")
|
||||
return await status.edit_text(
|
||||
f"❌ Konnte das Bild nicht laden: {exc}\n"
|
||||
"(Telegram-Bots können Dateien bis 20 MB laden.)")
|
||||
|
||||
await ctx.bot.send_chat_action(chat_id, ChatAction.UPLOAD_DOCUMENT)
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
result = await loop.run_in_executor(
|
||||
EXECUTOR, _do_job, action, scale, prompt, image_bytes)
|
||||
except Exception as exc:
|
||||
log.exception("Verarbeitung fehlgeschlagen")
|
||||
return await status.edit_text(f"❌ Fehler bei der Verarbeitung: {exc}")
|
||||
|
||||
caption = (f"✅ {label}\n{result.width}×{result.height}px · {result.seconds:.1f}s"
|
||||
+ (f"\n{result.note}" if result.note else ""))
|
||||
try:
|
||||
await ctx.bot.send_document(
|
||||
chat_id=chat_id,
|
||||
document=InputFile(result.data, filename=f"klarbild.{result.fmt}"),
|
||||
caption=caption,
|
||||
)
|
||||
await status.edit_text(f"✅ Fertig: {label}")
|
||||
except Exception as exc:
|
||||
log.exception("Senden fehlgeschlagen")
|
||||
await status.edit_text(f"❌ Konnte Ergebnis nicht senden: {exc}")
|
||||
|
||||
|
||||
async def on_image(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
@@ -119,8 +196,16 @@ async def on_image(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
else:
|
||||
return await msg.reply_text("Bitte ein Bild senden.")
|
||||
|
||||
# Bild mit Bildunterschrift + AI aktiv => Unterschrift als freier AI-Prompt
|
||||
caption = (msg.caption or "").strip()
|
||||
if caption and config.AI_EDIT_ENABLED:
|
||||
status = await msg.reply_text("🪄 AI-Edit vorbereiten …")
|
||||
return await _process(ctx, msg.chat_id, status, file_id, "aiprompt", 0, caption)
|
||||
|
||||
PENDING[update.effective_user.id] = {"file_id": file_id, "source": source}
|
||||
hint = "" if source == "document" else "\n_(Tipp: als Datei senden = volle Auflösung)_"
|
||||
if config.AI_EDIT_ENABLED:
|
||||
hint += "\n💬 _Oder Bild mit Bildunterschrift senden = eigener AI-Prompt._"
|
||||
await msg.reply_text(
|
||||
"Was soll ich damit machen?" + hint,
|
||||
reply_markup=_menu(),
|
||||
@@ -145,54 +230,11 @@ async def on_action(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
|
||||
action, _, arg = query.data.partition(":")
|
||||
scale = int(arg) if arg.isdigit() else 0
|
||||
|
||||
labels = {"up": f"Klarbild ×{scale}", "cut": "Freistellen",
|
||||
"cutup": f"Freistellen + Klarbild ×{scale}"}
|
||||
await query.edit_message_text(f"⏳ {labels.get(action, action)} läuft … "
|
||||
"(kann bei großen Bildern etwas dauern)")
|
||||
|
||||
# Original holen (volle Aufloesung)
|
||||
try:
|
||||
tg_file = await ctx.bot.get_file(pend["file_id"])
|
||||
image_bytes = bytes(await tg_file.download_as_bytearray())
|
||||
except Exception as exc:
|
||||
log.exception("Download fehlgeschlagen")
|
||||
return await query.edit_message_text(
|
||||
f"❌ Konnte das Bild nicht laden: {exc}\n"
|
||||
"(Telegram-Bots können Dateien bis 20 MB laden.)")
|
||||
|
||||
await ctx.bot.send_chat_action(update.effective_chat.id, ChatAction.UPLOAD_DOCUMENT)
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def work() -> enhance.Result:
|
||||
if action == "up":
|
||||
return enhance.upscale(image_bytes, scale)
|
||||
if action == "cut":
|
||||
return enhance.cutout(image_bytes)
|
||||
if action == "cutup":
|
||||
return enhance.cutout_then_upscale(image_bytes, scale)
|
||||
raise ValueError(action)
|
||||
prompt = arg if action == "ai" else "" # bei AI-Presets ist arg der Preset-Key
|
||||
|
||||
try:
|
||||
result = await loop.run_in_executor(EXECUTOR, work)
|
||||
except Exception as exc:
|
||||
log.exception("Verarbeitung fehlgeschlagen")
|
||||
return await query.edit_message_text(f"❌ Fehler bei der Verarbeitung: {exc}")
|
||||
|
||||
fname = f"klarbild.{result.fmt}"
|
||||
caption = (f"✅ {labels.get(action, action)}\n"
|
||||
f"{result.width}×{result.height}px · {result.seconds:.1f}s"
|
||||
+ (f"\n{result.note}" if result.note else ""))
|
||||
try:
|
||||
await ctx.bot.send_document(
|
||||
chat_id=update.effective_chat.id,
|
||||
document=InputFile(result.data, filename=fname),
|
||||
caption=caption,
|
||||
)
|
||||
await query.edit_message_text(f"✅ Fertig: {labels.get(action, action)}")
|
||||
except Exception as exc:
|
||||
log.exception("Senden fehlgeschlagen")
|
||||
await query.edit_message_text(f"❌ Konnte Ergebnis nicht senden: {exc}")
|
||||
await _process(ctx, update.effective_chat.id, query.message,
|
||||
pend["file_id"], action, scale, prompt)
|
||||
finally:
|
||||
PENDING.pop(uid, None)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user