Files
uj-mastering-master/static/app.css
T
irene 5df802a574 Add web UI: same backend, browser front end
Reuses the analysis backend unchanged. metrics.py already split compute()
from build_spec(), and plotspec.PlotSpec is backend-agnostic, so the web UI
reimplements only the renderer: uPlot for curves/bands, a canvas for the
spectrogram heatmap. No PyQt5, pyqtgraph or matplotlib on the web path.

Uploaded audio is never persisted on the host. The browser copies each file
into Origin Private File System (sandboxed browser storage) and streams the
bytes to the server per request; the server spools each body to a temp file,
decodes, computes, and unlinks it in a finally before responding. There is no
upload directory, no cache of audio, and no session state keyed on a path:
file_id is the SHA-256 of the audio, so re-sending a master for another metric
reuses the first decode. Verified: a spool is observable mid-request and gone
after the response.

Being precise: the bytes do have to reach the server, since librosa decodes
from a path and the analysis is CPU-bound numpy/scipy. The claim is that
running this does not accumulate masters on the host, not that audio is never
on the machine mid-request.

New:
  metrics_json.py       PlotSpec/metric data -> JSON (the renderer seam)
  web_session.py        headless analysis session, content-addressed tracks
  webapp.py             FastAPI routes, raw-body analysis, ujm-web entry point
  static/               browser front end + test suite
  scripts/webapi_smoke.py  61 end-to-end assertions against a running server

Also fixes a bug the direct-uvicorn dev loop had hidden: the CLI advertises
logger_setup's WARN level but passed it straight to uvicorn, which only knows
'warning' -- `ujm-web --log-level WARN` died with KeyError: 'warn'.

Removes an earlier /api/collect debug route that served every source module
over HTTP.

pyproject: 'web' extra (fastapi, uvicorn, python-multipart), package-data for
static/, Apache-2.0 licence.
2026-09-14 01:44:48 +09:00

271 lines
7.5 KiB
CSS

