Custom Report Section
The interactive browser report is built from sections (tabs). Each section is a Python class that:
- Collects data from the dataset (
compute()) - Provides the JavaScript that renders it (
js_function()) - Optionally adds HTML structure (
panel_html()) or CSS (css())
Add a new tab = write one class. No changes to server.py, builder.py, or template.py.
Base class contract
from plantain2asr import BaseSection
class BaseSection(ABC):
@property
def name(self) -> str: ... # abstract — unique id, e.g. "length"
@property
def title(self) -> str: ... # abstract — tab label, e.g. "Length Stats"
@property
def icon(self) -> str: ... # abstract — emoji icon, e.g. "📏"
def compute(self, dataset) -> dict: ... # abstract — JSON-serializable data
def js_function(self) -> str: ... # abstract — JS string with render_{name}()
def panel_html(self) -> str: ... # optional — inner HTML; default: spinner
def css(self) -> str: ... # optional — section-specific CSS
compute() is called once when the server starts. Its result is served at /api/{name} and
available in JavaScript as S.data['{name}'].
js_function() must define a global function named render_{name}().
The template calls it automatically when the user switches to the tab.
Minimal example: word-count distribution
from plantain2asr import BaseSection
class LengthSection(BaseSection):
@property
def name(self) -> str: return "length"
@property
def title(self) -> str: return "Length Stats"
@property
def icon(self) -> str: return "📏"
def compute(self, dataset) -> dict:
rows = []
for s in dataset:
for model, res in s.asr_results.items():
hyp = res.get("hypothesis", "") or ""
rows.append({
"id": s.id,
"model": model,
"ref_words": len(s.text.split()),
"hyp_words": len(hyp.split()),
})
return {"rows": rows}
def js_function(self) -> str:
return r"""
function render_length() {
const rows = S.data.length.rows.filter(r => r.model === S.activeModel);
const avg_r = rows.reduce((a,b) => a + b.ref_words, 0) / (rows.length || 1);
const avg_h = rows.reduce((a,b) => a + b.hyp_words, 0) / (rows.length || 1);
document.getElementById('length-panel').innerHTML =
'<p>Avg ref words: <b>' + avg_r.toFixed(1) + '</b></p>' +
'<p>Avg hyp words: <b>' + avg_h.toFixed(1) + '</b></p>';
}
"""
Register with ReportServer
from plantain2asr import ReportServer
ReportServer(
norm,
audio_dir="data/golos",
sections=[LengthSection()], # appended after built-in tabs
).serve()
Accessing globals inside JS
Inside js_function() you have access to these globals provided by the base template:
| Global | Type | Description |
|---|---|---|
S.data |
object |
All section data (keyed by section name) |
S.activeModel |
string |
Currently selected model |
esc(s) |
function |
HTML-escapes a string |
fmtNum(v) |
function |
Formats a float to 2 decimal places |
Two-column layout
Override panel_html() when you need custom HTML structure (e.g., a sidebar + main area):
def panel_html(self) -> str:
return """
<div style="display:flex;height:100%;gap:16px">
<div id="length-sidebar" style="width:260px;overflow-y:auto"></div>
<div id="length-main" style="flex:1;overflow-y:auto"></div>
</div>
"""
Adding custom CSS
def css(self) -> str:
return """
#length-panel .stat { font-size:1.4em; font-weight:bold; color:#4caf50; }
"""
Full built-in examples
See the built-in sections for complete reference implementations:
plantain2asr/reporting/sections/metrics.py— metrics table with model filterplantain2asr/reporting/sections/errors.py— error frequency + clickable examplesplantain2asr/reporting/sections/diff.py— word-level diff with audio playback