Aleksmorshen commited on
Commit
9db5a7f
·
verified ·
1 Parent(s): d9130cb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +272 -167
app.py CHANGED
@@ -8,7 +8,10 @@ import numpy as np
8
 
9
  app = Flask(__name__)
10
 
11
- API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Hardcoded API Key
 
 
 
12
 
13
  html_template = """
14
  <!DOCTYPE html>
@@ -17,41 +20,62 @@ html_template = """
17
  <meta charset="UTF-8">
18
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
19
  <title>EVA - Генератор постов</title>
 
 
 
20
  <style>
21
  :root {
22
- --system-gray-100-light: #f2f2f7;
23
- --system-gray-75-light: #f8f8fa;
24
- --system-gray-50-light: #ffffff;
25
- --system-gray-dark-100-light: #000000;
26
- --system-gray-dark-75-light: #1c1c1e;
27
- --system-gray-dark-50-light: #3a3a3c;
28
- --system-gray-light-75-light: #8e8e93;
29
- --system-gray-light-50-light: #aeaeb2;
30
- --system-blue-light: #007aff;
31
- --system-blue-light-hover: #005ecf;
32
- --system-red-light: #ff3b30;
33
- --system-separator-light: rgba(60, 60, 67, 0.29);
34
- --system-separator-opaque-light: #d1d1d6;
35
-
36
- --system-gray-100-dark: #1c1c1e;
37
- --system-gray-75-dark: #2c2c2e;
38
- --system-gray-50-dark: #000000;
39
- --system-gray-dark-100-dark: #ffffff;
40
- --system-gray-dark-75-dark: #f2f2f7;
41
- --system-gray-dark-50-dark: #e5e5ea;
42
- --system-gray-light-75-dark: #8e8e93;
43
- --system-gray-light-50-dark: #636366;
44
- --system-blue-dark: #0a84ff;
45
- --system-blue-dark-hover: #3b9eff;
46
- --system-red-dark: #ff453a;
47
- --system-separator-dark: rgba(84, 84, 88, 0.65);
48
- --system-separator-opaque-dark: #38383a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
  @media (prefers-color-scheme: dark) {
52
  :root {
53
- --bg-color: var(--system-gray-50-dark);
54
- --content-bg: var(--system-gray-100-dark);
55
  --text-color: var(--system-gray-dark-100-dark);
56
  --secondary-text-color: var(--system-gray-light-75-dark);
57
  --tertiary-text-color: var(--system-gray-light-50-dark);
@@ -61,33 +85,21 @@ html_template = """
61
  --primary-color: var(--system-blue-dark);
62
  --primary-color-hover: var(--system-blue-dark-hover);
63
  --error-color: var(--system-red-dark);
64
- }
65
- }
66
-
67
- @media (prefers-color-scheme: light) {
68
- :root {
69
- --bg-color: var(--system-gray-100-light);
70
- --content-bg: var(--system-gray-50-light);
71
- --text-color: var(--system-gray-dark-100-light);
72
- --secondary-text-color: var(--system-gray-light-75-light);
73
- --tertiary-text-color: var(--system-gray-light-50-light);
74
- --border-color: var(--system-separator-light);
75
- --border-color-opaque: var(--system-separator-opaque-light);
76
- --input-bg: var(--system-gray-75-light);
77
- --primary-color: var(--system-blue-light);
78
- --primary-color-hover: var(--system-blue-light-hover);
79
- --error-color: var(--system-red-light);
80
  }
81
  }
82
 
83
  html {
84
- height: -webkit-fill-available;
85
  }
86
 
87
  body {
88
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
89
  margin: 0;
90
- padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom);
 
 
 
91
  background-color: var(--bg-color);
92
  color: var(--text-color);
93
  display: flex;
@@ -98,100 +110,114 @@ html_template = """
98
  line-height: 1.45;
99
  -webkit-font-smoothing: antialiased;
100
  -moz-osx-font-smoothing: grayscale;
 
 
101
  }
102
 
103
  .container {
104
  background-color: var(--content-bg);
105
- padding: 25px 30px 30px 30px;
106
- border-radius: 24px;
107
- box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
108
- max-width: 580px;
109
- width: calc(100% - 40px);
110
  box-sizing: border-box;
111
  margin-top: 30px;
 
112
  }
113
 
114
  h1 {
115
- font-size: 32px;
116
  font-weight: 700;
117
  text-align: center;
118
- margin-bottom: 8px;
119
  color: var(--text-color);
120
- letter-spacing: -0.5px;
121
  }
122
 
123
  p.subtitle {
124
- font-size: 17px;
125
  color: var(--secondary-text-color);
126
  text-align: center;
127
- margin-bottom: 35px;
128
  font-weight: 400;
129
  }
130
 
131
  .form-group {
132
- margin-bottom: 28px;
133
  }
134
 
135
  label.input-label {
136
  display: block;
137
  font-weight: 500;
138
- margin-bottom: 10px;
139
- font-size: 15px;
140
  color: var(--secondary-text-color);
141
  padding-left: 5px;
142
  }
143
 
144
  input[type="file"] {
 
145
  width: 100%;
146
- padding: 14px 18px;
147
  border: 1px solid var(--border-color-opaque);
148
- border-radius: 12px;
149
- font-size: 16px;
150
  background-color: var(--input-bg);
151
  color: var(--text-color);
152
  box-sizing: border-box;
153
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
154
  cursor: pointer;
155
  font-family: inherit;
 
156
  }
157
 
158
  input[type="file"]:focus {
159
  border-color: var(--primary-color);
160
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
161
  outline: none;
162
  }
163
 
164
  input[type="file"]::file-selector-button {
165
- font-weight: 500;
166
  color: var(--primary-color);
167
  background-color: transparent;
168
  border: none;
169
  padding: 0;
170
- margin-right: 12px;
171
  cursor: pointer;
172
- font-size: 16px;
 
 
 
 
 
173
  }
174
 
175
  .language-options {
176
  display: flex;
177
  flex-wrap: wrap;
178
- gap: 10px;
179
  margin-top: 5px;
 
180
  }
181
 
182
  .language-options label {
183
  display: flex;
184
  align-items: center;
185
  background-color: var(--input-bg);
186
- padding: 10px 18px;
187
- border-radius: 25px;
188
  cursor: pointer;
189
- transition: background-color 0.2s ease, color 0.2s ease, transform 0.1s ease;
190
  font-weight: 500;
191
- font-size: 14px;
192
  color: var(--tertiary-text-color);
193
- border: 1px solid transparent;
194
  user-select: none;
 
 
 
195
  }
196
 
197
  .language-options input[type="radio"] {
@@ -202,63 +228,68 @@ html_template = """
202
  background-color: var(--primary-color);
203
  color: white;
204
  font-weight: 600;
205
- border-color: transparent;
206
  }
207
 
208
- .language-options label:hover {
209
  background-color: color-mix(in srgb, var(--input-bg) 85%, var(--secondary-text-color));
 
210
  }
211
 
212
  .language-options input[type="radio"]:checked + label:hover {
213
  background-color: var(--primary-color-hover);
 
214
  }
215
 
216
  .language-options label:active {
217
- transform: scale(0.96);
218
  }
219
 
220
-
221
  button#generate-button {
222
  width: 100%;
223
- padding: 16px;
224
  background-color: var(--primary-color);
225
  color: white;
226
  border: none;
227
- border-radius: 12px;
228
- font-size: 17px;
229
  font-weight: 600;
230
  cursor: pointer;
231
- transition: background-color 0.2s ease, transform 0.1s ease;
232
- margin-top: 15px;
 
233
  }
234
 
235
  button#generate-button:hover {
236
  background-color: var(--primary-color-hover);
 
237
  }
238
 
239
  button#generate-button:active {
240
  transform: scale(0.98);
 
241
  }
242
 
243
  button#generate-button:disabled {
244
  background-color: var(--tertiary-text-color);
245
  cursor: not-allowed;
 
246
  }
247
 
248
  .output-section {
249
- margin-top: 35px;
250
  }
251
 
252
  .output-header {
253
  display: flex;
254
  justify-content: space-between;
255
  align-items: center;
256
- margin-bottom: 10px;
257
  }
258
 
259
  label#output-label {
260
  font-weight: 500;
261
- font-size: 15px;
262
  color: var(--secondary-text-color);
263
  padding-left: 5px;
264
  }
@@ -267,55 +298,61 @@ html_template = """
267
  background-color: transparent;
268
  border: none;
269
  color: var(--primary-color);
270
- font-size: 14px;
271
  font-weight: 500;
272
  cursor: pointer;
273
- padding: 5px 8px;
274
- border-radius: 6px;
275
- transition: background-color 0.2s ease, color 0.2s ease;
276
  display: none;
 
277
  }
278
 
279
  button#copy-button:hover {
280
  background-color: color-mix(in srgb, var(--primary-color) 15%, transparent);
281
  }
282
-
283
  button#copy-button:active {
284
  background-color: color-mix(in srgb, var(--primary-color) 25%, transparent);
 
285
  }
