First
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# API Configuration
|
||||
PIAPI_API_KEY=80d3e864cb1c0a74a728d85f627d8aacf4fc334a43c489392bbd4e43f378e583
|
||||
PIAPI_API_URL=https://api.piapi.ai/api/v1/task
|
||||
|
||||
# IMGBB API Key (бесплатно на https://imgbb.com/)
|
||||
IMGBB_API_KEY=e03fae8208e94b5fca324fca257ff86f
|
||||
|
||||
# Proxy Configuration
|
||||
PROXY_URL=socks5://FYqj9n:n4br4L@185.168.251.163:8000
|
||||
PROXY_TYPE=socks5
|
||||
USE_PROXY=true
|
||||
|
||||
# Server Configuration
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
|
||||
# Model Configuration for Nano Banana Pro
|
||||
MODEL_NAME=gemini
|
||||
TASK_TYPE=nano-banana-pro
|
||||
ASPECT_RATIO=9:16
|
||||
STRENGTH=0.65
|
||||
SAFETY_LEVEL=medium
|
||||
OUTPUT_FORMAT=png
|
||||
|
||||
# Default Prompt
|
||||
DEFAULT_PROMPT=Сделай фото профессиональным, улучши качество, сделай вертикальный портрет 9:16, улучши освещение
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Project specific
|
||||
photos/
|
||||
static/
|
||||
*.db
|
||||
*.sqlite
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
Generated
+3
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (ai_photosession)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (ai_photosession)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ai_photosession.iml" filepath="$PROJECT_DIR$/.idea/ai_photosession.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
# This is a sample Python script.
|
||||
|
||||
# Press Shift+F10 to execute it or replace it with your code.
|
||||
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
|
||||
|
||||
|
||||
def print_hi(name):
|
||||
# Use a breakpoint in the code line below to debug your script.
|
||||
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
|
||||
|
||||
|
||||
# Press the green button in the gutter to run the script.
|
||||
if __name__ == '__main__':
|
||||
print_hi('PyCharm')
|
||||
|
||||
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
|
||||
@@ -0,0 +1,9 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
python-multipart==0.0.6
|
||||
Pillow==10.1.0
|
||||
qrcode==7.4.2
|
||||
python-dotenv==1.0.0
|
||||
aiofiles==23.2.1
|
||||
aiohttp==3.9.0
|
||||
aiohttp-socks==0.8.0
|
||||
@@ -0,0 +1,606 @@
|
||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
|
||||
from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import aiohttp
|
||||
from aiohttp_socks import ProxyConnector
|
||||
import asyncio
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
import base64
|
||||
from typing import Optional
|
||||
import json
|
||||
import logging
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Настройка CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Создание папок
|
||||
os.makedirs("photos", exist_ok=True)
|
||||
os.makedirs("static", exist_ok=True)
|
||||
|
||||
# Монтируем статику
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
app.mount("/photos", StaticFiles(directory="photos"), name="photos")
|
||||
|
||||
# Конфигурация из .env
|
||||
PIAPI_API_KEY = os.getenv("PIAPI_API_KEY")
|
||||
PIAPI_API_URL = os.getenv("PIAPI_API_URL", "https://api.piapi.ai/api/v1/task")
|
||||
PROXY_URL = os.getenv("PROXY_URL")
|
||||
USE_PROXY = os.getenv("USE_PROXY", "true").lower() == "true"
|
||||
IMGBB_API_KEY = os.getenv("IMGBB_API_KEY", "")
|
||||
|
||||
MODEL_NAME = os.getenv("MODEL_NAME", "gemini")
|
||||
TASK_TYPE = os.getenv("TASK_TYPE", "nano-banana-pro")
|
||||
ASPECT_RATIO = os.getenv("ASPECT_RATIO", "9:16")
|
||||
STRENGTH = float(os.getenv("STRENGTH", "0.65"))
|
||||
SAFETY_LEVEL = os.getenv("SAFETY_LEVEL", "medium")
|
||||
OUTPUT_FORMAT = os.getenv("OUTPUT_FORMAT", "png")
|
||||
|
||||
HOST = os.getenv("HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("PORT", 8000))
|
||||
|
||||
# Хранилище опубликованных фото
|
||||
published_photos = []
|
||||
# Временное хранилище для фото перед стилизацией
|
||||
temp_photos = {}
|
||||
|
||||
# Список фотосессий
|
||||
PHOTO_SESSIONS = {
|
||||
"vintage": {
|
||||
"name": "📷 Винтажная фотосессия",
|
||||
"description": "Эффект старой фотографии 1950-х годов",
|
||||
"icon": "📷",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into a vintage 1950s style photograph. Add sepia tone, film grain, soft vignette, and aged paper texture. The photo should look like it was taken with an old film camera. Preserve all facial details and natural expressions."""
|
||||
},
|
||||
"cyberpunk": {
|
||||
"name": "🤖 Cyberpunk 2077",
|
||||
"description": "Неоновый киберпанк стиль",
|
||||
"icon": "🤖",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into a cyberpunk style with neon lights (pink, cyan, purple), dark futuristic city background, holographic elements, and sci-fi atmosphere. Add neon rim lighting on the person's face and body. Style inspired by Cyberpunk 2077."""
|
||||
},
|
||||
"fantasy": {
|
||||
"name": "🧝 Эльфийская фэнтези",
|
||||
"description": "Волшебный фэнтези мир",
|
||||
"icon": "🧝",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into a fantasy/elf style. Add magical glowing forest background with fireflies, mystical atmosphere, subtle elf ears if appropriate, flowing elegant clothes, and soft magical lighting. Create an enchanted forest vibe."""
|
||||
},
|
||||
"royal": {
|
||||
"name": "👑 Королевский портрет",
|
||||
"description": "Портрет в стиле европейской аристократии",
|
||||
"icon": "👑",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into a royal/aristocratic portrait. Add elegant palace background with golden details, luxurious velvet curtains, ornate frames, and dramatic Rembrandt lighting. Style reminiscent of 18th century European nobility portraits."""
|
||||
},
|
||||
"anime": {
|
||||
"name": "🎨 Аниме-арт",
|
||||
"description": "Превращение в аниме персонажа",
|
||||
"icon": "🎨",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into an anime/manga art style. Use cel-shading technique, vibrant colors, anime-style large expressive eyes (while preserving original face shape), and detailed anime background. Style inspired by modern Japanese animation."""
|
||||
},
|
||||
"painting": {
|
||||
"name": "🎭 Художественная картина",
|
||||
"description": "Стиль масляной живописи",
|
||||
"icon": "🎭",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into an oil painting masterpiece. Use visible brushstrokes, rich textures, artistic color palette, and gallery lighting. Style inspired by classical portrait paintings like Rembrandt or Vermeer. Create a timeless artistic quality."""
|
||||
},
|
||||
"beach": {
|
||||
"name": "🏖️ Пляжная фотосессия",
|
||||
"description": "Летнее настроение на пляже",
|
||||
"icon": "🏖️",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into a beautiful beach photoshoot. Add tropical beach background with golden sand, turquoise ocean, palm trees, and golden hour sunset lighting. Create a relaxed summer vacation atmosphere with warm colors."""
|
||||
},
|
||||
"professional": {
|
||||
"name": "💼 Деловой портрет",
|
||||
"description": "Профессиональный корпоративный стиль",
|
||||
"icon": "💼",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into a professional corporate portrait. Add modern office background with city view, professional lighting, business attire enhancement, and confident atmosphere. Create a polished headshot suitable for LinkedIn or company website."""
|
||||
},
|
||||
"space": {
|
||||
"name": "🚀 Космическое путешествие",
|
||||
"description": "Астронавт в космосе",
|
||||
"icon": "🚀",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into a space/astronaut theme. Add spacesuit elements, cosmic starry background with nebulas, Earth or Moon in the distance, sci-fi helmet reflection, and futuristic space station interior. Create an epic space exploration feel."""
|
||||
},
|
||||
"wedding": {
|
||||
"name": "💒 Свадебная фотосессия",
|
||||
"description": "Романтический свадебный стиль",
|
||||
"icon": "💒",
|
||||
"prompt": """CRITICAL: Keep the person's face, facial features, and identity EXACTLY as in the original image. Do not change or replace the face.
|
||||
|
||||
Transform this photo into a romantic wedding/engagement style. Add elegant wedding attire, beautiful floral arch background with roses and peonies, soft golden hour lighting, and romantic dreamy atmosphere. Create timeless love story vibes."""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_proxy_connector():
|
||||
"""Создает новый ProxyConnector для SOCKS5 при каждом вызове"""
|
||||
if not USE_PROXY or not PROXY_URL:
|
||||
return None
|
||||
|
||||
try:
|
||||
match = re.search(r'socks5://([^:]+):([^@]+)@([^:]+):(\d+)', PROXY_URL)
|
||||
if match:
|
||||
username = match.group(1)
|
||||
password = match.group(2)
|
||||
host = match.group(3)
|
||||
port = int(match.group(4))
|
||||
connector = ProxyConnector.from_url(f"socks5://{username}:{password}@{host}:{port}")
|
||||
return connector
|
||||
else:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(PROXY_URL)
|
||||
connector = ProxyConnector.from_url(f"socks5://{parsed.hostname}:{parsed.port}")
|
||||
return connector
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка создания прокси коннектора: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def upload_image_to_imgbb(image_bytes: bytes) -> str:
|
||||
"""Загружает изображение на imgbb и возвращает URL"""
|
||||
if not IMGBB_API_KEY:
|
||||
return await upload_image_to_telegraph(image_bytes)
|
||||
|
||||
logger.info("📤 Загрузка изображения на imgbb...")
|
||||
|
||||
image_base64 = base64.b64encode(image_bytes).decode('utf-8')
|
||||
|
||||
data = {
|
||||
'key': IMGBB_API_KEY,
|
||||
'image': image_base64,
|
||||
'expiration': 3600
|
||||
}
|
||||
|
||||
try:
|
||||
connector = get_proxy_connector()
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.post('https://api.imgbb.com/1/upload', data=data, timeout=30) as response:
|
||||
if response.status == 200:
|
||||
result = await response.json()
|
||||
if result.get('success'):
|
||||
url = result['data']['url']
|
||||
logger.info(f"✅ Изображение загружено на imgbb")
|
||||
return url
|
||||
else:
|
||||
raise Exception(f"Ошибка imgbb: {result}")
|
||||
else:
|
||||
raise Exception(f"HTTP {response.status}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка загрузки на imgbb: {e}")
|
||||
return await upload_image_to_telegraph(image_bytes)
|
||||
|
||||
|
||||
async def upload_image_to_telegraph(image_bytes: bytes) -> str:
|
||||
"""Загружает изображение на Telegraph"""
|
||||
logger.info("📤 Загрузка изображения на Telegraph...")
|
||||
|
||||
form_data = aiohttp.FormData()
|
||||
form_data.add_field('file', image_bytes, filename='image.jpg', content_type='image/jpeg')
|
||||
|
||||
try:
|
||||
connector = get_proxy_connector()
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.post('https://telegra.ph/upload', data=form_data, timeout=30) as response:
|
||||
if response.status == 200:
|
||||
result = await response.json()
|
||||
if result and len(result) > 0:
|
||||
url = f"https://telegra.ph{result[0]['src']}"
|
||||
logger.info(f"✅ Изображение загружено на Telegraph")
|
||||
return url
|
||||
else:
|
||||
raise Exception(f"Ошибка Telegraph: {result}")
|
||||
else:
|
||||
raise Exception(f"HTTP {response.status}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка загрузки на Telegraph: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def poll_task_result(task_id: str, max_attempts: int = 60) -> str:
|
||||
"""Ожидание завершения задачи"""
|
||||
get_url = f"https://api.piapi.ai/api/v1/task/{task_id}"
|
||||
headers = {"x-api-key": PIAPI_API_KEY}
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
connector = get_proxy_connector()
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.get(get_url, headers=headers, timeout=30) as response:
|
||||
if response.status != 200:
|
||||
logger.warning(f"HTTP {response.status}, повтор...")
|
||||
await asyncio.sleep(3)
|
||||
continue
|
||||
|
||||
data = await response.json()
|
||||
data_info = data.get('data', {})
|
||||
status = data_info.get('status', 'unknown')
|
||||
output = data_info.get('output')
|
||||
|
||||
logger.info(f"Задача {task_id}: статус {status} (попытка {attempt + 1})")
|
||||
|
||||
if status == 'completed' and output:
|
||||
result_url = None
|
||||
if isinstance(output, dict):
|
||||
if output.get('image_urls') and len(output['image_urls']) > 0:
|
||||
result_url = output['image_urls'][0]
|
||||
elif output.get('url'):
|
||||
result_url = output['url']
|
||||
elif output.get('image'):
|
||||
result_url = output['image']
|
||||
|
||||
if result_url:
|
||||
logger.info(f"✅ Задача {task_id} выполнена!")
|
||||
return result_url
|
||||
|
||||
elif status == 'failed':
|
||||
error = data_info.get('error', {})
|
||||
error_msg = error.get('message', 'Unknown error')
|
||||
raise Exception(f"Задача не выполнена: {error_msg}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка опроса (попытка {attempt + 1}): {e}")
|
||||
|
||||
await asyncio.sleep(3)
|
||||
|
||||
raise Exception("Превышено время ожидания")
|
||||
|
||||
|
||||
async def process_with_nano_banana(image_bytes: bytes, prompt: str, session_id: str = None) -> str:
|
||||
"""Обработка изображения через Nano Banana Pro"""
|
||||
|
||||
if not PIAPI_API_KEY or PIAPI_API_KEY == "your_api_key_here":
|
||||
raise Exception("API ключ не настроен")
|
||||
|
||||
image_url = await upload_image_to_imgbb(image_bytes)
|
||||
if not image_url:
|
||||
raise Exception("Не удалось загрузить изображение на хостинг")
|
||||
|
||||
logger.info(f"🔄 Отправка запроса в Nano Banana Pro...")
|
||||
|
||||
payload = {
|
||||
"model": MODEL_NAME,
|
||||
"task_type": TASK_TYPE,
|
||||
"input": {
|
||||
"image_urls": [image_url],
|
||||
"prompt": prompt,
|
||||
"output_format": OUTPUT_FORMAT,
|
||||
"aspect_ratio": ASPECT_RATIO,
|
||||
"strength": STRENGTH,
|
||||
"safety_level": SAFETY_LEVEL
|
||||
}
|
||||
}
|
||||
|
||||
headers = {
|
||||
"x-api-key": PIAPI_API_KEY,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
connector = get_proxy_connector()
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.post(PIAPI_API_URL, headers=headers, json=payload, timeout=60) as response:
|
||||
if response.status != 200:
|
||||
response_text = await response.text()
|
||||
raise Exception(f"HTTP {response.status}: {response_text[:200]}")
|
||||
|
||||
data = await response.json()
|
||||
|
||||
if data.get('code') != 200:
|
||||
raise Exception(f"API Error: {data.get('message')}")
|
||||
|
||||
task_id = data.get('data', {}).get('task_id')
|
||||
if not task_id:
|
||||
raise Exception(f"Не получен task_id: {data}")
|
||||
|
||||
logger.info(f"✅ Создана задача: {task_id}")
|
||||
|
||||
result_url = await poll_task_result(task_id)
|
||||
return result_url
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
with open("static/index.html", "r", encoding="utf-8") as f:
|
||||
return HTMLResponse(content=f.read())
|
||||
|
||||
|
||||
@app.get("/display")
|
||||
async def display():
|
||||
with open("static/display.html", "r", encoding="utf-8") as f:
|
||||
return HTMLResponse(content=f.read())
|
||||
|
||||
|
||||
@app.get("/sessions")
|
||||
async def get_sessions():
|
||||
"""Возвращает список доступных фотосессий"""
|
||||
return JSONResponse({"sessions": PHOTO_SESSIONS})
|
||||
|
||||
|
||||
@app.post("/upload-temp")
|
||||
async def upload_temp(file: UploadFile = File(...)):
|
||||
"""Временная загрузка фото перед выбором стиля"""
|
||||
try:
|
||||
content = await file.read()
|
||||
session_id = hashlib.md5(f"{content}{datetime.now().timestamp()}".encode()).hexdigest()
|
||||
|
||||
# Сохраняем оригинал
|
||||
original_filename = f"temp_{session_id}_{file.filename}"
|
||||
original_path = f"photos/{original_filename}"
|
||||
|
||||
with open(original_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
temp_photos[session_id] = {
|
||||
"path": original_path,
|
||||
"filename": original_filename,
|
||||
"content": content,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"session_id": session_id,
|
||||
"preview_url": f"/photos/{original_filename}"
|
||||
})
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/process-with-style")
|
||||
async def process_with_style(
|
||||
session_id: str = Form(...),
|
||||
style_id: str = Form(...)
|
||||
):
|
||||
"""Обработка фото с выбранным стилем"""
|
||||
try:
|
||||
if session_id not in temp_photos:
|
||||
raise HTTPException(status_code=404, detail="Фото не найдено")
|
||||
|
||||
if style_id not in PHOTO_SESSIONS:
|
||||
raise HTTPException(status_code=404, detail="Стиль не найден")
|
||||
|
||||
photo_data = temp_photos[session_id]
|
||||
style = PHOTO_SESSIONS[style_id]
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||
|
||||
logger.info(f"🔄 Обработка фото со стилем: {style['name']}")
|
||||
|
||||
if not PIAPI_API_KEY or PIAPI_API_KEY == "your_api_key_here":
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"original": photo_data["path"],
|
||||
"processed": photo_data["path"],
|
||||
"original_url": f"/photos/{photo_data['filename']}",
|
||||
"processed_url": f"/photos/{photo_data['filename']}",
|
||||
"timestamp": timestamp,
|
||||
"warning": "⚠️ API ключ не настроен"
|
||||
})
|
||||
|
||||
try:
|
||||
result_url = await process_with_nano_banana(photo_data["content"], style["prompt"])
|
||||
|
||||
if result_url:
|
||||
connector = get_proxy_connector()
|
||||
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.get(result_url, timeout=60) as resp:
|
||||
if resp.status == 200:
|
||||
processed_filename = f"processed_{timestamp}.{OUTPUT_FORMAT}"
|
||||
processed_path = f"photos/{processed_filename}"
|
||||
|
||||
processed_content = await resp.read()
|
||||
with open(processed_path, "wb") as f:
|
||||
f.write(processed_content)
|
||||
|
||||
logger.info(f"✅ Обработанное фото сохранено: {processed_filename}")
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"original": photo_data["path"],
|
||||
"processed": processed_path,
|
||||
"original_url": f"/photos/{photo_data['filename']}",
|
||||
"processed_url": f"/photos/{processed_filename}",
|
||||
"timestamp": timestamp,
|
||||
"style_name": style["name"]
|
||||
})
|
||||
else:
|
||||
raise Exception(f"HTTP {resp.status}")
|
||||
else:
|
||||
raise Exception("Не получен URL результата")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка: {e}")
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"original": photo_data["path"],
|
||||
"processed": photo_data["path"],
|
||||
"original_url": f"/photos/{photo_data['filename']}",
|
||||
"processed_url": f"/photos/{photo_data['filename']}",
|
||||
"timestamp": timestamp,
|
||||
"warning": f"⚠️ Ошибка: {str(e)[:100]}"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Критическая ошибка: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/publish-photo")
|
||||
async def publish_photo(photo_path: str = Form(...)):
|
||||
try:
|
||||
photo_info = {
|
||||
"path": photo_path,
|
||||
"published_at": datetime.now().isoformat(),
|
||||
"id": hashlib.md5(f"{photo_path}{datetime.now().timestamp()}".encode()).hexdigest()
|
||||
}
|
||||
published_photos.append(photo_info)
|
||||
|
||||
logger.info(f"📢 Фото опубликовано: {photo_path}")
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"photo_id": photo_info["id"],
|
||||
"message": "Фото опубликовано"
|
||||
})
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/get-published-photos")
|
||||
async def get_published_photos():
|
||||
return JSONResponse({"photos": published_photos})
|
||||
|
||||
|
||||
@app.get("/download-photo/{photo_id}")
|
||||
async def download_photo(photo_id: str):
|
||||
for photo in published_photos:
|
||||
if photo["id"] == photo_id:
|
||||
photo_path = photo["path"].lstrip("/")
|
||||
if os.path.exists(photo_path):
|
||||
return FileResponse(
|
||||
photo_path,
|
||||
media_type='image/jpeg',
|
||||
filename=os.path.basename(photo_path)
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="Фото не найдено")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
proxy_status = "not configured"
|
||||
if USE_PROXY and PROXY_URL:
|
||||
connector = get_proxy_connector()
|
||||
if connector:
|
||||
try:
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.get("https://api.ipify.org?format=json", timeout=10) as resp:
|
||||
if resp.status == 200:
|
||||
ip_data = await resp.json()
|
||||
proxy_status = f"working (IP: {ip_data.get('ip')})"
|
||||
else:
|
||||
proxy_status = "error"
|
||||
except Exception as e:
|
||||
proxy_status = f"error: {str(e)}"
|
||||
else:
|
||||
proxy_status = "failed to create connector"
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"api_configured": bool(PIAPI_API_KEY and PIAPI_API_KEY != "your_api_key_here"),
|
||||
"model": MODEL_NAME,
|
||||
"proxy_enabled": USE_PROXY,
|
||||
"proxy_status": proxy_status,
|
||||
"published_photos_count": len(published_photos),
|
||||
"available_sessions": len(PHOTO_SESSIONS)
|
||||
}
|
||||
|
||||
|
||||
@app.get("/test-proxy")
|
||||
async def test_proxy():
|
||||
results = {
|
||||
"proxy_enabled": USE_PROXY,
|
||||
"tests": {}
|
||||
}
|
||||
|
||||
if not USE_PROXY or not PROXY_URL:
|
||||
results["message"] = "Прокси не настроен"
|
||||
return JSONResponse(results)
|
||||
|
||||
connector = get_proxy_connector()
|
||||
|
||||
if not connector:
|
||||
results["tests"]["proxy_connector"] = {
|
||||
"status": "error",
|
||||
"message": "Не удалось создать прокси коннектор"
|
||||
}
|
||||
return JSONResponse(results)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.get("https://api.ipify.org?format=json", timeout=15) as resp:
|
||||
if resp.status == 200:
|
||||
ip_data = await resp.json()
|
||||
results["tests"]["ip_check"] = {
|
||||
"status": "success",
|
||||
"ip": ip_data.get("ip")
|
||||
}
|
||||
else:
|
||||
results["tests"]["ip_check"] = {
|
||||
"status": "error",
|
||||
"message": f"HTTP {resp.status}"
|
||||
}
|
||||
except Exception as e:
|
||||
results["tests"]["ip_check"] = {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
success_tests = sum(1 for test in results["tests"].values() if test.get("status") == "success")
|
||||
results["summary"] = {
|
||||
"tests_passed": success_tests,
|
||||
"tests_total": len(results["tests"]),
|
||||
"proxy_working": success_tests > 0
|
||||
}
|
||||
|
||||
return JSONResponse(results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("=" * 60)
|
||||
print("📸 Нейрофотосессия - Сервер запущен")
|
||||
print("=" * 60)
|
||||
print(f"🌐 Адрес: http://localhost:{PORT}")
|
||||
print(f"📱 Оператор: http://localhost:{PORT}")
|
||||
print(f"🖼️ Галерея: http://localhost:{PORT}/display")
|
||||
print("=" * 60)
|
||||
print(f"🎨 Доступно стилей: {len(PHOTO_SESSIONS)}")
|
||||
for style_id, style in PHOTO_SESSIONS.items():
|
||||
print(f" {style['icon']} {style['name']}")
|
||||
print("=" * 60)
|
||||
|
||||
uvicorn.run(app, host=HOST, port=PORT)
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Проверка наличия .env файла
|
||||
if [ ! -f .env ]; then
|
||||
echo "❌ Файл .env не найден!"
|
||||
echo "📝 Создай файл .env из примера"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверка Python
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "❌ Python не установлен"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Установка зависимостей
|
||||
echo "📦 Установка зависимостей..."
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
# Запуск сервера
|
||||
echo "🚀 Запуск сервера..."
|
||||
python3 server.py
|
||||
@@ -0,0 +1,535 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Нейрофотосессия - Галерея</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Главный контейнер - сплит экран */
|
||||
.split-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Левая половина - галерея фото (50%) */
|
||||
.gallery-section {
|
||||
flex: 1;
|
||||
width: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Правая половина - QR код (50%) */
|
||||
.qr-section {
|
||||
flex: 1;
|
||||
width: 50%;
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
/* Заголовки */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 3px 3px 6px rgba(0,0,0,0.3);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: rgba(255,255,255,0.95);
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* Галерея фото */
|
||||
.photos-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 25px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
animation: fadeInUp 0.5s ease;
|
||||
}
|
||||
|
||||
.photo-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.photo-item img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.photo-info {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.photo-date {
|
||||
color: #667eea;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 25px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* QR секция */
|
||||
.qr-container {
|
||||
background: white;
|
||||
border-radius: 30px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
|
||||
animation: fadeInUp 0.5s ease;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.qr-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.qr-subtitle {
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.qr-code-wrapper {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.qr-code-wrapper img {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.current-photo-preview {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid #eee;
|
||||
}
|
||||
|
||||
.current-photo-preview img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.qr-instruction {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 60px 20px;
|
||||
font-size: 1.2rem;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных устройств */
|
||||
@media (max-width: 768px) {
|
||||
.split-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gallery-section,
|
||||
.qr-section {
|
||||
width: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.gallery-section {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.qr-code-wrapper img {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Стиль скроллбара */
|
||||
.gallery-section::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.gallery-section::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.gallery-section::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.gallery-section::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
/* Уведомление о новом фото */
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
animation: slideInRight 0.5s ease;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="split-container">
|
||||
<!-- Левая половина: Галерея фото -->
|
||||
<div class="gallery-section">
|
||||
<div class="header">
|
||||
<div class="main-title">НЕЙРОФОТОСЕССИЯ</div>
|
||||
<div class="subtitle">Молодежный Парламент ДНР</div>
|
||||
</div>
|
||||
<div id="gallery" class="photos-gallery">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📸</div>
|
||||
<p>Здесь появятся опубликованные фото</p>
|
||||
<p style="font-size: 0.9rem; margin-top: 10px;">Оператор опубликует фото, и они сразу появятся здесь</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая половина: QR Code -->
|
||||
<div class="qr-section">
|
||||
<div id="qrContainer" class="qr-container">
|
||||
<div class="qr-title">📱 СКАЧАТЬ ФОТО</div>
|
||||
<div class="qr-subtitle">Отсканируйте QR-код, чтобы скачать последнее фото</div>
|
||||
<div id="qrCodeWrapper" class="qr-code-wrapper">
|
||||
<div id="qrPlaceholder" style="width: 250px; height: 250px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border-radius: 20px;">
|
||||
<span style="color: #999;">Ожидание фото...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="currentPhotoPreview" class="current-photo-preview" style="display: none;">
|
||||
<div style="font-size: 12px; color: #888;">Текущее фото:</div>
|
||||
<img id="currentPhotoImg" src="" alt="Текущее фото">
|
||||
</div>
|
||||
<div class="qr-instruction">
|
||||
✨ QR-код обновляется автоматически при публикации нового фото
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPhotos = [];
|
||||
let updateInterval = null;
|
||||
let lastPhotoId = null;
|
||||
|
||||
// Загрузка опубликованных фото
|
||||
async function loadPublishedPhotos() {
|
||||
try {
|
||||
const response = await fetch('/get-published-photos');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.photos && data.photos.length > 0) {
|
||||
updateGallery(data.photos);
|
||||
|
||||
// Проверяем, есть ли новое фото
|
||||
if (lastPhotoId !== data.photos[data.photos.length - 1]?.id) {
|
||||
const lastPhoto = data.photos[data.photos.length - 1];
|
||||
if (lastPhoto && lastPhoto.id !== lastPhotoId) {
|
||||
lastPhotoId = lastPhoto.id;
|
||||
showNotification('Новое фото опубликовано!');
|
||||
updateQRCode(lastPhoto);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateGallery([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки фото:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление галереи
|
||||
function updateGallery(photos) {
|
||||
const gallery = document.getElementById('gallery');
|
||||
|
||||
if (!photos || photos.length === 0) {
|
||||
gallery.innerHTML = `<div class="empty-state">
|
||||
<div class="empty-state-icon">📸</div>
|
||||
<p>Здесь появятся опубликованные фото</p>
|
||||
<p style="font-size: 0.9rem; margin-top: 10px;">Оператор опубликует фото, и они сразу появятся здесь</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем наличие новых фото
|
||||
const newPhotos = photos.filter(photo => !currentPhotos.find(p => p.id === photo.id));
|
||||
|
||||
if (newPhotos.length > 0 || currentPhotos.length !== photos.length) {
|
||||
// Перестраиваем галерею
|
||||
gallery.innerHTML = '';
|
||||
|
||||
// Показываем фото в обратном порядке (новые сверху)
|
||||
[...photos].reverse().forEach(photo => {
|
||||
const photoDiv = createPhotoElement(photo);
|
||||
gallery.appendChild(photoDiv);
|
||||
});
|
||||
|
||||
currentPhotos = [...photos];
|
||||
|
||||
// Если есть новое фото, обновляем QR для последнего
|
||||
if (newPhotos.length > 0) {
|
||||
updateQRCode(newPhotos[0]);
|
||||
|
||||
// Прокручиваем галерею к новому фото
|
||||
setTimeout(() => {
|
||||
const firstPhoto = gallery.firstChild;
|
||||
if (firstPhoto) {
|
||||
firstPhoto.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, 100);
|
||||
} else if (photos.length > 0) {
|
||||
// Обновляем QR для последнего фото
|
||||
updateQRCode(photos[photos.length - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Создание элемента фото
|
||||
function createPhotoElement(photo) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'photo-item';
|
||||
|
||||
const downloadUrl = `${window.location.origin}/download-photo/${photo.id}`;
|
||||
const date = new Date(photo.published_at);
|
||||
const formattedDate = date.toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
div.innerHTML = `
|
||||
<img src="${photo.path}" alt="Фото" loading="lazy" onclick="viewFullImage('${photo.path}')">
|
||||
<div class="photo-info">
|
||||
<div class="photo-date">📅 ${formattedDate}</div>
|
||||
<a href="${downloadUrl}" class="download-btn" download>💾 Скачать фото</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
// Просмотр полноразмерного изображения
|
||||
window.viewFullImage = function(url) {
|
||||
const modal = document.createElement('div');
|
||||
modal.style.position = 'fixed';
|
||||
modal.style.top = '0';
|
||||
modal.style.left = '0';
|
||||
modal.style.width = '100%';
|
||||
modal.style.height = '100%';
|
||||
modal.style.backgroundColor = 'rgba(0,0,0,0.95)';
|
||||
modal.style.zIndex = '1001';
|
||||
modal.style.display = 'flex';
|
||||
modal.style.alignItems = 'center';
|
||||
modal.style.justifyContent = 'center';
|
||||
modal.style.cursor = 'pointer';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.style.maxWidth = '90%';
|
||||
img.style.maxHeight = '90%';
|
||||
img.style.borderRadius = '10px';
|
||||
img.style.objectFit = 'contain';
|
||||
|
||||
modal.appendChild(img);
|
||||
modal.onclick = () => modal.remove();
|
||||
document.body.appendChild(modal);
|
||||
};
|
||||
|
||||
// Обновление QR-кода
|
||||
function updateQRCode(photo) {
|
||||
if (!photo) return;
|
||||
|
||||
const downloadUrl = `${window.location.origin}/download-photo/${photo.id}`;
|
||||
const qrWrapper = document.getElementById('qrCodeWrapper');
|
||||
const previewContainer = document.getElementById('currentPhotoPreview');
|
||||
const previewImg = document.getElementById('currentPhotoImg');
|
||||
|
||||
// Создаем QR код через API
|
||||
const qrImg = document.createElement('img');
|
||||
qrImg.src = `https://api.qrserver.com/v1/create-qr-code/?size=250x250&margin=10&data=${encodeURIComponent(downloadUrl)}`;
|
||||
qrImg.alt = 'QR Code';
|
||||
qrImg.style.width = '250px';
|
||||
qrImg.style.height = '250px';
|
||||
qrImg.style.borderRadius = '10px';
|
||||
|
||||
qrWrapper.innerHTML = '';
|
||||
qrWrapper.appendChild(qrImg);
|
||||
|
||||
// Обновляем превью текущего фото
|
||||
previewImg.src = photo.path;
|
||||
previewContainer.style.display = 'block';
|
||||
|
||||
// Добавляем информацию о ссылке в консоль для отладки
|
||||
console.log('📱 QR код обновлен для фото:', downloadUrl);
|
||||
}
|
||||
|
||||
// Показать уведомление о новом фото
|
||||
function showNotification(message) {
|
||||
// Удаляем старые уведомления
|
||||
const oldNotification = document.querySelector('.notification');
|
||||
if (oldNotification) {
|
||||
oldNotification.remove();
|
||||
}
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'notification';
|
||||
notification.innerHTML = `✨ ${message}`;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Запускаем периодическое обновление
|
||||
loadPublishedPhotos();
|
||||
updateInterval = setInterval(loadPublishedPhotos, 3000);
|
||||
|
||||
// Обработка закрытия страницы
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,724 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Нейрофотосессия - Оператор</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.camera-section {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button, .file-label {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button:active, .file-label:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #38a169;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ed8936;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
video, canvas {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sessions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.session-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.session-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.session-card.selected {
|
||||
border-color: #ffd700;
|
||||
background: linear-gradient(135deg, #5a67d8 0%, #6b46a0 100%);
|
||||
}
|
||||
|
||||
.session-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.session-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.session-desc {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.photos-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.photo-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.photo-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.photo-card img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.photo-info {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.photo-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.comparison-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.comparison-container button {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0,0,0,0.9);
|
||||
border-radius: 20px;
|
||||
z-index: 1000;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid rgba(255,255,255,0.3);
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.camera-placeholder {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: white;
|
||||
margin: 20px 0 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sessions-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.photos-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="apiStatus" class="status-badge"></div>
|
||||
|
||||
<div class="container">
|
||||
<h1>📸 Нейрофотосессия - Студия</h1>
|
||||
|
||||
<div class="camera-section">
|
||||
<div class="camera-controls">
|
||||
<button class="btn-primary" id="startCamera">📷 Включить камеру</button>
|
||||
<label for="photoUpload" class="file-label btn-warning">📱 Выбрать из галереи</label>
|
||||
<button class="btn-primary" id="takePhoto" disabled>📸 Сделать фото</button>
|
||||
<button class="btn-danger" id="stopCamera" disabled>⏹️ Остановить</button>
|
||||
</div>
|
||||
<div class="video-container">
|
||||
<video id="video" autoplay playsinline style="display: none;"></video>
|
||||
<canvas id="canvas" style="display: none;"></canvas>
|
||||
<div id="cameraPlaceholder" class="camera-placeholder">
|
||||
📷 Нажми "Включить камеру" или выбери фото из галереи
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sessionsSection" style="display: none;">
|
||||
<div class="section-title">🎨 Выбери фотосессию:</div>
|
||||
<div id="sessionsGrid" class="sessions-grid"></div>
|
||||
<div class="camera-controls" style="margin-top: 15px;">
|
||||
<button id="processBtn" class="btn-success" style="display: none;">✨ Обработать фото</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="photosSection" style="display: none;">
|
||||
<div class="section-title">🖼️ Обработанные фото:</div>
|
||||
<div id="photosContainer" class="photos-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" id="photoUpload" accept="image/*" capture="environment" style="display: none;">
|
||||
<div id="loading" class="loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<p id="loadingText">Обработка фото...</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let video = document.getElementById('video');
|
||||
let canvas = document.getElementById('canvas');
|
||||
let stream = null;
|
||||
let cameraActive = false;
|
||||
let currentSessionId = null;
|
||||
let currentPreviewUrl = null;
|
||||
let selectedStyle = null;
|
||||
let availableSessions = {};
|
||||
|
||||
// Проверка статуса API
|
||||
async function checkAPIStatus() {
|
||||
try {
|
||||
const response = await fetch('/health');
|
||||
const data = await response.json();
|
||||
const statusDiv = document.getElementById('apiStatus');
|
||||
|
||||
if (data.api_configured) {
|
||||
statusDiv.innerHTML = '✅ Нейросеть готова';
|
||||
statusDiv.style.backgroundColor = '#48bb78';
|
||||
} else {
|
||||
statusDiv.innerHTML = '⚠️ Нейрообработка не настроена';
|
||||
statusDiv.style.backgroundColor = '#f56565';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки статуса:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка доступных стилей
|
||||
async function loadSessions() {
|
||||
try {
|
||||
const response = await fetch('/sessions');
|
||||
const data = await response.json();
|
||||
availableSessions = data.sessions;
|
||||
|
||||
const grid = document.getElementById('sessionsGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
for (const [id, session] of Object.entries(availableSessions)) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'session-card';
|
||||
card.setAttribute('data-style-id', id);
|
||||
card.innerHTML = `
|
||||
<div class="session-icon">${session.icon}</div>
|
||||
<div class="session-name">${session.name}</div>
|
||||
<div class="session-desc">${session.description}</div>
|
||||
`;
|
||||
card.onclick = () => selectStyle(id);
|
||||
grid.appendChild(card);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки стилей:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Выбор стиля
|
||||
function selectStyle(styleId) {
|
||||
selectedStyle = styleId;
|
||||
|
||||
// Подсветка выбранного стиля
|
||||
document.querySelectorAll('.session-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
if (card.getAttribute('data-style-id') === styleId) {
|
||||
card.classList.add('selected');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('processBtn').style.display = 'block';
|
||||
}
|
||||
|
||||
// Запуск камеры
|
||||
document.getElementById('startCamera').addEventListener('click', async () => {
|
||||
const takePhotoBtn = document.getElementById('takePhoto');
|
||||
const stopCameraBtn = document.getElementById('stopCamera');
|
||||
const placeholder = document.getElementById('cameraPlaceholder');
|
||||
|
||||
try {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
alert('Ваш браузер не поддерживает доступ к камере');
|
||||
return;
|
||||
}
|
||||
|
||||
const constraints = {
|
||||
video: { facingMode: { exact: "environment" } }
|
||||
};
|
||||
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
video.srcObject = stream;
|
||||
video.style.display = 'block';
|
||||
canvas.style.display = 'none';
|
||||
placeholder.style.display = 'none';
|
||||
|
||||
await video.play();
|
||||
cameraActive = true;
|
||||
|
||||
takePhotoBtn.disabled = false;
|
||||
stopCameraBtn.disabled = false;
|
||||
|
||||
} catch (err) {
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
video.srcObject = stream;
|
||||
video.style.display = 'block';
|
||||
canvas.style.display = 'none';
|
||||
placeholder.style.display = 'none';
|
||||
await video.play();
|
||||
cameraActive = true;
|
||||
takePhotoBtn.disabled = false;
|
||||
stopCameraBtn.disabled = false;
|
||||
} catch (err2) {
|
||||
alert('Ошибка доступа к камере. Используйте кнопку "Выбрать из галереи"');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Остановка камеры
|
||||
document.getElementById('stopCamera').addEventListener('click', () => {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
video.srcObject = null;
|
||||
video.style.display = 'none';
|
||||
cameraActive = false;
|
||||
|
||||
document.getElementById('takePhoto').disabled = true;
|
||||
document.getElementById('stopCamera').disabled = true;
|
||||
|
||||
const placeholder = document.getElementById('cameraPlaceholder');
|
||||
placeholder.style.display = 'block';
|
||||
placeholder.innerHTML = '📷 Камера отключена';
|
||||
}
|
||||
});
|
||||
|
||||
// Выбор фото из галереи
|
||||
document.getElementById('photoUpload').addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
showLoading('Загрузка фото...');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/upload-temp', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentSessionId = result.session_id;
|
||||
currentPreviewUrl = result.preview_url;
|
||||
|
||||
// Показываем превью
|
||||
canvas.width = 640;
|
||||
canvas.height = 640;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
canvas.style.display = 'block';
|
||||
video.style.display = 'none';
|
||||
document.getElementById('cameraPlaceholder').style.display = 'none';
|
||||
};
|
||||
img.src = currentPreviewUrl;
|
||||
|
||||
// Показываем выбор стилей
|
||||
document.getElementById('sessionsSection').style.display = 'block';
|
||||
document.getElementById('photosSection').style.display = 'block';
|
||||
|
||||
await loadSessions();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка загрузки: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
e.target.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Сделать фото с камеры
|
||||
document.getElementById('takePhoto').addEventListener('click', () => {
|
||||
if (!cameraActive || !video.srcObject) {
|
||||
alert('Сначала включите камеру!');
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
canvas.style.display = 'block';
|
||||
video.style.display = 'none';
|
||||
|
||||
canvas.toBlob(async (blob) => {
|
||||
showLoading('Загрузка фото...');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, 'photo.jpg');
|
||||
|
||||
try {
|
||||
const response = await fetch('/upload-temp', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentSessionId = result.session_id;
|
||||
currentPreviewUrl = result.preview_url;
|
||||
|
||||
document.getElementById('sessionsSection').style.display = 'block';
|
||||
document.getElementById('photosSection').style.display = 'block';
|
||||
|
||||
await loadSessions();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка загрузки: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
video.style.display = 'block';
|
||||
canvas.style.display = 'none';
|
||||
}
|
||||
}, 'image/jpeg', 0.9);
|
||||
});
|
||||
|
||||
// Обработка фото с выбранным стилем
|
||||
document.getElementById('processBtn').addEventListener('click', async () => {
|
||||
if (!selectedStyle) {
|
||||
alert('Выберите стиль фотосессии!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentSessionId) {
|
||||
alert('Сначала загрузите фото!');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading('Обработка фото через нейросеть...');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('session_id', currentSessionId);
|
||||
formData.append('style_id', selectedStyle);
|
||||
|
||||
try {
|
||||
const response = await fetch('/process-with-style', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
addPhotoToGrid(result);
|
||||
if (result.warning) {
|
||||
setTimeout(() => alert(result.warning), 100);
|
||||
}
|
||||
} else {
|
||||
alert('Ошибка при обработке');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// Добавление фото в сетку
|
||||
function addPhotoToGrid(photoData) {
|
||||
const container = document.getElementById('photosContainer');
|
||||
const card = document.createElement('div');
|
||||
card.className = 'photo-card';
|
||||
card.setAttribute('data-original', photoData.original_url);
|
||||
card.setAttribute('data-processed', photoData.processed_url);
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const styleName = photoData.style_name || 'Обработанное фото';
|
||||
|
||||
card.innerHTML = `
|
||||
<img src="${photoData.processed_url}" alt="Обработанное фото" onclick="showComparison('${photoData.original_url}', '${photoData.processed_url}')">
|
||||
<div class="photo-info">
|
||||
<div class="photo-title">✨ ${styleName} | ${timestamp}</div>
|
||||
<div class="comparison-container">
|
||||
<button class="btn-primary" onclick="viewFullImage('${photoData.processed_url}')">👁️ Увеличить</button>
|
||||
<button class="btn-success" onclick="publishPhoto('${photoData.processed_url}')">📢 Опубликовать</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertBefore(card, container.firstChild);
|
||||
}
|
||||
|
||||
// Просмотр полноразмерного изображения
|
||||
window.viewFullImage = function(url) {
|
||||
const modal = document.createElement('div');
|
||||
modal.style.position = 'fixed';
|
||||
modal.style.top = '0';
|
||||
modal.style.left = '0';
|
||||
modal.style.width = '100%';
|
||||
modal.style.height = '100%';
|
||||
modal.style.backgroundColor = 'rgba(0,0,0,0.95)';
|
||||
modal.style.zIndex = '1001';
|
||||
modal.style.display = 'flex';
|
||||
modal.style.alignItems = 'center';
|
||||
modal.style.justifyContent = 'center';
|
||||
modal.style.cursor = 'pointer';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.style.maxWidth = '90%';
|
||||
img.style.maxHeight = '90%';
|
||||
img.style.borderRadius = '10px';
|
||||
img.style.objectFit = 'contain';
|
||||
|
||||
modal.appendChild(img);
|
||||
modal.onclick = () => modal.remove();
|
||||
document.body.appendChild(modal);
|
||||
};
|
||||
|
||||
// Сравнение фото
|
||||
window.showComparison = function(originalUrl, processedUrl) {
|
||||
const modal = document.createElement('div');
|
||||
modal.style.position = 'fixed';
|
||||
modal.style.top = '0';
|
||||
modal.style.left = '0';
|
||||
modal.style.width = '100%';
|
||||
modal.style.height = '100%';
|
||||
modal.style.backgroundColor = 'rgba(0,0,0,0.95)';
|
||||
modal.style.zIndex = '1001';
|
||||
modal.style.display = 'flex';
|
||||
modal.style.flexDirection = 'column';
|
||||
modal.style.alignItems = 'center';
|
||||
modal.style.justifyContent = 'center';
|
||||
modal.style.padding = '20px';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div style="display: flex; gap: 20px; flex-wrap: wrap; justify-content: center;">
|
||||
<div>
|
||||
<h3 style="color: white; text-align: center; margin-bottom: 10px;">📸 Оригинал</h3>
|
||||
<img src="${originalUrl}" style="max-width: 300px; max-height: 300px; border-radius: 10px; object-fit: contain;">
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="color: white; text-align: center; margin-bottom: 10px;">✨ Обработанное</h3>
|
||||
<img src="${processedUrl}" style="max-width: 300px; max-height: 300px; border-radius: 10px; object-fit: contain;">
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="this.closest('div').remove()" style="margin-top: 20px; padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 10px; cursor: pointer;">Закрыть</button>
|
||||
`;
|
||||
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
};
|
||||
|
||||
document.body.appendChild(modal);
|
||||
};
|
||||
|
||||
// Публикация фото
|
||||
window.publishPhoto = async function(photoPath) {
|
||||
const formData = new FormData();
|
||||
formData.append('photo_path', photoPath);
|
||||
|
||||
try {
|
||||
const response = await fetch('/publish-photo', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✅ Фото опубликовано!');
|
||||
const downloadUrl = `${window.location.origin}/download-photo/${result.photo_id}`;
|
||||
console.log('Ссылка для скачивания:', downloadUrl);
|
||||
} else {
|
||||
alert('Ошибка при публикации');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
function showLoading(text) {
|
||||
document.getElementById('loadingText').innerText = text;
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
checkAPIStatus();
|
||||
setInterval(checkAPIStatus, 30000);
|
||||
|
||||
// Загрузка стилей при старте
|
||||
loadSessions();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from aiohttp_socks import ProxyConnector
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import base64
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Конфигурация
|
||||
PIAPI_API_KEY = os.getenv("PIAPI_API_KEY")
|
||||
PIAPI_API_URL = os.getenv("PIAPI_API_URL", "https://api.piapi.ai/api/v1/task")
|
||||
PROXY_URL = os.getenv("PROXY_URL")
|
||||
USE_PROXY = os.getenv("USE_PROXY", "true").lower() == "true"
|
||||
|
||||
MODEL_NAME = os.getenv("MODEL_NAME", "gemini")
|
||||
TASK_TYPE = os.getenv("TASK_TYPE", "nano-banana-pro")
|
||||
ASPECT_RATIO = os.getenv("ASPECT_RATIO", "9:16")
|
||||
STRENGTH = float(os.getenv("STRENGTH", "0.65"))
|
||||
SAFETY_LEVEL = os.getenv("SAFETY_LEVEL", "medium")
|
||||
OUTPUT_FORMAT = os.getenv("OUTPUT_FORMAT", "png")
|
||||
|
||||
IMGBB_API_KEY = os.getenv("IMGBB_API_KEY", "")
|
||||
|
||||
|
||||
def get_proxy_connector():
|
||||
"""Создает ProxyConnector для SOCKS5"""
|
||||
if not USE_PROXY or not PROXY_URL:
|
||||
return None
|
||||
|
||||
try:
|
||||
match = re.search(r'socks5://([^:]+):([^@]+)@([^:]+):(\d+)', PROXY_URL)
|
||||
if match:
|
||||
username = match.group(1)
|
||||
password = match.group(2)
|
||||
host = match.group(3)
|
||||
port = int(match.group(4))
|
||||
connector = ProxyConnector.from_url(f"socks5://{username}:{password}@{host}:{port}")
|
||||
logger.info(f"✅ Прокси настроен: {host}:{port}")
|
||||
return connector
|
||||
else:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(PROXY_URL)
|
||||
connector = ProxyConnector.from_url(f"socks5://{parsed.hostname}:{parsed.port}")
|
||||
logger.info(f"✅ Прокси настроен (без авторизации): {parsed.hostname}:{parsed.port}")
|
||||
return connector
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка настройки прокси: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def poll_task_result(task_id: str, max_attempts: int = 60) -> str:
|
||||
"""Ожидание результата задачи с правильным управлением сессией"""
|
||||
print("\n" + "=" * 60)
|
||||
print("⏳ Ожидание результата обработки")
|
||||
print("=" * 60)
|
||||
|
||||
get_url = f"https://api.piapi.ai/api/v1/task/{task_id}"
|
||||
headers = {"x-api-key": PIAPI_API_KEY}
|
||||
|
||||
print(f"📋 ID задачи: {task_id}")
|
||||
print(f"🔄 Ожидание результата (максимум {max_attempts * 3} секунд)...")
|
||||
|
||||
connector = get_proxy_connector()
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
# Создаем новую сессию для каждой попытки
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.get(get_url, headers=headers, timeout=30) as response:
|
||||
if response.status != 200:
|
||||
print(f"⚠️ HTTP {response.status}, повтор через 3 секунды...")
|
||||
await asyncio.sleep(3)
|
||||
continue
|
||||
|
||||
data = await response.json()
|
||||
data_info = data.get('data', {})
|
||||
status = data_info.get('status', 'unknown')
|
||||
output = data_info.get('output')
|
||||
|
||||
print(f" Попытка {attempt + 1}: статус = {status}")
|
||||
|
||||
if status == 'completed' and output:
|
||||
result_url = None
|
||||
if isinstance(output, dict):
|
||||
if output.get('image_urls') and len(output['image_urls']) > 0:
|
||||
result_url = output['image_urls'][0]
|
||||
print(f" ✅ Найден URL в image_urls[0]")
|
||||
elif output.get('url'):
|
||||
result_url = output['url']
|
||||
print(f" ✅ Найден URL в url")
|
||||
elif output.get('image'):
|
||||
result_url = output['image']
|
||||
print(f" ✅ Найден URL в image")
|
||||
|
||||
if result_url:
|
||||
print(f"\n✅ ЗАДАЧА ВЫПОЛНЕНА!")
|
||||
print(f"📸 URL результата: {result_url[:100]}...")
|
||||
return result_url
|
||||
else:
|
||||
print(f"⚠️ Статус completed, но URL не найден")
|
||||
print(f" Output: {output}")
|
||||
|
||||
elif status == 'failed':
|
||||
error = data_info.get('error', {})
|
||||
error_msg = error.get('message', 'Unknown error')
|
||||
print(f"❌ Задача не выполнена: {error_msg}")
|
||||
return None
|
||||
|
||||
elif status in ['pending', 'processing']:
|
||||
# Продолжаем ожидание
|
||||
pass
|
||||
|
||||
else:
|
||||
print(f" Неизвестный статус: {status}")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print(f"⚠️ Таймаут при опросе (попытка {attempt + 1})")
|
||||
except aiohttp.ClientError as e:
|
||||
print(f"⚠️ Клиентская ошибка (попытка {attempt + 1}): {e}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Ошибка (попытка {attempt + 1}): {e}")
|
||||
|
||||
# Ждем перед следующей попыткой
|
||||
await asyncio.sleep(3)
|
||||
|
||||
print("❌ Превышено время ожидания")
|
||||
return None
|
||||
|
||||
|
||||
async def download_result(url: str):
|
||||
"""Скачивание результата"""
|
||||
print("\n" + "=" * 60)
|
||||
print("💾 Скачивание результата")
|
||||
print("=" * 60)
|
||||
|
||||
connector = get_proxy_connector()
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.get(url, timeout=30) as response:
|
||||
if response.status == 200:
|
||||
content = await response.read()
|
||||
print(f"✅ Изображение скачано! Размер: {len(content)} байт")
|
||||
|
||||
os.makedirs("test_results", exist_ok=True)
|
||||
filename = f"test_results/test_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
||||
with open(filename, "wb") as f:
|
||||
f.write(content)
|
||||
print(f"💾 Сохранено в: {filename}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Ошибка скачивания: HTTP {response.status}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def check_status(task_id: str):
|
||||
"""Проверка статуса задачи"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🔍 ПРОВЕРКА СТАТУСА ЗАДАЧИ")
|
||||
print("=" * 60)
|
||||
|
||||
get_url = f"https://api.piapi.ai/api/v1/task/{task_id}"
|
||||
headers = {"x-api-key": PIAPI_API_KEY}
|
||||
connector = get_proxy_connector()
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with session.get(get_url, headers=headers, timeout=30) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
# Проверяем статус
|
||||
data_info = data.get('data', {})
|
||||
status = data_info.get('status')
|
||||
output = data_info.get('output')
|
||||
|
||||
if status == 'completed' and output:
|
||||
print("\n✅ Задача выполнена!")
|
||||
if isinstance(output, dict):
|
||||
if output.get('image_urls'):
|
||||
print(f"📸 URL: {output['image_urls'][0]}")
|
||||
elif status == 'failed':
|
||||
error = data_info.get('error', {})
|
||||
print(f"\n❌ Задача не выполнена: {error.get('message')}")
|
||||
else:
|
||||
print(f"\n⏳ Статус: {status}")
|
||||
|
||||
return data
|
||||
else:
|
||||
print(f"❌ HTTP {response.status}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def full_test(task_id: str):
|
||||
"""Полный тест с ожиданием результата"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🚀 ПОЛНЫЙ ТЕСТ")
|
||||
print("=" * 60)
|
||||
|
||||
# Проверяем статус задачи
|
||||
data = await check_status(task_id)
|
||||
if not data:
|
||||
print("❌ Не удалось получить статус задачи")
|
||||
return
|
||||
|
||||
data_info = data.get('data', {})
|
||||
status = data_info.get('status')
|
||||
|
||||
# Если задача уже выполнена, сразу скачиваем
|
||||
if status == 'completed':
|
||||
output = data_info.get('output', {})
|
||||
if isinstance(output, dict):
|
||||
result_url = output.get('image_urls', [None])[0] or output.get('url')
|
||||
if result_url:
|
||||
await download_result(result_url)
|
||||
return
|
||||
|
||||
# Иначе ждем
|
||||
result_url = await poll_task_result(task_id)
|
||||
if result_url:
|
||||
await download_result(result_url)
|
||||
else:
|
||||
print("\n❌ Не удалось получить результат")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 2 and sys.argv[1] == "--full":
|
||||
task_id = sys.argv[2]
|
||||
asyncio.run(full_test(task_id))
|
||||
elif len(sys.argv) > 2 and sys.argv[1] == "--status":
|
||||
task_id = sys.argv[2]
|
||||
asyncio.run(check_status(task_id))
|
||||
else:
|
||||
print("Использование:")
|
||||
print(" python test_api.py --status TASK_ID - проверить статус")
|
||||
print(" python test_api.py --full TASK_ID - дождаться результата и скачать")
|
||||
Reference in New Issue
Block a user