MpakoImageAIbot/prompt_enhancer.py

136 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import copy
import json
import logging
import re
import uuid
from pathlib import Path
from typing import Any
from comfy_client import ComfyClient
from config import Settings
logger = logging.getLogger(__name__)
SYSTEM_PROMPTS = {
"t2i": (
"You are a prompt engineer for an image generation model. "
"Translate the user's request into English and rewrite it as a concise, "
"descriptive image prompt. Preserve every requested subject, action, style, "
"color, composition and constraint. Do not invent new objects, camera angles "
"or details. Return only the final English prompt, without quotes or "
"explanations."
),
"i2i": (
"You are an image-editing prompt translator. Translate the user's editing "
"instruction into English and rewrite it clearly. Preserve the requested "
"changes, composition, pose, lighting and all constraints. Do not add new "
"objects, camera angles or details. Return only the final English "
"instruction, without quotes or explanations."
),
}
class PromptEnhanceError(RuntimeError):
pass
class PromptEnhancer:
def __init__(self, comfy: ComfyClient, settings: Settings) -> None:
self._comfy = comfy
self._settings = settings
self._template = self._load_template(settings.workflow_prompt)
self._text_node_id = self._find_text_node(self._template)
if not any(
node.get("class_type") == "PreviewAny"
for node in self._template.values()
if isinstance(node, dict)
):
raise PromptEnhanceError(
"В workflow улучшения промпта нет узла PreviewAny"
)
@property
def enabled(self) -> bool:
return self._settings.prompt_enhance_enabled
@staticmethod
def _load_template(path: Path) -> dict[str, Any]:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise PromptEnhanceError(f"Не найден workflow: {path}") from exc
except json.JSONDecodeError as exc:
raise PromptEnhanceError(f"Некорректный JSON в {path}: {exc}") from exc
if not isinstance(data, dict) or not data:
raise PromptEnhanceError("Workflow улучшения промпта пустой")
return data
@staticmethod
def _find_text_node(workflow: dict[str, Any]) -> str:
node_ids = [
node_id
for node_id, node in workflow.items()
if isinstance(node, dict) and node.get("class_type") == "TextGenerate"
]
if len(node_ids) != 1:
raise PromptEnhanceError(
"В workflow улучшения промпта должен быть один TextGenerate"
)
return str(node_ids[0])
async def enhance(self, prompt: str, mode: str) -> str:
if not self.enabled:
return prompt
user_prompt = prompt.strip()
for token in ("<|im_start|>", "<|im_end|>"):
user_prompt = user_prompt.replace(token, "")
if not user_prompt:
return prompt
workflow = copy.deepcopy(self._template)
workflow[self._text_node_id]["inputs"]["prompt"] = self._chat_prompt(
user_prompt, mode
)
prompt_id = await self._comfy.submit(workflow, uuid.uuid4().hex)
entry = await self._comfy.wait_for_history(
prompt_id,
timeout=self._settings.prompt_enhance_timeout,
)
text = self._comfy.find_output_text(entry)
if not text:
raise PromptEnhanceError("ComfyUI вернул пустой промпт")
cleaned = self._clean(text)
if not cleaned:
raise PromptEnhanceError("Не удалось очистить сгенерированный промпт")
return cleaned[: self._settings.max_prompt_length]
@staticmethod
def _chat_prompt(prompt: str, mode: str) -> str:
system = SYSTEM_PROMPTS.get(mode, SYSTEM_PROMPTS["t2i"])
return (
f"<|im_start|>system\n{system}<|im_end|>\n"
f"<|im_start|>user\n{prompt}<|im_end|>\n"
"<|im_start|>assistant\n"
)
@staticmethod
def _clean(text: str) -> str:
text = " ".join(text.split()).strip()
for token in ("<|im_end|>", "<|im_start|>"):
text = text.split(token)[0].strip()
text = text.strip("`").strip()
for prefix in ("English prompt:", "Prompt:", "Assistant:"):
if text.lower().startswith(prefix.lower()):
text = text[len(prefix):].strip()
text = text.strip(" \"'")
text = text.lstrip(" ,.;:-")
if text.lower() in {"user", "assistant", "system"}:
return ""
if not re.search(r"[A-Za-z]", text):
return ""
return text