added RAG, Multiuser, TG bot
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Сборка Anima-промптов без LLM (теги, без POV/hybrid)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
ANIMA_QUALITY = "masterpiece, best quality, score_7, anime"
|
||||
ANIMA_NEGATIVE = "worst quality, low quality, score_1, score_2, score_3, blurry, jpeg artifacts, sepia"
|
||||
|
||||
_JUNK_STANDALONE_TAGS = frozenset({
|
||||
"white", "black", "skin", "ear", "ears", "girl", "boy", "fox", "wolf", "cat",
|
||||
"short", "tall", "golden", "silver", "red", "blue", "green", "purple",
|
||||
"pink", "brown", "eye", "eyes", "hair",
|
||||
})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AnimaPromptBundle:
|
||||
positive: str
|
||||
negative: str
|
||||
|
||||
|
||||
def _sanitize_tags(tag_str: str) -> str:
|
||||
if not tag_str:
|
||||
return ""
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in tag_str.split(","):
|
||||
t = raw.strip()
|
||||
if not t:
|
||||
continue
|
||||
key = t.lower().replace(" ", "_")
|
||||
if key in seen or len(key) <= 2:
|
||||
continue
|
||||
if "_" not in key and key in _JUNK_STANDALONE_TAGS:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(t if "_" in t else key)
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
def _append_lora(parts: list[str], lora_name: str, lora_weight: float) -> None:
|
||||
lora = (lora_name or "").strip()
|
||||
if not lora:
|
||||
return
|
||||
weight = lora_weight if lora_weight > 0 else 0.8
|
||||
parts.append(f"<lora:{lora}:{weight}>")
|
||||
|
||||
|
||||
def build_draw_self_prompt(
|
||||
appearance_tags: str,
|
||||
*,
|
||||
lora_name: str = "",
|
||||
lora_weight: float = 0.8,
|
||||
) -> AnimaPromptBundle:
|
||||
"""Портрет «нарисуй себя» — только booru-теги, без POV и prose."""
|
||||
appearance = _sanitize_tags(appearance_tags)
|
||||
action = "looking_at_viewer, smile, upper_body, portrait"
|
||||
environment = "simple_background, soft_lighting"
|
||||
|
||||
parts = [ANIMA_QUALITY]
|
||||
if appearance:
|
||||
parts.append(appearance)
|
||||
parts.append(action)
|
||||
parts.append(environment)
|
||||
_append_lora(parts, lora_name, lora_weight)
|
||||
|
||||
positive = ", ".join(p.strip() for p in parts if p.strip())
|
||||
return AnimaPromptBundle(positive=positive, negative=ANIMA_NEGATIVE)
|
||||
|
||||
|
||||
def build_scene_tags_prompt(
|
||||
scene_tags: str,
|
||||
appearance_tags: str,
|
||||
*,
|
||||
lora_name: str = "",
|
||||
lora_weight: float = 0.8,
|
||||
) -> AnimaPromptBundle:
|
||||
"""Прямая сцена из booru-тегов (без LLM)."""
|
||||
appearance = _sanitize_tags(appearance_tags)
|
||||
scene = _sanitize_tags(scene_tags)
|
||||
parts = [ANIMA_QUALITY]
|
||||
if appearance:
|
||||
parts.append(appearance)
|
||||
if scene:
|
||||
parts.append(scene)
|
||||
_append_lora(parts, lora_name, lora_weight)
|
||||
positive = ", ".join(p.strip() for p in parts if p.strip())
|
||||
return AnimaPromptBundle(positive=positive, negative=ANIMA_NEGATIVE)
|
||||
|
||||
|
||||
def looks_like_booru_tags(text: str) -> bool:
|
||||
"""Грубая эвристика: строка похожа на теги, а не на прозу."""
|
||||
raw = (text or "").strip()
|
||||
if not raw or len(raw) > 400:
|
||||
return False
|
||||
if raw.count(",") >= 2:
|
||||
return True
|
||||
return bool(re.search(r"\b\d+(girl|boy)s?\b", raw, re.I))
|
||||
@@ -267,6 +267,7 @@ class ComfyUIClient:
|
||||
"filename": filename,
|
||||
"url": f"/api/v1/media/generated/{filename}",
|
||||
"prompt": positive,
|
||||
"negative_prompt": negative,
|
||||
"backend": "anima" if _use_anima(self.settings) else "checkpoint",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.character.service import CharacterService
|
||||
from app.config import get_settings
|
||||
from app.db.models import Message
|
||||
from app.homelab.anima_prompt import AnimaPromptBundle, build_draw_self_prompt, build_scene_tags_prompt, looks_like_booru_tags
|
||||
from app.homelab.comfyui import ComfyUIClient
|
||||
from app.integrations.rp_chat import RpChatClient
|
||||
|
||||
@@ -17,6 +16,10 @@ def _card_image_settings(db: Session, user_id: int) -> dict[str, Any]:
|
||||
def _session_messages(db: Session, session_id: int | None, limit: int = 8) -> list[dict[str, str]]:
|
||||
if not session_id:
|
||||
return []
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.models import Message
|
||||
|
||||
rows = db.scalars(
|
||||
select(Message)
|
||||
.where(
|
||||
@@ -36,6 +39,43 @@ def _append_lora(positive: str, lora_name: str, lora_weight: float) -> str:
|
||||
return f"{positive} <lora:{lora_name}:{lora_weight}>"
|
||||
|
||||
|
||||
async def _generate_from_bundle(
|
||||
bundle: AnimaPromptBundle,
|
||||
*,
|
||||
backend: str,
|
||||
persona_id: str = "",
|
||||
) -> dict[str, Any]:
|
||||
settings = get_settings()
|
||||
if backend == "rp_chat":
|
||||
client = RpChatClient()
|
||||
gen_result = await client.generate(bundle.positive, bundle.negative)
|
||||
if not gen_result.get("ok"):
|
||||
return gen_result
|
||||
saved = await client.save_image_locally(gen_result["image_path"])
|
||||
if not saved.get("ok"):
|
||||
return saved
|
||||
return {
|
||||
"ok": True,
|
||||
"url": saved["url"],
|
||||
"filename": saved["filename"],
|
||||
"prompt": bundle.positive,
|
||||
"negative_prompt": bundle.negative,
|
||||
"backend": "rp_chat",
|
||||
"persona_id": persona_id,
|
||||
"prompt_mode": "direct",
|
||||
}
|
||||
|
||||
result = await ComfyUIClient().generate_image(
|
||||
bundle.positive,
|
||||
negative_prompt=bundle.negative,
|
||||
)
|
||||
if result.get("ok"):
|
||||
result["backend"] = "comfyui_local"
|
||||
result["prompt_mode"] = "direct"
|
||||
result["negative_prompt"] = bundle.negative
|
||||
return result
|
||||
|
||||
|
||||
async def generate_image(
|
||||
db: Session,
|
||||
*,
|
||||
@@ -54,25 +94,49 @@ async def generate_image(
|
||||
return {"ok": False, "error": "Нужен draw_self=true или scene_description"}
|
||||
|
||||
appearance = (card.get("appearance_tags") or "").strip()
|
||||
if draw_self and not appearance:
|
||||
return {
|
||||
"ok": False,
|
||||
"error": "Заполни appearance_tags в настройках персонажа для «нарисуй себя»",
|
||||
}
|
||||
lora_name = (card.get("lora_name") or "").strip()
|
||||
lora_weight = float(card.get("lora_weight") or 0.8)
|
||||
persona_id = (card.get("rp_persona_id") or "").strip() or "default"
|
||||
backend = "rp_chat" if settings.rp_chat_enabled else "comfyui_local"
|
||||
|
||||
if draw_self:
|
||||
if not appearance:
|
||||
return {
|
||||
"ok": False,
|
||||
"error": "Заполни appearance_tags в настройках персонажа для «нарисуй себя»",
|
||||
}
|
||||
bundle = build_draw_self_prompt(
|
||||
appearance,
|
||||
lora_name=lora_name,
|
||||
lora_weight=lora_weight,
|
||||
)
|
||||
return await _generate_from_bundle(bundle, backend=backend, persona_id=persona_id)
|
||||
|
||||
scene = scene_description.strip()
|
||||
if looks_like_booru_tags(scene):
|
||||
if not appearance:
|
||||
bundle = build_scene_tags_prompt(scene, "", lora_name=lora_name, lora_weight=lora_weight)
|
||||
else:
|
||||
bundle = build_scene_tags_prompt(
|
||||
scene,
|
||||
appearance,
|
||||
lora_name=lora_name,
|
||||
lora_weight=lora_weight,
|
||||
)
|
||||
return await _generate_from_bundle(bundle, backend=backend, persona_id=persona_id)
|
||||
|
||||
messages = _session_messages(db, session_id)
|
||||
if scene_description.strip():
|
||||
messages = messages + [{"role": "user", "content": scene_description.strip()}]
|
||||
elif draw_self and messages:
|
||||
messages = messages + [{"role": "user", "content": "Illustrate the current scene with the character."}]
|
||||
elif draw_self:
|
||||
messages = [{"role": "user", "content": "Portrait of the character, looking at viewer, friendly expression."}]
|
||||
messages = messages + [{"role": "user", "content": scene}]
|
||||
|
||||
if settings.rp_chat_enabled:
|
||||
appearance_override = (card.get("appearance_tags") or "").strip() or None
|
||||
return await _generate_via_rp_chat(card, messages, appearance_override)
|
||||
return await _generate_via_rp_chat(
|
||||
card,
|
||||
messages,
|
||||
appearance_override=appearance or None,
|
||||
)
|
||||
|
||||
return await _generate_via_local_comfy(scene_description or "anime character portrait")
|
||||
fallback = f"{appearance}, {scene}" if appearance else scene
|
||||
return await ComfyUIClient().generate_image(fallback)
|
||||
|
||||
|
||||
async def _generate_via_rp_chat(
|
||||
@@ -119,13 +183,8 @@ async def _generate_via_rp_chat(
|
||||
"url": saved["url"],
|
||||
"filename": saved["filename"],
|
||||
"prompt": positive,
|
||||
"negative_prompt": negative,
|
||||
"backend": "rp_chat",
|
||||
"persona_id": persona_id,
|
||||
"prompt_mode": "llm",
|
||||
}
|
||||
|
||||
|
||||
async def _generate_via_local_comfy(prompt: str) -> dict[str, Any]:
|
||||
result = await ComfyUIClient().generate_image(prompt)
|
||||
if result.get("ok"):
|
||||
result["backend"] = "comfyui_local"
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user