Jindrich3 commited on
Commit
42b5eae
·
verified ·
1 Parent(s): 8cbbafe

Code-only deploy

Browse files
Files changed (2) hide show
  1. static/stats.html +30 -9
  2. stt_quality.py +22 -3
static/stats.html CHANGED
@@ -285,19 +285,40 @@ function qualityPanel(q) {
285
  if (!q?.visits) return '<h2>Transcription quality</h2><div class="panel"><p class="empty">No visit reports yet.</p></div>';
286
  return `<h2>Transcription quality – All</h2>${qualityGroup(q, 'All visits')}
287
  <h2>By operating system</h2>${q.by_os.map(g => qualityGroup(g, g.name)).join('')}
288
- <h2>By visitor (anyone with a visit long enough to judge)</h2>${q.by_visitor.map(g => qualityGroup(g, `${g.name} · ${g.device}`)).join('')}`;
289
  }
290
  // Closed: verdict counts, then each problem's share of the group's /t time. Open: the group's full numbers.
291
- function qualityGroup(g, name) {
 
 
 
292
  const short = g.verdicts.too_short || 0, judged = `${g.visits - short} judged, ${short} under ${MIN_ACTIVE_S} s`;
293
  const verdicts = Object.entries(g.verdicts).filter(([k]) => k !== 'too_short')
294
- .map(([k, n]) => ` · ${VERDICT[k]?.split(':')[0] || k} ${n}`).join('');
295
- const shares = Object.entries(g.problem_secs).map(([c, s]) =>
296
- `${c} ${g.total_s ? Math.round(100 * s / g.total_s) : 0}%`).join(' · ');
297
  return `<details class="group"><summary><b>${esc(name)}</b> · ${plural(g.visits, 'visit')} (${judged})${verdicts}
298
- <br><span class="sub">${shares || 'no problem shown'}</span>${verdictBar(g.verdicts, 'thin')}</summary>${qualityBody(g)}</details>`;
299
  }
300
- function qualityBody(q) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  const v = q.verdicts, judged = q.visits - (v.too_short || 0);
302
  const pct = n => judged ? ` (${Math.round(100 * (n || 0) / judged)}%)` : '';
303
  const share = s => mins(s) + (q.total_s ? ` (${Math.round(100 * s / q.total_s)}%)` : '');
@@ -311,10 +332,10 @@ function qualityBody(q) {
311
  ];
312
  // A chart with nothing in it is only named, on one line under the rest.
313
  const empty = charts.filter(([, e]) => !e.some(([, n]) => n)).map(([title]) => title.split(' (')[0]);
314
- return `<div class="panel">${verdictBar(v, 'big')}
315
  <div class="legend verdicts">${JUDGED.map(k =>
316
  `<span><i style="background:${VERDICT_COLOR[k]}"></i>${v[k] || 0} ${VERDICT[k]}${pct(v[k])}</span>`).join('')}</div>
317
- <p class="sub">${v.too_short || 0} too short to judge · ${mins(q.missed_s)} speech missed (drops, lost audio)</p></div>
318
  <div class="grid2">${charts.filter(([, e]) => e.some(([, n]) => n)).map(([title, e]) =>
319
  `<div><h2>${title}</h2><div class="panel">${bars(e)}</div></div>`).join('')}</div>
320
  ${empty.length ? `<p class="sub" style="margin-top:12px">Nothing to show: ${empty.join(' · ')}.</p>` : ''}`;
 
285
  if (!q?.visits) return '<h2>Transcription quality</h2><div class="panel"><p class="empty">No visit reports yet.</p></div>';
286
  return `<h2>Transcription quality – All</h2>${qualityGroup(q, 'All visits')}
287
  <h2>By operating system</h2>${q.by_os.map(g => qualityGroup(g, g.name)).join('')}
288
+ <h2>By visitor (anyone with a visit long enough to judge)</h2>${q.by_visitor.map(g => qualityGroup(g, `${g.name} · ${g.device}`, sessionList(g.sessions))).join('')}`;
289
  }
290
  // Closed: verdict counts, then each problem's share of the group's /t time. Open: the group's full numbers.
