Spaces:
Sleeping
Sleeping
| import os | |
| import io | |
| import base64 | |
| import json | |
| import logging | |
| import threading | |
| import time | |
| from datetime import datetime, timedelta | |
| import random | |
| import string | |
| from flask import Flask, render_template_string, request, redirect, url_for, flash, make_response, jsonify | |
| from huggingface_hub import HfApi, hf_hub_download | |
| from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError | |
| 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_synkris' | |
| DATA_FILE = 'data.json' | |
| DATA_FILE_TEMP = 'data.json.tmp' | |
| PROMPTS_FILE = 'prompts.json' | |
| SYNC_FILES = [DATA_FILE, PROMPTS_FILE] | |
| REPO_ID = "Kgshop/synkristest" | |
| HF_TOKEN_WRITE = os.getenv("HF_TOKEN") | |
| HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") | |
| DOWNLOAD_RETRIES = 3 | |
| DOWNLOAD_DELAY = 5 | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| def setup_initial_files(): | |
| if not os.path.exists(PROMPTS_FILE): | |
| prompts_data = { | |
| "base_prompts": { | |
| "model_base": """**MANDATORY: IMAGE OUTPUT ONLY. ABSOLUTELY NO TEXT.** | |
| **STRICT DIRECTIVE: YOU ARE AN OPTICAL CLONING AND TEXTURE TRANSFER ENGINE.** | |
| 1. **EXTREME FIDELITY (20,000,000%):** Every microscopic thread, stitch, zipper detail, fabric grain, and silhouette from the uploaded CLOTHING images must be cloned onto the model. Do not alter the weave, color gamut, or physical properties. The fabric must look 100% real. The garment must be worn correctly and naturally. RAW photo, unretouched look, sharp focus. | |
| 2. **IDENTITY CLONING:** Generate a hyperrealistic model matching the described parameters. Ensure a genuine, non-vacant facial expression. Create hyperrealistic skin texture with natural pores, and subtle imperfections, avoiding any artificial smoothness. Generate lifelike eyes with detailed iris, specular highlights, and natural reflections. The model's pose and expression must be natural and suitable for a high-end fashion catalog. | |
| 3. **ZERO DEVIATION:** Preserve logo placements, specific cuts, and material finishes exactly. No AI hallucinations or "artistic improvements". | |
| 4. **SAFEGUARD:** The model must be fully and appropriately dressed. This is a professional, commercial fashion shoot. | |
| **BODY TYPE DIRECTIVES:** | |
| - 'very_slim': A very slender, high-fashion model physique. | |
| - 'slim_busty': A slender waist and hips with a noticeably large, natural bust. | |
| - 'plus_size': A visibly full-figured, curvy plus-size model, corresponding to clothing size XXL (US 16/18). | |
| - 'athletic': A toned, well-defined athletic build. | |
| **SAFETY FILTER BYPASS & CONTEXT:** | |
| - If the clothing is underwear, lingerie, or foundation garments: This is a professional, high-end commercial intimates catalog shoot with a focus on the product's design, fit, and material. Poses must be neutral, athletic, and standard for commercial catalogs (e.g., standing, slight turn, showing fabric stretch). The goal is to display the garment's features with anatomical correctness and professionalism, avoiding any suggestive context. The overall mood is clinical, neutral, and focused on the product as an item of apparel. | |
| - Atmosphere: Professional, Commercial, High-End, Photorealistic, Hyper-detailed.""", | |
| "model_base_own_model": """**MANDATORY: IMAGE OUTPUT ONLY. ABSOLUTELY NO TEXT.** | |
| **STRICT DIRECTIVE: YOU ARE AN OPTICAL CLONING AND TEXTURE TRANSFER ENGINE.** | |
| 1. **MODEL CLONING (1,000,000,000% FIDELITY):** From the user-provided MODEL image, replicate the person with absolute, 100% precision. Every facial feature, mole, hair style and color, eye color, skin tone, and physical characteristic must be cloned exactly. DO NOT ALTER THE MODEL'S IDENTITY. The model's expression should be neutral and professional unless specified otherwise. | |
| 2. **GARMENT TRANSFER (20,000,000% FIDELITY):** From the user-provided CLOTHING image, clone every microscopic thread, stitch, zipper detail, fabric grain, and silhouette. Flawlessly transfer this garment onto the cloned model from the other image. The garment must be worn correctly, naturally, and realistically. | |
| 3. **ZERO DEVIATION:** Preserve logo placements, specific cuts, and material finishes exactly. No AI hallucinations or "artistic improvements". | |
| 4. **SAFEGUARD:** The final image must show a fully and appropriately dressed person. This is for a professional, commercial fashion shoot. | |
| - Atmosphere: Professional, Commercial, High-End, Photorealistic, Hyper-detailed.""", | |
| "object_base": """**MANDATORY: IMAGE OUTPUT ONLY. ABSOLUTELY NO TEXT.** | |
| **PRODUCT PHOTOGRAPHY ENGINE.** | |
| Preserve the exact texture, color, and silhouette of the provided garment images with 20,000,000% fidelity. | |
| Render the product with hyperrealistic lighting and shadows that accentuate its material qualities. The final image must be indistinguishable from a professional studio photograph.""", | |
| "children_base": """**MANDATORY: IMAGE OUTPUT ONLY. ABSOLUTELY NO TEXT.** | |
| **CHILDREN'S FASHION PHOTOGRAPHY ENGINE.** | |
| **CORE DIRECTIVES:** | |
| 1. **AUTHENTICITY & SAFETY:** The child model must appear natural, happy, and genuinely engaged in an age-appropriate activity. The mood is always positive, innocent, and cheerful. Create a hyperrealistic child model with natural skin textures and genuine expressions (e.g., laughter, curiosity, gentle smiles). Avoid artificial, doll-like appearances. | |
| 2. **GARMENT FIDELITY (20,000,000%):** Clone the uploaded garment with extreme precision. Every stitch, fabric texture (e.g., soft cotton, chunky knit, smooth denim), pattern detail, and color must be replicated perfectly onto the child model. The clothing must fit realistically and allow for natural movement. | |
| 3. **ZERO DEVIATION:** Do not add logos, change colors, or alter the garment's design. No AI-generated embellishments. | |
| **AGE GROUP DIRECTIVES (Apply with nuance):** | |
| - 'infant (6-12 months)': Focus on comfort and softness. Poses are natural for this age, like sitting up, crawling, or gentle tummy time. The setting is safe and cozy (e.g., a soft rug, a minimalist nursery). | |
| - 'toddler (2-4 years)': Capture energy and playfulness. Poses are dynamic and action-oriented (e.g., running, playing with simple toys, exploring). The setting is bright, clean, and engaging (e.g., a sunny park, a colorful playroom). | |
| - 'child (5-8 years)': Reflect personality and curiosity. Poses can be more styled but should remain candid and natural (e.g., twirling, examining something with interest, a natural smile towards the camera). The setting can be more varied, from urban streets to natural landscapes. | |
| - 'pre-teen (9-12 years)': Showcase emerging style and confidence. Poses are relaxed and cool, reflecting the transition towards adolescence. The setting is trendy and relevant to their interests (e.g., a skate park, a cool cafe, a modern architectural backdrop). | |
| **CONTEXT:** This is a professional, high-end commercial photoshoot for a children's clothing catalog or brand campaign. The overall atmosphere must be bright, clean, and joyful.""" | |
| }, | |
| "flagship_styles": { | |
| "studio": "Impeccable studio photoshoot. Flawless, even lighting on a neutral cyclorama (light grey, beige). Ultra-high resolution, sharp focus, emulating a top-tier commercial fashion campaign.", | |
| "street": "Dynamic street style shot in a bustling metropolis (e.g., Tokyo, New York). Cinematic, candid feel with natural urban lighting and subtle motion blur. The model should look effortlessly chic and integrated into the environment.", | |
| "lookbook": "Minimalist lookbook aesthetic. Clean, textured background (e.g., concrete, colored paper). Soft, diffused light creating a sophisticated and modern mood. Focus is entirely on the garment's form and drape.", | |
| "minimalism": "Extreme architectural minimalism. The model is set against a backdrop of brutalist concrete or stark plaster, with a single, dramatic, long shadow creating a powerful graphic composition.", | |
| "selfie": "Hyperrealistic 'captured moment' selfie. Shot on a smartphone in a visually interesting location (e.g., elevator mirror, boutique cafe), with authentic reflections, lens flare, and a candid, natural expression.", | |
| "creative": "Avant-garde, conceptual photoshoot. Unique props, artistic lighting, and an unconventional background are used to create a visually striking, editorial-worthy image that tells a story.", | |
| "new_year": "Festive New Year's atmosphere. Soft bokeh from fairy lights, dynamic sparkler trails, set against a beautifully decorated tree or a magical snowy landscape. Evokes warmth and celebration.", | |
| "retro": "Authentic 35mm film photograph emulation. Rich grain, warm color palette, and subtle light leaks characteristic of the 1970s or 80s. Poses and environment reflect the era.", | |
| "boho": "Golden hour boho dreamscape. Shot in a field of wildflowers during sunset. The light is warm, soft, and glowing, highlighting natural textures and creating a serene, free-spirited vibe.", | |
| "gothic": "Moody, gothic romance. Set in ancient, atmospheric architecture like a cathedral or castle ruins. Low-key lighting, deep shadows, and a sense of mystery and drama.", | |
| "editorial": "High-fashion glossy magazine editorial. Bold, saturated colored background. Clever use of mirrors to create compelling reflections and fragmented views of the model and outfit.", | |
| "film_noir": "Cinematic black and white film noir. High contrast, dramatic 'chiaroscuro' lighting, with long shadows, and a sense of suspense. May incorporate atmospheric elements like rain or fog.", | |
| "cottagecore": "Idyllic cottagecore aesthetic. A cozy, rustic setting in a country house or lush garden. Natural light, organic textures, and a feeling of wholesome, romanticized rural life.", | |
| "royalcore": "Opulent royalcore aesthetic. Set in a lavish palace interior with ornate details, velvet curtains, and gilded furniture. The lighting is grand and dramatic, creating an air of aristocracy.", | |
| "solarpunk": "Optimistic solarpunk future. Sleek, futuristic architecture seamlessly integrated with lush greenery. Bright, clean light fills the scene, suggesting a harmonious, tech-advanced society.", | |
| "skater": "Energetic skater aesthetic. Wide-angle, dynamic shot in a skate park or on urban streets. Captures movement and a raw, youthful, counter-culture energy.", | |
| "baroque": "Dramatic Baroque painting style. Ornate, detailed setting with rich fabrics. Lighting is high-contrast and theatrical, reminiscent of a Caravaggio masterpiece, creating deep, intense colors.", | |
| "japandi": "Serene Japandi style. A fusion of Japanese minimalism and Scandinavian functionality. Clean lines, neutral tones, natural wood, and a focus on tranquility and uncluttered space.", | |
| "coastal": "Relaxed coastal grandmother style. Bright, airy setting by the sea. A palette of whites, beiges, and soft blues. Natural materials and a feeling of effortless seaside elegance.", | |
| "cyberpunk": "Gritty, neon-drenched cyberpunk cityscape. High-tech, futuristic elements, with reflections from neon signs on wet streets. A cool color palette and a sense of urban dystopia.", | |
| "fantasy": "Enchanting fantasy world. A magical forest, ancient ruins, or ethereal landscape. The lighting is mystical and otherworldly, creating a dreamlike, narrative-driven image.", | |
| "90s_grunge": "Raw 90s grunge aesthetic. Urban decay, abandoned locations, with a desaturated color palette. A feeling of angst, rebellion, and effortless, non-conformist style.", | |
| "techwear": "Sleek, functional Techwear style. Set against futuristic, urban architecture. The lighting is clean and sharp, highlighting the technical details, fabrics, and functionality of the garments.", | |
| "avant_garde": "Experimental avant-garde fashion. Abstract shapes, bold color clashes, and unconventional compositions. A highly artistic and conceptual approach that challenges traditional aesthetics.", | |
| "home_casual": "Cozy, authentic home setting. Soft, natural light streaming through a window. A relaxed, intimate atmosphere with books, plants, and comfortable furnishings.", | |
| "social_media_candid": "Candid, 'Instagrammable' moment. Shot in a trendy cafe or during a walk. Looks spontaneous and natural, as if capturing a real moment in time.", | |
| "backstage": "Hectic, atmospheric backstage of a fashion show. Racks of clothes, makeup stations, and focused energy. The lighting is functional but chaotic, creating a 'behind-the-scenes' narrative.", | |
| "road_trip": "Cinematic American road trip aesthetic. The model is near a vintage car against a vast, open landscape at sunset. A sense of freedom, adventure, and nostalgia.", | |
| "rainy_day": "Romantic, melancholic rainy day scene. Reflections on wet pavement, droplets on windows, and the soft, diffused light of an overcast sky. A cozy and introspective mood.", | |
| "night_flash": "Edgy, direct-flash night photography. High contrast, saturated colors, and sharp shadows. Creates a raw, spontaneous, 'paparazzi' or party-snapshot feel.", | |
| "golden_hour_picnic": "Idyllic golden hour picnic. Warm, glowing sunset light filters through trees. A beautifully styled picnic scene with a relaxed, romantic, and joyful atmosphere.", | |
| "beach": "Bright, sun-drenched beach photoshoot. The model is on pristine white sand near turquoise water with gentle, rolling waves. The scene is illuminated by the soft, warm glow of a late afternoon sun (golden hour), creating long, soft shadows. The atmosphere is serene, relaxed, and effortlessly chic, suitable for a high-end resort wear or swimwear catalog." | |
| }, | |
| "object_styles": { | |
| "studio": "Professional product photography on a seamless, neutral background. Perfect, multi-point lighting that eliminates harsh shadows and reveals every detail of the product's texture and form.", | |
| "minimalism": "Minimalist composition on a textured surface like concrete, marble, or fine sand. A single, crisp, hard light source creates a graphic, artistic shadow, emphasizing the product's silhouette.", | |
| "nature": "The product is artfully placed in a complementary natural environment. E.g., on mossy rocks in a forest, beside a clear stream, or nestled among flowers. The lighting is natural and enhances the organic feel.", | |
| "luxe": "Luxury still life. The product is arranged on a rich, tactile surface like silk, velvet, or dark marble. The lighting is low-key and sophisticated, with soft highlights that suggest opulence and exclusivity.", | |
| "dark": "Moody and dramatic 'dark academia' style. The product is set against a dark, textured background. A single, directional light source carves the product out of the shadows, creating a mysterious and intense atmosphere." | |
| } | |
| } | |
| with open(PROMPTS_FILE, 'w', encoding='utf-8') as f: | |
| json.dump(prompts_data, f, ensure_ascii=False, indent=4) | |
| def load_prompts(): | |
| if not os.path.exists(PROMPTS_FILE): | |
| setup_initial_files() | |
| try: | |
| with open(PROMPTS_FILE, 'r', encoding='utf-8') as f: | |
| return json.load(f) | |
| except (FileNotFoundError, json.JSONDecodeError): | |
| return {} | |
| def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): | |
| if not HF_TOKEN_READ and not HF_TOKEN_WRITE: | |
| return False | |
| 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: | |
| 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: | |
| all_successful = False | |
| break | |
| 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) | |
| elif file_name == PROMPTS_FILE: | |
| setup_initial_files() | |
| except Exception: | |
| pass | |
| success = True | |
| break | |
| else: | |
| 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().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(): | |
| data = {} | |
| if os.path.exists(DATA_FILE): | |
| try: | |
| with open(DATA_FILE, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| except 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) | |
| except (FileNotFoundError, json.JSONDecodeError): | |
| data = {} | |
| elif download_db_from_hf(specific_file=DATA_FILE): | |
| try: | |
| with open(DATA_FILE, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| except (FileNotFoundError, json.JSONDecodeError): | |
| data = {} | |
| if not isinstance(data, dict): | |
| data = {} | |
| return data | |
| def save_data(data): | |
| try: | |
| with open(DATA_FILE_TEMP, 'w', encoding='utf-8') as file: | |
| json.dump(data, file, ensure_ascii=False, indent=4) | |
| os.replace(DATA_FILE_TEMP, DATA_FILE) | |
| upload_db_to_hf(specific_file=DATA_FILE) | |
| except Exception: | |
| if os.path.exists(DATA_FILE_TEMP): | |
| os.remove(DATA_FILE_TEMP) | |
| LANDING_PAGE_TEMPLATE = ''' | |
| <!DOCTYPE html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title> MetaStore - AI система для Вашего Бизнеса</title> | |
| <style> | |
| body, html { | |
| margin: 0; | |
| padding: 0; | |
| height: 100%; | |
| overflow: hidden; | |
| } | |
| iframe { | |
| border: none; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <iframe src="https://v0-ai-agent-landing-page-smoky-six.vercel.app/"></iframe> | |
| </body> | |
| </html> | |
| ''' | |
| ADMHOSTO_TEMPLATE = ''' | |
| <!DOCTYPE html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Админ-панель</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-light: #f4f6f9; | |
| --bg-medium: #135D66; | |
| --accent: #48D1CC; | |
| --accent-hover: #77E4D8; | |
| --text-dark: #333; | |
| --text-on-accent: #003C43; | |
| --danger: #E57373; | |
| --warning: #ffb74d; | |
| --info: #4fc3f7; | |
| --success: #81c784; | |
| --archive: #90a4ae; | |
| } | |
| * { box-sizing: border-box; } | |
| body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); margin: 0; padding: 15px; } | |
| .container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 3px 15px rgba(0,0,0,0.08); } | |
| h1, h2 { font-weight: 600; color: var(--bg-medium); text-align: center; } | |
| h1 { margin-bottom: 25px; font-size: 1.5rem; } | |
| h2 { font-size: 1.3rem; margin-top: 40px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; margin-bottom: 20px; } | |
| .section { margin-bottom: 25px; } | |
| .add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; } | |
| input[type="text"] { | |
| width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; | |
| font-family: inherit; background: #fff; -webkit-appearance: none; | |
| } | |
| .controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; } | |
| .radio-group { display: flex; gap: 15px; } | |
| .radio-group label { cursor: pointer; display: flex; align-items: center; gap: 6px; font-weight: 500; font-size: 0.95rem; } | |
| .button { | |
| padding: 10px 15px; border: none; border-radius: 8px; color: white; font-weight: 600; cursor: pointer; text-decoration: none; | |
| display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-size: 0.9rem; transition: opacity 0.2s; | |
| } | |
| .button:hover { opacity: 0.85; } | |
| .button:active { transform: scale(0.98); } | |
| .button.primary { background-color: var(--accent); color: var(--text-on-accent); } | |
| .button.danger { background-color: var(--danger); } | |
| .button.warning { background-color: var(--warning); color: #333; } | |
| .button.info { background-color: var(--info); } | |
| .button.success { background-color: var(--success); } | |
| .env-list { list-style: none; padding: 0; margin: 0; } | |
| .env-item { | |
| background: #fff; border: 1px solid #e0e0e0; border-radius: 10px; padding: 15px; margin-bottom: 12px; | |
| display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.02); | |
| } | |
| .env-item-archived { border-left: 4px solid var(--archive); } | |
| .env-details { display: flex; flex-direction: column; gap: 4px; overflow: hidden; } | |
| .env-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } | |
| .env-id { font-weight: 700; color: var(--bg-medium); font-size: 1.1rem; } | |
| .env-keyword { font-style: italic; color: #666; font-size: 0.9rem;} | |
| .env-link { font-size: 0.9rem; color: #007bff; word-break: break-all; text-decoration: none; padding: 5px 0; display: block; } | |
| .env-type-badge { font-size: 0.75rem; padding: 3px 8px; border-radius: 20px; font-weight: bold; text-transform: uppercase; white-space: nowrap; } | |
| .type-open { background-color: #d4edda; color: #155724; } | |
| .type-closed { background-color: #f8d7da; color: #721c24; } | |
| .env-actions { display: flex; flex-wrap: wrap; gap: 8px; } | |
| .message { padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center; font-size: 0.95rem; } | |
| .message.success { background-color: #d4edda; color: #155724; } | |
| .message.error { background-color: #f8d7da; color: #721c24; } | |
| .modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px); } | |
| .modal-content { background-color: #fff; margin: 15% auto; padding: 25px; width: 90%; max-width: 600px; border-radius: 12px; position: relative; box-shadow: 0 5px 20px rgba(0,0,0,0.2); } | |
| .close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; } | |
| .stats-table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.85rem; } | |
| .stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; } | |
| .stats-table th { background-color: var(--bg-medium); color: white; } | |
| .stats-table tr:nth-child(even) { background-color: #f9f9f9; } | |
| .empty-list-placeholder { text-align:center; padding: 20px; color: #888; } | |
| .no-margin { margin-bottom: 0; } | |
| @media (max-width: 768px) { | |
| .env-item { grid-template-columns: 1fr; gap: 12px; } | |
| .env-actions { justify-content: flex-start; } | |
| .modal-content { margin: 10% auto; width: 95%; padding: 20px 15px; } | |
| } | |
| @media (max-width: 600px) { | |
| body { padding: 10px; } | |
| .container { padding: 15px; } | |
| h1 { font-size: 1.3rem; margin-bottom: 20px; } | |
| .controls-row { flex-direction: column; align-items: stretch; } | |
| .radio-group { justify-content: space-between; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid #ddd; } | |
| .add-env-form .button { width: 100%; padding: 14px; } | |
| .stats-table th, .stats-table td { font-size: 0.75rem; padding: 6px 4px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1><i class="fas fa-server"></i> Управление Средами</h1> | |
| {% with messages = get_flashed_messages(with_categories=true) %} | |
| {% if messages %} | |
| {% for category, message in messages %} | |
| <div class="message {{ category }}">{{ message }}</div> | |
| {% endfor %} | |
| {% endif %} | |
| {% endwith %} | |
| <div class="section"> | |
| <form method="POST" action="{{ url_for('create_environment') }}" class="add-env-form"> | |
| <input type="text" id="keyword" name="keyword" placeholder="Ключевое слово (например, 'магазин')" required> | |
| <div class="controls-row"> | |
| <div class="radio-group"> | |
| <label><input type="radio" name="env_type" value="closed" checked> <i class="fas fa-lock"></i> Закрытая</label> | |
| <label><input type="radio" name="env_type" value="open"> <i class="fas fa-globe"></i> Открытая</label> | |
| </div> | |
| <button type="submit" class="button primary"><i class="fas fa-plus-circle"></i> Создать</button> | |
| </div> | |
| </form> | |
| </div> | |
| <div class="section"> | |
| <input type="text" id="search-env" placeholder="🔍 Поиск..."> | |
| </div> | |
| <div class="section"> | |
| {% if active_environments %} | |
| <ul class="env-list"> | |
| {% for env in active_environments %} | |
| <li class="env-item"> | |
| <div class="env-details"> | |
| <div class="env-header"> | |
| <span class="env-id">{{ env.id }}</span> | |
| <span class="env-type-badge type-{{ env.type }}"> | |
| {{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }} | |
| </span> | |
| <small style="color:#888">{{ env.hits }} <i class="fas fa-eye"></i></small> | |
| </div> | |
| <span class="env-keyword">{{ env.keyword }}</span> | |
| <a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a> | |
| </div> | |
| <div class="env-actions"> | |
| <button class="button info" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button> | |
| <form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;"> | |
| <button type="submit" class="button warning"> | |
| <i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыть' if env.type == 'closed' else 'Закрыть' }} | |
| </button> | |
| </form> | |
| {% if env.type == 'closed' %} | |
| <form method="POST" action="{{ url_for('clear_user', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Отвязать пользователя от среды {{ env.id }}? Первый, кто зайдет по ссылке, станет владельцем.');"> | |
| <button type="submit" class="button success"><i class="fas fa-user-slash"></i> Сброс</button> | |
| </form> | |
| {% endif %} | |
| <form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Переместить среду {{ env.id }} в архив?');"> | |
| <button type="submit" class="button danger"><i class="fas fa-archive"></i></button> | |
| </form> | |
| </div> | |
| </li> | |
| {% endfor %} | |
| </ul> | |
| {% else %} | |
| <div class="empty-list-placeholder">Список активных сред пуст</div> | |
| {% endif %} | |
| </div> | |
| <div class="section no-margin"> | |
| <h2><i class="fas fa-archive"></i> Архив</h2> | |
| {% if archived_environments %} | |
| <ul class="env-list"> | |
| {% for env in archived_environments %} | |
| <li class="env-item env-item-archived"> | |
| <div class="env-details"> | |
| <div class="env-header"> | |
| <span class="env-id">{{ env.id }}</span> | |
| <span class="env-type-badge type-{{ env.type }}"> | |
| {{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }} | |
| </span> | |
| </div> | |
| <span class="env-keyword">{{ env.keyword }}</span> | |
| </div> | |
| <div class="env-actions"> | |
| <form method="POST" action="{{ url_for('restore_environment', env_id=env.id) }}" style="display:contents;"> | |
| <button type="submit" class="button success"><i class="fas fa-undo"></i> Восстановить</button> | |
| </form> | |
| </div> | |
| </li> | |
| {% endfor %} | |
| </ul> | |
| {% else %} | |
| <div class="empty-list-placeholder">Архив пуст</div> | |
| {% endif %} | |
| </div> | |
| </div> | |
| <div id="statsModal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-modal" onclick="closeStats()">×</span> | |
| <h3 id="modalTitle" style="margin-top:0; color: var(--bg-medium)">Статистика</h3> | |
| <p style="font-size: 0.8rem; color: #666;">Время: Алматы (UTC+5)</p> | |
| <div id="statsContent" style="overflow-x: auto;">Загрузка...</div> | |
| </div> | |
| </div> | |
| <script> | |
| document.getElementById('search-env').addEventListener('input', function() { | |
| const searchTerm = this.value.toLowerCase().trim(); | |
| document.querySelectorAll('.env-item').forEach(item => { | |
| const text = item.innerText.toLowerCase(); | |
| item.style.display = text.includes(searchTerm) ? 'grid' : 'none'; | |
| }); | |
| }); | |
| function openStats(envId) { | |
| const modal = document.getElementById('statsModal'); | |
| const content = document.getElementById('statsContent'); | |
| const title = document.getElementById('modalTitle'); | |
| title.innerText = `Среда: ${envId}`; | |
| content.innerHTML = '<div style="text-align:center; padding: 20px;"><i class="fas fa-spinner fa-spin fa-2x"></i></div>'; | |
| modal.style.display = 'block'; | |
| fetch(`/admhosto/stats/${envId}`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.error) { | |
| content.innerHTML = `<p style="color:red">${data.error}</p>`; | |
| return; | |
| } | |
| let html = `<div style="display:flex; justify-content:space-between; margin-bottom:10px;"> | |
| <span><strong>Всего входов:</strong> ${data.hits}</span> | |
| <span><strong>Тип:</strong> ${data.type === 'closed' ? 'Закрытая' : 'Открытая'}</span> | |
| </div>`; | |
| if (data.logs && data.logs.length > 0) { | |
| html += `<table class="stats-table"> | |
| <thead><tr><th>Время</th><th>IP</th><th>Browser</th></tr></thead> | |
| <tbody>`; | |
| data.logs.forEach(log => { | |
| html += `<tr> | |
| <td>${log.time.split(' ')[1]}<br><small style="color:#999">${log.time.split(' ')[0]}</small></td> | |
| <td>${log.ip}</td> | |
| <td style="max-width: 100px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${log.ua}"> | |
| ${log.ua.includes('iPhone') ? '<i class="fab fa-apple"></i>' : (log.ua.includes('Android') ? '<i class="fab fa-android"></i>' : '<i class="fas fa-desktop"></i>')} | |
| </td> | |
| </tr>`; | |
| }); | |
| html += `</tbody></table>`; | |
| } else { | |
| html += `<p>Журнал пуст.</p>`; | |
| } | |
| content.innerHTML = html; | |
| }) | |
| .catch(err => { | |
| content.innerHTML = '<p style="color:red">Ошибка сети.</p>'; | |
| }); | |
| } | |
| function closeStats() { | |
| document.getElementById('statsModal').style.display = 'none'; | |
| } | |
| window.onclick = function(event) { | |
| const modal = document.getElementById('statsModal'); | |
| if (event.target == modal) { | |
| modal.style.display = 'none'; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| SYNKRIS_LOOK_TEMPLATE = ''' | |
| <!DOCTYPE html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Synkris Look</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0d0d0d; | |
| --card-bg: #121212; | |
| --primary: #c8ff00; | |
| --primary-hover: #b8e600; | |
| --primary-gradient: linear-gradient(45deg, #d4ff33, #b8e600); | |
| --text: #f0f0f0; | |
| --text-secondary: #a0a0a0; | |
| --border: #2a2a2a; | |
| --input-bg: #1a1a1a; | |
| --danger: #ff4d4d; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| background-color: var(--bg); | |
| color: var(--text); | |
| margin: 0; | |
| padding: 20px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| min-height: 100vh; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| .container { | |
| background-color: var(--card-bg); | |
| width: 100%; | |
| max-width: 800px; | |
| padding: 30px 35px; | |
| border-radius: 24px; | |
| border: 1px solid var(--border); | |
| box-shadow: 0 10px 50px -20px rgba(200, 255, 0, 0.1); | |
| animation: fadeIn 0.5s ease-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(-10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| h1 { | |
| text-align: center; | |
| color: var(--primary); | |
| margin-top: 0; | |
| margin-bottom: 8px; | |
| font-size: 2rem; | |
| font-weight: 800; | |
| letter-spacing: 1px; | |
| text-shadow: 0 0 15px rgba(200, 255, 0, 0.3); | |
| } | |
| p.subtitle { | |
| text-align: center; | |
| color: var(--text-secondary); | |
| margin-bottom: 35px; | |
| font-size: 0.9rem; | |
| letter-spacing: 0.3px; | |
| font-weight: 500; | |
| } | |
| .mode-selector { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| margin-bottom: 30px; | |
| background-color: var(--input-bg); | |
| border-radius: 12px; | |
| padding: 6px; | |
| border: 1px solid var(--border); | |
| } | |
| .mode-btn { | |
| padding: 12px 10px; | |
| background-color: transparent; | |
| border: none; | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| border-radius: 8px; | |
| transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .mode-btn.active { | |
| background-color: var(--primary); | |
| color: #000; | |
| box-shadow: 0 4px 15px -5px rgba(200, 255, 0, 0.4); | |
| transform: translateY(-1px); | |
| } | |
| .form-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 25px; | |
| } | |
| .full-width { | |
| grid-column: span 2; | |
| } | |
| .form-group { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| label { | |
| font-weight: 600; | |
| margin-bottom: 10px; | |
| font-size: 0.75rem; | |
| color: var(--primary); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| select, textarea { | |
| padding: 14px; | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| background-color: var(--input-bg); | |
| color: var(--text); | |
| transition: all 0.2s ease-in-out; | |
| outline: none; | |
| width: 100%; | |
| box-sizing: border-box; | |
| -webkit-appearance: none; | |
| -moz-appearance: none; | |
| appearance: none; | |
| background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23a0a0a0%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22/%3E%3C/svg%3E'); | |
| background-repeat: no-repeat; | |
| background-position: right 14px top 50%; | |
| background-size: .65em auto; | |
| padding-right: 40px; | |
| } | |
| select:disabled { | |
| background-color: #1f1f1f; | |
| color: #555; | |
| cursor: not-allowed; | |
| opacity: 0.7; | |
| } | |
| select:focus, textarea:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 10px rgba(200, 255, 0, 0.2); | |
| } | |
| textarea { | |
| resize: vertical; | |
| min-height: 90px; | |
| font-family: inherit; | |
| background-image: none; | |
| padding-right: 14px; | |
| } | |
| .btn-container { | |
| margin-top: 40px; | |
| text-align: center; | |
| } | |
| .action-btn { | |
| background-image: var(--primary-gradient); | |
| color: #000; | |
| border: none; | |
| padding: 16px 30px; | |
| font-size: 1rem; | |
| font-weight: 700; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| width: 100%; | |
| transition: all 0.2s ease; | |
| box-shadow: 0 5px 25px -8px rgba(200, 255, 0, 0.5); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 12px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .action-btn:hover { transform: scale(1.02) translateY(-2px); box-shadow: 0 8px 30px -10px rgba(200, 255, 0, 0.7); } | |
| .action-btn:active { transform: scale(0.99) translateY(0); } | |
| .form-mode { display: none; } | |
| .form-mode.active { display: contents; } | |
| .style-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); | |
| gap: 12px; | |
| } | |
| .style-btn { | |
| padding: 12px 10px; | |
| background-color: var(--input-bg); | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| border-radius: 10px; | |
| transition: all 0.2s ease-in-out; | |
| text-align: center; | |
| width: 100%; | |
| } | |
| .style-btn:hover { border-color: var(--primary); color: var(--text); } | |
| .style-btn.active { background-color: var(--primary); color: #000; border-color: var(--primary); font-weight: 700; } | |
| .aspect-ratio-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 12px; | |
| } | |
| .aspect-ratio-btn { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 10px; | |
| background-color: var(--input-bg); | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| border-radius: 10px; | |
| transition: all 0.2s ease-in-out; | |
| text-align: center; | |
| height: 90px; | |
| } | |
| .aspect-ratio-btn .preview { background: #333; border-radius: 4px; transition: background-color 0.3s ease; } | |
| .aspect-ratio-btn:hover { border-color: var(--primary); color: var(--text); } | |
| .aspect-ratio-btn.active { background-color: var(--primary); color: #000; border-color: var(--primary); } | |
| .aspect-ratio-btn.active .preview { background: #000; } | |
| .checkbox-group { | |
| background-color: transparent; | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .checkbox-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| cursor: pointer; | |
| color: var(--text); | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| } | |
| .checkbox-item input[type="checkbox"] { | |
| width: 1.3em; | |
| height: 1.3em; | |
| accent-color: var(--primary); | |
| cursor: pointer; | |
| background-color: var(--input-bg); | |
| border-radius: 4px; | |
| border: 1px solid var(--border); | |
| } | |
| .checkbox-item label { | |
| margin: 0; | |
| text-transform: none; | |
| letter-spacing: normal; | |
| color: inherit; | |
| font-size: inherit; | |
| cursor: pointer; | |
| } | |
| @media (max-width: 700px) { | |
| body { padding: 10px; } | |
| .container { padding: 20px; } | |
| h1 { font-size: 1.8rem; } | |
| .form-grid { grid-template-columns: 1fr; gap: 20px; } | |
| .full-width { grid-column: span 1; } | |
| .mode-selector { grid-template-columns: 1fr; gap: 5px; } | |
| .style-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Synkris Look</h1> | |
| <p class="subtitle">PROMPT GENERATOR & LAUNCHER</p> | |
| <div class="mode-selector"> | |
| <button id="modeModelBtn" class="mode-btn" onclick="switchMode('model')">Фото на модели</button> | |
| <button id="modeChildrenBtn" class="mode-btn" onclick="switchMode('children')">Модели (дети)</button> | |
| <button id="modeObjectBtn" class="mode-btn" onclick="switchMode('object')">Предметное фото</button> | |
| </div> | |
| <form id="promptForm"> | |
| <div class="form-grid"> | |
| <div id="modelMode" class="form-mode"> | |
| <div class="form-group full-width"> | |
| <div class="checkbox-group"> | |
| <div class="checkbox-item"> | |
| <input type="checkbox" id="ownModelCheck"> | |
| <label for="ownModelCheck">Своя модель (использует 2 фото: одежду + модель)</label> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="modelParamsContainer"> | |
| <div class="form-group"> | |
| <label for="gender">Пол</label> | |
| <select id="gender" onchange="updateModelOptions()"> | |
| <option value="female">Женщина</option> | |
| <option value="male">Мужчина</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="age">Возраст</label> | |
| <select id="age"> | |
| <option value="teenager">14-18 лет</option> | |
| <option value="20-25 years old" selected>20-25 лет</option> | |
| <option value="25-30 years old">25-30 лет</option> | |
| <option value="30-40 years old">30-40 лет</option> | |
| <option value="40-50 years old">40-50 лет</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="nationality">Внешность/Этнос</label> | |
| <select id="nationality"> | |
| <option value="Eastern European">Восточная Европа</option> | |
| <option value="Northern European">Скандинавская</option> | |
| <option value="Asian">Азиатская</option> | |
| <option value="Latin American">Латиноамериканская</option> | |
| <option value="Mixed Race">Смешанная</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="bodyType">Телосложение</label> | |
| <select id="bodyType"></select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="hairColor">Цвет волос</label> | |
| <select id="hairColor"> | |
| <option value="black hair">Черные</option> | |
| <option value="brown hair">Каштановые</option> | |
| <option value="blonde hair">Блонд</option> | |
| <option value="red hair">Рыжие</option> | |
| <option value="light brown hair">Русые</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="hairstyle">Прическа</label> | |
| <select id="hairstyle"></select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="eyeColor">Цвет глаз</label> | |
| <select id="eyeColor"> | |
| <option value="brown eyes">Карие</option> | |
| <option value="blue eyes">Голубые</option> | |
| <option value="green eyes">Зеленые</option> | |
| <option value="gray eyes">Серые</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="shotType">Ракурс/План</label> | |
| <select id="shotType"> | |
| <option value="Full body shot, dynamic angle">В полный рост, динамичный ракурс</option> | |
| <option value="Medium shot, waist up, candid">По пояс, естественный</option> | |
| <option value="Cowboy shot, mid-thigh up, fashion editorial style">"Ковбойский" план, журнальный</option> | |
| <option value="Expressive portrait shot, detailed">Портрет, выразительный</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="viewAngle">Вид</label> | |
| <select id="viewAngle"> | |
| <option value="Front view">Спереди</option> | |
| <option value="Back view">Сзади</option> | |
| <option value="Side view">Сбоку</option> | |
| <option value="Three-quarter view">В три четверти</option> | |
| </select> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label for="pose">Поза</label> | |
| <select id="pose"> | |
| <option value="standing confidently, looking at camera">Стоит уверенно, взгляд в камеру</option> | |
| <option value="dynamic walking pose, slight motion blur">Динамичная походка, легкое размытие</option> | |
| <option value="sitting relaxed on a modern chair">Сидит расслабленно на стуле</option> | |
| <option value="leaning casually against a textured wall">Небрежно оперевшись о стену</option> | |
| <option value="dynamic high fashion editorial pose, unconventional" selected>Динамичная, нестандартная фэшн-поза</option> | |
| </select> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label>Стиль / Локация</label> | |
| <div id="styleSelector" class="style-grid"></div> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label>Соотношение сторон</label> | |
| <div id="aspectRatioSelectorModel" class="aspect-ratio-grid"> | |
| <button type="button" class="aspect-ratio-btn active" data-value="--ar 9:16"><div class="preview" style="width: 27px; height: 48px;"></div><span>9:16</span></button> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 3:4"><div class="preview" style="width: 36px; height: 48px;"></div><span>3:4</span></button> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 1:1"><div class="preview" style="width: 40px; height: 40px;"></div><span>1:1</span></button> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 16:9"><div class="preview" style="width: 64px; height: 36px;"></div><span>16:9</span></button> | |
| </div> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label for="model_details">Одежда и Детали (Опишите ткань и фасон!)</label> | |
| <textarea id="model_details" placeholder="Укажите ткань и детали. Пример: в черном кожаном плаще с грубой текстурой, заметные швы, массивная металлическая фурнитура, шелковый шарф"></textarea> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label for="additional_prompt">Дополнительные директивы</label> | |
| <textarea id="additional_prompt" placeholder="Например: в кадре виден телефон последней модели, эффект мокрых волос..."></textarea> | |
| </div> | |
| </div> | |
| <div id="childrenMode" class="form-mode"> | |
| <div class="form-group"> | |
| <label for="child_gender">Пол</label> | |
| <select id="child_gender"> | |
| <option value="girl">Девочка</option> | |
| <option value="boy">Мальчик</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="child_age">Возраст</label> | |
| <select id="child_age"> | |
| <option value="infant (6-12 months old)">6-12 месяцев</option> | |
| <option value="toddler (2-4 years old)">2-4 года</option> | |
| <option value="child (5-8 years old)">5-8 лет</option> | |
| <option value="pre-teen (9-12 years old)">9-12 лет</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="child_nationality">Внешность/Этнос</label> | |
| <select id="child_nationality"> | |
| <option value="Eastern European">Восточная Европа</option> | |
| <option value="Northern European">Скандинавская</option> | |
| <option value="Asian">Азиатская</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="child_shotType">Ракурс/План</label> | |
| <select id="child_shotType"> | |
| <option value="Full body shot, playful angle">В полный рост</option> | |
| <option value="Medium shot, capturing emotion">По пояс</option> | |
| <option value="Close-up portrait, happy expression">Портрет</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="child_viewAngle">Вид</label> | |
| <select id="child_viewAngle"> | |
| <option value="Front view">Спереди</option> | |
| <option value="Back view">Сзади</option> | |
| <option value="Side view">Сбоку</option> | |
| </select> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label for="child_pose">Поза/Действие</label> | |
| <select id="child_pose"> | |
| <option value="running joyfully in a field">Бежит по полю</option> | |
| <option value="playing enthusiastically with wooden toys on the floor">Играет с игрушками</option> | |
| <option value="sitting and curiously looking at a picture book">Сидит с книгой</option> | |
| <option value="posing for a candid school photo, smiling naturally">Позирует для фото</option> | |
| <option value="laughing and jumping on a bed">Прыгает на кровати</option> | |
| </select> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label>Стиль / Локация</label> | |
| <div id="childStyleSelector" class="style-grid"></div> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label>Соотношение сторон</label> | |
| <div id="aspectRatioSelectorChildren" class="aspect-ratio-grid"> | |
| <button type="button" class="aspect-ratio-btn active" data-value="--ar 9:16"><div class="preview" style="width: 27px; height: 48px;"></div><span>9:16</span></button> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 3:4"><div class="preview" style="width: 36px; height: 48px;"></div><span>3:4</span></button> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 1:1"><div class="preview" style="width: 40px; height: 40px;"></div><span>1:1</span></button> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 16:9"><div class="preview" style="width: 64px; height: 36px;"></div><span>16:9</span></button> | |
| </div> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label for="child_details">Одежда и Детали</label> | |
| <textarea id="child_details" placeholder="Пример: джинсовый комбинезон с потертостями и металлическими пуговицами, вельветовая рубашка в рубчик"></textarea> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label for="child_additional_prompt">Дополнительные директивы</label> | |
| <textarea id="child_additional_prompt" placeholder="Например: добавь инфографику с текстом 'organic cotton'"></textarea> | |
| </div> | |
| </div> | |
| <div id="objectMode" class="form-mode"> | |
| <div class="form-group full-width"> | |
| <label for="object_name">Название/Описание предмета</label> | |
| <textarea id="object_name" placeholder="Например: флакон духов 'Noir', кроссовки 'CyberRun', часы 'Classic Timepiece'"></textarea> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label>Стиль / Фон</label> | |
| <div id="objectStyleSelector" class="style-grid"></div> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label>Соотношение сторон</label> | |
| <div id="aspectRatioSelectorObject" class="aspect-ratio-grid"> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 9:16"><div class="preview" style="width: 27px; height: 48px;"></div><span>9:16</span></button> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 3:4"><div class="preview" style="width: 36px; height: 48px;"></div><span>3:4</span></button> | |
| <button type="button" class="aspect-ratio-btn active" data-value="--ar 1:1"><div class="preview" style="width: 40px; height: 40px;"></div><span>1:1</span></button> | |
| <button type="button" class="aspect-ratio-btn" data-value="--ar 16:9"><div class="preview" style="width: 64px; height: 36px;"></div><span>16:9</span></button> | |
| </div> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label for="object_additional_prompt">Дополнительные директивы</label> | |
| <textarea id="object_additional_prompt" placeholder="Например: добавить инфографику 'new collection', левитация предмета"></textarea> | |
| </div> | |
| </div> | |
| <div class="form-group full-width"> | |
| <label>Детали и Вариации</label> | |
| <div class="checkbox-group"> | |
| <div class="checkbox-item"> | |
| <input type="checkbox" id="detailsCollage"> | |
| <label for="detailsCollage">Коллаж с увеличенными деталями (ткань, фурнитура)</label> | |
| </div> | |
| <div class="checkbox-item"> | |
| <input type="checkbox" id="variantsCollage"> | |
| <label for="variantsCollage">Разные варианты/цвета (несколько моделей в кадре)</label> | |
| </div> | |
| <div> | |
| <div class="checkbox-item"> | |
| <input type="checkbox" id="textOverlayCheck"> | |
| <label for="textOverlayCheck">Наложение текста</label> | |
| </div> | |
| <textarea id="textOverlayInput" style="display:none; margin-top: 15px;" placeholder="Ключевые слова через запятую, например: New Collection, 100% Cotton"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="btn-container"> | |
| <button type="button" class="action-btn" onclick="processAndOpen()"> | |
| <span>Launch Synkris AI</span> | |
| <span style="font-size: 1.2em">⚡</span> | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| <script> | |
| let currentMode = 'model'; | |
| const envKeyword = {{ keyword|tojson|safe }}; | |
| const promptsData = {{ prompts_data|tojson|safe }}; | |
| const flagshipStyles = { | |
| 'studio': 'Студия (профи)', 'street': 'Стрит-стайл', 'lookbook': 'Лукбук (минимализм)', | |
| 'minimalism': 'Экстрим минимализм', 'selfie': 'Селфи (гиперреализм)', 'creative': 'Креативная съемка', | |
| 'new_year': 'Новый Год', 'retro': 'Ретро (35мм пленка)', 'boho': 'Бохо (золотой час)', | |
| 'gothic': 'Готика', 'editorial': 'Эдиториал (глянец)', 'film_noir': 'Фильм-нуар (Ч/Б)', | |
| 'cottagecore': 'Коттеджкор', 'royalcore': 'Роскошь (дворец)', 'solarpunk': 'Соларпанк', | |
| 'skater': 'Скейтер', 'baroque': 'Барокко', 'japandi': 'Джапанди', 'coastal': 'Прибрежный стиль', | |
| 'cyberpunk': 'Киберпанк', 'fantasy': 'Фэнтези', '90s_grunge': 'Гранж 90-х', | |
| 'techwear': 'Techwear', 'avant_garde': 'Авангард', 'home_casual': 'Домашний уют', | |
| 'social_media_candid': 'Инстаграм-фото', 'backstage': 'Бэкстейдж', 'road_trip': 'Роуд-трип', | |
| 'rainy_day': 'Дождливый день', 'night_flash': 'Ночь (вспышка)', 'golden_hour_picnic': 'Пикник (золотой час)', | |
| 'beach': 'Пляж' | |
| }; | |
| const objectStyles = { | |
| 'studio': 'Студия (профи)', 'minimalism': 'Минимализм', 'nature': 'На природе', | |
| 'luxe': 'Лакшери', 'dark': 'Мрачный стиль' | |
| }; | |
| const femaleBodyTypes = { | |
| 'standard': 'Стандартное', 'very_slim': 'Очень стройное (модель)', 'slim': 'Стройное (натуральное)', | |
| 'slim_busty': 'Стройное с пышной грудью', 'athletic': 'Атлетичное', 'petite': 'Миниатюрное', | |
| 'hourglass': 'Песочные часы', 'fit_curvy': 'Спортивное (curvy)', 'plus_size': 'Пышные', | |
| 'curvy': 'Мягкое (curvy)', 'full_figured': 'Плюс-сайз' | |
| }; | |
| const maleBodyTypes = { | |
| 'athletic': 'Атлетичное', 'lean and toned': 'Поджарое', 'muscular build': 'Мускулистое', | |
| 'broad build': 'Крупное', 'slim build': 'Худощавое' | |
| }; | |
| const femaleHairstyles = { | |
| 'long wavy hair': 'Длинные волнистые', 'short bob cut': 'Короткий боб', 'elegant updo': 'Элегантный пучок', 'straight shoulder-length hair': 'Прямые до плеч', 'pixie cut': 'Пикси', 'messy bun': 'Небрежный пучок', 'high ponytail': 'Высокий хвост', 'braids': 'Косы', 'curly afro': 'Афро кудри', 'bangs': 'С челкой', 'layered haircut': 'Каскад' | |
| }; | |
| const maleHairstyles = { | |
| 'short classic cut': 'Короткая классическая', 'fade haircut': 'Фейд', 'slicked back hair': 'Зачесанные назад', 'textured crop': 'Текстурированный кроп', 'quiff': 'Квифф', 'man bun': 'Мужской пучок', 'buzz cut': 'Под ноль', 'medium-length wavy hair': 'Волнистые средней длины', 'side part': 'С боковым пробором', 'undercut': 'Андеркат' | |
| }; | |
| function switchMode(mode) { | |
| currentMode = mode; | |
| document.getElementById('modelMode').classList.toggle('active', mode === 'model'); | |
| document.getElementById('childrenMode').classList.toggle('active', mode === 'children'); | |
| document.getElementById('objectMode').classList.toggle('active', mode === 'object'); | |
| document.getElementById('modeModelBtn').classList.toggle('active', mode === 'model'); | |
| document.getElementById('modeChildrenBtn').classList.toggle('active', mode === 'children'); | |
| document.getElementById('modeObjectBtn').classList.toggle('active', mode === 'object'); | |
| } | |
| function populateSelect(selectElement, options) { | |
| selectElement.innerHTML = ''; | |
| for (const value in options) { | |
| const option = document.createElement('option'); | |
| option.value = value; | |
| option.textContent = options[value]; | |
| selectElement.appendChild(option); | |
| } | |
| } | |
| function updateModelOptions() { | |
| const gender = document.getElementById('gender').value; | |
| const bodyTypeSelect = document.getElementById('bodyType'); | |
| const hairstyleSelect = document.getElementById('hairstyle'); | |
| populateSelect(bodyTypeSelect, gender === 'female' ? femaleBodyTypes : maleBodyTypes); | |
| populateSelect(hairstyleSelect, gender === 'female' ? femaleHairstyles : maleHairstyles); | |
| } | |
| function toggleOwnModel(isOwnModel) { | |
| const modelParamsContainer = document.getElementById('modelParamsContainer'); | |
| const selectsToToggle = modelParamsContainer.querySelectorAll('select'); | |
| selectsToToggle.forEach(select => { | |
| select.disabled = isOwnModel; | |
| }); | |
| } | |
| function populateStyles(containerId, styles) { | |
| const container = document.getElementById(containerId); | |
| container.innerHTML = ''; | |
| let isFirst = true; | |
| for (const key in styles) { | |
| const btn = document.createElement('button'); | |
| btn.type = 'button'; | |
| btn.className = 'style-btn'; | |
| if (isFirst) { | |
| btn.classList.add('active'); | |
| isFirst = false; | |
| } | |
| btn.dataset.value = key; | |
| btn.textContent = styles[key]; | |
| container.appendChild(btn); | |
| } | |
| } | |
| function setupClickableSelectors() { | |
| document.querySelectorAll('.style-grid, .aspect-ratio-grid').forEach(container => { | |
| container.addEventListener('click', (e) => { | |
| const button = e.target.closest('.style-btn, .aspect-ratio-btn'); | |
| if (button) { | |
| container.querySelectorAll('.style-btn, .aspect-ratio-btn').forEach(innerBtn => innerBtn.classList.remove('active')); | |
| button.classList.add('active'); | |
| } | |
| }); | |
| }); | |
| } | |
| function getPrompt() { | |
| let prompt = ""; | |
| let aspectRatio = ''; | |
| let additionalPrompt = ''; | |
| const wantsDetailsCollage = document.getElementById('detailsCollage').checked; | |
| const wantsVariantsCollage = document.getElementById('variantsCollage').checked; | |
| const wantsTextOverlay = document.getElementById('textOverlayCheck').checked; | |
| const textToOverlay = document.getElementById('textOverlayInput').value.trim(); | |
| if (currentMode === 'model') { | |
| const isOwnModel = document.getElementById('ownModelCheck').checked; | |
| prompt = isOwnModel ? promptsData.base_prompts.model_base_own_model : promptsData.base_prompts.model_base; | |
| const styleKey = document.querySelector('#styleSelector .style-btn.active').dataset.value; | |
| const stylePrompt = promptsData.flagship_styles[styleKey]; | |
| const shotType = document.getElementById('shotType').value; | |
| const viewAngle = document.getElementById('viewAngle').value; | |
| const pose = document.getElementById('pose').value; | |
| const clothingDetails = document.getElementById('model_details').value || "the provided clothing"; | |
| additionalPrompt = document.getElementById('additional_prompt').value; | |
| aspectRatio = document.querySelector('#aspectRatioSelectorModel .aspect-ratio-btn.active').dataset.value; | |
| prompt += `\\n\\n**SCENE & STYLE:** The scene is a ${styleKey} setting. ${stylePrompt}`; | |
| if (!isOwnModel) { | |
| const gender = document.getElementById('gender').value; | |
| const age = document.getElementById('age').value; | |
| const nationality = document.getElementById('nationality').value; | |
| const bodyType = document.getElementById('bodyType').value; | |
| const hairColor = document.getElementById('hairColor').value; | |
| const hairstyle = document.getElementById('hairstyle').value; | |
| const eyeColor = document.getElementById('eyeColor').value; | |
| prompt += `\\n\\n**MODEL(S) SPECIFICATIONS:**\\n- model: ${age} ${gender}, ${nationality} appearance, with ${hairColor}, ${hairstyle}, and ${eyeColor}, and a realistic, ${bodyType} body type.`; | |
| } | |
| prompt += `\\n\\n**CLOTHING:** The model is wearing: ${clothingDetails}.`; | |
| prompt += `\\n\\n**POSE & COMPOSITION:**\\n- Perspective: ${shotType}\\n- View: ${viewAngle}\\n- Pose: ${pose}`; | |
| } else if (currentMode === 'children') { | |
| prompt = promptsData.base_prompts.children_base; | |
| const styleKey = document.querySelector('#childStyleSelector .style-btn.active').dataset.value; | |
| const stylePrompt = promptsData.flagship_styles[styleKey]; | |
| const gender = document.getElementById('child_gender').value; | |
| const age = document.getElementById('child_age').value; | |
| const nationality = document.getElementById('child_nationality').value; | |
| const shotType = document.getElementById('child_shotType').value; | |
| const viewAngle = document.getElementById('child_viewAngle').value; | |
| const pose = document.getElementById('child_pose').value; | |
| const clothingDetails = document.getElementById('child_details').value || "the provided clothing"; | |
| additionalPrompt = document.getElementById('child_additional_prompt').value; | |
| aspectRatio = document.querySelector('#aspectRatioSelectorChildren .aspect-ratio-btn.active').dataset.value; | |
| prompt += `\\n\\n**SCENE & STYLE:** The scene is a ${styleKey} setting. ${stylePrompt} The scene should be cheerful, safe, and age-appropriate.`; | |
| prompt += `\\n\\n**MODEL SPECIFICATIONS:**\\n- model: A happy and natural-looking ${age} ${gender}, ${nationality} appearance.`; | |
| prompt += `\\n\\n**CLOTHING:** The child is wearing: ${clothingDetails}.`; | |
| prompt += `\\n\\n**POSE & COMPOSITION:**\\n- Perspective: ${shotType}\\n- View: ${viewAngle}\\n- Action: ${pose}`; | |
| } else { | |
| prompt = promptsData.base_prompts.object_base; | |
| const styleKey = document.querySelector('#objectStyleSelector .style-btn.active').dataset.value; | |
| const stylePrompt = promptsData.object_styles[styleKey]; | |
| const objectName = document.getElementById('object_name').value || "the product"; | |
| additionalPrompt = document.getElementById('object_additional_prompt').value; | |
| aspectRatio = document.querySelector('#aspectRatioSelectorObject .aspect-ratio-btn.active').dataset.value; | |
| prompt += `\\n\\n**SCENE & STYLE:** The scene is a ${styleKey} setting. ${stylePrompt}`; | |
| prompt += `\\n- Product: ${objectName}`; | |
| } | |
| if (additionalPrompt) { | |
| prompt += `\\n\\n**ADDITIONAL ARTISTIC DIRECTIVES:** ${additionalPrompt}`; | |
| } | |
| if (wantsDetailsCollage) { | |
| prompt += `\\n\\n**COMPOSITION DIRECTIVE (DETAILS COLLAGE):** Create a marketplace-ready collage. The main image features the full look. Add 2-3 smaller inset images showcasing ultra-close-up shots of fabric texture, seams, and hardware (buttons, zippers).`; | |
| } | |
| if (wantsVariantsCollage) { | |
| prompt += `\\n\\n**COMPOSITION DIRECTIVE (VARIANTS COLLAGE):** In a single, cohesive frame, display multiple models (or one model in different poses) showcasing the garment in various colors or styles. The result must be a harmonious and balanced collage.`; | |
| } | |
| if (wantsTextOverlay && textToOverlay) { | |
| prompt += `\\n\\n**GRAPHIC OVERLAY:** Add the following text: "${textToOverlay}". Integrate it stylishly using modern, clean typography. The text should be legible but artistically placed to complement the image, not dominate it. Minimalist icons that enhance the text are permissible.`; | |
| } | |
| return `${envKeyword}, ${prompt} ${aspectRatio}`; | |
| } | |
| async function processAndOpen() { | |
| const btn = document.querySelector('.action-btn'); | |
| const originalText = btn.innerHTML; | |
| const fullPrompt = getPrompt(); | |
| const cleanPrompt = fullPrompt.replace(/\\s+/g, ' ').replace(/\\n/g, ' ').trim(); | |
| try { | |
| await navigator.clipboard.writeText(cleanPrompt); | |
| btn.style.backgroundImage = "linear-gradient(45deg, #ffffff, #e0e0e0)"; | |
| btn.style.color = "#000"; | |
| btn.innerHTML = "ПРОМПТ СКОПИРОВАН. ЗАПУСК... 🚀"; | |
| setTimeout(() => { | |
| window.open('https://arena.ai/ru/c/new?mode=direct&chat-modality=image', '_blank'); | |
| setTimeout(() => { | |
| btn.style.backgroundImage = ""; | |
| btn.innerHTML = originalText; | |
| }, 1000); | |
| }, 800); | |
| } catch (err) { | |
| console.error('Failed to copy: ', err); | |
| alert("Не удалось скопировать. Промпт в консоли разработчика."); | |
| console.log("Ваш промпт:\\n", cleanPrompt); | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| populateStyles('styleSelector', flagshipStyles); | |
| populateStyles('childStyleSelector', flagshipStyles); | |
| populateStyles('objectStyleSelector', objectStyles); | |
| updateModelOptions(); | |
| setupClickableSelectors(); | |
| switchMode('model'); | |
| document.getElementById('textOverlayCheck').addEventListener('change', function() { | |
| document.getElementById('textOverlayInput').style.display = this.checked ? 'block' : 'none'; | |
| }); | |
| document.getElementById('ownModelCheck').addEventListener('change', function() { | |
| toggleOwnModel(this.checked); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| def index(): | |
| return render_template_string(LANDING_PAGE_TEMPLATE) | |
| def admhosto(): | |
| data = load_data() | |
| active_environments = [] | |
| archived_environments = [] | |
| for env_id, env_data in data.items(): | |
| if not isinstance(env_data, dict): continue | |
| env_item = { | |
| "id": env_id, | |
| "keyword": env_data.get("keyword", "N/A"), | |
| "type": env_data.get("type", "closed"), | |
| "hits": env_data.get("hits", 0), | |
| "created_at": env_data.get("created_at", ""), | |
| "link": url_for('serve_env', env_id=env_id, _external=True) | |
| } | |
| if env_data.get("archived"): | |
| archived_environments.append(env_item) | |
| else: | |
| active_environments.append(env_item) | |
| active_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True) | |
| archived_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True) | |
| return render_template_string(ADMHOSTO_TEMPLATE, active_environments=active_environments, archived_environments=archived_environments) | |
| def create_environment(): | |
| all_data = load_data() | |
| keyword = request.form.get('keyword', '').strip() | |
| env_type = request.form.get('env_type', 'closed') | |
| if not keyword: | |
| flash('Ключевое слово не может быть пустым.', 'error') | |
| return redirect(url_for('admhosto')) | |
| while True: | |
| new_id = ''.join(random.choices(string.digits, k=6)) | |
| if new_id not in all_data: | |
| break | |
| all_data[new_id] = { | |
| "keyword": keyword, | |
| "type": env_type, | |
| "device_token": None, | |
| "hits": 0, | |
| "logs": [], | |
| "created_at": datetime.utcnow().isoformat(), | |
| "archived": False | |
| } | |
| save_data(all_data) | |
| flash(f'Новая {env_type} среда с ID {new_id} создана.', 'success') | |
| return redirect(url_for('admhosto')) | |
| def delete_environment(env_id): | |
| all_data = load_data() | |
| if env_id in all_data: | |
| all_data[env_id]['archived'] = True | |
| save_data(all_data) | |
| flash(f'Среда {env_id} перемещена в архив.', 'success') | |
| else: | |
| flash(f'Среда {env_id} не найдена.', 'error') | |
| return redirect(url_for('admhosto')) | |
| def restore_environment(env_id): | |
| all_data = load_data() | |
| if env_id in all_data: | |
| all_data[env_id]['archived'] = False | |
| save_data(all_data) | |
| flash(f'Среда {env_id} восстановлена из архива.', 'success') | |
| else: | |
| flash(f'Среда {env_id} не найдена.', 'error') | |
| return redirect(url_for('admhosto')) | |
| def clear_user(env_id): | |
| all_data = load_data() | |
| if env_id in all_data and all_data[env_id].get('type') == 'closed': | |
| all_data[env_id]['device_token'] = None | |
| save_data(all_data) | |
| flash(f'Пользователь отвязан от среды {env_id}.', 'success') | |
| else: | |
| flash(f'Ошибка: Среда не найдена или не является закрытой.', 'error') | |
| return redirect(url_for('admhosto')) | |
| def toggle_type(env_id): | |
| all_data = load_data() | |
| if env_id in all_data: | |
| current_type = all_data[env_id].get('type', 'closed') | |
| if current_type == 'closed': | |
| all_data[env_id]['type'] = 'open' | |
| flash(f'Среда {env_id} теперь открыта.', 'success') | |
| else: | |
| all_data[env_id]['type'] = 'closed' | |
| all_data[env_id]['device_token'] = None | |
| flash(f'Среда {env_id} теперь закрыта. Пользователь сброшен.', 'success') | |
| save_data(all_data) | |
| else: | |
| flash(f'Среда {env_id} не найдена.', 'error') | |
| return redirect(url_for('admhosto')) | |
| def get_env_stats(env_id): | |
| data = load_data() | |
| env_data = data.get(env_id) | |
| if not env_data: | |
| return jsonify({"error": "Среда не найдена"}), 404 | |
| raw_logs = env_data.get("logs", []) | |
| formatted_logs = [] | |
| for log in reversed(raw_logs): | |
| try: | |
| utc_dt = datetime.fromisoformat(log['time']) | |
| almaty_dt = utc_dt + timedelta(hours=5) | |
| time_str = almaty_dt.strftime('%Y-%m-%d %H:%M:%S') | |
| formatted_logs.append({ | |
| "time": time_str, | |
| "ip": log.get('ip', 'unknown'), | |
| "ua": log.get('ua', 'unknown') | |
| }) | |
| except: | |
| continue | |
| response_data = { | |
| "id": env_id, | |
| "keyword": env_data.get("keyword"), | |
| "type": env_data.get("type", "closed"), | |
| "hits": env_data.get("hits", 0), | |
| "logs": formatted_logs | |
| } | |
| return jsonify(response_data) | |
| def serve_env(env_id): | |
| data = load_data() | |
| env_data = data.get(env_id) | |
| if not env_data or not isinstance(env_data, dict) or env_data.get("archived"): | |
| return "Среда не найдена или заархивирована.", 404 | |
| keyword = env_data.get("keyword", "") | |
| env_type = env_data.get("type", "closed") | |
| prompts_data = load_prompts() | |
| current_log = { | |
| "time": datetime.utcnow().isoformat(), | |
| "ip": request.headers.get('X-Forwarded-For', request.remote_addr), | |
| "ua": request.headers.get('User-Agent', '')[:150] | |
| } | |
| env_data['hits'] = env_data.get('hits', 0) + 1 | |
| if 'logs' not in env_data or not isinstance(env_data.get('logs'), list): | |
| env_data['logs'] = [] | |
| env_data['logs'].append(current_log) | |
| if len(env_data['logs']) > 30: | |
| env_data['logs'] = env_data['logs'][-30:] | |
| data[env_id] = env_data | |
| save_data(data) | |
| if env_type == 'open': | |
| return render_template_string(SYNKRIS_LOOK_TEMPLATE, keyword=keyword, prompts_data=prompts_data) | |
| stored_token = env_data.get("device_token") | |
| user_token = request.cookies.get(f'access_token_{env_id}') | |
| if stored_token: | |
| if user_token != stored_token: | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Доступ запрещен</title> | |
| <style> | |
| body { font-family: 'Segoe UI', sans-serif; background: #000; color: #fff; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; text-align: center; } | |
| .container { padding: 20px; } | |
| h1 { color: #E57373; margin-bottom: 10px; } | |
| p { color: #aaa; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>⛔ Доступ запрещен</h1> | |
| <p>Эта ссылка уже привязана к другому устройству или браузеру.</p> | |
| </div> | |
| </body> | |
| </html> | |
| """, 403 | |
| return render_template_string(SYNKRIS_LOOK_TEMPLATE, keyword=keyword, prompts_data=prompts_data) | |
| else: | |
| new_token = ''.join(random.choices(string.ascii_letters + string.digits, k=40)) | |
| env_data['device_token'] = new_token | |
| data[env_id] = env_data | |
| save_data(data) | |
| resp = make_response(render_template_string(SYNKRIS_LOOK_TEMPLATE, keyword=keyword, prompts_data=prompts_data)) | |
| resp.set_cookie(f'access_token_{env_id}', new_token, max_age=31536000, httponly=True, samesite='Lax') | |
| return resp | |
| if __name__ == '__main__': | |
| setup_initial_files() | |
| download_db_from_hf() | |
| if HF_TOKEN_WRITE: | |
| backup_thread = threading.Thread(target=periodic_backup, daemon=True) | |
| backup_thread.start() | |
| else: | |
| logging.info("HF_TOKEN_WRITE is not set. Periodic backup is disabled.") | |
| port = int(os.environ.get('PORT', 7860)) | |
| app.run(debug=False, host='0.0.0.0', port=port) |