/* Audio Mastering Analysis Toolkit — web front end.
Dark by default: a spectrogram is read against its surroundings, and the
desktop app's pythonic defaults are what these numbers were tuned against. */
:root {
--bg: #12141a;
--panel: #191c24;
--panel-2: #1f232d;
--line: #2b303c;
--text: #e8eaf0;
--muted: #939aa8;
--accent: #4fc3f7;
--accent-dim: #2b7fa3;
--warn: #ffb74d;
--error: #ef5350;
--ok: #81c784;
--radius: 6px;
--mono: ui-monospace, "Cascadia Mono", "Source Code Pro", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
height: 100%;
background: var(--bg);
color: var(--text);
font: 13px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", sans-serif;
overflow: hidden;
}
/* ---- top bar ---- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
height: 48px;
padding: 0 14px;
background: var(--panel);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: baseline; gap: 8px; }
.logo { color: var(--accent); font-size: 16px; }
.brand-name { font-weight: 600; letter-spacing: .2px; }
.brand-sub {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--bg);
background: var(--accent-dim);
border-radius: 3px;
padding: 1px 5px;
}
.topbar-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.field { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; }
select, .btn, .toggle {
background: var(--panel-2);
color: var(--text);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 5px 9px;
font: inherit;
font-size: 12px;
cursor: pointer;
}
select:hover, .btn:hover, .toggle:hover { border-color: var(--accent-dim); }
select:focus-visible, .btn:focus-visible, .toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.btn.primary { background: var(--accent-dim); border-color: var(--accent); color: #eaf7ff; }
.btn.primary:hover { background: var(--accent); color: #06202b; }
.btn.small { padding: 3px 7px; font-size: 11px; }
.btn:disabled, .toggle:disabled { opacity: .45; cursor: not-allowed; }
.toggle.on { border-color: var(--accent); color: var(--accent); }
.toggle.off { color: var(--muted); }
.separator { width: 1px; height: 22px; background: var(--line); }
/* ---- layout ---- */
.layout {
display: grid;
grid-template-columns: 288px minmax(0, 1fr);
height: calc(100% - 48px);
}
.sidebar {
background: var(--panel);
border-right: 1px solid var(--line);
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.panel {
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 10px;
}
.panel h2 {
margin: 0 0 8px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .8px;
color: var(--muted);
display: flex;
align-items: center;
gap: 6px;
}
.badge {
background: var(--line);
color: var(--text);
border-radius: 8px;
padding: 0 6px;
font-size: 10px;
font-variant-numeric: tabular-nums;
}
/* ---- file list ---- */
.file-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.file-row {
display: flex;
align-items: center;
gap: 7px;
padding: 5px 6px;
border-radius: 4px;
cursor: pointer;
border: 1px solid transparent;
}
.file-row:hover { background: #252a35; }
.file-row.selected { background: #232a38; border-color: var(--accent-dim); }
.file-row input[type=checkbox] { margin: 0; accent-color: var(--accent-dim); flex: none; }
.file-swatch { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.file-name {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
}
.file-remove {
flex: none;
border: 0;
background: none;
color: var(--muted);
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 0 2px;
opacity: 0;
}
.file-row:hover .file-remove { opacity: 1; }
.file-remove:hover { color: var(--error); }
.empty, .hint { color: var(--muted); font-size: 11.5px; margin: 4px 0 0; }
.hint code { background: #000; padding: 0 3px; border-radius: 3px; }
/* ---- metadata ---- */
.metadata { display: grid; grid-template-columns: auto 1fr; gap: 3px 10px; margin: 0; font-size: 12px; }
.metadata dt { color: var(--muted); }
.metadata dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.metadata dd.muted { color: var(--muted); }
/* ---- reference lines ---- */
.ref-list { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.ref-row { display: flex; align-items: center; gap: 6px; font-size: 12px; }
.ref-row input[type=color] { width: 20px; height: 18px; padding: 0; border: 1px solid var(--line); background: none; }
.ref-row input[type=text] { width: 62px; }
.ref-row input[type=number] { width: 62px; }
.ref-row input { background: var(--bg); color: var(--text); border: 1px solid var(--line); border-radius: 4px; padding: 2px 4px; font: inherit; font-size: 11px; }
.ref-row .ref-remove { border: 0; background: none; color: var(--muted); cursor: pointer; }
.ref-row .ref-remove:hover { color: var(--error); }
.ref-actions { display: flex; gap: 6px; }
/* ---- plot stage ---- */
.stage { display: flex; flex-direction: column; min-width: 0; padding: 10px 12px 0; }
.plot-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 24px; }
.plot-head h1 { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
.legend { display: flex; flex-wrap: wrap; gap: 10px; font-size: 11.5px; color: var(--muted); }
.legend .item { display: flex; align-items: center; gap: 5px; }
.legend .swatch { width: 9px; height: 9px; border-radius: 2px; }
.legend .annotation { font-family: var(--mono); color: var(--text); }
.plot-host {
position: relative;
flex: 1;
min-height: 200px;
margin-top: 6px;
background: #0d0f14;
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
}
.plot-host .uplot { position: absolute; inset: 0; width: 100% !important; height: 100% !important; }
.plot-host canvas.heatmap { position: absolute; inset: 0; width: 100%; height: 100%; cursor: crosshair; }
.placeholder {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--muted);
gap: 6px;
padding: 20px;
}
.ph-title { font-size: 15px; color: var(--text); margin: 0; }
.ph-body { margin: 0; max-width: 44ch; font-size: 12px; }
.ph-body code { background: #000; padding: 0 3px; border-radius: 3px; }
.statusbar {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 2px;
font-size: 11.5px;
color: var(--muted);
min-height: 24px;
}
.status.working { color: var(--accent); }
.status.error { color: var(--error); }
.status.warn { color: var(--warn); }
.timing { font-family: var(--mono); }
/* ---- drag & drop ---- */
.drop-overlay {
position: fixed;
inset: 0;
background: rgba(10, 20, 30, .82);
border: 3px dashed var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: var(--accent);
z-index: 40;
}
/* ---- tooltip ---- */
.tooltip {
position: fixed;
background: #05070b;
border: 1px solid var(--line);
border-radius: 4px;
padding: 5px 7px;
font: 11px/1.4 var(--mono);
color: var(--text);
pointer-events: none;
z-index: 30;
white-space: nowrap;
}