← back to kudinow__photoshoot_bot

Function bodies 69 total

All specs Real LLM only Function bodies
Settings class · python · L9-L31 (23 LOC)
bot/config.py
class Settings(BaseSettings):
    # Telegram
    bot_token: str

    # kie.ai
    kie_api_key: str
    kie_api_url: str = "https://kie.ai"

    # OpenRouter (замена OpenAI для работы из РФ)
    openrouter_api_key: str
    openrouter_base_url: str = "https://openrouter.ai/api/v1"

    # YooKassa
    yookassa_shop_id: str
    yookassa_secret_key: str
    yookassa_return_url: str = "https://t.me/photoshoot_generator_bot"

    # Settings
    debug: bool = False

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"
Config class · python · L29-L31 (3 LOC)
bot/config.py
    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"
CreditPackage class · python · L45-L52 (8 LOC)
bot/config.py
class CreditPackage:
    """Пакет генераций для покупки"""

    id: str
    credits: int
    price_rub: int
    price_kopecks: int
    label: str
get_package_by_id function · python · L80-L85 (6 LOC)
bot/config.py
def get_package_by_id(package_id: str) -> CreditPackage | None:
    """Возвращает пакет по его ID"""
    for pkg in CREDIT_PACKAGES:
        if pkg.id == package_id:
            return pkg
    return None
build_system_prompt function · python · L475-L478 (4 LOC)
bot/config.py
def build_system_prompt(gender: str, style: str) -> str:
    """Собирает полный системный промпт из базы + стиль одежды"""
    style_section = STYLE_PROMPTS.get((gender, style), PROMPT_CASUAL_MALE)
    return PROMPT_BASE + style_section + PROMPT_DIVERSITY_SUFFIX
show_packages function · python · L41-L70 (30 LOC)
bot/handlers/payment.py
async def show_packages(callback: CallbackQuery) -> None:
    """Показывает доступные пакеты генераций"""
    await callback.answer()

    user_id = callback.from_user.id
    paid = get_paid_credits(user_id)

    text = "💳 <b>Пакеты генераций</b>\n\n"

    for pkg in CREDIT_PACKAGES:
        per_unit = pkg.price_rub / pkg.credits
        text += f"• <b>{pkg.label}</b> ({per_unit:.0f} ₽/шт)\n"

    if paid > 0:
        text += (
            f"\n📊 У тебя сейчас: {paid} оплаченных генераций"
        )

    # Если кнопка была на сообщении с фото — edit_text невозможен,
    # отправляем новое сообщение
    if callback.message.photo:
        await callback.message.answer(
            text,
            reply_markup=get_packages_keyboard(),
        )
    else:
        await callback.message.edit_text(
            text,
            reply_markup=get_packages_keyboard(),
        )
select_package function · python · L74-L97 (24 LOC)
bot/handlers/payment.py
async def select_package(callback: CallbackQuery) -> None:
    """Пользователь выбрал пакет — показываем подтверждение"""
    await callback.answer()

    package_id = callback.data.split(":")[1]
    pkg = get_package_by_id(package_id)

    if not pkg:
        await callback.message.edit_text(
            "Пакет не найден. Попробуй ещё раз."
        )
        return

    text = (
        f"📦 <b>Подтверждение покупки</b>\n\n"
        f"Пакет: <b>{pkg.credits} генераций</b>\n"
        f"Стоимость: <b>{pkg.price_rub} ₽</b>\n\n"
        f"Нажми «Оплатить» для продолжения."
    )

    await callback.message.edit_text(
        text,
        reply_markup=get_confirm_package_keyboard(package_id),
    )