286
-
287
  button#copy-button.copied {
288
- color: #34c759; /* System Green */
 
289
  }
290
- @media (prefers-color-scheme: dark) {
291
- button#copy-button.copied {
292
- color: #30d158; /* System Green Dark */
293
- }
294
  }
295
 
296
 
297
  #output-container {
298
  background-color: var(--input-bg);
299
- padding: 18px 20px;
300
- border-radius: 12px;
301
- min-height: 120px;
302
  border: 1px solid var(--border-color);
303
  white-space: pre-wrap;
304
  word-wrap: break-word;
305
- font-size: 15px;
306
  color: var(--text-color);
307
- line-height: 1.5;
308
- transition: border-color 0.2s ease, background-color 0.2s ease;
 
309
  }
310
 
311
- #output-container.loading::before {
312
- content: "Генерация...";
313
- display: block;
 
314
  text-align: center;
315
  font-style: italic;
316
  color: var(--secondary-text-color);
317
  animation: fadePulse 1.8s infinite ease-in-out;
318
- padding-top: 30px;
 
 
 
319
  }
320
 
321
  #output-container.error {
@@ -323,21 +360,26 @@ html_template = """
323
  font-weight: 500;
324
  border-color: color-mix(in srgb, var(--error-color) 50%, transparent);
325
  background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg));
 
326
  }
327
 
328
  #image-preview-container {
329
- margin-top: 15px;
330
  text-align: center;
 
331
  }
332
 
333
  #image-preview {
334
  max-width: 100%;
335
- max-height: 220px;
336
- border-radius: 12px;
337
  margin-top: 10px;
338
  border: 1px solid var(--border-color-opaque);
339
  display: none;
340
- background-color: var(--input-bg); /* Placeholder bg */
 
 
 
341
  }
342
 
343
  @keyframes fadePulse {
@@ -345,43 +387,105 @@ html_template = """
345
  50% { opacity: 1; }
