devaanand commited on
Commit
a9d817a
·
1 Parent(s): 01ad145

new upload restrictions

Browse files
backend/app/ccr.py CHANGED
@@ -42,16 +42,39 @@ class SentenceTransformerBackend:
42
 
43
  _cache: dict[str, object] = {}
44
 
45
- def __init__(self, model_name: str, revision: str | None = None):
 
 
 
 
 
 
46
  self.name = model_name
47
  self.revision = revision
 
 
48
 
49
  def _model(self):
50
  if self.name not in self._cache:
51
  from sentence_transformers import SentenceTransformer # lazy: heavy import
52
 
53
- kwargs = {"revision": self.revision} if self.revision else {}
54
- self._cache[self.name] = SentenceTransformer(self.name, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  return self._cache[self.name]
56
 
57
  @property
@@ -68,7 +91,7 @@ class SentenceTransformerBackend:
68
  emb = model.encode(
69
  texts[i : i + batch],
70
  convert_to_numpy=True,
71
- normalize_embeddings=True,
72
  show_progress_bar=False,
73
  )
74
  out.append(emb)
@@ -115,7 +138,12 @@ def get_backend(model_id: str) -> EmbeddingBackend:
115
  from . import registry # local import: engine stays importable without yaml deps
116
 
117
  cfg = registry.get_model(model_id)
118
- return SentenceTransformerBackend(cfg.provider_model_id, revision=cfg.pinned_revision)
 
 
 
 
 
119
 
120
 
121
  @dataclass
@@ -201,8 +229,8 @@ def run_ccr(
201
  doc_emb = encode_unique(backend, texts_for_encoding, progress_cb=doc_progress)
202
 
203
  # Both matrices are L2-normalized -> cosine similarity is a dot product.
204
- sims = doc_emb @ item_emb.T
205
- scores = sims.mean(axis=1)
206
 
207
  finished = datetime.now(timezone.utc)
208
  items_hash = hashlib.sha256("\n".join(items).encode()).hexdigest()[:16]
 
42
 
43
  _cache: dict[str, object] = {}
44
 
45
+ def __init__(
46
+ self,
47
+ model_name: str,
48
+ revision: str | None = None,
49
+ pooling_fallback: str | None = None,
50
+ max_seq_length: int | None = None,
51
+ ):
52
  self.name = model_name
53
  self.revision = revision
54
+ self.pooling_fallback = pooling_fallback
55
+ self._max_seq_length = max_seq_length
56
 
57
  def _model(self):
58
  if self.name not in self._cache:
59
  from sentence_transformers import SentenceTransformer # lazy: heavy import
60
 
61
+ if self.pooling_fallback:
62
+ # Registry-flagged repos ship modules.json without the pooling
63
+ # config it references, so auto-loading fails; build the module
64
+ # stack explicitly with the pooling the model card documents.
65
+ from sentence_transformers import models as st_models
66
+
67
+ margs = {"model_args": {"revision": self.revision}} if self.revision else {}
68
+ word = st_models.Transformer(
69
+ self.name, max_seq_length=self._max_seq_length, **margs
70
+ )
71
+ # renamed in newer sentence-transformers; support both
72
+ get_dim = getattr(word, "get_embedding_dimension", None) or word.get_word_embedding_dimension
73
+ pool = st_models.Pooling(get_dim(), pooling_mode=self.pooling_fallback)
74
+ self._cache[self.name] = SentenceTransformer(modules=[word, pool])
75
+ else:
76
+ kwargs = {"revision": self.revision} if self.revision else {}
77
+ self._cache[self.name] = SentenceTransformer(self.name, **kwargs)
78
  return self._cache[self.name]
79
 
80
  @property
 
91
  emb = model.encode(
92
  texts[i : i + batch],
93
  convert_to_numpy=True,
94
+ normalize_embeddings=True, # scales every vector to length 1
95
  show_progress_bar=False,
96
  )
97
  out.append(emb)
 
138
  from . import registry # local import: engine stays importable without yaml deps
139
 
140
  cfg = registry.get_model(model_id)
141
+ return SentenceTransformerBackend(
142
+ cfg.provider_model_id,
143
+ revision=cfg.pinned_revision,
144
+ pooling_fallback=cfg.pooling_fallback,
145
+ max_seq_length=cfg.max_seq_length,
146
+ )
147
 
148
 
149
  @dataclass
 
229
  doc_emb = encode_unique(backend, texts_for_encoding, progress_cb=doc_progress)
230
 
231
  # Both matrices are L2-normalized -> cosine similarity is a dot product.
232
+ sims = doc_emb @ item_emb.T # similarity of every text to every item
233
+ scores = sims.mean(axis=1) # average across items = the CCR score
234
 
235
  finished = datetime.now(timezone.utc)
236
  items_hash = hashlib.sha256("\n".join(items).encode()).hexdigest()[:16]
backend/app/jobs.py CHANGED
@@ -304,6 +304,8 @@ def run_job(job_id: str) -> None:
304
  "model_registry_id": model_cfg.id if model_cfg else job.model_name,
305
  "provider_model_id": model_cfg.provider_model_id if model_cfg else job.model_name,
306
  "model_revision": model_cfg.revision if model_cfg else None,
 
 
307
  "scoring": {"adjustment_strategy": "none", "aggregate": "mean_all_items"},
308
  "output_schema": (
309
  list(work_df.columns)
 
304
  "model_registry_id": model_cfg.id if model_cfg else job.model_name,
305
  "provider_model_id": model_cfg.provider_model_id if model_cfg else job.model_name,
306
  "model_revision": model_cfg.revision if model_cfg else None,
307
+ "model_pooling_fallback": model_cfg.pooling_fallback if model_cfg else None,
308
+ "model_max_seq_length": model_cfg.max_seq_length if model_cfg else None,
309
  "scoring": {"adjustment_strategy": "none", "aggregate": "mean_all_items"},
310
  "output_schema": (
311
  list(work_df.columns)
backend/app/registry.py CHANGED
@@ -49,6 +49,11 @@ class ModelConfig:
49
  text_prefix: str
50
  normalize_embeddings: bool
51
  lazy_load: bool
 
 
 
 
 
52
  user_warnings: tuple[str, ...] = field(default_factory=tuple)
53
 
54
  @property
@@ -89,6 +94,7 @@ def _parse_model(raw: dict, lang_sets) -> ModelConfig:
89
  text_prefix=usage.get("text_prefix") or sym or "",
90
  normalize_embeddings=bool(usage.get("normalize_embeddings", True)),
91
  lazy_load=bool(ops.get("lazy_load", False)),
 
92
  user_warnings=tuple(raw.get("warnings", []) or []),
93
  )
94
 
 
49
  text_prefix: str
50
  normalize_embeddings: bool
51
  lazy_load: bool
52
+ # Set ("mean"/"cls") for repos whose sentence-transformers packaging is
53
+ # incomplete (modules.json references a pooling config the repo lacks):
54
+ # the backend then builds Transformer+Pooling explicitly instead of
55
+ # relying on SentenceTransformer(id) auto-loading.
56
+ pooling_fallback: str | None = None
57
  user_warnings: tuple[str, ...] = field(default_factory=tuple)
58
 
59
  @property
 
94
  text_prefix=usage.get("text_prefix") or sym or "",
95
  normalize_embeddings=bool(usage.get("normalize_embeddings", True)),
96
  lazy_load=bool(ops.get("lazy_load", False)),
97
+ pooling_fallback=usage.get("pooling_fallback") or None,
98
  user_warnings=tuple(raw.get("warnings", []) or []),
99
  )
100
 
backend/app/reproducibility.py CHANGED
@@ -47,6 +47,23 @@ def script_text(metadata: dict) -> str:
47
  provider = metadata.get("provider_model_id", metadata.get("model", ""))
48
  revision = metadata.get("model_revision")
49
  revision_arg = f", revision={revision!r}" if revision and revision != "PIN_ME" else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  item_prefix = metadata.get("item_prefix", "")
51
  text_prefix = metadata.get("text_prefix", "")
52
  text_column = metadata.get("text_column", "text")
@@ -78,7 +95,7 @@ import sys
78
 
79
  import numpy as np
80
  import pandas as pd
81
- from sentence_transformers import SentenceTransformer
82
 
83
  TEXT_COLUMN = {text_column!r}
84
  ITEM_PREFIX = {item_prefix!r} # model-required prefix (E5 family); empty = none
@@ -94,7 +111,7 @@ def main(csv_path: str) -> None:
94
  work = df.loc[mask].reset_index(drop=True) # platform drops empty rows the same way
95
  texts = work[TEXT_COLUMN].astype(str).tolist()
96
 
97
- model = SentenceTransformer({provider!r}{revision_arg})
98
  item_texts = [ITEM_PREFIX + i["text"] for i in ITEMS]
99
  doc_texts = [TEXT_PREFIX + t for t in texts]
100
 
 
47
  provider = metadata.get("provider_model_id", metadata.get("model", ""))
48
  revision = metadata.get("model_revision")
49
  revision_arg = f", revision={revision!r}" if revision and revision != "PIN_ME" else ""
50
+ pooling_fallback = metadata.get("model_pooling_fallback")
51
+ max_seq = metadata.get("model_max_seq_length")
52
+ if pooling_fallback:
53
+ # Repos with incomplete sentence-transformers packaging (pooling config
54
+ # missing) must be assembled module-by-module, exactly as the platform
55
+ # backend does - otherwise the exported script cannot load the model.
56
+ rev_model_args = f", model_args={{'revision': {revision!r}}}" if revision and revision != "PIN_ME" else ""
57
+ model_loader = (
58
+ f"word = st_models.Transformer({provider!r}, max_seq_length={max_seq!r}{rev_model_args})\n"
59
+ f" get_dim = getattr(word, 'get_embedding_dimension', None) or word.get_word_embedding_dimension\n"
60
+ f" pool = st_models.Pooling(get_dim(), pooling_mode={pooling_fallback!r})\n"
61
+ f" model = SentenceTransformer(modules=[word, pool])"
62
+ )
63
+ st_import = "from sentence_transformers import SentenceTransformer\nfrom sentence_transformers import models as st_models"
64
+ else:
65
+ model_loader = f"model = SentenceTransformer({provider!r}{revision_arg})"
66
+ st_import = "from sentence_transformers import SentenceTransformer"
67
  item_prefix = metadata.get("item_prefix", "")
68
  text_prefix = metadata.get("text_prefix", "")
69
  text_column = metadata.get("text_column", "text")
 
95
 
96
  import numpy as np
97
  import pandas as pd
98
+ {st_import}
99
 
100
  TEXT_COLUMN = {text_column!r}
101
  ITEM_PREFIX = {item_prefix!r} # model-required prefix (E5 family); empty = none
 
111
  work = df.loc[mask].reset_index(drop=True) # platform drops empty rows the same way
112
  texts = work[TEXT_COLUMN].astype(str).tolist()
113
 
114
+ {model_loader}
115
  item_texts = [ITEM_PREFIX + i["text"] for i in ITEMS]
116
  doc_texts = [TEXT_PREFIX + t for t in texts]
117
 
backend/tests/test_registry_and_prefixes.py CHANGED
@@ -108,3 +108,43 @@ def test_model_language_unsupported_warning():
108
  assert w["code"] == "MODEL_LANGUAGE_UNSUPPORTED"
109
  assert model_language_warning("en", "m", frozenset({"en"}), None) is None
110
  assert model_language_warning("zz", "m", frozenset(), None) is None # unknown coverage: no warning
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  assert w["code"] == "MODEL_LANGUAGE_UNSUPPORTED"
109
  assert model_language_warning("en", "m", frozenset({"en"}), None) is None
110
  assert model_language_warning("zz", "m", frozenset(), None) is None # unknown coverage: no warning
111
+
112
+
113
+ # ------------------------------------- psyembedding models (pooling fallback)
114
+ def test_psyembedding_models_registered_with_pooling_fallback():
115
+ """The published PsyEmbedding repos lack the 1_Pooling config their
116
+ modules.json references, so plain SentenceTransformer(id) cannot load
117
+ them; the registry must flag them for explicit module assembly."""
118
+ from app import registry
119
+
120
+ psy = [m for m in registry.list_models() if m.id.startswith("psyembedding-")]
121
+ assert len(psy) == 4
122
+ for m in psy:
123
+ assert m.pooling_fallback == "mean", m.id
124
+ assert not m.requires_prefix, m.id
125
+ assert m.embedding_dimension == 1024 and m.max_seq_length == 512, m.id
126
+ assert m.lazy_load, m.id
127
+
128
+
129
+ def test_repro_script_uses_explicit_modules_for_pooling_fallback():
130
+ from app.reproducibility import script_text
131
+
132
+ meta = {
133
+ "construct_snapshot": {"items": [{"text": "I am satisfied.", "reverse_scored": False}],
134
+ "name": "X", "version": 1, "item_hash": "ab" * 16},
135
+ "model_registry_id": "psyembedding-bert-large",
136
+ "provider_model_id": "Culture-and-Morality-Lab/psyembedding-bert-large-uncased",
137
+ "model_revision": "PIN_ME",
138
+ "model_pooling_fallback": "mean",
139
+ "model_max_seq_length": 512,
140
+ "text_column": "text",
141
+ }
142
+ src = script_text(meta)
143
+ compile(src, "reproduce_analysis.py", "exec")
144
+ assert "st_models.Pooling" in src and "pooling_mode='mean'" in src
145
+ # plain models keep the one-line loader
146
+ plain = script_text({**meta, "model_pooling_fallback": None,
147
+ "model_registry_id": "all-minilm-l6-v2",
148
+ "provider_model_id": "sentence-transformers/all-MiniLM-L6-v2"})
149
+ compile(plain, "reproduce_analysis.py", "exec")
150
+ assert "st_models" not in plain
packages/model_registry/models.yaml CHANGED
@@ -77,3 +77,127 @@ models:
77
  warnings:
78
  - "Quality varies by language, especially lower-resource languages."
79
  - "Scores are not comparable across model families."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  warnings:
78
  - "Quality varies by language, especially lower-resource languages."
79
  - "Scores are not comparable across model families."
80
+
81
+ # PsyEmbedding family (Culture & Morality Lab): sentence-transformers models
82
+ # fine-tuned for psychological text analysis on sentence-similarity pairs.
83
+ # Added on PI request 2026-07-18. All four: mean pooling, no prefix, English,
84
+ # 1024-dim, 512-token window, ~1.3 GB download on first use (lazy).
85
+
86
+ - id: psyembedding-bert-large
87
+ provider_model_id: Culture-and-Morality-Lab/psyembedding-bert-large-uncased
88
+ display_name: "PsyEmbedding BERT Large (psychology fine-tune)"
89
+ revision: PIN_ME
90
+ default: false
91
+ supported_languages: ["en"]
92
+ embedding_dimension: 1024
93
+ max_seq_length: 512
94
+ quality_tier: specialized
95
+ speed_tier: slow
96
+ usage_config:
97
+ requires_prefix: false
98
+ symmetric_similarity_prefix: null
99
+ pooling: mean # per model card; see pooling_fallback
100
+ # The published repos' modules.json references a 1_Pooling config the
101
+ # repos do not contain, so SentenceTransformer(id) cannot auto-load
102
+ # them; the backend builds Transformer+mean-Pooling explicitly.
103
+ pooling_fallback: mean
104
+ normalize_embeddings: true
105
+ operational_config:
106
+ expected_time_per_1k_texts_cpu: benchmark_required
107
+ worker_ram_min_gb: 4
108
+ lazy_load: true
109
+ max_resident_models: 1
110
+ warnings:
111
+ - "Fine-tuned for psychological text; general-domain performance untested."
112
+ - "Substantially slower and heavier than MiniLM."
113
+ - "Scores are not comparable across model families."
114
+
115
+ - id: psyembedding-roberta-large
116
+ provider_model_id: Culture-and-Morality-Lab/psyembedding-roberta-large
117
+ display_name: "PsyEmbedding RoBERTa Large (psychology fine-tune)"
118
+ revision: PIN_ME
119
+ default: false
120
+ supported_languages: ["en"]
121
+ embedding_dimension: 1024
122
+ max_seq_length: 512
123
+ quality_tier: specialized
124
+ speed_tier: slow
125
+ usage_config:
126
+ requires_prefix: false
127
+ symmetric_similarity_prefix: null
128
+ pooling: mean # per model card; see pooling_fallback
129
+ # The published repos' modules.json references a 1_Pooling config the
130
+ # repos do not contain, so SentenceTransformer(id) cannot auto-load
131
+ # them; the backend builds Transformer+mean-Pooling explicitly.
132
+ pooling_fallback: mean
133
+ normalize_embeddings: true
134
+ operational_config:
135
+ expected_time_per_1k_texts_cpu: benchmark_required
136
+ worker_ram_min_gb: 4
137
+ lazy_load: true
138
+ max_resident_models: 1
139
+ warnings:
140
+ - "Fine-tuned for psychological text; general-domain performance untested."
141
+ - "Substantially slower and heavier than MiniLM."
142
+ - "Scores are not comparable across model families."
143
+
144
+ - id: psyembedding-gte-large
145
+ provider_model_id: Culture-and-Morality-Lab/psyembedding-gte-large
146
+ display_name: "PsyEmbedding GTE Large (psychology fine-tune)"
147
+ revision: PIN_ME
148
+ default: false
149
+ supported_languages: ["en"]
150
+ embedding_dimension: 1024
151
+ max_seq_length: 512
152
+ quality_tier: specialized
153
+ speed_tier: slow
154
+ usage_config:
155
+ requires_prefix: false
156
+ symmetric_similarity_prefix: null
157
+ pooling: mean # per model card; see pooling_fallback
158
+ # The published repos' modules.json references a 1_Pooling config the
159
+ # repos do not contain, so SentenceTransformer(id) cannot auto-load
160
+ # them; the backend builds Transformer+mean-Pooling explicitly.
161
+ pooling_fallback: mean
162
+ normalize_embeddings: true
163
+ operational_config:
164
+ expected_time_per_1k_texts_cpu: benchmark_required
165
+ worker_ram_min_gb: 4
166
+ lazy_load: true
167
+ max_resident_models: 1
168
+ warnings:
169
+ - "Fine-tuned for psychological text; general-domain performance untested."
170
+ - "Substantially slower and heavier than MiniLM."
171
+ - "Scores are not comparable across model families."
172
+
173
+ - id: psyembedding-e5-large-v2
174
+ provider_model_id: Culture-and-Morality-Lab/psyembedding-e5-large-v2
175
+ display_name: "PsyEmbedding E5 Large v2 (psychology fine-tune)"
176
+ revision: PIN_ME
177
+ default: false
178
+ supported_languages: ["en"]
179
+ embedding_dimension: 1024
180
+ max_seq_length: 512
181
+ quality_tier: specialized
182
+ speed_tier: slow
183
+ usage_config:
184
+ # Unlike base intfloat/e5-large-v2, the fine-tune's card specifies direct
185
+ # encoding with NO "query: " prefix; the ST package carries its own
186
+ # pooling+normalize config, so we match the card exactly.
187
+ requires_prefix: false
188
+ symmetric_similarity_prefix: null
189
+ pooling: mean # per model card; see pooling_fallback
190
+ # The published repos' modules.json references a 1_Pooling config the
191
+ # repos do not contain, so SentenceTransformer(id) cannot auto-load
192
+ # them; the backend builds Transformer+mean-Pooling explicitly.
193
+ pooling_fallback: mean
194
+ normalize_embeddings: true
195
+ operational_config:
196
+ expected_time_per_1k_texts_cpu: benchmark_required
197
+ worker_ram_min_gb: 4
198
+ lazy_load: true
199
+ max_resident_models: 1
200
+ warnings:
201
+ - "Fine-tuned for psychological text; general-domain performance untested."
202
+ - "Substantially slower and heavier than MiniLM."
203
+ - "Scores are not comparable across model families."