tokenrace 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tokenrace/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """TokenRace: benchmark della velocità di server LLM locali compatibili OpenAI."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,11 @@
1
+ # Nessuna statistica d'uso inviata a Streamlit: l'app resta tutta locale.
2
+ [browser]
3
+ gatherUsageStats = false
4
+
5
+ [client]
6
+ # Nasconde il pulsante "Deploy" e lascia solo il menu essenziale.
7
+ toolbarMode = "minimal"
8
+
9
+ [theme]
10
+ primaryColor = "#FF5A1F"
11
+ baseRadius = "0.6rem"
tokenrace/_data/app.py ADDED
@@ -0,0 +1,414 @@
1
+ """TokenRace - Local LLM Speed Test.
2
+
3
+ Interfaccia Streamlit. La logica di misura sta in tokenrace/bench.py,
4
+ i valori modificabili in config/, i testi tradotti in locales/.
5
+
6
+ Avvio: streamlit run app.py
7
+ Licenza: MIT
8
+ """
9
+
10
+ import base64
11
+ import json
12
+ import os
13
+ import time
14
+ import uuid
15
+ from datetime import datetime
16
+
17
+ import altair as alt # incluso con Streamlit
18
+ import pandas as pd
19
+ import streamlit as st
20
+
21
+ from tokenrace import bench
22
+ from tokenrace.config import ROOT, load_defaults, load_prompts, load_servers
23
+ from tokenrace.i18n import Translator, language_info
24
+
25
+ # Metriche riassunte nella tabella di riepilogo (chiavi interne del CSV)
26
+ SUMMARY_METRICS = ["ttft", "tok_s_gen", "tok_s_eff", "total", "prompt_tok_s"]
27
+ # Colonne della tabella delle ripetizioni del test in corso
28
+ RUN_COLUMNS = ["run", "cold"] + SUMMARY_METRICS + ["tok_out"]
29
+ # Spiegazione (sezione [help]) di ciascuna colonna; le altre usano [colhelp]
30
+ COLUMN_HELP = {"ttft": "help.ttft", "tok_s_gen": "help.gen", "tok_s_eff": "help.eff",
31
+ "total": "help.total", "prompt_tok_s": "help.prompt_speed",
32
+ "ram_before": "help.ram_before", "ram_peak": "help.ram_peak"}
33
+ PRESET_IDS = ["short", "long", "cache"]
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Utilità
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def fmt(v, dec=2):
41
+ return "—" if v is None else f"{v:.{dec}f}"
42
+
43
+
44
+ def reasoning_md(t, text: str) -> str:
45
+ if not text:
46
+ return ""
47
+ return f"> 💭 *{t('label.reasoning')}:* " + text[-3000:].replace("\n", " ") + "\n\n"
48
+
49
+
50
+ def status_badge(text: str, state: str) -> str:
51
+ """Etichetta dello stato della ripetizione (state: running, done)."""
52
+ return (f'<div class="tr-status-wrap"><span class="tr-status tr-status-{state}">'
53
+ f'<span class="tr-dot"></span>{text}</span></div>')
54
+
55
+
56
+ def error_text(t, e: bench.ServerError) -> str:
57
+ return t(f"err.{e.code}", **e.params)
58
+
59
+
60
+ def pick_language(languages: list[str], default: str) -> str:
61
+ """Lingua indicata nell'URL (?lang=), altrimenti quella predefinita."""
62
+ qp = st.query_params.get("lang")
63
+ env = os.environ.get("TOKENRACE_LANG") # impostata dal comando `tokenrace --lang`
64
+ return qp if qp in languages else env if env in languages else default
65
+
66
+
67
+ def show_table(t, df: pd.DataFrame):
68
+ """Mostra un DataFrame con colonne interne tradotte e spiegazioni nelle intestazioni."""
69
+ names, config = {}, {}
70
+ for c in df.columns:
71
+ base, _, stat = c.partition("·")
72
+ names[c] = t(f"col.{base}") + (f" · {t('stat.' + stat)}" if stat else "")
73
+ help_key = COLUMN_HELP.get(base, f"colhelp.{base}")
74
+ if base in ("server", "model"):
75
+ config[names[c]] = st.column_config.TextColumn(width="medium")
76
+ elif t.has(help_key):
77
+ config[names[c]] = st.column_config.Column(help=t(help_key))
78
+ st.dataframe(df.rename(columns=names), hide_index=True, width="stretch", column_config=config)
79
+
80
+
81
+ def make_row(ctx, rip: int, m: dict) -> dict:
82
+ p = ctx["params"]
83
+ return {
84
+ "ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
85
+ "server": ctx["server"].name,
86
+ "url": ctx["base_url"],
87
+ "model": ctx["model"],
88
+ "prompt_type": ctx["preset"],
89
+ "prompt_lang": ctx["lang"],
90
+ "run": rip,
91
+ "cold": rip == 1,
92
+ "temperature": p["temperature"],
93
+ "seed": p["seed"],
94
+ "max_tokens": p["max_tokens"],
95
+ "repetitions": p["repetitions"],
96
+ "no_reasoning": bool(p["no_reasoning"]),
97
+ "no_reasoning_fields": json.dumps(p["no_reasoning"]) if p["no_reasoning"] else "",
98
+ **{k: m[k] for k in ("ttft", "tok_s_gen", "tok_s_eff", "total", "tok_in", "tok_out",
99
+ "tok_reason", "tok_answer", "prompt_tok_s", "ram_before",
100
+ "ram_peak", "estimated", "split_estimated")},
101
+ }
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Sezioni della pagina
106
+ # ---------------------------------------------------------------------------
107
+
108
+ # Mascotte nell'intestazione, incorporata nella pagina come data URI
109
+ LOGO = "data:image/svg+xml;base64," + base64.b64encode(
110
+ (ROOT / "assets" / "logo" / "tokenrace-icon.svg").read_bytes()).decode()
111
+
112
+
113
+ def render_header(t, n_servers: int, languages: list[str], lang: str) -> str:
114
+ left, right = st.columns([6, 1], vertical_alignment="center")
115
+ left.markdown(
116
+ f"""<div class="tr-hero"><h1><img src="{LOGO}" alt="">Token<span>Race</span></h1>
117
+ <p>{t('app.tagline')}</p>
118
+ <span class="tr-pill">{t('app.servers_pill', n=n_servers)}</span></div>""",
119
+ unsafe_allow_html=True)
120
+ info = {code: language_info(code) for code in languages}
121
+ # Il valore iniziale arriva da st.session_state["lang_choice"], impostato in main()
122
+ choice = right.segmented_control(
123
+ t("label.language"), languages, key="lang_choice",
124
+ format_func=lambda c: f"{info[c]['flag']} {c.upper()}",
125
+ label_visibility="collapsed", help=t("help.language"))
126
+ return choice or lang
127
+
128
+
129
+ def render_server_form(t, servers, measure):
130
+ ss = st.session_state
131
+ by_id = {s.id: s for s in servers}
132
+ with st.container(border=True):
133
+ st.markdown(f"##### {t('section.server')}")
134
+ c1, c2, c5, c3, c4 = st.columns([1.4, 1.9, 0.9, 2, 1.6], vertical_alignment="bottom")
135
+ server = by_id[c1.selectbox(t("label.server"), list(by_id), key="server",
136
+ format_func=lambda i: by_id[i].name, help=t("help.server"))]
137
+ base_url = c2.text_input(t("label.url"), value=server.url, key=f"url_{server.id}",
138
+ help=t("help.url"))
139
+ api_key = c5.text_input(t("label.api_key"), type="password", key=f"key_{server.id}",
140
+ help=t("help.api_key"))
141
+ error = None
142
+ if c4.button(t("label.refresh"), help=t("help.refresh"), width="stretch"):
143
+ try:
144
+ ss.models[base_url] = bench.fetch_models(base_url, api_key,
145
+ measure["models_timeout_s"])
146
+ if not ss.models[base_url]:
147
+ error = ("warning", t("msg.no_models"))
148
+ except bench.ServerError as e:
149
+ ss.models.pop(base_url, None)
150
+ error = ("error", error_text(t, e))
151
+ models = ss.models.get(base_url, [])
152
+ model = c3.selectbox(t("label.model"), models, key=f"model_{base_url}",
153
+ placeholder=t("label.model_placeholder"),
154
+ index=0 if models else None, help=t("help.model"))
155
+ note = t(f"server_notes.{server.id}")
156
+ start = f"**{t('label.start_cmd')}:** `{server.start}` · " if server.start else ""
157
+ st.caption(start + note)
158
+ if error:
159
+ getattr(st, error[0])(error[1])
160
+ return server, base_url, api_key, model
161
+
162
+
163
+ def render_prompt_form(t, server, presets, defaults, model):
164
+ ss = st.session_state
165
+ d = defaults["params"]
166
+ with st.container(border=True):
167
+ st.markdown(f"##### {t('section.prompt')}")
168
+ r = st.columns([1.3, 1, 1, 1, 1, 1.2, 1], vertical_alignment="bottom")
169
+ # Una chiave per lingua: le etichette cambiano con la lingua, il valore scelto
170
+ # (short/long/cache) è tenuto a parte in ss.preset.
171
+ widget_key = f"preset_{t.lang}"
172
+
173
+ def on_preset():
174
+ ss.preset = ss[widget_key]
175
+ ss.prompt = presets[ss.preset]
176
+
177
+ r[0].selectbox(t("label.preset"), PRESET_IDS, index=PRESET_IDS.index(ss.preset),
178
+ key=widget_key, format_func=lambda i: t(f"preset.{i}"),
179
+ on_change=on_preset, help=t("help.preset"))
180
+ params = {
181
+ "temperature": r[1].number_input(t("label.temperature"), 0.0, 2.0, float(d["temperature"]),
182
+ 0.1, key="temperature", help=t("help.temperature")),
183
+ "seed": int(r[2].number_input(t("label.seed"), value=int(d["seed"]), step=1, key="seed",
184
+ help=t("help.seed"))),
185
+ "max_tokens": int(r[3].number_input(t("label.max_tokens"), 1, 32768, int(d["max_tokens"]),
186
+ 64, key="max_tokens", help=t("help.max_tokens"))),
187
+ "repetitions": int(r[4].number_input(t("label.repetitions"), 1, 50, int(d["repetitions"]),
188
+ 1, key="repetitions", help=t("help.repetitions"))),
189
+ }
190
+ start = r[5].button(t("label.start"), type="primary", disabled=not model,
191
+ help=t("help.start"), width="stretch")
192
+ stop = r[6].button(t("label.stop"), help=t("help.stop"), width="stretch")
193
+
194
+ prompt = st.text_area(t("label.prompt"), key="prompt", height=90, help=t("help.prompt"))
195
+ n1, n2 = st.columns([3, 1.2], vertical_alignment="center")
196
+ n1.caption(f"**{t('preset.' + ss.preset)}**: {t('preset_descr.' + ss.preset)} · "
197
+ + t("preset_descr.token_estimate",
198
+ n=f"{bench.estimate_tokens(prompt):,}".replace(",", ".")))
199
+ fields = json.dumps(server.no_reasoning)
200
+ state = t("msg.nr_tested") if server.no_reasoning_tested else t("msg.nr_untested")
201
+ nr_on = n2.toggle(t("label.no_reasoning"), key="no_reasoning",
202
+ help=t("help.no_reasoning") + "\n\n"
203
+ + t("msg.nr_help_suffix", server=server.name, fields=fields) + f" ({state}).")
204
+ params["no_reasoning"] = server.no_reasoning if nr_on else None
205
+ if nr_on:
206
+ st.caption(t("msg.nr_sends", fields=fields, state=state))
207
+ if stop:
208
+ st.warning(t("msg.stopped"))
209
+ return prompt, params, start
210
+
211
+
212
+ def run_test(t, ctx, prompt, run_id_label, measure, resp_box, status_box, live_box, reps_box):
213
+ """Esegue le ripetizioni aggiornando risposta e misure in tempo reale."""
214
+ ss = st.session_state
215
+ p = ctx["params"]
216
+ rows = []
217
+ for rip in range(1, p["repetitions"] + 1):
218
+ text = prompt
219
+ if ctx["preset"] in ("short", "long"):
220
+ text = f"[{run_id_label}: {uuid.uuid4()}]\n{prompt}"
221
+ last_ui = [0.0]
222
+ status = t("msg.run_status", i=rip, n=p["repetitions"]) + (t("msg.cold") if rip == 1 else "")
223
+ status_box.markdown(status_badge(f"{status} · {t('msg.in_progress')}", "running"),
224
+ unsafe_allow_html=True)
225
+
226
+ def on_update(r_txt, c_txt, live, status=status):
227
+ now = time.perf_counter()
228
+ if now - last_ui[0] < measure["ui_refresh_s"]:
229
+ return
230
+ last_ui[0] = now
231
+ resp_box.markdown(reasoning_md(t, r_txt) + c_txt)
232
+ with live_box.container():
233
+ a, b = st.columns(2)
234
+ a.metric(t("metric.ttft"), f"{live['ttft']:.2f}", help=t("help.ttft"), border=True)
235
+ b.metric(t("metric.chunks"), live["chunks"], help=t("help.chunks"), border=True)
236
+ a.metric(t("metric.elapsed"), f"{live['elapsed']:.1f}", help=t("help.elapsed"),
237
+ border=True)
238
+ b.metric(t("metric.ram_peak"), f"{live['ram_peak']:.1f}", help=t("help.ram_peak"),
239
+ border=True)
240
+
241
+ try:
242
+ m = bench.run_once(ctx["base_url"], ctx["model"], text, p["temperature"], p["seed"],
243
+ p["max_tokens"], on_update, p["no_reasoning"], ctx["api_key"],
244
+ ram_interval=measure["ram_interval_s"],
245
+ timeout=(measure["connect_timeout_s"], measure["read_timeout_s"]))
246
+ except bench.ServerError as e:
247
+ status_box.empty()
248
+ st.error(error_text(t, e))
249
+ break
250
+
251
+ resp_box.markdown(reasoning_md(t, m["reasoning"]) + m["content"])
252
+ status_box.markdown(status_badge(f"{status} · {t('msg.completed')}", "done"),
253
+ unsafe_allow_html=True)
254
+ row = make_row(ctx, rip, m)
255
+ ss.history.append(row)
256
+ rows.append(row)
257
+
258
+ with live_box.container():
259
+ a, b, c = st.columns(3)
260
+ a.metric(t("metric.ttft"), fmt(m["ttft"]), help=t("help.ttft"), border=True)
261
+ b.metric(t("metric.gen"), fmt(m["tok_s_gen"], 1), help=t("help.gen"), border=True)
262
+ c.metric(t("metric.eff"), fmt(m["tok_s_eff"], 1), help=t("help.eff"), border=True)
263
+ a.metric(t("metric.total"), fmt(m["total"]), help=t("help.total"), border=True)
264
+ b.metric(t("metric.tokens_io"), f"{m['tok_in']}/{m['tok_out']}", help=t("help.tokens_io"),
265
+ border=True)
266
+ c.metric(t("metric.prompt_speed"), fmt(m["prompt_tok_s"], 0), help=t("help.prompt_speed"),
267
+ border=True)
268
+ a.metric(t("metric.split"), f"{m['tok_reason']}/{m['tok_answer']}", help=t("help.split"),
269
+ border=True)
270
+ b.metric(t("metric.ram_before"), f"{m['ram_before']:.1f}", help=t("help.ram_before"),
271
+ border=True)
272
+ c.metric(t("metric.ram_peak"), f"{m['ram_peak']:.1f}", help=t("help.ram_peak"), border=True)
273
+ st.caption(t("msg.ram_note"))
274
+ if m["estimated"]:
275
+ st.warning(t("msg.estimated"))
276
+ if p["no_reasoning"] and m["tok_reason"] > 0:
277
+ st.warning(t("msg.reasoning_still"))
278
+ if m["split_estimated"]:
279
+ st.caption(t("msg.split_estimated"))
280
+
281
+ with reps_box.container():
282
+ show_table(t, pd.DataFrame(rows)[RUN_COLUMNS].round(2))
283
+
284
+
285
+ def render_results(t, primary_color: str):
286
+ ss = st.session_state
287
+ with st.container(border=True):
288
+ st.markdown(f"##### {t('section.results')}")
289
+ if not ss.history:
290
+ st.info(t("msg.no_results"))
291
+ return
292
+ df = pd.DataFrame(ss.history)
293
+ df["prompt_type"] = df["prompt_type"].map(lambda i: t(f"preset.{i}"))
294
+ keys = ["server", "model", "prompt_type"]
295
+ warm = df[~df["cold"]]
296
+ medians, mins, maxs = [], [], []
297
+ for m in SUMMARY_METRICS:
298
+ medians.append(warm.groupby(keys)[m].median().rename(f"{m}·median"))
299
+ mins.append(df.groupby(keys)[m].min().rename(f"{m}·min"))
300
+ maxs.append(df.groupby(keys)[m].max().rename(f"{m}·max"))
301
+ summary = pd.concat(medians + mins + maxs, axis=1).reset_index()
302
+
303
+ g1, g2 = st.columns([3, 2])
304
+ with g1:
305
+ st.markdown(t("label.summary"))
306
+ show_table(t, summary.round(2))
307
+ with g2:
308
+ st.markdown(t("label.chart"))
309
+ if warm.empty:
310
+ st.caption(t("msg.need_two"))
311
+ else:
312
+ # Una barra per server e modello; il tipo di prompt si aggiunge
313
+ # all'etichetta solo se nello storico ce n'è più di uno.
314
+ groups = keys if warm["prompt_type"].nunique() > 1 else keys[:2]
315
+ bars = warm.groupby(groups)["tok_s_eff"].median().rename("tok/s").reset_index()
316
+ axis = t("label.chart_axis")
317
+ bars[axis] = bars[groups].astype(str).agg(" · ".join, axis=1)
318
+ base = alt.Chart(bars).encode(
319
+ x=alt.X("tok/s:Q", title="tok/s"),
320
+ y=alt.Y(f"{axis}:N", sort="-x", title=None, axis=alt.Axis(labelLimit=0)),
321
+ tooltip=[axis, alt.Tooltip("tok/s:Q", format=".1f")])
322
+ chart = base.mark_bar(color=primary_color, cornerRadiusEnd=4) + base.mark_text(
323
+ align="left", dx=4, fontWeight="bold").encode(text=alt.Text("tok/s:Q", format=".1f"))
324
+ st.altair_chart(chart.properties(height=50 + 45 * len(bars)), width="stretch")
325
+
326
+ with st.expander(t("label.all_runs")):
327
+ show_table(t, df.round(3))
328
+
329
+ d1, d2, _ = st.columns([1, 1, 5])
330
+ csv = df.rename(columns=lambda c: t(f"col.{c}")).to_csv(index=False).encode("utf-8")
331
+ d1.download_button(t("label.download"), csv,
332
+ file_name=f"tokenrace_{datetime.now():%Y%m%d_%H%M%S}.csv",
333
+ mime="text/csv", help=t("help.download"), width="stretch")
334
+ if d2.button(t("label.clear"), help=t("help.clear"), width="stretch"):
335
+ ss.history = []
336
+ st.rerun()
337
+
338
+
339
+ # ---------------------------------------------------------------------------
340
+ # Pagina
341
+ # ---------------------------------------------------------------------------
342
+
343
+ def main():
344
+ st.set_page_config(page_title="TokenRace", page_icon=str(ROOT / "assets" / "logo" / "tokenrace-favicon.png"),
345
+ layout="wide")
346
+ defaults = load_defaults()
347
+ ui, measure = defaults["ui"], defaults["measure"]
348
+ servers = load_servers()
349
+ css = (ROOT / "assets" / "style.css").read_text(encoding="utf-8")
350
+ st.markdown(f"<style>:root {{--tr-primary: {ui['primary_color']};}}\n{css}</style>",
351
+ unsafe_allow_html=True)
352
+
353
+ ss = st.session_state
354
+ ss.setdefault("history", [])
355
+ ss.setdefault("models", {})
356
+
357
+ # Lingua: scelta nell'intestazione e ricordata nell'URL (?lang=it).
358
+ # Se l'URL cambia da fuori (link, ricarica), ha la precedenza sulla scelta in memoria.
359
+ url_lang = pick_language(ui["languages"], ui["default_language"])
360
+ if url_lang != ss.get("applied_lang"):
361
+ ss.lang_choice = url_lang
362
+ lang = ss.get("lang_choice") or ss.get("applied_lang") or url_lang
363
+ t = Translator(lang, ui["fallback_language"])
364
+ render_header(t, len(servers), ui["languages"], lang)
365
+ st.query_params["lang"] = lang
366
+ ss.applied_lang = lang
367
+
368
+ all_prompts = load_prompts()
369
+ target = defaults["long_prompt"]["target_chars"]
370
+ prompt_texts = all_prompts.get(lang, all_prompts[ui["fallback_language"]])
371
+ presets = bench.build_prompts(prompt_texts, target)
372
+ ss.setdefault("preset", "short")
373
+ # Alla prima apertura, o se cambia la lingua e il prompt non è stato modificato,
374
+ # mostra il prompt pronto nella lingua scelta.
375
+ old_lang = ss.get("prompt_lang")
376
+ if "prompt" not in ss:
377
+ ss.prompt = presets[ss.preset]
378
+ elif old_lang and old_lang != lang and old_lang in all_prompts:
379
+ if ss.prompt == bench.build_prompts(all_prompts[old_lang], target)[ss.preset]:
380
+ ss.prompt = presets[ss.preset]
381
+ ss.prompt_lang = lang
382
+
383
+ server, base_url, api_key, model = render_server_form(t, servers, measure)
384
+ prompt, params, start = render_prompt_form(t, server, presets, defaults, model)
385
+
386
+ left, right = st.columns([3, 2])
387
+ with left.container(border=True):
388
+ st.markdown(f"##### {t('section.response')}")
389
+ resp_box = st.container(height=ui["response_height"], border=False).empty()
390
+ with right.container(border=True):
391
+ h1, h2 = st.columns([3, 2], vertical_alignment="center")
392
+ h1.markdown(f"##### {t('section.live')}")
393
+ status_box = h2.empty()
394
+ live_box = st.empty()
395
+ reps_box = st.empty()
396
+
397
+ if start and model:
398
+ ctx = {"server": server, "base_url": base_url, "api_key": api_key, "model": model,
399
+ "preset": ss.preset, "lang": lang, "params": params}
400
+ run_test(t, ctx, prompt, prompt_texts["run_id"], measure, resp_box, status_box, live_box,
401
+ reps_box)
402
+ else:
403
+ resp_box.caption(t("msg.response_placeholder"))
404
+ live_box.caption(t("msg.live_placeholder"))
405
+
406
+ render_results(t, ui["primary_color"])
407
+ repo = ui["repo_url"]
408
+ st.markdown(f'<div class="tr-footer">TokenRace · {t("app.footer")} · '
409
+ f'<a href="{repo}" target="_blank">{repo.removeprefix("https://")}</a></div>',
410
+ unsafe_allow_html=True)
411
+
412
+
413
+ if __name__ == "__main__":
414
+ main()
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <defs><pattern id="c2" width="10" height="10" patternUnits="userSpaceOnUse">
3
+ <rect width="10" height="10" fill="#FFFFFF"/><rect width="5" height="5" fill="#1E1E1E"/><rect x="5" y="5" width="5" height="5" fill="#1E1E1E"/></pattern>
4
+ <clipPath id="cc"><circle cx="31" cy="33" r="27"/></clipPath></defs>
5
+ <circle cx="33" cy="33.5" r="30.5" fill="#FFFFFF"/>
6
+ <circle cx="34" cy="34" r="27" fill="#B93A0C" stroke="#2A1A12" stroke-width="4"/>
7
+ <circle cx="31" cy="33" r="27" fill="#FF5A1F"/>
8
+ <g clip-path="url(#cc)"><path d="M0 16 Q31 6 64 16 L64 26 Q31 16 0 26 Z" fill="url(#c2)" stroke="#2A1A12" stroke-width="3"/></g>
9
+ <circle cx="31" cy="33" r="27" fill="none" stroke="#2A1A12" stroke-width="4"/>
10
+ <ellipse cx="24" cy="35" rx="5" ry="6.5" fill="#FFFFFF" stroke="#2A1A12" stroke-width="2.5"/>
11
+ <ellipse cx="39" cy="35" rx="5" ry="6.5" fill="#FFFFFF" stroke="#2A1A12" stroke-width="2.5"/>
12
+ <circle cx="26" cy="36" r="3" fill="#2A1A12"/><circle cx="41" cy="36" r="3" fill="#2A1A12"/>
13
+ <path d="M22 46 Q31 55 41 45 Z" fill="#2A1A12" stroke="#2A1A12" stroke-width="2.5" stroke-linejoin="round"/>
14
+ </svg>
@@ -0,0 +1,47 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6 28 250 206">
2
+ <defs>
3
+ <filter id="sticker" x="-15%" y="-15%" width="130%" height="130%">
4
+ <feMorphology in="SourceAlpha" operator="dilate" radius="6" result="d"/>
5
+ <feFlood flood-color="#FFFFFF"/><feComposite in2="d" operator="in" result="w"/>
6
+ <feMerge><feMergeNode in="w"/><feMergeNode in="SourceGraphic"/></feMerge>
7
+ </filter>
8
+ <clipPath id="edge"><circle cx="153" cy="133" r="80"/></clipPath>
9
+ <pattern id="chk" width="12" height="12" patternUnits="userSpaceOnUse">
10
+ <rect width="12" height="12" fill="#FFFFFF"/>
11
+ <rect width="6" height="6" fill="#1E1E1E"/><rect x="6" y="6" width="6" height="6" fill="#1E1E1E"/>
12
+ </pattern>
13
+ <radialGradient id="face" cx="0.38" cy="0.32" r="0.8">
14
+ <stop offset="0" stop-color="#FF8A4F"/><stop offset="0.55" stop-color="#FF5A1F"/><stop offset="1" stop-color="#E5470F"/>
15
+ </radialGradient>
16
+ <clipPath id="coin"><circle cx="140" cy="128" r="80"/></clipPath>
17
+ <clipPath id="mouth"><path d="M114 150 Q141 186 170 147 Z"/></clipPath>
18
+ </defs><g filter="url(#sticker)"><g transform="rotate(4 140 140)">
19
+ <g stroke-linecap="round" fill="none">
20
+ <path d="M4 150 H34" stroke="#FF5A1F" stroke-width="9"/>
21
+ <path d="M14 174 H52" stroke="#2A1A12" stroke-width="9"/>
22
+ <path d="M8 198 H58" stroke="#FFA27A" stroke-width="9"/>
23
+ </g>
24
+ <g stroke="#2A1A12" stroke-width="5" stroke-linejoin="round">
25
+ <path d="M70 86 C50 76 34 84 14 72 L20 94 C38 100 52 96 70 102 Z" fill="url(#chk)"/>
26
+ <path d="M70 100 C54 108 40 118 22 116 L32 132 C48 130 58 120 72 112 Z" fill="url(#chk)"/>
27
+ </g>
28
+ <circle cx="153" cy="133" r="80" fill="#B93A0C" stroke="#2A1A12" stroke-width="7"/>
29
+ <g clip-path="url(#edge)" stroke="#2A1A12" stroke-width="3.5" stroke-opacity="0.8"><path d="M164.8 66.0 L167.6 50.3"/><path d="M174.0 68.3 L179.0 53.1"/><path d="M182.8 71.9 L189.8 57.5"/><path d="M191.0 76.6 L200.0 63.4"/><path d="M198.5 82.5 L209.2 70.6"/><path d="M205.1 89.3 L217.3 79.0"/><path d="M210.7 97.0 L224.2 88.5"/><path d="M215.1 105.3 L229.7 98.8"/><path d="M218.4 114.3 L233.7 109.8"/><path d="M220.3 123.5 L236.2 121.3"/><path d="M221.0 133.0 L237.0 133.0"/><path d="M220.3 142.5 L236.2 144.7"/><path d="M218.4 151.7 L233.7 156.2"/><path d="M215.1 160.7 L229.7 167.2"/><path d="M210.7 169.0 L224.2 177.5"/><path d="M205.1 176.7 L217.3 187.0"/><path d="M198.5 183.5 L209.2 195.4"/><path d="M191.0 189.4 L200.0 202.6"/><path d="M182.8 194.1 L189.8 208.5"/><path d="M174.0 197.7 L179.0 212.9"/><path d="M164.8 200.0 L167.6 215.7"/><path d="M155.4 201.0 L155.9 216.9"/><path d="M145.9 200.6 L144.2 216.5"/><path d="M136.5 199.0 L132.7 214.5"/><path d="M127.5 196.0 L121.5 210.9"/><path d="M119.0 191.9 L111.0 205.7"/><path d="M111.1 186.6 L101.3 199.2"/><path d="M104.1 180.2 L92.6 191.4"/><path d="M98.0 173.0 L85.0 182.4"/><path d="M93.0 164.9 L78.8 172.4"/><path d="M89.1 156.3 L74.1 161.7"/><path d="M86.5 147.1 L70.8 150.5"/><path d="M85.2 137.7 L69.2 138.9"/><path d="M85.2 128.3 L69.2 127.1"/></g>
30
+ <circle cx="140" cy="128" r="80" fill="url(#face)"/>
31
+ <circle cx="140" cy="128" r="68" fill="none" stroke="#FFFFFF" stroke-opacity="0.28" stroke-width="4"/>
32
+ <path d="M82 118 A60 60 0 0 1 100 84" fill="none" stroke="#FFFFFF" stroke-opacity="0.55" stroke-width="7" stroke-linecap="round"/><g clip-path="url(#coin)">
33
+ <path d="M50 86 Q140 60 230 86 L230 106 Q140 80 50 106 Z" fill="url(#chk)" stroke="#2A1A12" stroke-width="5"/>
34
+ </g><circle cx="140" cy="128" r="80" fill="none" stroke="#2A1A12" stroke-width="7"/>
35
+ <g stroke="#2A1A12" stroke-linecap="round">
36
+ <path d="M106 106 Q117 100 129 106" stroke-width="7" fill="none"/><path d="M153 106 Q165 100 177 106" stroke-width="7" fill="none"/>
37
+ </g>
38
+ <g stroke="#2A1A12" stroke-width="5">
39
+ <ellipse cx="118" cy="128" rx="13" ry="17" fill="#FFFFFF"/>
40
+ <ellipse cx="164" cy="128" rx="13" ry="17" fill="#FFFFFF"/>
41
+ </g>
42
+ <circle cx="123" cy="131" r="7.5" fill="#2A1A12"/><circle cx="169" cy="131" r="7.5" fill="#2A1A12"/>
43
+ <circle cx="126" cy="127" r="2.6" fill="#FFFFFF"/><circle cx="172" cy="127" r="2.6" fill="#FFFFFF"/>
44
+ <ellipse cx="98" cy="154" rx="9" ry="6" fill="#FFB4A0" opacity="0.75"/>
45
+ <ellipse cx="186" cy="152" rx="9" ry="6" fill="#FFB4A0" opacity="0.75"/>
46
+ <path d="M114 150 Q141 186 170 147 Z" fill="#2A1A12" stroke="#2A1A12" stroke-width="5" stroke-linejoin="round"/>
47
+ <g clip-path="url(#mouth)"><ellipse cx="145" cy="172" rx="14" ry="8" fill="#FF8FA3"/></g></g></g></svg>
@@ -0,0 +1,57 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -4 272 264">
2
+ <defs>
3
+ <filter id="sticker" x="-15%" y="-15%" width="130%" height="130%">
4
+ <feMorphology in="SourceAlpha" operator="dilate" radius="6" result="d"/>
5
+ <feFlood flood-color="#FFFFFF"/><feComposite in2="d" operator="in" result="w"/>
6
+ <feMerge><feMergeNode in="w"/><feMergeNode in="SourceGraphic"/></feMerge>
7
+ </filter>
8
+ <clipPath id="edge"><circle cx="153" cy="133" r="80"/></clipPath>
9
+ <pattern id="chk" width="12" height="12" patternUnits="userSpaceOnUse">
10
+ <rect width="12" height="12" fill="#FFFFFF"/>
11
+ <rect width="6" height="6" fill="#1E1E1E"/><rect x="6" y="6" width="6" height="6" fill="#1E1E1E"/>
12
+ </pattern>
13
+ <radialGradient id="face" cx="0.38" cy="0.32" r="0.8">
14
+ <stop offset="0" stop-color="#FF8A4F"/><stop offset="0.55" stop-color="#FF5A1F"/><stop offset="1" stop-color="#E5470F"/>
15
+ </radialGradient>
16
+ <clipPath id="coin"><circle cx="140" cy="128" r="80"/></clipPath>
17
+ <clipPath id="mouth"><path d="M114 150 Q141 186 170 147 Z"/></clipPath>
18
+ </defs><g filter="url(#sticker)"><g transform="rotate(6 140 140)">
19
+ <g stroke-linecap="round" fill="none">
20
+ <path d="M4 150 H34" stroke="#FF5A1F" stroke-width="9"/>
21
+ <path d="M14 174 H52" stroke="#2A1A12" stroke-width="9"/>
22
+ <path d="M8 198 H58" stroke="#FFA27A" stroke-width="9"/>
23
+ </g>
24
+ <g stroke="#2A1A12" stroke-width="5" stroke-linejoin="round">
25
+ <path d="M70 86 C50 76 34 84 14 72 L20 94 C38 100 52 96 70 102 Z" fill="url(#chk)"/>
26
+ <path d="M70 100 C54 108 40 118 22 116 L32 132 C48 130 58 120 72 112 Z" fill="url(#chk)"/>
27
+ </g>
28
+ <g stroke="#2A1A12" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-width="9">
29
+ <path d="M74 150 Q54 158 46 142"/>
30
+ <path d="M118 200 Q104 214 86 204"/>
31
+ </g>
32
+ <circle cx="45" cy="135" r="11" fill="#FFFFFF" stroke="#2A1A12" stroke-width="5"/>
33
+ <g transform="translate(78 196) rotate(35)" stroke="#2A1A12" stroke-width="5" stroke-linejoin="round"><path d="M-17 7 Q-17 -10 0 -10 Q21 -8 23 5 Q21 11 -17 7 Z" fill="#FFFFFF"/><path d="M-12 3 H18" stroke="#FF5A1F" stroke-width="4"/></g>
34
+ <path d="M160 204 Q172 218 174 234" stroke="#2A1A12" stroke-width="9" stroke-linecap="round" fill="none"/>
35
+ <g transform="translate(180 238) rotate(0)" stroke="#2A1A12" stroke-width="5" stroke-linejoin="round"><path d="M-17 7 Q-17 -10 0 -10 Q21 -8 23 5 Q21 11 -17 7 Z" fill="#FFFFFF"/><path d="M-12 3 H18" stroke="#FF5A1F" stroke-width="4"/></g>
36
+ <circle cx="153" cy="133" r="80" fill="#B93A0C" stroke="#2A1A12" stroke-width="7"/>
37
+ <g clip-path="url(#edge)" stroke="#2A1A12" stroke-width="3.5" stroke-opacity="0.8"><path d="M164.8 66.0 L167.6 50.3"/><path d="M174.0 68.3 L179.0 53.1"/><path d="M182.8 71.9 L189.8 57.5"/><path d="M191.0 76.6 L200.0 63.4"/><path d="M198.5 82.5 L209.2 70.6"/><path d="M205.1 89.3 L217.3 79.0"/><path d="M210.7 97.0 L224.2 88.5"/><path d="M215.1 105.3 L229.7 98.8"/><path d="M218.4 114.3 L233.7 109.8"/><path d="M220.3 123.5 L236.2 121.3"/><path d="M221.0 133.0 L237.0 133.0"/><path d="M220.3 142.5 L236.2 144.7"/><path d="M218.4 151.7 L233.7 156.2"/><path d="M215.1 160.7 L229.7 167.2"/><path d="M210.7 169.0 L224.2 177.5"/><path d="M205.1 176.7 L217.3 187.0"/><path d="M198.5 183.5 L209.2 195.4"/><path d="M191.0 189.4 L200.0 202.6"/><path d="M182.8 194.1 L189.8 208.5"/><path d="M174.0 197.7 L179.0 212.9"/><path d="M164.8 200.0 L167.6 215.7"/><path d="M155.4 201.0 L155.9 216.9"/><path d="M145.9 200.6 L144.2 216.5"/><path d="M136.5 199.0 L132.7 214.5"/><path d="M127.5 196.0 L121.5 210.9"/><path d="M119.0 191.9 L111.0 205.7"/><path d="M111.1 186.6 L101.3 199.2"/><path d="M104.1 180.2 L92.6 191.4"/><path d="M98.0 173.0 L85.0 182.4"/><path d="M93.0 164.9 L78.8 172.4"/><path d="M89.1 156.3 L74.1 161.7"/><path d="M86.5 147.1 L70.8 150.5"/><path d="M85.2 137.7 L69.2 138.9"/><path d="M85.2 128.3 L69.2 127.1"/></g>
38
+ <circle cx="140" cy="128" r="80" fill="url(#face)"/>
39
+ <circle cx="140" cy="128" r="68" fill="none" stroke="#FFFFFF" stroke-opacity="0.28" stroke-width="4"/>
40
+ <path d="M82 118 A60 60 0 0 1 100 84" fill="none" stroke="#FFFFFF" stroke-opacity="0.55" stroke-width="7" stroke-linecap="round"/><g clip-path="url(#coin)">
41
+ <path d="M50 86 Q140 60 230 86 L230 106 Q140 80 50 106 Z" fill="url(#chk)" stroke="#2A1A12" stroke-width="5"/>
42
+ </g><circle cx="140" cy="128" r="80" fill="none" stroke="#2A1A12" stroke-width="7"/>
43
+ <g stroke="#2A1A12" stroke-linecap="round">
44
+ <path d="M106 106 Q117 100 129 106" stroke-width="7" fill="none"/><path d="M153 106 Q165 100 177 106" stroke-width="7" fill="none"/>
45
+ </g>
46
+ <g stroke="#2A1A12" stroke-width="5">
47
+ <ellipse cx="118" cy="128" rx="13" ry="17" fill="#FFFFFF"/>
48
+ <ellipse cx="164" cy="128" rx="13" ry="17" fill="#FFFFFF"/>
49
+ </g>
50
+ <circle cx="123" cy="131" r="7.5" fill="#2A1A12"/><circle cx="169" cy="131" r="7.5" fill="#2A1A12"/>
51
+ <circle cx="126" cy="127" r="2.6" fill="#FFFFFF"/><circle cx="172" cy="127" r="2.6" fill="#FFFFFF"/>
52
+ <ellipse cx="98" cy="154" rx="9" ry="6" fill="#FFB4A0" opacity="0.75"/>
53
+ <ellipse cx="186" cy="152" rx="9" ry="6" fill="#FFB4A0" opacity="0.75"/>
54
+ <path d="M114 150 Q141 186 170 147 Z" fill="#2A1A12" stroke="#2A1A12" stroke-width="5" stroke-linejoin="round"/>
55
+ <g clip-path="url(#mouth)"><ellipse cx="145" cy="172" rx="14" ry="8" fill="#FF8FA3"/></g>
56
+ <path d="M214 148 Q240 152 242 126" stroke="#2A1A12" stroke-width="9" stroke-linecap="round" fill="none"/>
57
+ <circle cx="242" cy="119" r="11" fill="#FFFFFF" stroke="#2A1A12" stroke-width="5"/></g></g></svg>
@@ -0,0 +1,37 @@
1
+ /* Stile di TokenRace. Il colore principale arriva da config/defaults.toml
2
+ come variabile --tr-primary. */
3
+
4
+ .block-container {padding-top: 2rem; padding-bottom: 1rem; max-width: 1400px;}
5
+ h5 {margin: 0 0 0.6rem 0 !important; padding: 0 !important;}
6
+ [data-testid="stVerticalBlock"] {gap: 0.5rem;}
7
+ [data-testid="stWidgetLabel"] p {font-size: 0.8rem;}
8
+ .stTextInput input, .stNumberInput input, .stTextArea textarea,
9
+ [data-baseweb="select"] div {font-size: 0.85rem;}
10
+ [data-testid="stCaptionContainer"] {margin-top: -0.3rem;}
11
+ [data-testid="stMetricValue"] {font-size: 1.5rem;}
12
+ [data-testid="stMetricLabel"] p {font-size: 0.75rem;}
13
+
14
+ /* Intestazione */
15
+ .tr-hero {display: flex; align-items: baseline; gap: 0.8rem; flex-wrap: wrap;
16
+ margin-bottom: 0.6rem;}
17
+ .tr-hero h1 {font-size: 2rem; margin: 0; padding: 0; display: flex; align-items: center;}
18
+ .tr-hero h1 img {height: 2.6rem; margin-right: 0.45rem;}
19
+ .tr-hero h1 span {color: var(--tr-primary);}
20
+ .tr-hero p {margin: 0; opacity: 0.65; font-size: 0.95rem;}
21
+ .tr-pill {display: inline-block; padding: 0.1rem 0.6rem; border-radius: 999px;
22
+ background: color-mix(in srgb, var(--tr-primary) 12%, transparent);
23
+ color: var(--tr-primary); font-size: 0.75rem; font-weight: 600;}
24
+
25
+ /* Piè di pagina */
26
+ .tr-footer {text-align: center; opacity: 0.55; font-size: 0.8rem; margin-top: 1.5rem;}
27
+
28
+ /* Stato della ripetizione, in alto a destra del riquadro delle misure */
29
+ .tr-status-wrap {text-align: right; margin-bottom: 1rem;}
30
+ .tr-status {display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.15rem 0.7rem;
31
+ border-radius: 999px; font-size: 0.75rem; font-weight: 600; white-space: nowrap;}
32
+ .tr-dot {width: 0.5rem; height: 0.5rem; border-radius: 50%; background: currentColor;}
33
+ .tr-status-running {color: var(--tr-primary);
34
+ background: color-mix(in srgb, var(--tr-primary) 12%, transparent);}
35
+ .tr-status-running .tr-dot {animation: tr-pulse 1s ease-in-out infinite;}
36
+ .tr-status-done {color: #1a9f53; background: color-mix(in srgb, #1a9f53 12%, transparent);}
37
+ @keyframes tr-pulse {0%, 100% {opacity: 1;} 50% {opacity: 0.25;}}
@@ -0,0 +1,27 @@
1
+ # Valori predefiniti di TokenRace. Modificali liberamente.
2
+
3
+ [params]
4
+ # Valori iniziali dei campi del test (si possono cambiare anche dall'interfaccia)
5
+ temperature = 0.0
6
+ seed = 42
7
+ max_tokens = 512
8
+ repetitions = 3
9
+
10
+ [measure]
11
+ ram_interval_s = 0.5 # ogni quanto campionare la RAM di sistema
12
+ ui_refresh_s = 0.1 # ogni quanto aggiornare risposta e misure durante lo streaming
13
+ connect_timeout_s = 5 # attesa massima per collegarsi al server
14
+ read_timeout_s = 600 # attesa massima per la risposta
15
+ models_timeout_s = 5 # attesa massima per GET /models
16
+
17
+ [long_prompt]
18
+ target_chars = 32000 # lunghezza del contesto dei prompt "Lungo" e "Cache" (≈ 8.000 token)
19
+
20
+ [ui]
21
+ languages = ["en", "it"] # lingue disponibili: un file locales/<codice>.toml per ciascuna
22
+ default_language = "en" # lingua all'apertura (si cambia dal selettore o con ?lang=it)
23
+ fallback_language = "en" # usata per i testi che mancano in una traduzione
24
+ response_height = 430 # altezza in pixel del riquadro della risposta
25
+ # Il colore principale va cambiato in due posti: qui (per lo stile) e in .streamlit/config.toml
26
+ primary_color = "#FF5A1F"
27
+ repo_url = "https://github.com/ernext/tokenrace"