Spaces:
Running
perf: shared cached model + background preload at home page
Browse filesThe parsing demo called spacy.load() directly on every rerun (the
@st .cache_resource loader beside it was dead code), so it reloaded the
~500MB lg model on every interaction — the main cause of its slowness.
The custom-label demo had the same uncached-load-per-rerun pattern.
Changes:
- Add model_helpers.py: one @st .cache_resource load_model() shared by all
read-only demos, so they reuse a single in-memory model instead of each
page caching its own copy. start_preload() warms the default lg model in
a daemon thread from app.py, so the model loads while the visitor reads
the home page and the first demo opened finds it already resident.
- Parsing/senter/NER/dependency demos now use the shared loader; drop their
per-page loaders and now-unused imports (spacy, gc, subprocess, json,
registry, and the dead load_model_in_subprocess helper).
- Custom-label demo keeps its OWN cached instance (it mutates the pipeline
via add_pipe("dcc_core")); sharing would leak that pipe into other demos.
Its load is now cached with an idempotent add_pipe guard.
Smoke-tested: shared loader returns a single instance, preload is
non-blocking, trf_vectors disabled, parse works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- app.py +7 -0
- model_helpers.py +63 -0
- pages/1_parsing_demo.py +7 -39
- pages/2_custom_label_demo.py +13 -4
- pages/3_senter_demo.py +3 -7
- pages/4_ner_demo.py +2 -6
- pages/5_dependency_demo.py +2 -6
|
@@ -1,10 +1,17 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
|
|
|
|
|
|
|
| 3 |
st.set_page_config(
|
| 4 |
page_title="LatinCy Dashboard | Home",
|
| 5 |
page_icon="🏠",
|
| 6 |
)
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
st.write("# LatinCy Dashboard")
|
| 9 |
|
| 10 |
st.sidebar.success("Select a demo above.")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
|
| 3 |
+
from model_helpers import start_preload
|
| 4 |
+
|
| 5 |
st.set_page_config(
|
| 6 |
page_title="LatinCy Dashboard | Home",
|
| 7 |
page_icon="🏠",
|
| 8 |
)
|
| 9 |
|
| 10 |
+
# Warm the default lg model in a background thread while the visitor reads this
|
| 11 |
+
# page, so the first demo they open finds it already loaded. Non-blocking and
|
| 12 |
+
# idempotent — safe to call on every render.
|
| 13 |
+
start_preload()
|
| 14 |
+
|
| 15 |
st.write("# LatinCy Dashboard")
|
| 16 |
|
| 17 |
st.sidebar.success("Select a demo above.")
|
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared spaCy model loading + background preload for the dashboard.
|
| 2 |
+
|
| 3 |
+
Read-only demos import ``load_model()`` from here so they all share ONE model
|
| 4 |
+
instance per name across the whole process, instead of each page caching its
|
| 5 |
+
own copy (which multiplied a ~500 MB model across pages in RAM).
|
| 6 |
+
``start_preload()`` warms the default model in a background thread from the
|
| 7 |
+
home page, so navigating into a demo finds the model already resident.
|
| 8 |
+
|
| 9 |
+
Demos that MUTATE their pipeline (e.g. the custom-label demo, which adds a
|
| 10 |
+
component with ``add_pipe``) must NOT use this shared instance — they should
|
| 11 |
+
cache their own copy — because mutating the shared model would leak the change
|
| 12 |
+
into every other demo.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import threading
|
| 16 |
+
|
| 17 |
+
import spacy
|
| 18 |
+
import streamlit as st
|
| 19 |
+
|
| 20 |
+
DEFAULT_MODEL = "la_core_web_lg"
|
| 21 |
+
|
| 22 |
+
# Single source of truth for loaded models, shared between the background
|
| 23 |
+
# preloader and the synchronous loader. Guarded by _lock so a preload and a
|
| 24 |
+
# concurrent load_model() can never start two loads of the same model.
|
| 25 |
+
_models = {}
|
| 26 |
+
_lock = threading.Lock()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _load(model_name):
|
| 30 |
+
"""Load a model with no Streamlit (st.*) calls — safe off the main thread."""
|
| 31 |
+
nlp = spacy.load(model_name)
|
| 32 |
+
if "trf_vectors" in nlp.pipe_names: # heavy, unused in these demos
|
| 33 |
+
nlp.disable_pipe("trf_vectors")
|
| 34 |
+
return nlp
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _get(model_name):
|
| 38 |
+
with _lock:
|
| 39 |
+
if model_name not in _models:
|
| 40 |
+
_models[model_name] = _load(model_name)
|
| 41 |
+
return _models[model_name]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def start_preload(model_name=DEFAULT_MODEL):
|
| 45 |
+
"""Kick off a non-blocking background load of the default model.
|
| 46 |
+
|
| 47 |
+
Called from the home page so the model warms while the user reads the demo
|
| 48 |
+
list. Cheap to call repeatedly: once loaded, the worker returns at once.
|
| 49 |
+
"""
|
| 50 |
+
if model_name in _models:
|
| 51 |
+
return
|
| 52 |
+
threading.Thread(target=_get, args=(model_name,), daemon=True).start()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@st.cache_resource(show_spinner="Loading LatinCy model…")
|
| 56 |
+
def load_model(model_name=DEFAULT_MODEL):
|
| 57 |
+
"""Return the shared, cached spaCy model for ``model_name``.
|
| 58 |
+
|
| 59 |
+
Reuses the background-preloaded instance when ready; otherwise loads it once
|
| 60 |
+
synchronously. Read-only use only — do not ``add_pipe``/``disable`` on the
|
| 61 |
+
result, since every demo shares this instance.
|
| 62 |
+
"""
|
| 63 |
+
return _get(model_name)
|
|
@@ -1,11 +1,8 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
-
import spacy
|
| 3 |
import pandas as pd
|
| 4 |
import datetime
|
| 5 |
-
|
| 6 |
-
import
|
| 7 |
-
import json
|
| 8 |
-
from spacy.util import registry
|
| 9 |
|
| 10 |
st.set_page_config(page_title="Parsing Demo", layout="wide")
|
| 11 |
st.sidebar.header("Parsing Demo")
|
|
@@ -73,41 +70,12 @@ def analyze_text(text):
|
|
| 73 |
|
| 74 |
st.title("LatinCy Text Analyzer")
|
| 75 |
|
| 76 |
-
#
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
# Function to unload the current model
|
| 82 |
-
@st.cache_resource
|
| 83 |
-
def load_model(model_name):
|
| 84 |
-
gc.collect() # Attempt to free memory
|
| 85 |
-
nlp = spacy.load(model_name)
|
| 86 |
-
try:
|
| 87 |
-
if "trf_vectors" in nlp.pipe_names: # Disable trf_vectors if it exists
|
| 88 |
-
nlp.disable_pipe("trf_vectors")
|
| 89 |
-
except Exception as e:
|
| 90 |
-
st.warning(f"Failed to disable 'trf_vectors': {e}")
|
| 91 |
-
return nlp
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
# Function to load model in a subprocess to avoid conflicts
|
| 95 |
-
def load_model_in_subprocess(model_name):
|
| 96 |
-
script = (
|
| 97 |
-
"import spacy, json; "
|
| 98 |
-
'nlp = spacy.load("' + model_name + '"); '
|
| 99 |
-
"print(json.dumps(nlp.meta))"
|
| 100 |
-
)
|
| 101 |
-
result = subprocess.run(["python", "-c", script], capture_output=True, text=True)
|
| 102 |
-
if result.returncode != 0:
|
| 103 |
-
raise RuntimeError(f"Failed to load model {model_name}: {result.stderr}")
|
| 104 |
-
return json.loads(result.stdout)
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
model_name = "la_core_web_lg" # Hardcoded to use only the lg model
|
| 108 |
-
nlp = spacy.load(model_name) # Directly load the lg model
|
| 109 |
|
| 110 |
-
st.write(f"Loaded model: {model_name} (v{
|
| 111 |
|
| 112 |
df = None
|
| 113 |
|
|
|
|
| 1 |
import streamlit as st
|
|
|
|
| 2 |
import pandas as pd
|
| 3 |
import datetime
|
| 4 |
+
|
| 5 |
+
from model_helpers import load_model
|
|
|
|
|
|
|
| 6 |
|
| 7 |
st.set_page_config(page_title="Parsing Demo", layout="wide")
|
| 8 |
st.sidebar.header("Parsing Demo")
|
|
|
|
| 70 |
|
| 71 |
st.title("LatinCy Text Analyzer")
|
| 72 |
|
| 73 |
+
# Shared, cached lg model — warmed at startup by the home-page preloader, so
|
| 74 |
+
# this returns instantly on all but the very first (cold) load.
|
| 75 |
+
model_name = "la_core_web_lg"
|
| 76 |
+
nlp = load_model(model_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
st.write(f"Loaded model: {model_name} (v{nlp.meta['version']})")
|
| 79 |
|
| 80 |
df = None
|
| 81 |
|
|
@@ -46,10 +46,19 @@ model_selectbox = st.sidebar.selectbox(
|
|
| 46 |
("la_core_web_lg", "la_core_web_md", "la_core_web_sm")
|
| 47 |
)
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
#
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
tab1, tab2 = st.tabs(["Analyze", "About"])
|
| 55 |
|
|
|
|
| 46 |
("la_core_web_lg", "la_core_web_md", "la_core_web_sm")
|
| 47 |
)
|
| 48 |
|
| 49 |
+
# This demo mutates its pipeline (adds the dcc_core component), so it caches
|
| 50 |
+
# its OWN instance rather than sharing the read-only model from model_helpers —
|
| 51 |
+
# mutating a shared model would leak dcc_core into the other demos. The guard
|
| 52 |
+
# keeps add_pipe idempotent across Streamlit reruns of the cached instance.
|
| 53 |
+
@st.cache_resource
|
| 54 |
+
def load_dcc_model(model_name):
|
| 55 |
+
nlp = spacy.load(model_name)
|
| 56 |
+
if "dcc_core" not in nlp.pipe_names:
|
| 57 |
+
nlp.add_pipe("dcc_core", last=True)
|
| 58 |
+
return nlp
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
nlp = load_dcc_model(model_selectbox)
|
| 62 |
|
| 63 |
tab1, tab2 = st.tabs(["Analyze", "About"])
|
| 64 |
|
|
@@ -1,17 +1,13 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
-
import spacy
|
| 3 |
import datetime
|
| 4 |
|
|
|
|
|
|
|
| 5 |
st.set_page_config(page_title="Sentence Segmenter", layout="wide")
|
| 6 |
st.sidebar.header("Sentence Segmenter")
|
| 7 |
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
@st.cache_resource
|
| 11 |
-
def load_model():
|
| 12 |
-
return spacy.load("la_core_web_lg")
|
| 13 |
-
|
| 14 |
-
|
| 15 |
nlp = load_model()
|
| 16 |
|
| 17 |
st.title("Latin Sentence Segmenter")
|
|
|
|
| 1 |
import streamlit as st
|
|
|
|
| 2 |
import datetime
|
| 3 |
|
| 4 |
+
from model_helpers import load_model
|
| 5 |
+
|
| 6 |
st.set_page_config(page_title="Sentence Segmenter", layout="wide")
|
| 7 |
st.sidebar.header("Sentence Segmenter")
|
| 8 |
|
| 9 |
|
| 10 |
+
# Shared, cached lg model (warmed by the home-page preloader)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
nlp = load_model()
|
| 12 |
|
| 13 |
st.title("Latin Sentence Segmenter")
|
|
@@ -1,7 +1,8 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
-
import spacy
|
| 3 |
from spacy_streamlit import visualize_ner
|
| 4 |
|
|
|
|
|
|
|
| 5 |
st.set_page_config(page_title="NER Demo", layout="wide")
|
| 6 |
st.sidebar.header("NER Demo")
|
| 7 |
|
|
@@ -23,11 +24,6 @@ model_selectbox = st.sidebar.selectbox(
|
|
| 23 |
)
|
| 24 |
|
| 25 |
|
| 26 |
-
@st.cache_resource
|
| 27 |
-
def load_model(model_name):
|
| 28 |
-
return spacy.load(model_name)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
nlp = load_model(model_selectbox)
|
| 32 |
|
| 33 |
tab1, tab2 = st.tabs(["Recognize", "About"])
|
|
|
|
| 1 |
import streamlit as st
|
|
|
|
| 2 |
from spacy_streamlit import visualize_ner
|
| 3 |
|
| 4 |
+
from model_helpers import load_model
|
| 5 |
+
|
| 6 |
st.set_page_config(page_title="NER Demo", layout="wide")
|
| 7 |
st.sidebar.header("NER Demo")
|
| 8 |
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
nlp = load_model(model_selectbox)
|
| 28 |
|
| 29 |
tab1, tab2 = st.tabs(["Recognize", "About"])
|
|
@@ -1,7 +1,8 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
-
import spacy
|
| 3 |
from spacy_streamlit import visualize_parser
|
| 4 |
|
|
|
|
|
|
|
| 5 |
st.set_page_config(page_title="Dependency Demo", layout="wide")
|
| 6 |
st.sidebar.header("Dependency Demo")
|
| 7 |
|
|
@@ -32,11 +33,6 @@ model_selectbox = st.sidebar.selectbox(
|
|
| 32 |
compact = st.sidebar.checkbox("Compact mode", value=False)
|
| 33 |
|
| 34 |
|
| 35 |
-
@st.cache_resource
|
| 36 |
-
def load_model(model_name):
|
| 37 |
-
return spacy.load(model_name)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
nlp = load_model(model_selectbox)
|
| 41 |
|
| 42 |
tab1, tab2 = st.tabs(["Parse", "About"])
|
|
|
|
| 1 |
import streamlit as st
|
|
|
|
| 2 |
from spacy_streamlit import visualize_parser
|
| 3 |
|
| 4 |
+
from model_helpers import load_model
|
| 5 |
+
|
| 6 |
st.set_page_config(page_title="Dependency Demo", layout="wide")
|
| 7 |
st.sidebar.header("Dependency Demo")
|
| 8 |
|
|
|
|
| 33 |
compact = st.sidebar.checkbox("Compact mode", value=False)
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
nlp = load_model(model_selectbox)
|
| 37 |
|
| 38 |
tab1, tab2 = st.tabs(["Parse", "About"])
|