346
  }
347
 
348
- @media (max-width: 620px) {
349
  body {
350
- padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom);
351
- align-items: flex-start;
 
 
352
  }
353
  .container {
354
- padding: 20px 20px 25px 20px;
355
  margin-top: 15px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  border-radius: 20px;
357
- width: calc(100% - 30px);
358
  }
359
  h1 {
360
  font-size: 28px;
361
  }
362
- p.subtitle {
363
  font-size: 16px;
364
  margin-bottom: 25px;
365
  }
366
- .form-group {
367
- margin-bottom: 22px;
 
368
  }
369
  input[type="file"] {
370
  padding: 12px 15px;
 
371
  }
372
  input[type="file"]::file-selector-button {
373
- font-size: 15px;
374
- margin-right: 10px;
375
  }
376
- .language-options {
377
- gap: 8px;
378
- justify-content: space-evenly;
379
- }
380
- .language-options label {
381
- padding: 9px 16px;
382
- font-size: 13px;
383
- }
384
- button#generate-button {
385
  padding: 15px;
386
  font-size: 16px;
387
  }
@@ -389,12 +493,11 @@ html_template = """
389
  padding: 15px 18px;
390
  font-size: 14px;
391
  min-height: 100px;
392
- }
393
- .output-section {
394
  margin-top: 30px;
395
  }
396
  }
397
-
398
  </style>
399
  </head>
400
  <body>
@@ -423,7 +526,7 @@ html_template = """
423
 
424
  <div class="form-group">
425
  <label for="image-upload" class="input-label">Загрузить изображение</label>
426
- <input type="file" id="image-upload" name="image" accept="image/*" required>
427
  <div id="image-preview-container">
428
  <img id="image-preview" src="#" alt="Предпросмотр изображения"/>
429
  </div>
@@ -453,13 +556,20 @@ html_template = """
453
 
454
  imageInput.addEventListener('change', function(event) {
455
  const file = event.target.files[0];
456
- if (file && file.type.startsWith('image/')) {
457
- const reader = new FileReader();
458
- reader.onload = function(e) {
459
- imagePreview.src = e.target.result;
460
- imagePreview.style.display = 'block';
 
 
 
 
 
 
 
 
461
  }
462
- reader.readAsDataURL(file);
463
  } else {
464
  imagePreview.src = '#';
465
  imagePreview.style.display = 'none';
@@ -505,7 +615,6 @@ html_template = """
505
  copyButton.style.display = 'none';
506
  }
507
 
