jejunepixels commited on
Commit
d5b23e9
·
verified ·
1 Parent(s): b37c042

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +432 -0
app.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import io
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import plotly.graph_objects as go
7
+ import streamlit as st
8
+ from plotly.colors import qualitative
9
+
10
+ from embeddings import Model, embedding_name, load_tokenizer, nearest_tokens, search_embeddings
11
+ from relationships import relationship_figure
12
+ from relationship_ui import render_relationship_controls
13
+ from model_picker import show_model_picker
14
+ from hub_ui import render_hub_loader
15
+ from upload_ui import render_upload_loader
16
+ from app_environment import hosted_mode, remove_session_directory, UPLOAD_ROOT, PRIVATE_CACHE_ROOT
17
+
18
+
19
+ st.set_page_config(page_title='Token Visualiser', page_icon='🔬', layout='wide', initial_sidebar_state='collapsed')
20
+ hosted = hosted_mode()
21
+ if st.session_state.pop('clear_hub_token', False):
22
+ st.session_state.pop('hf_token', None)
23
+ if 'pending_chart_view' in st.session_state:
24
+ st.session_state.chart_view = st.session_state.pop('pending_chart_view')
25
+ if 'pending_model' in st.session_state:
26
+ pending_model = st.session_state.pop('pending_model')
27
+ if not hosted:
28
+ st.session_state.model_location = pending_model
29
+
30
+
31
+ def clear_model():
32
+ # Keep the last browsing location, but release model data and all analysis state.
33
+ model = st.session_state.get('model')
34
+ if model is not None:
35
+ model.close()
36
+ for key, root in [('upload_directory', UPLOAD_ROOT), ('private_hub_cache', PRIVATE_CACHE_ROOT)]:
37
+ if key in st.session_state:
38
+ try:
39
+ remove_session_directory(st.session_state[key], root)
40
+ except OSError:
41
+ pass # Files still held by a disconnected worker disappear on restart.
42
+ for key in list(st.session_state):
43
+ if key not in {'picker_directory', 'picker_address'}:
44
+ del st.session_state[key]
45
+
46
+
47
+ def activate_model(location, tokenizer_location=None, display_name=None):
48
+ model = Model(location)
49
+ try:
50
+ p = model.path
51
+ auto = (p if p.is_dir() else p.parent) / 'tokenizer.json'
52
+ selected_tokenizer = tokenizer_location or (auto if auto.exists() else None)
53
+ tok = load_tokenizer(selected_tokenizer) if selected_tokenizer else None
54
+ except Exception:
55
+ model.close()
56
+ raise
57
+ st.session_state.model = model
58
+ st.session_state.tok = tok
59
+ st.session_state.loaded = str(model.path)
60
+ st.session_state.model_display_name = display_name or model.path.name
61
+ st.session_state.nearest_results = {}
62
+ st.session_state.clear_hub_token = True
63
+
64
+
65
+ title_col, action_col = st.columns([6, 1], vertical_alignment='center')
66
+ if 'model' in st.session_state:
67
+ title_col.subheader('Token Visualiser')
68
+ else:
69
+ title_col.title('Token Visualiser')
70
+ if 'model' in st.session_state:
71
+ action_col.button('Clear model', key='clear_model', on_click=clear_model, use_container_width=True)
72
+ if 'model' not in st.session_state:
73
+ st.caption('Explore the numbers inside a token. Models run in this Space · No GPU required' if hosted else 'Explore the numbers inside a token. Local model files · No GPU required')
74
+
75
+ if 'model' not in st.session_state:
76
+ _, loader, _ = st.columns([1, 3, 1])
77
+ with loader, st.container(border=True):
78
+ st.header('Load a model')
79
+ st.write('Open a local model or download weights from Hugging Face to explore token embeddings.')
80
+ if hosted:
81
+ hub_tab, local_tab = st.tabs(['Hugging Face', 'Upload files'])
82
+ else:
83
+ local_tab, hub_tab = st.tabs(['Local files', 'Hugging Face'])
84
+ with local_tab:
85
+ if hosted:
86
+ render_upload_loader(activate_model)
87
+ if not hosted:
88
+ with local_tab:
89
+ if st.button('Open model…', key='open_model', use_container_width=True):
90
+ st.session_state.picker_open = True
91
+ if st.session_state.get('picker_open', False):
92
+ show_model_picker()
93
+ location = st.text_input('Model file or folder', placeholder=r'C:\Models\gemma.gguf', key='model_location')
94
+ with st.expander('Tokenizer (optional)'):
95
+ tokenizer_path = st.text_input('Tokenizer JSON or folder', key='tokenizer_location',
96
+ help='Use the tokenizer from the exact same model. Enables text tokenization without added special tokens. Detected automatically beside the model when available.')
97
+ load = st.button('Load model', key='load_model', type='primary', disabled=not location.strip(), use_container_width=True)
98
+ st.caption('GGUF · F32/F16/BF16 safetensors · Sharded model folders')
99
+ st.caption('Files stay on this computer.')
100
+ if load:
101
+ try:
102
+ with st.spinner('Reading model metadata…'):
103
+ activate_model(location.strip().strip('"'), tokenizer_path)
104
+ st.rerun()
105
+ except Exception as e:
106
+ st.error(f'Could not load model: {e}')
107
+ with hub_tab:
108
+ render_hub_loader(activate_model)
109
+ st.stop()
110
+
111
+ model = st.session_state.model
112
+ st.markdown('<style>' + Path(__file__).with_name('viewer.css').read_text(encoding='utf-8') + '</style>', unsafe_allow_html=True)
113
+ chart_pane = st.container(key='chart_pane', height='stretch', border=True)
114
+ chart_slot = chart_pane.empty()
115
+ with st.container(key='controls_pane'):
116
+ with st.container(key='control_columns'):
117
+ token_panel, neighbour_panel, calculation_panel, details_panel = st.columns([1.15, 1.6, 1.15, 1.05], gap='medium')
118
+
119
+ with details_panel:
120
+ st.subheader('Chart & model')
121
+ st.caption(st.session_state.get('model_display_name', Path(st.session_state.loaded).name))
122
+ names = sorted(model.tensors, key=lambda n: (not embedding_name(n), n))
123
+ name = st.selectbox('Embedding tensor', names)
124
+ if not embedding_name(name):
125
+ st.warning('This tensor may not contain input token embeddings.')
126
+ chart_view = st.selectbox('Chart view', ['Dimensions', 'Relationship 2D', 'Relationship 3D'], key='chart_view')
127
+ style = st.radio('Chart style', ['Line', 'Scatter'], horizontal=True)
128
+ shape = model.tensors[name][2]
129
+ vocab = model.vocab
130
+ tok = st.session_state.tok
131
+ get_row = lambda i: model.row(name, i)
132
+ read_rows = lambda start, stop: model.rows(name, start, stop)
133
+ with details_panel:
134
+ a, b = st.columns(2)
135
+ a.metric('Vocabulary rows', f'{shape[0]:,}')
136
+ b.metric('Values per token', f'{shape[1]:,}')
137
+ st.caption('Stored input embeddings, before layer processing. Drag the chart to zoom; double-click to reset. Click a legend entry to hide or show a token.')
138
+
139
+ with token_panel:
140
+ ids = []
141
+ labels = {}
142
+ comparison_scope = f'{st.session_state.loaded}:{name}'
143
+ calculation_key = f'calculations:{comparison_scope}'
144
+ st.subheader('Compare tokens')
145
+ st.caption('Search, select and add tokens. Each curve has its own colour.')
146
+ if tok is not None:
147
+ text = st.text_input('Type text', 'hello world')
148
+ try:
149
+ encoded = tok.encode(text, add_special_tokens=False)
150
+ st.caption('Tokenized pieces: ' + ' · '.join(f'{piece!r} (ID {i})' for piece, i in zip(encoded.tokens, encoded.ids)))
151
+ choices = list(range(len(encoded.ids)))
152
+ positions = st.multiselect('Select tokens to plot', choices, default=choices[:2],
153
+ format_func=lambda p: f'{p + 1}: {encoded.tokens[p]!r} · ID {encoded.ids[p]}')
154
+ for p in positions:
155
+ i = encoded.ids[p]
156
+ if i not in ids:
157
+ ids.append(i)
158
+ labels[i] = f'{encoded.tokens[p]} · {i}'
159
+ except Exception as e:
160
+ st.error(f'Tokenization failed: {e}')
161
+ elif vocab is not None:
162
+ query = st.text_input('Find a vocabulary token', 'hello', help='Searches stored token spellings, not text tokenization. Spaces may appear as ▁ or Ġ.')
163
+ matches = [i for i, word in enumerate(vocab) if query in word][:200] if query else list(range(min(200, len(vocab))))
164
+ matches.sort(key=lambda i: (vocab[i] != query, len(vocab[i]), i))
165
+ # The chart selection lives independently of changing search-result widgets.
166
+ selection_key = f'chart_tokens:{comparison_scope}'
167
+ if selection_key not in st.session_state:
168
+ st.session_state[selection_key] = matches[:1]
169
+ selected = [i for i in st.session_state[selection_key] if 0 <= i < min(shape[0], len(vocab))]
170
+ result, add = st.columns([2.3, 1.7], vertical_alignment='bottom')
171
+ candidate = result.selectbox('Matching token', matches, key=f'match:{comparison_scope}:{query}',
172
+ format_func=lambda i: f'{vocab[i]!r} · ID {i}')
173
+ if add.button('Add to chart', type='primary', disabled=candidate is None or candidate in selected or len(selected) + len(st.session_state.get(calculation_key, [])) >= 20):
174
+ selected.append(candidate)
175
+ st.session_state[selection_key] = selected
176
+ ids = st.multiselect('Tokens on chart', selected, default=selected,
177
+ key=f'chart_selection:{comparison_scope}:{tuple(selected)}',
178
+ format_func=lambda i: f'{vocab[i]!r} · ID {i}')
179
+ if ids != selected:
180
+ st.session_state[selection_key] = ids
181
+ st.rerun()
182
+ st.caption('Search for another token and click Add to chart to compare it. Remove a token with the × beside its name.')
183
+ labels = {i: f'{vocab[i]} · {i}' for i in ids}
184
+ if not matches:
185
+ st.info('No matching vocabulary entries. Try a substring or enter an ID below.')
186
+ else:
187
+ st.info('Add the matching tokenizer.json to type text. You can inspect any token by ID below.')
188
+
189
+ ids = list(ids)
190
+ manual = st.text_input('Additional token IDs (comma-separated)', placeholder='0, 42, 100', key='manual_ids')
191
+ try:
192
+ for part in manual.split(','):
193
+ if part.strip():
194
+ i = int(part.strip())
195
+ if not 0 <= i < shape[0]:
196
+ raise ValueError(f'ID {i} is outside this tensor’s row range.')
197
+ if i not in ids:
198
+ ids.append(i)
199
+ labels.setdefault(i, f'ID {i}')
200
+ except ValueError as e:
201
+ st.error(str(e))
202
+ st.stop()
203
+ def token_label(i):
204
+ if vocab is not None and i < len(vocab):
205
+ return f'{vocab[i]!r} · ID {i}'
206
+ if tok is not None:
207
+ piece = tok.id_to_token(i)
208
+ if piece is not None:
209
+ return f'{piece!r} · ID {i}'
210
+ return f'ID {i}'
211
+
212
+
213
+ extra_key = f'added_neighbours:{comparison_scope}'
214
+ extra_ids = [i for i in st.session_state.get(extra_key, []) if i not in ids and 0 <= i < shape[0]]
215
+ if extra_ids:
216
+ kept = st.multiselect('Added neighbours on chart', extra_ids, default=extra_ids,
217
+ format_func=token_label, key=f'added_neighbour_selection:{comparison_scope}:{tuple(extra_ids)}')
218
+ if kept != extra_ids:
219
+ st.session_state[extra_key] = kept
220
+ st.rerun()
221
+ for i in kept:
222
+ ids.append(i)
223
+ labels[i] = token_label(i)
224
+ if not ids and not st.session_state.get(calculation_key):
225
+ chart_slot.info('Select a token in the controls below to see its embedding values.')
226
+ st.stop()
227
+ if len(ids) > 20:
228
+ st.warning('Select at most 20 tokens for a readable comparison.')
229
+ st.stop()
230
+
231
+
232
+ def add_neighbour(i):
233
+ added = list(st.session_state.get(extra_key, []))
234
+ if i not in added:
235
+ added.append(i)
236
+ st.session_state[extra_key] = added
237
+
238
+
239
+ try:
240
+ vectors = {i: get_row(i) for i in ids}
241
+ except Exception as e:
242
+ chart_slot.error(f'Could not read embedding: {e}')
243
+ st.stop()
244
+
245
+ calculations = st.session_state.setdefault(calculation_key, [])
246
+ for calculation in calculations:
247
+ vectors[calculation['id']] = calculation['values']
248
+ labels[calculation['id']] = calculation['label']
249
+
250
+
251
+ def remove_calculation(calculation_id):
252
+ st.session_state[calculation_key] = [c for c in st.session_state[calculation_key] if c['id'] != calculation_id]
253
+
254
+
255
+ def operand_label(key):
256
+ return labels[key] if isinstance(key, str) else token_label(key)
257
+
258
+
259
+ def expression_label(key):
260
+ if isinstance(key, str):
261
+ return '(' + labels[key].removeprefix('calc: ') + ')'
262
+ if vocab is not None and key < len(vocab):
263
+ return repr(vocab[key])
264
+ if tok is not None and tok.id_to_token(key) is not None:
265
+ return repr(tok.id_to_token(key))
266
+ return f'ID {key}'
267
+
268
+
269
+ def save_calculation(values, label):
270
+ label = label if label.startswith('calc: ') else 'calc: ' + label
271
+ serial = st.session_state.get('calculation_serial', 0) + 1
272
+ while label in labels.values():
273
+ label += f' [{serial}]'
274
+ st.session_state.calculation_serial = serial
275
+ calculations.append({'id': f'calc:{serial}', 'label': label, 'values': np.asarray(values).copy()})
276
+ st.rerun()
277
+
278
+
279
+ with calculation_panel:
280
+ st.subheader('Vector tools')
281
+ arithmetic_tab, relationship_tab = st.tabs(['Calculations', 'Relationships'])
282
+ with arithmetic_tab:
283
+ st.caption('Calculate A − B or A + B, dimension by dimension. Results become derived curves labelled calc:.')
284
+ operands = list(vectors)
285
+ left_operand = st.selectbox('Vector A', operands, format_func=operand_label, key=f'calc_a:{comparison_scope}')
286
+ operation = st.selectbox('Operation', ['Subtract (A − B)', 'Add (A + B)'], key=f'calc_operation:{comparison_scope}')
287
+ right_operand = st.selectbox('Vector B', operands, index=min(1, len(operands) - 1),
288
+ format_func=operand_label, key=f'calc_b:{comparison_scope}')
289
+ result_name = st.text_input('Result name (optional)', placeholder='e.g. hello minus world', key=f'calc_name:{comparison_scope}')
290
+ if st.button('Calculate & add to chart', key='add_calculation', type='primary',
291
+ disabled=len(vectors) >= 20, use_container_width=True):
292
+ subtract = operation.startswith('Subtract')
293
+ values = vectors[left_operand] - vectors[right_operand] if subtract else vectors[left_operand] + vectors[right_operand]
294
+ symbol = '−' if subtract else '+'
295
+ expression = f'{expression_label(left_operand)} {symbol} {expression_label(right_operand)}'
296
+ label = 'calc: ' + (result_name.strip() or expression)
297
+ save_calculation(values, label)
298
+ if calculations:
299
+ st.caption('Calculated curves stay available when you change the selected tokens. They are included in CSV exports.')
300
+ for calculation in calculations:
301
+ label_col, remove_col = st.columns([3, 1.2], vertical_alignment='center')
302
+ label_col.text(calculation['label'])
303
+ remove_col.button('Remove', key=f'remove_calculation:{calculation["id"]}',
304
+ on_click=remove_calculation, args=(calculation['id'],))
305
+ if len(vectors) >= 20:
306
+ st.caption('The chart supports up to 20 token and calculated curves in total.')
307
+
308
+ with relationship_tab:
309
+ analysis = render_relationship_controls(vectors, operand_label, comparison_scope, save_calculation)
310
+
311
+ if len(vectors) > 20:
312
+ token_panel.warning('Remove a token or calculated curve to keep the chart within its 20-curve limit.')
313
+ st.stop()
314
+ chart_ids = list(vectors)
315
+ colour_key = f'token_colours:{comparison_scope}'
316
+ colours = st.session_state.setdefault(colour_key, {})
317
+ # Retain colours across searches, keeping every currently selected token distinct.
318
+ used = set()
319
+ for i in chart_ids:
320
+ if i not in colours or colours[i] in used:
321
+ colours[i] = next(c for c in qualitative.Light24 if c not in used and
322
+ c not in {colours.get(other) for other in chart_ids if other != i})
323
+ used.add(colours[i])
324
+ details_panel.caption(f'Tokens: {len(ids)} · Calculations: {len(calculations)}')
325
+ if chart_view == 'Dimensions':
326
+ fig = go.Figure()
327
+ for i, values in vectors.items():
328
+ fig.add_trace(go.Scattergl(x=np.arange(len(values)), y=values,
329
+ mode='lines' if style == 'Line' else 'markers', name=labels[i],
330
+ line=dict(color=colours[i], width=2 if isinstance(i, str) else 1.5,
331
+ dash='dash' if isinstance(i, str) else 'solid'),
332
+ marker=dict(color=colours[i], size=4),
333
+ hovertemplate='Dimension %{x}<br>Value %{y:.7f}<extra>%{fullData.name}</extra>'))
334
+ fig.update_layout(autosize=True, xaxis_title='Embedding dimension (zero-based)', yaxis_title='Stored embedding value',
335
+ template='plotly_dark', hovermode='x', margin=dict(l=20, r=20, t=20, b=30),
336
+ legend=dict(orientation='h', y=1.02, yanchor='bottom', title_text='Tokens'), showlegend=True,
337
+ uirevision=f'{comparison_scope}:{style}')
338
+ fig.add_hline(y=0, line_width=1, line_color='gray')
339
+ chart_slot.plotly_chart(fig, width='stretch', height='stretch', config={'scrollZoom': True}, key='embedding_chart')
340
+
341
+ elif analysis is None:
342
+ chart_slot.info('Open Relationships in Vector tools, choose your inputs, then click Show relationship.')
343
+ else:
344
+ fig = relationship_figure(analysis, 2 if chart_view == 'Relationship 2D' else 3, vectors, labels)
345
+ chart_slot.plotly_chart(fig, width='stretch', height='stretch', key='embedding_chart', config={'scrollZoom': True})
346
+
347
+ with neighbour_panel:
348
+ st.subheader('Closest tokens')
349
+ search_mode = st.selectbox('Search mode', ['Nearest tokens', 'Analogy answers'], key=f'neighbour_mode:{comparison_scope}')
350
+ results = st.session_state.setdefault('nearest_results', {})
351
+ eligible = True
352
+ report_id = None
353
+ if search_mode == 'Nearest tokens':
354
+ source_id = st.selectbox('Find neighbours of', chart_ids, format_func=operand_label,
355
+ key=f'neighbour_source:{comparison_scope}')
356
+ query_vector = vectors[source_id]
357
+ excluded = [source_id] if isinstance(source_id, int) else []
358
+ result_key = ('ranked', comparison_scope, source_id)
359
+ row_key = str(source_id)
360
+ search_label = 'Find closest tokens'
361
+ st.caption('Cosine similarity uses the full vector, including calculated queries.')
362
+ elif analysis is None:
363
+ st.info('Show a relationship in Vector tools before searching for analogy answers.')
364
+ eligible = False
365
+ else:
366
+ query_vector = analysis['metrics']['prediction']
367
+ excluded = analysis['exclude_ids']
368
+ expected = analysis['expected']
369
+ report_id = expected['key'] if expected and isinstance(expected['key'], int) else None
370
+ result_key = ('analogy', comparison_scope, analysis['id'])
371
+ row_key = f'analogy:{analysis["id"]}'
372
+ search_label = 'Refresh answers'
373
+ st.caption('Search near C + mean(B − A), excluding the input tokens. Results refer to the last shown relationship.')
374
+ if eligible:
375
+ count_col, button_col = st.columns([1, 2], vertical_alignment='bottom')
376
+ neighbour_count = count_col.number_input('Results', min_value=1, max_value=50, value=10, step=1)
377
+ search = button_col.button(search_label, type='primary', key='find_neighbours')
378
+ requested = st.session_state.pop(f'run_analogy:{comparison_scope}', False) if search_mode == 'Analogy answers' else False
379
+ previous_search = any(isinstance(key, tuple) and len(key) == 3 and key[:2] == (comparison_scope, source_id)
380
+ for key in results) if search_mode == 'Nearest tokens' else False
381
+ if search or requested or (result_key not in results and previous_search):
382
+ bar = st.progress(0, text='Scanning embedding vectors…')
383
+ try:
384
+ found = search_embeddings(read_rows, shape[0], query_vector, count=50, exclude_ids=excluded, report_id=report_id,
385
+ progress=lambda done, total: bar.progress(done / total, text=f'Scanning embeddings: {done:,} / {total:,}'))
386
+ results[result_key] = found if search_mode == 'Analogy answers' else found['neighbours']
387
+ except Exception as e:
388
+ st.error(f'Could not find neighbours: {e}')
389
+ finally:
390
+ bar.empty()
391
+ if result_key in results:
392
+ found = results[result_key]
393
+ if search_mode == 'Analogy answers':
394
+ neighbours = found['neighbours'][:int(neighbour_count)]
395
+ if report_id is not None:
396
+ if found['expected_rank'] is not None:
397
+ st.caption(f'Expected {token_label(report_id)}: rank {found["expected_rank"]:,} of {found["valid_count"]:,}, cosine {found["expected_score"]:.6f}.')
398
+ else:
399
+ st.caption('Expected D is excluded as an input token, or has a zero/nonfinite embedding.')
400
+ else:
401
+ neighbours = found[:int(neighbour_count)]
402
+ if neighbours:
403
+ st.caption(f'Showing {len(neighbours)} neighbours. Change Results to show more or fewer without searching again.')
404
+ with st.container(border=False):
405
+ header = st.columns([.35, 1.7, 1, 1.8])
406
+ for column, title in zip(header, ['#', 'Token', 'Cosine', '']):
407
+ if title:
408
+ column.markdown(f'**{title}**')
409
+ for rank, (i, score) in enumerate(neighbours, 1):
410
+ rank_col, token_col, score_col, action_col = st.columns([.35, 1.7, 1, 1.8], vertical_alignment='center')
411
+ rank_col.text(str(rank))
412
+ token_col.text(token_label(i))
413
+ score_col.write(f'{score:.6f}')
414
+ action_col.button('On chart' if i in ids else 'Add to chart',
415
+ key=f'add_neighbour:{comparison_scope}:{row_key}:{i}',
416
+ disabled=i in ids or len(vectors) >= 20, on_click=add_neighbour, args=(i,), use_container_width=True)
417
+ if len(vectors) >= 20:
418
+ st.caption('The chart has reached its 20-curve limit. Remove a curve before adding another.')
419
+ st.caption('Cosine similarity ranges from −1 to 1. Nearby tokens can reflect spelling, boundaries or usage as well as meaning.')
420
+ else:
421
+ st.info('No other eligible tokens with finite, nonzero embeddings were found.')
422
+ with details_panel:
423
+ st.dataframe([{'Token': labels[i], 'Min': float(v.min()), 'Max': float(v.max()),
424
+ 'Mean': float(v.mean()), 'Std dev': float(v.std()), 'L2 norm': float(np.linalg.norm(v))}
425
+ for i, v in vectors.items()], use_container_width=True, hide_index=True)
426
+ output = io.StringIO()
427
+ writer = csv.writer(output)
428
+ writer.writerow(['dimension'] + [labels[i] for i in chart_ids])
429
+ writer.writerows([d] + [float(vectors[i][d]) for i in chart_ids] for d in range(shape[1]))
430
+ st.download_button('Download values as CSV', output.getvalue(), 'token_embeddings.csv', 'text/csv')
431
+ with st.expander('Inspect exact values'):
432
+ st.dataframe({'dimension': np.arange(shape[1]), **{labels[i]: vectors[i] for i in chart_ids}}, use_container_width=True)