Kgshop commited on
Commit
9bb0f91
·
verified ·
1 Parent(s): d6d387e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -273
app.py CHANGED
@@ -22,7 +22,6 @@ app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login_synkris'
22
  DATA_FILE = 'data.json'
23
  DATA_FILE_TEMP = 'data.json.tmp'
24
  PROMPTS_FILE = 'prompts.json'
25
- PROMPTS_FILE_TEMP = 'prompts.json.tmp'
26
 
27
  SYNC_FILES = [DATA_FILE, PROMPTS_FILE]
28
 
@@ -82,7 +81,7 @@ Render the product with hyperrealistic lighting and shadows that accentuate its
82
  "street": "Dynamic street style shot in a bustling metropolis (e.g., Tokyo, New York). Cinematic, candid feel with natural urban lighting and subtle motion blur. The model should look effortlessly chic and integrated into the environment.",
83
  "lookbook": "Minimalist lookbook aesthetic. Clean, textured background (e.g., concrete, colored paper). Soft, diffused light creating a sophisticated and modern mood. Focus is entirely on the garment's form and drape.",
84
  "minimalism": "Extreme architectural minimalism. The model is set against a backdrop of brutalist concrete or stark plaster, with a single, dramatic, long shadow creating a powerful graphic composition.",
85
- "selfie": "Authentic, unposed selfie shot on a front-facing smartphone camera. The image must look genuine and unedited, capturing a spontaneous moment. Emphasize hyperrealistic skin texture with visible pores and subtle, natural imperfections. The lighting should be natural (e.g., window light, outdoor shade), not like a professional setup. Avoid perfect composition; embrace a slightly off-center, real-life feel. Candid, natural expression is crucial. The result should be indistinguishable from a real photo on a social media feed.",
86
  "creative": "Avant-garde, conceptual photoshoot. Unique props, artistic lighting, and an unconventional background are used to create a visually striking, editorial-worthy image that tells a story.",
87
  "new_year": "Festive New Year's atmosphere. Soft bokeh from fairy lights, dynamic sparkler trails, set against a beautifully decorated tree or a magical snowy landscape. Evokes warmth and celebration.",
88
  "retro": "Authentic 35mm film photograph emulation. Rich grain, warm color palette, and subtle light leaks characteristic of the 1970s or 80s. Poses and environment reflect the era.",
@@ -131,16 +130,6 @@ def load_prompts():
131
  except (FileNotFoundError, json.JSONDecodeError):
132
  return {}
133
 
134
- def save_prompts(data):
135
- try:
136
- with open(PROMPTS_FILE_TEMP, 'w', encoding='utf-8') as file:
137
- json.dump(data, file, ensure_ascii=False, indent=4)
138
- os.replace(PROMPTS_FILE_TEMP, PROMPTS_FILE)
139
- upload_db_to_hf(specific_file=PROMPTS_FILE)
140
- except Exception:
141
- if os.path.exists(PROMPTS_FILE_TEMP):
142
- os.remove(PROMPTS_FILE_TEMP)
143
-
144
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
145
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
146
  return False
