Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -84,45 +84,38 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 84 |
force_download=True,
|
| 85 |
resume_download=False
|
| 86 |
)
|
| 87 |
-
logging.info(f"Successfully downloaded {file_name} to {local_path}")
|
| 88 |
success = True
|
| 89 |
break
|
| 90 |
except RepositoryNotFoundError:
|
| 91 |
-
logging.error(f"Repository {REPO_ID} not found. Cannot download file.")
|
| 92 |
return False
|
| 93 |
except HfHubHTTPError as e:
|
| 94 |
if e.response.status_code == 404:
|
| 95 |
-
logging.warning(f"{file_name} not found in repo. Will try to create a local empty one if needed.")
|
| 96 |
if attempt == 0 and not os.path.exists(file_name):
|
| 97 |
try:
|
| 98 |
if file_name == DATA_FILE:
|
| 99 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 100 |
json.dump({}, f)
|
| 101 |
-
logging.info(f"Created empty local file: {file_name}")
|
| 102 |
except Exception as create_e:
|
| 103 |
-
|
| 104 |
success = False
|
| 105 |
break
|
| 106 |
else:
|
| 107 |
-
|
| 108 |
except requests.exceptions.RequestException as e:
|
| 109 |
-
|
| 110 |
except Exception as e:
|
| 111 |
-
|
| 112 |
|
| 113 |
if attempt < retries:
|
| 114 |
-
logging.info(f"Retrying download of {file_name} in {delay} seconds...")
|
| 115 |
time.sleep(delay)
|
| 116 |
|
| 117 |
if not success:
|
| 118 |
-
logging.error(f"Failed to download {file_name} after {retries+1} attempts.")
|
| 119 |
all_successful = False
|
| 120 |
|
| 121 |
return all_successful
|
| 122 |
|
| 123 |
def upload_db_to_hf(specific_file=None):
|
| 124 |
if not HF_TOKEN_WRITE:
|
| 125 |
-
logging.warning("HF_TOKEN_WRITE not set. Skipping upload to Hugging Face.")
|
| 126 |
return
|
| 127 |
try:
|
| 128 |
api = HfApi()
|
|
@@ -139,19 +132,17 @@ def upload_db_to_hf(specific_file=None):
|
|
| 139 |
token=HF_TOKEN_WRITE,
|
| 140 |
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 141 |
)
|
| 142 |
-
logging.info(f"Successfully uploaded {file_name} to Hugging Face.")
|
| 143 |
except Exception as e:
|
| 144 |
-
|
| 145 |
else:
|
| 146 |
-
|
| 147 |
except Exception as e:
|
| 148 |
-
|
| 149 |
|
| 150 |
def periodic_backup():
|
| 151 |
backup_interval = 1800
|
| 152 |
while True:
|
| 153 |
time.sleep(backup_interval)
|
| 154 |
-
logging.info("Starting periodic backup...")
|
| 155 |
upload_db_to_hf()
|
| 156 |
|
| 157 |
def load_data():
|
|
@@ -159,19 +150,15 @@ def load_data():
|
|
| 159 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 160 |
data = json.load(f)
|
| 161 |
if not isinstance(data, dict):
|
| 162 |
-
logging.warning(f"{DATA_FILE} does not contain a dictionary. Resetting to empty.")
|
| 163 |
data = {}
|
| 164 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 165 |
-
logging.warning(f"{DATA_FILE} not found or corrupted. Attempting to download from Hugging Face.")
|
| 166 |
if download_db_from_hf(specific_file=DATA_FILE):
|
| 167 |
try:
|
| 168 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 169 |
data = json.load(f)
|
| 170 |
if not isinstance(data, dict):
|
| 171 |
-
logging.warning(f"Downloaded {DATA_FILE} is not a dictionary. Resetting to empty.")
|
| 172 |
data = {}
|
| 173 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 174 |
-
logging.error(f"Failed to load {DATA_FILE} even after download. Using empty data.")
|
| 175 |
data = {}
|
| 176 |
else:
|
| 177 |
data = {}
|
|
@@ -181,11 +168,9 @@ def save_data(data):
|
|
| 181 |
try:
|
| 182 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 183 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
| 184 |
-
|
| 185 |
-
upload_thread.daemon = True
|
| 186 |
-
upload_thread.start()
|
| 187 |
except Exception as e:
|
| 188 |
-
|
| 189 |
|
| 190 |
def is_chat_active(env_id):
|
| 191 |
data = get_env_data(env_id)
|
|
@@ -266,14 +251,11 @@ def save_env_data(env_id, env_data):
|
|
| 266 |
|
| 267 |
def configure_gemini():
|
| 268 |
if not GOOGLE_API_KEY:
|
| 269 |
-
logging.warning("GOOGLE_API_KEY is not set. Gemini AI features will be disabled.")
|
| 270 |
return False
|
| 271 |
try:
|
| 272 |
genai.configure(api_key=GOOGLE_API_KEY)
|
| 273 |
-
logging.info("Google Gemini AI configured successfully.")
|
| 274 |
return True
|
| 275 |
except Exception as e:
|
| 276 |
-
logging.error(f"Failed to configure Google Gemini AI: {e}")
|
| 277 |
return False
|
| 278 |
|
| 279 |
def generate_ai_description_from_image(image_data, language):
|
|
@@ -286,7 +268,6 @@ def generate_ai_description_from_image(image_data, language):
|
|
| 286 |
image_stream = io.BytesIO(image_data)
|
| 287 |
image = Image.open(image_stream).convert('RGB')
|
| 288 |
except Exception as e:
|
| 289 |
-
logging.error(f"Error processing image for AI description: {e}")
|
| 290 |
raise ValueError(f"Не удалось обработать изображение. Убедитесь, что это действительный файл изображения.")
|
| 291 |
|
| 292 |
base_prompt = "Напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами и 25 тематических хэштегов с ключевыми словами разных вариантов, чтобы мои клиенты могли найти меня в поиске Instagram, Google и т.д. по ключевым словам. Пост пиши исключительно под товар, который на фото, без адресов и номеров телефона."
|
|
@@ -304,7 +285,7 @@ def generate_ai_description_from_image(image_data, language):
|
|
| 304 |
final_prompt = f"{base_prompt}{lang_suffix}"
|
| 305 |
|
| 306 |
try:
|
| 307 |
-
model = genai.GenerativeModel('gemma-
|
| 308 |
|
| 309 |
response = model.generate_content([final_prompt, image])
|
| 310 |
|
|
@@ -318,13 +299,12 @@ def generate_ai_description_from_image(image_data, language):
|
|
| 318 |
return response.text
|
| 319 |
|
| 320 |
except Exception as e:
|
| 321 |
-
logging.error(f"Error during AI content generation: {e}")
|
| 322 |
if "API key not valid" in str(e):
|
| 323 |
raise ValueError("Внутренняя ошибка конфигурации API.")
|
| 324 |
elif " Billing account not found" in str(e):
|
| 325 |
raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
|
| 326 |
elif "Could not find model" in str(e):
|
| 327 |
-
raise ValueError(f"Модель '
|
| 328 |
elif "resource has been exhausted" in str(e).lower():
|
| 329 |
raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
|
| 330 |
elif "content has been blocked" in str(e).lower():
|
|
@@ -394,7 +374,7 @@ def generate_chat_response(message, chat_history_from_client, env_id):
|
|
| 394 |
response = None
|
| 395 |
|
| 396 |
try:
|
| 397 |
-
model = genai.GenerativeModel('gemma-
|
| 398 |
|
| 399 |
model_chat_history_for_gemini = [
|
| 400 |
{'role': 'user', 'parts': [{'text': system_instruction_content}]}
|
|
@@ -424,7 +404,6 @@ def generate_chat_response(message, chat_history_from_client, env_id):
|
|
| 424 |
return generated_text
|
| 425 |
|
| 426 |
except Exception as e:
|
| 427 |
-
logging.error(f"Error generating chat response: {e}")
|
| 428 |
if "API key not valid" in str(e):
|
| 429 |
return "Внутренняя ошибка конфигурации API."
|
| 430 |
elif " Billing account not found" in str(e):
|
|
@@ -465,6 +444,15 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 465 |
h1 { font-weight: 600; color: var(--bg-medium); margin-bottom: 25px; text-align: center; }
|
| 466 |
.section { margin-bottom: 30px; }
|
| 467 |
.add-env-form { margin-bottom: 20px; text-align: center; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
.button { padding: 10px 18px; border: none; border-radius: 6px; background-color: var(--accent); color: var(--text-on-accent); font-weight: 600; cursor: pointer; transition: background-color 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 5px; }
|
| 469 |
.button:hover { background-color: var(--accent-hover); }
|
| 470 |
.button:disabled { background-color: #ccc; cursor: not-allowed; }
|
|
@@ -501,10 +489,7 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 501 |
</div>
|
| 502 |
|
| 503 |
<div class="section">
|
| 504 |
-
|
| 505 |
-
<i class="fas fa-search" style="position: absolute; top: 50%; left: 15px; transform: translateY(-50%); color: #aaa;"></i>
|
| 506 |
-
<input type="text" id="env-search-input" onkeyup="filterEnvironments()" placeholder="Поиск по ID среды..." style="width: 100%; padding: 10px 15px 10px 40px; border-radius: 6px; border: 1px solid #ddd; font-size: 1rem; box-sizing: border-box;">
|
| 507 |
-
</div>
|
| 508 |
</div>
|
| 509 |
|
| 510 |
<div class="section">
|
|
@@ -549,40 +534,18 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 549 |
</div>
|
| 550 |
</div>
|
| 551 |
<script>
|
| 552 |
-
function
|
| 553 |
-
const
|
| 554 |
-
const
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
if (envIdElement) {
|
| 562 |
-
const txtValue = envIdElement.textContent || envIdElement.innerText;
|
| 563 |
-
if (txtValue.toLowerCase().indexOf(filter) > -1) {
|
| 564 |
-
items[i].style.display = "";
|
| 565 |
-
found = true;
|
| 566 |
-
} else {
|
| 567 |
-
items[i].style.display = "none";
|
| 568 |
-
}
|
| 569 |
-
}
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
let noResultsMsg = document.getElementById('no-results');
|
| 573 |
-
if (!found && filter !== '') {
|
| 574 |
-
if (!noResultsMsg) {
|
| 575 |
-
noResultsMsg = document.createElement('p');
|
| 576 |
-
noResultsMsg.id = 'no-results';
|
| 577 |
-
noResultsMsg.textContent = 'Среда с таким ID не найдена.';
|
| 578 |
-
noResultsMsg.style.textAlign = 'center';
|
| 579 |
-
noResultsMsg.style.marginTop = '20px';
|
| 580 |
-
envList.parentNode.appendChild(noResultsMsg);
|
| 581 |
}
|
| 582 |
-
}
|
| 583 |
-
|
| 584 |
-
}
|
| 585 |
-
}
|
| 586 |
</script>
|
| 587 |
</body>
|
| 588 |
</html>
|
|
@@ -2119,13 +2082,12 @@ ADMIN_TEMPLATE = '''
|
|
| 2119 |
.item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
|
| 2120 |
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
| 2121 |
.edit-form-container { margin-top: 15px; padding: 20px; background: #E0F2F1; border: 1px dashed #B2DFDB; border-radius: 6px; display: none; }
|
| 2122 |
-
details { background-color: #fdfdff; border-radius: 8px; margin-bottom:
|
| 2123 |
-
details:
|
| 2124 |
-
details > summary {
|
| 2125 |
-
details:not(.section) > summary { border-bottom: 1px solid #e0e0e0; }
|
| 2126 |
-
details[open] > summary { border-bottom: 1px solid #e0e0e0; }
|
| 2127 |
details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: var(--bg-medium); }
|
| 2128 |
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
|
|
|
|
| 2129 |
details .form-content { padding: 20px; }
|
| 2130 |
.color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
| 2131 |
.color-input-group input { flex-grow: 1; margin: 0; }
|
|
@@ -2187,8 +2149,8 @@ ADMIN_TEMPLATE = '''
|
|
| 2187 |
<p>Чат <strong>активен</strong>. Срок действия истекает: <strong class="{{ 'status-indicator expires-soon' if chat_status.expires_soon else '' }}">{{ chat_status.expires_date }}</strong></p>
|
| 2188 |
{% else %}
|
| 2189 |
<p>Чат <strong>неактивен</strong>. {% if chat_status.expires_date %}Срок действия истек: {{ chat_status.expires_date }}{% endif %}</p>
|
| 2190 |
-
<p>Для активации
|
| 2191 |
-
<a href="https://wa.me/77472479197" class="button add-button"
|
| 2192 |
{% endif %}
|
| 2193 |
</div>
|
| 2194 |
|
|
@@ -2206,9 +2168,8 @@ ADMIN_TEMPLATE = '''
|
|
| 2206 |
</div>
|
| 2207 |
|
| 2208 |
<div class="section">
|
| 2209 |
-
<h2><i class="fas fa-cog"></i> Настройки магазина и чата</h2>
|
| 2210 |
<details>
|
| 2211 |
-
<summary><i class="fas fa-
|
| 2212 |
<div class="form-content">
|
| 2213 |
<form method="POST" enctype="multipart/form-data">
|
| 2214 |
<input type="hidden" name="action" value="update_settings">
|
|
@@ -2251,19 +2212,21 @@ ADMIN_TEMPLATE = '''
|
|
| 2251 |
|
| 2252 |
<div class="section">
|
| 2253 |
<details>
|
| 2254 |
-
<summary><
|
| 2255 |
-
<div class="
|
| 2256 |
-
|
| 2257 |
-
{%
|
| 2258 |
-
|
| 2259 |
-
<
|
| 2260 |
-
|
| 2261 |
-
|
| 2262 |
-
|
| 2263 |
-
|
| 2264 |
-
|
| 2265 |
-
|
| 2266 |
-
|
|
|
|
|
|
|
| 2267 |
</div>
|
| 2268 |
</details>
|
| 2269 |
</div>
|
|
@@ -2635,7 +2598,7 @@ ADMIN_TEMPLATE = '''
|
|
| 2635 |
modalBody.innerHTML = 'Загрузка...';
|
| 2636 |
openModal('chat-modal');
|
| 2637 |
try {
|
| 2638 |
-
const response = await fetch(`/${
|
| 2639 |
if(!response.ok) throw new Error('Chat not found');
|
| 2640 |
const chatHistory = await response.json();
|
| 2641 |
|
|
@@ -2889,7 +2852,6 @@ def create_order(env_id):
|
|
| 2889 |
})
|
| 2890 |
total_price += price * quantity
|
| 2891 |
except (ValueError, TypeError) as e:
|
| 2892 |
-
logging.error(f"Invalid cart item format: {e}")
|
| 2893 |
return jsonify({"error": "Неверная цена или количество в товаре."}), 400
|
| 2894 |
|
| 2895 |
order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
|
|
@@ -2914,7 +2876,6 @@ def create_order(env_id):
|
|
| 2914 |
return jsonify({"order_id": order_id}), 201
|
| 2915 |
|
| 2916 |
except Exception as e:
|
| 2917 |
-
logging.error(f"Server error while creating order: {e}")
|
| 2918 |
return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
|
| 2919 |
|
| 2920 |
@app.route('/<env_id>/order/<order_id>')
|
|
@@ -3002,8 +2963,8 @@ def admin(env_id):
|
|
| 3002 |
if old_avatar:
|
| 3003 |
try:
|
| 3004 |
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 3005 |
-
except Exception
|
| 3006 |
-
|
| 3007 |
|
| 3008 |
ext = os.path.splitext(avatar_file.filename)[1].lower()
|
| 3009 |
avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}"
|
|
@@ -3023,7 +2984,6 @@ def admin(env_id):
|
|
| 3023 |
flash("Аватар чата успешно обновлен.", 'success')
|
| 3024 |
except Exception as e:
|
| 3025 |
flash(f"Ошибка при загрузке аватара: {e}", 'error')
|
| 3026 |
-
logging.error(f"Error uploading avatar: {e}")
|
| 3027 |
else:
|
| 3028 |
flash("HF_TOKEN (write) не настроен. Аватар не был загружен.", "warning")
|
| 3029 |
|
|
@@ -3087,7 +3047,6 @@ def admin(env_id):
|
|
| 3087 |
uploaded_count += 1
|
| 3088 |
except Exception as e:
|
| 3089 |
flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
|
| 3090 |
-
logging.error(f"Error uploading photo {photo.filename}: {e}")
|
| 3091 |
if os.path.exists(temp_path):
|
| 3092 |
try: os.remove(temp_path)
|
| 3093 |
except OSError: pass
|
|
@@ -3097,7 +3056,7 @@ def admin(env_id):
|
|
| 3097 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 3098 |
os.rmdir(uploads_dir)
|
| 3099 |
except OSError as e:
|
| 3100 |
-
|
| 3101 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 3102 |
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 3103 |
|
|
@@ -3171,7 +3130,6 @@ def admin(env_id):
|
|
| 3171 |
uploaded_count += 1
|
| 3172 |
except Exception as e:
|
| 3173 |
flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
|
| 3174 |
-
logging.error(f"Error uploading new photo {photo.filename}: {e}")
|
| 3175 |
if os.path.exists(temp_path):
|
| 3176 |
try: os.remove(temp_path)
|
| 3177 |
except OSError: pass
|
|
@@ -3179,7 +3137,7 @@ def admin(env_id):
|
|
| 3179 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 3180 |
os.rmdir(uploads_dir)
|
| 3181 |
except OSError as e:
|
| 3182 |
-
|
| 3183 |
|
| 3184 |
if new_photos_list:
|
| 3185 |
old_photos = product_to_edit.get('photos', [])
|
|
@@ -3195,7 +3153,6 @@ def admin(env_id):
|
|
| 3195 |
)
|
| 3196 |
except Exception as e:
|
| 3197 |
flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
|
| 3198 |
-
logging.warning(f"Could not delete old photos for product {product_to_edit['name']}: {e}")
|
| 3199 |
product_to_edit['photos'] = new_photos_list
|
| 3200 |
flash("Фотографии товара успешно обновлены.", "success")
|
| 3201 |
elif uploaded_count == 0 and any(f.filename for f in photos_files):
|
|
@@ -3230,7 +3187,6 @@ def admin(env_id):
|
|
| 3230 |
)
|
| 3231 |
except Exception as e:
|
| 3232 |
flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
|
| 3233 |
-
logging.warning(f"Could not delete photos for product {product_name}: {e}")
|
| 3234 |
elif photos_to_delete and not HF_TOKEN_WRITE:
|
| 3235 |
flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
|
| 3236 |
|
|
@@ -3244,7 +3200,6 @@ def admin(env_id):
|
|
| 3244 |
return redirect(url_for('admin', env_id=env_id))
|
| 3245 |
|
| 3246 |
except Exception as e:
|
| 3247 |
-
logging.error(f"An internal error occurred in admin POST action '{action}': {e}")
|
| 3248 |
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
|
| 3249 |
return redirect(url_for('admin', env_id=env_id))
|
| 3250 |
|
|
@@ -3305,7 +3260,6 @@ def handle_generate_description_ai():
|
|
| 3305 |
except ValueError as ve:
|
| 3306 |
return jsonify({"error": str(ve)}), 400
|
| 3307 |
except Exception as e:
|
| 3308 |
-
logging.error(f"Internal server error during AI description generation: {e}")
|
| 3309 |
return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
|
| 3310 |
|
| 3311 |
@app.route('/<env_id>/chat_with_ai', methods=['POST'])
|
|
@@ -3339,7 +3293,6 @@ def handle_chat_with_ai(env_id):
|
|
| 3339 |
|
| 3340 |
return jsonify({"text": ai_response_text})
|
| 3341 |
except Exception as e:
|
| 3342 |
-
logging.error(f"Error in chat handler: {e}")
|
| 3343 |
return jsonify({"error": f"Ошибка чата: {e}"}), 500
|
| 3344 |
|
| 3345 |
@app.route('/<env_id>/chat')
|
|
@@ -3381,7 +3334,6 @@ def force_upload(env_id):
|
|
| 3381 |
flash("Данные успешно загружены на Hugging Face.", 'success')
|
| 3382 |
except Exception as e:
|
| 3383 |
flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
|
| 3384 |
-
logging.error(f"Error during forced upload: {e}")
|
| 3385 |
return redirect(url_for('admin', env_id=env_id))
|
| 3386 |
|
| 3387 |
@app.route('/<env_id>/force_download', methods=['POST'])
|
|
@@ -3393,7 +3345,6 @@ def force_download(env_id):
|
|
| 3393 |
flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error')
|
| 3394 |
except Exception as e:
|
| 3395 |
flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
|
| 3396 |
-
logging.error(f"Error during forced download: {e}")
|
| 3397 |
return redirect(url_for('admin', env_id=env_id))
|
| 3398 |
|
| 3399 |
if __name__ == '__main__':
|
|
@@ -3404,9 +3355,8 @@ if __name__ == '__main__':
|
|
| 3404 |
if HF_TOKEN_WRITE:
|
| 3405 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 3406 |
backup_thread.start()
|
| 3407 |
-
logging.info("Periodic backup thread started.")
|
| 3408 |
else:
|
| 3409 |
-
|
| 3410 |
|
| 3411 |
port = int(os.environ.get('PORT', 7860))
|
| 3412 |
app.run(debug=False, host='0.0.0.0', port=port)
|
|
|
|
| 84 |
force_download=True,
|
| 85 |
resume_download=False
|
| 86 |
)
|
|
|
|
| 87 |
success = True
|
| 88 |
break
|
| 89 |
except RepositoryNotFoundError:
|
|
|
|
| 90 |
return False
|
| 91 |
except HfHubHTTPError as e:
|
| 92 |
if e.response.status_code == 404:
|
|
|
|
| 93 |
if attempt == 0 and not os.path.exists(file_name):
|
| 94 |
try:
|
| 95 |
if file_name == DATA_FILE:
|
| 96 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 97 |
json.dump({}, f)
|
|
|
|
| 98 |
except Exception as create_e:
|
| 99 |
+
pass
|
| 100 |
success = False
|
| 101 |
break
|
| 102 |
else:
|
| 103 |
+
pass
|
| 104 |
except requests.exceptions.RequestException as e:
|
| 105 |
+
pass
|
| 106 |
except Exception as e:
|
| 107 |
+
pass
|
| 108 |
|
| 109 |
if attempt < retries:
|
|
|
|
| 110 |
time.sleep(delay)
|
| 111 |
|
| 112 |
if not success:
|
|
|
|
| 113 |
all_successful = False
|
| 114 |
|
| 115 |
return all_successful
|
| 116 |
|
| 117 |
def upload_db_to_hf(specific_file=None):
|
| 118 |
if not HF_TOKEN_WRITE:
|
|
|
|
| 119 |
return
|
| 120 |
try:
|
| 121 |
api = HfApi()
|
|
|
|
| 132 |
token=HF_TOKEN_WRITE,
|
| 133 |
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 134 |
)
|
|
|
|
| 135 |
except Exception as e:
|
| 136 |
+
pass
|
| 137 |
else:
|
| 138 |
+
pass
|
| 139 |
except Exception as e:
|
| 140 |
+
pass
|
| 141 |
|
| 142 |
def periodic_backup():
|
| 143 |
backup_interval = 1800
|
| 144 |
while True:
|
| 145 |
time.sleep(backup_interval)
|
|
|
|
| 146 |
upload_db_to_hf()
|
| 147 |
|
| 148 |
def load_data():
|
|
|
|
| 150 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 151 |
data = json.load(f)
|
| 152 |
if not isinstance(data, dict):
|
|
|
|
| 153 |
data = {}
|
| 154 |
except (FileNotFoundError, json.JSONDecodeError):
|
|
|
|
| 155 |
if download_db_from_hf(specific_file=DATA_FILE):
|
| 156 |
try:
|
| 157 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 158 |
data = json.load(f)
|
| 159 |
if not isinstance(data, dict):
|
|
|
|
| 160 |
data = {}
|
| 161 |
except (FileNotFoundError, json.JSONDecodeError):
|
|
|
|
| 162 |
data = {}
|
| 163 |
else:
|
| 164 |
data = {}
|
|
|
|
| 168 |
try:
|
| 169 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 170 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
| 171 |
+
upload_db_to_hf(specific_file=DATA_FILE)
|
|
|
|
|
|
|
| 172 |
except Exception as e:
|
| 173 |
+
pass
|
| 174 |
|
| 175 |
def is_chat_active(env_id):
|
| 176 |
data = get_env_data(env_id)
|
|
|
|
| 251 |
|
| 252 |
def configure_gemini():
|
| 253 |
if not GOOGLE_API_KEY:
|
|
|
|
| 254 |
return False
|
| 255 |
try:
|
| 256 |
genai.configure(api_key=GOOGLE_API_KEY)
|
|
|
|
| 257 |
return True
|
| 258 |
except Exception as e:
|
|
|
|
| 259 |
return False
|
| 260 |
|
| 261 |
def generate_ai_description_from_image(image_data, language):
|
|
|
|
| 268 |
image_stream = io.BytesIO(image_data)
|
| 269 |
image = Image.open(image_stream).convert('RGB')
|
| 270 |
except Exception as e:
|
|
|
|
| 271 |
raise ValueError(f"Не удалось обработать изображение. Убедитесь, что это действительный файл изображения.")
|
| 272 |
|
| 273 |
base_prompt = "Напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами и 25 тематических хэштегов с ключевыми словами разных вариантов, чтобы мои клиенты могли найти меня в поиске Instagram, Google и т.д. по ключевым словам. Пост пиши исключительно под товар, который на фото, без адресов и номеров телефона."
|
|
|
|
| 285 |
final_prompt = f"{base_prompt}{lang_suffix}"
|
| 286 |
|
| 287 |
try:
|
| 288 |
+
model = genai.GenerativeModel('gemma-3-27b-it')
|
| 289 |
|
| 290 |
response = model.generate_content([final_prompt, image])
|
| 291 |
|
|
|
|
| 299 |
return response.text
|
| 300 |
|
| 301 |
except Exception as e:
|
|
|
|
| 302 |
if "API key not valid" in str(e):
|
| 303 |
raise ValueError("Внутренняя ошибка конфигурации API.")
|
| 304 |
elif " Billing account not found" in str(e):
|
| 305 |
raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
|
| 306 |
elif "Could not find model" in str(e):
|
| 307 |
+
raise ValueError(f"Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна.")
|
| 308 |
elif "resource has been exhausted" in str(e).lower():
|
| 309 |
raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
|
| 310 |
elif "content has been blocked" in str(e).lower():
|
|
|
|
| 374 |
response = None
|
| 375 |
|
| 376 |
try:
|
| 377 |
+
model = genai.GenerativeModel('gemma-3-27b-it')
|
| 378 |
|
| 379 |
model_chat_history_for_gemini = [
|
| 380 |
{'role': 'user', 'parts': [{'text': system_instruction_content}]}
|
|
|
|
| 404 |
return generated_text
|
| 405 |
|
| 406 |
except Exception as e:
|
|
|
|
| 407 |
if "API key not valid" in str(e):
|
| 408 |
return "Внутренняя ошибка конфигурации API."
|
| 409 |
elif " Billing account not found" in str(e):
|
|
|
|
| 444 |
h1 { font-weight: 600; color: var(--bg-medium); margin-bottom: 25px; text-align: center; }
|
| 445 |
.section { margin-bottom: 30px; }
|
| 446 |
.add-env-form { margin-bottom: 20px; text-align: center; }
|
| 447 |
+
#search-env {
|
| 448 |
+
width: 100%;
|
| 449 |
+
padding: 10px;
|
| 450 |
+
border: 1px solid #ddd;
|
| 451 |
+
border-radius: 6px;
|
| 452 |
+
box-sizing: border-box;
|
| 453 |
+
font-size: 1rem;
|
| 454 |
+
font-family: 'Montserrat', sans-serif;
|
| 455 |
+
}
|
| 456 |
.button { padding: 10px 18px; border: none; border-radius: 6px; background-color: var(--accent); color: var(--text-on-accent); font-weight: 600; cursor: pointer; transition: background-color 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 5px; }
|
| 457 |
.button:hover { background-color: var(--accent-hover); }
|
| 458 |
.button:disabled { background-color: #ccc; cursor: not-allowed; }
|
|
|
|
| 489 |
</div>
|
| 490 |
|
| 491 |
<div class="section">
|
| 492 |
+
<input type="text" id="search-env" placeholder="Поиск по ID среды...">
|
|
|
|
|
|
|
|
|
|
| 493 |
</div>
|
| 494 |
|
| 495 |
<div class="section">
|
|
|
|
| 534 |
</div>
|
| 535 |
</div>
|
| 536 |
<script>
|
| 537 |
+
document.getElementById('search-env').addEventListener('input', function() {
|
| 538 |
+
const searchTerm = this.value.toLowerCase().trim();
|
| 539 |
+
const envItems = document.querySelectorAll('.env-item');
|
| 540 |
+
envItems.forEach(item => {
|
| 541 |
+
const envId = item.querySelector('.env-id').textContent.toLowerCase();
|
| 542 |
+
if (envId.includes(searchTerm)) {
|
| 543 |
+
item.style.display = 'grid';
|
| 544 |
+
} else {
|
| 545 |
+
item.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
}
|
| 547 |
+
});
|
| 548 |
+
});
|
|
|
|
|
|
|
| 549 |
</script>
|
| 550 |
</body>
|
| 551 |
</html>
|
|
|
|
| 2082 |
.item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
|
| 2083 |
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
| 2084 |
.edit-form-container { margin-top: 15px; padding: 20px; background: #E0F2F1; border: 1px dashed #B2DFDB; border-radius: 6px; display: none; }
|
| 2085 |
+
details { background-color: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
|
| 2086 |
+
details > summary { cursor: pointer; font-weight: 600; color: var(--bg-medium); display: block; padding: 15px; list-style: none; position: relative; font-size: 1.2rem; }
|
| 2087 |
+
details > summary:hover { background-color: #fafafa; }
|
|
|
|
|
|
|
| 2088 |
details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: var(--bg-medium); }
|
| 2089 |
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
|
| 2090 |
+
details[open] > summary { border-bottom: 1px solid #e0e0e0; }
|
| 2091 |
details .form-content { padding: 20px; }
|
| 2092 |
.color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
| 2093 |
.color-input-group input { flex-grow: 1; margin: 0; }
|
|
|
|
| 2149 |
<p>Чат <strong>активен</strong>. Срок действия истекает: <strong class="{{ 'status-indicator expires-soon' if chat_status.expires_soon else '' }}">{{ chat_status.expires_date }}</strong></p>
|
| 2150 |
{% else %}
|
| 2151 |
<p>Чат <strong>неактивен</strong>. {% if chat_status.expires_date %}Срок действия истек: {{ chat_status.expires_date }}{% endif %}</p>
|
| 2152 |
+
<p style="color: var(--danger);">Для активации чат-бота, пожалуйста, свяжитесь с администратором.</p>
|
| 2153 |
+
<a href="https://wa.me/77472479197" target="_blank" class="button add-button" style="margin-top: 10px;"><i class="fab fa-whatsapp"></i> Написать администратору</a>
|
| 2154 |
{% endif %}
|
| 2155 |
</div>
|
| 2156 |
|
|
|
|
| 2168 |
</div>
|
| 2169 |
|
| 2170 |
<div class="section">
|
|
|
|
| 2171 |
<details>
|
| 2172 |
+
<summary><i class="fas fa-cog"></i> Настройки магазина и чата</summary>
|
| 2173 |
<div class="form-content">
|
| 2174 |
<form method="POST" enctype="multipart/form-data">
|
| 2175 |
<input type="hidden" name="action" value="update_settings">
|
|
|
|
| 2212 |
|
| 2213 |
<div class="section">
|
| 2214 |
<details>
|
| 2215 |
+
<summary><i class="fas fa-comments"></i> Диалоги с {{ settings.chat_name }}</summary>
|
| 2216 |
+
<div class="form-content">
|
| 2217 |
+
<div class="item-list">
|
| 2218 |
+
{% if chats %}
|
| 2219 |
+
{% for chat_id, chat_data in chats.items()|sort(reverse=True) %}
|
| 2220 |
+
<div class="chat-log-item" onclick="viewChat('{{ chat_id }}')">
|
| 2221 |
+
<strong>ID Диалога:</strong> {{ chat_id }}
|
| 2222 |
+
<br>
|
| 2223 |
+
<small>Сообщений: {{ chat_data|length }} | Последнее сообщение: {{ chat_data[-1].timestamp if chat_data and 'timestamp' in chat_data[-1] else 'N/A' }}</small>
|
| 2224 |
+
</div>
|
| 2225 |
+
{% endfor %}
|
| 2226 |
+
{% else %}
|
| 2227 |
+
<p>Пока не было ни одного диалога.</p>
|
| 2228 |
+
{% endif %}
|
| 2229 |
+
</div>
|
| 2230 |
</div>
|
| 2231 |
</details>
|
| 2232 |
</div>
|
|
|
|
| 2598 |
modalBody.innerHTML = 'Загрузка...';
|
| 2599 |
openModal('chat-modal');
|
| 2600 |
try {
|
| 2601 |
+
const response = await fetch(`/${env_id}/get_chat/${chatId}`);
|
| 2602 |
if(!response.ok) throw new Error('Chat not found');
|
| 2603 |
const chatHistory = await response.json();
|
| 2604 |
|
|
|
|
| 2852 |
})
|
| 2853 |
total_price += price * quantity
|
| 2854 |
except (ValueError, TypeError) as e:
|
|
|
|
| 2855 |
return jsonify({"error": "Неверная цена или количество в товаре."}), 400
|
| 2856 |
|
| 2857 |
order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
|
|
|
|
| 2876 |
return jsonify({"order_id": order_id}), 201
|
| 2877 |
|
| 2878 |
except Exception as e:
|
|
|
|
| 2879 |
return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
|
| 2880 |
|
| 2881 |
@app.route('/<env_id>/order/<order_id>')
|
|
|
|
| 2963 |
if old_avatar:
|
| 2964 |
try:
|
| 2965 |
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 2966 |
+
except Exception:
|
| 2967 |
+
pass
|
| 2968 |
|
| 2969 |
ext = os.path.splitext(avatar_file.filename)[1].lower()
|
| 2970 |
avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}"
|
|
|
|
| 2984 |
flash("Аватар чата успешно обновлен.", 'success')
|
| 2985 |
except Exception as e:
|
| 2986 |
flash(f"Ошибка при загрузке аватара: {e}", 'error')
|
|
|
|
| 2987 |
else:
|
| 2988 |
flash("HF_TOKEN (write) не настроен. Аватар не был загружен.", "warning")
|
| 2989 |
|
|
|
|
| 3047 |
uploaded_count += 1
|
| 3048 |
except Exception as e:
|
| 3049 |
flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
|
|
|
|
| 3050 |
if os.path.exists(temp_path):
|
| 3051 |
try: os.remove(temp_path)
|
| 3052 |
except OSError: pass
|
|
|
|
| 3056 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 3057 |
os.rmdir(uploads_dir)
|
| 3058 |
except OSError as e:
|
| 3059 |
+
pass
|
| 3060 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 3061 |
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 3062 |
|
|
|
|
| 3130 |
uploaded_count += 1
|
| 3131 |
except Exception as e:
|
| 3132 |
flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
|
|
|
|
| 3133 |
if os.path.exists(temp_path):
|
| 3134 |
try: os.remove(temp_path)
|
| 3135 |
except OSError: pass
|
|
|
|
| 3137 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 3138 |
os.rmdir(uploads_dir)
|
| 3139 |
except OSError as e:
|
| 3140 |
+
pass
|
| 3141 |
|
| 3142 |
if new_photos_list:
|
| 3143 |
old_photos = product_to_edit.get('photos', [])
|
|
|
|
| 3153 |
)
|
| 3154 |
except Exception as e:
|
| 3155 |
flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
|
|
|
|
| 3156 |
product_to_edit['photos'] = new_photos_list
|
| 3157 |
flash("Фотографии товара успешно обновлены.", "success")
|
| 3158 |
elif uploaded_count == 0 and any(f.filename for f in photos_files):
|
|
|
|
| 3187 |
)
|
| 3188 |
except Exception as e:
|
| 3189 |
flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
|
|
|
|
| 3190 |
elif photos_to_delete and not HF_TOKEN_WRITE:
|
| 3191 |
flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
|
| 3192 |
|
|
|
|
| 3200 |
return redirect(url_for('admin', env_id=env_id))
|
| 3201 |
|
| 3202 |
except Exception as e:
|
|
|
|
| 3203 |
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
|
| 3204 |
return redirect(url_for('admin', env_id=env_id))
|
| 3205 |
|
|
|
|
| 3260 |
except ValueError as ve:
|
| 3261 |
return jsonify({"error": str(ve)}), 400
|
| 3262 |
except Exception as e:
|
|
|
|
| 3263 |
return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
|
| 3264 |
|
| 3265 |
@app.route('/<env_id>/chat_with_ai', methods=['POST'])
|
|
|
|
| 3293 |
|
| 3294 |
return jsonify({"text": ai_response_text})
|
| 3295 |
except Exception as e:
|
|
|
|
| 3296 |
return jsonify({"error": f"Ошибка чата: {e}"}), 500
|
| 3297 |
|
| 3298 |
@app.route('/<env_id>/chat')
|
|
|
|
| 3334 |
flash("Данные успешно загружены на Hugging Face.", 'success')
|
| 3335 |
except Exception as e:
|
| 3336 |
flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
|
|
|
|
| 3337 |
return redirect(url_for('admin', env_id=env_id))
|
| 3338 |
|
| 3339 |
@app.route('/<env_id>/force_download', methods=['POST'])
|
|
|
|
| 3345 |
flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error')
|
| 3346 |
except Exception as e:
|
| 3347 |
flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
|
|
|
|
| 3348 |
return redirect(url_for('admin', env_id=env_id))
|
| 3349 |
|
| 3350 |
if __name__ == '__main__':
|
|
|
|
| 3355 |
if HF_TOKEN_WRITE:
|
| 3356 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 3357 |
backup_thread.start()
|
|
|
|
| 3358 |
else:
|
| 3359 |
+
pass
|
| 3360 |
|
| 3361 |
port = int(os.environ.get('PORT', 7860))
|
| 3362 |
app.run(debug=False, host='0.0.0.0', port=port)
|