add translation cache and queue
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,3 +2,6 @@ venv/
|
|||||||
images/
|
images/
|
||||||
output/
|
output/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
|
product_translation_cache.json
|
||||||
|
translation_queue.json
|
||||||
|
|||||||
@@ -328,5 +328,77 @@
|
|||||||
"name": "Корпуси для комп'ютерів",
|
"name": "Корпуси для комп'ютерів",
|
||||||
"portal_id": "720",
|
"portal_id": "720",
|
||||||
"url": "https://www.euro.com.pl/obudowy-pc.bhtml"
|
"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 deep_translator import GoogleTranslator
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
import time
|
import time
|
||||||
|
from translation_cache import TranslationCache
|
||||||
|
|
||||||
|
|
||||||
class ProductTranslator:
|
class ProductTranslator:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.translator = GoogleTranslator(source="pl", target="uk")
|
self.translator = GoogleTranslator(source="pl", target="uk")
|
||||||
|
self.cache = TranslationCache() # 🧠 кеш ініціалізація
|
||||||
|
|
||||||
def translate_text(self, text: str) -> str:
|
def translate_text(self, text: str) -> str:
|
||||||
"""Переводит текст с обработкой ошибок и задержкой"""
|
"""Переводит текст с кешем, обработкой ошибок и задержкой"""
|
||||||
if not text or not isinstance(text, str):
|
if not text or not isinstance(text, str):
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
# 🧠 Перевірка кешу
|
||||||
|
cached = self.cache.get(text)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
|
||||||
try:
|
try:
|
||||||
translated = self.translator.translate(text)
|
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
|
return translated
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return text
|
||||||
|
|
||||||
def translate_list(self, items: List[str]) -> List[str]:
|
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
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
import time
|
import time
|
||||||
from config import BASE_URL
|
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()
|
login_manager = LoginManager()
|
||||||
@@ -457,6 +460,8 @@ def start_translation(filename: str):
|
|||||||
translation_status["processed_items"] = 0
|
translation_status["processed_items"] = 0
|
||||||
translation_status["error"] = None
|
translation_status["error"] = None
|
||||||
|
|
||||||
|
notify_telegram(f"[🚀] Початок перекладу: {filename}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs("output/translated", exist_ok=True)
|
os.makedirs("output/translated", exist_ok=True)
|
||||||
|
|
||||||
@@ -499,16 +504,23 @@ def start_translation(filename: str):
|
|||||||
) as f:
|
) as f:
|
||||||
json.dump(translated_products, f, ensure_ascii=False, indent=2)
|
json.dump(translated_products, f, ensure_ascii=False, indent=2)
|
||||||
print(f"[OK] Збережено переклад: {output_filename}")
|
print(f"[OK] Збережено переклад: {output_filename}")
|
||||||
|
notify_telegram(
|
||||||
|
f"[✅] Переклад завершено: {filename} ({len(translated_products)} товарів)"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(f"[SKIP] Жодного перекладеного товару: {filename}. Файл не створено.")
|
print(f"[SKIP] Жодного перекладеного товару: {filename}. Файл не створено.")
|
||||||
|
notify_telegram(f"[⚠️] Жодного товару не перекладено: {filename}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
translation_status["error"] = str(e)
|
translation_status["error"] = str(e)
|
||||||
print(f"Ошибка перевода: {e}")
|
notify_telegram(f"[❌] Помилка перекладу: {filename}\n{e}")
|
||||||
|
add_to_queue(filename)
|
||||||
|
|
||||||
|
raise e
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
translation_status["is_running"] = False
|
translation_status["is_running"] = False
|
||||||
active_translations.discard(filename)
|
active_translations.discard(filename)
|
||||||
print(f"[DONE] Переклад завершено: {filename}")
|
|
||||||
|
|
||||||
|
|
||||||
def refresh_all_categories_daily():
|
def refresh_all_categories_daily():
|
||||||
@@ -555,31 +567,66 @@ def refresh_all_categories_daily():
|
|||||||
|
|
||||||
|
|
||||||
def translate_all_parsed_once():
|
def translate_all_parsed_once():
|
||||||
"""Одноразово запускає переклад для всіх ще не перекладених файлів"""
|
"""Розумне оновлення перекладу: неперекладені → найстаріші перекладені"""
|
||||||
print("[START] Одноразовий переклад всіх категорій...")
|
print("[START] Розумне оновлення перекладу...")
|
||||||
|
|
||||||
parsed_folder = "output"
|
parsed_folder = "output"
|
||||||
translated_folder = "output/translated"
|
translated_folder = "output/translated"
|
||||||
|
|
||||||
for filename in os.listdir(parsed_folder):
|
parsed_files = {
|
||||||
if filename.endswith("_products.json") and not filename.endswith(
|
f: os.path.getmtime(os.path.join(parsed_folder, f))
|
||||||
"_translated_products.json"
|
for f in os.listdir(parsed_folder)
|
||||||
):
|
if f.endswith("_products.json") and not f.endswith("_translated_products.json")
|
||||||
parsed_path = os.path.join(parsed_folder, filename)
|
}
|
||||||
translated_name = filename.replace(
|
|
||||||
"_products.json", "_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}")
|
untranslated = [f for f in parsed_files if f not in translated_files]
|
||||||
continue
|
# Потім найстаріші з перекладених
|
||||||
|
outdated_translations = sorted(
|
||||||
|
(f for f in parsed_files if f in translated_files),
|
||||||
|
key=lambda f: translated_files[f],
|
||||||
|
)
|
||||||
|
|
||||||
print(f"[TRANSLATE] Запускаємо переклад: {filename}")
|
to_translate = untranslated + outdated_translations
|
||||||
threading.Thread(target=start_translation, args=(filename,)).start()
|
|
||||||
time.sleep(2) # трохи затримки між потоками, щоб не навантажувати
|
|
||||||
|
|
||||||
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"])
|
@app.route("/manual-translate-all", methods=["POST"])
|
||||||
|
|||||||
Reference in New Issue
Block a user