mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
257 lines
7.5 KiB
Python
257 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
import shutil
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
|
|
|
from .entity_analysis import normalize_token
|
|
from .utils import get_internal_cache_path, get_user_settings_dir
|
|
|
|
_DB_LOCK = threading.RLock()
|
|
_SCHEMA_VERSION = 1
|
|
|
|
|
|
def _store_path() -> Path:
|
|
try:
|
|
base_dir = Path(get_user_settings_dir())
|
|
except ModuleNotFoundError:
|
|
base_dir = Path(get_internal_cache_path("pronunciations"))
|
|
target = base_dir / "overrides.json"
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
return target
|
|
|
|
|
|
def _migrate_legacy_sqlite(target_json_path: Path) -> None:
|
|
try:
|
|
base_dir = Path(get_user_settings_dir())
|
|
except ModuleNotFoundError:
|
|
base_dir = Path(get_internal_cache_path("pronunciations"))
|
|
|
|
sqlite_path = base_dir / "pronunciations.db"
|
|
if not sqlite_path.exists():
|
|
return
|
|
|
|
try:
|
|
conn = sqlite3.connect(sqlite_path)
|
|
conn.row_factory = sqlite3.Row
|
|
|
|
# Check if table exists
|
|
cursor = conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='overrides'"
|
|
)
|
|
if not cursor.fetchone():
|
|
conn.close()
|
|
return
|
|
|
|
cursor = conn.execute("SELECT * FROM overrides")
|
|
rows = cursor.fetchall()
|
|
|
|
data = {"version": _SCHEMA_VERSION, "overrides": {}}
|
|
|
|
for row in rows:
|
|
lang = row["language"]
|
|
if lang not in data["overrides"]:
|
|
data["overrides"][lang] = {}
|
|
|
|
entry = {
|
|
"id": str(row["id"]),
|
|
"normalized": row["normalized"],
|
|
"token": row["token"],
|
|
"language": row["language"],
|
|
"pronunciation": row["pronunciation"],
|
|
"voice": row["voice"],
|
|
"notes": row["notes"],
|
|
"context": row["context"],
|
|
"usage_count": row["usage_count"],
|
|
"created_at": row["created_at"],
|
|
"updated_at": row["updated_at"],
|
|
}
|
|
data["overrides"][lang][row["normalized"]] = entry
|
|
|
|
conn.close()
|
|
|
|
# Save to JSON
|
|
with open(target_json_path, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
|
|
# Rename old DB
|
|
sqlite_path.rename(sqlite_path.with_suffix(".db.bak"))
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _load_db() -> Dict[str, Any]:
|
|
path = _store_path()
|
|
if not path.exists():
|
|
_migrate_legacy_sqlite(path)
|
|
if not path.exists():
|
|
return {"version": _SCHEMA_VERSION, "overrides": {}}
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except (json.JSONDecodeError, OSError):
|
|
return {"version": _SCHEMA_VERSION, "overrides": {}}
|
|
|
|
|
|
def _save_db(data: Dict[str, Any]) -> None:
|
|
path = _store_path()
|
|
# Atomic write
|
|
temp_path = path.with_suffix(".tmp")
|
|
with open(temp_path, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
shutil.move(str(temp_path), str(path))
|
|
|
|
|
|
def load_overrides(language: str, tokens: Iterable[str]) -> Dict[str, Dict[str, Any]]:
|
|
normalized_tokens = {normalize_token(token) for token in tokens if token}
|
|
if not normalized_tokens:
|
|
return {}
|
|
|
|
with _DB_LOCK:
|
|
db = _load_db()
|
|
lang_overrides = db.get("overrides", {}).get(language, {})
|
|
|
|
results: Dict[str, Dict[str, Any]] = {}
|
|
for normalized in normalized_tokens:
|
|
if normalized in lang_overrides:
|
|
results[normalized] = lang_overrides[normalized]
|
|
return results
|
|
|
|
|
|
def search_overrides(
|
|
language: str, query: str, *, limit: int = 15
|
|
) -> List[Dict[str, Any]]:
|
|
if not query:
|
|
return []
|
|
|
|
query = query.lower()
|
|
with _DB_LOCK:
|
|
db = _load_db()
|
|
lang_overrides = db.get("overrides", {}).get(language, {})
|
|
|
|
matches = []
|
|
for entry in lang_overrides.values():
|
|
if query in entry["normalized"] or query in entry["token"].lower():
|
|
matches.append(entry)
|
|
|
|
# Sort by usage count desc, then updated_at desc
|
|
matches.sort(
|
|
key=lambda x: (x.get("usage_count", 0), x.get("updated_at", 0)),
|
|
reverse=True,
|
|
)
|
|
return matches[:limit]
|
|
|
|
|
|
def save_override(
|
|
*,
|
|
language: str,
|
|
token: str,
|
|
pronunciation: Optional[str] = None,
|
|
voice: Optional[str] = None,
|
|
notes: Optional[str] = None,
|
|
context: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
normalized = normalize_token(token)
|
|
if not normalized:
|
|
raise ValueError("Provide a token to override")
|
|
|
|
timestamp = time.time()
|
|
with _DB_LOCK:
|
|
db = _load_db()
|
|
overrides = db.setdefault("overrides", {})
|
|
lang_overrides = overrides.setdefault(language, {})
|
|
|
|
existing = lang_overrides.get(normalized)
|
|
|
|
if existing:
|
|
entry = existing
|
|
entry["token"] = token
|
|
entry["pronunciation"] = pronunciation
|
|
entry["voice"] = voice
|
|
entry["notes"] = notes
|
|
entry["context"] = context
|
|
entry["updated_at"] = timestamp
|
|
else:
|
|
entry = {
|
|
"id": str(uuid.uuid4()),
|
|
"normalized": normalized,
|
|
"token": token,
|
|
"language": language,
|
|
"pronunciation": pronunciation,
|
|
"voice": voice,
|
|
"notes": notes,
|
|
"context": context,
|
|
"usage_count": 0,
|
|
"created_at": timestamp,
|
|
"updated_at": timestamp,
|
|
}
|
|
lang_overrides[normalized] = entry
|
|
|
|
_save_db(db)
|
|
return entry
|
|
|
|
|
|
def delete_override(*, language: str, token: str) -> None:
|
|
normalized = normalize_token(token)
|
|
if not normalized:
|
|
return
|
|
|
|
with _DB_LOCK:
|
|
db = _load_db()
|
|
lang_overrides = db.get("overrides", {}).get(language, {})
|
|
|
|
if normalized in lang_overrides:
|
|
del lang_overrides[normalized]
|
|
_save_db(db)
|
|
|
|
|
|
def all_overrides(language: str) -> List[Dict[str, Any]]:
|
|
with _DB_LOCK:
|
|
db = _load_db()
|
|
lang_overrides = db.get("overrides", {}).get(language, {})
|
|
|
|
results = list(lang_overrides.values())
|
|
results.sort(key=lambda x: x.get("updated_at", 0), reverse=True)
|
|
return results
|
|
|
|
|
|
def increment_usage(*, language: str, token: str, amount: int = 1) -> None:
|
|
normalized = normalize_token(token)
|
|
if not normalized:
|
|
return
|
|
|
|
with _DB_LOCK:
|
|
db = _load_db()
|
|
lang_overrides = db.get("overrides", {}).get(language, {})
|
|
|
|
if normalized in lang_overrides:
|
|
entry = lang_overrides[normalized]
|
|
entry["usage_count"] = entry.get("usage_count", 0) + amount
|
|
entry["updated_at"] = time.time()
|
|
_save_db(db)
|
|
|
|
|
|
def get_override_stats(language: str) -> Dict[str, int]:
|
|
with _DB_LOCK:
|
|
db = _load_db()
|
|
lang_overrides = db.get("overrides", {}).get(language, {})
|
|
|
|
total = len(lang_overrides)
|
|
with_pronunciation = sum(
|
|
1 for x in lang_overrides.values() if x.get("pronunciation")
|
|
)
|
|
with_voice = sum(1 for x in lang_overrides.values() if x.get("voice"))
|
|
|
|
return {
|
|
"total": total,
|
|
"filtered": total,
|
|
"with_pronunciation": with_pronunciation,
|
|
"with_voice": with_voice,
|
|
}
|