This commit is contained in:
2026-05-07 00:17:25 +03:00
commit 1daa045273
15 changed files with 2279 additions and 0 deletions
+26
View File
@@ -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
View File
@@ -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
*~
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+8
View File
@@ -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
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+7
View File
@@ -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>
+8
View File
@@ -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
View File
@@ -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>
+16
View File
@@ -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/
+9
View File
@@ -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
+606
View File
@@ -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)
+22
View File
@@ -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
+535
View File
@@ -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>
+724
View File
@@ -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
View File
@@ -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 - дождаться результата и скачать")