508
-
509
  } catch (error) {
510
  console.error("Fetch Error:", error);
511
  showError(`Ошибка: ${error.message}`);
@@ -519,7 +628,7 @@ html_template = """
519
 
520
  copyButton.addEventListener('click', () => {
521
  const textToCopy = outputContainer.textContent;
522
- if (!textToCopy) return;
523
 
524
  navigator.clipboard.writeText(textToCopy).then(() => {
525
  copyButton.textContent = 'Скопировано!';
@@ -553,18 +662,15 @@ def generate_ai_content(image_data, language):
553
  try:
554
  genai.configure(api_key=API_KEY_INTERNAL)
555
  except Exception as e:
556
- print(f"Error configuring GenAI: {e}")
557
- raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
558
 
559
- try:
560
- if not image_data:
561
- raise ValueError("Файл изображения не найден.")
562
 
 
563
  image_stream = io.BytesIO(image_data)
564
  image = Image.open(image_stream).convert('RGB')
565
-
566
  except Exception as e:
567
- print(f"Error processing image: {e}")
568
  raise ValueError(f"Не удалось обработать изображение. Убедитесь, что это действительный файл изображения. Ошибка: {e}")
569
 
570
  base_prompt = "Напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами и 25 тематических хэштегов с ключевыми словами разных вариантов, чтобы мои клиенты могли найти меня в поиске Instagram, Google и т.д. по ключевым словам. Пост пиши исключительно под товар, который на фото, без адресов и номеров телефона."
@@ -578,35 +684,35 @@ def generate_ai_content(image_data, language):
578
  lang_suffix = " Пиши на казахском языке."
579
  elif language == "Узбекский":
580
  lang_suffix = " Пиши на узбекском языке."
 
 
581
 
582
  final_prompt = f"{base_prompt}{lang_suffix}"
583
 
584
  try:
585
- model = genai.GenerativeModel('gemini-2.0-flash-lite')
586
-
587
  response = model.generate_content([final_prompt, image])
588
 
589
  if hasattr(response, 'text'):
590
  return response.text
591
  else:
 
592
  if response.parts:
593
  return "".join(part.text for part in response.parts if hasattr(part, 'text'))
594
  else:
595
- print("Warning: Unexpected response structure:", response)
596
- response.resolve()
597
- return response.text
598
 
599
  except Exception as e:
600
- print(f"Error generating content with GenAI: {e}")
601
- if "API key not valid" in str(e):
602
- raise ValueError("Внутренняя ошибка конфигурации API.")
603
- elif " Billing account not found" in str(e):
604
- raise ValueError("Проблема с биллингом аккаунта Google Cloud.")
605
- elif "Could not find model" in str(e):
606
- raise ValueError(f"Модель 'gemini-1.5-flash' не найдена или недоступна.")
607
- elif "resource has been exhausted" in str(e).lower():
608
- raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
609
- elif "content has been blocked" in str(e).lower() or (hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason):
610
  reason = response.prompt_feedback.block_reason if hasattr(response, 'prompt_feedback') else "неизвестна"
611
  raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другое изображение или запрос.")
612
  else:
@@ -638,7 +744,6 @@ def handle_generate():
638
  except ValueError as ve:
639
  return jsonify({"error": str(ve)}), 400
640
  except Exception as e:
641
- print(f"Unexpected error during generation: {e}")
642
  return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
643
 
644
 
 
8
 
9
  app = Flask(__name__)
10
 
11
+ # It is highly recommended to load API keys from environment variables
12
+ # instead of hardcoding them for security and flexibility.
13
+ # Example: API_KEY_INTERNAL = os.environ.get("GEMINI_API_KEY")
14
+ API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Replace with your actual Google Gemini API Key
15
 
16
  html_template = """
17
  <!DOCTYPE html>
 
20
  <meta charset="UTF-8">
21
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
22
  <title>EVA - Генератор постов</title>
23
+ <link rel="preconnect" href="https://fonts.googleapis.com">
24
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
25
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
26
  <style>
27
  :root {
28
+ /* Light Theme System Colors */
29
+ --system-gray-100-light: #F2F2F7; /* Background */
30
+ --system-gray-75-light: #F8F8FA; /* Input/Card background slightly lighter */
31
+ --system-gray-50-light: #FFFFFF; /* Content Card background */
32
+ --system-gray-dark-100-light: #000000; /* Primary Text */
33
+ --system-gray-dark-75-light: #1C1C1E; /* Secondary Text (darker) */
34
+ --system-gray-dark-50-light: #3A3A3C; /* Tertiary Text (darker) */
35
+ --system-gray-light-75-light: #8E8E93; /* Secondary Text (lighter) */
36
+ --system-gray-light-50-light: #AEAEB2; /* Tertiary Text (lighter) */
37
+ --system-blue-light: #007AFF; /* Primary Accent */
38
+ --system-blue-light-hover: #005ECF; /* Primary Accent Hover */
39
+ --system-red-light: #FF3B30; /* Error */
40
+ --system-green-light: #34C759; /* Success */
41
+ --system-separator-light: rgba(60, 60, 67, 0.29); /* Subtle Separator */
42
+ --system-separator-opaque-light: #D1D1D6; /* Opaque Separator */
43
+
44
+ /* Dark Theme System Colors */
45
+ --system-gray-100-dark: #1C1C1E; /* Background */
46
+ --system-gray-75-dark: #2C2C2E; /* Input/Card background slightly lighter */
47
+ --system-gray-50-dark: #000000; /* Content Card background (pure black for OLED) */
48
+ --system-gray-dark-100-dark: #FFFFFF; /* Primary Text */
49
+ --system-gray-dark-75-dark: #F2F2F7; /* Secondary Text (lighter) */
50
+ --system-gray-dark-50-dark: #E5E5EA; /* Tertiary Text (lighter) */
51
+ --system-gray-light-75-dark: #8E8E93; /* Secondary Text (darker) */
52
+ --system-gray-light-50-dark: #636366; /* Tertiary Text (darker) */
53
+ --system-blue-dark: #0A84FF; /* Primary Accent */
54
+ --system-blue-dark-hover: #3B9EFF; /* Primary Accent Hover */
55
+ --system-red-dark: #FF453A; /* Error */
56
+ --system-green-dark: #30D158; /* Success */
57
+ --system-separator-dark: rgba(84, 84, 88, 0.65); /* Subtle Separator */
58
+ --system-separator-opaque-dark: #38383A; /* Opaque Separator */
59
+
60
+ /* Semantic Colors (Resolved by prefers-color-scheme) */
61
+ --bg-color: var(--system-gray-100-light);
62
+ --content-bg: var(--system-gray-50-light);
63
+ --text-color: var(--system-gray-dark-100-light);
64
+ --secondary-text-color: var(--system-gray-light-75-light);
65
+ --tertiary-text-color: var(--system-gray-light-50-light);
66
+ --border-color: var(--system-separator-light);
67
+ --border-color-opaque: var(--system-separator-opaque-light);
68
+ --input-bg: var(--system-gray-75-light);
69
+ --primary-color: var(--system-blue-light);
70
+ --primary-color-hover: var(--system-blue-light-hover);
71
+ --error-color: var(--system-red-light);
72
+ --success-color: var(--system-green-light);
73
  }
74
 
75
  @media (prefers-color-scheme: dark) {
76
  :root {
77
+ --bg-color: var(--system-gray-100-dark);
78
+ --content-bg: var(--system-gray-50-dark);
79
  --text-color: var(--system-gray-dark-100-dark);
80
  --secondary-text-color: var(--system-gray-light-75-dark);
81
  --tertiary-text-color: var(--system-gray-light-50-dark);
 
85
  --primary-color: var(--system-blue-dark);
86
  --primary-color-hover: var(--system-blue-dark-hover);
87
  --error-color: var(--system-red-dark);
88
+ --success-color: var(--system-green-dark);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
  }
91
 
92
  html {
93
+ height: -webkit-fill-available;
94
  }
95
 
96
  body {
97
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
98
  margin: 0;
99
+ padding-top: 20px;
100
+ padding-bottom: 20px;
101
+ padding-left: max(20px, env(safe-area-inset-left));
102
+ padding-right: max(20px, env(safe-area-inset-right));
103
  background-color: var(--bg-color);
104
  color: var(--text-color);
105
  display: flex;
 
110
  line-height: 1.45;
111
  -webkit-font-smoothing: antialiased;
112
  -moz-osx-font-smoothing: grayscale;
113
+ box-sizing: border-box;
114
+ transition: background-color 0.3s ease;
115
  }
116
 
117
  .container {
118
  background-color: var(--content-bg);
119
+ padding: 30px 30px 35px 30px;
120
+ border-radius: 28px;
121
+ box-shadow: 0 16px 32px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0,0,0,0.08);
122
+ max-width: 600px;
123
+ width: 100%;
124
  box-sizing: border-box;
125
  margin-top: 30px;
126
+ transition: background-color 0.3s ease, box-shadow 0.3s ease;
127
  }
128
 
129
  h1 {
130
+ font-size: 34px;
131
  font-weight: 700;
132
  text-align: center;
133
+ margin-bottom: 6px;
134
  color: var(--text-color);
135
+ letter-spacing: -0.6px;
136
  }
137
 
138
  p.subtitle {
139
+ font-size: 18px;
140
  color: var(--secondary-text-color);
141
  text-align: center;
142
+ margin-bottom: 40px;
143
  font-weight: 400;
144
  }
145
 
146
  .form-group {
147
+ margin-bottom: 30px;
148
  }
149
 
150
  label.input-label {
151
  display: block;
152
  font-weight: 500;
153
+ margin-bottom: 12px;
154
+ font-size: 16px;
155
  color: var(--secondary-text-color);
156
  padding-left: 5px;
157
  }
158
 
159
  input[type="file"] {
160
+ display: block;
161
  width: 100%;
162
+ padding: 16px 20px;
163
  border: 1px solid var(--border-color-opaque);
164
+ border-radius: 14px;
165
+ font-size: 17px;
166
  background-color: var(--input-bg);
167
  color: var(--text-color);
168
  box-sizing: border-box;
169
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
170
  cursor: pointer;
171
  font-family: inherit;
172
+ line-height: 1;
173
  }
174
 
175
  input[type="file"]:focus {
176
  border-color: var(--primary-color);
177
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary-color) 25%, transparent);
178
  outline: none;
