TdelaSelle commited on
Commit
da96d8d
·
1 Parent(s): 141e24d

Add semtantic segmentation + Change layout + minor fixes + drop hide negatives

Browse files
Files changed (4) hide show
  1. Paraphrasis_main.html +304 -0
  2. app.py +262 -48
  3. paraphrasis_annotator.html +1 -71
  4. requirements.txt +2 -1
Paraphrasis_main.html ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Paraphrasis — Similarité sémantique entre corpus</title>
7
+ <meta name="description" content="Paraphrasis projette deux corpus de textes dans un espace vectoriel dense et mesure leur proximité sémantique phrase à phrase, par similarité cosinus. Outil de linguistique computationnelle pour les sciences humaines.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Spectral:ital,wght@0,400;0,500;1,400&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
11
+ <style>
12
+ :root{
13
+ --paper: #e7e3d9;
14
+ --paper-2: #ddd8cb;
15
+ --ink: #1b1d21;
16
+ --ink-soft: #6c685d;
17
+ --lapis: #28349a;
18
+ --lapis-ink:#1d2670;
19
+ --hair: #c7c2b4;
20
+ --maxw: 1140px;
21
+ }
22
+
23
+ *{box-sizing:border-box;}
24
+ html{-webkit-text-size-adjust:100%;}
25
+ body{
26
+ margin:0;
27
+ background:var(--paper);
28
+ color:var(--ink);
29
+ font-family:"Spectral",Georgia,serif;
30
+ font-size:18px;
31
+ line-height:1.65;
32
+ -webkit-font-smoothing:antialiased;
33
+ }
34
+ ::selection{background:var(--lapis);color:var(--paper);}
35
+
36
+ a{color:var(--lapis-ink);text-decoration-thickness:1px;text-underline-offset:3px;}
37
+ a:focus-visible,button:focus-visible{
38
+ outline:2px solid var(--lapis);
39
+ outline-offset:3px;
40
+ }
41
+
42
+ .wrap{max-width:var(--maxw);margin:0 auto;padding:0 28px;}
43
+
44
+ .mono{
45
+ font-family:"IBM Plex Mono",ui-monospace,monospace;
46
+ font-size:.72rem;
47
+ letter-spacing:.14em;
48
+ text-transform:uppercase;
49
+ }
50
+
51
+ /* ---------- Hero ---------- */
52
+ .hero{padding:64px 0 40px;}
53
+ .hero-grid{
54
+ display:grid;
55
+ grid-template-columns:1.15fr .85fr;
56
+ gap:48px;
57
+ align-items:center;
58
+ }
59
+ .eyebrow{color:var(--lapis-ink);margin:0 0 22px;}
60
+ .eyebrow::before{
61
+ content:"";display:inline-block;width:26px;height:1px;
62
+ background:var(--lapis-ink);vertical-align:middle;margin-right:12px;
63
+ }
64
+ h1.title{
65
+ font-family:"Fraunces",serif;
66
+ font-weight:600;
67
+ font-size:clamp(3.4rem,9vw,6.6rem);
68
+ line-height:.92;
69
+ letter-spacing:-.015em;
70
+ margin:0;
71
+ }
72
+ h1.title .dot{color:var(--lapis);}
73
+ .thesis{
74
+ font-size:clamp(1.15rem,2.4vw,1.5rem);
75
+ line-height:1.4;
76
+ max-width:30ch;
77
+ margin:24px 0 0;
78
+ color:var(--ink);
79
+ }
80
+ .thesis em{font-style:italic;color:var(--lapis-ink);}
81
+
82
+ /* ---------- Signature: cosine diagram ---------- */
83
+ .figure{align-self:center;}
84
+ .figure svg{width:100%;height:auto;display:block;}
85
+ .figure figcaption{
86
+ margin-top:14px;text-align:center;color:var(--ink-soft);
87
+ }
88
+ @media (prefers-reduced-motion:no-preference){
89
+ .vec{stroke-dasharray:230;stroke-dashoffset:230;animation:draw 1s cubic-bezier(.2,.7,.2,1) .25s forwards;}
90
+ .vec-b{animation-delay:.5s;}
91
+ .arc{stroke-dasharray:90;stroke-dashoffset:90;animation:draw .7s ease .95s forwards;}
92
+ .lbl{opacity:0;animation:fade .5s ease 1.2s forwards;}
93
+ .tip{opacity:0;animation:fade .4s ease forwards;}
94
+ .tip-a{animation-delay:1.05s;}
95
+ .tip-b{animation-delay:1.25s;}
96
+ }
97
+ @keyframes draw{to{stroke-dashoffset:0;}}
98
+ @keyframes fade{to{opacity:1;}}
99
+
100
+ /* ---------- Method ---------- */
101
+ .method{
102
+ border-top:1px solid var(--hair);
103
+ padding:54px 0 30px;
104
+ }
105
+ .intro{
106
+ font-size:1.18rem;
107
+ max-width:62ch;
108
+ margin:0 0 52px;
109
+ }
110
+ .steps{
111
+ display:grid;
112
+ grid-template-columns:repeat(3,1fr);
113
+ gap:40px;
114
+ counter-reset:step;
115
+ }
116
+ .step{padding-top:18px;border-top:2px solid var(--ink);}
117
+ .step .num{color:var(--lapis-ink);display:block;margin-bottom:14px;}
118
+ .step h3{
119
+ font-family:"Fraunces",serif;
120
+ font-weight:600;
121
+ font-size:1.45rem;
122
+ margin:0 0 8px;
123
+ letter-spacing:-.01em;
124
+ }
125
+ .step p{margin:0;font-size:1rem;color:var(--ink);}
126
+ .step p code{
127
+ font-family:"IBM Plex Mono",monospace;
128
+ font-size:.9em;
129
+ background:var(--paper-2);
130
+ padding:1px 5px;border-radius:3px;
131
+ }
132
+
133
+ /* ---------- App ---------- */
134
+ .app{padding:46px 0 20px;}
135
+ .app-head{
136
+ display:flex;justify-content:space-between;align-items:baseline;
137
+ gap:20px;flex-wrap:wrap;margin-bottom:18px;
138
+ }
139
+ .app-head h2{
140
+ font-family:"Fraunces",serif;font-weight:600;
141
+ font-size:1.6rem;margin:0;letter-spacing:-.01em;
142
+ }
143
+ .fullscreen{
144
+ font-family:"IBM Plex Mono",monospace;
145
+ font-size:.78rem;letter-spacing:.04em;
146
+ text-transform:uppercase;white-space:nowrap;
147
+ }
148
+ .note{
149
+ font-size:.95rem;color:var(--ink-soft);
150
+ margin:0 0 18px;max-width:64ch;
151
+ }
152
+ .frame{
153
+ position:relative;
154
+ border:1px solid var(--hair);
155
+ background:var(--paper-2);
156
+ border-radius:2px;
157
+ overflow:hidden;
158
+ }
159
+ .frame::before{
160
+ /* placeholder shown until the iframe paints over it */
161
+ content:"Chargement de l’application…";
162
+ position:absolute;inset:0;
163
+ display:flex;align-items:center;justify-content:center;
164
+ font-family:"IBM Plex Mono",monospace;
165
+ font-size:.8rem;letter-spacing:.1em;text-transform:uppercase;
166
+ color:var(--ink-soft);
167
+ }
168
+ .frame iframe{
169
+ position:relative;
170
+ display:block;width:100%;height:900px;border:0;background:transparent;
171
+ }
172
+
173
+ /* ---------- Footer ---------- */
174
+ footer{
175
+ border-top:1px solid var(--hair);
176
+ margin-top:54px;padding:38px 0 60px;
177
+ color:var(--ink-soft);font-size:.95rem;
178
+ }
179
+ footer .foot-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:32px;}
180
+ footer strong{color:var(--ink);font-weight:500;}
181
+ footer .colophon{text-align:right;}
182
+
183
+ /* ---------- Responsive ---------- */
184
+ @media (max-width:820px){
185
+ .hero-grid{grid-template-columns:1fr;gap:36px;}
186
+ .figure{max-width:360px;margin:0 auto;}
187
+ .steps{grid-template-columns:1fr;gap:30px;}
188
+ .frame iframe{height:78vh;min-height:620px;}
189
+ footer .foot-grid{grid-template-columns:1fr;gap:20px;}
190
+ footer .colophon{text-align:left;}
191
+ }
192
+ </style>
193
+ </head>
194
+ <body>
195
+
196
+ <main class="wrap">
197
+
198
+ <!-- HERO -->
199
+ <section class="hero">
200
+ <div class="hero-grid">
201
+ <div>
202
+ <p class="eyebrow mono">Linguistique computationnelle</p>
203
+ <h1 class="title">Paraphrasis<span class="dot">.</span></h1>
204
+ <p class="thesis">La proximité sémantique entre deux corpus, mesurée <em>phrase à phrase</em>.</p>
205
+ </div>
206
+
207
+ <figure class="figure">
208
+ <!-- Signature: cosine similarity is the cosine of the angle between two embedding vectors -->
209
+ <svg viewBox="0 0 320 250" role="img" aria-label="Diagramme de la similarité cosinus : l’angle thêta entre deux vecteurs de phrases A et B.">
210
+ <!-- axes hint -->
211
+ <line x1="60" y1="225" x2="300" y2="225" stroke="var(--hair)" stroke-width="1"/>
212
+ <line x1="60" y1="225" x2="60" y2="20" stroke="var(--hair)" stroke-width="1"/>
213
+ <!-- angle arc -->
214
+ <path class="arc" d="M 96.8 184.1 A 55 55 0 0 1 83.2 175.2"
215
+ fill="none" stroke="var(--lapis)" stroke-width="1.5"/>
216
+ <!-- vector A -->
217
+ <line class="vec vec-a" x1="60" y1="225" x2="144.5" y2="43.7"
218
+ stroke="var(--ink)" stroke-width="2.5" stroke-linecap="round"/>
219
+ <!-- vector B -->
220
+ <line class="vec vec-b" x1="60" y1="225" x2="193.8" y2="76.4"
221
+ stroke="var(--lapis)" stroke-width="2.5" stroke-linecap="round"/>
222
+ <!-- tips -->
223
+ <circle class="tip tip-a" cx="144.5" cy="43.7" r="4.5" fill="var(--ink)"/>
224
+ <circle class="tip tip-b" cx="193.8" cy="76.4" r="4.5" fill="var(--lapis)"/>
225
+ <!-- labels -->
226
+ <g class="lbl" font-family="IBM Plex Mono, monospace">
227
+ <text x="150" y="38" font-size="15" fill="var(--ink)">A</text>
228
+ <text x="201" y="72" font-size="15" fill="var(--lapis-ink)">B</text>
229
+ <text x="104" y="160" font-size="15" font-style="italic" fill="var(--lapis-ink)" font-family="Spectral, serif">θ</text>
230
+ <text x="44" y="240" font-size="12" fill="var(--ink-soft)">O</text>
231
+ </g>
232
+ </svg>
233
+ <figcaption class="mono">similarité = cos θ</figcaption>
234
+ </figure>
235
+ </div>
236
+ </section>
237
+
238
+ <!-- METHOD -->
239
+ <section class="method">
240
+ <p class="intro">
241
+ Paraphrasis projette chaque phrase de deux corpus dans un espace vectoriel dense,
242
+ puis quantifie leur ressemblance par la similarité cosinus. L’outil offre aux
243
+ chercheur·e·s en sciences humaines un moyen d’explorer correspondances, échos et
244
+ reformulations entre textes, à une échelle que la lecture rapprochée seule n’atteint pas.
245
+ </p>
246
+
247
+ <div class="steps">
248
+ <div class="step">
249
+ <span class="num mono">01 · Encodage</span>
250
+ <h3>Plongement</h3>
251
+ <p>Chaque phrase est encodée par un modèle de type transformeur (<em>Sentence-Transformers</em>), entraîné à rapprocher les énoncés de sens voisin.</p>
252
+ </div>
253
+ <div class="step">
254
+ <span class="num mono">02 · Représentation</span>
255
+ <h3>Espace vectoriel</h3>
256
+ <p>Les phrases deviennent des vecteurs denses dans <code>ℝ^d</code>, où <code>d</code> vaut 384 à 768 selon le modèle retenu.</p>
257
+ </div>
258
+ <div class="step">
259
+ <span class="num mono">03 · Mesure</span>
260
+ <h3>Cosinus</h3>
261
+ <p>La similarité compare l’orientation des vecteurs : <code>cos θ = 1</code> pour une équivalence sémantique, <code>0</code> pour l’indépendance.</p>
262
+ </div>
263
+ </div>
264
+ </section>
265
+
266
+ <!-- APP -->
267
+ <section class="app">
268
+ <div class="app-head">
269
+ <h2>L’outil</h2>
270
+ <a class="fullscreen" href="https://tdelaselle-paraphrasis.hf.space/" target="_blank" rel="noopener">Ouvrir en plein écran ↗</a>
271
+ </div>
272
+ <p class="note">
273
+ L’application est hébergée sur Hugging Face Spaces et calcule sur GPU.
274
+ Après une période d’inactivité, son démarrage peut demander quelques instants —
275
+ laissez la fenêtre se charger.
276
+ </p>
277
+ <div class="frame">
278
+ <iframe
279
+ src="https://tdelaselle-paraphrasis.hf.space/?embed=true"
280
+ title="Application Paraphrasis"
281
+ loading="lazy"
282
+ allow="clipboard-write"></iframe>
283
+ </div>
284
+ </section>
285
+
286
+ <!-- FOOTER -->
287
+ <footer>
288
+ <div class="foot-grid">
289
+ <div>
290
+ <strong>Paraphrasis</strong> — projet de linguistique computationnelle
291
+ appliqué aux textes culturels. Les corpus sont traités à la demande&nbsp;;
292
+ aucune donnée n’est conservée après le calcul.
293
+ </div>
294
+ <div class="colophon mono">
295
+ Plongements · Similarité cosinus<br>
296
+ Hébergé sur Hugging&nbsp;Face Spaces
297
+ </div>
298
+ </div>
299
+ </footer>
300
+
301
+ </main>
302
+
303
+ </body>
304
+ </html>
app.py CHANGED
@@ -295,7 +295,7 @@ st.markdown(
295
  .panel-title {
296
  margin-bottom: 0.35rem;
297
  font-family: "IBM Plex Mono", monospace;
298
- font-size: 0.78rem;
299
  text-transform: uppercase;
300
  letter-spacing: 0.08em;
301
  color: var(--ink-mute);
@@ -469,6 +469,22 @@ st.markdown(
469
  background: var(--paper);
470
  }
471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  .review-banner {
473
  margin-bottom: 0.85rem;
474
  padding: 0.65rem 0.9rem;
@@ -732,11 +748,12 @@ def filter_matches_by_threshold(
732
  return matches
733
 
734
 
735
- def build_detection_cache_key(source_text, target_text, model_name, finetune=False):
736
  """Builds a stable cache key so threshold-only reruns reuse embeddings."""
737
  payload = "||".join([
738
  model_name.strip(),
739
  "finetune" if finetune else "base",
 
740
  source_text.strip(),
741
  target_text.strip(),
742
  ])
@@ -1149,7 +1166,128 @@ def resolve_detection_input(uploaded_file, raw_text):
1149
  "error": f"Unsupported file type for {filename}. Use .txt, .csv, or .tsv.",
1150
  }
1151
 
1152
- def segment_and_index(text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1153
  """Segments text into clauses and assigns a 1-based ID to each."""
1154
  # Clean newlines and split based on punctuation
1155
  text = text.replace('\n', ' ')
@@ -1170,8 +1308,8 @@ def segment_and_index(text):
1170
  'enim', 'vero', 'nam', 'sicut', 'igitur', 'ergo'
1171
  }
1172
 
1173
- # We set strict limits: minimum 10 words, maximum 100 words per chunk
1174
- segmenter = ClauseSegmenter(min_words=10, max_words=100, linguistic_markers=linguistic_markers)
1175
 
1176
  raw_sentences = segmenter.segment(text)
1177
 
@@ -1200,9 +1338,9 @@ with tab1:
1200
  st.markdown(
1201
  """
1202
  <div class="hero-shell">
1203
- <h1 class="hero-mark">Paraphrasis detector</h1>
1204
  <div class="hero-row">
1205
- <div class="hero-sub">Semantic Similarity Measures through Sentence Transformers Representations</div>
1206
  <div class="hero-credit">by T. de la Selle, CNRS</div>
1207
  </div>
1208
  </div>
@@ -1210,13 +1348,15 @@ with tab1:
1210
  unsafe_allow_html=True,
1211
  )
1212
 
1213
- left_col, right_col = st.columns([2, 1.3], gap="large")
1214
  with left_col:
 
1215
  source_file = st.file_uploader(
1216
- "Corpus 1 Source",
1217
- label_visibility="visible",
1218
  type=["txt", "csv", "tsv"],
1219
  key="source_file_upload",
 
1220
  help=(
1221
  "Example corpora in various languages are available on the repository: "
1222
  "https://huggingface.co/spaces/TdelaSelle/Paraphrasis/tree/main/test_example "
@@ -1235,6 +1375,7 @@ with tab1:
1235
  label_visibility="collapsed",
1236
  type=["txt", "csv", "tsv"],
1237
  key="target_file_upload",
 
1238
  )
1239
  text2 = st.text_area(
1240
  "Target Text (Corpus 2)",
@@ -1245,47 +1386,108 @@ with tab1:
1245
 
1246
  with right_col:
1247
 
1248
- st.markdown('<div class="panel-title">Settings</div>', unsafe_allow_html=True)
1249
- model_path = st.text_input(
1250
- "Hugging Face Model Path",
1251
- value="TdelaSelle/PatriLaSE",
1252
- placeholder="Enter a sentence-transformers model path...",
1253
- help="Select a sentence-transformers model or a language model (local path or HF hub ID). \\ The default 'TdelaSelle/PatriLaSE' is dedicated to Latin Sentence Embeddings for semantic similarity. For more general paraphrase mining, test with multilingual models like 'intfloat/multilingual-e5-large-instruct' or 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2' (faster computation). For French texts, see 'dangvantuan/sentence-camembert-base'",
1254
- )
1255
- enable_finetune = st.checkbox(
1256
- "Adapted Sentence Representation fine-tuning",
1257
- value=False,
1258
- help=(
1259
- "Before detection, fine-tune the model on your own corpora with an "
1260
- "unsupervised SimCSE objective (up to 6000 sentences sampled from "
1261
- "corpus 1, 1 epoch, LR 1e-5, batch 32, GPU with CPU fallback)."
1262
- "Especially recommended for specialized domains or if base model is not a sentence-transformers model."
1263
- ),
1264
- )
 
 
 
 
 
1265
 
1266
- # set a scrolling menu for selecting the task definition, with a fixed height and overflow scroll
1267
- task_definition = st.selectbox(
1268
- "Select a task",
1269
- options=[
1270
- "Semantic correspondence detection: list all sentence pairs above threshold",
1271
- "Semantic search:find top-k best matches in target for each source sentence",
1272
- ],
1273
- help=(
1274
- "The default 'Semantic correspondence detection' lists all sentence pairs with similarity above the threshold and therefore treats both corpora equally. The 'Semantic search' option finds the top-k best matches in the target corpus for each source sentence."
1275
- ),
1276
- )
1277
 
1278
- if task_definition == "Semantic search:find top-k best matches in target for each source sentence":
1279
- top_k = st.number_input(
1280
- "Top-k matches per source sentence",
 
 
 
1281
  min_value=1,
1282
- max_value=20,
1283
- value=5,
1284
  step=1,
1285
- help="When using 'Semantic search', specify how many top matches to retrieve for each source sentence. Higher values may increase runtime and review load.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1286
  )
1287
- else: threshold = st.slider("Similarity Threshold", 0.0, 1.0, 0.70, 0.01, help="Adjust the semantic correspondence detection sensitivity. Higher values mean more similar.")
1288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1289
 
1290
  run_detection = st.button("Run Detection", type="primary", use_container_width=True)
1291
  run_status_box = st.container()
@@ -1339,6 +1541,11 @@ with tab1:
1339
  target_input["signature"],
1340
  model_path,
1341
  finetune=enable_finetune,
 
 
 
 
 
1342
  )
1343
  cached = st.session_state.get('detection_cache')
1344
  data1 = None
@@ -1355,9 +1562,16 @@ with tab1:
1355
  runtime_device = cached.get('runtime_device', 'cpu')
1356
  used_cached_similarity = True
1357
  else:
1358
- update_detection_progress(0.15, "Segmenting corpora...")
1359
- data1 = source_input["data_rows"] if source_input["data_rows"] is not None else segment_and_index(source_input["raw_text"])
1360
- data2 = target_input["data_rows"] if target_input["data_rows"] is not None else segment_and_index(target_input["raw_text"])
 
 
 
 
 
 
 
1361
 
1362
  if not data1 or not data2:
1363
  st.session_state['detection_feedback'] = {
 
295
  .panel-title {
296
  margin-bottom: 0.35rem;
297
  font-family: "IBM Plex Mono", monospace;
298
+ font-size: 1.25rem;
299
  text-transform: uppercase;
300
  letter-spacing: 0.08em;
301
  color: var(--ink-mute);
 
469
  background: var(--paper);
470
  }
471
 
472
+ /* Tighten vertical spacing around st.divider().
473
+ The space is the vertical block's flex gap, not the hr margin, so we
474
+ pull the divider's element container in with negative margins. */
475
+ [data-testid="stElementContainer"]:has(> [data-testid="stDivider"]),
476
+ [data-testid="stElementContainer"]:has(> hr) {
477
+ margin-top: -0.3rem !important;
478
+ margin-bottom: -0.3rem !important;
479
+ }
480
+
481
+ [data-testid="stDivider"],
482
+ [data-testid="stDivider"] hr,
483
+ hr {
484
+ margin-top: 0 !important;
485
+ margin-bottom: 0 !important;
486
+ }
487
+
488
  .review-banner {
489
  margin-bottom: 0.85rem;
490
  padding: 0.65rem 0.9rem;
 
748
  return matches
749
 
750
 
751
+ def build_detection_cache_key(source_text, target_text, model_name, finetune=False, segmentation="clause"):
752
  """Builds a stable cache key so threshold-only reruns reuse embeddings."""
753
  payload = "||".join([
754
  model_name.strip(),
755
  "finetune" if finetune else "base",
756
+ f"seg:{segmentation}",
757
  source_text.strip(),
758
  target_text.strip(),
759
  ])
 
1166
  "error": f"Unsupported file type for {filename}. Use .txt, .csv, or .tsv.",
1167
  }
1168
 
1169
+ @st.cache_resource
1170
+ def get_sat_model(model_name="sat-3l-sm"):
1171
+ """Loads and caches the SaT (Segment any Text) sentence segmenter.
1172
+
1173
+ SaT models from wtpsplit are multilingual and language-agnostic: they do
1174
+ not require a language code, so the language is effectively selected
1175
+ automatically. The model is moved to GPU (half precision) when CUDA is
1176
+ usable, with a transparent CPU fallback.
1177
+ """
1178
+ from wtpsplit import SaT
1179
+
1180
+ model = SaT(model_name)
1181
+ if _select_inference_device() == "cuda":
1182
+ try:
1183
+ model.half().to("cuda")
1184
+ except Exception:
1185
+ # Keep the CPU model if moving to GPU fails on this machine.
1186
+ pass
1187
+ return model
1188
+
1189
+
1190
+ MIN_SEGMENT_WORDS = 8
1191
+
1192
+ # SaT boundary-probability threshold: higher -> fewer/longer sentences.
1193
+ SAT_DEFAULT_THRESHOLD = 0.5
1194
+
1195
+ # Punctuation marks that close a full sentence (as opposed to a clause break).
1196
+ _SENTENCE_END_CHARS = ('.', '!')
1197
+
1198
+
1199
+ def _ends_sentence(segment):
1200
+ """True when a segment ends on sentence-closing punctuation (. or !)."""
1201
+ return segment.rstrip().rstrip('"”’\')').rstrip().endswith(_SENTENCE_END_CHARS)
1202
+
1203
+
1204
+ def _merge_short_segments(segments, min_segment_length=MIN_SEGMENT_WORDS):
1205
+ """Merges segments shorter than `min_segment_length` words into a neighbor.
1206
+
1207
+ SaT can over-segment into tiny fragments (e.g. "dicit illis iesus:"). This
1208
+ fallback, mirroring the orphan-merge in segmenter.py, folds each too-short
1209
+ segment into a neighbor based on surrounding sentence punctuation:
1210
+
1211
+ - current does NOT end on .! and previous DOES -> merge with next
1212
+ (the fragment opens a new sentence; keep it with what follows).
1213
+ - current DOES end on .! and previous does NOT -> merge with previous
1214
+ (the fragment closes the previous sentence).
1215
+ - otherwise -> merge with the
1216
+ shorter neighbor (in words).
1217
+
1218
+ Edge fragments with only one neighbor merge into that neighbor.
1219
+ """
1220
+ segments = [s.strip() for s in segments if s and s.strip()]
1221
+ if len(segments) <= 1:
1222
+ return segments
1223
+
1224
+ def word_count(segment):
1225
+ return len(segment.split())
1226
+
1227
+ # Restart after each merge: indices shift and a merged segment may still be
1228
+ # short, so it gets re-evaluated on the next pass until everything is stable.
1229
+ changed = True
1230
+ while changed and len(segments) > 1:
1231
+ changed = False
1232
+ for i, segment in enumerate(segments):
1233
+ if word_count(segment) >= min_segment_length:
1234
+ continue
1235
+
1236
+ prev_segment = segments[i - 1] if i > 0 else None
1237
+ next_segment = segments[i + 1] if i < len(segments) - 1 else None
1238
+
1239
+ if prev_segment is None:
1240
+ merge_with_next = True
1241
+ elif next_segment is None:
1242
+ merge_with_next = False
1243
+ elif not _ends_sentence(segment) and _ends_sentence(prev_segment):
1244
+ merge_with_next = True
1245
+ elif _ends_sentence(segment) and not _ends_sentence(prev_segment):
1246
+ merge_with_next = False
1247
+ else:
1248
+ merge_with_next = word_count(next_segment) < word_count(prev_segment)
1249
+
1250
+ if merge_with_next:
1251
+ segments[i] = f"{segment} {segments[i + 1]}"
1252
+ del segments[i + 1]
1253
+ else:
1254
+ segments[i - 1] = f"{segments[i - 1]} {segment}"
1255
+ del segments[i]
1256
+
1257
+ changed = True
1258
+ break
1259
+
1260
+ return segments
1261
+
1262
+
1263
+ def segment_and_index_sat(text, min_segment_length=MIN_SEGMENT_WORDS, threshold=None):
1264
+ """Segments text into sentences with SaT and assigns a 1-based ID to each.
1265
+
1266
+ Uses SaT (Segment any Text, wtpsplit) for multilingual semantic sentence
1267
+ segmentation. The language is detected automatically by the model, and the
1268
+ text is split on sentence boundaries. Robust to missing punctuation and
1269
+ noisy line breaks. A merge fallback folds over-short segments into a
1270
+ neighbor (see _merge_short_segments).
1271
+
1272
+ `threshold` is the boundary probability above which SaT marks a sentence
1273
+ split (0-1). Higher values yield fewer, longer sentences; lower values
1274
+ split more aggressively. When None, SaT's model default is used.
1275
+ """
1276
+ if not text or not text.strip():
1277
+ return []
1278
+
1279
+ model = get_sat_model()
1280
+ split_kwargs = {} if threshold is None else {"threshold": threshold}
1281
+ sentences = model.split(text, **split_kwargs)
1282
+ merged = _merge_short_segments(sentences, min_segment_length=min_segment_length)
1283
+
1284
+ return [
1285
+ {"id": i, "text": sentence.strip()}
1286
+ for i, sentence in enumerate(merged, start=1)
1287
+ ]
1288
+
1289
+
1290
+ def segment_and_index(text, min_segment_length=MIN_SEGMENT_WORDS):
1291
  """Segments text into clauses and assigns a 1-based ID to each."""
1292
  # Clean newlines and split based on punctuation
1293
  text = text.replace('\n', ' ')
 
1308
  'enim', 'vero', 'nam', 'sicut', 'igitur', 'ergo'
1309
  }
1310
 
1311
+ # We set strict limits: minimum 10 words, maximum 150 words per chunk
1312
+ segmenter = ClauseSegmenter(min_words=min_segment_length, max_words=150, linguistic_markers=linguistic_markers)
1313
 
1314
  raw_sentences = segmenter.segment(text)
1315
 
 
1338
  st.markdown(
1339
  """
1340
  <div class="hero-shell">
1341
+ <h1 class="hero-mark">Paraphrasis analyzer</h1>
1342
  <div class="hero-row">
1343
+ <div class="hero-sub">Semantic Similarity Measures through Sentence Representations</div>
1344
  <div class="hero-credit">by T. de la Selle, CNRS</div>
1345
  </div>
1346
  </div>
 
1348
  unsafe_allow_html=True,
1349
  )
1350
 
1351
+ left_col, right_col = st.columns([1.6, 1.56], gap="large")
1352
  with left_col:
1353
+ st.markdown('<div class="panel-title">Corpus 1 Source</div>', unsafe_allow_html=True)
1354
  source_file = st.file_uploader(
1355
+ label="Corpus 1 Source",
1356
+ label_visibility="collapsed",
1357
  type=["txt", "csv", "tsv"],
1358
  key="source_file_upload",
1359
+ max_upload_size=1,
1360
  help=(
1361
  "Example corpora in various languages are available on the repository: "
1362
  "https://huggingface.co/spaces/TdelaSelle/Paraphrasis/tree/main/test_example "
 
1375
  label_visibility="collapsed",
1376
  type=["txt", "csv", "tsv"],
1377
  key="target_file_upload",
1378
+ max_upload_size=1,
1379
  )
1380
  text2 = st.text_area(
1381
  "Target Text (Corpus 2)",
 
1386
 
1387
  with right_col:
1388
 
1389
+ # --- MODEL ---
1390
+ st.markdown('<div class="panel-title">Model</div>', unsafe_allow_html=True)
1391
+ model_col, finetune_col = st.columns([3, 2], gap="small")
1392
+ with model_col:
1393
+ model_path = st.text_input(
1394
+ "Hugging Face Model Path",
1395
+ value="TdelaSelle/PatriSBERT-STS",
1396
+ # label_visibility="collapsed",
1397
+ placeholder="Enter a sentence-transformers model path...",
1398
+ help="Select a sentence-transformers model or a language model (local path or HF hub ID). \\ The default 'TdelaSelle/PatriLaSE' is dedicated to Latin Sentence Embeddings for semantic similarity. For more general paraphrase mining, test with multilingual models like 'intfloat/multilingual-e5-large-instruct' or 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2' (faster computation). For French texts, see 'dangvantuan/sentence-camembert-base'",
1399
+ )
1400
+ with finetune_col:
1401
+ enable_finetune = st.checkbox(
1402
+ "Model fine-tuning on corpus 1",
1403
+ value=False,
1404
+ help=(
1405
+ "Before detection, fine-tune the model on your own corpora with an "
1406
+ "unsupervised SimCSE objective (up to 6000 sentences sampled from "
1407
+ "corpus 1, 1 epoch, LR 1e-5, batch 32, GPU with CPU fallback)."
1408
+ "Use it if base model is not a sentence-transformers model or if data is domain-specific or noisy. Fine-tuning is not needed for general-purpose sentence-transformers models."
1409
+ ),
1410
+ )
1411
 
1412
+ st.divider()
 
 
 
 
 
 
 
 
 
 
1413
 
1414
+ # --- SEGMENTATION ---
1415
+ st.markdown('<div class="panel-title">Segmentation</div>', unsafe_allow_html=True)
1416
+ min_words_col, sat_col = st.columns([1,3], gap="small")
1417
+ with min_words_col:
1418
+ min_segment_words = st.number_input(
1419
+ "Minimum words per segment",
1420
  min_value=1,
1421
+ max_value=50,
1422
+ value=MIN_SEGMENT_WORDS,
1423
  step=1,
1424
+ help=(
1425
+ "Shortest segment kept during segmentation. With the clause "
1426
+ "segmenter this is the minimum chunk size; with SaT it drives the "
1427
+ "merge fallback that folds shorter sentences into a neighbor. "
1428
+ "Applies only to raw text inputs."
1429
+ ),
1430
+ )
1431
+ with sat_col:
1432
+ use_sat_segmentation = st.checkbox(
1433
+ "Semantic segmentation (splitting into sentences even without/noisy punctuation)",
1434
+ value=False,
1435
+ help=(
1436
+ "Segment pasted/.txt corpora into sentences with SaT (wtpsplit) "
1437
+ "instead of the default clause segmenter. SaT is multilingual and "
1438
+ "selects the language automatically, splitting on sentence "
1439
+ "boundaries even when punctuation is missing. Applies only to raw "
1440
+ "text inputs; pre-segmented .csv/.tsv corpora are used as provided."
1441
+ ),
1442
  )
 
1443
 
1444
+ sat_threshold = SAT_DEFAULT_THRESHOLD
1445
+ if use_sat_segmentation:
1446
+ sat_threshold = st.slider(
1447
+ "SaT segmentation threshold",
1448
+ min_value=0.05,
1449
+ max_value=0.95,
1450
+ value=SAT_DEFAULT_THRESHOLD,
1451
+ step=0.05,
1452
+ format="%.2f",
1453
+ help=(
1454
+ "Boundary probability above which SaT marks a sentence split. "
1455
+ "Higher values produce fewer, longer sentences; lower values "
1456
+ "split more aggressively. Default ~0.025."
1457
+ ),
1458
+ )
1459
+
1460
+ st.divider()
1461
+
1462
+ # --- TASK ---
1463
+ st.markdown('<div class="panel-title">Task</div>', unsafe_allow_html=True)
1464
+ task_col, param_col = st.columns([5, 2], gap="small")
1465
+ with task_col:
1466
+ # set a scrolling menu for selecting the task definition, with a fixed height and overflow scroll
1467
+ task_definition = st.selectbox(
1468
+ "Select a task",
1469
+ options=[
1470
+ "Semantic correspondence detection: list all sentence pairs above threshold",
1471
+ "Semantic search:find top-k best matches in target for each source sentence",
1472
+ ],
1473
+ help=(
1474
+ "The default 'Semantic correspondence detection' lists all sentence pairs with similarity above the threshold and therefore treats both corpora equally. The 'Semantic search' option finds the top-k best matches in the target corpus for each source sentence."
1475
+ ),
1476
+ )
1477
+ with param_col:
1478
+ if task_definition == "Semantic search:find top-k best matches in target for each source sentence":
1479
+ top_k = st.number_input(
1480
+ "Top-k matches per source sentence",
1481
+ min_value=1,
1482
+ max_value=20,
1483
+ value=5,
1484
+ step=1,
1485
+ help="When using 'Semantic search', specify how many top matches to retrieve for each source sentence. Higher values may increase runtime and review load.",
1486
+ )
1487
+ else:
1488
+ threshold = st.slider("Similarity Threshold", 0.0, 1.0, 0.80, 0.01, help="Adjust the semantic correspondence detection sensitivity. Higher values mean more similar.")
1489
+
1490
+ # st.divider()
1491
 
1492
  run_detection = st.button("Run Detection", type="primary", use_container_width=True)
1493
  run_status_box = st.container()
 
1541
  target_input["signature"],
1542
  model_path,
1543
  finetune=enable_finetune,
1544
+ segmentation=(
1545
+ f"sat:{sat_threshold:.3f}:min{min_segment_words}"
1546
+ if use_sat_segmentation
1547
+ else f"clause:min{min_segment_words}"
1548
+ ),
1549
  )
1550
  cached = st.session_state.get('detection_cache')
1551
  data1 = None
 
1562
  runtime_device = cached.get('runtime_device', 'cpu')
1563
  used_cached_similarity = True
1564
  else:
1565
+ segment_text = segment_and_index_sat if use_sat_segmentation else segment_and_index
1566
+ segment_kwargs = (
1567
+ {"min_segment_length": min_segment_words, "threshold": sat_threshold}
1568
+ if use_sat_segmentation
1569
+ else {"min_segment_length": min_segment_words}
1570
+ )
1571
+ segmentation_label = "SaT (Segment any Text)" if use_sat_segmentation else "clause"
1572
+ update_detection_progress(0.15, f"Segmenting corpora ({segmentation_label})...")
1573
+ data1 = source_input["data_rows"] if source_input["data_rows"] is not None else segment_text(source_input["raw_text"], **segment_kwargs)
1574
+ data2 = target_input["data_rows"] if target_input["data_rows"] is not None else segment_text(target_input["raw_text"], **segment_kwargs)
1575
 
1576
  if not data1 or not data2:
1577
  st.session_state['detection_feedback'] = {
paraphrasis_annotator.html CHANGED
@@ -721,10 +721,6 @@
721
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h12"/><path d="M3 12h9"/><path d="M3 18h6"/><path d="M17 4v16"/><polyline points="13 16 17 20 21 16"/></svg>
722
  <span id="btnSortLabel">Similarity</span>
723
  </button>
724
- <button class="btn btn-toggle" id="btnHideNeg" title="Hide pairs labelled as rejected">
725
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
726
- Hide&nbsp;negatives
727
- </button>
728
  </div>
729
 
730
  <div class="ctrl-group">
@@ -831,7 +827,6 @@ const state = {
831
  threshold: 0.95,
832
  filterText: '',
833
  sortMode: 'similarity', // 'similarity' (file order) | 'id' (by Source_ID)
834
- hideNegatives: false, // hide pairs tagged as rejected
835
  pairsFileName: null,
836
  c1FileName: null,
837
  c2FileName: null,
@@ -1081,7 +1076,6 @@ function applyFilter() {
1081
  state.filtered = [];
1082
  for (let i = 0; i < state.pairs.length; i++) {
1083
  const p = state.pairs[i];
1084
- if (state.hideNegatives && p.tag === 'r') continue;
1085
  if (t && !(String(p.idA).toLowerCase().includes(t) || String(p.idB).toLowerCase().includes(t))) continue;
1086
  state.filtered.push(i);
1087
  }
@@ -1252,34 +1246,6 @@ function applyTag(tag) {
1252
  });
1253
  updateStats();
1254
 
1255
- // If Hide-negatives is on and we just rejected rows, they must disappear
1256
- // from the filtered view. Remember the *next* visible pair's identity before
1257
- // re-filtering so we can land on it, not on the first row in the list.
1258
- if (state.hideNegatives && tag === 'r') {
1259
- const last = indices[indices.length - 1];
1260
- let anchorPairIdx = -1;
1261
- for (let fi = last + 1; fi < state.filtered.length; fi++) {
1262
- const pi = state.filtered[fi];
1263
- if (state.pairs[pi].tag !== 'r') { anchorPairIdx = pi; break; }
1264
- }
1265
- applyFilter();
1266
- state.selection.clear();
1267
- if (anchorPairIdx >= 0) {
1268
- const next = state.filtered.indexOf(anchorPairIdx);
1269
- if (next >= 0) { selectOne(next); return; }
1270
- }
1271
- // No trailing visible row → fall back to whatever is still in view.
1272
- if (state.filtered.length > 0) {
1273
- const target = Math.min(indices[0], state.filtered.length - 1);
1274
- selectOne(target);
1275
- } else {
1276
- state.pivot = -1;
1277
- renderVisibleRows();
1278
- renderCurrentTexts();
1279
- }
1280
- return;
1281
- }
1282
-
1283
  // Normal auto-advance to the next row.
1284
  const last = indices[indices.length - 1];
1285
  const next = last + 1;
@@ -1445,7 +1411,7 @@ function readFile(file, cb) {
1445
  }
1446
 
1447
  /* =========================================================
1448
- TOGGLES: SORT & HIDE-NEGATIVES
1449
  ========================================================= */
1450
  function toggleSort() {
1451
  const pivotPairIdx = state.pivot >= 0 ? state.filtered[state.pivot] : -1;
@@ -1472,50 +1438,15 @@ function toggleSort() {
1472
  : 'Restored similarity order.');
1473
  }
1474
 
1475
- function toggleHideNegatives() {
1476
- const pivotPairIdx = state.pivot >= 0 ? state.filtered[state.pivot] : -1;
1477
- const selPairs = new Set([...state.selection].map(fi => state.filtered[fi]));
1478
-
1479
- state.hideNegatives = !state.hideNegatives;
1480
- applyFilter();
1481
-
1482
- // Remap selection and pivot after filter change.
1483
- state.selection.clear();
1484
- for (let i = 0; i < state.filtered.length; i++) {
1485
- if (selPairs.has(state.filtered[i])) state.selection.add(i);
1486
- }
1487
- if (pivotPairIdx >= 0) {
1488
- const newPivot = state.filtered.indexOf(pivotPairIdx);
1489
- if (newPivot >= 0) {
1490
- state.pivot = newPivot;
1491
- scrollFilteredIntoView(newPivot);
1492
- } else if (state.filtered.length > 0) {
1493
- // Pivot was itself a negative that just got hidden — fall back to row 0.
1494
- selectOne(0);
1495
- } else {
1496
- state.pivot = -1;
1497
- renderCurrentTexts();
1498
- }
1499
- }
1500
- renderVisibleRows();
1501
- updateToggleButtons();
1502
- const nHidden = state.pairs.filter(p => p.tag === 'r').length;
1503
- toast(state.hideNegatives
1504
- ? `Hiding ${nHidden.toLocaleString()} rejected pair${nHidden === 1 ? '' : 's'}.`
1505
- : 'Showing all pairs.');
1506
- }
1507
-
1508
  function updateToggleButtons() {
1509
  const bS = document.getElementById('btnSort');
1510
  const bSLabel = document.getElementById('btnSortLabel');
1511
- const bH = document.getElementById('btnHideNeg');
1512
  if (bS && bSLabel) {
1513
  // The label always reflects the *current* sort rule.
1514
  bSLabel.textContent = state.sortMode === 'id' ? 'Id' : 'Similarity';
1515
  // Visually mark "Id" as the accent (non-default) mode.
1516
  bS.classList.toggle('active', state.sortMode === 'id');
1517
  }
1518
- if (bH) bH.classList.toggle('active', state.hideNegatives);
1519
  }
1520
 
1521
 
@@ -1536,7 +1467,6 @@ document.getElementById('fileC2').addEventListener('change', e => {
1536
  });
1537
  document.getElementById('btnExport').addEventListener('click', exportPairs);
1538
  document.getElementById('btnSort').addEventListener('click', toggleSort);
1539
- document.getElementById('btnHideNeg').addEventListener('click', toggleHideNegatives);
1540
 
1541
  document.getElementById('pairsScroll').addEventListener('scroll', () => {
1542
  requestAnimationFrame(renderVisibleRows);
 
721
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h12"/><path d="M3 12h9"/><path d="M3 18h6"/><path d="M17 4v16"/><polyline points="13 16 17 20 21 16"/></svg>
722
  <span id="btnSortLabel">Similarity</span>
723
  </button>
 
 
 
 
724
  </div>
725
 
726
  <div class="ctrl-group">
 
827
  threshold: 0.95,
828
  filterText: '',
829
  sortMode: 'similarity', // 'similarity' (file order) | 'id' (by Source_ID)
 
830
  pairsFileName: null,
831
  c1FileName: null,
832
  c2FileName: null,
 
1076
  state.filtered = [];
1077
  for (let i = 0; i < state.pairs.length; i++) {
1078
  const p = state.pairs[i];
 
1079
  if (t && !(String(p.idA).toLowerCase().includes(t) || String(p.idB).toLowerCase().includes(t))) continue;
1080
  state.filtered.push(i);
1081
  }
 
1246
  });
1247
  updateStats();
1248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1249
  // Normal auto-advance to the next row.
1250
  const last = indices[indices.length - 1];
1251
  const next = last + 1;
 
1411
  }
1412
 
1413
  /* =========================================================
1414
+ TOGGLES: SORT
1415
  ========================================================= */
1416
  function toggleSort() {
1417
  const pivotPairIdx = state.pivot >= 0 ? state.filtered[state.pivot] : -1;
 
1438
  : 'Restored similarity order.');
1439
  }
1440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1441
  function updateToggleButtons() {
1442
  const bS = document.getElementById('btnSort');
1443
  const bSLabel = document.getElementById('btnSortLabel');
 
1444
  if (bS && bSLabel) {
1445
  // The label always reflects the *current* sort rule.
1446
  bSLabel.textContent = state.sortMode === 'id' ? 'Id' : 'Similarity';
1447
  // Visually mark "Id" as the accent (non-default) mode.
1448
  bS.classList.toggle('active', state.sortMode === 'id');
1449
  }
 
1450
  }
1451
 
1452
 
 
1467
  });
1468
  document.getElementById('btnExport').addEventListener('click', exportPairs);
1469
  document.getElementById('btnSort').addEventListener('click', toggleSort);
 
1470
 
1471
  document.getElementById('pairsScroll').addEventListener('scroll', () => {
1472
  requestAnimationFrame(renderVisibleRows);
requirements.txt CHANGED
@@ -3,4 +3,5 @@ sentence-transformers
3
  datasets
4
  accelerate>=0.26.0
5
  torch
6
- pandas
 
 
3
  datasets
4
  accelerate>=0.26.0
5
  torch
6
+ pandas
7
+ wtpsplit