Kgshop commited on
Commit
ce8d15e
·
verified ·
1 Parent(s): 4269587

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -172
app.py CHANGED
@@ -166,7 +166,7 @@ ADMHOSTO_TEMPLATE = '''
166
  <meta charset="UTF-8">
167
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
168
  <title>Админ-панель</title>
169
- <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600&display=swap" rel="stylesheet">
170
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
171
  <style>
172
  :root {
@@ -177,114 +177,68 @@ ADMHOSTO_TEMPLATE = '''
177
  --text-dark: #333;
178
  --text-on-accent: #003C43;
179
  --danger: #E57373;
180
- --warning: #ffcc80;
 
 
 
181
  }
182
  * { box-sizing: border-box; }
183
  body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); margin: 0; padding: 15px; }
184
  .container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 3px 15px rgba(0,0,0,0.08); }
185
- h1 { font-weight: 600; color: var(--bg-medium); margin-bottom: 25px; text-align: center; font-size: 1.5rem; }
 
 
186
 
187
  .section { margin-bottom: 25px; }
188
 
189
- .add-env-form {
190
- display: flex;
191
- flex-direction: column;
192
- gap: 15px;
193
- background: #f8f9fa;
194
- padding: 15px;
195
- border-radius: 10px;
196
- border: 1px solid #e9ecef;
197
- }
198
 
199
  input[type="text"] {
200
- width: 100%;
201
- padding: 12px;
202
- border: 1px solid #ddd;
203
- border-radius: 8px;
204
- font-size: 1rem;
205
- font-family: inherit;
206
- background: #fff;
207
- -webkit-appearance: none;
208
  }
209
 
210
- .controls-row {
211
- display: flex;
212
- align-items: center;
213
- justify-content: space-between;
214
- gap: 15px;
215
- flex-wrap: wrap;
216
- }
217
-
218
  .radio-group { display: flex; gap: 15px; }
219
  .radio-group label { cursor: pointer; display: flex; align-items: center; gap: 6px; font-weight: 500; font-size: 0.95rem; }
220
 
221
  .button {
222
- padding: 12px 20px;
223
- border: none;
224
- border-radius: 8px;
225
- background-color: var(--accent);
226
- color: var(--text-on-accent);
227
- font-weight: 600;
228
- cursor: pointer;
229
- text-decoration: none;
230
- display: inline-flex;
231
- align-items: center;
232
- justify-content: center;
233
- gap: 8px;
234
- font-size: 1rem;
235
- transition: opacity 0.2s;
236
  }
237
- .button:hover { opacity: 0.9; }
238
  .button:active { transform: scale(0.98); }
239
 
 
 
 
 
 
 
240
  .env-list { list-style: none; padding: 0; margin: 0; }
241
  .env-item {
242
- background: #fff;
243
- border: 1px solid #e0e0e0;
244
- border-radius: 10px;
245
- padding: 15px;
246
- margin-bottom: 12px;
247
- display: grid;
248
- grid-template-columns: 1fr auto;
249
- align-items: center;
250
- gap: 15px;
251
- box-shadow: 0 2px 5px rgba(0,0,0,0.02);
252
  }
253
-
 