179
  }
180
 
181
  input[type="file"]::file-selector-button {
182
+ font-weight: 600;
183
  color: var(--primary-color);
184
  background-color: transparent;
185
  border: none;
186
  padding: 0;
187
+ margin-right: 15px;
188
  cursor: pointer;
189
+ font-size: 17px;
190
+ transition: color 0.2s ease;
191
+ }
192
+
193
+ input[type="file"]:hover::file-selector-button {
194
+ color: var(--primary-color-hover);
195
  }
196
 
197
  .language-options {
198
  display: flex;
199
  flex-wrap: wrap;
200
+ gap: 12px;
201
  margin-top: 5px;
202
+ justify-content: center; /* Center options horizontally */
203
  }
204
 
205
  .language-options label {
206
  display: flex;
207
  align-items: center;
208
  background-color: var(--input-bg);
209
+ padding: 12px 22px;
210
+ border-radius: 30px;
211
  cursor: pointer;
212
+ transition: background-color 0.2s ease, color 0.2s ease, transform 0.1s ease, border-color 0.2s ease;
213
  font-weight: 500;
214
+ font-size: 15px;
215
  color: var(--tertiary-text-color);
216
+ border: 1px solid var(--border-color); /* Subtle border */
217
  user-select: none;
218
+ min-width: 90px;
219
+ text-align: center;
220
+ justify-content: center;
221
  }
