Implement Caching of previously translated payloads to save big money
This commit is contained in:
parent
b97c9c5926
commit
25091c07d2
1 changed files with 182 additions and 7 deletions
|
|
@ -9,6 +9,8 @@ import re
|
||||||
import json
|
import json
|
||||||
import tiktoken
|
import tiktoken
|
||||||
import openai
|
import openai
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from retry import retry
|
from retry import retry
|
||||||
|
|
@ -33,6 +35,72 @@ else:
|
||||||
# Always set API key from 'key' env var (trim whitespace)
|
# Always set API key from 'key' env var (trim whitespace)
|
||||||
openai.api_key = os.getenv("key", "").strip()
|
openai.api_key = os.getenv("key", "").strip()
|
||||||
|
|
||||||
|
# Translation cache management
|
||||||
|
CACHE_FILE = Path("log/translation_cache.json")
|
||||||
|
CACHE_LOCK = threading.Lock()
|
||||||
|
_cache = None
|
||||||
|
|
||||||
|
def clear_cache():
|
||||||
|
"""Clear the translation cache (called at start of each run)"""
|
||||||
|
global _cache
|
||||||
|
with CACHE_LOCK:
|
||||||
|
_cache = {}
|
||||||
|
|
||||||
|
def load_cache():
|
||||||
|
"""Load the translation cache from disk"""
|
||||||
|
global _cache
|
||||||
|
if _cache is not None:
|
||||||
|
return _cache
|
||||||
|
|
||||||
|
with CACHE_LOCK:
|
||||||
|
if _cache is not None: # Double-check after acquiring lock
|
||||||
|
return _cache
|
||||||
|
|
||||||
|
# Start with empty cache each run
|
||||||
|
_cache = {}
|
||||||
|
return _cache
|
||||||
|
|
||||||
|
def save_cache():
|
||||||
|
"""Save the translation cache to disk"""
|
||||||
|
global _cache
|
||||||
|
if _cache is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Use atomic write
|
||||||
|
tmp_file = CACHE_FILE.with_suffix(".tmp")
|
||||||
|
with open(tmp_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(_cache, f, ensure_ascii=False, indent=2)
|
||||||
|
tmp_file.replace(CACHE_FILE)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_cache_key(payload, language):
|
||||||
|
"""Generate a cache key for a payload (can be single string or JSON batch)"""
|
||||||
|
# Use hash to keep keys short but unique
|
||||||
|
payload_str = str(payload) if payload is not None else ""
|
||||||
|
combined = f"{payload_str}|{language}"
|
||||||
|
return hashlib.md5(combined.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def get_cached_translation(payload, language):
|
||||||
|
"""Get cached translation if it exists"""
|
||||||
|
cache = load_cache()
|
||||||
|
key = get_cache_key(payload, language)
|
||||||
|
return cache.get(key)
|
||||||
|
|
||||||
|
def cache_translation(payload, translation, language):
|
||||||
|
"""Cache a translation payload and its response"""
|
||||||
|
global _cache
|
||||||
|
cache = load_cache()
|
||||||
|
key = get_cache_key(payload, language)
|
||||||
|
|
||||||
|
with CACHE_LOCK:
|
||||||
|
cache[key] = translation
|
||||||
|
# Save every 10 new entries to avoid excessive disk writes
|
||||||
|
if len(cache) % 10 == 0:
|
||||||
|
save_cache()
|
||||||
|
|
||||||
|
|
||||||
class TranslationConfig:
|
class TranslationConfig:
|
||||||
"""Configuration class to hold all translation settings"""
|
"""Configuration class to hold all translation settings"""
|
||||||
|
|
@ -662,13 +730,35 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
|
||||||
|
|
||||||
with open(config.logFilePath, "a+", encoding="utf-8") as logFile:
|
with open(config.logFilePath, "a+", encoding="utf-8") as logFile:
|
||||||
totalTokens = [0, 0]
|
totalTokens = [0, 0]
|
||||||
|
cache_hits = 0
|
||||||
|
cache_misses = 0
|
||||||
|
|
||||||
if isinstance(text, list):
|
if isinstance(text, list):
|
||||||
formatType = "json"
|
formatType = "json"
|
||||||
tList = batchList(text, config.batchSize)
|
|
||||||
|
# Deduplicate strings to avoid translating the same text multiple times
|
||||||
|
# This is crucial for files with many repeated strings
|
||||||
|
unique_texts = []
|
||||||
|
text_to_indices = {} # Maps unique text to list of original indices
|
||||||
|
|
||||||
|
for i, item in enumerate(text):
|
||||||
|
item_str = str(item) if item is not None else ""
|
||||||
|
if item_str in text_to_indices:
|
||||||
|
text_to_indices[item_str].append(i)
|
||||||
|
else:
|
||||||
|
text_to_indices[item_str] = [i]
|
||||||
|
unique_texts.append(item)
|
||||||
|
|
||||||
|
# Only batch the unique texts
|
||||||
|
tList = batchList(unique_texts, config.batchSize)
|
||||||
|
|
||||||
|
# Track which unique items we've translated
|
||||||
|
translated_unique = [None] * len(unique_texts)
|
||||||
else:
|
else:
|
||||||
formatType = "text"
|
formatType = "text"
|
||||||
tList = [text]
|
tList = [text]
|
||||||
|
text_to_indices = None
|
||||||
|
translated_unique = None
|
||||||
|
|
||||||
for index, tItem in enumerate(tList):
|
for index, tItem in enumerate(tList):
|
||||||
# Check if text contains target language
|
# Check if text contains target language
|
||||||
|
|
@ -676,8 +766,17 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
|
||||||
if pbar is not None:
|
if pbar is not None:
|
||||||
pbar.update(len(tItem) if isinstance(tItem, list) else 1)
|
pbar.update(len(tItem) if isinstance(tItem, list) else 1)
|
||||||
if isinstance(tItem, list):
|
if isinstance(tItem, list):
|
||||||
for j in range(len(tItem)):
|
# For deduplication: store cleaned items in translated_unique
|
||||||
tItem[j] = cleanTranslatedText(tItem[j], config.language)
|
if translated_unique is not None:
|
||||||
|
batch_start = index * config.batchSize
|
||||||
|
for i in range(len(tItem)):
|
||||||
|
tItem[i] = cleanTranslatedText(tItem[i], config.language)
|
||||||
|
unique_idx = batch_start + i
|
||||||
|
if unique_idx < len(translated_unique):
|
||||||
|
translated_unique[unique_idx] = tItem[i]
|
||||||
|
else:
|
||||||
|
for j in range(len(tItem)):
|
||||||
|
tItem[j] = cleanTranslatedText(tItem[j], config.language)
|
||||||
tList[index] = tItem
|
tList[index] = tItem
|
||||||
else:
|
else:
|
||||||
tList[index] = cleanTranslatedText(tItem, config.language)
|
tList[index] = cleanTranslatedText(tItem, config.language)
|
||||||
|
|
@ -699,14 +798,47 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
|
||||||
else:
|
else:
|
||||||
subbedT = tItem
|
subbedT = tItem
|
||||||
|
|
||||||
|
# Check cache for this exact payload
|
||||||
|
cached_result = get_cached_translation(subbedT, config.language)
|
||||||
|
if cached_result is not None:
|
||||||
|
cache_hits += 1
|
||||||
|
|
||||||
|
if isinstance(tItem, list):
|
||||||
|
if translated_unique is not None:
|
||||||
|
batch_start = index * config.batchSize
|
||||||
|
for i, cached_item in enumerate(cached_result):
|
||||||
|
unique_idx = batch_start + i
|
||||||
|
if unique_idx < len(translated_unique):
|
||||||
|
translated_unique[unique_idx] = cached_item
|
||||||
|
else:
|
||||||
|
tList[index] = cached_result
|
||||||
|
history = cached_result[-config.maxHistory:]
|
||||||
|
else:
|
||||||
|
tList[index] = cached_result
|
||||||
|
history = cached_result
|
||||||
|
|
||||||
|
if lock and pbar is not None:
|
||||||
|
with lock:
|
||||||
|
pbar.update(len(tItem) if isinstance(tItem, list) else 1)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
# Create context
|
# Create context
|
||||||
system, user = createContext(config, fullPromptFlag, subbedT, formatType, history)
|
system, user = createContext(config, fullPromptFlag, subbedT, formatType, history)
|
||||||
|
|
||||||
# Calculate estimate if in estimate mode
|
# Calculate estimate if in estimate mode
|
||||||
if config.estimateMode:
|
if config.estimateMode:
|
||||||
|
cache_misses += 1
|
||||||
estimate = countTokens(system, user, history)
|
estimate = countTokens(system, user, history)
|
||||||
totalTokens[0] += estimate[0]
|
totalTokens[0] += estimate[0]
|
||||||
totalTokens[1] += estimate[1]
|
totalTokens[1] += estimate[1]
|
||||||
|
|
||||||
|
# Cache the payload with original text as placeholder for future estimates
|
||||||
|
if isinstance(tItem, list):
|
||||||
|
cache_translation(subbedT, tItem, config.language)
|
||||||
|
else:
|
||||||
|
cache_translation(subbedT, [tItem], config.language)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# --- Translation and Validation Retry Block ---
|
# --- Translation and Validation Retry Block ---
|
||||||
|
|
@ -773,8 +905,20 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
|
||||||
logFile.write(f"Input:\n{subbedT}\n")
|
logFile.write(f"Input:\n{subbedT}\n")
|
||||||
logFile.write(f"Output:\n{formatted_output}\n")
|
logFile.write(f"Output:\n{formatted_output}\n")
|
||||||
|
|
||||||
|
# Cache the entire payload and its translation
|
||||||
|
if not config.estimateMode:
|
||||||
|
cache_translation(subbedT, final_translations, config.language)
|
||||||
|
|
||||||
if isinstance(tItem, list):
|
if isinstance(tItem, list):
|
||||||
tList[index] = final_translations
|
# For deduplication: store the translated batch items in translated_unique
|
||||||
|
if translated_unique is not None:
|
||||||
|
batch_start = index * config.batchSize
|
||||||
|
for i, translated_item in enumerate(final_translations):
|
||||||
|
unique_idx = batch_start + i
|
||||||
|
if unique_idx < len(translated_unique):
|
||||||
|
translated_unique[unique_idx] = translated_item
|
||||||
|
else:
|
||||||
|
tList[index] = final_translations
|
||||||
history = final_translations[-config.maxHistory:]
|
history = final_translations[-config.maxHistory:]
|
||||||
else:
|
else:
|
||||||
tList[index] = final_translations
|
tList[index] = final_translations
|
||||||
|
|
@ -801,11 +945,42 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
|
||||||
if filename and mismatchList is not None and filename not in mismatchList:
|
if filename and mismatchList is not None and filename not in mismatchList:
|
||||||
mismatchList.append(filename)
|
mismatchList.append(filename)
|
||||||
|
|
||||||
tList[index] = tItem
|
if translated_unique is not None:
|
||||||
|
# For deduplication: mark failed items in translated_unique
|
||||||
|
batch_start = index * config.batchSize
|
||||||
|
for i, original_item in enumerate(tItem):
|
||||||
|
unique_idx = batch_start + i
|
||||||
|
if unique_idx < len(translated_unique):
|
||||||
|
translated_unique[unique_idx] = original_item
|
||||||
|
else:
|
||||||
|
tList[index] = tItem
|
||||||
history = text[-config.maxHistory:] if isinstance(text, list) else text
|
history = text[-config.maxHistory:] if isinstance(text, list) else text
|
||||||
|
|
||||||
# Combine if multilist
|
# Reconstruct the full list from deduplicated translations
|
||||||
if tList and isinstance(tList[0], list):
|
if text_to_indices is not None and translated_unique is not None:
|
||||||
|
# Create result list with all original positions
|
||||||
|
result_list = [None] * len(text)
|
||||||
|
|
||||||
|
# Apply newly translated items
|
||||||
|
for unique_idx, unique_text in enumerate(unique_texts):
|
||||||
|
unique_text_str = str(unique_text) if unique_text is not None else ""
|
||||||
|
translated_item = translated_unique[unique_idx]
|
||||||
|
|
||||||
|
# If translation is still None, use the original text
|
||||||
|
if translated_item is None:
|
||||||
|
translated_item = unique_text
|
||||||
|
|
||||||
|
# Get all indices that had this unique text
|
||||||
|
for original_idx in text_to_indices[unique_text_str]:
|
||||||
|
result_list[original_idx] = translated_item
|
||||||
|
|
||||||
|
tList = result_list
|
||||||
|
|
||||||
|
# Save cache after processing
|
||||||
|
if not config.estimateMode:
|
||||||
|
save_cache()
|
||||||
|
# Combine if multilist (original logic for non-deduplicated batches)
|
||||||
|
elif tList and isinstance(tList[0], list):
|
||||||
tList = [t for sublist in tList for t in sublist]
|
tList = [t for sublist in tList for t in sublist]
|
||||||
|
|
||||||
# Return result
|
# Return result
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue