import os import io import base64 import json import logging import threading import time import math from datetime import datetime, timedelta, timezone from uuid import uuid4 import random import string from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session, make_response from PIL import Image import google.generativeai as genai from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError from werkzeug.utils import secure_filename from dotenv import load_dotenv import requests load_dotenv() app = Flask(__name__) app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login' DATA_FILE = 'data.json' SYNC_FILES = [DATA_FILE] REPO_ID = "Kgshop/bc" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") DOWNLOAD_RETRIES = 3 DOWNLOAD_DELAY = 5 ALMATY_TZ = timezone(timedelta(hours=6)) db_lock = threading.RLock() CURRENCIES = { 'KGS': 'Кыргызский сом', 'KZT': 'Казахстанский тенге', 'UAH': 'Украинская гривна', 'RUB': 'Российский рубль', 'USD': 'Доллар США', 'EUR': 'Евро' } COLOR_SCHEMES = { 'default': 'Бирюзовый (по умолч.)', 'forest': 'Лесной зеленый', 'ocean': 'Глубокий синий', 'sunset': 'Закатный оранжевый', 'lavender': 'Лавандовый', 'vintage': 'Винтажный', 'dark': 'Полночь (тёмная)', 'cosmic': 'Космическая ночь', 'minty': 'Свежая мята', 'mocha': 'Кофейный мокко', 'crimson': 'Багровый рассвет', 'solar': 'Солнечная вспышка', 'cyberpunk': 'Киберпанк неон', 'neon': 'Неоновая вспышка', 'pastel': 'Пастельный (светлый)', 'emerald': 'Изумрудный город', 'gold': 'Роскошное золото', 'sakura': 'Цветение сакуры (светлый)', 'arctic': 'Арктический лед (светлый)', 'volcano': 'Магма', 'monochrome_light': 'Классика (Светлая)', 'monochrome_dark': 'Классика (Темная)', 'nord': 'Скандинавский Норд', 'dracula': 'Дракула (Темный)', 'ruby': 'Глубокий Рубин', 'sapphire': 'Королевский Сапфир', 'amethyst': 'Аметистовый блеск' } ICONS = { 'fa-link': 'Ссылка (вебсайт)', 'fa-phone': 'Телефон', 'fa-whatsapp': 'WhatsApp', 'fa-telegram': 'Telegram', 'fa-instagram': 'Instagram', 'fa-envelope': 'Email', 'fa-map-marker-alt': 'Локация/Адрес', 'fa-youtube': 'YouTube', 'fa-tiktok': 'TikTok' } logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): if not HF_TOKEN_READ and not HF_TOKEN_WRITE: pass token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE files_to_download = [specific_file] if specific_file else SYNC_FILES all_successful = True for file_name in files_to_download: success = False for attempt in range(retries + 1): try: local_path = hf_hub_download( repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False ) success = True break except RepositoryNotFoundError: return False except HfHubHTTPError as e: if e.response.status_code == 404: if attempt == 0 and not os.path.exists(file_name): try: if file_name == DATA_FILE: with open(file_name, 'w', encoding='utf-8') as f: json.dump({}, f) except Exception: pass success = False break else: pass except requests.exceptions.RequestException: pass except Exception: pass if attempt < retries: time.sleep(delay) if not success: all_successful = False return all_successful def upload_db_to_hf(specific_file=None): if not HF_TOKEN_WRITE: return try: api = HfApi() files_to_upload = [specific_file] if specific_file else SYNC_FILES for file_name in files_to_upload: if os.path.exists(file_name): try: api.upload_file( path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')}" ) except Exception: pass except Exception: pass def periodic_backup(): backup_interval = 1800 while True: time.sleep(backup_interval) upload_db_to_hf() def load_data(): with db_lock: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if not isinstance(data, dict): data = {} except (FileNotFoundError, json.JSONDecodeError): if download_db_from_hf(specific_file=DATA_FILE): try: with open(DATA_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if not isinstance(data, dict): data = {} except (FileNotFoundError, json.JSONDecodeError): data = {} else: data = {} return data def save_data(data): with db_lock: try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) except Exception: pass upload_db_to_hf(specific_file=DATA_FILE) def get_env_data(env_id): with db_lock: all_data = load_data() default_settings = { "vcard_firstname": "Имя", "vcard_lastname": "Фамилия", "vcard_job": "Специалист", "organization_name": "Моя Компания", "currency_code": "KGS", "chat_avatar": None, "color_scheme": "default", "admin_password_enabled": False, "admin_password": "", "categories_as_lines": False, "about_text": "Привет! Это моя онлайн-визитка." } env_data = all_data.get(env_id, {}) if not env_data: env_data = { 'products': [], 'categories':[], 'blocks':[], 'settings': default_settings } if 'products' not in env_data: env_data['products'] = [] if 'categories' not in env_data: env_data['categories'] = [] if 'settings' not in env_data: env_data['settings'] = default_settings if 'blocks' not in env_data: env_data['blocks'] = [] settings_changed = False for key, value in default_settings.items(): if key not in env_data['settings']: env_data['settings'][key] = value settings_changed = True products_changed = False for product in env_data['products']: if 'product_id' not in product: product['product_id'] = uuid4().hex products_changed = True if 'views' not in product: product['views'] = 0 products_changed = True if 'price' not in product: product['price'] = 0.0 products_changed = True if products_changed or settings_changed: save_env_data(env_id, env_data) return env_data def save_env_data(env_id, env_data): with db_lock: all_data = load_data() all_data[env_id] = env_data save_data(all_data) def configure_gemini(): if not GOOGLE_API_KEY: return False try: genai.configure(api_key=GOOGLE_API_KEY) return True except Exception: return False def generate_ai_description_from_image(image_data, language): if not configure_gemini(): raise ValueError("Google AI API не настроен.") try: if not image_data: raise ValueError("Файл изображения не найден.") image_stream = io.BytesIO(image_data) image = Image.open(image_stream).convert('RGB') except Exception: raise ValueError("Не удалось обработать изображение.") base_prompt = "Напиши привлекательное описание для этого товара/услуги. Текст должен быть емким, продающим, с использованием эмодзи. Не пиши цены, адреса и телефоны." lang_suffix = "" if language == "Русский": lang_suffix = " Пиши на русском языке." elif language == "Кыргызский": lang_suffix = " Пиши на кыргызском языке." elif language == "Казахский": lang_suffix = " Пиши на казахском языке." elif language == "Узбекский": lang_suffix = " Пиши на узбекском языке." final_prompt = f"{base_prompt}{lang_suffix}" try: model = genai.GenerativeModel('gemma-4-31b-it') response = model.generate_content([final_prompt, image]) if hasattr(response, 'text'): return response.text else: if response.parts: return "".join(part.text for part in response.parts if hasattr(part, 'text')) else: response.resolve() return response.text except Exception as e: raise ValueError(f"Ошибка при генерации контента: {e}") LANDING_PAGE_TEMPLATE = ''' Платформа Онлайн-Визиток ''' LOGIN_TEMPLATE = ''' Вход в Админ-панель

