Perplexed7675 commited on
Commit
133dffc
·
verified ·
1 Parent(s): 8b20fbe

Sync from kink_cli (Docker Space)

Browse files
Files changed (3) hide show
  1. api.py +34 -7
  2. backend/catalog.py +57 -1
  3. models.py +13 -0
api.py CHANGED
@@ -138,6 +138,25 @@ def _warn_or_fail_ephemeral_store(path: Path) -> None:
138
  raise RuntimeError(msg)
139
 
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  def _get_backend() -> Backend:
142
  """Open SQLite + warm caches on first use so uvicorn can bind before a multi‑GB Hub download finishes."""
143
  global _backend_impl
@@ -146,25 +165,33 @@ def _get_backend() -> Backend:
146
  with _backend_lock:
147
  if _backend_impl is not None:
148
  return _backend_impl
149
- ensure_media_cache(MEDIA_ROOT)
150
- path = ensure_store_db(_default_store)
 
 
 
151
  _warn_or_fail_ephemeral_store(path)
152
- _restore_user_snapshot_on_boot(path)
153
- b = Backend(path)
 
 
154
  # Catalog must be ready before the first recommendations request. Build it once here:
155
  # starting a warm thread and then blocking can double-build on slow cpu-basic Spaces.
156
- b._catalog()
 
157
  # PPR / full similarity graph warm is RAM-heavy on multi‑GB catalogs; skip only that on
158
  # small Spaces (see Dockerfile KINK_SKIP_HEAVY_WARM).
159
  if os.environ.get("KINK_SKIP_HEAVY_WARM", "").strip().lower() not in ("1", "true", "yes", "on"):
160
- from backend.recsys_graph import warm_ppr_caches
 
161
 
162
- warm_ppr_caches(b)
163
  _backend_impl = b
164
  if os.environ.get("KINK_BACKGROUND_PPR_WARM", "").strip().lower() in ("1", "true", "yes", "on"):
165
  from backend.recsys_graph import warm_ppr_caches_in_background
166
 
167
  warm_ppr_caches_in_background(b)
 
168
  return b
169
 
170
 
 
138
  raise RuntimeError(msg)
139
 
140
 
141
+ def _boot_phase(name: str) -> "_BootTimer":
142
+ return _BootTimer(name)
143
+
144
+
145
+ class _BootTimer:
146
+ def __init__(self, name: str) -> None:
147
+ self.name = name
148
+ self._t0 = 0.0
149
+
150
+ def __enter__(self) -> "_BootTimer":
151
+ self._t0 = time.monotonic()
152
+ return self
153
+
154
+ def __exit__(self, exc_type, exc, tb) -> None:
155
+ dt = time.monotonic() - self._t0
156
+ status = "err" if exc_type else "ok"
157
+ print(f"[kink_cli boot] phase={self.name} seconds={dt:.2f} status={status}", flush=True)
158
+
159
+
160
  def _get_backend() -> Backend:
161
  """Open SQLite + warm caches on first use so uvicorn can bind before a multi‑GB Hub download finishes."""
162
  global _backend_impl
 
165
  with _backend_lock:
166
  if _backend_impl is not None:
167
  return _backend_impl
168
+ total_t0 = time.monotonic()
169
+ with _boot_phase("media_cache"):
170
+ ensure_media_cache(MEDIA_ROOT)
171
+ with _boot_phase("ensure_store_db"):
172
+ path = ensure_store_db(_default_store)
173
  _warn_or_fail_ephemeral_store(path)
174
+ with _boot_phase("user_snapshot_restore"):
175
+ _restore_user_snapshot_on_boot(path)
176
+ with _boot_phase("backend_ctor"):
177
+ b = Backend(path)
178
  # Catalog must be ready before the first recommendations request. Build it once here:
179
  # starting a warm thread and then blocking can double-build on slow cpu-basic Spaces.