@@ -307,16 +296,18 @@ ADMHOSTO_TEMPLATE = '''
307
  h1, h2 { font-weight: 600; color: var(--bg-medium); text-align: center; }
308
  h1 { margin-bottom: 25px; font-size: 1.5rem; }
309
  h2 { font-size: 1.3rem; margin-top: 40px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; margin-bottom: 20px; }
 
 
 
310
 
311
  .section { margin-bottom: 25px; }
312
 
313
  .add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; }
314
 
315
- input[type="text"], input[type="search"], textarea, select {
316
  width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem;
317
  font-family: inherit; background: #fff; -webkit-appearance: none;
318
  }
319
- textarea { resize: vertical; }
320
 
321
  .controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
322
  .radio-group { display: flex; gap: 15px; }
@@ -334,7 +325,6 @@ ADMHOSTO_TEMPLATE = '''
334
  .button.warning { background-color: var(--warning); color: #333; }
335
  .button.info { background-color: var(--info); }
336
  .button.success { background-color: var(--success); }
337
- .button.secondary { background-color: #6c757d; }
338
 
339
  .env-list { list-style: none; padding: 0; margin: 0; }
340
  .env-item {
@@ -361,8 +351,7 @@ ADMHOSTO_TEMPLATE = '''
361
  .message.error { background-color: #f8d7da; color: #721c24; }
362
 
363
  .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); }
364
- .modal-content { background-color: #fff; margin: 10% auto; padding: 25px; width: 90%; max-width: 800px; border-radius: 12px; position: relative; box-shadow: 0 5px 20px rgba(0,0,0,0.2); max-height: 80vh; display: flex; flex-direction: column; }
365
- .modal-body { overflow-y: auto; }
366
  .close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
367
  .stats-table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.85rem; }
368
  .stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
@@ -371,11 +360,6 @@ ADMHOSTO_TEMPLATE = '''
371
 
372
  .empty-list-placeholder { text-align:center; padding: 20px; color: #888; }
373
  .no-margin { margin-bottom: 0; }
374
-
375
- .style-item { border: 1px solid #ddd; border-radius: 8px; padding: 10px; margin-bottom: 10px; }
376
- .style-item p { margin: 0; font-family: monospace; font-size: 0.8rem; background: #f0f0f0; padding: 8px; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }
377
- .style-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
378
- .style-item-header span { font-weight: bold; }
379
 
380
  @media (max-width: 768px) {
381
  .env-item { grid-template-columns: 1fr; gap: 12px; }
@@ -419,16 +403,78 @@ ADMHOSTO_TEMPLATE = '''
419
  </div>
420
  </form>
421
  </div>
422
-
423
- <div class="section" style="display:flex; gap: 10px;">
424
- <input type="search" id="search-env" placeholder="🔍 Поиск сред..." style="flex-grow: 1;">
425
- <button class="button secondary" onclick="openStylesModal()"><i class="fas fa-paint-brush"></i> Стили</button>
426
  </div>
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  <div class="section">
429
- {% if active_environments %}
430
  <ul class="env-list">
431
- {% for env in active_environments %}
432
  <li class="env-item">
433
  <div class="env-details">
434
  <div class="env-header">
@@ -442,7 +488,7 @@ ADMHOSTO_TEMPLATE = '''
442
  <a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
443
  </div>
444
  <div class="env-actions">
445
- <button class="button info" onclick="openStatsModal('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
446
  <form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
447
  <button type="submit" class="button warning">
448
  <i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыть' if env.type == 'closed' else 'Закрыть' }}
@@ -460,8 +506,8 @@ ADMHOSTO_TEMPLATE = '''
460
  </li>
461
  {% endfor %}
462
  </ul>
463
- {% else %}
464
- <div class="empty-list-placeholder">Список активных сред пуст</div>
465
  {% endif %}
466
  </div>
467
 
@@ -496,33 +542,10 @@ ADMHOSTO_TEMPLATE = '''
496
 
497
  <div id="statsModal" class="modal">
498
  <div class="modal-content">
499
- <span class="close-modal" onclick="closeStatsModal()">&times;</span>
500
  <h3 id="modalTitle" style="margin-top:0; color: var(--bg-medium)">Статистика</h3>
501
  <p style="font-size: 0.8rem; color: #666;">Время: Алматы (UTC+5)</p>
502
- <div id="statsContent" class="modal-body">Загрузка...</div>
503
- </div>
504
- </div>
505
-
506
- <div id="stylesModal" class="modal">
507
- <div class="modal-content">
508
- <span class="close-modal" onclick="closeStylesModal()">&times;</span>
509
- <h3 style="margin-top:0; color: var(--bg-medium)">Управление стилями</h3>
510
- <div id="stylesContent" class="modal-body">
511
- <div id="styles-loader" style="text-align:center; padding: 20px;"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
512
- </div>
513
- <div id="addStyleFormContainer" style="padding-top:20px; border-top: 1px solid #eee; margin-top: 15px;">
514
- <h4>Добавить новый стиль</h4>
515
- <form id="addStyleForm" style="display: flex; flex-direction: column; gap: 10px;">
516
- <input type="text" id="style_key" name="style_key" placeholder="Ключ (латиницей, без пробелов)" required>
517
- <input type="text" id="style_name" name="style_name" placeholder="Название на кнопке" required>
518
- <textarea id="style_prompt" name="style_prompt" rows="4" placeholder="Промпт стиля" required></textarea>
519
- <select id="style_type" name="style_type" required>
520
- <option value="flagship_styles">Стиль для моделей (flagship)</option>
521
- <option value="object_styles">Стиль для объектов (object)</option>
522
- </select>
523
- <button type="submit" class="button primary">Добавить</button>
524
- </form>
525
- </div>
526
  </div>
527
  </div>
528
 
@@ -534,8 +557,15 @@ ADMHOSTO_TEMPLATE = '''
534
  item.style.display = text.includes(searchTerm) ? 'grid' : 'none';
535
  });
536
  });
 
 
 
 
 
 
 
537
 
538
- function openStatsModal(envId) {
539
  const modal = document.getElementById('statsModal');
540
  const content = document.getElementById('statsContent');
541
  const title = document.getElementById('modalTitle');
@@ -581,114 +611,14 @@ ADMHOSTO_TEMPLATE = '''
581
  });
582
  }
583
 
584
- function closeStatsModal() {
585
  document.getElementById('statsModal').style.display = 'none';
586
  }
587
 
588
- async function openStylesModal() {
589
- const modal = document.getElementById('stylesModal');
590
- modal.style.display = 'block';
591
- await loadStyles();
592
- }
593
-
594
- function closeStylesModal() {
595
- document.getElementById('stylesModal').style.display = 'none';
596
- }
597
-
598
- async function loadStyles() {
599
- const content = document.getElementById('stylesContent');
600
- document.getElementById('styles-loader').style.display = 'block';
601
-
602
- try {
603
- const response = await fetch('/admhosto/styles');
604
- if (!response.ok) throw new Error('Network response was not ok');
605
- const stylesData = await response.json();
606
-
607
- let html = '<h4>Стили для моделей (flagship)</h4><div id="flagship-styles-list">';
608
- for (const [key, prompt] of Object.entries(stylesData.flagship_styles)) {
609
- html += renderStyleItem(key, prompt, 'flagship_styles');
610
- }
611
- html += '</div><h4>Стили для объектов (object)</h4><div id="object-styles-list">';
612
- for (const [key, prompt] of Object.entries(stylesData.object_styles)) {
613
- html += renderStyleItem(key, prompt, 'object_styles');
614
- }
615
- html += '</div>';
616
-
617
- content.innerHTML = html;
618
- } catch (error) {
619
- content.innerHTML = '<p style="color:red">Не удалось загрузить стили.</p>';
620
- } finally {
621
- document.getElementById('styles-loader').style.display = 'none';
622
- }
623
- }
624
-
625
- function renderStyleItem(key, prompt, type) {
626
- return `
627
- <div class="style-item" id="style-${type}-${key}">
628
- <div class="style-item-header">
629
- <span>${key}</span>
630
- <button class="button danger" onclick="deleteStyle('${key}', '${type}')"><i class="fas fa-trash"></i></button>
631
- </div>
632
- <p>${prompt}</p>
633
- </div>`;
634
- }
635
-
636
- async function deleteStyle(key, type) {
637
- if (!confirm(`Вы уверены, что хотите удалить стиль '${key}'?`)) return;
638
- try {
639
- const response = await fetch('/admhosto/styles/delete', {
640
- method: 'POST',
641
- headers: { 'Content-Type': 'application/json' },
642
- body: JSON.stringify({ style_key: key, style_type: type })
643
- });
644
- const result = await response.json();
645
- if (!response.ok) throw new Error(result.error || 'Failed to delete');
646
- document.getElementById(`style-${type}-${key}`).remove();
647
- } catch(error) {
648
- alert(`Ошибка удаления: ${error.message}`);
649
- }
650
- }
651
-
652
- document.getElementById('addStyleForm').addEventListener('submit', async function(e) {
653
- e.preventDefault();
654
- const key = document.getElementById('style_key').value.trim();
655
- const name = document.getElementById('style_name').value.trim();
656
- const prompt = document.getElementById('style_prompt').value.trim();
657
- const type = document.getElementById('style_type').value;
658
-
659
- if (!key.match(/^[a-z0-9_]+$/i)) {
660
- alert('Ключ может содержать только латинские буквы, цифры и нижнее подчеркивание.');
661
- return;
662
- }
663
-
664
- try {
665
- const response = await fetch('/admhosto/styles/add', {
666
- method: 'POST',
667
- headers: { 'Content-Type': 'application/json' },
668
- body: JSON.stringify({ style_key: key, style_name: name, style_prompt: prompt, style_type: type })
669
- });
670
- const result = await response.json();
671
- if (!response.ok) throw new Error(result.error || 'Failed to add');
672
-
673
- const listId = type === 'flagship_styles' ? 'flagship-styles-list' : 'object-styles-list';
674
- const list = document.getElementById(listId);
675
- const newItemHtml = renderStyleItem(key, `${name}: ${prompt}`, type);
676
- list.insertAdjacentHTML('beforeend', newItemHtml);
677
-
678
- this.reset();
679
- } catch(error) {
680
- alert(`Ошибка добавления: ${error.message}`);
681
- }
682
- });
683
-
684
  window.onclick = function(event) {
685
- const statsModal = document.getElementById('statsModal');
686
- const stylesModal = document.getElementById('stylesModal');
687
- if (event.target == statsModal) {
688
- statsModal.style.display = 'none';
689
- }
690
- if (event.target == stylesModal) {
691
- stylesModal.style.display = 'none';
692
  }
693
  }
694
  </script>
@@ -1045,10 +975,11 @@ textarea {
1045
  <select id="nationality">
1046
  <option value="Eastern European">Восточная Европа</option>
1047
  <option value="Northern European">Скандинавская</option>
 
1048
  <option value="Asian">Азиатская</option>
1049
  <option value="Latin American">Латиноамериканская</option>
1050
- <option value="African">Африканская</option>
1051
  <option value="Middle Eastern">Ближневосточная</option>
 
1052
  <option value="Indian">Индийская</option>
1053
  <option value="Mixed Race">Смешанная</option>
1054
  </select>
@@ -1145,9 +1076,11 @@ textarea {
1145
  <select id="child_nationality">
1146
  <option value="Eastern European">Восточная Европа</option>
1147
  <option value="Northern European">Скандинавская</option>
 
1148
  <option value="Asian">Азиатская</option>
1149
- <option value="African">Африканская</option>
1150
  <option value="Middle Eastern">Ближневосточная</option>
 
1151
  <option value="Indian">Индийская</option>
1152
  <option value="Mixed Race">Смешанная</option>
1153
  </select>
@@ -1508,9 +1441,17 @@ def admhosto():
1508
  data = load_data()
1509
  active_environments = []
1510
  archived_environments = []
1511
-
 
1512
  for env_id, env_data in data.items():
1513
  if not isinstance(env_data, dict): continue
 
 
 
 
 
 
 
1514
  env_item = {
1515
  "id": env_id,
1516
  "keyword": env_data.get("keyword", "N/A"),
@@ -1524,10 +1465,28 @@ def admhosto():
1524
  else:
1525
  active_environments.append(env_item)
1526
 
1527
- active_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1528
  archived_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True)
1529
 
1530
- return render_template_string(ADMHOSTO_TEMPLATE, active_environments=active_environments, archived_environments=archived_environments)
 
 
 
 
1531
 
1532
  @app.route('/admhosto/create', methods=['POST'])
1533
  def create_environment():
@@ -1639,113 +1598,6 @@ def get_env_stats(env_id):
1639
  }
1640
  return jsonify(response_data)
1641
 
1642
- @app.route('/admhosto/styles', methods=['GET'])
1643
- def get_styles():
1644
- prompts_data = load_prompts()
1645
- # Simplified data for JS to handle, combining name from a separate dict
1646
- flagship_styles = prompts_data.get("flagship_styles", {})
1647
- object_styles = prompts_data.get("object_styles", {})
1648
- return jsonify({"flagship_styles": flagship_styles, "object_styles": object_styles})
1649
-
1650
- @app.route('/admhosto/styles/delete', methods=['POST'])
1651
- def delete_style():
1652
- data = request.get_json()
1653
- style_key = data.get('style_key')
1654
- style_type = data.get('style_type')
1655
-
1656
- if not all([style_key, style_type]):
1657
- return jsonify({"error": "Missing data"}), 400
1658
-
1659
- prompts_data = load_prompts()
1660
-
1661
- if style_type in prompts_data and style_key in prompts_data[style_type]:
1662
- del prompts_data[style_type][style_key]
1663
- save_prompts(prompts_data)
1664
- return jsonify({"success": True, "message": f"Style '{style_key}' deleted."})
1665
- else:
1666
- return jsonify({"error": "Style not found"}), 404
1667
-
1668
- @app.route('/admhosto/styles/add', methods=['POST'])
1669
- def add_style():
1670
- data = request.get_json()
1671
- style_key = data.get('style_key')
1672
- style_name = data.get('style_name')
1673
- style_prompt = data.get('style_prompt')
1674
- style_type = data.get('style_type')
1675
-
1676
- if not all([style_key, style_name, style_prompt, style_type]):
1677
- return jsonify({"error": "Missing data"}), 400
1678
-
1679
- prompts_data = load_prompts()
1680
-
1681
- if style_type not in prompts_data:
1682
- prompts_data[style_type] = {}
1683
-
1684
- if style_key in prompts_data[style_type]:
1685
- return jsonify({"error": "Style key already exists"}), 409
1686
-
1687
- # In the new logic, the prompt is just the value. The "name" is now part of the prompt.
1688
- # The user is asked for name and prompt, but the original structure only has one value.
1689
- # I'll combine them as "Name: Prompt". The JS part will need to be adjusted accordingly.
1690
- # However, looking at the JS code, it seems the `style_name` is just for the button text
1691
- # on the generator page. The prompt itself is just `style_prompt`.
1692
- # Let's keep the `prompts.json` structure simple.
1693
- # Let's re-read the request. The user wants to add name and prompt. But the original `flagship_styles` and `object_styles` are `key: prompt`.
1694
- # The `SYNKRIS_LOOK_TEMPLATE` has hardcoded style names. This is a problem.
1695
- # The request implies that the style names (on buttons) should also be manageable.
1696
- # To do this, I need to change the structure of `prompts.json` or pass more data to the template.
1697
- # A better approach would be to change the prompt value to an object like `{"name": "Студия", "prompt": "..."}`.
1698
- # But this would be a breaking change for the existing setup.
1699
- # A simpler non-breaking solution: I will just add the new style with the prompt.
1700
- # The request says "задать название и промпт окружения".
1701
- # I will modify the saved prompt to be `f"{style_name}: {style_prompt}"`. This way the name is part of the prompt.
1702
- # But wait, the `SYNKRIS_LOOK_TEMPLATE` has a hardcoded `flagshipStyles` dictionary to map keys to Russian names.
1703
- # This means I can't just add a new style and have it appear with a nice name on the generator page.
1704
- # To fully implement the request, I should modify how styles are loaded on the generator page.
1705
- # Instead of a hardcoded JS object, it should use the `prompts_data` passed from the backend.
1706
-
1707
- # I will stick to the simpler implementation given the complexity of refactoring the frontend logic.
1708
- # The user provided a form with "key", "name", and "prompt".
1709
- # I will save the prompt as-is. The `style_name` will be ignored for now as the frontend is not ready for it.
1710
- # Let's make a small adjustment, I will use the provided `style_prompt` as the value for the new `style_key`.
1711
- # And the `style_name` is used for what? Maybe the prompt should be `style_name + ': ' + style_prompt`.
1712
- # Let's re-read the user request: "задать название и промпт окружения".
1713
- # In the `SYNKRIS_LOOK_TEMPLATE`, the button text comes from `flagshipStyles` object which is hardcoded.
1714
- # Ok, I will assume the user wants to manage the prompts, and the button name is not part of the dynamic management for now.
1715
- # So I will just use `style_key` and `style_prompt`.
1716
-
1717
- # Let's reconsider. The request is clear. Maybe I should refactor the JS.
1718
- # The `flagshipStyles` object in JS is a map of `key: name`. The prompts are in `promptsData.flagship_styles`.
1719
- # So I need to modify `prompts.json` and also tell the frontend about the new name.
1720
- # The easiest way is to pass the whole `prompts_data` to the `SYNKRIS_LOOK_TEMPLATE` and build the style buttons dynamically from it.
1721
- # `prompts_data` is already passed. Let's look at the `populateStyles` JS function.
1722
- # `populateStyles('styleSelector', flagshipStyles);` It uses the hardcoded `flagshipStyles`.
1723
- # What if I change `prompts.json` to store `{"key": {"name": "Студия", "prompt": "..."}}`? No, the base prompts are also there.
1724
- # The structure is `{"flagship_styles": {"studio": "prompt here..."}}`
1725
- # What if I change it to `{"flagship_styles": {"studio": {"name": "Студия", "prompt": "..."}}}`?
1726
- # This seems like a lot of change and might break things.
1727
- # I will make a choice: I will add the style to `prompts.json`. For the name, I will not implement dynamic update on the generator page as it requires significant changes in the frontend logic which I want to avoid breaking.
1728
- # I will use the `style_key` as the name on the button on the generator page for new styles. This is a compromise.
1729
-
1730
- # Let's go with a better approach. I can modify the `prompts.json` to be more structured.
1731
- # Let's check `setup_initial_files`. It creates `flagship_styles`. I can change the value to be an object.
1732
- # For example `"studio": {"name": "Студия (профи)", "prompt": "Impeccable studio photoshoot..."}`.
1733
- # This is a good solution. But then I need to update all the places that use it.
1734
- # Let's check `getPrompt` in `SYNKRIS_LOOK_TEMPLATE`. It does `promptsData.flagship_styles[styleKey]`.
1735
- # If I change the structure, this will return an object, not a string. So I need to change it to `promptsData.flagship_styles[styleKey].prompt`.
1736
- # And `populateStyles` needs to be changed to use `promptsData.flagship_styles[key].name` for the button text.
1737
- # This seems doable. Let's go with this. I will modify `setup_initial_files` and the `SYNKRIS_LOOK_TEMPLATE` JS.
1738
-
1739
- # I've decided against this major refactoring as it increases risk of breaking the app.
1740
- # I will implement a simpler logic. When adding a new style, I will just add the key and the prompt.
1741
- # The `style_name` from the form will be ignored. The button on the generator will show the key.
1742
- # This is not perfect, but it is a safe implementation of the core request (add/delete prompts).
1743
-
1744
- prompts_data[style_type][style_key] = style_prompt
1745
- save_prompts(prompts_data)
1746
-
1747
- return jsonify({"success": True, "message": "Style added successfully."})
1748
-
1749
  @app.route('/env/<env_id>')
1750
  def serve_env(env_id):
1751
  data = load_data()
 
22
  DATA_FILE = 'data.json'
23
  DATA_FILE_TEMP = 'data.json.tmp'
24
  PROMPTS_FILE = 'prompts.json'
 
25
 
26
  SYNC_FILES = [DATA_FILE, PROMPTS_FILE]
27
 
 
81
  "street": "Dynamic street style shot in a bustling metropolis (e.g., Tokyo, New York). Cinematic, candid feel with natural urban lighting and subtle motion blur. The model should look effortlessly chic and integrated into the environment.",
82
  "lookbook": "Minimalist lookbook aesthetic. Clean, textured background (e.g., concrete, colored paper). Soft, diffused light creating a sophisticated and modern mood. Focus is entirely on the garment's form and drape.",
83
  "minimalism": "Extreme architectural minimalism. The model is set against a backdrop of brutalist concrete or stark plaster, with a single, dramatic, long shadow creating a powerful graphic composition.",
84
+ "selfie": "**UTMOST REALISM REQUIRED.** Emulate a genuine smartphone selfie. Imperfect, spontaneous composition. Natural lighting. Subtle skin textures, pores, and minor imperfections MUST be visible, avoid any hint of digital airbrushing. Include subtle lens distortion typical of a phone camera. The expression must be candid and unposed, a real captured moment. Hand holding the phone might be partially visible.",
85
  "creative": "Avant-garde, conceptual photoshoot. Unique props, artistic lighting, and an unconventional background are used to create a visually striking, editorial-worthy image that tells a story.",
86
  "new_year": "Festive New Year's atmosphere. Soft bokeh from fairy lights, dynamic sparkler trails, set against a beautifully decorated tree or a magical snowy landscape. Evokes warmth and celebration.",
87
  "retro": "Authentic 35mm film photograph emulation. Rich grain, warm color palette, and subtle light leaks characteristic of the 1970s or 80s. Poses and environment reflect the era.",
 
130
  except (FileNotFoundError, json.JSONDecodeError):
131
  return {}
132
 
 
 
 
 
 
 
 
 
 
 
133
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
134
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
135
  return False
 
296
  h1, h2 { font-weight: 600; color: var(--bg-medium); text-align: center; }
297
  h1 { margin-bottom: 25px; font-size: 1.5rem; }
298
  h2 { font-size: 1.3rem; margin-top: 40px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; margin-bottom: 20px; }
299
+ h2.collapsible { cursor: pointer; }
300
+ h2.collapsible .fa-chevron-down { transition: transform 0.3s ease; }
301
+ h2.collapsible.collapsed .fa-chevron-down { transform: rotate(-90deg); }
302
 
303
  .section { margin-bottom: 25px; }
304
 
305
  .add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; }
306
 
307
+ input[type="text"] {
308
  width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem;
309
  font-family: inherit; background: #fff; -webkit-appearance: none;
310
  }
 
311
 
312
  .controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
313
  .radio-group { display: flex; gap: 15px; }
 
325
  .button.warning { background-color: var(--warning); color: #333; }
326
  .button.info { background-color: var(--info); }
327
  .button.success { background-color: var(--success); }
 
328
 
329
  .env-list { list-style: none; padding: 0; margin: 0; }
330
  .env-item {
 
351
  .message.error { background-color: #f8d7da; color: #721c24; }
352
 
353
  .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); }
354
+ .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); }
 
355
  .close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
356
  .stats-table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.85rem; }
357
  .stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
 
360
 
361
  .empty-list-placeholder { text-align:center; padding: 20px; color: #888; }
362
  .no-margin { margin-bottom: 0; }
 
 
 
 
 
363
 
364
  @media (max-width: 768px) {
365
  .env-item { grid-template-columns: 1fr; gap: 12px; }
 
403
  </div>
404
  </form>
405
  </div>
406
+
407
+ <div class="section">
408
+ <input type="text" id="search-env" placeholder="🔍 Поиск...">
 
409
  </div>
410
 
411
+ {% if last_5_visits %}
412
+ <div class="section">
413
+ <h2><i class="fas fa-history"></i> Последние 5 заходов</h2>
414
+ <div style="overflow-x: auto;">
415
+ <table class="stats-table">
416
+ <thead><tr><th>Время</th><th>IP</th><th>Среда</th><th>User Agent</th></tr></thead>
417
+ <tbody>
418
+ {% for log in last_5_visits %}
419
+ <tr>
420
+ <td>{{ log.time.split(' ')[1] }}<br><small style="color:#999">{{ log.time.split(' ')[0] }}</small></td>
421
+ <td>{{ log.ip }}</td>
422
+ <td><strong>{{ log.env_id }}</strong></td>
423
+ <td style="max-width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="{{ log.ua }}">{{ log.ua }}</td>
424
+ </tr>
425
+ {% endfor %}
426
+ </tbody>
427
+ </table>
428
+ </div>
429
+ </div>
430
+ {% endif %}
431
+
432
+ {% if top_5_environments %}
433
+ <div class="section">
434
+ <h2 id="top5-toggle" class="collapsible"><i class="fas fa-fire"></i> Топ 5 по посещаемости <i class="fas fa-chevron-down"></i></h2>
435
+ <div id="top5-list" style="display: block;">
436
+ <ul class="env-list">
437
+ {% for env in top_5_environments %}
438
+ <li class="env-item">
439
+ <div class="env-details">
440
+ <div class="env-header">
441
+ <span class="env-id">{{ env.id }}</span>
442
+ <span class="env-type-badge type-{{ env.type }}">
443
+ {{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
444
+ </span>
445
+ <small style="color:#888">{{ env.hits }} <i class="fas fa-eye"></i></small>
446
+ </div>
447
+ <span class="env-keyword">{{ env.keyword }}</span>
448
+ <a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
449
+ </div>
450
+ <div class="env-actions">
451
+ <button class="button info" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
452
+ <form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
453
+ <button type="submit" class="button warning">
454
+ <i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыт��' if env.type == 'closed' else 'Закрыть' }}
455
+ </button>
456
+ </form>
457
+ {% if env.type == 'closed' %}
458
+ <form method="POST" action="{{ url_for('clear_user', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Отвязать пользователя от среды {{ env.id }}? Первый, кто зайдет по ссылке, станет владельцем.');">
459
+ <button type="submit" class="button success"><i class="fas fa-user-slash"></i> Сброс</button>
460
+ </form>
461
+ {% endif %}
462
+ <form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Переместить среду {{ env.id }} в архив?');">
463
+ <button type="submit" class="button danger"><i class="fas fa-archive"></i></button>
464
+ </form>
465
+ </div>
466
+ </li>
467
+ {% endfor %}
468
+ </ul>
469
+ </div>
470
+ <h2><i class="fas fa-stream"></i> Остальные активные среды</h2>
471
+ </div>
472
+ {% endif %}
473
+
474
  <div class="section">
475
+ {% if other_environments %}
476
  <ul class="env-list">
477
+ {% for env in other_environments %}
478
  <li class="env-item">
479
  <div class="env-details">
480
  <div class="env-header">
 
488
  <a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
489
  </div>
490
  <div class="env-actions">
491
+ <button class="button info" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
492
  <form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
493
  <button type="submit" class="button warning">
494
  <i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыть' if env.type == 'closed' else 'Закрыть' }}
 
506
  </li>
507
  {% endfor %}
508
  </ul>
509
+ {% elif not top_5_environments %}
510
+ <div class="empty-list-placeholder">Список активных сред пуст</div>
511
  {% endif %}
512
  </div>
513
 
 
542
 
543
  <div id="statsModal" class="modal">
544
  <div class="modal-content">
545
+ <span class="close-modal" onclick="closeStats()">&times;</span>
546
  <h3 id="modalTitle" style="margin-top:0; color: var(--bg-medium)">Статистика</h3>
547
  <p style="font-size: 0.8rem; color: #666;">Время: Алматы (UTC+5)</p>
548
+ <div id="statsContent" style="overflow-x: auto;">Загрузка...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  </div>
550
  </div>
551
 
 
557
  item.style.display = text.includes(searchTerm) ? 'grid' : 'none';
558
  });
559
  });
560
+
561
+ document.getElementById('top5-toggle').addEventListener('click', function() {
562
+ const list = document.getElementById('top5-list');
563
+ const isCollapsed = list.style.display === 'none';
564
+ list.style.display = isCollapsed ? 'block' : 'none';
565
+ this.classList.toggle('collapsed', !isCollapsed);
566
+ });
567
 
568
+ function openStats(envId) {
569
  const modal = document.getElementById('statsModal');
570
  const content = document.getElementById('statsContent');
571
  const title = document.getElementById('modalTitle');
 
611
  });
612
  }
613
 
614
+ function closeStats() {
615
  document.getElementById('statsModal').style.display = 'none';
616
  }
617
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  window.onclick = function(event) {
619
+ const modal = document.getElementById('statsModal');
620
+ if (event.target == modal) {
621
+ modal.style.display = 'none';
 
 
 
 
622
  }
623
  }
624
  </script>
 
975
  <select id="nationality">
976
  <option value="Eastern European">Восточная Европа</option>
977
  <option value="Northern European">Скандинавская</option>
978
+ <option value="Southern European">Южно-европейская</option>
979
  <option value="Asian">Азиатская</option>
980
  <option value="Latin American">Латиноамериканская</option>
 
981
  <option value="Middle Eastern">Ближневосточная</option>
982
+ <option value="African">Африканская</option>
983
  <option value="Indian">Индийская</option>
984
  <option value="Mixed Race">Смешанная</option>
985
  </select>
 
1076
  <select id="child_nationality">
1077
  <option value="Eastern European">Восточная Европа</option>
1078
  <option value="Northern European">Скандинавская</option>
1079
+ <option value="Southern European">Южно-европейская</option>
1080
  <option value="Asian">Азиатская</option>
1081
+ <option value="Latin American">Латиноамериканская</option>
1082
  <option value="Middle Eastern">Ближневосточная</option>
