Spaces:
Running
Running
| """ | |
| lumen_images.py — генерация изображений через Pollinations.ai. | |
| Вынесено из bot.py при разбиении на модули (см. README, раздел "Автоматический выбор | |
| модели" и историю аудита техдолга) — самый изолированный кандидат из пяти намеченных: | |
| не пишет ни в chat_state, ни в GLOBAL_QUOTA, не зовёт Telegram API напрямую и не зависит | |
| от глобальных `bot`/`client`. | |
| Единственная внешняя зависимость каждого вызова — aiohttp-сессия, но модуль намеренно НЕ | |
| хранит собственную сессию и не заводит новый module-level singleton для неё: в bot.py уже | |
| есть один общий httр-session getter (`_get_http_session`), которым пользуются TikTok/ | |
| OpenRouter/эта же генерация картинок — плодить второй, изолированный источник управления | |
| HTTP-соединениями было бы речь не о разделении ответственности, а о случайном дублировании. | |
| Поэтому `_pollinations_generate`/`_hf_text_to_image` принимают уже готовую сессию параметром; | |
| вызывающий код (см. `inline_draw` в bot.py) сам получает её через `_get_http_session()` и | |
| передаёт сюда — то же самое соглашение, по которому TikTok-скачивание в bot.py принимает | |
| сессию параметром в `_download_url_bin`/`_resolve_tiktok_short` и т.п. | |
| Публичные имена и поведение не изменились относительно прежнего кода внутри bot.py — кроме | |
| добавленного параметра `session`, который раньше был получен неявно через `_get_http_session()` | |
| внутри самой `_pollinations_generate`. | |
| УБРАНО (аудит техдолга, 19 августа 2026): команда `/imgmodel` и ручной выбор модели через | |
| inline-клавиатуру — тот же класс изменения, что уже был сделан для текстового провайдера/ | |
| модели (см. "Автоматический выбор модели" в README): пользователь никогда явно не выбирает | |
| модель, роутер выбирает сам на каждый запрос (`_pick_image_model` ниже). `_hf_model_catalog`/ | |
| `_imgmodel_keyboard` были нужны ТОЛЬКО для этой клавиатуры — убраны вместе с ней как мёртвый | |
| код, а не оставлены "на будущее". | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import urllib.parse | |
| from typing import Any | |
| import aiohttp | |
| DEFAULT_HF_IMAGE_MODEL = os.getenv("HF_IMAGE_MODEL", "flux").strip() | |
| HF_IMAGE_MODELS: dict[str, dict[str, Any]] = { | |
| "flux": { | |
| "name": "FLUX Pro", | |
| "desc": "Высококачественный FLUX. Фотореализм, точное следование промпту, богатая детализация.", | |
| }, | |
| "flux-realism": { | |
| "name": "FLUX Realism", | |
| "desc": "FLUX с акцентом на гиперреализм — детализированные текстуры, естественное освещение, кинематографичность.", | |
| }, | |
| "flux-anime": { | |
| "name": "FLUX Anime", | |
| "desc": "FLUX для аниме и иллюстраций — характерные пропорции, яркие цвета, стилизация под японскую графику.", | |
| }, | |
| "turbo": { | |
| "name": "Turbo", | |
| "desc": "Быстрая дистиллированная модель. Результат за несколько секунд — для черновиков и быстрых итераций.", | |
| }, | |
| "dreamshaper": { | |
| "name": "DreamShaper", | |
| "desc": "Художественная модель для фэнтези, концепт-арта и стилизованных иллюстраций.", | |
| }, | |
| } | |
| # ── Автоматический выбор модели генерации по содержимому промпта ── | |
| # Тот же принцип, что и у текстового роутера (_looks_like_heavy_query/ | |
| # _looks_like_freshness_query в lumen_router_config.py): грубая эвристика по | |
| # ключевым словам без обращения к LLM — отдельный классифицирующий вызов модели | |
| # стоил бы дороже, чем просто попробовать разумный дефолт и дать сработать | |
| # существующей fallback-цепочке (см. inline_draw в bot.py) при неудаче/плохом | |
| # результате. Порядок проверки — от самых специфичных категорий к общей: аниме/ | |
| # фэнтези/реализм/черновик — явные сигналы жанра, при их отсутствии остаётся | |
| # DEFAULT_HF_IMAGE_MODEL (универсальный FLUX Pro). | |
| _ANIME_RE = re.compile(r"аниме|манг[аи]|манхв\w*|вебтун\w*|чиби|ваифу|anime|manga|waifu|chibi", re.IGNORECASE) | |
| _FANTASY_RE = re.compile( | |
| r"фэнтези|фентези|фентезийн\w*|концепт-?арт\w*|дракон\w*|эльф\w*|волшебн\w*|магическ\w*|" | |
| r"орк\w*|фея|фей\b|замок\w*|рыцар\w*|fantasy|concept\s?art|dragon|wizard|elf|elves", | |
| re.IGNORECASE, | |
| ) | |
| _REALISM_RE = re.compile( | |
| r"фотореалистичн\w*|гиперреалистичн\w*|реалистичн\w*|как\s+(на\s+)?фото|фотограф\w*|" | |
| r"portrait|realistic|photorealistic|hyperrealistic", | |
| re.IGNORECASE, | |
| ) | |
| _QUICK_RE = re.compile(r"побыстрее|быстро|набросок|черновик|скетч|эскиз|draft|sketch|quick", re.IGNORECASE) | |
| def _pick_image_model(prompt: str) -> str: | |
| """Заменяет ручной выбор через убранную команду /imgmodel — см. докстринг модуля | |
| и README, раздел "Автоматический выбор модели". Проверки НЕ взаимоисключающие по | |
| смыслу (промпт может упоминать и аниме, и дракона), поэтому порядок фиксирован: | |
| аниме/манга — самый визуально узнаваемый и однозначный стиль, проверяется первым; | |
| фэнтези/концепт-арт — вторая по специфичности категория; фотореализм — просьба | |
| "как фото" достаточно однозначна сама по себе; черновик/скетч — про скорость, а | |
| не стиль, поэтому последний перед дефолтом (стилевые сигналы важнее просьбы | |
| "побыстрее", если оба есть в одном промпте).""" | |
| if not prompt: | |
| return DEFAULT_HF_IMAGE_MODEL | |
| if _ANIME_RE.search(prompt): | |
| return "flux-anime" | |
| if _FANTASY_RE.search(prompt): | |
| return "dreamshaper" | |
| if _REALISM_RE.search(prompt): | |
| return "flux-realism" | |
| if _QUICK_RE.search(prompt): | |
| return "turbo" | |
| return DEFAULT_HF_IMAGE_MODEL | |
| async def _pollinations_generate(session: aiohttp.ClientSession, model_name: str, prompt: str) -> bytes: | |
| """Бесплатная генерация через Pollinations.ai — не требует авторизации. | |
| `session` передаётся вызывающим кодом (см. докстринг модуля) — раньше получалась | |
| неявно через `_get_http_session()` внутри этой же функции, когда она жила в bot.py.""" | |
| encoded = urllib.parse.quote(prompt[:600], safe="") | |
| url = ( | |
| f"https://image.pollinations.ai/prompt/{encoded}" | |
| f"?width=1024&height=1024&model={model_name}&nologo=true&enhance=false" | |
| ) | |
| async with session.get(url, timeout=aiohttp.ClientTimeout(total=90)) as resp: | |
| if resp.status == 200: | |
| ctype = (resp.headers.get("Content-Type") or "").lower() | |
| body = await resp.read() | |
| if body and (ctype.startswith("image/") or body[:4] in (b"\x89PNG", b"\xff\xd8\xff", b"RIFF", b"GIF8")): | |
| return body | |
| raise RuntimeError(f"Pollinations вернул не-изображение: {ctype}") | |
| raise RuntimeError(f"Pollinations.ai HTTP {resp.status}") | |
| async def _hf_text_to_image(session: aiohttp.ClientSession, model_id: str, prompt: str) -> bytes: | |
| # Pollinations.ai — единственный провайдер генерации изображений (HF Inference | |
| # API-ветка убрана: старые HF-модели регулярно устаревали на стороне провайдера, | |
| # см. историю: FLUX.1-dev вернул 410 Gone). Раз провайдер всего один, отдельная | |
| # "pollinations:" приставка на каждом ключе HF_IMAGE_MODELS была лишней — | |
| # model_id и есть имя модели Pollinations как есть. | |
| if model_id not in HF_IMAGE_MODELS: | |
| raise ValueError(f"Неизвестная модель генерации изображений: {model_id}") | |
| return await _pollinations_generate(session, model_id, prompt) | |
| def _image_model_label(model_id: str) -> str: | |
| meta = HF_IMAGE_MODELS.get(model_id, {}) | |
| return meta.get("name", model_id) | |