291
+ const shortVerdict = k => VERDICT[k]?.split(':')[0] || k;
292
+ const shareLine = g => Object.entries(g.problem_secs).map(([c, s]) =>
293
+ `${c} ${g.total_s ? Math.round(100 * s / g.total_s) : 0}%`).join(' · ') || 'no problem shown';
294
+ function qualityGroup(g, name, more = '') {
295
  const short = g.verdicts.too_short || 0, judged = `${g.visits - short} judged, ${short} under ${MIN_ACTIVE_S} s`;
296
  const verdicts = Object.entries(g.verdicts).filter(([k]) => k !== 'too_short')
297
+ .map(([k, n]) => ` · ${shortVerdict(k)} ${n}`).join('');
 
 
298
  return `<details class="group"><summary><b>${esc(name)}</b> · ${plural(g.visits, 'visit')} (${judged})${verdicts}
299
+ <br><span class="sub">${shareLine(g)}</span>${verdictBar(g.verdicts, 'thin')}</summary>${more}${qualityBody(g)}</details>`;
300
  }
301
+ // A visit's clock time in Prague, as much of the date as the period needs:
302
+ // "9:20–9:30" today, "Tue 9:20–9:30" over 7 days, "Tue 27.10. 9:20–9:30" beyond.
303
+ const PRAGUE = new Intl.DateTimeFormat('en-GB', {timeZone: 'Europe/Prague', weekday: 'short',
304
+ day: 'numeric', month: 'numeric', hour: 'numeric', minute: '2-digit', hourCycle: 'h23'});
305
+ function span(s) {
306
+ if (!s.start) return 'Time unknown';
307
+ const at = iso => Object.fromEntries(PRAGUE.formatToParts(new Date(iso)).map(p => [p.type, p.value]));
308
+ const a = at(s.start), b = at(s.end || s.start), hm = p => `${+p.hour}:${p.minute}`;
309
+ const day = days === '1' ? '' : `${a.weekday} ${days === '7' ? '' : `${+a.day}.${+a.month}. `}`;
310
+ return `${day}${hm(a)}–${hm(b)}`;
311
+ }
312
+ // First thing in an open visitor, right under its summary line.
313
+ const sessionList = list => list?.length ? `<h2 style="margin-top:12px">Sessions, newest first</h2>${list.map(sessionGroup).join('')}
314
+ <h2>All sessions of this visitor</h2>` : '';
315
+ // One visit: when, its verdict, its on-screen time. Open: its own numbers.
316
+ function sessionGroup(s) {
317
+ const [k] = Object.keys(s.verdicts);
318
+ return `<details class="group"><summary><b>${span(s)}</b> · <b style="color:${VERDICT_COLOR[k] || 'var(--muted)'}">${
319
+ esc(shortVerdict(k))}</b> · ${mins(s.total_s)}<br><span class="sub">${shareLine(s)}</span></summary>${qualityBody(s, true)}</details>`;
320
+ }
321
+ function qualityBody(q, single = false) {
322
  const v = q.verdicts, judged = q.visits - (v.too_short || 0);
323
  const pct = n => judged ? ` (${Math.round(100 * (n || 0) / judged)}%)` : '';
324
  const share = s => mins(s) + (q.total_s ? ` (${Math.round(100 * s / q.total_s)}%)` : '');
 
332
  ];
333
  // A chart with nothing in it is only named, on one line under the rest.
334
  const empty = charts.filter(([, e]) => !e.some(([, n]) => n)).map(([title]) => title.split(' (')[0]);
335
+ return `${single ? `<p class="sub">${mins(q.missed_s)} speech missed (drops, lost audio)</p>` : `<div class="panel">${verdictBar(v, 'big')}
336
  <div class="legend verdicts">${JUDGED.map(k =>
337
  `<span><i style="background:${VERDICT_COLOR[k]}"></i>${v[k] || 0} ${VERDICT[k]}${pct(v[k])}</span>`).join('')}</div>