All rows above produced by Repobility · https://repobility.com
confirm_buy function · python · L101-L173 (73 LOC)
bot/handlers/payment.py
async def confirm_buy(
    callback: CallbackQuery, bot: Bot
) -> None:
    """
    Подтверждение покупки — создаёт платёж в YooKassa
    и отправляет ссылку на оплату.
    """
    await callback.answer()

    user_id = callback.from_user.id
    package_id = callback.data.split(":")[1]
    pkg = get_package_by_id(package_id)

    if not pkg:
        await callback.message.edit_text("Пакет не найден.")
        return

    # Создаём запись в нашей БД
    internal_id = create_payment(
        user_id=user_id,
        package_id=pkg.id,
        credits=pkg.credits,
        amount=pkg.price_kopecks,
    )

    # Создаём платёж в YooKassa
    try:
        yookassa_id, payment_url = await create_yookassa_payment(
            amount_kopecks=pkg.price_kopecks,
            description=f"{pkg.credits} генераций фото",
            user_id=user_id,
            package_id=pkg.id,
            internal_payment_id=internal_id,
        )
    except Exception as e:
        logger.error(
            f"Yoo
check_payment_status function · python · L177-L260 (84 LOC)
bot/handlers/payment.py
async def check_payment_status(callback: CallbackQuery) -> None:
    """Ручная проверка статуса платежа по кнопке"""
    await callback.answer("Проверяю статус оплаты...")

    internal_id = int(callback.data.split(":")[1])
    user_id = callback.from_user.id
    payment = get_payment(internal_id)

    if not payment:
        await callback.message.edit_text(
            "❌ Платёж не найден."
        )
        return

    if payment["status"] == "confirmed":
        # Уже подтверждён (возможно фоновым polling)
        remaining = get_remaining_generations(user_id)
        await callback.message.edit_text(
            f"✅ <b>Оплата уже подтверждена!</b>\n\n"
            f"Доступно генераций: <b>{remaining}</b>",
            reply_markup=get_after_payment_keyboard(),
        )
        return

    if payment["status"] != "pending":
        await callback.message.edit_text(
            "❌ Платёж отменён или истёк.\n"
            "Попробуй оформить новый.",
            reply_markup=get_pack
back_from_packages function · python · L264-L274 (11 LOC)
bot/handlers/payment.py
async def back_from_packages(
    callback: CallbackQuery, state: FSMContext
) -> None:
    """Возврат из экрана пакетов"""
    await callback.answer()

    await callback.message.edit_text(
        "Выбери стиль фотографии:",
        reply_markup=get_gender_keyboard(),
    )
    await state.set_state(GenerationStates.selecting_gender)
_poll_payment function · python · L277-L365 (89 LOC)
bot/handlers/payment.py
async def _poll_payment(
    bot: Bot,
    internal_id: int,
    yookassa_id: str,
    user_id: int,
    pkg,
) -> None:
    """
    Фоновая задача: периодически проверяет статус платежа
    в YooKassa и при успехе зачисляет кредиты.
    """
    elapsed = 0

    while elapsed < _POLL_MAX_DURATION:
        await asyncio.sleep(_POLL_INTERVAL)
        elapsed += _POLL_INTERVAL

        # Проверяем, не подтверждён ли уже (вручную)
        payment = get_payment(internal_id)
        if not payment or payment["status"] != "pending":
            logger.debug(
                f"Poll: payment {internal_id} "
                f"is no longer pending, stopping"
            )
            return

        try:
            status = await check_yookassa_payment(yookassa_id)
        except Exception as e:
            logger.warning(
                f"Poll: YooKassa check error "
                f"for {yookassa_id}: {e}"
            )
            continue

        if status == "succeeded":
            succ
handle_photo function · python · L24-L171 (148 LOC)
bot/handlers/photo.py
async def handle_photo(
    message: Message, state: FSMContext, bot: Bot
) -> None:
    """Обработчик получения фото"""
    user_id = message.from_user.id

    # Проверяем лимит генераций
    if not can_generate(user_id):
        await message.answer(
            "К сожалению, все генерации использованы 😔\n\n"
            "Купи пакет генераций, чтобы продолжить!",
            reply_markup=get_buy_keyboard(),
        )
        await state.clear()
        return

    await state.set_state(GenerationStates.processing)

    # Показываем оставшиеся генерации
    remaining = get_remaining_generations(user_id)
    remaining_text = (
        ""
        if remaining == -1
        else f"\n(Осталось генераций: {remaining - 1})"
    )

    # Отправляем сообщение о начале обработки
    processing_msg = await message.answer(
        "Фото получено! Создаю профессиональный портрет...\n"
        f"Это может занять 1-2 минуты.{remaining_text}"
    )

    try:
        # Получаем данные из состояния
  
handle_photo_without_state function · python · L175-L185 (11 LOC)
bot/handlers/photo.py
async def handle_photo_without_state(
    message: Message, state: FSMContext
) -> None:
    """Обработчик фото без выбранного стиля"""
    from bot.keyboards.inline import get_gender_keyboard

    await message.answer(
        "Сначала выбери стиль фотографии:",
        reply_markup=get_gender_keyboard(),
    )
    await state.set_state(GenerationStates.selecting_gender)
handle_not_photo function · python · L189-L195 (7 LOC)
bot/handlers/photo.py
async def handle_not_photo(message: Message) -> None:
    """Обработчик не-фото сообщений в состоянии ожидания фото"""
    await message.answer(
        "Пожалуйста, отправь фотографию.\n"
        "Лучше всего подойдёт портретное фото, "
        "где хорошо видно лицо."
    )
handle_message_while_processing function · python · L199-L205 (7 LOC)
bot/handlers/photo.py
async def handle_message_while_processing(
    message: Message,
) -> None:
    """Обработчик сообщений во время обработки"""
    await message.answer(
        "Подожди, я ещё обрабатываю предыдущее фото..."
    )
Open data scored by Repobility · https://repobility.com
cmd_start function · python · L33-L76 (44 LOC)
bot/handlers/start.py
async def cmd_start(message: Message, state: FSMContext, command: CommandObject) -> None:
    """Обработчик команды /start"""
    await state.clear()

    user_id = message.from_user.id

    # Сохраняем источник перехода (диплинк)
    source = command.args
    if source:
        save_referral(user_id, source)
        logger.info(f"User {user_id} came from: {source}")
    remaining = get_remaining_generations(user_id)

    # Формируем текст о лимите
    if is_admin(user_id):
        limit_text = "👑 У тебя безлимитный доступ."
    elif remaining > 0:
        paid = get_paid_credits(user_id)
        if paid > 0:
            limit_text = f"💳 У тебя {remaining} генераций ({paid} оплаченных)."
        else:
            limit_text = "🎁 У тебя 1 бесплатная генерация."
    else:
        limit_text = (
            "⚠️ Генерации закончились. "
            "Купи пакет, чтобы продолжить!"
        )

    welcome_text = (
        "Привет! Я помогу превратить твоё фото "
        "в профессиональный ст
cmd_stats function · python · L80-L97 (18 LOC)
bot/handlers/start.py
async def cmd_stats(message: Message) -> None:
    """Статистика источников трафика (только для админа)"""
    if not is_admin(message.from_user.id):
        return

    stats = get_referral_stats()
    if not stats:
        await message.answer("Нет данных о переходах.")
        return

    total = sum(count for _, count in stats)
    lines = ["📊 *Источники трафика:*\n"]
    for source, count in stats:
        pct = round(count / total * 100)
        lines.append(f"• `{source}`: {count} чел. ({pct}%)")
    lines.append(f"\nВсего: {total}")

    await message.answer("\n".join(lines), parse_mode="Markdown")
restart_generation function · python · L101-L112 (12 LOC)
bot/handlers/start.py
async def restart_generation(
    callback: CallbackQuery, state: FSMContext
) -> None:
    """Обработчик кнопки 'Создать ещё'"""
    await callback.answer()
    await state.clear()

    await callback.message.answer(
        "Выбери пол:",
        reply_markup=get_gender_keyboard(),
    )
    await state.set_state(GenerationStates.selecting_gender)
regenerate_photo function · python · L116-L261 (146 LOC)
bot/handlers/start.py
async def regenerate_photo(
    callback: CallbackQuery, state: FSMContext
) -> None:
    """Обработчик кнопки 'Сгенерировать заново'"""
    await callback.answer()

    user_id = callback.from_user.id

    # Проверяем лимит генераций
    if not can_generate(user_id):
        await callback.message.answer(
            "К сожалению, все генерации использованы 😔\n\n"
            "Купи пакет генераций, чтобы продолжить!",
            reply_markup=get_buy_keyboard(),
        )
        await state.clear()
        return

    # Получаем последнюю фотографию
    photo_url, gender, style = get_last_photo(user_id)

    if not photo_url or not gender:
        await callback.message.answer(
            "У меня нет сохранённой фотографии. "
            "Пожалуйста, отправь новое фото.",
            reply_markup=get_gender_keyboard(),
        )
        await state.set_state(GenerationStates.selecting_gender)
        return

    style = style or "casual"

    # Импортируем здесь, чтобы избежать цикл
select_gender function · python · L267-L282 (16 LOC)
bot/handlers/start.py
async def select_gender(
    callback: CallbackQuery, state: FSMContext
) -> None:
    """Обработчик выбора пола → переход к выбору стиля"""
    await callback.answer()

    gender = callback.data.split(":")[1]  # male или female
    await state.update_data(gender=gender)

    gender_text = "мужской" if gender == "male" else "женский"
    await callback.message.edit_text(
        f"Пол: {gender_text}\n\n"
        "Теперь выбери стиль одежды:",
        reply_markup=get_style_keyboard(),
    )
    await state.set_state(GenerationStates.selecting_style)
select_style function · python · L288-L307 (20 LOC)
bot/handlers/start.py
async def select_style(
    callback: CallbackQuery, state: FSMContext
) -> None:
    """Обработчик выбора стиля одежды → переход к загрузке фото"""
    await callback.answer()

    style = callback.data.split(":")[1]  # business, casual, creative
    await state.update_data(style=style)

    data = await state.get_data()
    gender = data.get("gender", "male")
    gender_text = "мужской" if gender == "male" else "женский"
    style_text = STYLE_LABELS.get(style, style)

    await callback.message.edit_text(
        f"Пол: {gender_text}, стиль: {style_text}\n\n"
        "Теперь отправь мне своё фото "
        "(лучше всего портретное, где хорошо видно лицо)."
    )
    await state.set_state(GenerationStates.awaiting_photo)
get_gender_keyboard function · python · L6-L15 (10 LOC)
bot/keyboards/inline.py
def get_gender_keyboard() -> InlineKeyboardMarkup:
    """Клавиатура выбора пола"""
    return InlineKeyboardMarkup(
        inline_keyboard=[
            [
                InlineKeyboardButton(text="👨 Мужской", callback_data="gender:male"),
                InlineKeyboardButton(text="👩 Женский", callback_data="gender:female"),
            ]
        ]
    )
get_style_keyboard function · python · L18-L26 (9 LOC)
bot/keyboards/inline.py
def get_style_keyboard() -> InlineKeyboardMarkup:
    """Клавиатура выбора стиля одежды"""
    return InlineKeyboardMarkup(
        inline_keyboard=[
            [InlineKeyboardButton(text="👔 Деловой", callback_data="style:business")],
            [InlineKeyboardButton(text="👕 Кежуал", callback_data="style:casual")],
            [InlineKeyboardButton(text="🎨 Креативный", callback_data="style:creative")],
        ]
    )
About: code-quality intelligence by Repobility · https://repobility.com
get_restart_keyboard function · python · L29-L49 (21 LOC)
bot/keyboards/inline.py
def get_restart_keyboard(
    has_last_photo: bool = False, has_credits: bool = True
) -> InlineKeyboardMarkup:
    """Клавиатура для повторной генерации"""
    buttons = []

    if has_last_photo:
        buttons.append([
            InlineKeyboardButton(text="🔄 Сгенерировать заново", callback_data="regenerate"),
        ])

    buttons.append([
        InlineKeyboardButton(text="✨ Создать с новым фото", callback_data="restart"),
    ])

    if not has_credits:
        buttons.append([
            InlineKeyboardButton(text="💳 Купить генерации", callback_data="buy_credits"),
        ])

    return InlineKeyboardMarkup(inline_keyboard=buttons)
get_buy_keyboard function · python · L52-L59 (8 LOC)
bot/keyboards/inline.py
def get_buy_keyboard() -> InlineKeyboardMarkup:
    """Клавиатура с кнопкой покупки (когда лимит исчерпан)"""
    return InlineKeyboardMarkup(
        inline_keyboard=[
            [InlineKeyboardButton(text="💳 Купить генерации", callback_data="buy_credits")],
            [InlineKeyboardButton(text="✨ Создать с новым фото", callback_data="restart")],
        ]
    )
get_packages_keyboard function · python · L62-L75 (14 LOC)
bot/keyboards/inline.py
def get_packages_keyboard() -> InlineKeyboardMarkup:
    """Клавиатура выбора пакета генераций"""
    buttons = []
    for pkg in CREDIT_PACKAGES:
        buttons.append([
            InlineKeyboardButton(
                text=pkg.label,
                callback_data=f"package:{pkg.id}",
            )
        ])
    buttons.append([
        InlineKeyboardButton(text="« Назад", callback_data="back_from_packages"),
    ])
    return InlineKeyboardMarkup(inline_keyboard=buttons)
get_confirm_package_keyboard function · python · L78-L91 (14 LOC)
bot/keyboards/inline.py
def get_confirm_package_keyboard(package_id: str) -> InlineKeyboardMarkup:
    """Клавиатура подтверждения покупки пакета"""
    return InlineKeyboardMarkup(
        inline_keyboard=[
            [InlineKeyboardButton(
                text="Оплатить",
                callback_data=f"confirm_buy:{package_id}",
            )],
            [InlineKeyboardButton(
                text="« Выбрать другой пакет",
                callback_data="buy_credits",
            )],
        ]
    )
get_payment_url_keyboard function · python · L94-L113 (20 LOC)
bot/keyboards/inline.py
def get_payment_url_keyboard(
    payment_url: str, payment_id: int
) -> InlineKeyboardMarkup:
    """Клавиатура со ссылкой на оплату и кнопкой проверки"""
    return InlineKeyboardMarkup(
        inline_keyboard=[
            [InlineKeyboardButton(
                text="💳 Перейти к оплате",
                url=payment_url,
            )],
            [InlineKeyboardButton(
                text="✅ Проверить оплату",
                callback_data=f"check_payment:{payment_id}",
            )],
            [InlineKeyboardButton(
                text="« Отмена",
                callback_data="buy_credits",
            )],
        ]
    )
get_after_payment_keyboard function · python · L116-L125 (10 LOC)
bot/keyboards/inline.py
def get_after_payment_keyboard() -> InlineKeyboardMarkup:
    """Клавиатура после успешной оплаты"""
    return InlineKeyboardMarkup(
        inline_keyboard=[
            [InlineKeyboardButton(
                text="📸 Создать фото",
                callback_data="restart",
            )],
        ]
    )
setup_logging function · python · L15-L22 (8 LOC)
bot/main.py
def setup_logging() -> None:
    """Настройка логирования"""
    level = logging.DEBUG if settings.debug else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        handlers=[logging.StreamHandler(sys.stdout)],
    )
main function · python · L25-L52 (28 LOC)
bot/main.py
async def main() -> None:
    """Точка входа"""
    setup_logging()
    logger = logging.getLogger(__name__)

    logger.info("Starting bot...")

    # Инициализируем БД
    init_db()

    # Создаём бота и диспетчер
    bot = Bot(
        token=settings.bot_token,
        default=DefaultBotProperties(parse_mode=ParseMode.HTML),
    )
    dp = Dispatcher(storage=MemoryStorage())

    # Регистрируем роутеры
    dp.include_router(start.router)
    dp.include_router(payment.router)
    dp.include_router(photo.router)

    # Запускаем бота
    try:
        logger.info("Bot started successfully!")
        await dp.start_polling(bot)
    finally:
        await bot.session.close()
Source: Repobility analyzer · https://repobility.com
KieClientError class · python · L12-L14 (3 LOC)
bot/services/kie_client.py
class KieClientError(Exception):
    """Ошибка клиента kie.ai"""
    pass
KieClient class · python · L17-L191 (175 LOC)
bot/services/kie_client.py
class KieClient:
    """Клиент для работы с kie.ai API"""

    def __init__(self):
        self.base_url = settings.kie_api_url
        self.api_key = settings.kie_api_key
        self.headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }

    async def create_task(
        self,
        image_url: str,
        prompt: str,
        output_format: str = "jpeg",
        image_size: str = "auto",
    ) -> str:
        """
        Создаёт задачу на генерацию изображения.

        Args:
            image_url: URL исходного изображения
            prompt: Промпт для трансформации
            output_format: Формат выхода (jpeg/png)
            image_size: Соотношение сторон

        Returns:
            str: ID созданной задачи
        """
        url = f"{self.base_url}/api/v1/jobs/createTask"

        payload = {
            "model": "google/nano-banana-edit",
            "input": {
                "prompt": prompt,
    
__init__ method · python · L20-L26 (7 LOC)
bot/services/kie_client.py
    def __init__(self):
        self.base_url = settings.kie_api_url
        self.api_key = settings.kie_api_key
        self.headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }
create_task method · python · L28-L79 (52 LOC)
bot/services/kie_client.py
    async def create_task(
        self,
        image_url: str,
        prompt: str,
        output_format: str = "jpeg",
        image_size: str = "auto",
    ) -> str:
        """
        Создаёт задачу на генерацию изображения.

        Args:
            image_url: URL исходного изображения
            prompt: Промпт для трансформации
            output_format: Формат выхода (jpeg/png)
            image_size: Соотношение сторон

        Returns:
            str: ID созданной задачи
        """
        url = f"{self.base_url}/api/v1/jobs/createTask"

        payload = {
            "model": "google/nano-banana-edit",
            "input": {
                "prompt": prompt,
                "image_urls": [image_url],
                "output_format": output_format,
                "image_size": image_size,
            }
        }

        async with aiohttp.ClientSession() as session:
            async with session.post(url, json=payload, headers=self.headers) as resp:
                
get_task_status method · python · L81-L102 (22 LOC)
bot/services/kie_client.py
    async def get_task_status(self, task_id: str) -> dict:
        """
        Получает статус задачи.

        Args:
            task_id: ID задачи

        Returns:
            dict: Информация о задаче (поле data из ответа)
        """
        url = f"{self.base_url}/api/v1/jobs/recordInfo"
        params = {"taskId": task_id}

        async with aiohttp.ClientSession() as session:
            async with session.get(url, params=params, headers=self.headers) as resp:
                if resp.status != 200:
                    text = await resp.text()
                    logger.error(f"Failed to get task status: {resp.status} - {text}")
                    raise KieClientError(f"Failed to get task status: {resp.status}")

                response = await resp.json()
                return response.get("data", {})
wait_for_result method · python · L104-L153 (50 LOC)
bot/services/kie_client.py
    async def wait_for_result(
        self,
        task_id: str,
        timeout: int = 300,
        poll_interval: int = 3,
    ) -> str:
        """
        Ожидает завершения задачи и возвращает URL результата.

        Args:
            task_id: ID задачи
            timeout: Максимальное время ожидания в секундах
            poll_interval: Интервал между проверками в секундах

        Returns:
            str: URL готового изображения
        """
        import json

        elapsed = 0

        while elapsed < timeout:
            data = await self.get_task_status(task_id)
            logger.debug(f"Task {task_id} status: {data}")

            state = data.get("state")

            if state == "success":
                # resultJson содержит JSON-строку: {"resultUrls": ["https://..."]}
                result_json_str = data.get("resultJson")
                if result_json_str:
                    try:
                        result_data = json.loads(result_json_str)
           
transform_photo method · python · L155-L175 (21 LOC)
bot/services/kie_client.py
    async def transform_photo(
        self,
        image_url: str,
        prompt: str,
        output_format: str = "jpeg",
        image_size: str = "auto",
    ) -> str:
        """
        Полный цикл трансформации фото: создание задачи и ожидание результата.

        Args:
            image_url: URL исходного изображения
            prompt: Промпт для трансформации
            output_format: Формат выхода (jpeg/png)
            image_size: Соотношение сторон

        Returns:
            str: URL готового изображения
        """
        task_id = await self.create_task(image_url, prompt, output_format, image_size)
        return await self.wait_for_result(task_id)
download_image method · python · L177-L191 (15 LOC)
bot/services/kie_client.py
    async def download_image(self, url: str) -> bytes:
        """
        Скачивает изображение по URL.

        Args:
            url: URL изображения

        Returns:
            bytes: Содержимое изображения
        """
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                if resp.status != 200:
                    raise KieClientError(f"Failed to download image: {resp.status}")
                return await resp.read()
All rows above produced by Repobility · https://repobility.com
OpenAIClientError class · python · L16-L19 (4 LOC)
bot/services/openai_client.py
class OpenAIClientError(Exception):
    """Ошибка клиента OpenAI/OpenRouter"""

    pass
OpenAIClient class · python · L22-L111 (90 LOC)
bot/services/openai_client.py
class OpenAIClient:
    """Клиент для генерации промптов через OpenRouter API.

    Совместим с OpenAI SDK.
    """

    def __init__(self) -> None:
        self.client = AsyncOpenAI(
            api_key=settings.openrouter_api_key,
            base_url=settings.openrouter_base_url,
        )
        # OpenRouter использует тот же формат модели
        # GPT-5.2 для максимального качества генерации промптов
        self.model = "openai/gpt-5.2"

    async def generate_prompt(
        self, gender: str, style: str = "casual", max_retries: int = 3
    ) -> str:
        """
        Генерирует промпт для изображения на основе пола и стиля.

        Args:
            gender: "male" или "female"
            style: "casual", "business" или "creative"
            max_retries: максимальное количество попыток

        Returns:
            Сгенерированный промпт для kie.ai
        """
        gender_text = "мужчины" if gender == "male" else "женщины"
        style_text = STYLE_LABELS.get(style, "
__init__ method · python · L28-L35 (8 LOC)
bot/services/openai_client.py
    def __init__(self) -> None:
        self.client = AsyncOpenAI(
            api_key=settings.openrouter_api_key,
            base_url=settings.openrouter_base_url,
        )
        # OpenRouter использует тот же формат модели
        # GPT-5.2 для максимального качества генерации промптов
        self.model = "openai/gpt-5.2"
generate_prompt method · python · L37-L111 (75 LOC)
bot/services/openai_client.py
    async def generate_prompt(
        self, gender: str, style: str = "casual", max_retries: int = 3
    ) -> str:
        """
        Генерирует промпт для изображения на основе пола и стиля.

        Args:
            gender: "male" или "female"
            style: "casual", "business" или "creative"
            max_retries: максимальное количество попыток

        Returns:
            Сгенерированный промпт для kie.ai
        """
        gender_text = "мужчины" if gender == "male" else "женщины"
        style_text = STYLE_LABELS.get(style, "кежуал")
        system_prompt = build_system_prompt(gender, style)

        user_message = (
            f"Сгенерируй один уникальный промпт для профессионального "
            f"студийного портрета {gender_text} в стиле «{style_text}». "
            f"Следуй структуре промпта из гайдлайнов. "
            f"ВАЖНО: Выбери РАЗНЫЕ предметы одежды, цвет и текстуру, "
            f"чем в предыдущих примерах. "
            f"Создай оригинальную комбин
_get_db_path function · python · L26-L30 (5 LOC)
bot/services/user_limits.py
def _get_db_path() -> Path:
    """Возвращает путь к файлу БД"""
    if _PROD_DIR.exists():
        return _PROD_DB
    return _LOCAL_DB
_get_json_path function · python · L33-L37 (5 LOC)
bot/services/user_limits.py
def _get_json_path() -> Path:
    """Возвращает путь к старому JSON-файлу (для миграции)"""
    if _PROD_DIR.exists():
        return _PROD_JSON
    return _LOCAL_JSON
_get_conn function · python · L40-L42 (3 LOC)
bot/services/user_limits.py
def _get_conn() -> sqlite3.Connection:
    """Возвращает соединение с БД"""
    return sqlite3.connect(_get_db_path())
init_db function · python · L45-L104 (60 LOC)
bot/services/user_limits.py
def init_db() -> None:
    """Инициализирует БД и мигрирует данные из JSON, если он существует"""
    with _get_conn() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS users (
                user_id INTEGER PRIMARY KEY,
                generations INTEGER NOT NULL DEFAULT 0,
                last_photo_url TEXT,
                last_gender TEXT
            )
        """)

        # Миграция: добавляем колонку paid_credits
        try:
            conn.execute(
                "ALTER TABLE users ADD COLUMN paid_credits INTEGER NOT NULL DEFAULT 0"
            )
            logger.info("Added paid_credits column to users table")
        except sqlite3.OperationalError:
            pass  # Колонка уже существует

        # Миграция: добавляем колонку last_style
        try:
            conn.execute(
                "ALTER TABLE users ADD COLUMN last_style TEXT"
            )
            logger.info("Added last_style column to users table")
        except sqlite3.Ope
Open data scored by Repobility · https://repobility.com
_migrate_from_json function · python · L107-L147 (41 LOC)
bot/services/user_limits.py
def _migrate_from_json(json_path: Path) -> None:
    """Мигрирует данные из старого JSON-файла в SQLite"""
    try:
        data = json.loads(json_path.read_text())
    except Exception as e:
        logger.error(f"Failed to read JSON for migration: {e}")
        return

    migrated = 0
    with _get_conn() as conn:
        for user_key, user_data in data.items():
            try:
                user_id = int(user_key)
            except ValueError:
                continue

            # Поддержка старого формата (просто число)
            if isinstance(user_data, int):
                generations = user_data
                last_photo_url = None
                last_gender = None
            else:
                generations = user_data.get("generations", 0)
                last_photo_url = user_data.get("last_photo_url")
                last_gender = user_data.get("last_gender")

            conn.execute(
                """INSERT OR IGNORE INTO users
                   (user_id, 
is_admin function · python · L150-L152 (3 LOC)
bot/services/user_limits.py
def is_admin(user_id: int) -> bool:
    """Проверяет, является ли пользователь админом"""
    return user_id == ADMIN_ID
get_generations_count function · python · L155-L162 (8 LOC)
bot/services/user_limits.py
def get_generations_count(user_id: int) -> int:
    """Возвращает количество использованных бесплатных генераций"""
    with _get_conn() as conn:
        row = conn.execute(
            "SELECT generations FROM users WHERE user_id = ?",
            (user_id,),
        ).fetchone()
    return row[0] if row else 0
page 1 / 2next ›