222
 
223
  .language-options input[type="radio"] {
 
228
  background-color: var(--primary-color);
229
  color: white;
230
  font-weight: 600;
231
+ border-color: var(--primary-color);
232
  }
233
 
234
+ .language-options label:hover:not(:checked) {
235
  background-color: color-mix(in srgb, var(--input-bg) 85%, var(--secondary-text-color));
236
+ border-color: color-mix(in srgb, var(--border-color-opaque) 70%, var(--primary-color));
237
  }
238
 
239
  .language-options input[type="radio"]:checked + label:hover {
240
  background-color: var(--primary-color-hover);
241
+ border-color: var(--primary-color-hover);
242
  }
243
 
244
  .language-options label:active {
245
+ transform: scale(0.97);
246
  }
247
 
 
248
  button#generate-button {
249
  width: 100%;
250
+ padding: 18px;
251
  background-color: var(--primary-color);
252
  color: white;
253
  border: none;
254
+ border-radius: 14px;
255
+ font-size: 18px;
256
  font-weight: 600;
257
  cursor: pointer;
258
+ transition: background-color 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
259
+ margin-top: 20px;
260
+ box-shadow: 0 4px 10px rgba(0, 122, 255, 0.2); /* Soft shadow for button */
261
  }
262
 
263
  button#generate-button:hover {
264
  background-color: var(--primary-color-hover);
265
+ box-shadow: 0 6px 15px rgba(0, 122, 255, 0.3);
266
  }
267
 
268
  button#generate-button:active {
269
  transform: scale(0.98);
270
+ box-shadow: 0 2px 5px rgba(0, 122, 255, 0.2);
271
  }
272
 
273
  button#generate-button:disabled {
274
  background-color: var(--tertiary-text-color);
275
  cursor: not-allowed;
276
+ box-shadow: none;
277
  }
278
 
279
  .output-section {
280
+ margin-top: 40px;
281
  }
282
 
283
  .output-header {
284
  display: flex;
285
  justify-content: space-between;
286
  align-items: center;
287
+ margin-bottom: 12px;
288
  }
289
 
290
  label#output-label {
291
  font-weight: 500;
292
+ font-size: 16px;
293
  color: var(--secondary-text-color);
294
  padding-left: 5px;
295
  }
 
298
  background-color: transparent;
299
  border: none;
300
  color: var(--primary-color);
301
+ font-size: 15px;
302
  font-weight: 500;
303
  cursor: pointer;
304
+ padding: 8px 12px;
305
+ border-radius: 8px;
306
+ transition: background-color 0.2s ease, color 0.2s ease, transform 0.1s ease;
307
  display: none;
308
+ text-wrap: nowrap;
309
  }
310
 
311
  button#copy-button:hover {
312
  background-color: color-mix(in srgb, var(--primary-color) 15%, transparent);
313
  }
314
+
315
  button#copy-button:active {
316
  background-color: color-mix(in srgb, var(--primary-color) 25%, transparent);
317
+ transform: scale(0.98);
318
  }
319
+
320
  button#copy-button.copied {
321
+ color: var(--success-color);
322
+ background-color: color-mix(in srgb, var(--success-color) 15%, transparent);
323
  }
324
+ button#copy-button.copied:hover {
325
+ background-color: color-mix(in srgb, var(--success-color) 25%, transparent);
 
 
326
  }
327
 
328
 
329
  #output-container {
330
  background-color: var(--input-bg);
331
+ padding: 20px 22px;
332
+ border-radius: 14px;
333
+ min-height: 150px;
334
  border: 1px solid var(--border-color);
