Truly

Technical design

How Truly is built

The system structure, the recommendation algorithm, the two on-device models and how they were trained, the browser runtime, client-side state, and the reasons behind the main design choices. Written for engineers and machine learning practitioners. The interactive figures run the app's own code on the real collection.

1Overview

Truly maps a listener's self-reported mood and energy to a point in valence and arousal space, plans a straight line from that point to a goal chosen by direction, and fills the line with recordings whose published valence, arousal and tempo place them close to it. Two optional models, a face expression model and a voice model, may shift the start point by a small, bounded amount. Everything runs in the browser.

HostingStatic files on GitHub Pages; no server code
ClientPlain ES modules, no build step, no framework
RecommendationGeometry, a cost function and weighted sampling, in music.js
ModelsTwo ONNX models distilled from LAION teachers, run with ONNX Runtime Web (WebAssembly)
Face modelEfficientNet-B0, 4.06 M parameters, 16.2 MB, 224 × 224 input, 40 outputs
Voice modelLog-mel CNN, 420,410 parameters, 2.4 MB, 80,000-sample input, 2 outputs
StateIn memory per visit; optional localStorage, off by default
OfflineService worker caching public files only

Constraints that shape the design

ConstraintConsequence
Static hosting, no backendAll computation in the client; models in ONNX; catalogue as a JSON file
No personal data leaves the deviceNo analytics or third-party requests; recordings self-hosted; inference on device
Non-clinical scopeModels only adjust a start point; no output is shown as a reading of the person; mood answers never stored
Phones as the main targetSmall models, lazy loading, single-threaded WebAssembly, offline support
Small catalogue (18 recordings)Quantile-based placement of answers; candidate pooling; forward re-ordering of paths

2System structure

Components

/Truly/app/