180
+ with _boot_phase("catalog_build"):
181
+ b._catalog()
182
  # PPR / full similarity graph warm is RAM-heavy on multi‑GB catalogs; skip only that on
183
  # small Spaces (see Dockerfile KINK_SKIP_HEAVY_WARM).
184
  if os.environ.get("KINK_SKIP_HEAVY_WARM", "").strip().lower() not in ("1", "true", "yes", "on"):
185
+ with _boot_phase("ppr_warm"):
186
+ from backend.recsys_graph import warm_ppr_caches
187
 
188
+ warm_ppr_caches(b)
189
  _backend_impl = b
190
  if os.environ.get("KINK_BACKGROUND_PPR_WARM", "").strip().lower() in ("1", "true", "yes", "on"):
191
  from backend.recsys_graph import warm_ppr_caches_in_background
192
 
193
  warm_ppr_caches_in_background(b)
194
+ print(f"[kink_cli boot] phase=total seconds={time.monotonic() - total_t0:.2f} status=ok", flush=True)
195
  return b
196
 
197
 
backend/catalog.py CHANGED
@@ -29,10 +29,60 @@ from models import (
29
  FetlifePictureRef,
30
  Kink,
31
  KinkExample,
 
32
  KinkScenarioParent,
33
  SimilarityEdge,
34
  )
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  _IMAGE_TOKEN_RE = re.compile(r"[a-z0-9]+")
37
  _IMAGE_RELEVANCE_STOPWORDS = {"and", "the", "play", "sex", "in", "with"}