335
  white-space: pre-wrap;
336
  word-wrap: break-word;
337
+ font-size: 16px;
338
  color: var(--text-color);
339
+ line-height: 1.6;
340
+ transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
341
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); /* Subtle inner shadow */
342
  }
343
 
344
+ #output-container.loading {
345
+ display: flex;
346
+ justify-content: center;
347
+ align-items: center;
348
  text-align: center;
349
  font-style: italic;
350
  color: var(--secondary-text-color);
351
  animation: fadePulse 1.8s infinite ease-in-out;
352
+ }
353
+ #output-container.loading::before {
354
+ content: "Генерация...";
355
+ display: block;
356
  }
357
 
358
  #output-container.error {
 
360
  font-weight: 500;
361
  border-color: color-mix(in srgb, var(--error-color) 50%, transparent);
362
  background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg));
363
+ box-shadow: inset 0 1px 3px rgba(255, 0, 0, 0.1);
364
  }
365
 
366
  #image-preview-container {
367
+ margin-top: 20px;
368
  text-align: center;
369
+ padding: 10px;
370
  }
371
 
372
  #image-preview {
373
  max-width: 100%;
374
+ max-height: 250px;
375
+ border-radius: 16px;
376
  margin-top: 10px;
377
  border: 1px solid var(--border-color-opaque);
378
  display: none;
379
+ background-color: var(--input-bg);
380
+ object-fit: contain;
381
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
382
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
383
  }
384
 
385
  @keyframes fadePulse {
 
387
  50% { opacity: 1; }
388
  }
389
 
390
+ @media (max-width: 680px) {
391
  body {
392
+ padding-top: 15px;
393
+ padding-bottom: 15px;
394
+ padding-left: max(15px, env(safe-area-inset-left));
395
+ padding-right: max(15px, env(safe-area-inset-right));
396
  }
397
  .container {
398
+ padding: 25px 20px 30px 20px;
399
  margin-top: 15px;
400
+ border-radius: 24px;
401
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
402
+ }
403
+ h1 {
404
+ font-size: 30px;
405
+ }
406
+ p.subtitle {
407
+ font-size: 17px;
408
+ margin-bottom: 30px;
409
+ }
410
+ .form-group {
411
+ margin-bottom: 25px;
412
+ }
413
+ label.input-label {
414
+ font-size: 15px;
415
+ margin-bottom: 10px;
416
+ }
417
+ input[type="file"] {
418
+ padding: 14px 18px;
419
+ font-size: 16px;
420
+ border-radius: 12px;
421
+ }
422
+ input[type="file"]::file-selector-button {
423
+ font-size: 16px;
424
+ margin-right: 12px;
425
+ }
426
+ .language-options {
427
+ gap: 10px;
428
+ justify-content: space-between;
429
+ }
430
+ .language-options label {
431
+ padding: 10px 18px;
432
+ font-size: 14px;
433
+ min-width: unset;
434
+ flex-grow: 1;
435
+ }
436
+ button#generate-button {
437
+ padding: 16px;
438
+ font-size: 17px;
439
+ border-radius: 12px;
440
+ margin-top: 18px;
441
+ }
442
+ .output-section {
443
+ margin-top: 35px;
444
+ }
445
+ #output-container {
446
+ padding: 18px 20px;
447
+ font-size: 15px;
448
+ min-height: 120px;
449
+ border-radius: 12px;
450
+ }
451
+ label#output-label {
452
+ font-size: 15px;
453
+ }
454
+ button#copy-button {
455
+ font-size: 14px;
456
+ padding: 6px 10px;
457
+ }
458
+ #image-preview {
459
+ max-height: 200px;
460
+ border-radius: 14px;
461
+ }
462
+ }
463
+
464
+ @media (max-width: 480px) {
465
+ .container {
466
+ padding: 20px 15px 25px 15px;
467
  border-radius: 20px;
 
468
  }
469
  h1 {
470
  font-size: 28px;
471
  }
472
+ p.subtitle {
473
  font-size: 16px;
474
  margin-bottom: 25px;
475
  }
476
+ .language-options label {
477
+ font-size: 13px;
478
+ padding: 9px 15px;
479
  }
480
  input[type="file"] {
481
  padding: 12px 15px;
482
+ font-size: 15px;
483
  }
484
  input[type="file"]::file-selector-button {
485
+ font-size: 15px;
486
+ margin-right: 10px;
487
  }
488
+ button#generate-button {
 
 
 
 
 
 
 
 
489
  padding: 15px;
490
  font-size: 16px;
491
  }
 
493
  padding: 15px 18px;
494
  font-size: 14px;
495
  min-height: 100px;
496
+ }
497
+ .output-section {
498
  margin-top: 30px;
499
  }
500
  }
 
501
  </style>
502
  </head>