338
+ <p class="sub">${v.too_short || 0} too short to judge · ${mins(q.missed_s)} speech missed (drops, lost audio)</p></div>`}
339
  <div class="grid2">${charts.filter(([, e]) => e.some(([, n]) => n)).map(([title, e]) =>
340
  `<div><h2>${title}</h2><div class="panel">${bars(e)}</div></div>`).join('')}</div>
341
  ${empty.length ? `<p class="sub" style="margin-top:12px">Nothing to show: ${empty.join(' · ')}.</p>` : ''}`;
stt_quality.py CHANGED
@@ -170,11 +170,27 @@ def summarize(visits: list[dict]) -> dict:
170
  }
171
 
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  def breakdown(visits: list[dict], numbers: dict[str, int]) -> dict:
174
  """summarize for all visits, plus "by_os" and "by_visitor": the same per
175
  group. OSes: most /t time first. Visitors: those with a judged (not
176
- too_short) visit, most visits first. `numbers` maps a visits log id to its
177
- "Visitor N" on the Visitors tab."""
 
178
  # Reports from before "vid" was logged borrow it from a later one of the same client.
179
  vid_of = {v["client"]: v["vid"] for v in visits if v.get("vid")}
180
  by_os: dict[str, list[dict]] = defaultdict(list)
@@ -186,7 +202,10 @@ def breakdown(visits: list[dict], numbers: dict[str, int]) -> dict:
186
  oses = [{"name": name, **summarize(group)} for name, group in by_os.items()]
187
  people = [{"name": f"Visitor {numbers[key]}" if key in numbers else f"id {key[:6]}",
188
  "device": f"{group[-1].get('os', '')} {group[-1].get('browser', '')}".strip(),
189
- **summarize(group)} for key, group in by_who.items()]
 
 
 
190
  oses.sort(key=lambda s: -s["total_s"])
191
  people = sorted((p for p in people if p["visits"] > p["verdicts"].get("too_short", 0)),
192
  key=lambda s: (-s["visits"], -s["total_s"]))
 
170
  }
171
 
172
 
173
+ def times(visit: dict) -> tuple[str, str]:
174
+ """(start, end) of a visit, ISO UTC, "" when unknown. The page ends the sid
175
+ with its start time (Date.now() in base 36); the end is the last report."""
176
+ end = visit.get("ts", "")
177
+ try:
178
+ start = datetime.fromtimestamp(int(visit["sid"][-8:], 36) / 1000, timezone.utc)
179
+ except (KeyError, ValueError, OverflowError, OSError):
180
+ return "", end
181
+ if start.year < 2025: # not a time: a sid from before it carried one
182
+ return "", end
183
+ start_iso = start.isoformat(timespec="seconds")
184
+ # the page's clock, not ours: never after the report that closed the visit
185
+ return (min(start_iso, end) if end else start_iso), end
186
+
187
+
188
  def breakdown(visits: list[dict], numbers: dict[str, int]) -> dict:
189
  """summarize for all visits, plus "by_os" and "by_visitor": the same per
190
  group. OSes: most /t time first. Visitors: those with a judged (not
191
+ too_short) visit, most visits first, each with its "sessions" (one per
192
+ visit, newest first, with "start" and "end"). `numbers` maps a visits log
193
+ id to its "Visitor N" on the Visitors tab."""
194
  # Reports from before "vid" was logged borrow it from a later one of the same client.
195
  vid_of = {v["client"]: v["vid"] for v in visits if v.get("vid")}
196
  by_os: dict[str, list[dict]] = defaultdict(list)
 
202
  oses = [{"name": name, **summarize(group)} for name, group in by_os.items()]
203
  people = [{"name": f"Visitor {numbers[key]}" if key in numbers else f"id {key[:6]}",
204
  "device": f"{group[-1].get('os', '')} {group[-1].get('browser', '')}".strip(),
205
+ **summarize(group),
206
+ "sessions": [dict(zip(("start", "end"), times(v))) | summarize([v])
207
+ for v in reversed(group)]}
208
+ for key, group in by_who.items()]
209
  oses.sort(key=lambda s: -s["total_s"])
210
  people = sorted((p for p in people if p["visits"] > p["verdicts"].get("too_short", 0)),
211
  key=lambda s: (-s["visits"], -s["total_s"]))