added posibility to select older multilingual spaces
Browse files
README.md
CHANGED
|
@@ -22,11 +22,15 @@ The app does not use `aligned_all.vec`.
|
|
| 22 |
|
| 23 |
## Runtime configuration
|
| 24 |
|
| 25 |
-
By default, the app scans the stage 6 prefix
|
| 26 |
-
folder that contains `config.json`:
|
| 27 |
|
| 28 |
`s3://131-component-staging/multilingual-static-word-embeddings/stage-6/`
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
Set these Hugging Face Space secrets for S3-compatible storage:
|
| 31 |
|
| 32 |
- `SE_ACCESS_KEY`
|
|
|
|
| 22 |
|
| 23 |
## Runtime configuration
|
| 24 |
|
| 25 |
+
By default, the app scans the stage 6 prefix, downloads the newest artifact
|
| 26 |
+
folder that contains `config.json`, and selects it in the UI:
|
| 27 |
|
| 28 |
`s3://131-component-staging/multilingual-static-word-embeddings/stage-6/`
|
| 29 |
|
| 30 |
+
Older artifact folders found under the same prefix are available from the
|
| 31 |
+
Dictionary artifact dropdown. Switching artifacts reloads the aligned space and
|
| 32 |
+
updates the displayed load time.
|
| 33 |
+
|
| 34 |
Set these Hugging Face Space secrets for S3-compatible storage:
|
| 35 |
|
| 36 |
- `SE_ACCESS_KEY`
|
app.py
CHANGED
|
@@ -5,6 +5,7 @@ import json
|
|
| 5 |
import os
|
| 6 |
import re
|
| 7 |
import sys
|
|
|
|
| 8 |
import unicodedata
|
| 9 |
from dataclasses import dataclass
|
| 10 |
from functools import lru_cache
|
|
@@ -26,6 +27,9 @@ DEFAULT_LOCAL_SPACE = Path("multilingual_dict_20260603_122323")
|
|
| 26 |
DEFAULT_LANGS = ["de", "en", "fr", "lb"]
|
| 27 |
REQUIRED_FILES = ("aligned_all.faiss", "all_metadata.jsonl", "config.json")
|
| 28 |
CACHE_DIR = Path(os.getenv("ARTIFACT_CACHE_DIR", "/tmp/multilingual_space_artifacts"))
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
@dataclass
|
|
@@ -96,24 +100,21 @@ def make_s3_client():
|
|
| 96 |
return boto3.client(**kwargs)
|
| 97 |
|
| 98 |
|
| 99 |
-
def
|
| 100 |
explicit = os.getenv("SPACE_ARTIFACT_S3_URI", "").strip().rstrip("/")
|
| 101 |
if explicit:
|
| 102 |
-
return explicit
|
| 103 |
|
| 104 |
prefix_override = os.getenv("SPACE_ARTIFACT_S3_PREFIX", "").strip()
|
| 105 |
prefix_uri = prefix_override or DEFAULT_ARTIFACT_PREFIX
|
| 106 |
bucket, prefix = parse_s3_uri(prefix_uri)
|
| 107 |
prefix = prefix.rstrip("/") + "/"
|
| 108 |
-
pattern = re.compile(
|
| 109 |
-
r"(.*multilingual_(?:dict|space)_(\d{8}_\d{6})(?:\.json)?)/config\.json$"
|
| 110 |
-
)
|
| 111 |
candidates: list[tuple[str, str]] = []
|
| 112 |
|
| 113 |
paginator = client.get_paginator("list_objects_v2")
|
| 114 |
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
| 115 |
for obj in page.get("Contents", []):
|
| 116 |
-
match =
|
| 117 |
if match:
|
| 118 |
candidates.append((match.group(2), match.group(1)))
|
| 119 |
|
|
@@ -123,20 +124,74 @@ def latest_artifact_uri(client) -> str:
|
|
| 123 |
)
|
| 124 |
|
| 125 |
# Run ids are timestamps: YYYYMMDD_HHMMSS. Lexicographic sort gives newest run.
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
| 129 |
return uri
|
| 130 |
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
def local_cache_for_uri(uri: str) -> Path:
|
| 133 |
_, key = parse_s3_uri(uri)
|
| 134 |
return CACHE_DIR / Path(key.rstrip("/")).name
|
| 135 |
|
| 136 |
|
| 137 |
-
def download_space_from_s3() -> tuple[Path, str]:
|
| 138 |
client = make_s3_client()
|
| 139 |
-
uri = latest_artifact_uri(client)
|
| 140 |
local_dir = local_cache_for_uri(uri)
|
| 141 |
local_dir.mkdir(parents=True, exist_ok=True)
|
| 142 |
|
|
@@ -153,7 +208,16 @@ def download_space_from_s3() -> tuple[Path, str]:
|
|
| 153 |
return local_dir, uri
|
| 154 |
|
| 155 |
|
| 156 |
-
def find_space_dir() -> tuple[Path, str]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
local_override = os.getenv("SPACE_DIR", "").strip()
|
| 158 |
if local_override:
|
| 159 |
path = Path(local_override)
|
|
@@ -303,8 +367,8 @@ def build_lookup(languages: dict[str, LangVectors]) -> dict[str, dict[str, list[
|
|
| 303 |
|
| 304 |
|
| 305 |
@lru_cache(maxsize=1)
|
| 306 |
-
def load_space() -> Space:
|
| 307 |
-
space_dir, artifact_uri = find_space_dir()
|
| 308 |
config = read_config(space_dir)
|
| 309 |
metadata, ids_by_lang = read_metadata(space_dir)
|
| 310 |
vectors_by_lang = load_vectors_from_faiss(space_dir, ids_by_lang)
|
|
@@ -527,9 +591,10 @@ def translate_like_terminal(
|
|
| 527 |
filter_stopwords: bool,
|
| 528 |
filter_bad_tokens: bool,
|
| 529 |
use_surface: bool,
|
|
|
|
| 530 |
) -> tuple[str, list[list[Any]]]:
|
| 531 |
try:
|
| 532 |
-
space = load_space()
|
| 533 |
use_surface = bool(use_surface and space.has_surface_forms)
|
| 534 |
opts = make_options(
|
| 535 |
top_k,
|
|
@@ -617,17 +682,23 @@ def translate_like_terminal(
|
|
| 617 |
return f"Error: {exc}", []
|
| 618 |
|
| 619 |
|
| 620 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
try:
|
| 622 |
-
space = load_space()
|
| 623 |
opts = default_options(space.config)
|
| 624 |
source_lang = space.config.get("pivot_lang", "de")
|
| 625 |
if source_lang not in space.languages:
|
| 626 |
source_lang = space.languages[0]
|
| 627 |
-
status = (
|
| 628 |
-
f"Loaded {space.artifact_uri} with "
|
| 629 |
-
f"{sum(len(item.metas) for item in space.by_lang.values()):,} vectors."
|
| 630 |
-
)
|
| 631 |
return (
|
| 632 |
status,
|
| 633 |
gr.update(choices=space.languages, value=source_lang),
|
|
@@ -665,6 +736,40 @@ def initialize() -> tuple[Any, ...]:
|
|
| 665 |
)
|
| 666 |
|
| 667 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
CSS = """
|
| 669 |
body { background: #f7f5ef; }
|
| 670 |
.gradio-container { max-width: 1120px !important; }
|
|
@@ -681,6 +786,12 @@ with gr.Blocks(title="Multilingual Dictionary Explorer", css=CSS) as demo:
|
|
| 681 |
elem_classes=["app-title"],
|
| 682 |
)
|
| 683 |
status = gr.Markdown("Loading artifacts...", elem_classes=["status"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
|
| 685 |
with gr.Row():
|
| 686 |
with gr.Column(scale=1, min_width=320):
|
|
@@ -808,14 +919,35 @@ with gr.Blocks(title="Multilingual Dictionary Explorer", css=CSS) as demo:
|
|
| 808 |
filter_stopwords,
|
| 809 |
filter_bad_tokens,
|
| 810 |
use_surface,
|
|
|
|
| 811 |
]
|
| 812 |
search.click(translate_like_terminal, inputs=inputs, outputs=[output_text, output_table])
|
| 813 |
query.submit(translate_like_terminal, inputs=inputs, outputs=[output_text, output_table])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 814 |
|
| 815 |
demo.load(
|
| 816 |
initialize,
|
| 817 |
outputs=[
|
| 818 |
status,
|
|
|
|
| 819 |
source_lang,
|
| 820 |
top_k,
|
| 821 |
min_score,
|
|
|
|
| 5 |
import os
|
| 6 |
import re
|
| 7 |
import sys
|
| 8 |
+
import time
|
| 9 |
import unicodedata
|
| 10 |
from dataclasses import dataclass
|
| 11 |
from functools import lru_cache
|
|
|
|
| 27 |
DEFAULT_LANGS = ["de", "en", "fr", "lb"]
|
| 28 |
REQUIRED_FILES = ("aligned_all.faiss", "all_metadata.jsonl", "config.json")
|
| 29 |
CACHE_DIR = Path(os.getenv("ARTIFACT_CACHE_DIR", "/tmp/multilingual_space_artifacts"))
|
| 30 |
+
ARTIFACT_PATTERN = re.compile(
|
| 31 |
+
r"(.*multilingual_(?:dict|space)_(\d{8}_\d{6})(?:\.json)?)/config\.json$"
|
| 32 |
+
)
|
| 33 |
|
| 34 |
|
| 35 |
@dataclass
|
|
|
|
| 100 |
return boto3.client(**kwargs)
|
| 101 |
|
| 102 |
|
| 103 |
+
def list_s3_artifact_uris(client) -> list[str]:
|
| 104 |
explicit = os.getenv("SPACE_ARTIFACT_S3_URI", "").strip().rstrip("/")
|
| 105 |
if explicit:
|
| 106 |
+
return [explicit]
|
| 107 |
|
| 108 |
prefix_override = os.getenv("SPACE_ARTIFACT_S3_PREFIX", "").strip()
|
| 109 |
prefix_uri = prefix_override or DEFAULT_ARTIFACT_PREFIX
|
| 110 |
bucket, prefix = parse_s3_uri(prefix_uri)
|
| 111 |
prefix = prefix.rstrip("/") + "/"
|
|
|
|
|
|
|
|
|
|
| 112 |
candidates: list[tuple[str, str]] = []
|
| 113 |
|
| 114 |
paginator = client.get_paginator("list_objects_v2")
|
| 115 |
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
| 116 |
for obj in page.get("Contents", []):
|
| 117 |
+
match = ARTIFACT_PATTERN.match(obj["Key"])
|
| 118 |
if match:
|
| 119 |
candidates.append((match.group(2), match.group(1)))
|
| 120 |
|
|
|
|
| 124 |
)
|
| 125 |
|
| 126 |
# Run ids are timestamps: YYYYMMDD_HHMMSS. Lexicographic sort gives newest run.
|
| 127 |
+
return [f"s3://{bucket}/{key}" for _, key in sorted(candidates, reverse=True)]
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def latest_artifact_uri(client) -> str:
|
| 131 |
+
uri = list_s3_artifact_uris(client)[0]
|
| 132 |
+
print(f"Selected latest stage 6 artifact: {uri}", file=sys.stderr)
|
| 133 |
return uri
|
| 134 |
|
| 135 |
|
| 136 |
+
def local_artifact_uris() -> list[str]:
|
| 137 |
+
local_override = os.getenv("SPACE_DIR", "").strip()
|
| 138 |
+
if local_override:
|
| 139 |
+
path = Path(local_override)
|
| 140 |
+
if path.exists():
|
| 141 |
+
return [str(path)]
|
| 142 |
+
|
| 143 |
+
local_candidates = sorted(
|
| 144 |
+
[*Path(".").glob("multilingual_dict_*"), *Path(".").glob("multilingual_space_*.json")],
|
| 145 |
+
reverse=True,
|
| 146 |
+
)
|
| 147 |
+
if DEFAULT_LOCAL_SPACE.exists():
|
| 148 |
+
local_candidates = [
|
| 149 |
+
DEFAULT_LOCAL_SPACE,
|
| 150 |
+
*[path for path in local_candidates if path != DEFAULT_LOCAL_SPACE],
|
| 151 |
+
]
|
| 152 |
+
return [str(path) for path in local_candidates]
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def available_artifact_uris() -> list[str]:
|
| 156 |
+
local_uris = local_artifact_uris()
|
| 157 |
+
if local_uris:
|
| 158 |
+
return local_uris
|
| 159 |
+
return list_s3_artifact_uris(make_s3_client())
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def artifact_name(uri: str) -> str:
|
| 163 |
+
uri = str(uri or "").strip().rstrip("/")
|
| 164 |
+
if not uri:
|
| 165 |
+
return "latest artifact"
|
| 166 |
+
if uri.startswith("s3://"):
|
| 167 |
+
parsed = urlparse(uri)
|
| 168 |
+
return Path(parsed.path).name or uri
|
| 169 |
+
return Path(uri).name or uri
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def artifact_choices() -> tuple[list[tuple[str, str]], str]:
|
| 173 |
+
uris = available_artifact_uris()
|
| 174 |
+
choices = [
|
| 175 |
+
(f"{artifact_name(uri)}{' (default)' if index == 0 else ''}", uri)
|
| 176 |
+
for index, uri in enumerate(uris)
|
| 177 |
+
]
|
| 178 |
+
return choices, uris[0]
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def format_duration(seconds: float) -> str:
|
| 182 |
+
if seconds < 1:
|
| 183 |
+
return f"{seconds * 1000:.0f} ms"
|
| 184 |
+
return f"{seconds:.1f}s"
|
| 185 |
+
|
| 186 |
+
|
| 187 |
def local_cache_for_uri(uri: str) -> Path:
|
| 188 |
_, key = parse_s3_uri(uri)
|
| 189 |
return CACHE_DIR / Path(key.rstrip("/")).name
|
| 190 |
|
| 191 |
|
| 192 |
+
def download_space_from_s3(uri: str | None = None) -> tuple[Path, str]:
|
| 193 |
client = make_s3_client()
|
| 194 |
+
uri = uri or latest_artifact_uri(client)
|
| 195 |
local_dir = local_cache_for_uri(uri)
|
| 196 |
local_dir.mkdir(parents=True, exist_ok=True)
|
| 197 |
|
|
|
|
| 208 |
return local_dir, uri
|
| 209 |
|
| 210 |
|
| 211 |
+
def find_space_dir(artifact_uri: str = "") -> tuple[Path, str]:
|
| 212 |
+
artifact_uri = str(artifact_uri or "").strip()
|
| 213 |
+
if artifact_uri:
|
| 214 |
+
if artifact_uri.startswith("s3://"):
|
| 215 |
+
return download_space_from_s3(artifact_uri)
|
| 216 |
+
path = Path(artifact_uri)
|
| 217 |
+
if path.exists():
|
| 218 |
+
return path, str(path)
|
| 219 |
+
raise FileNotFoundError(f"Selected artifact does not exist: {artifact_uri}")
|
| 220 |
+
|
| 221 |
local_override = os.getenv("SPACE_DIR", "").strip()
|
| 222 |
if local_override:
|
| 223 |
path = Path(local_override)
|
|
|
|
| 367 |
|
| 368 |
|
| 369 |
@lru_cache(maxsize=1)
|
| 370 |
+
def load_space(artifact_uri: str = "") -> Space:
|
| 371 |
+
space_dir, artifact_uri = find_space_dir(artifact_uri)
|
| 372 |
config = read_config(space_dir)
|
| 373 |
metadata, ids_by_lang = read_metadata(space_dir)
|
| 374 |
vectors_by_lang = load_vectors_from_faiss(space_dir, ids_by_lang)
|
|
|
|
| 591 |
filter_stopwords: bool,
|
| 592 |
filter_bad_tokens: bool,
|
| 593 |
use_surface: bool,
|
| 594 |
+
artifact_uri: str,
|
| 595 |
) -> tuple[str, list[list[Any]]]:
|
| 596 |
try:
|
| 597 |
+
space = load_space(artifact_uri)
|
| 598 |
use_surface = bool(use_surface and space.has_surface_forms)
|
| 599 |
opts = make_options(
|
| 600 |
top_k,
|
|
|
|
| 682 |
return f"Error: {exc}", []
|
| 683 |
|
| 684 |
|
| 685 |
+
def loaded_status(space: Space, elapsed: float) -> str:
|
| 686 |
+
total_vectors = sum(len(item.metas) for item in space.by_lang.values())
|
| 687 |
+
return (
|
| 688 |
+
f"Loaded {artifact_name(space.artifact_uri)} in {format_duration(elapsed)} "
|
| 689 |
+
f"with {total_vectors:,} vectors."
|
| 690 |
+
)
|
| 691 |
+
|
| 692 |
+
|
| 693 |
+
def load_artifact_settings(artifact_uri: str) -> tuple[Any, ...]:
|
| 694 |
+
start = time.perf_counter()
|
| 695 |
try:
|
| 696 |
+
space = load_space(artifact_uri)
|
| 697 |
opts = default_options(space.config)
|
| 698 |
source_lang = space.config.get("pivot_lang", "de")
|
| 699 |
if source_lang not in space.languages:
|
| 700 |
source_lang = space.languages[0]
|
| 701 |
+
status = loaded_status(space, time.perf_counter() - start)
|
|
|
|
|
|
|
|
|
|
| 702 |
return (
|
| 703 |
status,
|
| 704 |
gr.update(choices=space.languages, value=source_lang),
|
|
|
|
| 736 |
)
|
| 737 |
|
| 738 |
|
| 739 |
+
def initialize() -> tuple[Any, ...]:
|
| 740 |
+
try:
|
| 741 |
+
choices, artifact_uri = artifact_choices()
|
| 742 |
+
settings = load_artifact_settings(artifact_uri)
|
| 743 |
+
return (
|
| 744 |
+
settings[0],
|
| 745 |
+
gr.update(choices=choices, value=artifact_uri),
|
| 746 |
+
*settings[1:],
|
| 747 |
+
)
|
| 748 |
+
except Exception as exc:
|
| 749 |
+
return (
|
| 750 |
+
f"Load error: {exc}",
|
| 751 |
+
gr.update(choices=[], value=None),
|
| 752 |
+
gr.update(choices=DEFAULT_LANGS, value="de"),
|
| 753 |
+
3,
|
| 754 |
+
0.15,
|
| 755 |
+
10,
|
| 756 |
+
9,
|
| 757 |
+
50,
|
| 758 |
+
True,
|
| 759 |
+
gr.update(
|
| 760 |
+
value=False,
|
| 761 |
+
interactive=False,
|
| 762 |
+
label="show surface forms (no aligned space loaded)",
|
| 763 |
+
),
|
| 764 |
+
)
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def mark_artifact_loading(artifact_uri: str) -> tuple[str, str, list[list[Any]]]:
|
| 768 |
+
load_space.cache_clear()
|
| 769 |
+
gc.collect()
|
| 770 |
+
return f"Loading {artifact_name(artifact_uri)}...", "", []
|
| 771 |
+
|
| 772 |
+
|
| 773 |
CSS = """
|
| 774 |
body { background: #f7f5ef; }
|
| 775 |
.gradio-container { max-width: 1120px !important; }
|
|
|
|
| 786 |
elem_classes=["app-title"],
|
| 787 |
)
|
| 788 |
status = gr.Markdown("Loading artifacts...", elem_classes=["status"])
|
| 789 |
+
artifact_version = gr.Dropdown(
|
| 790 |
+
label="Dictionary artifact",
|
| 791 |
+
choices=[],
|
| 792 |
+
value=None,
|
| 793 |
+
interactive=True,
|
| 794 |
+
)
|
| 795 |
|
| 796 |
with gr.Row():
|
| 797 |
with gr.Column(scale=1, min_width=320):
|
|
|
|
| 919 |
filter_stopwords,
|
| 920 |
filter_bad_tokens,
|
| 921 |
use_surface,
|
| 922 |
+
artifact_version,
|
| 923 |
]
|
| 924 |
search.click(translate_like_terminal, inputs=inputs, outputs=[output_text, output_table])
|
| 925 |
query.submit(translate_like_terminal, inputs=inputs, outputs=[output_text, output_table])
|
| 926 |
+
artifact_version.change(
|
| 927 |
+
mark_artifact_loading,
|
| 928 |
+
inputs=[artifact_version],
|
| 929 |
+
outputs=[status, output_text, output_table],
|
| 930 |
+
).then(
|
| 931 |
+
load_artifact_settings,
|
| 932 |
+
inputs=[artifact_version],
|
| 933 |
+
outputs=[
|
| 934 |
+
status,
|
| 935 |
+
source_lang,
|
| 936 |
+
top_k,
|
| 937 |
+
min_score,
|
| 938 |
+
csls_k,
|
| 939 |
+
candidate_retrieval_k,
|
| 940 |
+
csls_prefetch_k,
|
| 941 |
+
bidirectional,
|
| 942 |
+
use_surface,
|
| 943 |
+
],
|
| 944 |
+
).then(translate_like_terminal, inputs=inputs, outputs=[output_text, output_table])
|
| 945 |
|
| 946 |
demo.load(
|
| 947 |
initialize,
|
| 948 |
outputs=[
|
| 949 |
status,
|
| 950 |
+
artifact_version,
|
| 951 |
source_lang,
|
| 952 |
top_k,
|
| 953 |
min_score,
|