EVAALLERT / app.py
Aleksmorshen's picture
Update app.py
dd1edbe verified
import os
import io
import base64
from flask import Flask, request, jsonify, Response
from PIL import Image
import google.generativeai as genai
import numpy as np
app = Flask(__name__)
# It is highly recommended to load API keys from environment variables
# instead of hardcoding them for security and flexibility.
# Example: API_KEY_INTERNAL = os.environ.get("GEMINI_API_KEY")
API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Replace with your actual Google Gemini API Key
html_template = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>EVA - Генератор постов</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
/* Light Theme System Colors */
--system-gray-100-light: #F2F2F7; /* Background */
--system-gray-75-light: #F8F8FA; /* Input/Card background slightly lighter */
--system-gray-50-light: #FFFFFF; /* Content Card background */
--system-gray-dark-100-light: #000000; /* Primary Text */
--system-gray-dark-75-light: #1C1C1E; /* Secondary Text (darker) */
--system-gray-dark-50-light: #3A3A3C; /* Tertiary Text (darker) */
--system-gray-light-75-light: #8E8E93; /* Secondary Text (lighter) */
--system-gray-light-50-light: #AEAEB2; /* Tertiary Text (lighter) */
--system-blue-light: #007AFF; /* Primary Accent */
--system-blue-light-hover: #005ECF; /* Primary Accent Hover */
--system-red-light: #FF3B30; /* Error */
--system-green-light: #34C759; /* Success */
--system-separator-light: rgba(60, 60, 67, 0.29); /* Subtle Separator */
--system-separator-opaque-light: #D1D1D6; /* Opaque Separator */
/* Dark Theme System Colors */
--system-gray-100-dark: #1C1C1E; /* Background */
--system-gray-75-dark: #2C2C2E; /* Input/Card background slightly lighter */
--system-gray-50-dark: #000000; /* Content Card background (pure black for OLED) */
--system-gray-dark-100-dark: #FFFFFF; /* Primary Text */
--system-gray-dark-75-dark: #F2F2F7; /* Secondary Text (lighter) */
--system-gray-dark-50-dark: #E5E5EA; /* Tertiary Text (lighter) */
--system-gray-light-75-dark: #8E8E93; /* Secondary Text (darker) */
--system-gray-light-50-dark: #636366; /* Tertiary Text (darker) */
--system-blue-dark: #0A84FF; /* Primary Accent */
--system-blue-dark-hover: #3B9EFF; /* Primary Accent Hover */
--system-red-dark: #FF453A; /* Error */
--system-green-dark: #30D158; /* Success */
--system-separator-dark: rgba(84, 84, 88, 0.65); /* Subtle Separator */
--system-separator-opaque-dark: #38383A; /* Opaque Separator */
/* Semantic Colors (Resolved by prefers-color-scheme) */
--bg-color: var(--system-gray-100-light);
--content-bg: var(--system-gray-50-light);
--text-color: var(--system-gray-dark-100-light);
--secondary-text-color: var(--system-gray-light-75-light);
--tertiary-text-color: var(--system-gray-light-50-light);
--border-color: var(--system-separator-light);
--border-color-opaque: var(--system-separator-opaque-light);
--input-bg: var(--system-gray-75-light);
--primary-color: var(--system-blue-light);
--primary-color-hover: var(--system-blue-light-hover);
--error-color: var(--system-red-light);
--success-color: var(--system-green-light);
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: var(--system-gray-100-dark);
--content-bg: var(--system-gray-50-dark);
--text-color: var(--system-gray-dark-100-dark);
--secondary-text-color: var(--system-gray-light-75-dark);
--tertiary-text-color: var(--system-gray-light-50-dark);
--border-color: var(--system-separator-dark);
--border-color-opaque: var(--system-separator-opaque-dark);
--input-bg: var(--system-gray-75-dark);
--primary-color: var(--system-blue-dark);
--primary-color-hover: var(--system-blue-dark-hover);
--error-color: var(--system-red-dark);
--success-color: var(--system-green-dark);
}
}
html {
height: -webkit-fill-available;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding-top: 20px;
padding-bottom: 20px;
padding-left: max(20px, env(safe-area-inset-left));
padding-right: max(20px, env(safe-area-inset-right));
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
min-height: -webkit-fill-available;
line-height: 1.45;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
transition: background-color 0.3s ease;
}
.container {
background-color: var(--content-bg);
padding: 30px 30px 35px 30px;
border-radius: 28px;
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0,0,0,0.08);
max-width: 600px;
width: 100%;
box-sizing: border-box;
margin-top: 30px;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
h1 {
font-size: 34px;
font-weight: 700;
text-align: center;
margin-bottom: 6px;
color: var(--text-color);
letter-spacing: -0.6px;
}
p.subtitle {
font-size: 18px;
color: var(--secondary-text-color);
text-align: center;
margin-bottom: 40px;
font-weight: 400;
}
.form-group {
margin-bottom: 30px;
}
label.input-label {
display: block;
font-weight: 500;
margin-bottom: 12px;
font-size: 16px;
color: var(--secondary-text-color);
padding-left: 5px;
}
input[type="file"] {
display: block;
width: 100%;
padding: 16px 20px;
border: 1px solid var(--border-color-opaque);
border-radius: 14px;
font-size: 17px;
background-color: var(--input-bg);
color: var(--text-color);
box-sizing: border-box;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
cursor: pointer;
font-family: inherit;
line-height: 1;
}
input[type="file"]:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary-color) 25%, transparent);
outline: none;
}
input[type="file"]::file-selector-button {
font-weight: 600;
color: var(--primary-color);
background-color: transparent;
border: none;
padding: 0;
margin-right: 15px;
cursor: pointer;
font-size: 17px;
transition: color 0.2s ease;
}
input[type="file"]:hover::file-selector-button {
color: var(--primary-color-hover);
}
.language-options {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 5px;
justify-content: center; /* Center options horizontally */
}
.language-options label {
display: flex;
align-items: center;
background-color: var(--input-bg);
padding: 12px 22px;
border-radius: 30px;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.1s ease, border-color 0.2s ease;
font-weight: 500;
font-size: 15px;
color: var(--tertiary-text-color);
border: 1px solid var(--border-color); /* Subtle border */
user-select: none;
min-width: 90px;
text-align: center;
justify-content: center;
}
.language-options input[type="radio"] {
display: none;
}
.language-options input[type="radio"]:checked + label {
background-color: var(--primary-color);
color: white;
font-weight: 600;
border-color: var(--primary-color);
}
.language-options label:hover:not(:checked) {
background-color: color-mix(in srgb, var(--input-bg) 85%, var(--secondary-text-color));
border-color: color-mix(in srgb, var(--border-color-opaque) 70%, var(--primary-color));
}
.language-options input[type="radio"]:checked + label:hover {
background-color: var(--primary-color-hover);
border-color: var(--primary-color-hover);
}
.language-options label:active {
transform: scale(0.97);
}
button#generate-button {
width: 100%;
padding: 18px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 14px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
margin-top: 20px;
box-shadow: 0 4px 10px rgba(0, 122, 255, 0.2); /* Soft shadow for button */
}
button#generate-button:hover {
background-color: var(--primary-color-hover);
box-shadow: 0 6px 15px rgba(0, 122, 255, 0.3);
}
button#generate-button:active {
transform: scale(0.98);
box-shadow: 0 2px 5px rgba(0, 122, 255, 0.2);
}
button#generate-button:disabled {
background-color: var(--tertiary-text-color);
cursor: not-allowed;
box-shadow: none;
}
.output-section {
margin-top: 40px;
}
.output-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
label#output-label {
font-weight: 500;
font-size: 16px;
color: var(--secondary-text-color);
padding-left: 5px;
}
button#copy-button {
background-color: transparent;
border: none;
color: var(--primary-color);
font-size: 15px;
font-weight: 500;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.1s ease;
display: none;
text-wrap: nowrap;
}
button#copy-button:hover {
background-color: color-mix(in srgb, var(--primary-color) 15%, transparent);
}
button#copy-button:active {
background-color: color-mix(in srgb, var(--primary-color) 25%, transparent);
transform: scale(0.98);
}
button#copy-button.copied {
color: var(--success-color);
background-color: color-mix(in srgb, var(--success-color) 15%, transparent);
}
button#copy-button.copied:hover {
background-color: color-mix(in srgb, var(--success-color) 25%, transparent);
}
#output-container {
background-color: var(--input-bg);
padding: 20px 22px;
border-radius: 14px;
min-height: 150px;
border: 1px solid var(--border-color);
white-space: pre-wrap;
word-wrap: break-word;
font-size: 16px;
color: var(--text-color);
line-height: 1.6;
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); /* Subtle inner shadow */
}
#output-container.loading {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-style: italic;
color: var(--secondary-text-color);
animation: fadePulse 1.8s infinite ease-in-out;
}
#output-container.loading::before {
content: "Генерация...";
display: block;
}
#output-container.error {
color: var(--error-color);
font-weight: 500;
border-color: color-mix(in srgb, var(--error-color) 50%, transparent);
background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg));
box-shadow: inset 0 1px 3px rgba(255, 0, 0, 0.1);
}
#image-preview-container {
margin-top: 20px;
text-align: center;
padding: 10px;
}
#image-preview {
max-width: 100%;
max-height: 250px;
border-radius: 16px;
margin-top: 10px;
border: 1px solid var(--border-color-opaque);
display: none;
background-color: var(--input-bg);
object-fit: contain;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
@keyframes fadePulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
@media (max-width: 680px) {
body {
padding-top: 15px;
padding-bottom: 15px;
padding-left: max(15px, env(safe-area-inset-left));
padding-right: max(15px, env(safe-area-inset-right));
}
.container {
padding: 25px 20px 30px 20px;
margin-top: 15px;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
}
h1 {
font-size: 30px;
}
p.subtitle {
font-size: 17px;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 25px;
}
label.input-label {
font-size: 15px;
margin-bottom: 10px;
}
input[type="file"] {
padding: 14px 18px;
font-size: 16px;
border-radius: 12px;
}
input[type="file"]::file-selector-button {
font-size: 16px;
margin-right: 12px;
}
.language-options {
gap: 10px;
justify-content: space-between;
}
.language-options label {
padding: 10px 18px;
font-size: 14px;
min-width: unset;
flex-grow: 1;
}
button#generate-button {
padding: 16px;
font-size: 17px;
border-radius: 12px;
margin-top: 18px;
}
.output-section {
margin-top: 35px;
}
#output-container {
padding: 18px 20px;
font-size: 15px;
min-height: 120px;
border-radius: 12px;
}
label#output-label {
font-size: 15px;
}
button#copy-button {
font-size: 14px;
padding: 6px 10px;
}
#image-preview {
max-height: 200px;
border-radius: 14px;
}
}
@media (max-width: 480px) {
.container {
padding: 20px 15px 25px 15px;
border-radius: 20px;
}
h1 {
font-size: 28px;
}
p.subtitle {
font-size: 16px;
margin-bottom: 25px;
}
.language-options label {
font-size: 13px;
padding: 9px 15px;
}
input[type="file"] {
padding: 12px 15px;
font-size: 15px;
}
input[type="file"]::file-selector-button {
font-size: 15px;
margin-right: 10px;
}
button#generate-button {
padding: 15px;
font-size: 16px;
}
#output-container {
padding: 15px 18px;
font-size: 14px;
min-height: 100px;
}
.output-section {
margin-top: 30px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>EVA</h1>
<p class="subtitle">Генератор рекламных постов</p>
<form id="generate-form">
<div class="form-group">
<label class="input-label">Выберите язык поста</label>
<div class="language-options">
<input type="radio" id="lang-ru" name="language" value="Русский" checked>
<label for="lang-ru">Русский</label>
<input type="radio" id="lang-kg" name="language" value="Кыргызский">
<label for="lang-kg">Кыргызский</label>
<input type="radio" id="lang-kz" name="language" value="Казахский">
<label for="lang-kz">Казахский</label>
<input type="radio" id="lang-uz" name="language" value="Узбекский">
<label for="lang-uz">Узбекский</label>
</div>
</div>
<div class="form-group">
<label for="image-upload" class="input-label">Загрузить изображение</label>
<input type="file" id="image-upload" name="image" accept="image/png, image/jpeg, image/webp" required>
<div id="image-preview-container">
<img id="image-preview" src="#" alt="Предпросмотр изображения"/>
</div>
</div>
<button type="submit" id="generate-button">Пуск</button>
</form>
<div class="output-section">
<div class="output-header">
<label id="output-label">Результат</label>
<button id="copy-button">Копировать</button>
</div>
<div id="output-container" aria-live="polite">
</div>
</div>
</div>
<script>
const form = document.getElementById('generate-form');
const imageInput = document.getElementById('image-upload');
const imagePreview = document.getElementById('image-preview');
const outputContainer = document.getElementById('output-container');
const generateButton = document.getElementById('generate-button');
const imagePreviewContainer = document.getElementById('image-preview-container');
const copyButton = document.getElementById('copy-button');
imageInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
imagePreview.src = e.target.result;
imagePreview.style.display = 'block';
}
reader.readAsDataURL(file);
} else {
showError("Выбранный файл не является изображением. Пожалуйста, загрузите файл формата PNG, JPG или WEBP.");
imagePreview.src = '#';
imagePreview.style.display = 'none';
imageInput.value = ''; // Clear the input
}
} else {
imagePreview.src = '#';
imagePreview.style.display = 'none';
}
});
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (!imageInput.files || imageInput.files.length === 0) {
showError("Пожалуйста, загрузите изображение.");
return;
}
const formData = new FormData(form);
generateButton.disabled = true;
generateButton.textContent = 'Генерация...';
outputContainer.innerHTML = '';
outputContainer.classList.add('loading');
outputContainer.classList.remove('error');
copyButton.style.display = 'none';
copyButton.textContent = 'Копировать';
copyButton.classList.remove('copied');
try {
const response = await fetch('/generate', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || `Ошибка сервера: ${response.status}`);
}
outputContainer.textContent = result.text;
if (result.text && result.text.trim().length > 0) {
copyButton.style.display = 'block';
} else {
copyButton.style.display = 'none';
}
} catch (error) {
console.error("Fetch Error:", error);
showError(`Ошибка: ${error.message}`);
copyButton.style.display = 'none';
} finally {
generateButton.disabled = false;
generateButton.textContent = 'Пуск';
outputContainer.classList.remove('loading');
}
});
copyButton.addEventListener('click', () => {
const textToCopy = outputContainer.textContent;
if (!textToCopy || textToCopy.trim().length === 0) return;
navigator.clipboard.writeText(textToCopy).then(() => {
copyButton.textContent = 'Скопировано!';
copyButton.classList.add('copied');
setTimeout(() => {
copyButton.textContent = 'Копировать';
copyButton.classList.remove('copied');
}, 1500);
}).catch(err => {
console.error('Ошибка копирования: ', err);
copyButton.textContent = 'Ошибка';
setTimeout(() => {
copyButton.textContent = 'Копировать';
}, 1500);
});
});
function showError(message) {
outputContainer.textContent = message;
outputContainer.classList.add('error');
outputContainer.classList.remove('loading');
copyButton.style.display = 'none';
}
</script>
</body>
</html>
"""
def generate_ai_content(image_data, language):
try:
genai.configure(api_key=API_KEY_INTERNAL)
except Exception as e:
raise ValueError(f"Не удалось настроить Google AI: {e}. Проверьте ключ API или подключение.")
if not image_data:
raise ValueError("Файл изображения пуст или не был передан.")
try:
image_stream = io.BytesIO(image_data)
image = Image.open(image_stream).convert('RGB')
except Exception as e:
raise ValueError(f"Не удалось обработать изображение. Убедитесь, что это действительный файл изображения. Ошибка: {e}")
base_prompt = "ты профессионал в сфере распознавание изображение на использование ИИ в его генерации или редактировании , проанализируй это изображение , выяви все нужные артефакты ИИ , выдай свой вердикт и скажи где именно использовалось ИИ."
lang_suffix = ""
if language == "Русский":
lang_suffix = " Пиши на русском языке."
elif language == "Кыргызский":
lang_suffix = " Пиши на кыргызском языке."
elif language == "Казахский":
lang_suffix = " Пиши на казахском языке."
elif language == "Узбекский":
lang_suffix = " Пиши на узбекском языке."
else:
raise ValueError("Выбран неподдерживаемый язык.")
final_prompt = f"{base_prompt}{lang_suffix}"
try:
model = genai.GenerativeModel('gemma-3-27b-it') # Using the latest flash model
response = model.generate_content([final_prompt, image])
if hasattr(response, 'text'):
return response.text
else:
# Fallback for models that might return content in parts, though .text is standard
if response.parts:
return "".join(part.text for part in response.parts if hasattr(part, 'text'))
else:
raise ValueError("Не удалось получить текст ответа от модели. Возможно, не было сгенерировано контента.")
except Exception as e:
error_message = str(e)
if "API key not valid" in error_message:
raise ValueError("Внутренняя ошибка конфигурации API. Проверьте ваш ключ Google Gemini API.")
elif "Billing account not found" in error_message or "CREDENTIALS_LOCATION_MISSING" in error_message:
raise ValueError("Проблема с биллингом аккаунта Google Cloud. Убедитесь, что биллинг включен.")
elif "Could not find model" in error_message:
raise ValueError(f"Модель 'gemini-1.5-flash-latest' не найдена или недоступна в вашем регионе.")
elif "resource has been exhausted" in error_message.lower():
raise ValueError("Квота запросов исчерпана. Пожалуйста, попробуйте позже.")
elif "content has been blocked" in error_message.lower() or (hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason):
reason = response.prompt_feedback.block_reason if hasattr(response, 'prompt_feedback') else "неизвестна"
raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другое изображение или запрос.")
else:
raise ValueError(f"Ошибка при генерации контента: {e}")
@app.route('/')
def index():
return Response(html_template, mimetype='text/html')
@app.route('/generate', methods=['POST'])
def handle_generate():
if 'image' not in request.files:
return jsonify({"error": "Изображение не найдено в запросе."}), 400
if 'language' not in request.form:
return jsonify({"error": "Язык не найден в запросе."}), 400
image_file = request.files['image']
language = request.form['language']
image_data = image_file.read()
if not image_data:
return jsonify({"error": "Загруженный файл изображения пуст."}), 400
try:
result_text = generate_ai_content(image_data, language)
return jsonify({"text": result_text})
except ValueError as ve:
return jsonify({"error": str(ve)}), 400
except Exception as e:
return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860, debug=False)