254
  .env-details { display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
255
  .env-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
256
  .env-id { font-weight: 700; color: var(--bg-medium); font-size: 1.1rem; }
257
  .env-keyword { font-style: italic; color: #666; font-size: 0.9rem;}
258
 
259
- .env-link {
260
- font-size: 0.9rem;
261
- color: #007bff;
262
- word-break: break-all;
263
- text-decoration: none;
264
- padding: 5px 0;
265
- display: block;
266
- }
267
 
268
- .env-type-badge {
269
- font-size: 0.75rem;
270
- padding: 3px 8px;
271
- border-radius: 20px;
272
- font-weight: bold;
273
- text-transform: uppercase;
274
- white-space: nowrap;
275
- }
276
  .type-open { background-color: #d4edda; color: #155724; }
277
  .type-closed { background-color: #f8d7da; color: #721c24; }
278
 
279
- .env-actions { display: flex; gap: 8px; }
280
- .stats-button { background-color: #5d4037; color: white; padding: 10px 15px; font-size: 0.9rem;}
281
- .delete-button { background-color: var(--danger); color: white; padding: 10px 15px; font-size: 0.9rem;}
282
 
283
  .message { padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center; font-size: 0.95rem; }
284
  .message.success { background-color: #d4edda; color: #155724; }
285
  .message.error { background-color: #f8d7da; color: #721c24; }
286
 
287
- /* Modal */
288
  .modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }
289
  .modal-content { background-color: #fff; margin: 15% auto; padding: 25px; width: 90%; max-width: 600px; border-radius: 12px; position: relative; box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
290
  .close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
@@ -292,47 +246,25 @@ ADMHOSTO_TEMPLATE = '''
292
  .stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
293
  .stats-table th { background-color: var(--bg-medium); color: white; }
294
  .stats-table tr:nth-child(even) { background-color: #f9f9f9; }
 
 
 
295
 
296
- /* Mobile Optimization */
 
 
 
 
 
297
  @media (max-width: 600px) {
298
  body { padding: 10px; }
299
  .container { padding: 15px; }
300
  h1 { font-size: 1.3rem; margin-bottom: 20px; }
301
 
302
- .controls-row {
303
- flex-direction: column;
304
- align-items: stretch;
305
- }
306
- .radio-group {
307
- justify-content: space-between;
308
- background: #fff;
309
- padding: 10px;
310
- border-radius: 8px;
311
- border: 1px solid #ddd;
312
- }
313
- .button { width: 100%; padding: 14px; }
314
-
315
- .env-item {
316
- grid-template-columns: 1fr;
317
- gap: 12px;
318
- position: relative;
319
- padding-bottom: 60px; /* Space for buttons */
320
- }
321
-
322
- .env-actions {
323
- position: absolute;
324
- bottom: 15px;
325
- left: 15px;
326
- right: 15px;
327
- display: grid;
328
- grid-template-columns: 3fr 1fr;
329
- gap: 10px;
330
- }
331
-
332
- .env-actions button { width: 100%; }
333
- .env-actions form { width: 100%; }
334
 
335
- .modal-content { margin: 10% auto; width: 95%; padding: 20px 15px; }
336
  .stats-table th, .stats-table td { font-size: 0.75rem; padding: 6px 4px; }
337
  }
338
  </style>
@@ -356,7 +288,7 @@ ADMHOSTO_TEMPLATE = '''
356
  <label><input type="radio" name="env_type" value="closed" checked> <i class="fas fa-lock"></i> Закрытая</label>
357
  <label><input type="radio" name="env_type" value="open"> <i class="fas fa-globe"></i> Открытая</label>
358
  </div>
359
- <button type="submit" class="button"><i class="fas fa-plus-circle"></i> Создать</button>
360
  </div>
361
  </form>
362
  </div>
@@ -366,9 +298,9 @@ ADMHOSTO_TEMPLATE = '''
366
  </div>
367
 
368
  <div class="section">
369
- {% if environments %}
370
  <ul class="env-list">
371
- {% for env in environments %}
372
  <li class="env-item">
373
  <div class="env-details">
374
  <div class="env-header">
@@ -382,21 +314,58 @@ ADMHOSTO_TEMPLATE = '''
382
  <a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
383
  </div>
384
  <div class="env-actions">
385
- <button class="button stats-button" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
386
- <form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Удалить среду {{ env.id }}?');">
387
- <button type="submit" class="button delete-button"><i class="fas fa-trash"></i></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  </form>
389
  </div>
390
  </li>
391
  {% endfor %}
392
  </ul>
393
  {% else %}
394
- <div style="text-align:center; padding: 20px; color: #888;">Список пуст</div>
395
  {% endif %}
396
  </div>
397
  </div>
398
 
399
- <!-- Modal -->
400
  <div id="statsModal" class="modal">
401
  <div class="modal-content">
402
  <span class="close-modal" onclick="closeStats()">&times;</span>
@@ -909,20 +878,22 @@ select option {
909
  <option value="shot on 35mm film, Kodak Portra 400">Пленка Kodak Portra</option>
910
  </select>
911
  </div>
912
- <div class="form-group full-width">
913
- <label for="location">Локация</label>
914
- <select id="location">
915
- <option value="in a clean white seamless studio background">Студия (белый фон)</option>
916
- <option value="in a creative editorial studio with mirrors and geometric shapes">Студия "Эдиториал" (зеркала)</option>
917
- <option value="on a rainy night street in Tokyo with neon lights">Улица (ночной Токио, неон)</option>
918
- <option value="in a minimalist modern interior with concrete walls">Минимализм (лофт, бетон)</option>
919
- <option value="on a rooftop overlooking the city skyline at sunset">Крыша с видом на город</option>
920
- <option value="in a luxurious vintage room with classic furniture">Роскошный интерьер (винтаж)</option>
921
- <option value="in a dense, magical forest with sunbeams filtering through">Природа (сказочный лес)</option>
922
- <option value="on a beautiful sandy beach during the golden hour">Природа (пляж, закат)</option>
923
- <option value="in a vibrant, bustling urban market">Город (оживленный рынок)</option>
924
- <option value="inside a futuristic, sci-fi corridor with glowing lines">Фантастика (коридор)</option>
925
- </select>
 
 
926
  </div>
927
  <div class="form-group full-width">
928
  <label for="model_details">Одежда и Детали (Опишите ткань и фасон!)</label>
@@ -1231,19 +1202,23 @@ function toggleCreativeMode() {
1231
  document.getElementById('object_background').disabled = isCreative;
1232
  }
1233
 
1234
- function setupStyleSelector() {
1235
- const styleContainer = document.getElementById('styleSelector');
1236
- if (!styleContainer) return;
1237
- const styleButtons = styleContainer.querySelectorAll('.style-btn');
1238
-
1239
- styleButtons.forEach(btn => {
1240
- btn.addEventListener('click', () => {
1241
- styleButtons.forEach(innerBtn => innerBtn.classList.remove('active'));
1242
- btn.classList.add('active');
 
 
 
1243
  });
1244
  });
1245
  }
1246
 
 
1247
  async function processAndOpen() {
1248
  const btn = document.querySelector('.action-btn');
1249
  const originalText = btn.innerHTML;
@@ -1255,22 +1230,20 @@ async function processAndOpen() {
1255
  const shotType = document.getElementById('shotType').value;
1256
  const bodyType = document.getElementById('bodyType').value;
1257
  const pose = document.getElementById('pose').value;
1258
- const location = document.getElementById('location').value;
1259
  const light = document.getElementById('light').value;
1260
  const camera = document.getElementById('camera').value;
1261
 
1262
  if (isMyModel) {
1263
  const details = document.getElementById('model_details').value || "the clothing from the reference image";
1264
  fullPrompt = `${envKeyword}, VIRTUAL TRY-ON.
1265
- INSTRUCTIONS FOR AI: This is a virtual try-on task. You will be given two images.
1266
- - Image 1 (Model): Use this as the reference for the model's face, identity, pose, and body shape.
1267
- - Image 2 (Garment): Use this image for the clothing item.
1268
- Task: Accurately dress the model from Image 1 in the clothing from Image 2. The final image must preserve the model's exact likeness and facial identity. The model should have a body type of "${bodyType}".
1269
- Style: ${style}, hyper-detailed, luxury brand campaign, Vogue aesthetic, impeccable.
1270
  Composition: ${shotType}.
1271
  Final Scene: Place the model in the following environment: ${location}.
1272
- Lighting: The scene should have ${light}.
1273
- Technical: Masterpiece professional photograph, shot on ${camera}, 8k UHD, tack sharp focus, breathtaking detail, perfect color grading.`;
1274
  } else {
1275
  const age = document.getElementById('age').value;
1276
  const nationality = document.getElementById('nationality').value;
@@ -1280,15 +1253,15 @@ Technical: Masterpiece professional photograph, shot on ${camera}, 8k UHD, tack
1280
  const emotion = document.getElementById('emotion').value;
1281
  const details = document.getElementById('model_details').value || "high-end fashion garments";
1282
 
1283
- fullPrompt = `${envKeyword}, style:: ${style}, high fashion editorial, luxury brand campaign (like Dior, D&G), Vogue aesthetic, hyper-detailed, impeccable.
1284
  composition:: ${shotType}.
1285
  subject:: An ultra-high-resolution, flawless photograph of a striking ${age} ${nationality} high fashion model.
1286
- model_characteristics:: physique ${bodyType}, ${hairColor} ${hairstyle} styled to perfection, expression ${emotion} (powerful yet serene), pose ${pose}.
1287
  clothing_focus:: The model is wearing ${details}, emphasizing haute couture craftsmanship.
1288
- texture_&_material_fidelity:: Extreme macro precision on textiles. Render the fabric weave, thread count, visible stitching, material weight, realistic creases and folds, tactile surface imperfections, and how light interacts with the fabric.
1289
- human_realism_details:: Capture flawless yet realistic skin texture. Perfect complexion, highlighting bone structure with expert contouring light. The image must look professionally retouched for a high-end magazine, but retain believability. Avoid any hint of digital artifacting.
1290
  scene_environment:: ${location}, creating a sophisticated and aspirational atmosphere.
1291
- technical:: Masterpiece professional photograph, ${light} meticulously crafted to sculpt the subject, shot on ${camera}, 8k UHD, tack sharp focus, breathtaking detail, uncompressed, color graded to perfection, award-winning photography.`;
1292
  }
1293
 
1294
  } else if (currentMode === 'children') {
@@ -1306,7 +1279,7 @@ technical:: Masterpiece professional photograph, ${light} meticulously crafted t
1306
  const emotion = document.getElementById('newborn_emotion').value;
1307
  const pose = document.getElementById('newborn_pose').value;
1308
  clothing_details = document.getElementById('newborn_details').value || "soft knitted fabric";
1309
- subject = `A beautiful, ultra-photorealistic portrait of a ${age} ${ethnicity} baby. Face expression is ${emotion}.`;
1310
  pose_info = `The baby is ${pose}.`;
1311
  } else {
1312
  const age = document.getElementById('child_age').value;
@@ -1315,7 +1288,7 @@ technical:: Masterpiece professional photograph, ${light} meticulously crafted t
1315
  const emotion = document.getElementById('child_emotion').value;
1316
  const pose = document.getElementById('child_pose').value;
1317
  clothing_details = document.getElementById('child_details').value || "detailed textured casual clothes";
1318
- subject = `A beautiful, ultra-photorealistic portrait of a ${age} ${ethnicity} ${gender}. Face expression is ${emotion}.`;
1319
  pose_info = `The child is ${pose}.`;
1320
  }
1321
 
@@ -1323,8 +1296,8 @@ technical:: Masterpiece professional photograph, ${light} meticulously crafted t
1323
  composition:: ${shotType}.
1324
  subject:: ${subject} The photograph must look like a cover shot for a high-end children's fashion magazine.
1325
  clothing_focus:: The child is wearing ${clothing_details}, presented as a luxury garment.
1326
- texture_&_material_fidelity:: Macro-level detail on clothing textures. Focus on the weave of cotton, the softness of wool, the texture of denim. Show realistic wrinkles, creases from movement, and even subtle fabric pilling. 100% texture fidelity is crucial.
1327
- human_realism_details:: Capture the pure, innocent beauty of the child. Flawless, dewy skin with a natural glow. The light should have a painterly, almost magical quality, highlighting their features beautifully. Eyes must be expressive and full of life.
1328
  scene_activity:: ${pose_info} The location is ${location}, creating a whimsical and high-end narrative.
1329
  technical:: Masterpiece photograph, ${light} creating a magical and soft atmosphere, shot on Fujifilm XT4, 56mm F1.2 lens, 8k, tack sharp focus, impeccable detail, perfect color grading, looks like a real captured moment of wonder from a luxury campaign.`;
1330
 
@@ -1344,11 +1317,11 @@ technical:: Masterpiece photograph, ${light} creating a magical and soft atmosph
1344
  }
1345
 
1346
  fullPrompt = `${envKeyword}, style:: Luxury product advertising, ${objectStyle}, sophisticated, sleek, ultra-photorealistic.
1347
- subject:: A breathtaking, hyper-realistic photograph of the luxury product: ${objectName}. The image must evoke desire and exclusivity.
1348
- material_focus:: Achieve 100% physical accuracy with an emphasis on perfection. Render pristine, flawless surfaces. Showcase the intricate details of the material grain, polished metal sheen, or crystal-clear refractions. Even microscopic details should look clean and perfect.
1349
  scene_context:: Placed ${background}. Additional details: ${objectDetails}, arranged with artistic precision.
1350
  composition:: ${objectComposition}, creating a powerful and elegant visual statement.
1351
- technical:: Advertisement-grade photograph, ${objectLighting} designed to accentuate the product's luxury form, 8k UHD resolution, flawless focus, extreme macro detail, advanced ray-traced reflections, impeccably clean, exudes quality and high-end appeal, masterpiece.`;
1352
  }
1353
 
1354
  const cleanPrompt = fullPrompt.replace(/\\s+/g, ' ').replace(/\\n/g, ' ').trim();
@@ -1377,7 +1350,7 @@ document.addEventListener('DOMContentLoaded', () => {
1377
  switchMode('model');
1378
  switchChildrenSubMode('newborn');
1379
  autoAdjustDefaults();
1380
- setupStyleSelector();
1381
  });
1382
  </script>
1383
 
@@ -1392,20 +1365,27 @@ def index():
1392
  @app.route('/admhosto', methods=['GET'])
1393
  def admhosto():
1394
  data = load_data()
1395
- environments_data = []
 
 
1396
  for env_id, env_data in data.items():
1397
- environments_data.append({
1398
  "id": env_id,
1399
  "keyword": env_data.get("keyword", "N/A"),
1400
  "type": env_data.get("type", "closed"),
1401
  "hits": env_data.get("hits", 0),
1402
  "created_at": env_data.get("created_at", ""),
1403
  "link": url_for('serve_env', env_id=env_id, _external=True)
1404
- })
1405
-
1406
- environments_data.sort(key=lambda x: x['created_at'], reverse=True)
 
 
 
 
 
1407
 
1408
- return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data)
1409
 
1410
  @app.route('/admhosto/create', methods=['POST'])
1411
  def create_environment():
@@ -1428,7 +1408,8 @@ def create_environment():
1428
  "device_token": None,
1429
  "hits": 0,
1430
  "logs": [],
1431
- "created_at": datetime.utcnow().isoformat()
 
1432
  }
1433
  save_data(all_data)
1434
  flash(f'Новая {env_type} среда с ID {new_id} создана.', 'success')
@@ -1438,9 +1419,48 @@ def create_environment():
1438
  def delete_environment(env_id):
1439
  all_data = load_data()
1440
  if env_id in all_data:
1441
- del all_data[env_id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1442
  save_data(all_data)
1443
- flash(f'Среда {env_id} была удалена.', 'success')
1444
  else:
1445
  flash(f'Среда {env_id} не найдена.', 'error')
1446
  return redirect(url_for('admhosto'))
@@ -1481,8 +1501,8 @@ def get_env_stats(env_id):
1481
  def serve_env(env_id):
1482
  data = load_data()
1483
  env_data = data.get(env_id)
1484
- if not env_data:
1485
- return "Среда не найдена.", 404
1486
 
1487
  keyword = env_data.get("keyword", "")
1488
  env_type = env_data.get("type", "closed")
@@ -1490,7 +1510,7 @@ def serve_env(env_id):
1490
  current_log = {
1491
  "time": datetime.utcnow().isoformat(),
1492
  "ip": request.remote_addr,
1493
- "ua": request.headers.get('User-Agent')[:50]
1494
  }
1495
 
1496
  env_data['hits'] = env_data.get('hits', 0) + 1
 
166
  <meta charset="UTF-8">
167
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
168
  <title>Админ-панель</title>
169
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
170
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
171
  <style>
172
  :root {
 
177
  --text-dark: #333;
178
  --text-on-accent: #003C43;
179
  --danger: #E57373;
180
+ --warning: #ffb74d;
181
+ --info: #4fc3f7;
182
+ --success: #81c784;
183
+ --archive: #90a4ae;
184
  }
185
  * { box-sizing: border-box; }
186
  body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); margin: 0; padding: 15px; }
187
  .container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 3px 15px rgba(0,0,0,0.08); }
188
+ h1, h2 { font-weight: 600; color: var(--bg-medium); text-align: center; }
189
+ h1 { margin-bottom: 25px; font-size: 1.5rem; }
190
+ h2 { font-size: 1.3rem; margin-top: 40px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; margin-bottom: 20px; }
191
 
192
  .section { margin-bottom: 25px; }
193
 
194
+ .add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; }
 
 
 
 
 
 
 
 
195
 
196
  input[type="text"] {
197
+ width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem;
198
+ font-family: inherit; background: #fff; -webkit-appearance: none;
 
 
 
 
 
 
199
  }
200
 
201
+ .controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
 
 
 
 
 
 
 
202
  .radio-group { display: flex; gap: 15px; }
203
  .radio-group label { cursor: pointer; display: flex; align-items: center; gap: 6px; font-weight: 500; font-size: 0.95rem; }
204
 
205
  .button {
206
+ padding: 10px 15px; border: none; border-radius: 8px; color: white; font-weight: 600; cursor: pointer; text-decoration: none;
207
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-size: 0.9rem; transition: opacity 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
208
  }
209
+ .button:hover { opacity: 0.85; }
210
  .button:active { transform: scale(0.98); }
211
 
212
+ .button.primary { background-color: var(--accent); color: var(--text-on-accent); }
213
+ .button.danger { background-color: var(--danger); }
214
+ .button.warning { background-color: var(--warning); color: #333; }
215
+ .button.info { background-color: var(--info); }
216
+ .button.success { background-color: var(--success); }
217
+
218
  .env-list { list-style: none; padding: 0; margin: 0; }
219
  .env-item {
220
+ background: #fff; border: 1px solid #e0e0e0; border-radius: 10px; padding: 15px; margin-bottom: 12px;
221
+ display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.02);
 
 
 
 
 
 
 
 
222
  }
223
+ .env-item-archived { border-left: 4px solid var(--archive); }
224
+
225
  .env-details { display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
226
  .env-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
227
  .env-id { font-weight: 700; color: var(--bg-medium); font-size: 1.1rem; }
228
  .env-keyword { font-style: italic; color: #666; font-size: 0.9rem;}
229
 
230
+ .env-link { font-size: 0.9rem; color: #007bff; word-break: break-all; text-decoration: none; padding: 5px 0; display: block; }
 
 
 
 
 
 
 
231
 
232
+ .env-type-badge { font-size: 0.75rem; padding: 3px 8px; border-radius: 20px; font-weight: bold; text-transform: uppercase; white-space: nowrap; }
 
 
 
 
 
 
 
233
  .type-open { background-color: #d4edda; color: #155724; }
234
  .type-closed { background-color: #f8d7da; color: #721c24; }
235
 
236
+ .env-actions { display: flex; flex-wrap: wrap; gap: 8px; }
 
 
237
 
238
  .message { padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center; font-size: 0.95rem; }
239
  .message.success { background-color: #d4edda; color: #155724; }
240
  .message.error { background-color: #f8d7da; color: #721c24; }
241
 
 
242
  .modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }
243
  .modal-content { background-color: #fff; margin: 15% auto; padding: 25px; width: 90%; max-width: 600px; border-radius: 12px; position: relative; box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
244
  .close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
 
246
  .stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
247
  .stats-table th { background-color: var(--bg-medium); color: white; }
248
  .stats-table tr:nth-child(even) { background-color: #f9f9f9; }
249
+
250
+ .empty-list-placeholder { text-align:center; padding: 20px; color: #888; }
251
+ .no-margin { margin-bottom: 0; }
252
 
253
+ @media (max-width: 768px) {
254
+ .env-item { grid-template-columns: 1fr; gap: 12px; }
255
+ .env-actions { justify-content: flex-start; }
256
+ .modal-content { margin: 10% auto; width: 95%; padding: 20px 15px; }
257
+ }
258
+
259
  @media (max-width: 600px) {
260
  body { padding: 10px; }
261
  .container { padding: 15px; }
262
  h1 { font-size: 1.3rem; margin-bottom: 20px; }
263
 
264
+ .controls-row { flex-direction: column; align-items: stretch; }
265
+ .radio-group { justify-content: space-between; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid #ddd; }
266
+ .add-env-form .button { width: 100%; padding: 14px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
 
268
  .stats-table th, .stats-table td { font-size: 0.75rem; padding: 6px 4px; }
269
  }
270
  </style>
 
288
  <label><input type="radio" name="env_type" value="closed" checked> <i class="fas fa-lock"></i> Закрытая</label>
289
  <label><input type="radio" name="env_type" value="open"> <i class="fas fa-globe"></i> Открытая</label>
290
  </div>
291
+ <button type="submit" class="button primary"><i class="fas fa-plus-circle"></i> Создать</button>
292
  </div>
293
  </form>
294
  </div>
 
298
  </div>
299
 
300
  <div class="section">
301
+ {% if active_environments %}
302
  <ul class="env-list">
303
+ {% for env in active_environments %}
304
  <li class="env-item">
305
  <div class="env-details">
306
  <div class="env-header">
 
314
  <a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
315
  </div>
316
  <div class="env-actions">
317
+ <button class="button info" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
318
+ <form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
319
+ <button type="submit" class="button warning">
320
+ <i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыть' if env.type == 'closed' else 'Закрыть' }}
321
+ </button>
322
+ </form>
323
+ {% if env.type == 'closed' %}
324
+ <form method="POST" action="{{ url_for('clear_user', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Отвязать пользователя от среды {{ env.id }}? Первый, кто зайдет по ссылке, станет владельцем.');">
325
+ <button type="submit" class="button success"><i class="fas fa-user-slash"></i> Сброс</button>
326
+ </form>
327
+ {% endif %}
328
+ <form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Переместить среду {{ env.id }} в архив?');">
329
+ <button type="submit" class="button danger"><i class="fas fa-archive"></i></button>
330
+ </form>
331
+ </div>
332
+ </li>
333
+ {% endfor %}
334
+ </ul>
335
+ {% else %}
336
+ <div class="empty-list-placeholder">Список активных сред пуст</div>
337
+ {% endif %}
338
+ </div>
339
+
340
+ <div class="section no-margin">
341
+ <h2><i class="fas fa-archive"></i> Архив</h2>
342
+ {% if archived_environments %}
343
+ <ul class="env-list">
344
+ {% for env in archived_environments %}
345
+ <li class="env-item env-item-archived">
346
+ <div class="env-details">
347
+ <div class="env-header">
348
+ <span class="env-id">{{ env.id }}</span>
349
+ <span class="env-type-badge type-{{ env.type }}">
350
+ {{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
351
+ </span>
352
+ </div>
353
+ <span class="env-keyword">{{ env.keyword }}</span>
354
+ </div>
355
+ <div class="env-actions">
356
+ <form method="POST" action="{{ url_for('restore_environment', env_id=env.id) }}" style="display:contents;">
357
+ <button type="submit" class="button success"><i class="fas fa-undo"></i> Восстановить</button>
358
  </form>
359
  </div>
360
  </li>
361
  {% endfor %}
362
  </ul>
363
  {% else %}
364
+ <div class="empty-list-placeholder">Архив пуст</div>
365
  {% endif %}
366
  </div>
367
  </div>
368
 
 
369
  <div id="statsModal" class="modal">
370
  <div class="modal-content">
371
  <span class="close-modal" onclick="closeStats()">&times;</span>
 
878
  <option value="shot on 35mm film, Kodak Portra 400">Пленка Kodak Portra</option>
879
  </select>
880
  </div>
881
+ <div class="form-group full-width">
882
+ <label>Локация</label>
883
+ <div id="locationSelector" class="style-grid">
884
+ <button type="button" class="style-btn active" data-value="in a clean white seamless studio background">Студия (белый фон)</button>
885
+ <button type="button" class="style-btn" data-value="in a creative editorial studio with mirrors and geometric shapes">Студия "Эдиториал"</button>
886
+ <button type="button" class="style-btn" data-value="on a rainy night street in Tokyo with neon lights">Ночной Токио (неон)</button>
887
+ <button type="button" class="style-btn" data-value="in a minimalist modern interior with concrete walls">Лофт (бетон)</button>
888
+ <button type="button" class="style-btn" data-value="on a rooftop overlooking the city skyline at sunset">Крыша (закат)</button>
889
+ <button type="button" class="style-btn" data-value="in a luxurious vintage room with classic furniture">Роскошный интерьер</button>
890
+ <button type="button" class="style-btn" data-value="in a dense, magical forest with sunbeams filtering through">Сказочный лес</button>
891
+ <button type="button" class="style-btn" data-value="on a beautiful sandy beach during the golden hour">Пляж (золотой час)</button>
892
+ <button type="button" class="style-btn" data-value="in a vibrant, bustling urban market">Городской рынок</button>
893
+ <button type="button" class="style-btn" data-value="inside a futuristic, sci-fi corridor with glowing lines">НФ коридор</button>
894
+ <button type="button" class="style-btn" data-value="in an old, atmospheric library with tall bookshelves">Старая библиотека</button>
895
+ <button type="button" class="style-btn" data-value="at a vibrant, colorful carnival at night">Ночной карнавал</button>
896
+ </div>
897
  </div>
898
  <div class="form-group full-width">
899
  <label for="model_details">Одежда и Детали (Опишите ткань и фасон!)</label>
 
1202
  document.getElementById('object_background').disabled = isCreative;
1203
  }
1204
 
1205
+ function setupClickableSelectors() {
1206
+ const selectors = ['styleSelector', 'locationSelector'];
1207
+ selectors.forEach(selectorId => {
1208
+ const container = document.getElementById(selectorId);
1209
+ if (!container) return;
1210
+ const buttons = container.querySelectorAll('.style-btn');
1211
+
1212
+ buttons.forEach(btn => {
1213
+ btn.addEventListener('click', () => {
1214
+ buttons.forEach(innerBtn => innerBtn.classList.remove('active'));
1215
+ btn.classList.add('active');
1216
+ });
1217
  });
1218
  });
1219
  }
1220
 
1221
+
1222
  async function processAndOpen() {
1223
  const btn = document.querySelector('.action-btn');
1224
  const originalText = btn.innerHTML;
 
1230
  const shotType = document.getElementById('shotType').value;
1231
  const bodyType = document.getElementById('bodyType').value;
1232
  const pose = document.getElementById('pose').value;
1233
+ const location = document.querySelector('#locationSelector .style-btn.active').dataset.value;
1234
  const light = document.getElementById('light').value;
1235
  const camera = document.getElementById('camera').value;
1236
 
1237
  if (isMyModel) {
1238
  const details = document.getElementById('model_details').value || "the clothing from the reference image";
1239
  fullPrompt = `${envKeyword}, VIRTUAL TRY-ON.
1240
+ INSTRUCTIONS: This is a virtual try-on task. Use the model's photo for identity, face, and pose. Use the garment photo for the clothing.
1241
+ Task: Accurately transfer the clothing onto the model. The final image must preserve the model's exact facial identity, body shape of "${bodyType}", and pose.
1242
+ Style: ${style}, hyper-detailed, luxury brand campaign, Vogue aesthetic, ultra-photorealistic.
 
 
1243
  Composition: ${shotType}.
1244
  Final Scene: Place the model in the following environment: ${location}.
1245
+ Lighting: The scene must have ${light}.
1246
+ Technical: Masterpiece professional photograph, shot on ${camera}, 8k UHD, razor-sharp focus, extreme detail, perfect color grading, no digital artifacts.`;
1247
  } else {
1248
  const age = document.getElementById('age').value;
1249
  const nationality = document.getElementById('nationality').value;
 
1253
  const emotion = document.getElementById('emotion').value;
1254
  const details = document.getElementById('model_details').value || "high-end fashion garments";
1255
 
1256
+ fullPrompt = `${envKeyword}, style:: ${style}, high fashion editorial, luxury brand campaign (like Dior, D&G), Vogue aesthetic, hyper-realistic photography, award-winning.
1257
  composition:: ${shotType}.
1258
  subject:: An ultra-high-resolution, flawless photograph of a striking ${age} ${nationality} high fashion model.
1259
+ model_characteristics:: physique ${bodyType}, ${hairColor} ${hairstyle} with individual strands visible and natural flyaways, expression ${emotion} (powerful yet serene), pose ${pose}.
1260
  clothing_focus:: The model is wearing ${details}, emphasizing haute couture craftsmanship.
1261
+ texture_&_material_fidelity:: Extreme macro precision on textiles. Render the fabric weave, individual threads, visible stitching, material weight, realistic creases and folds, tactile surface imperfections like denim twill or wool fibers, and how light interacts with the fabric.
1262
+ human_realism_details:: Capture hyper-realistic skin texture, showing pores, vellus hair, and subtle imperfections. Avoid any plastic or airbrushed look. Expertly sculpted light highlights the bone structure. Eyes must have realistic reflections and depth.
1263
  scene_environment:: ${location}, creating a sophisticated and aspirational atmosphere.
1264
+ technical:: Masterpiece professional photograph, ${light} meticulously crafted to sculpt the subject with soft shadows and catchlights in eyes, shot on ${camera}, 8k UHD, razor-sharp focus, breathtaking detail, uncompressed, color graded to perfection.`;
1265
  }
1266
 
1267
  } else if (currentMode === 'children') {
 
1279
  const emotion = document.getElementById('newborn_emotion').value;
1280
  const pose = document.getElementById('newborn_pose').value;
1281
  clothing_details = document.getElementById('newborn_details').value || "soft knitted fabric";
1282
+ subject = `An ultra-photorealistic portrait of a ${age} ${ethnicity} baby. Face expression is ${emotion}.`;
1283
  pose_info = `The baby is ${pose}.`;
1284
  } else {
1285
  const age = document.getElementById('child_age').value;
 
1288
  const emotion = document.getElementById('child_emotion').value;
1289
  const pose = document.getElementById('child_pose').value;
1290
  clothing_details = document.getElementById('child_details').value || "detailed textured casual clothes";
1291
+ subject = `An ultra-photorealistic portrait of a ${age} ${ethnicity} ${gender}. Face expression is ${emotion}.`;
1292
  pose_info = `The child is ${pose}.`;
1293
  }
1294
 
 
1296
  composition:: ${shotType}.
1297
  subject:: ${subject} The photograph must look like a cover shot for a high-end children's fashion magazine.
1298
  clothing_focus:: The child is wearing ${clothing_details}, presented as a luxury garment.
1299
+ texture_&_material_fidelity:: Macro-level detail on clothing textures. Focus on the weave of cotton, the softness of wool fibers, the texture of denim. Show realistic wrinkles, creases from movement, and even subtle fabric pilling. Absolute texture fidelity is crucial.
1300
+ human_realism_details:: Capture the pure, innocent beauty of the child. Hyper-realistic, dewy skin with a natural glow and visible texture, not airbrushed. The light should have a painterly, almost magical quality, highlighting their features beautifully. Eyes must be expressive, with realistic reflections and depth. Individual hair strands should be visible.
1301
  scene_activity:: ${pose_info} The location is ${location}, creating a whimsical and high-end narrative.
1302
  technical:: Masterpiece photograph, ${light} creating a magical and soft atmosphere, shot on Fujifilm XT4, 56mm F1.2 lens, 8k, tack sharp focus, impeccable detail, perfect color grading, looks like a real captured moment of wonder from a luxury campaign.`;
1303
 
 
1317
  }
1318
 
1319
  fullPrompt = `${envKeyword}, style:: Luxury product advertising, ${objectStyle}, sophisticated, sleek, ultra-photorealistic.
1320
+ subject:: A breathtaking, hyper-realistic photograph of the luxury product: ${objectName}. The image must evoke desire and exclusivity, looking like a real, professionally shot advertisement.
1321
+ material_focus:: Achieve 1000% physical accuracy with an emphasis on perfection. Render pristine, flawless surfaces with micro-scratches and realistic imperfections. Showcase the intricate details of the material grain, polished metal sheen with fingerprints, or crystal-clear refractions. Even microscopic dust particles should look clean and perfect.
1322
  scene_context:: Placed ${background}. Additional details: ${objectDetails}, arranged with artistic precision.
1323
  composition:: ${objectComposition}, creating a powerful and elegant visual statement.
1324
+ technical:: Advertisement-grade photograph, ${objectLighting} designed to accentuate the product's luxury form and textures, 8k UHD resolution, flawless focus, extreme macro detail, advanced ray-traced reflections and caustics, impeccably clean, exudes quality and high-end appeal, masterpiece.`;
1325
  }
1326
 
1327
  const cleanPrompt = fullPrompt.replace(/\\s+/g, ' ').replace(/\\n/g, ' ').trim();
 
1350
  switchMode('model');
1351
  switchChildrenSubMode('newborn');
1352
  autoAdjustDefaults();
1353
+ setupClickableSelectors();
1354
  });
1355
  </script>
1356
 
 
1365
  @app.route('/admhosto', methods=['GET'])
1366
  def admhosto():
1367
  data = load_data()
1368
+ active_environments = []
1369
+ archived_environments = []
1370
+
1371
  for env_id, env_data in data.items():
1372
+ env_item = {
1373
  "id": env_id,
1374
  "keyword": env_data.get("keyword", "N/A"),
1375
  "type": env_data.get("type", "closed"),
1376
  "hits": env_data.get("hits", 0),
1377
  "created_at": env_data.get("created_at", ""),
1378
  "link": url_for('serve_env', env_id=env_id, _external=True)
1379
+ }
1380
+ if env_data.get("archived"):
1381
+ archived_environments.append(env_item)
1382
+ else:
1383
+ active_environments.append(env_item)
1384
+
1385
+ active_environments.sort(key=lambda x: x['created_at'], reverse=True)
1386
+ archived_environments.sort(key=lambda x: x['created_at'], reverse=True)
1387
 
1388
+ return render_template_string(ADMHOSTO_TEMPLATE, active_environments=active_environments, archived_environments=archived_environments)
1389
 
1390
  @app.route('/admhosto/create', methods=['POST'])
1391
  def create_environment():
 
1408
  "device_token": None,
1409
  "hits": 0,
1410
  "logs": [],
1411
+ "created_at": datetime.utcnow().isoformat(),
1412
+ "archived": False
1413
  }
1414
  save_data(all_data)
1415
  flash(f'Новая {env_type} среда с ID {new_id} создана.', 'success')
 
1419
  def delete_environment(env_id):
1420
  all_data = load_data()
1421
  if env_id in all_data:
1422
+ all_data[env_id]['archived'] = True
1423
+ save_data(all_data)
1424
+ flash(f'Среда {env_id} перемещена в архив.', 'success')
1425
+ else:
1426
+ flash(f'Среда {env_id} не найдена.', 'error')
1427
+ return redirect(url_for('admhosto'))
1428
+
1429
+ @app.route('/admhosto/restore/<env_id>', methods=['POST'])
1430
+ def restore_environment(env_id):
1431
+ all_data = load_data()
1432
+ if env_id in all_data:
1433
+ all_data[env_id]['archived'] = False
1434
+ save_data(all_data)
1435
+ flash(f'Среда {env_id} восстановлена из архива.', 'success')
1436
+ else:
1437
+ flash(f'Среда {env_id} не найдена.', 'error')
1438
+ return redirect(url_for('admhosto'))
1439
+
1440
+ @app.route('/admhosto/clear_user/<env_id>', methods=['POST'])
1441
+ def clear_user(env_id):
1442
+ all_data = load_data()
1443
+ if env_id in all_data and all_data[env_id].get('type') == 'closed':
1444
+ all_data[env_id]['device_token'] = None
1445
+ save_data(all_data)
1446
+ flash(f'Пользователь отвязан от среды {env_id}.', 'success')
1447
+ else:
1448
+ flash(f'Ошибка: Среда не найдена или не является закрытой.', 'error')
1449
+ return redirect(url_for('admhosto'))
1450
+
1451
+ @app.route('/admhosto/toggle_type/<env_id>', methods=['POST'])
1452
+ def toggle_type(env_id):
1453
+ all_data = load_data()
1454
+ if env_id in all_data:
1455
+ current_type = all_data[env_id].get('type', 'closed')
1456
+ if current_type == 'closed':
1457
+ all_data[env_id]['type'] = 'open'
1458
+ flash(f'Среда {env_id} теперь открыта.', 'success')
1459
+ else:
1460
+ all_data[env_id]['type'] = 'closed'
1461
+ all_data[env_id]['device_token'] = None
1462
+ flash(f'Среда {env_id} теперь закрыта. Пользователь сброшен.', 'success')
1463
  save_data(all_data)
 
1464
  else:
1465
  flash(f'Среда {env_id} не найдена.', 'error')
1466
  return redirect(url_for('admhosto'))
 
1501
  def serve_env(env_id):
1502
  data = load_data()
1503
  env_data = data.get(env_id)
1504
+ if not env_data or env_data.get("archived"):
1505
+ return "Среда не найдена или заархивирована.", 404
1506
 
1507
  keyword = env_data.get("keyword", "")
1508
  env_type = env_data.get("type", "closed")
 
1510
  current_log = {
1511
  "time": datetime.utcnow().isoformat(),
1512
  "ip": request.remote_addr,
1513
+ "ua": request.headers.get('User-Agent')[:150]
1514
  }
1515
 
1516
  env_data['hits'] = env_data.get('hits', 0) + 1