index.htmlapp.css app.jsmusic.jsstore.js face.jsreadout.jsvoice.js models/*.onnxvendor/ort/* sw.jsmanifest.webmanifest

/Truly-music/

tracks.jsonaudio/*.mp3
Two repositories served from one GitHub Pages origin. Teal: core application modules. Orange: machine learning modules and files, loaded only when the camera or voice is used.
ModuleResponsibilityDepends on
app.jsViews, event wiring, session state, player, preferences, historymusic.js, store.js; lazily face.js, voice.js
music.jsPure recommendation functions: placement, goals, planning, cost, selection, swaps, nudges, explanations, quality metricsnone
store.jsThe only module that touches localStoragenone
face.js, readout.jsCamera capture, face detection, face model, readout to a toneONNX Runtime Web
voice.jsMicrophone capture, resampling, loudness normalisation, voice model, readoutONNX Runtime Web
sw.jsCaching for offline usenone

music.js, readout.js and store.js have no DOM dependencies and run unchanged in Node.js, which is how they are tested.

Request flow of one session

#From → toMessage
1Listener → app.jsMood and energy, 1 to 5
2app.js → face.js or voice.jsOptional: readExpression() or readVoice(); the runtime and model are fetched on first use; a tone or {valence, arousal} is returned and kept in memory
3app.js → network or cachetracks.json
4app.js → music.jsstartPoint, nudgeStart, nudgeVoice, goalPoint, pathPoints
5app.js → store.jsFavourites, "not for me" and listening counts, if saved
6app.js → music.jsbuildPath(catalogue, points, prefs) returns the ordered path
7app.js → network or cacheaudio/*.mp3 over range requests
8app.js → store.jssavePath, recordListen, only while saving is on

3Catalogue and notation

tracks.json in the Truly-music repository lists every recording.

FieldTypeMeaning
keystringStable identifier, also the audio file name
title, composer, performerstringCredits
instrumentsstring[]Any of piano, strings, orchestra, winds
audiostringRelative path to a 96 kbps MP3, loudness-normalised
secondsnumberDuration
valence, arousalnumberFrom MCUD 1.0 (Du et al., 2026), averaged when a recording covers several entries
bpmnumber or nullTempo from MCUD 1.0
licence, licence_url, sourcestringLicence and Wikimedia Commons page
mcudnumber[]MCUD 1.0 song identifiers

Notation

A piece s has position (vs, as). The catalogue's valence and arousal have standard deviations σV and σA and empirical quantile functions QV and QA. All distances are in standardised units, so a step of mood and a step of energy weigh the same:

d(x, y) = √( ((vx − vy) / σV)2 + ((ax − ay) / σA)2 )

4Recommendation engine

All functions are in docs/app/music.js and are pure: the same inputs and random source give the same path.

4.1 Start point

Each answer ℓ ∈ {1, …, 5} selects a quantile level qℓ ∈ (0.08, 0.25, 0.50, 0.75, 0.92):

p0 = ( QV(qmood), QA(qenergy) )

Placing answers by quantile of the catalogue, rather than by a fixed mapping to the annotation scale, keeps every answer inside the region where recordings exist.

4.2 Optional nudges

The camera result is a tone τ and moves valence only. The voice result (v̂, â) is in standard deviations from everyday speech (7.6) and passes through c(x) = max(−1, min(1, x / 1.5)):

camera: v0 ← v0 + 0.35 σV · {+1 bright, 0 calm, −1 heavy} voice: v0 ← v0 + 0.12 σV c(v̂),   a0 ← a0 + 0.40 σA c(â)

4.3 Goal

DirectionGoal valenceGoal arousal
calmerv0 + 0.4 (max(v0, QV(0.55)) − v0)min(a0, QA(0.25))
brightermax(v0, QV(0.70))a0 + 0.4 (QA(0.45) − a0)
steadyv0a0

The default direction is steady when mood ≥ 4, calmer when mood ≤ 2 and energy ≥ 4, and brighter otherwise. The listener can always override it.

4.4 Planned points

pi = p0 + in − 1 (pg − p0),   i = 0, …, n − 1

Paths have n = 4, 6 or 9 pieces; "slow down" uses round(1.5 n) and keeps the first piece. The heading h is the unit vector from p0 to pg in standardised units. Starting near the listener's state and moving gradually follows the iso principle (Heiderscheit and Madson, 2015; Starcke and von Georgi, 2024).

Try it: the engine on the real collection

Mood
Energy
Direction
Pieces
Prefer

Path and explanations

    Average step
    Largest step
    Step variation
    Largest tempo change

    4.5 Cost of a candidate

    For candidate s at planned point pi, with previous piece r and Δ the standardised step from r to s:

    C(s) = d(s, pi)2 closeness   + 0.35 ‖Δ‖2 small step   + 1.0 min(0, Δ · h)2 no backward movement   + 0.3 ((bpms − bpmr) / 30)2 gradual tempo   + 0.6 [no preferred instrument] − 0.25 [favourite] − 0.24 λs preferences and listening history

    [·] is 1 when the condition holds, and λs ∈ [−1, 1] is the clipped difference between full plays and early skips, divided by 3 (section 9). Terms involving r are zero for the first piece. cost() returns every term separately; the chart below plots them for the path above.

    Cost terms of the cheapest candidates, per step

    Candidates exclude pieces and performers already earlier in the path. Negative terms (favourite, listening history) are zero in this demonstration.

    4.6 Selection

    buildPath(catalogue, points, keep, prefs, rng):
        path ← keep
        for each planned point p_i not covered by keep:
            candidates ← pieces not in path, performer not in path, not ruled out
            K ← the 8 candidates with the lowest C(s)
            pool ← members of K with rank < 4 and C ≤ C_min + 0.25
            choose from pool with weights 1 / (1 + rank)²    # 1, 1/4, 1/9, 1/16
            append the choice to path
        return path sorted by (v_s/σ_V, a_s/σ_A) · h          # forward order

    Sampling among near-ties varies paths between sessions without choosing poor matches. The final sort guarantees monotone progress along the heading, even when a later point selected a piece slightly behind an earlier one. replaceSong repeats the choice for one index, first restricted to candidates whose projection on h lies between the neighbours' projections, then without that bound, and sorts again.

    4.7 Explanations

    For piece i > 0, the standardised differences from piece i − 1 are compared with ±0.15: arousal gives "calmer" or "livelier", valence "brighter" or "more reflective". The first piece "matches the mood and energy you described"; the last piece of a moving path "is where the path settles". A note is added for a favourite, a matching preferred instrument, or λs ≥ 2/3.

    4.8 Path quality

    MetricDefinition, over the standardised steps ‖Δj‖
    Average stepδ̄ = meanj ‖Δj‖
    Largest stepmaxj ‖Δj‖
    Step variationsdj ‖Δj‖ / δ̄  (0 when all steps are equal)
    Largest tempo changemaxj |bpmj+1 − bpmj|

    These follow the smoothness and evenness criteria used for mood playlists (Donnelly and Gaur, 2022). Over all 25 answer pairs, 3 directions, 3 lengths and 5 seeds (1,125 paths) with the current catalogue:

    MetricMedian90th percentileMaximum
    Average step0.480.801.12
    Largest step0.921.842.01
    Step variation0.680.991.24
    Largest tempo change (bpm)214044

    The tests fail if any path exceeds a largest step of 2.2, an average step of 1.2 or a tempo change of 45 bpm.

    5On-device inference runtime

    RuntimeONNX Runtime Web 1.30.0, WebAssembly backend with SIMD (ort-wasm-simd-threaded.wasm, 14.2 MB)
    Threads1 (ort.env.wasm.numThreads = 1)
    LoadingRuntime and models load through dynamic import() only when the camera or voice is first used
    CachingService worker, cache first after the first download

    GitHub Pages cannot send the Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers needed for SharedArrayBuffer, so multi-threaded WebAssembly is unavailable; one thread is set explicitly to avoid a failed attempt.

    Inference time, median of five runs after one warm-up, on one thread of a 2.2 GHz Xeon in Node.js 20 with the same runtime:

    ModelInputSizeTime per run
    UltraFace RFB-3201 × 3 × 240 × 3201.3 MB377 ms
    Truly face model v11 × 3 × 224 × 22416.2 MB191 ms
    Truly voice model v11 × 80,0002.4 MB132 ms

    6Face pipeline

    6.1 Inference

    1. FrameDrawn from the video into a canvas; never stored.640 × 480
    2. NormaliseResize, then (x − 127) / 128 per channel.1 × 3 × 240 × 320
    3. UltraFace RFB-320Face probabilities and decoded boxes for 4,420 anchors. Best box above 0.7; no NMS needed.
    4. CropSquare of side 1.5 max(w, h) at the box centre; ImageNet mean and std.1 × 3 × 224 × 224
    5. EfficientNet-B0Classifier replaced by 1280 → 40; EmoNet-Face categories in alphabetical order.1 × 40
    6. ReadoutSeven class scores (6.2).7
    7. AggregateUp to 30 attempts 0.4 s apart, at most 10 frames, at least 3; mean, offsets, argmax, tone.

    6.2 Readout

    With model output z ∈ ℝ40 and per-category training statistics μ, σ from readout.json, and Gk the categories mapped to class k in mappings/emonet_categories.json:

    r = z ⊙ σ + μ gk = 1|Gk| Σc ∈ Gk rc   (k ≠ neutral),   gneutral = −maxk ≠ neutral gk k̂ = argmaxk (ḡk + bk)

    Happy and surprise map to the tone bright; angry, disgust, fear and sad to heavy; neutral to calm.

    6.3 Training by distillation

    TeacherEmpathic-Insight-Face-Large: 40 MLP heads (1152 → 1024 → 512 → 256 → 1, ReLU, dropout 0.2) on SigLIP 2 so400m patch16-384 embeddings (1152-d); about 470 M parameters in total
    StudentEfficientNet-B0 with a 40-output head, 4.06 M parameters
    Data79,999 face images: 40,000 FairFace, 20,000 EmoNet-Face Big, 19,999 EmoNet-Face Binary; 76,000 train, 3,999 validation
    TargetsTeacher scores for all 40 categories, standardised per category
    LossSmooth L1 (β = 1)
    OptimiserAdamW, weight decay 0.02; one-cycle learning rate, peak 10−3, 10% warm-up; batch 128; 8 epochs; mixed precision
    AugmentationRandom square crop of 80 to 100%, horizontal flip (0.5), rotation ±10°, greyscale (0.25), downscale to 48, 64 or 96 px and back (0.25), brightness × U(0.8, 1.2) plus U(−0.2, 0.2) on normalised pixels
    ComputeTwo NVIDIA T4 GPUs on Kaggle, 31.8 minutes including evaluation

    No FER-2013 data was used for training or calibration. The same network trained directly on EmoNet-Face Binary expert labels reached 30.7% on FER-2013 PrivateTest; distillation from the teacher's dense 40-dimensional scores reached 51.0%.

    6.4 Calibration

    The offsets b ∈ ℝ7 are fitted by coordinate ascent on CREMA-D video frames from actors with even identifiers, maximising macro F1 over the classes present: four rounds over the classes, each searching a 61-point grid on [−3 sd(g), 3 sd(g)]. Surprise does not occur in CREMA-D and keeps b = 0.

    6.5 Evaluation

    Test setImagesAccuracyMacro F1
    FER-2013 PrivateTest3,58951.0%0.422
    FER+ PrivateTest3,35059.5%0.461
    CREMA-D frames, odd actors7,06636.2%0.376
    Accuracy over seven classes; dashed line: chance, 14.3%.

    On EmoNet-Face HQ (2,500 images, four expert ratings per category) the mean Spearman correlation with expert intensity is 0.458, and the mean AUROC for categories rated 2 or higher is 0.793. Per-class results are in the face model card.

    7Voice pipeline

    7.1 Capture

    getUserMedia is called with echoCancellation: true, noiseSuppression: false and autoGainControl: false, so the signal keeps its natural dynamics. A ScriptProcessorNode with 4,096-frame buffers collects 5 fs samples at the device rate, and the stream stops immediately afterwards. An OfflineAudioContext resamples to 16 kHz and the first 80,000 samples are kept.

    7.2 Loudness normalisation

    Applied identically in training (voice_teacher.py) and in the browser (voice.js). For a signal x split into 400-sample frames with RMS values et:

    L = { t : et > max(10−4, 0.1 maxt et) } γ = 0.05 / √(meant ∈ L et2),   γ ← min(γ, 0.99 / max|x|)

    The output is γx, zero-padded or cut to 80,000 samples. If the loudest frame is below 0.002 the clip is rejected as silence. Normalising over the sounding frames only makes the level independent of pauses, and removes distance to the microphone as a source of variation.

    7.3 Model

    The spectrogram is computed inside the network, so the ONNX graph takes raw samples.

    StageOperationOutput shapeParameters
    Input5 s at 16 kHz1 × 80,000
    DFTConv1d, 402 fixed filters of length 400, stride 1601 × 402 × 498fixed
    Powerre² + im²1 × 201 × 498
    MelFixed 64 × 201 Slaney filter bank, 50 Hz to 8 kHz1 × 64 × 498fixed
    Log, standardiselog(x + 10−6), zero mean and unit variance per clip1 × 1 × 64 × 498
    Block 12 × (3 × 3 conv, batch norm, ReLU), 2 × 2 max pool1 × 24 × 32 × 2495,496
    Block 2same1 × 48 × 16 × 12431,296
    Block 3same1 × 96 × 8 × 62124,800
    Block 4same1 × 128 × 4 × 31258,560
    PoolGlobal average over frequency and time1 × 128
    HeadDropout 0.2, linear1 × 2258
    Total420,410

    The DFT filters, with the periodic Hann window w[n] = 0.5 − 0.5 cos(2πn / 400), are:

    Wk[n] = w[n] cos(2πkn / 400),   W201+k[n] = −w[n] sin(2πkn / 400),   k = 0, …, 200

    Writing the short-time Fourier transform as a strided convolution uses only standard ONNX operators (Conv, Mul, Add, MatMul, Log), which every ONNX Runtime backend supports. Per-clip standardisation of the log-mel image removes any level offset left after 7.2.

    The front end, run in this page

    A JavaScript copy of 7.2 and of the DFT, mel and log stages, applied to five seconds of a recording from the collection. It matches the model's own filter bank to 2 × 10−9 and its log-mel output to 2 × 10−5. No microphone is used; the recording is downloaded only when you press the button.

    7.4 Teacher

    Empathic-Insight-Voice-Small provides one regression head per attribute on BUD-E-Whisper encoder states. The audio is padded to Whisper's 30-second window, giving 1,500 × 768 hidden states; each head flattens them and applies 1,152,000 → 64 → 64 → 32 → 16 → 1 with ReLU. Only the Valence and Arousal heads are downloaded and run; no other attribute is ever computed. With the encoder, the teacher has about 235 M parameters, 560 times the student.

    7.5 Training by distillation

    Windows5 s at 16 kHz after the loudness step; windows with RMS below 0.01 skipped
    Data12,932 windows: 3,592 LAION's Got Talent (expressive acted speech), 6,000 Emolia YODAS (natural English speech), 3,340 CREMA-D actors with even identifiers
    Split12,286 training, 646 validation (random); test: 3,457 CREMA-D windows from actors with odd identifiers
    TargetsTeacher valence and arousal, standardised with the training mean and standard deviation
    LossSmooth L1 (β = 1)
    OptimiserAdamW, weight decay 0.01; one-cycle learning rate, peak 2 × 10−3, 15% warm-up; batch 64; 30 epochs; best validation checkpoint
    AugmentationCircular shift up to ±0.5 s, gain U(0.5, 1.6), Gaussian noise at 15 to 40 dB SNR (probability 0.7), clipping to [−1, 1]
    ComputeOne NVIDIA T4 on Kaggle, 19 minutes

    7.6 Readout

    The output z is in standard deviations of the training targets. The training mix leans towards expressive acted speech, so its mean sits above everyday speech; readout.json re-centres on the teacher's mean m over natural YODAS speech, so 0 means "as lively, or as bright, as ordinary speech":

    (v̂, â) = (z ⊙ σ + μ − m) / σ,   m = (0.162, 1.168), μ = (0.328, 1.247), σ = (0.684, 0.591)

    The engine then applies the bounded nudge of 4.2. Arousal carries most of the weight because the teacher's valence depends largely on lexical content, which Truly never uses.

    7.7 Evaluation

    SetWindowsr, arousalr, valence
    Validation6460.8970.870
    CREMA-D, odd actors3,4570.7870.139

    Pearson correlation with the teacher. CREMA-D actors read the same lexically neutral sentences in every emotion, so the teacher's valence barely varies on it, which explains the low valence correlation there.

    Mean arousal on the teacher scale for CREMA-D test clips, by the emotion listeners heard in the audio alone.

    8Export and verification

    Both students are exported with torch.onnx.export, opset 17, a dynamic batch axis, input names image and audio, and output name scores. The exports are checked in three ways:

    1. For the voice model, PyTorch and ONNX Runtime Web outputs on the same input agree to within 10−4.
    2. The JavaScript front end in 7.3 matches the model's filter bank to 2 × 10−9 and its log-mel output to 2 × 10−5.
    3. tests/models.test.mjs loads every shipped model with the vendored runtime and checks input names, output shapes and that outputs are finite.

    9Client state and storage

    StateWhereLifetime
    Mood and energy answers, camera tone, voice resultsession in app.jsCurrent check-in only; never written anywhere
    Video frames, audio samplesCanvas and typed arraysDiscarded after inference; audio buffers zero-filled
    Preferences, "not for me"Memory, and localStorage when saving is onVisit, or until deleted

    localStorage keys, written only by store.js and only while truly.saving.v1 is true:

    KeySchema
    truly.saving.v1true; absent means off
    truly.paths.v1[{ id, at, direction, pieces: [{ key, title, composer }], played: [key] }], last 50
    truly.favourites.v1[{ key, title, composer, performer }]
    truly.not-for-me.v1[{ key, title, composer, performer }]
    truly.preferences.v1{ instruments: [string], length: "short" | "standard" | "long" }
    truly.listening.v1{ [key]: { full, early, title } }, counts capped at 20

    A play counts as full on the audio ended event, and as early when the listener leaves the piece by choice (next, previous, another piece, or swapping the playing piece) within 30 seconds. The listening term is λs = clip(fulls − earlys, −3, 3) / 3. Turning saving off stops writes; the Privacy view exports all keys as JSON or deletes them.

    10Playback and offline use

    The player is one <audio> element, and recordings are served over HTTP range requests. Its transitions:

    StateEventNext state
    Idleplay(i)Loading
    LoadingplayingPlaying
    LoadingNo data for 15 sStalled (message shown, still waiting)
    PlayingwaitingLoading
    Playing or LoadingerrorRetry: reload once after 2 s at the same position, then the next piece
    PlayingendedNext piece, or Idle after the last
    Retry3 failures in a rowIdle, with a message

    tracks.json is fetched with a 10 s timeout and up to three attempts with exponential back-off. On canplaythrough, the next recording is preloaded in a detached Audio element. The Media Session API exposes metadata and the play, pause, next, previous and seek actions.

    sw.js (scope /Truly/app/) caches only public files:

    RequestStrategyCache
    App files, fonts, tracks.jsonNetwork first; the cached copy after 4 s or on failuretruly-shell-v1
    models/, vendor/Cache firsttruly-models-v1
    *.mp3From the cache if the listener kept it, with range responses built from the cached blob; otherwise the networktruly-offline-music

    While the browser is offline, pieces not in truly-offline-music are excluded from selection, so new paths use only kept recordings.

    11Testing

    node --test tests/ runs in GitHub Actions on every push and pull request.

    FileVerifies
    music.test.mjsPath length; no repeated performer; monotone progress along the heading, including after swaps and slowing down; bounded nudges; preferences shift selection and ruled-out pieces never appear; explanations; catalogue loading retries; quality metrics and limits; valid instrument tags
    store.test.mjsNothing written while saving is off; upserts, caps and deletion; preferences, "not for me" and listening counts
    face.test.mjsReadout groups match the category mapping; neutral scoring; frame averaging; tones
    voice.test.mjsLoudness normalisation level, silence rejection, peak limit and length; centring of the voice readout
    models.test.mjsEvery shipped ONNX model loads in ONNX Runtime Web and has the expected input and output shapes

    Browser behaviour (playback failures, offline use, preferences) was checked in headless Chromium with Playwright.

    12Design decisions

    DecisionAlternatives consideredReason
    Static site, all logic in the clientServer APINo personal data can leave the device if there is nowhere to send it; free hosting
    Published valence and arousal per recordingPredicting them from audioInspectable, citable values; no model error in the catalogue
    Quantile placement of answersLinear mapping to the annotation scaleEvery answer lands where recordings exist, whatever the catalogue's spread
    Straight-line plan, greedy selection, forward sortShortest path or dynamic programming over the catalogueWith fewer than 20 pieces, greedy choice plus re-ordering gives monotone paths in microseconds and is easy to explain
    Distilled studentsTraining on human labels directly; running the teachersDirect training reached 30.7% on FER-2013; the teachers have about 470 M and 235 M parameters and cannot run on phones
    Dense teacher targets (40 face categories)Hard class labelsMore signal per image and a readout that can change without retraining
    DFT as a fixed convolutionThe ONNX STFT operator; features computed in JavaScriptStandard operators run on every backend; one file holds the whole pipeline
    Loudness normalisation in training and inferenceGain augmentation onlyRemoves microphone distance as a variable at its source
    Energy-first voice nudgeEqual weightsThe teacher's valence depends on words, which are never used
    Bounded, additive nudgesLetting a model choose the startThe listener's own answers always lead, as the scope requires
    Single-threaded WebAssemblyThreads, WebGPUGitHub Pages cannot enable cross-origin isolation; WebGPU support on phones is uneven
    Opt-in, per-path offline audioCaching everythingA path is about 20 MB; the listener decides what uses their storage

    13Code map and extension points

    FileContents
    docs/app/app.jsViews, session, player, preferences, history, offline controls
    docs/app/music.jsRecommendation engine (section 4)
    docs/app/store.jsStorage (section 9)
    docs/app/face.js, readout.jsFace pipeline (section 6)
    docs/app/voice.jsVoice pipeline (section 7)
    docs/app/sw.js, manifest.webmanifestOffline use and installation (section 10)
    training/face_teacher.py, face_distill.pyFace teacher scoring, distillation, calibration and export
    training/voice_teacher.py, voice_distill.pyVoice windows and teacher scoring, distillation and export
    music/recordings.json, prepare_audio.pyCatalogue sources and audio preparation
    tests/Section 11

    Replacing a model. Any model that keeps the ONNX contract works without code changes: input image [N, 3, 224, 224] with ImageNet normalisation and output scores [N, 40] in EmoNet-Face category order, or input audio [N, 80000] after the loudness step and output scores [N, 2] (valence, arousal). Ship its readout.json, update models.test.mjs if shapes change, and raise VERSION in sw.js if the file name stays the same.

    Growing the catalogue. Add entries to music/recordings.json with a freely licensed source and published valence, arousal and tempo, run music/prepare_audio.py, and tag the instruments. Quantile placement and the quality tests adapt automatically, and the limits in 4.8 can then be tightened.

    Changing the cost. Every term is in cost(). The path tests and quality limits show whether a change keeps paths gradual and forward.

    The same document, with diagrams, is in references/ARCHITECTURE.md. Full references are in RESEARCH.md, DATASETS.md and RELATED_WORK.md.