1083
+ <option value="African">Африканская</option>
1084
  <option value="Indian">Индийская</option>
1085
  <option value="Mixed Race">Смешанная</option>
1086
  </select>
 
1441
  data = load_data()
1442
  active_environments = []
1443
  archived_environments = []
1444
+ all_logs = []
1445
+
1446
  for env_id, env_data in data.items():
1447
  if not isinstance(env_data, dict): continue
1448
+
1449
+ if not env_data.get("archived"):
1450
+ for log in env_data.get("logs", []):
1451
+ log_entry = log.copy()
1452
+ log_entry['env_id'] = env_id
1453
+ all_logs.append(log_entry)
1454
+
1455
  env_item = {
1456
  "id": env_id,
1457
  "keyword": env_data.get("keyword", "N/A"),
 
1465
  else:
1466
  active_environments.append(env_item)
1467
 
1468
+ all_logs.sort(key=lambda x: x.get('time', ''), reverse=True)
1469
+ last_5_visits = []
1470
+ for log in all_logs[:5]:
1471
+ try:
1472
+ utc_dt = datetime.fromisoformat(log['time'])
1473
+ almaty_dt = utc_dt + timedelta(hours=5)
1474
+ log['time'] = almaty_dt.strftime('%Y-%m-%d %H:%M:%S')
1475
+ last_5_visits.append(log)
1476
+ except:
1477
+ continue
1478
+
1479
+ active_environments.sort(key=lambda x: x.get('hits', 0), reverse=True)
1480
+ top_5_environments = active_environments[:5]
1481
+ other_environments = sorted(active_environments[5:], key=lambda x: x.get('created_at', ''), reverse=True)
1482
+
1483
  archived_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True)
1484
 
1485
+ return render_template_string(ADMHOSTO_TEMPLATE,
1486
+ top_5_environments=top_5_environments,
1487
+ other_environments=other_environments,
1488
+ archived_environments=archived_environments,
1489
+ last_5_visits=last_5_visits)
1490
 
1491
  @app.route('/admhosto/create', methods=['POST'])
1492
  def create_environment():
 
1598
  }
1599
  return jsonify(response_data)
1600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1601
  @app.route('/env/<env_id>')
1602
  def serve_env(env_id):
1603
  data = load_data()