503
  <body>
 
526
 
527
  <div class="form-group">
528
  <label for="image-upload" class="input-label">Загрузить изображение</label>
529
+ <input type="file" id="image-upload" name="image" accept="image/png, image/jpeg, image/webp" required>
530
  <div id="image-preview-container">
531
  <img id="image-preview" src="#" alt="Предпросмотр изображения"/>
532
  </div>
 
556
 
557
  imageInput.addEventListener('change', function(event) {
558
  const file = event.target.files[0];
559
+ if (file) {
560
+ if (file.type.startsWith('image/')) {
561
+ const reader = new FileReader();
562
+ reader.onload = function(e) {
563
+ imagePreview.src = e.target.result;
564
+ imagePreview.style.display = 'block';
565
+ }
566
+ reader.readAsDataURL(file);
567
+ } else {
568
+ showError("Выбранный файл не является изображением. Пожалуйста, загрузите файл формата PNG, JPG или WEBP.");
569
+ imagePreview.src = '#';
570
+ imagePreview.style.display = 'none';
571
+ imageInput.value = ''; // Clear the input
572
  }
 
573
  } else {
574
  imagePreview.src = '#';
575
  imagePreview.style.display = 'none';
 
615
  copyButton.style.display = 'none';
616
  }
617
 
 
618
  } catch (error) {
619
  console.error("Fetch Error:", error);
620
  showError(`Ошибка: ${error.message}`);
 
628
 
629
  copyButton.addEventListener('click', () => {
630
  const textToCopy = outputContainer.textContent;
631
+ if (!textToCopy || textToCopy.trim().length === 0) return;
632
 
633
  navigator.clipboard.writeText(textToCopy).then(() => {
634
  copyButton.textContent = 'Скопировано!';
 
662
  try:
663
  genai.configure(api_key=API_KEY_INTERNAL)
664
  except Exception as e:
665
+ raise ValueError(f"Не удалось настроить Google AI: {e}. Проверьте ключ API или подключение.")
 
666
 
667
+ if not image_data:
668
+ raise ValueError("Файл изображения пуст или не был передан.")
 
669
 
670
+ try:
671
  image_stream = io.BytesIO(image_data)
672
  image = Image.open(image_stream).convert('RGB')
 
673
  except Exception as e:
 
674
  raise ValueError(f"Не удалось обработать изображение. Убедитесь, что это действительный файл изображения. Ошибка: {e}")
675
 
676
  base_prompt = "Напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами и 25 тематических хэштегов с ключевыми словами разных вариантов, чтобы мои клиенты могли найти меня в поиске Instagram, Google и т.д. по ключевым словам. Пост пиши исключительно под товар, который на фото, без адресов и номеров телефона."
 
684
  lang_suffix = " Пиши на казахском языке."
685
  elif language == "Узбекский":
686
  lang_suffix = " Пиши на узбекском языке."
687
+ else:
688
+ raise ValueError("Выбран неподдерживаемый язык.")
689
 
690
  final_prompt = f"{base_prompt}{lang_suffix}"
691
 
692
  try:
693
+ model = genai.GenerativeModel('gemini-1.5-flash-latest') # Using the latest flash model
 
694
  response = model.generate_content([final_prompt, image])
695
 
696
  if hasattr(response, 'text'):
697
  return response.text
698
  else:
699
+ # Fallback for models that might return content in parts, though .text is standard
700
  if response.parts:
701
  return "".join(part.text for part in response.parts if hasattr(part, 'text'))
702
  else:
703
+ raise ValueError("Не удалось получить текст ответа от модели. Возможно, не было сгенерировано контента.")
 
 
704
 
705
  except Exception as e:
706
+ error_message = str(e)
707
+ if "API key not valid" in error_message:
708
+ raise ValueError("Внутренняя ошибка конфигурации API. Проверьте ваш ключ Google Gemini API.")
709
+ elif "Billing account not found" in error_message or "CREDENTIALS_LOCATION_MISSING" in error_message:
710
+ raise ValueError("Проблема с биллингом аккаунта Google Cloud. Убедитесь, что биллинг включен.")
711
+ elif "Could not find model" in error_message:
712
+ raise ValueError(f"Модель 'gemini-1.5-flash-latest' не найдена или недоступна в вашем регионе.")
713
+ elif "resource has been exhausted" in error_message.lower():
714
+ raise ValueError("Квота запросов исчерпана. Пожалуйста, попробуйте позже.")
715
+ elif "content has been blocked" in error_message.lower() or (hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason):
716
  reason = response.prompt_feedback.block_reason if hasattr(response, 'prompt_feedback') else "неизвестна"
717
  raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другое изображение или запрос.")
718
  else:
 
744
  except ValueError as ve:
745
  return jsonify({"error": str(ve)}), 400
746
  except Exception as e:
 
747
  return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
748
 
749