add translation cache and queue
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,3 +2,6 @@ venv/
|
||||
images/
|
||||
output/
|
||||
__pycache__/
|
||||
|
||||
product_translation_cache.json
|
||||
translation_queue.json
|
||||
|
||||
@@ -328,5 +328,77 @@
|
||||
"name": "Корпуси для комп'ютерів",
|
||||
"portal_id": "720",
|
||||
"url": "https://www.euro.com.pl/obudowy-pc.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "62",
|
||||
"name": "Материнські плати",
|
||||
"portal_id": "70702",
|
||||
"url": "https://www.euro.com.pl/plyty-glowne.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "63",
|
||||
"name": "Клавіатури",
|
||||
"portal_id": "71113",
|
||||
"url": "https://www.euro.com.pl/klawiatury.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "64",
|
||||
"name": "Каструлі",
|
||||
"portal_id": "15230307",
|
||||
"url": "https://www.euro.com.pl/garnki.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "65",
|
||||
"name": "Столові прибори",
|
||||
"portal_id": "15230409",
|
||||
"url": "https://www.euro.com.pl/sztucce.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "66",
|
||||
"name": "Кухонне приладдя",
|
||||
"portal_id": "15230409",
|
||||
"url": "https://www.euro.com.pl/przybory-kuchenne.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "67",
|
||||
"name": "Сковороди",
|
||||
"portal_id": "15230304",
|
||||
"url": "https://www.euro.com.pl/patelnie.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "68",
|
||||
"name": "Портативні колонки",
|
||||
"portal_id": "6371101",
|
||||
"url": "https://www.euro.com.pl/glosniki-przenosne.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "69",
|
||||
"name": "Маршрутизатори",
|
||||
"portal_id": "71902",
|
||||
"url": "https://www.euro.com.pl/routery.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "70",
|
||||
"name": "Побутові сушильні машини для білизни",
|
||||
"portal_id": "62702",
|
||||
"url": "https://www.euro.com.pl/suszarki.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "71",
|
||||
"name": "Вбудовані посудомийні машини",
|
||||
"portal_id": "615",
|
||||
"url": "https://www.euro.com.pl/zmywarki-do-zabudowy.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "72",
|
||||
"name": "Форми для випікання",
|
||||
"portal_id": "15230414",
|
||||
"url": "https://www.euro.com.pl/formy-i-akcesoria-do-pieczenia.bhtml"
|
||||
},
|
||||
{
|
||||
"id": "73",
|
||||
"name": "Сабвуфери",
|
||||
"portal_id": "518",
|
||||
"url": "https://www.euro.com.pl/subwoofery.bhtml"
|
||||
}
|
||||
]
|
||||
16
check_cache_size.py
Normal file
16
check_cache_size.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import json
|
||||
|
||||
CACHE_FILE = "product_translation_cache.json"
|
||||
|
||||
def check_cache_size():
|
||||
try:
|
||||
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
||||
cache = json.load(f)
|
||||
print(f"🔎 В кеші збережено {len(cache)} перекладів.")
|
||||
except FileNotFoundError:
|
||||
print("❌ Кеш-файл не знайдено.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Помилка читання кешу: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_cache_size()
|
||||
40
translation_cache.py
Normal file
40
translation_cache.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import json
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
CACHE_FILE = "product_translation_cache.json"
|
||||
MAX_CACHE_ENTRIES = 200_000 # максимум записів у кеші
|
||||
|
||||
class TranslationCache:
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
self.load_cache()
|
||||
|
||||
def get_hash(self, text: str) -> str:
|
||||
return hashlib.md5(text.strip().encode("utf-8")).hexdigest()
|
||||
|
||||
def load_cache(self):
|
||||
if os.path.exists(CACHE_FILE):
|
||||
try:
|
||||
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
||||
self.cache = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"[CACHE] Помилка завантаження кешу: {e}")
|
||||
|
||||
def save_cache(self):
|
||||
# якщо занадто великий кеш — залишаємо тільки останні MAX_CACHE_ENTRIES
|
||||
if len(self.cache) > MAX_CACHE_ENTRIES:
|
||||
# зберігаємо найновіші — останні додані (у Python 3.7+ dict зберігає порядок)
|
||||
trimmed = dict(list(self.cache.items())[-MAX_CACHE_ENTRIES:])
|
||||
self.cache = trimmed
|
||||
|
||||
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self.cache, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def get(self, text: str) -> str | None:
|
||||
key = self.get_hash(text)
|
||||
return self.cache.get(key)
|
||||
|
||||
def add(self, text: str, translated: str):
|
||||
key = self.get_hash(text)
|
||||
self.cache[key] = translated
|
||||
71
translation_queue.py
Normal file
71
translation_queue.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
QUEUE_FILE = "translation_queue.json"
|
||||
|
||||
|
||||
def load_queue() -> List[str]:
|
||||
if not os.path.exists(QUEUE_FILE):
|
||||
return []
|
||||
try:
|
||||
with open(QUEUE_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data.get("queue", [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def save_queue(queue: List[str]):
|
||||
with open(QUEUE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump({"queue": queue}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def add_to_queue(filename: str):
|
||||
queue = load_queue()
|
||||
if filename not in queue:
|
||||
queue.append(filename)
|
||||
save_queue(queue)
|
||||
|
||||
|
||||
def remove_from_queue(filename: str):
|
||||
queue = load_queue()
|
||||
if filename in queue:
|
||||
queue.remove(filename)
|
||||
save_queue(queue)
|
||||
|
||||
|
||||
def translate_from_queue(start_translation_func):
|
||||
from utils import notify_telegram
|
||||
import threading
|
||||
import time
|
||||
|
||||
queue = load_queue()
|
||||
if not queue:
|
||||
print("[QUEUE] Черга пуста")
|
||||
return
|
||||
|
||||
filename = queue[0]
|
||||
try:
|
||||
print(f"[QUEUE] Спроба перекладу з черги: {filename}")
|
||||
start_translation_func(filename)
|
||||
remove_from_queue(filename)
|
||||
notify_telegram(f"[✅] Файл з черги перекладено: {filename}")
|
||||
|
||||
if load_queue():
|
||||
threading.Timer(3, translate_from_queue, args=(start_translation_func,)).start()
|
||||
|
||||
except Exception as e:
|
||||
error_text = str(e).lower()
|
||||
if (
|
||||
"too many requests" in error_text
|
||||
or "you are allowed to make" in error_text
|
||||
or "5 requests per second" in error_text
|
||||
):
|
||||
notify_telegram(
|
||||
f"[🛑] Знову ліміт при перекладі з черги. Повтор через 30 хвилин.\nФайл: {filename}"
|
||||
)
|
||||
threading.Timer(1800, translate_from_queue, args=(start_translation_func,)).start()
|
||||
else:
|
||||
notify_telegram(f"[⚠️] Помилка при перекладі з черги: {filename}\n{str(e)}")
|
||||
remove_from_queue(filename)
|
||||
@@ -1,23 +1,45 @@
|
||||
from deep_translator import GoogleTranslator
|
||||
from typing import Dict, Any, List
|
||||
import time
|
||||
from translation_cache import TranslationCache
|
||||
|
||||
|
||||
class ProductTranslator:
|
||||
def __init__(self):
|
||||
self.translator = GoogleTranslator(source="pl", target="uk")
|
||||
self.cache = TranslationCache() # 🧠 кеш ініціалізація
|
||||
|
||||
def translate_text(self, text: str) -> str:
|
||||
"""Переводит текст с обработкой ошибок и задержкой"""
|
||||
"""Переводит текст с кешем, обработкой ошибок и задержкой"""
|
||||
if not text or not isinstance(text, str):
|
||||
return text
|
||||
|
||||
# 🧠 Перевірка кешу
|
||||
cached = self.cache.get(text)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
try:
|
||||
translated = self.translator.translate(text)
|
||||
time.sleep(0.5) # Задержка чтобы избежать блокировки
|
||||
time.sleep(0.5) # Затримка щоб уникнути блокування
|
||||
|
||||
if translated and translated != text:
|
||||
self.cache.add(text, translated)
|
||||
self.cache.save_cache() # 💾 збереження кешу
|
||||
|
||||
return translated
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка перевода: {e}")
|
||||
error_text = str(e).lower()
|
||||
print(f"[ERROR] Ошибка перевода: {e}")
|
||||
|
||||
if (
|
||||
"too many requests" in error_text
|
||||
or "you are allowed to make" in error_text
|
||||
or "5 requests per second" in error_text
|
||||
):
|
||||
raise e
|
||||
|
||||
return text
|
||||
|
||||
def translate_list(self, items: List[str]) -> List[str]:
|
||||
|
||||
12
utils.py
Normal file
12
utils.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import requests
|
||||
|
||||
TELEGRAM_TOKEN = "6452801861:AAHPzu1uvWzAznCVD905z7So-abg3R4wUKU"
|
||||
TELEGRAM_CHAT_ID = "802473090" # можна отримати через @userinfobot
|
||||
|
||||
def notify_telegram(message: str):
|
||||
try:
|
||||
url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
|
||||
data = {"chat_id": TELEGRAM_CHAT_ID, "text": message}
|
||||
requests.post(url, data=data)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Не вдалося надіслати повідомлення в Telegram: {e}")
|
||||
@@ -30,6 +30,9 @@ from urllib.parse import urljoin
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import time
|
||||
from config import BASE_URL
|
||||
from utils import notify_telegram
|
||||
import threading
|
||||
from translation_queue import add_to_queue, translate_from_queue
|
||||
|
||||
# Добавляем в начало файла
|
||||
login_manager = LoginManager()
|
||||
@@ -457,6 +460,8 @@ def start_translation(filename: str):
|
||||
translation_status["processed_items"] = 0
|
||||
translation_status["error"] = None
|
||||
|
||||
notify_telegram(f"[🚀] Початок перекладу: {filename}")
|
||||
|
||||
try:
|
||||
os.makedirs("output/translated", exist_ok=True)
|
||||
|
||||
@@ -499,16 +504,23 @@ def start_translation(filename: str):
|
||||
) as f:
|
||||
json.dump(translated_products, f, ensure_ascii=False, indent=2)
|
||||
print(f"[OK] Збережено переклад: {output_filename}")
|
||||
notify_telegram(
|
||||
f"[✅] Переклад завершено: {filename} ({len(translated_products)} товарів)"
|
||||
)
|
||||
else:
|
||||
print(f"[SKIP] Жодного перекладеного товару: {filename}. Файл не створено.")
|
||||
notify_telegram(f"[⚠️] Жодного товару не перекладено: {filename}")
|
||||
|
||||
except Exception as e:
|
||||
translation_status["error"] = str(e)
|
||||
print(f"Ошибка перевода: {e}")
|
||||
notify_telegram(f"[❌] Помилка перекладу: {filename}\n{e}")
|
||||
add_to_queue(filename)
|
||||
|
||||
raise e
|
||||
|
||||
finally:
|
||||
translation_status["is_running"] = False
|
||||
active_translations.discard(filename)
|
||||
print(f"[DONE] Переклад завершено: {filename}")
|
||||
|
||||
|
||||
def refresh_all_categories_daily():
|
||||
@@ -555,31 +567,66 @@ def refresh_all_categories_daily():
|
||||
|
||||
|
||||
def translate_all_parsed_once():
|
||||
"""Одноразово запускає переклад для всіх ще не перекладених файлів"""
|
||||
print("[START] Одноразовий переклад всіх категорій...")
|
||||
"""Розумне оновлення перекладу: неперекладені → найстаріші перекладені"""
|
||||
print("[START] Розумне оновлення перекладу...")
|
||||
|
||||
parsed_folder = "output"
|
||||
translated_folder = "output/translated"
|
||||
|
||||
for filename in os.listdir(parsed_folder):
|
||||
if filename.endswith("_products.json") and not filename.endswith(
|
||||
"_translated_products.json"
|
||||
):
|
||||
parsed_path = os.path.join(parsed_folder, filename)
|
||||
translated_name = filename.replace(
|
||||
"_products.json", "_translated_products.json"
|
||||
parsed_files = {
|
||||
f: os.path.getmtime(os.path.join(parsed_folder, f))
|
||||
for f in os.listdir(parsed_folder)
|
||||
if f.endswith("_products.json") and not f.endswith("_translated_products.json")
|
||||
}
|
||||
|
||||
translated_files = {
|
||||
f.replace("_translated_products.json", "_products.json"): os.path.getmtime(
|
||||
os.path.join(translated_folder, f)
|
||||
)
|
||||
translated_path = os.path.join(translated_folder, translated_name)
|
||||
for f in os.listdir(translated_folder)
|
||||
if f.endswith("_translated_products.json")
|
||||
}
|
||||
|
||||
if os.path.exists(translated_path):
|
||||
print(f"[SKIP] Вже перекладено: {filename}")
|
||||
continue
|
||||
# Спочатку неперекладені
|
||||
untranslated = [f for f in parsed_files if f not in translated_files]
|
||||
# Потім найстаріші з перекладених
|
||||
outdated_translations = sorted(
|
||||
(f for f in parsed_files if f in translated_files),
|
||||
key=lambda f: translated_files[f],
|
||||
)
|
||||
|
||||
print(f"[TRANSLATE] Запускаємо переклад: {filename}")
|
||||
threading.Thread(target=start_translation, args=(filename,)).start()
|
||||
time.sleep(2) # трохи затримки між потоками, щоб не навантажувати
|
||||
to_translate = untranslated + outdated_translations
|
||||
|
||||
print("[DONE] Запуск перекладу завершено.")
|
||||
for filename in to_translate:
|
||||
try:
|
||||
print(f"[TRANSLATE] {filename}")
|
||||
start_translation(filename) # ⬅️ БЕЗ ПОТОКУ
|
||||
time.sleep(1) # щоб трохи розвантажити
|
||||
except Exception as e:
|
||||
error_text = str(e).lower()
|
||||
if (
|
||||
"too many requests" in error_text
|
||||
or "you are allowed to make" in error_text
|
||||
or "5 requests per second" in error_text
|
||||
):
|
||||
notify_telegram(
|
||||
f"[🛑] Переклад зупинено після помилки ліміту на файлі: {filename}.\n"
|
||||
f"Повтор заплановано через 30 хвилин."
|
||||
)
|
||||
add_to_queue(filename)
|
||||
threading.Timer(1800, translate_from_queue).start()
|
||||
|
||||
break
|
||||
else:
|
||||
notify_telegram(f"[⚠️] Помилка перекладу {filename}:\n{str(e)}")
|
||||
|
||||
print("[DONE] translate_all_parsed_once завершено.")
|
||||
|
||||
|
||||
@app.route("/translate-from-queue", methods=["POST"])
|
||||
def run_translate_from_queue():
|
||||
threading.Thread(target=translate_from_queue, args=(start_translation,)).start()
|
||||
return jsonify({"status": "Запущено переклад з черги"})
|
||||
|
||||
|
||||
@app.route("/manual-translate-all", methods=["POST"])
|
||||
|
||||
Reference in New Issue
Block a user