38
  _PRODUCT_FLAG_KEYS = (
@@ -603,6 +653,7 @@ def _mark_runtime_duplicates(
603
  # "Cuddling after sex") that ``merge_signature``'s sorted-token-set misses. F1 0.80 on the
604
  # hand-labeled set vs F1 0.29 for ``merge_signature`` alone — see scripts/merge_dedup_eval.py.
605
  lemma_groups: defaultdict[tuple[str, str], list[str]] = defaultdict(list)
 
606
  for kid, summary in candidates.items():
607
  cluster = str(summary.get("cluster", "") or "")
608
  name = str(summary.get("name", "") or "")
@@ -612,9 +663,14 @@ def _mark_runtime_duplicates(
612
  cf = compact_form(name)
613
  if len(cf) >= 8:
614
  compact_groups[(cluster, cf)].append(kid)
615
- lemma_sig = lemma_signature(name)
 
 
 
 
616
  if lemma_sig:
617
  lemma_groups[(cluster, lemma_sig)].append(kid)
 
618
 
619
  def _canonical_key(kid: str) -> tuple[int, int, float, int, str]:
620
  """Sort key — higher is better. Strict preference: has_definition first, then well-cased
 
29
  FetlifePictureRef,
30
  Kink,
31
  KinkExample,
32
+ KinkLemma,
33
  KinkScenarioParent,
34
  SimilarityEdge,
35
  )
36
 
37
+
38
+ def _lemma_name_hash(name: str) -> str:
39
+ """Fingerprint a kink name so the lemma cache invalidates when the name changes."""
40
+ import hashlib
41
+ return hashlib.sha1(name.encode("utf-8")).hexdigest()
42
+
43
+
44
+ def _load_cached_lemma_signatures(
45
+ self,
46
+ candidates: dict[str, dict[str, Any]],
47
+ ) -> tuple[dict[str, str], list[tuple[str, str, str]]]:
48
+ """Return (cached_lemmas_by_kid, empty_writebacks_list).
49
+
50
+ A cached row is only returned when its ``name_hash`` matches the current candidate name —
51
+ stale rows are silently ignored (caller will recompute and overwrite).
52
+ """
53
+ try:
54
+ with self._sqlite() as conn:
55
+ rows = conn.execute("SELECT kink_id, name_hash, signature FROM kinklemma").fetchall()
56
+ except sqlite3.OperationalError:
57
+ return {}, []
58
+ cached: dict[str, str] = {}
59
+ for row in rows:
60
+ kid = row["kink_id"]
61
+ if kid not in candidates:
62
+ continue
63
+ if row["name_hash"] != _lemma_name_hash(str(candidates[kid].get("name", "") or "")):
64
+ continue
65
+ cached[kid] = row["signature"] or ""
66
+ return cached, []
67
+
68
+
69
+ def _persist_cached_lemma_signatures(
70
+ self,
71
+ rows: list[tuple[str, str, str]],
72
+ ) -> None:
73
+ if not rows:
74
+ return
75
+ try:
76
+ with self._sqlite() as conn:
77
+ conn.executemany(
78
+ "INSERT INTO kinklemma (kink_id, name_hash, signature) VALUES (?, ?, ?) "
79
+ "ON CONFLICT(kink_id) DO UPDATE SET name_hash=excluded.name_hash, signature=excluded.signature",
80
+ rows,
81
+ )
82
+ conn.commit()
83
+ except sqlite3.OperationalError:
84
+ return
85
+
86
  _IMAGE_TOKEN_RE = re.compile(r"[a-z0-9]+")
87
  _IMAGE_RELEVANCE_STOPWORDS = {"and", "the", "play", "sex", "in", "with"}
88
  _PRODUCT_FLAG_KEYS = (
 
653
  # "Cuddling after sex") that ``merge_signature``'s sorted-token-set misses. F1 0.80 on the
654
  # hand-labeled set vs F1 0.29 for ``merge_signature`` alone — see scripts/merge_dedup_eval.py.
655
  lemma_groups: defaultdict[tuple[str, str], list[str]] = defaultdict(list)
656
+ cached_lemmas, lemma_writebacks = _load_cached_lemma_signatures(self, candidates)
657
  for kid, summary in candidates.items():
658
  cluster = str(summary.get("cluster", "") or "")
659
  name = str(summary.get("name", "") or "")
 
663
  cf = compact_form(name)
664
  if len(cf) >= 8:
665
  compact_groups[(cluster, cf)].append(kid)
666
+ if kid in cached_lemmas:
667
+ lemma_sig = cached_lemmas[kid]
668
+ else:
669
+ lemma_sig = lemma_signature(name)
670
+ lemma_writebacks.append((kid, _lemma_name_hash(name), lemma_sig))
671
  if lemma_sig:
672
  lemma_groups[(cluster, lemma_sig)].append(kid)
673
+ _persist_cached_lemma_signatures(self, lemma_writebacks)
674
 
675
  def _canonical_key(kid: str) -> tuple[int, int, float, int, str]:
676
  """Sort key — higher is better. Strict preference: has_definition first, then well-cased
models.py CHANGED
@@ -151,6 +151,19 @@ class Alias(SQLModel, table=True):
151
  source_id: str = Field(foreign_key="source.id")
152
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  class KinkFacet(SQLModel, table=True):
155
  id: str = Field(primary_key=True)
156
  kink_id: str = Field(foreign_key="kink.id", index=True)
 
151
  source_id: str = Field(foreign_key="source.id")
152
 
153
 
154
+ class KinkLemma(SQLModel, table=True):
155
+ """Per-kink cached spaCy lemma signature used by play-dedupe at catalog build.
156
+
157
+ Populated lazily on first build and persisted so subsequent boots avoid the ~55s
158
+ spaCy pass over the full catalog. `name_hash` is the kink name at the time the
159
+ signature was computed; a name change forces a recompute via the dedupe loop.
160
+ """
161
+
162
+ kink_id: str = Field(primary_key=True, foreign_key="kink.id")
163
+ name_hash: str
164
+ signature: str
165
+
166
+
167
  class KinkFacet(SQLModel, table=True):
168
  id: str = Field(primary_key=True)
169
  kink_id: str = Field(foreign_key="kink.id", index=True)