Kgshop commited on
Commit
1d397af
·
verified ·
1 Parent(s): e73910c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -193
app.py CHANGED
@@ -62,7 +62,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
62
  try:
63
  if file_name == DATA_FILE:
64
  with open(file_name, 'w', encoding='utf-8') as f:
65
- json.dump({}, f)
66
  except Exception:
67
  pass
68
  success = True
@@ -110,18 +110,22 @@ def load_data():
110
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
111
  data = json.load(f)
112
  if not isinstance(data, dict):
113
- data = {}
 
 
 
 
114
  except (FileNotFoundError, json.JSONDecodeError):
115
  if download_db_from_hf(specific_file=DATA_FILE):
116
  try:
117
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
118
  data = json.load(f)
119
- if not isinstance(data, dict):
120
- data = {}
121
  except (FileNotFoundError, json.JSONDecodeError):
122
- data = {}
123
  else:
124
- data = {}
125
  return data
126
 
127
  def save_data(data):
@@ -178,113 +182,39 @@ ADMHOSTO_TEMPLATE = '''
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,46 +222,18 @@ 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
  }
@@ -377,26 +279,68 @@ ADMHOSTO_TEMPLATE = '''
377
  {{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
378
  </span>
379
  <small style="color:#888">{{ env.hits }} <i class="fas fa-eye"></i></small>
 
 
 
380
  </div>
381
  <span class="env-keyword">{{ env.keyword }}</span>
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>
@@ -721,13 +665,13 @@ select option {
721
  display: contents;
722
  }
723
 
724
- .style-grid {
725
  display: grid;
726
  grid-template-columns: 1fr 1fr;
727
  gap: 10px;
728
  }
729
 
730
- .style-btn {
731
  padding: 12px 10px;
732
  background-color: var(--input-bg);
733
  border: 1px solid var(--border);
@@ -741,12 +685,12 @@ select option {
741
  width: 100%;
742
  }
743
 
744
- .style-btn:hover {
745
  border-color: var(--primary);
746
  color: var(--text);
747
  }
748
 
749
- .style-btn.active {
750
  background-color: var(--primary);
751
  color: #000;
752
  border-color: var(--primary);
@@ -890,13 +834,13 @@ select option {
890
  </div>
891
  <div class="form-group full-width">
892
  <label>Эстетика</label>
893
- <div id="styleSelector" class="style-grid">
894
- <button type="button" class="style-btn active" data-value="Raw Candid Photography">Живое фото (Raw)</button>
895
- <button type="button" class="style-btn" data-value="Cinematic Movie Still">Кадр из фильма</button>
896
- <button type="button" class="style-btn" data-value="Fashion Editorial">Модный журнал</button>
897
- <button type="button" class="style-btn" data-value="Street Style Photo">Уличный стиль</button>
898
- <button type="button" class="style-btn" data-value="Dark Moody Atmosphere">Мрачная атмосфера</button>
899
- <button type="button" class="style-btn" data-value="Vintage Analog Film">Пленка (Vintage)</button>
900
  </div>
901
  </div>
902
  <div class="form-group">
@@ -910,19 +854,19 @@ select 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,14 +1175,14 @@ 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
  });
@@ -1251,11 +1195,11 @@ async function processAndOpen() {
1251
 
1252
  if (currentMode === 'model') {
1253
  const isMyModel = document.getElementById('my_model_checkbox').checked;
1254
- const style = document.querySelector('#styleSelector .style-btn.active').dataset.value;
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
 
@@ -1265,12 +1209,12 @@ async function processAndOpen() {
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 +1224,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') {
@@ -1321,12 +1265,12 @@ technical:: Masterpiece professional photograph, ${light} meticulously crafted t
1321
 
1322
  fullPrompt = `${envKeyword}, style:: ${style}, luxury children's fashion campaign, cinematic storytelling, enchanting, ultra-photorealistic.
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
 
1331
  } else {
1332
  const objectName = document.getElementById('object_name').value || "a product";
@@ -1345,7 +1289,7 @@ technical:: Masterpiece photograph, ${light} creating a magical and soft atmosph
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.`;
@@ -1377,7 +1321,8 @@ document.addEventListener('DOMContentLoaded', () => {
1377
  switchMode('model');
1378
  switchChildrenSubMode('newborn');
1379
  autoAdjustDefaults();
1380
- setupStyleSelector();
 
1381
  });
1382
  </script>
1383
 
@@ -1393,19 +1338,28 @@ def index():
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():
@@ -1419,10 +1373,10 @@ def create_environment():
1419
 
1420
  while True:
1421
  new_id = ''.join(random.choices(string.digits, k=6))
1422
- if new_id not in all_data:
1423
  break
1424
 
1425
- all_data[new_id] = {
1426
  "keyword": keyword,
1427
  "type": env_type,
1428
  "device_token": None,
@@ -1437,18 +1391,60 @@ def create_environment():
1437
  @app.route('/admhosto/delete/<env_id>', methods=['POST'])
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'))
1447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1448
  @app.route('/admhosto/stats/<env_id>')
1449
  def get_env_stats(env_id):
1450
  data = load_data()
1451
- env_data = data.get(env_id)
1452
  if not env_data:
1453
  return jsonify({"error": "Среда не найдена"}), 404
1454
 
@@ -1480,7 +1476,7 @@ def get_env_stats(env_id):
1480
  @app.route('/env/<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
 
@@ -1490,7 +1486,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
@@ -1501,7 +1497,7 @@ def serve_env(env_id):
1501
  if len(env_data['logs']) > 30:
1502
  env_data['logs'] = env_data['logs'][-30:]
1503
 
1504
- data[env_id] = env_data
1505
  save_data(data)
1506
 
1507
  if env_type == 'open':
@@ -1538,11 +1534,11 @@ def serve_env(env_id):
1538
  else:
1539
  new_token = ''.join(random.choices(string.ascii_letters + string.digits, k=40))
1540
  env_data['device_token'] = new_token
1541
- data[env_id] = env_data
1542
  save_data(data)
1543
 
1544
  resp = make_response(render_template_string(SYNKRIS_LOOK_TEMPLATE, keyword=keyword))
1545
- resp.set_cookie(f'access_token_{env_id}', new_token, max_age=31536000, httponly=True)
1546
  return resp
1547
 
1548
  if __name__ == '__main__':
 
62
  try:
63
  if file_name == DATA_FILE:
64
  with open(file_name, 'w', encoding='utf-8') as f:
65
+ json.dump({"environments": {}, "archive": {}}, f)
66
  except Exception:
67
  pass
68
  success = True
 
110
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
111
  data = json.load(f)
112
  if not isinstance(data, dict):
113
+ data = {"environments": {}, "archive": {}}
114
+ if "environments" not in data:
115
+ data["environments"] = {}
116
+ if "archive" not in data:
117
+ data["archive"] = {}
118
  except (FileNotFoundError, json.JSONDecodeError):
119
  if download_db_from_hf(specific_file=DATA_FILE):
120
  try:
121
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
122
  data = json.load(f)
123
+ if "environments" not in data or "archive" not in data:
124
+ data = {"environments": data, "archive": {}}
125
  except (FileNotFoundError, json.JSONDecodeError):
126
+ data = {"environments": {}, "archive": {}}
127
  else:
128
+ data = {"environments": {}, "archive": {}}
129
  return data
130
 
131
  def save_data(data):
 
182
  --text-on-accent: #003C43;
183
  --danger: #E57373;
184
  --warning: #ffcc80;
185
+ --info: #64b5f6;
186
+ --grey: #9e9e9e;
187
  }
188
  * { box-sizing: border-box; }
189
  body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); margin: 0; padding: 15px; }
190
  .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); }
191
+ h1, h2 { font-weight: 600; color: var(--bg-medium); margin-bottom: 25px; text-align: center; font-size: 1.5rem; }
192
+ h2 { font-size: 1.3rem; margin-top: 40px; border-top: 1px solid #eee; padding-top: 25px; }
193
  .section { margin-bottom: 25px; }
194
+ .add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; }
195
+ input[type="text"] { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; font-family: inherit; background: #fff; -webkit-appearance: none; }
196
+ .controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  .radio-group { display: flex; gap: 15px; }
198
  .radio-group label { cursor: pointer; display: flex; align-items: center; gap: 6px; font-weight: 500; font-size: 0.95rem; }
199
+ .button { padding: 12px 20px; border: none; border-radius: 8px; background-color: var(--accent); color: var(--text-on-accent); font-weight: 600; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-size: 1rem; transition: opacity 0.2s; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  .button:hover { opacity: 0.9; }
201
  .button:active { transform: scale(0.98); }
 
202
  .env-list { list-style: none; padding: 0; margin: 0; }
203
+ .env-item { background: #fff; border: 1px solid #e0e0e0; border-radius: 10px; padding: 15px; margin-bottom: 12px; display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.02); }
 
 
 
 
 
 
 
 
 
 
 
 
204
  .env-details { display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
205
  .env-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
206
  .env-id { font-weight: 700; color: var(--bg-medium); font-size: 1.1rem; }
207
  .env-keyword { font-style: italic; color: #666; font-size: 0.9rem;}
208
+ .env-link { font-size: 0.9rem; color: #007bff; word-break: break-all; text-decoration: none; padding: 5px 0; display: block; }
209
+ .env-type-badge { font-size: 0.75rem; padding: 3px 8px; border-radius: 20px; font-weight: bold; text-transform: uppercase; white-space: nowrap; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  .type-open { background-color: #d4edda; color: #155724; }
211
  .type-closed { background-color: #f8d7da; color: #721c24; }
212
+ .env-actions { display: flex; flex-wrap: wrap; gap: 8px; }
213
+ .action-button { background-color: var(--info); color: white; padding: 10px 15px; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px; }
 
214
  .delete-button { background-color: var(--danger); color: white; padding: 10px 15px; font-size: 0.9rem;}
 
215
  .message { padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center; font-size: 0.95rem; }
216
  .message.success { background-color: #d4edda; color: #155724; }
217
  .message.error { background-color: #f8d7da; color: #721c24; }
 
 
218
  .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); }
219
  .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); }
220
  .close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
 
222
  .stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
223
  .stats-table th { background-color: var(--bg-medium); color: white; }
224
  .stats-table tr:nth-child(even) { background-color: #f9f9f9; }
225
+ .archived-item { opacity: 0.6; border-left: 4px solid var(--grey); }
226
+ .archived-item .env-id { text-decoration: line-through; }
227
  @media (max-width: 600px) {
228
  body { padding: 10px; }
229
  .container { padding: 15px; }
230
  h1 { font-size: 1.3rem; margin-bottom: 20px; }
231
+ .controls-row { flex-direction: column; align-items: stretch; }
232
+ .radio-group { justify-content: space-between; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid #ddd; }
 
 
 
 
 
 
 
 
 
 
233
  .button { width: 100%; padding: 14px; }
234
+ .env-item { grid-template-columns: 1fr; gap: 12px; }
235
+ .env-actions { flex-direction: column; }
236
+ .env-actions .button { width: 100%; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  .modal-content { margin: 10% auto; width: 95%; padding: 20px 15px; }
238
  .stats-table th, .stats-table td { font-size: 0.75rem; padding: 6px 4px; }
239
  }
 
279
  {{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
280
  </span>
281
  <small style="color:#888">{{ env.hits }} <i class="fas fa-eye"></i></small>
282
+ {% if env.type == 'closed' and env.has_token %}
283
+ <i class="fas fa-user-check" title="Привязано к устройству" style="color: green;"></i>
284
+ {% endif %}
285
  </div>
286
  <span class="env-keyword">{{ env.keyword }}</span>
287
  <a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
288
  </div>
289
  <div class="env-actions">
290
+ <button class="button action-button" style="background-color: #5d4037;" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
291
+
292
+ <form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
293
+ <button type="submit" class="button action-button" style="background-color: var(--warning); color: #333;"><i class="fas fa-sync-alt"></i>
294
+ {{ 'Сделать открытой' if env.type == 'closed' else 'Сделать закрытой' }}
295
+ </button>
296
+ </form>
297
+
298
+ {% if env.type == 'closed' %}
299
+ <form method="POST" action="{{ url_for('reset_device', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Отвязать устройство от среды {{ env.id }}?');">
300
+ <button type="submit" class="button action-button" style="background-color: var(--info);"><i class="fas fa-user-slash"></i> Сброс</button>
301
+ </form>
302
+ {% endif %}
303
+
304
+ <form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Архивировать среду {{ env.id }}?');">
305
+ <button type="submit" class="button delete-button"><i class="fas fa-archive"></i></button>
306
  </form>
307
  </div>
308
  </li>
309
  {% endfor %}
310
  </ul>
311
  {% else %}
312
+ <div style="text-align:center; padding: 20px; color: #888;">Список активных сред пуст</div>
313
  {% endif %}
314
  </div>
315
+
316
+ {% if archived_environments %}
317
+ <h2><i class="fas fa-archive"></i> Архив</h2>
318
+ <div class="section">
319
+ <ul class="env-list">
320
+ {% for env in archived_environments %}
321
+ <li class="env-item archived-item">
322
+ <div class="env-details">
323
+ <div class="env-header">
324
+ <span class="env-id">{{ env.id }}</span>
325
+ <span class="env-type-badge type-{{ env.type }}">
326
+ {{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
327
+ </span>
328
+ </div>
329
+ <span class="env-keyword">{{ env.keyword }}</span>
330
+ </div>
331
+ <div class="env-actions">
332
+ <form method="POST" action="{{ url_for('restore_environment', env_id=env.id) }}" style="display:contents;">
333
+ <button type="submit" class="button action-button" style="background-color: #4caf50;"><i class="fas fa-undo"></i> Восстановить</button>
334
+ </form>
335
+ </div>
336
+ </li>
337
+ {% endfor %}
338
+ </ul>
339
+ </div>
340
+ {% endif %}
341
+
342
  </div>
343
 
 
344
  <div id="statsModal" class="modal">
345
  <div class="modal-content">
346
  <span class="close-modal" onclick="closeStats()">&times;</span>
 
665
  display: contents;
666
  }
667
 
668
+ .grid-selector {
669
  display: grid;
670
  grid-template-columns: 1fr 1fr;
671
  gap: 10px;
672
  }
673
 
674
+ .grid-btn {
675
  padding: 12px 10px;
676
  background-color: var(--input-bg);
677
  border: 1px solid var(--border);
 
685
  width: 100%;
686
  }
687
 
688
+ .grid-btn:hover {
689
  border-color: var(--primary);
690
  color: var(--text);
691
  }
692
 
693
+ .grid-btn.active {
694
  background-color: var(--primary);
695
  color: #000;
696
  border-color: var(--primary);
 
834
  </div>
835
  <div class="form-group full-width">
836
  <label>Эстетика</label>
837
+ <div id="styleSelector" class="grid-selector">
838
+ <button type="button" class="grid-btn active" data-value="Raw Candid Photography">Живое фото (Raw)</button>
839
+ <button type="button" class="grid-btn" data-value="Cinematic Movie Still">Кадр из фильма</button>
840
+ <button type="button" class="grid-btn" data-value="Fashion Editorial">Модный журнал</button>
841
+ <button type="button" class="grid-btn" data-value="Street Style Photo">Уличный стиль</button>
842
+ <button type="button" class="grid-btn" data-value="Dark Moody Atmosphere">Мрачная атмосфера</button>
843
+ <button type="button" class="grid-btn" data-value="Vintage Analog Film">Пленка (Vintage)</button>
844
  </div>
845
  </div>
846
  <div class="form-group">
 
854
  </select>
855
  </div>
856
  <div class="form-group full-width">
857
+ <label>Локация</label>
858
+ <div id="locationSelector" class="grid-selector">
859
+ <button type="button" class="grid-btn active" data-value="in a clean white seamless studio background">Студия (белый фон)</button>
860
+ <button type="button" class="grid-btn" data-value="in a raw industrial loft studio with exposed brick">Лофт ирпич, бетон)</button>
861
+ <button type="button" class="grid-btn" data-value="on a rainy night street in Tokyo with neon reflections">Ночной Токио (неон)</button>
862
+ <button type="button" class="grid-btn" data-value="in a minimalist modern interior with designer furniture">Минимализм (интерьер)</button>
863
+ <button type="button" class="grid-btn" data-value="on a rooftop overlooking the city skyline at sunset">Крыша акат)</button>
864
+ <button type="button" class="grid-btn" data-value="in a luxurious vintage room with classic furniture">Роскошный винтаж</button>
865
+ <button type="button" class="grid-btn" data-value="in a dense, magical forest with sunbeams filtering through">Сказочный лес</button>
866
+ <button type="button" class="grid-btn" data-value="on a beautiful sandy beach during the golden hour">Пляж (золотой час)</button>
867
+ <button type="button" class="grid-btn" data-value="in a charming cobblestone alley in a European city">Европейская улочка</button>
868
+ <button type="button" class="grid-btn" data-value="in a grand library with floor-to-ceiling bookshelves">Библиотека</button>
869
+ </div>
870
  </div>
871
  <div class="form-group full-width">
872
  <label for="model_details">Одежда и Детали (Опишите ткань и фасон!)</label>
 
1175
  document.getElementById('object_background').disabled = isCreative;
1176
  }
1177
 
1178
+ function setupGridSelector(containerId) {
1179
+ const container = document.getElementById(containerId);
1180
+ if (!container) return;
1181
+ const buttons = container.querySelectorAll('.grid-btn');
1182
 
1183
+ buttons.forEach(btn => {
1184
  btn.addEventListener('click', () => {
1185
+ buttons.forEach(innerBtn => innerBtn.classList.remove('active'));
1186
  btn.classList.add('active');
1187
  });
1188
  });
 
1195
 
1196
  if (currentMode === 'model') {
1197
  const isMyModel = document.getElementById('my_model_checkbox').checked;
1198
+ const style = document.querySelector('#styleSelector .grid-btn.active').dataset.value;
1199
  const shotType = document.getElementById('shotType').value;
1200
  const bodyType = document.getElementById('bodyType').value;
1201
  const pose = document.getElementById('pose').value;
1202
+ const location = document.querySelector('#locationSelector .grid-btn.active').dataset.value;
1203
  const light = document.getElementById('light').value;
1204
  const camera = document.getElementById('camera').value;
1205
 
 
1209
  INSTRUCTIONS FOR AI: This is a virtual try-on task. You will be given two images.
1210
  - Image 1 (Model): Use this as the reference for the model's face, identity, pose, and body shape.
1211
  - Image 2 (Garment): Use this image for the clothing item.
1212
+ Task: Accurately dress the model from Image 1 in the clothing from Image 2. The final image must preserve the model's exact likeness, facial identity, and hair. The model should have a body type of "${bodyType}".
1213
  Style: ${style}, hyper-detailed, luxury brand campaign, Vogue aesthetic, impeccable.
1214
  Composition: ${shotType}.
1215
  Final Scene: Place the model in the following environment: ${location}.
1216
  Lighting: The scene should have ${light}.
1217
+ Technical: Masterpiece professional photograph, shot on ${camera}, 8k UHD, tack sharp focus, breathtaking detail, perfect color grading, 100% photorealistic, no hint of AI.`;
1218
  } else {
1219
  const age = document.getElementById('age').value;
1220
  const nationality = document.getElementById('nationality').value;
 
1224
  const emotion = document.getElementById('emotion').value;
1225
  const details = document.getElementById('model_details').value || "high-end fashion garments";
1226
 
1227
+ fullPrompt = `${envKeyword}, style:: ${style}, high fashion editorial, luxury brand campaign, Vogue aesthetic, hyper-detailed, impeccable, 1000% photorealistic.
1228
  composition:: ${shotType}.
1229
  subject:: An ultra-high-resolution, flawless photograph of a striking ${age} ${nationality} high fashion model.
1230
+ model_characteristics:: physique ${bodyType}, ${hairColor} ${hairstyle} with visible individual hair strands and flyaways, expression ${emotion}.
1231
  clothing_focus:: The model is wearing ${details}, emphasizing haute couture craftsmanship.
1232
+ texture_&_material_fidelity:: Extreme macro precision on textiles. Render the fabric weave, thread count, visible stitching, material weight, realistic creases, tactile surface imperfections. The texture must be palpable.
1233
+ human_realism_details:: Capture flawless yet utterly realistic skin texture with visible pores and subtle imperfections. Perfect complexion, highlighting bone structure. Avoid any hint of digital, plastic, or airbrushed skin. Eyes must be hyper-realistic with natural reflections.
1234
+ scene_environment:: ${location}, creating a sophisticated atmosphere.
1235
+ technical:: Masterpiece professional photograph, ${light} meticulously crafted to sculpt the subject, shot on ${camera}, 8k UHD, tack sharp focus, breathtaking detail, uncompressed, subtle filmic grain, chromatic aberration, color graded to perfection, award-winning photography.`;
1236
  }
1237
 
1238
  } else if (currentMode === 'children') {
 
1265
 
1266
  fullPrompt = `${envKeyword}, style:: ${style}, luxury children's fashion campaign, cinematic storytelling, enchanting, ultra-photorealistic.
1267
  composition:: ${shotType}.
1268
+ subject:: ${subject} The photograph must look like a real, captured moment for a high-end children's fashion magazine.
1269
  clothing_focus:: The child is wearing ${clothing_details}, presented as a luxury garment.
1270
+ 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. 1000% texture fidelity is crucial.
1271
+ human_realism_details:: Capture the pure beauty of the child. Flawless, dewy skin with a natural glow and realistic texture, not airbrushed. The light should have a painterly, magical quality. Eyes must be expressive and full of life with natural reflections. Individual hair strands should be visible.
1272
  scene_activity:: ${pose_info} The location is ${location}, creating a whimsical and high-end narrative.
1273
+ 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 from a luxury campaign.`;
1274
 
1275
  } else {
1276
  const objectName = document.getElementById('object_name').value || "a product";
 
1289
 
1290
  fullPrompt = `${envKeyword}, style:: Luxury product advertising, ${objectStyle}, sophisticated, sleek, ultra-photorealistic.
1291
  subject:: A breathtaking, hyper-realistic photograph of the luxury product: ${objectName}. The image must evoke desire and exclusivity.
1292
+ material_focus:: Achieve 1000% physical accuracy. Render pristine, flawless surfaces. Showcase intricate details of the material grain, polished metal sheen, subtle surface imperfections, dust particles, and crystal-clear refractions. Even microscopic details should look clean and perfect.
1293
  scene_context:: Placed ${background}. Additional details: ${objectDetails}, arranged with artistic precision.
1294
  composition:: ${objectComposition}, creating a powerful and elegant visual statement.
1295
  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.`;
 
1321
  switchMode('model');
1322
  switchChildrenSubMode('newborn');
1323
  autoAdjustDefaults();
1324
+ setupGridSelector('styleSelector');
1325
+ setupGridSelector('locationSelector');
1326
  });
1327
  </script>
1328
 
 
1338
  def admhosto():
1339
  data = load_data()
1340
  environments_data = []
1341
+ for env_id, env_data in data.get("environments", {}).items():
1342
  environments_data.append({
1343
  "id": env_id,
1344
  "keyword": env_data.get("keyword", "N/A"),
1345
  "type": env_data.get("type", "closed"),
1346
  "hits": env_data.get("hits", 0),
1347
  "created_at": env_data.get("created_at", ""),
1348
+ "link": url_for('serve_env', env_id=env_id, _external=True),
1349
+ "has_token": bool(env_data.get("device_token"))
1350
  })
1351
 
1352
+ archived_environments_data = []
1353
+ for env_id, env_data in data.get("archive", {}).items():
1354
+ archived_environments_data.append({
1355
+ "id": env_id,
1356
+ "keyword": env_data.get("keyword", "N/A"),
1357
+ "type": env_data.get("type", "closed")
1358
+ })
1359
+
1360
  environments_data.sort(key=lambda x: x['created_at'], reverse=True)
1361
 
1362
+ return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data, archived_environments=archived_environments_data)
1363
 
1364
  @app.route('/admhosto/create', methods=['POST'])
1365
  def create_environment():
 
1373
 
1374
  while True:
1375
  new_id = ''.join(random.choices(string.digits, k=6))
1376
+ if new_id not in all_data["environments"] and new_id not in all_data["archive"]:
1377
  break
1378
 
1379
+ all_data["environments"][new_id] = {
1380
  "keyword": keyword,
1381
  "type": env_type,
1382
  "device_token": None,
 
1391
  @app.route('/admhosto/delete/<env_id>', methods=['POST'])
1392
  def delete_environment(env_id):
1393
  all_data = load_data()
1394
+ if env_id in all_data["environments"]:
1395
+ archived_env = all_data["environments"].pop(env_id)
1396
+ all_data["archive"][env_id] = archived_env
1397
  save_data(all_data)
1398
+ flash(f'Среда {env_id} была архивирована.', 'success')
1399
  else:
1400
  flash(f'Среда {env_id} не найдена.', 'error')
1401
  return redirect(url_for('admhosto'))
1402
 
1403
+ @app.route('/admhosto/restore/<env_id>', methods=['POST'])
1404
+ def restore_environment(env_id):
1405
+ all_data = load_data()
1406
+ if env_id in all_data["archive"]:
1407
+ restored_env = all_data["archive"].pop(env_id)
1408
+ all_data["environments"][env_id] = restored_env
1409
+ save_data(all_data)
1410
+ flash(f'Среда {env_id} была восстановлена.', 'success')
1411
+ else:
1412
+ flash(f'Среда {env_id} не найдена в архиве.', 'error')
1413
+ return redirect(url_for('admhosto'))
1414
+
1415
+ @app.route('/admhosto/reset_device/<env_id>', methods=['POST'])
1416
+ def reset_device(env_id):
1417
+ all_data = load_data()
1418
+ if env_id in all_data["environments"]:
1419
+ all_data["environments"][env_id]['device_token'] = None
1420
+ save_data(all_data)
1421
+ flash(f'Устройство для среды {env_id} было отвязано.', 'success')
1422
+ else:
1423
+ flash(f'Среда {env_id} не найдена.', 'error')
1424
+ return redirect(url_for('admhosto'))
1425
+
1426
+ @app.route('/admhosto/toggle_type/<env_id>', methods=['POST'])
1427
+ def toggle_type(env_id):
1428
+ all_data = load_data()
1429
+ if env_id in all_data["environments"]:
1430
+ current_type = all_data["environments"][env_id].get('type', 'closed')
1431
+ if current_type == 'closed':
1432
+ all_data["environments"][env_id]['type'] = 'open'
1433
+ flash(f'Среда {env_id} теперь открытая.', 'success')
1434
+ else:
1435
+ all_data["environments"][env_id]['type'] = 'closed'
1436
+ all_data["environments"][env_id]['device_token'] = None
1437
+ flash(f'Среда {env_id} теперь закрытая. Привязка к устройству сброшена.', 'success')
1438
+ save_data(all_data)
1439
+ else:
1440
+ flash(f'Среда {env_id} не найдена.', 'error')
1441
+ return redirect(url_for('admhosto'))
1442
+
1443
+
1444
  @app.route('/admhosto/stats/<env_id>')
1445
  def get_env_stats(env_id):
1446
  data = load_data()
1447
+ env_data = data.get("environments", {}).get(env_id)
1448
  if not env_data:
1449
  return jsonify({"error": "Среда не найдена"}), 404
1450
 
 
1476
  @app.route('/env/<env_id>')
1477
  def serve_env(env_id):
1478
  data = load_data()
1479
+ env_data = data.get("environments", {}).get(env_id)
1480
  if not env_data:
1481
  return "Среда не найдена.", 404
1482
 
 
1486
  current_log = {
1487
  "time": datetime.utcnow().isoformat(),
1488
  "ip": request.remote_addr,
1489
+ "ua": request.headers.get('User-Agent')[:150]
1490
  }
1491
 
1492
  env_data['hits'] = env_data.get('hits', 0) + 1
 
1497
  if len(env_data['logs']) > 30:
1498
  env_data['logs'] = env_data['logs'][-30:]
1499
 
1500
+ data["environments"][env_id] = env_data
1501
  save_data(data)
1502
 
1503
  if env_type == 'open':
 
1534
  else:
1535
  new_token = ''.join(random.choices(string.ascii_letters + string.digits, k=40))
1536
  env_data['device_token'] = new_token
1537
+ data["environments"][env_id] = env_data
1538
  save_data(data)
1539
 
1540
  resp = make_response(render_template_string(SYNKRIS_LOOK_TEMPLATE, keyword=keyword))
1541
+ resp.set_cookie(f'access_token_{env_id}', new_token, max_age=31536000, httponly=True, samesite='Lax')
1542
  return resp
1543
 
1544
  if __name__ == '__main__':