Вход

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
''' ADMHOSTO_TEMPLATE = ''' Главная Админ-панель

Управление Визитками

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}

Существующие визитки

{% if environments %} {% else %}

Пока не создано ни одной визитки.

{% endif %}
''' CATALOG_TEMPLATE = ''' {{ settings.vcard_firstname }} {{ settings.vcard_lastname }} - Визитка
Avatar
{{ settings.vcard_firstname }} {{ settings.vcard_lastname }}
{% if settings.vcard_job %}
{{ settings.vcard_job }}
{% endif %} {% if settings.organization_name %}
{{ settings.organization_name }}
{% endif %} {% if settings.about_text %}
{{ settings.about_text|replace('\\n', '
')|safe }}
{% endif %}
Сохранить в контакты {% if blocks %}
{% for block in blocks %} {% if block.type == 'link' %} {% if block.icon %} {% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %} {% endif %} {{ block.title }} {% elif block.type == 'text' %}
{% if block.title %}

{{ block.title }}

{% endif %}

{{ block.content|replace('\\n', '
')|safe }}

{% endif %} {% endfor %}
{% endif %} {% if products_json != '[]' %}

Каталог / Услуги

{% endif %}
''' ADMIN_TEMPLATE = ''' Настройки Визитки - {{ settings.organization_name }}

Сохранение данных...

Logo

Настройки Визитки

Открыть визитку {% if settings.admin_password_enabled %} Выход {% endif %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
Профиль и Внешний вид
{% if settings.chat_avatar %}

Текущий аватар:

{% endif %}

Пароль для входа сюда

Кнопки и Ссылки (для Визитки)

Добавить кнопку
{% if blocks %} {% for block in blocks %}
{% if block.icon %} {% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %} {% endif %} {{ block.title }} ({{ 'Ссылка' if block.type == 'link' else 'Текст' }}) {% if block.type == 'link' %}
{{ block.url }}{% endif %}
{% endfor %} {% else %}

Кнопки не добавлены.

{% endif %}

Категории товаров/услуг

Добавить новую категорию
{% if categories %}
{% for category in categories %}
{{ category }}
{% endfor %}
{% else %}

Категорий пока нет.

{% endif %}

Товары и Услуги

Добавить товар/услугу

Список товаров/услуг:

{% if search_q %} Сброс {% endif %}
{% if paginated_products %}
{% for product in paginated_products %}
{% if product.get('photos') %} Фото {% else %} Нет фото {% endif %}

{{ product['name'] }}

Категория: {{ product.get('category', 'Без категории') }}

Цена: {% if product.get('price', 0) > 0 %}{{ "%.2f"|format(product.get('price', 0)) }} {{ currency_code }}{% else %}Не указана{% endif %}

Редактирование

{% endfor %}
{% if total_pages > 1 %} {% endif %} {% else %}

Записей пока нет или по вашему запросу ничего не найдено.

{% endif %}
''' @app.route('/') def index(): return render_template_string(LANDING_PAGE_TEMPLATE) @app.route('/admhosto', methods=['GET']) def admhosto(): data = load_data() environments_data = [] for env_id, env_data in data.items(): settings = env_data.get('settings', {}) org_name = settings.get("organization_name", f"Визитка {env_id}") environments_data.append({ "id": env_id, "org_name": org_name, "pwd_enabled": settings.get("admin_password_enabled", False), "password": settings.get("admin_password", "") }) environments_data.sort(key=lambda x: x['id']) return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data) @app.route('/admhosto/create', methods=['POST']) def create_environment(): all_data = load_data() while True: new_id = ''.join(random.choices(string.digits, k=6)) if new_id not in all_data: break all_data[new_id] = { 'products': [], 'categories':[], 'blocks':[], 'settings': { "vcard_firstname": "Имя", "vcard_lastname": "Фамилия", "vcard_job": "Специалист", "organization_name": "Моя Компания", "currency_code": "KGS", "chat_avatar": None, "color_scheme": "default", "admin_password_enabled": False, "admin_password": "", "categories_as_lines": False, "about_text": "Привет! Это моя онлайн-визитка." } } save_data(all_data) flash(f'Новая визитка с ID {new_id} успешно создана.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/update_pwd/', methods=['POST']) def update_env_pwd(env_id): all_data = load_data() if env_id in all_data: pwd_enabled = 'pwd_enabled' in request.form password = request.form.get('password', '').strip() all_data[env_id]['settings']['admin_password_enabled'] = pwd_enabled all_data[env_id]['settings']['admin_password'] = password save_data(all_data) flash(f'Пароль для визитки {env_id} обновлен.', 'success') else: flash(f'Визитка {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) @app.route('/admhosto/delete/', methods=['POST']) def delete_environment(env_id): all_data = load_data() if env_id in all_data: del all_data[env_id] save_data(all_data) flash(f'Визитка {env_id} была удалена.', 'success') else: flash(f'Визитка {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) @app.route('//login', methods=['GET', 'POST']) def admin_login(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if not settings.get('admin_password_enabled'): return redirect(url_for('admin', env_id=env_id)) if request.method == 'POST': pwd = request.form.get('password', '') if pwd == settings.get('admin_password', ''): session[f'admin_auth_{env_id}'] = True return redirect(url_for('admin', env_id=env_id)) else: flash('Неверный пароль', 'error') return render_template_string(LOGIN_TEMPLATE, env_id=env_id) @app.route('//logout') def admin_logout(env_id): session.pop(f'admin_auth_{env_id}', None) return redirect(url_for('admin_login', env_id=env_id)) @app.route('//vcard') def download_vcard(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) blocks = data.get('blocks', []) first_name = settings.get("vcard_firstname", "") last_name = settings.get("vcard_lastname", "") org = settings.get("organization_name", "") title = settings.get("vcard_job", "") note = settings.get("about_text", "").replace('\n', '\\n') card_url = url_for('catalog', env_id=env_id, _external=True) vcard = [ "BEGIN:VCARD", "VERSION:3.0", f"N:{last_name};{first_name};;;", f"FN:{first_name} {last_name}".strip(), ] if org: vcard.append(f"ORG:{org}") if title: vcard.append(f"TITLE:{title}") if note: vcard.append(f"NOTE:{note}") vcard.append(f"URL;type=pref:{card_url}") for b in blocks: if b.get('type') == 'link' and b.get('url'): url = b.get('url', '').strip() if url.startswith('+') or url.replace('-', '').replace(' ', '').isdigit(): vcard.append(f"TEL;TYPE=CELL:{url}") elif '@' in url and not url.startswith('http'): vcard.append(f"EMAIL;TYPE=WORK:{url.replace('mailto:', '')}") else: vcard.append(f"URL:{url}") vcard.append("END:VCARD") vcard_str = "\r\n".join(vcard) response = make_response(vcard_str) response.headers["Content-Disposition"] = f"attachment; filename=contact_{env_id}.vcf" response.headers["Content-Type"] = "text/vcard; charset=utf-8" return response @app.route('//catalog') def catalog(env_id): data = get_env_data(env_id) all_products = data.get('products',[]) settings = data.get('settings', {}) blocks = data.get('blocks',[]) product_categories = set(p.get('category', 'Без категории') for p in all_products) admin_categories = set(data.get('categories',[])) all_cat_names = sorted(list(product_categories.union(admin_categories))) products_sorted_for_js = sorted(all_products, key=lambda p: p.get('name', '').lower()) products_by_category = {cat:[] for cat in all_cat_names} for product in all_products: products_by_category[product.get('category', 'Без категории')].append(product) ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)] chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" return render_template_string( CATALOG_TEMPLATE, ordered_categories=ordered_categories, products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id, blocks=blocks ) @app.route('//admin', methods=['GET', 'POST']) def admin(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) products = data.get('products',[]) categories = data.get('categories',[]) blocks = data.get('blocks',[]) page = request.args.get('p', 1, type=int) search_q = request.args.get('q', '').strip() if request.method == 'POST': action = request.form.get('action') try: if action == 'add_block': b_type = request.form.get('block_type') b_title = request.form.get('block_title', '').strip() b_icon = request.form.get('block_icon', '') b_url = request.form.get('block_url', '').strip() if b_type == 'link' and b_url: if not b_url.startswith(('http://', 'https://', 'mailto:', 'tel:')) and not b_url.startswith('+'): if '@' in b_url: b_url = 'mailto:' + b_url else: b_url = 'https://' + b_url elif b_url.startswith('+'): b_url = 'tel:' + b_url.replace(' ', '').replace('-', '') b_content = request.form.get('block_content', '').strip() blocks.append({ 'id': uuid4().hex[:8], 'type': b_type, 'title': b_title, 'icon': b_icon if b_type == 'link' else '', 'url': b_url, 'content': b_content }) data['blocks'] = blocks save_env_data(env_id, data) flash("Блок добавлен.", "success") elif action == 'delete_block': b_id = request.form.get('block_id') data['blocks'] = [b for b in blocks if b.get('id') != b_id] save_env_data(env_id, data) flash("Блок удален.", "success") elif action == 'move_block_up': b_id = request.form.get('block_id') idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1) if idx > 0: blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx] data['blocks'] = blocks save_env_data(env_id, data) flash("Блок перемещен выше.", "success") elif action == 'move_block_down': b_id = request.form.get('block_id') idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1) if idx != -1 and idx < len(blocks) - 1: blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx] data['blocks'] = blocks save_env_data(env_id, data) flash("Блок перемещен ниже.", "success") elif action == 'add_category': category_name = request.form.get('category_name', '').strip() if category_name and category_name not in categories: categories.append(category_name) data['categories'] = categories save_env_data(env_id, data) flash(f"Категория '{category_name}' успешно добавлена.", 'success') elif not category_name: flash("Название категории не может быть пустым.", 'error') else: flash(f"Категория '{category_name}' уже существует.", 'error') elif action == 'delete_category': category_to_delete = request.form.get('category_name') if category_to_delete and category_to_delete in categories: categories.remove(category_to_delete) updated_count = 0 for product in products: if product.get('category') == category_to_delete: product['category'] = 'Без категории' updated_count += 1 data['categories'] = categories data['products'] = products save_env_data(env_id, data) flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров/услуг обновлено.", 'success') else: flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error') elif action == 'update_settings': settings['admin_password_enabled'] = 'admin_password_enabled' in request.form settings['admin_password'] = request.form.get('admin_password', '').strip() settings['vcard_firstname'] = request.form.get('vcard_firstname', '').strip() settings['vcard_lastname'] = request.form.get('vcard_lastname', '').strip() settings['vcard_job'] = request.form.get('vcard_job', '').strip() settings['organization_name'] = request.form.get('organization_name', '').strip() settings['about_text'] = request.form.get('about_text', '').strip() settings['currency_code'] = request.form.get('currency_code', 'KGS') settings['color_scheme'] = request.form.get('color_scheme', 'default') avatar_file = request.files.get('chat_avatar') if avatar_file and avatar_file.filename: if HF_TOKEN_WRITE: try: api = HfApi() old_avatar = settings.get('chat_avatar') if old_avatar: try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass ext = os.path.splitext(avatar_file.filename)[1].lower() avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}" uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) temp_path = os.path.join(uploads_dir, avatar_filename) avatar_file.save(temp_path) api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"avatars/{avatar_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) settings['chat_avatar'] = avatar_filename os.remove(temp_path) flash("Аватар успешно обновлен.", 'success') except Exception as e: flash(f"Ошибка при загрузке аватара: {e}", 'error') else: flash("HF_TOKEN (write) не настроен. Аватар не был загружен.", "warning") data['settings'] = settings save_env_data(env_id, data) flash("Настройки успешно обновлены.", 'success') elif action == 'add_product' or action == 'edit_product': product_id = request.form.get('product_id') product_data = {} is_edit = action == 'edit_product' if is_edit: product_data = next((p for p in products if p.get('product_id') == product_id), None) if not product_data: flash(f"Ошибка: запись не найдена.", 'error') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) else: product_data['views'] = 0 product_data['name'] = request.form.get('name', '').strip() product_data['description'] = request.form.get('description', '').strip() try: product_data['price'] = float(request.form.get('price', 0)) except ValueError: product_data['price'] = 0.0 category = request.form.get('category') product_data['category'] = category if category in categories else 'Без категории' if not product_data['name']: flash("Название обязательно.", 'error') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) photos_files = request.files.getlist('photos') if photos_files and any(f.filename for f in photos_files): if HF_TOKEN_WRITE: uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) api = HfApi() new_photos_list = [] photo_limit = 10 uploaded_count = 0 for photo in photos_files: if uploaded_count >= photo_limit: break if photo and photo.filename: try: ext = os.path.splitext(photo.filename)[1].lower() if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50] photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}" temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) new_photos_list.append(photo_filename) os.remove(temp_path) uploaded_count += 1 except Exception as e: flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error') if new_photos_list and is_edit and product_data.get('photos'): try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass if new_photos_list: product_data['photos'] = new_photos_list else: flash("HF_TOKEN не настроен. Фотографии не загружены.", "warning") if is_edit: product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) if product_index != -1: products[product_index] = product_data flash(f"Запись '{product_data['name']}' обновлена.", 'success') else: product_data['product_id'] = uuid4().hex products.append(product_data) flash(f"Запись '{product_data['name']}' добавлена.", 'success') data['products'] = products save_env_data(env_id, data) elif action == 'delete_product': product_id = request.form.get('product_id') product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) if product_index == -1: flash(f"Ошибка удаления: запись не найдена.", 'error') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) deleted_product = products.pop(product_index) product_name = deleted_product.get('name', 'N/A') photos_to_delete = deleted_product.get('photos', []) if photos_to_delete and HF_TOKEN_WRITE: try: api = HfApi() api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass data['products'] = products save_env_data(env_id, data) flash(f"Запись '{product_name}' удалена.", 'success') else: flash(f"Неизвестное действие: {action}", 'warning') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) except Exception as e: flash(f"Ошибка при выполнении действия: {e}", 'error') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) filtered_products = products if search_q: q_lower = search_q.lower() filtered_products = [p for p in products if q_lower in p.get('name', '').lower() or q_lower in p.get('description', '').lower()] filtered_products = sorted(filtered_products, key=lambda p: p.get('name', '').lower()) PER_PAGE = 20 total_items = len(filtered_products) total_pages = math.ceil(total_items / PER_PAGE) if total_items > 0 else 1 if page < 1: page = 1 if page > total_pages: page = total_pages start_idx = (page - 1) * PER_PAGE end_idx = start_idx + PER_PAGE paginated_products = filtered_products[start_idx:end_idx] display_categories = sorted(categories) display_settings = settings chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{display_settings['chat_avatar']}" if display_settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" return render_template_string( ADMIN_TEMPLATE, paginated_products=paginated_products, total_pages=total_pages, page=page, search_q=search_q, categories=display_categories, settings=display_settings, blocks=blocks, repo_id=REPO_ID, currency_code=display_settings.get('currency_code', 'KGS'), chat_avatar_url=chat_avatar_url, currencies=CURRENCIES, color_schemes=COLOR_SCHEMES, icons=ICONS, env_id=env_id ) @app.route('/generate_description_ai', methods=['POST']) def handle_generate_description_ai(): request_data = request.get_json() base64_image = request_data.get('image') language = request_data.get('language', 'Русский') if not base64_image: return jsonify({"error": "Изображение не найдено в запросе."}), 400 try: image_data = base64.b64decode(base64_image) result_text = generate_ai_description_from_image(image_data, language) return jsonify({"text": result_text}) except ValueError as ve: return jsonify({"error": str(ve)}), 400 except Exception as e: return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500 if __name__ == '__main__': configure_gemini() download_db_from_hf() load_data() if HF_TOKEN_WRITE: backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() port = int(os.environ.get('PORT', 7860)) app.run(debug=False, host='0.0.0.0', port=port) import os import io import base64 import json import logging import threading import time import math from datetime import datetime, timedelta, timezone from uuid import uuid4 import random import string from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session, make_response from PIL import Image import google.generativeai as genai from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError from werkzeug.utils import secure_filename from dotenv import load_dotenv import requests load_dotenv() app = Flask(__name__) app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login' DATA_FILE = 'data.json' SYNC_FILES = [DATA_FILE] REPO_ID = "Kgshop/bc" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") DOWNLOAD_RETRIES = 3 DOWNLOAD_DELAY = 5 ALMATY_TZ = timezone(timedelta(hours=6)) db_lock = threading.RLock() CURRENCIES = { 'KGS': 'Кыргызский сом', 'KZT': 'Казахстанский тенге', 'UAH': 'Украинская гривна', 'RUB': 'Российский рубль', 'USD': 'Доллар США', 'EUR': 'Евро' } COLOR_SCHEMES = { 'default': 'Бирюзовый (по умолч.)', 'forest': 'Лесной зеленый', 'ocean': 'Глубокий синий', 'sunset': 'Закатный оранжевый', 'lavender': 'Лавандовый', 'vintage': 'Винтажный', 'dark': 'Полночь (тёмная)', 'cosmic': 'Космическая ночь', 'minty': 'Свежая мята', 'mocha': 'Кофейный мокко', 'crimson': 'Багровый рассвет', 'solar': 'Солнечная вспышка', 'cyberpunk': 'Киберпанк неон', 'neon': 'Неоновая вспышка', 'pastel': 'Пастельный (светлый)', 'emerald': 'Изумрудный город', 'gold': 'Роскошное золото', 'sakura': 'Цветение сакуры (светлый)', 'arctic': 'Арктический лед (светлый)', 'volcano': 'Магма', 'monochrome_light': 'Классика (Светлая)', 'monochrome_dark': 'Классика (Темная)', 'nord': 'Скандинавский Норд', 'dracula': 'Дракула (Темный)', 'ruby': 'Глубокий Рубин', 'sapphire': 'Королевский Сапфир', 'amethyst': 'Аметистовый блеск' } ICONS = { 'fa-link': 'Ссылка (вебсайт)', 'fa-phone': 'Телефон', 'fa-whatsapp': 'WhatsApp', 'fa-telegram': 'Telegram', 'fa-instagram': 'Instagram', 'fa-envelope': 'Email', 'fa-map-marker-alt': 'Локация/Адрес', 'fa-youtube': 'YouTube', 'fa-tiktok': 'TikTok' } logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): if not HF_TOKEN_READ and not HF_TOKEN_WRITE: pass token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE files_to_download = [specific_file] if specific_file else SYNC_FILES all_successful = True for file_name in files_to_download: success = False for attempt in range(retries + 1): try: local_path = hf_hub_download( repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False ) success = True break except RepositoryNotFoundError: return False except HfHubHTTPError as e: if e.response.status_code == 404: if attempt == 0 and not os.path.exists(file_name): try: if file_name == DATA_FILE: with open(file_name, 'w', encoding='utf-8') as f: json.dump({}, f) except Exception: pass success = False break else: pass except requests.exceptions.RequestException: pass except Exception: pass if attempt < retries: time.sleep(delay) if not success: all_successful = False return all_successful def upload_db_to_hf(specific_file=None): if not HF_TOKEN_WRITE: return try: api = HfApi() files_to_upload = [specific_file] if specific_file else SYNC_FILES for file_name in files_to_upload: if os.path.exists(file_name): try: api.upload_file( path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')}" ) except Exception: pass except Exception: pass def periodic_backup(): backup_interval = 1800 while True: time.sleep(backup_interval) upload_db_to_hf() def load_data(): with db_lock: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if not isinstance(data, dict): data = {} except (FileNotFoundError, json.JSONDecodeError): if download_db_from_hf(specific_file=DATA_FILE): try: with open(DATA_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if not isinstance(data, dict): data = {} except (FileNotFoundError, json.JSONDecodeError): data = {} else: data = {} return data def save_data(data): with db_lock: try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) except Exception: pass upload_db_to_hf(specific_file=DATA_FILE) def get_env_data(env_id): with db_lock: all_data = load_data() default_settings = { "vcard_firstname": "Имя", "vcard_lastname": "Фамилия", "vcard_job": "Специалист", "organization_name": "Моя Компания", "currency_code": "KGS", "chat_avatar": None, "color_scheme": "default", "admin_password_enabled": False, "admin_password": "", "categories_as_lines": False, "about_text": "Привет! Это моя онлайн-визитка." } env_data = all_data.get(env_id, {}) if not env_data: env_data = { 'products': [], 'categories':[], 'blocks':[], 'settings': default_settings } if 'products' not in env_data: env_data['products'] = [] if 'categories' not in env_data: env_data['categories'] = [] if 'settings' not in env_data: env_data['settings'] = default_settings if 'blocks' not in env_data: env_data['blocks'] = [] settings_changed = False for key, value in default_settings.items(): if key not in env_data['settings']: env_data['settings'][key] = value settings_changed = True products_changed = False for product in env_data['products']: if 'product_id' not in product: product['product_id'] = uuid4().hex products_changed = True if 'views' not in product: product['views'] = 0 products_changed = True if 'price' not in product: product['price'] = 0.0 products_changed = True if products_changed or settings_changed: save_env_data(env_id, env_data) return env_data def save_env_data(env_id, env_data): with db_lock: all_data = load_data() all_data[env_id] = env_data save_data(all_data) def configure_gemini(): if not GOOGLE_API_KEY: return False try: genai.configure(api_key=GOOGLE_API_KEY) return True except Exception: return False def generate_ai_description_from_image(image_data, language): if not configure_gemini(): raise ValueError("Google AI API не настроен.") try: if not image_data: raise ValueError("Файл изображения не найден.") image_stream = io.BytesIO(image_data) image = Image.open(image_stream).convert('RGB') except Exception: raise ValueError("Не удалось обработать изображение.") base_prompt = "Напиши привлекательное описание для этого товара/услуги. Текст должен быть емким, продающим, с использованием эмодзи. Не пиши цены, адреса и телефоны." lang_suffix = "" if language == "Русский": lang_suffix = " Пиши на русском языке." elif language == "Кыргызский": lang_suffix = " Пиши на кыргызском языке." elif language == "Казахский": lang_suffix = " Пиши на казахском языке." elif language == "Узбекский": lang_suffix = " Пиши на узбекском языке." final_prompt = f"{base_prompt}{lang_suffix}" try: model = genai.GenerativeModel('gemma-4-31b-it') response = model.generate_content([final_prompt, image]) if hasattr(response, 'text'): return response.text else: if response.parts: return "".join(part.text for part in response.parts if hasattr(part, 'text')) else: response.resolve() return response.text except Exception as e: raise ValueError(f"Ошибка при генерации контента: {e}") LANDING_PAGE_TEMPLATE = ''' Платформа Онлайн-Визиток ''' LOGIN_TEMPLATE = ''' Вход в Админ-панель ''' ADMHOSTO_TEMPLATE = ''' Главная Админ-панель

Управление Визитками

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}

Существующие визитки

{% if environments %} {% else %}

Пока не создано ни одной визитки.

{% endif %}
''' CATALOG_TEMPLATE = ''' {{ settings.vcard_firstname }} {{ settings.vcard_lastname }} - Визитка
Avatar
{{ settings.vcard_firstname }} {{ settings.vcard_lastname }}
{% if settings.vcard_job %}
{{ settings.vcard_job }}
{% endif %} {% if settings.organization_name %}
{{ settings.organization_name }}
{% endif %} {% if settings.about_text %}
{{ settings.about_text|replace('\\n', '
')|safe }}
{% endif %}
Сохранить в контакты {% if blocks %}
{% for block in blocks %} {% if block.type == 'link' %} {% if block.icon %} {% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %} {% endif %} {{ block.title }} {% elif block.type == 'text' %}
{% if block.title %}

{{ block.title }}

{% endif %}

{{ block.content|replace('\\n', '
')|safe }}

{% endif %} {% endfor %}
{% endif %} {% if products_json != '[]' %}

Каталог / Услуги

{% endif %}
''' ADMIN_TEMPLATE = ''' Настройки Визитки - {{ settings.organization_name }}

Сохранение данных...

Logo

Настройки Визитки

Открыть визитку {% if settings.admin_password_enabled %} Выход {% endif %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
Профиль и Внешний вид
{% if settings.chat_avatar %}

Текущий аватар:

{% endif %}

Пароль для входа сюда

Кнопки и Ссылки (для Визитки)

Добавить кнопку
{% if blocks %} {% for block in blocks %}
{% if block.icon %} {% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %} {% endif %} {{ block.title }} ({{ 'Ссылка' if block.type == 'link' else 'Текст' }}) {% if block.type == 'link' %}
{{ block.url }}{% endif %}
{% endfor %} {% else %}

Кнопки не добавлены.

{% endif %}

Категории товаров/услуг

Добавить новую категорию
{% if categories %}
{% for category in categories %}
{{ category }}
{% endfor %}
{% else %}

Категорий пока нет.

{% endif %}

Товары и Услуги

Добавить товар/услугу

Список товаров/услуг:

{% if search_q %} Сброс {% endif %}
{% if paginated_products %}
{% for product in paginated_products %}
{% if product.get('photos') %} Фото {% else %} Нет фото {% endif %}

{{ product['name'] }}

Категория: {{ product.get('category', 'Без категории') }}

Цена: {% if product.get('price', 0) > 0 %}{{ "%.2f"|format(product.get('price', 0)) }} {{ currency_code }}{% else %}Не указана{% endif %}

Редактирование

{% endfor %}
{% if total_pages > 1 %} {% endif %} {% else %}

Записей пока нет или по вашему запросу ничего не найдено.

{% endif %}
''' @app.route('/') def index(): return render_template_string(LANDING_PAGE_TEMPLATE) @app.route('/admhosto', methods=['GET']) def admhosto(): data = load_data() environments_data = [] for env_id, env_data in data.items(): settings = env_data.get('settings', {}) org_name = settings.get("organization_name", f"Визитка {env_id}") environments_data.append({ "id": env_id, "org_name": org_name, "pwd_enabled": settings.get("admin_password_enabled", False), "password": settings.get("admin_password", "") }) environments_data.sort(key=lambda x: x['id']) return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data) @app.route('/admhosto/create', methods=['POST']) def create_environment(): all_data = load_data() while True: new_id = ''.join(random.choices(string.digits, k=6)) if new_id not in all_data: break all_data[new_id] = { 'products': [], 'categories':[], 'blocks':[], 'settings': { "vcard_firstname": "Имя", "vcard_lastname": "Фамилия", "vcard_job": "Специалист", "organization_name": "Моя Компания", "currency_code": "KGS", "chat_avatar": None, "color_scheme": "default", "admin_password_enabled": False, "admin_password": "", "categories_as_lines": False, "about_text": "Привет! Это моя онлайн-визитка." } } save_data(all_data) flash(f'Новая визитка с ID {new_id} успешно создана.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/update_pwd/', methods=['POST']) def update_env_pwd(env_id): all_data = load_data() if env_id in all_data: pwd_enabled = 'pwd_enabled' in request.form password = request.form.get('password', '').strip() all_data[env_id]['settings']['admin_password_enabled'] = pwd_enabled all_data[env_id]['settings']['admin_password'] = password save_data(all_data) flash(f'Пароль для визитки {env_id} обновлен.', 'success') else: flash(f'Визитка {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) @app.route('/admhosto/delete/', methods=['POST']) def delete_environment(env_id): all_data = load_data() if env_id in all_data: del all_data[env_id] save_data(all_data) flash(f'Визитка {env_id} была удалена.', 'success') else: flash(f'Визитка {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) @app.route('//login', methods=['GET', 'POST']) def admin_login(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if not settings.get('admin_password_enabled'): return redirect(url_for('admin', env_id=env_id)) if request.method == 'POST': pwd = request.form.get('password', '') if pwd == settings.get('admin_password', ''): session[f'admin_auth_{env_id}'] = True return redirect(url_for('admin', env_id=env_id)) else: flash('Неверный пароль', 'error') return render_template_string(LOGIN_TEMPLATE, env_id=env_id) @app.route('//logout') def admin_logout(env_id): session.pop(f'admin_auth_{env_id}', None) return redirect(url_for('admin_login', env_id=env_id)) @app.route('//vcard') def download_vcard(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) blocks = data.get('blocks', []) first_name = settings.get("vcard_firstname", "") last_name = settings.get("vcard_lastname", "") org = settings.get("organization_name", "") title = settings.get("vcard_job", "") note = settings.get("about_text", "").replace('\n', '\\n') card_url = url_for('catalog', env_id=env_id, _external=True) vcard = [ "BEGIN:VCARD", "VERSION:3.0", f"N:{last_name};{first_name};;;", f"FN:{first_name} {last_name}".strip(), ] if org: vcard.append(f"ORG:{org}") if title: vcard.append(f"TITLE:{title}") if note: vcard.append(f"NOTE:{note}") vcard.append(f"URL;type=pref:{card_url}") for b in blocks: if b.get('type') == 'link' and b.get('url'): url = b.get('url', '').strip() if url.startswith('+') or url.replace('-', '').replace(' ', '').isdigit(): vcard.append(f"TEL;TYPE=CELL:{url}") elif '@' in url and not url.startswith('http'): vcard.append(f"EMAIL;TYPE=WORK:{url.replace('mailto:', '')}") else: vcard.append(f"URL:{url}") vcard.append("END:VCARD") vcard_str = "\r\n".join(vcard) response = make_response(vcard_str) response.headers["Content-Disposition"] = f"attachment; filename=contact_{env_id}.vcf" response.headers["Content-Type"] = "text/vcard; charset=utf-8" return response @app.route('//catalog') def catalog(env_id): data = get_env_data(env_id) all_products = data.get('products',[]) settings = data.get('settings', {}) blocks = data.get('blocks',[]) product_categories = set(p.get('category', 'Без категории') for p in all_products) admin_categories = set(data.get('categories',[])) all_cat_names = sorted(list(product_categories.union(admin_categories))) products_sorted_for_js = sorted(all_products, key=lambda p: p.get('name', '').lower()) products_by_category = {cat:[] for cat in all_cat_names} for product in all_products: products_by_category[product.get('category', 'Без категории')].append(product) ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)] chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" return render_template_string( CATALOG_TEMPLATE, ordered_categories=ordered_categories, products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id, blocks=blocks ) @app.route('//admin', methods=['GET', 'POST']) def admin(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) products = data.get('products',[]) categories = data.get('categories',[]) blocks = data.get('blocks',[]) page = request.args.get('p', 1, type=int) search_q = request.args.get('q', '').strip() if request.method == 'POST': action = request.form.get('action') try: if action == 'add_block': b_type = request.form.get('block_type') b_title = request.form.get('block_title', '').strip() b_icon = request.form.get('block_icon', '') b_url = request.form.get('block_url', '').strip() if b_type == 'link' and b_url: if not b_url.startswith(('http://', 'https://', 'mailto:', 'tel:')) and not b_url.startswith('+'): if '@' in b_url: b_url = 'mailto:' + b_url else: b_url = 'https://' + b_url elif b_url.startswith('+'): b_url = 'tel:' + b_url.replace(' ', '').replace('-', '') b_content = request.form.get('block_content', '').strip() blocks.append({ 'id': uuid4().hex[:8], 'type': b_type, 'title': b_title, 'icon': b_icon if b_type == 'link' else '', 'url': b_url, 'content': b_content }) data['blocks'] = blocks save_env_data(env_id, data) flash("Блок добавлен.", "success") elif action == 'delete_block': b_id = request.form.get('block_id') data['blocks'] = [b for b in blocks if b.get('id') != b_id] save_env_data(env_id, data) flash("Блок удален.", "success") elif action == 'move_block_up': b_id = request.form.get('block_id') idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1) if idx > 0: blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx] data['blocks'] = blocks save_env_data(env_id, data) flash("Блок перемещен выше.", "success") elif action == 'move_block_down': b_id = request.form.get('block_id') idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1) if idx != -1 and idx < len(blocks) - 1: blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx] data['blocks'] = blocks save_env_data(env_id, data) flash("Блок перемещен ниже.", "success") elif action == 'add_category': category_name = request.form.get('category_name', '').strip() if category_name and category_name not in categories: categories.append(category_name) data['categories'] = categories save_env_data(env_id, data) flash(f"Категория '{category_name}' успешно добавлена.", 'success') elif not category_name: flash("Название категории не может быть пустым.", 'error') else: flash(f"Категория '{category_name}' уже существует.", 'error') elif action == 'delete_category': category_to_delete = request.form.get('category_name') if category_to_delete and category_to_delete in categories: categories.remove(category_to_delete) updated_count = 0 for product in products: if product.get('category') == category_to_delete: product['category'] = 'Без категории' updated_count += 1 data['categories'] = categories data['products'] = products save_env_data(env_id, data) flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров/услуг обновлено.", 'success') else: flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error') elif action == 'update_settings': settings['admin_password_enabled'] = 'admin_password_enabled' in request.form settings['admin_password'] = request.form.get('admin_password', '').strip() settings['vcard_firstname'] = request.form.get('vcard_firstname', '').strip() settings['vcard_lastname'] = request.form.get('vcard_lastname', '').strip() settings['vcard_job'] = request.form.get('vcard_job', '').strip() settings['organization_name'] = request.form.get('organization_name', '').strip() settings['about_text'] = request.form.get('about_text', '').strip() settings['currency_code'] = request.form.get('currency_code', 'KGS') settings['color_scheme'] = request.form.get('color_scheme', 'default') avatar_file = request.files.get('chat_avatar') if avatar_file and avatar_file.filename: if HF_TOKEN_WRITE: try: api = HfApi() old_avatar = settings.get('chat_avatar') if old_avatar: try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass ext = os.path.splitext(avatar_file.filename)[1].lower() avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}" uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) temp_path = os.path.join(uploads_dir, avatar_filename) avatar_file.save(temp_path) api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"avatars/{avatar_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) settings['chat_avatar'] = avatar_filename os.remove(temp_path) flash("Аватар успешно обновлен.", 'success') except Exception as e: flash(f"Ошибка при загрузке аватара: {e}", 'error') else: flash("HF_TOKEN (write) не настроен. Аватар не был загружен.", "warning") data['settings'] = settings save_env_data(env_id, data) flash("Настройки успешно обновлены.", 'success') elif action == 'add_product' or action == 'edit_product': product_id = request.form.get('product_id') product_data = {} is_edit = action == 'edit_product' if is_edit: product_data = next((p for p in products if p.get('product_id') == product_id), None) if not product_data: flash(f"Ошибка: запись не найдена.", 'error') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) else: product_data['views'] = 0 product_data['name'] = request.form.get('name', '').strip() product_data['description'] = request.form.get('description', '').strip() try: product_data['price'] = float(request.form.get('price', 0)) except ValueError: product_data['price'] = 0.0 category = request.form.get('category') product_data['category'] = category if category in categories else 'Без категории' if not product_data['name']: flash("Название обязательно.", 'error') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) photos_files = request.files.getlist('photos') if photos_files and any(f.filename for f in photos_files): if HF_TOKEN_WRITE: uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) api = HfApi() new_photos_list = [] photo_limit = 10 uploaded_count = 0 for photo in photos_files: if uploaded_count >= photo_limit: break if photo and photo.filename: try: ext = os.path.splitext(photo.filename)[1].lower() if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50] photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}" temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) new_photos_list.append(photo_filename) os.remove(temp_path) uploaded_count += 1 except Exception as e: flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error') if new_photos_list and is_edit and product_data.get('photos'): try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass if new_photos_list: product_data['photos'] = new_photos_list else: flash("HF_TOKEN не настроен. Фотографии не загружены.", "warning") if is_edit: product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) if product_index != -1: products[product_index] = product_data flash(f"Запись '{product_data['name']}' обновлена.", 'success') else: product_data['product_id'] = uuid4().hex products.append(product_data) flash(f"Запись '{product_data['name']}' добавлена.", 'success') data['products'] = products save_env_data(env_id, data) elif action == 'delete_product': product_id = request.form.get('product_id') product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) if product_index == -1: flash(f"Ошибка удаления: запись не найдена.", 'error') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) deleted_product = products.pop(product_index) product_name = deleted_product.get('name', 'N/A') photos_to_delete = deleted_product.get('photos', []) if photos_to_delete and HF_TOKEN_WRITE: try: api = HfApi() api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass data['products'] = products save_env_data(env_id, data) flash(f"Запись '{product_name}' удалена.", 'success') else: flash(f"Неизвестное действие: {action}", 'warning') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) except Exception as e: flash(f"Ошибка при выполнении действия: {e}", 'error') return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) filtered_products = products if search_q: q_lower = search_q.lower() filtered_products = [p for p in products if q_lower in p.get('name', '').lower() or q_lower in p.get('description', '').lower()] filtered_products = sorted(filtered_products, key=lambda p: p.get('name', '').lower()) PER_PAGE = 20 total_items = len(filtered_products) total_pages = math.ceil(total_items / PER_PAGE) if total_items > 0 else 1 if page < 1: page = 1 if page > total_pages: page = total_pages start_idx = (page - 1) * PER_PAGE end_idx = start_idx + PER_PAGE paginated_products = filtered_products[start_idx:end_idx] display_categories = sorted(categories) display_settings = settings chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{display_settings['chat_avatar']}" if display_settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" return render_template_string( ADMIN_TEMPLATE, paginated_products=paginated_products, total_pages=total_pages, page=page, search_q=search_q, categories=display_categories, settings=display_settings, blocks=blocks, repo_id=REPO_ID, currency_code=display_settings.get('currency_code', 'KGS'), chat_avatar_url=chat_avatar_url, currencies=CURRENCIES, color_schemes=COLOR_SCHEMES, icons=ICONS, env_id=env_id ) @app.route('/generate_description_ai', methods=['POST']) def handle_generate_description_ai(): request_data = request.get_json() base64_image = request_data.get('image') language = request_data.get('language', 'Русский') if not base64_image: return jsonify({"error": "Изображение не найдено в запросе."}), 400 try: image_data = base64.b64decode(base64_image) result_text = generate_ai_description_from_image(image_data, language) return jsonify({"text": result_text}) except ValueError as ve: return jsonify({"error": str(ve)}), 400 except Exception as e: return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500 if __name__ == '__main__': configure_gemini() download_db_from_hf() load_data() if HF_TOKEN_WRITE: backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() port = int(os.environ.get('PORT', 7860)) app.run(debug=False, host='0.0.0.0', port=port)