com.jakesimonds.streetlight.jam.session

jakesimonds.com

Documentation

One saved jam. Immutable once written — there is no edit path, only save and delete.

main record

One saved jam. Immutable once written — there is no edit path, only save and delete.

Record Key any Any valid record key

Properties

audio string Required

Repo-relative path to the recording, always nodes/jam-capture/sessions/<id>/audio.webm. The file is GITIGNORED, so this path can point at nothing on a fresh clone — the page renders such a row with its text intact and no player rather than hiding it.

maxLength: 300 bytes
cheat array of ref #cheatRow Optional

The cheat sheet: each distinct chord name once, in order of first appearance, with how many times it recurred. The shape a person actually wants when reopening a jam — the chord changes, without the timeline.

chords array of ref #chord Optional

Notes struck within 180ms of each other, clustered and named. Derived at STOP by the page, then frozen into the row: re-deriving on read would let a change to the naming table silently rewrite what Jake played last month.

createdAt string datetime Required

When the jam was SAVED, not when recording started — the two differ by however long the name box sat open. ISO-8601. The three migrated rows carry the save-time epoch the old server.mjs stored as a number, converted; nothing else was reconstructible.

maxLength: 64 bytes
durationMs integer Required

How long the recording ran, measured by the page's own clock (performance.now deltas), not read back off the audio file. A jam whose audio failed to flush would still carry a duration here.

minimum: 0
id string Required

The jam's id and its directory name under sessions/: "jam-" + the save time in base 36 + a short random tail, so ids sort chronologically as plain strings. Minted server-side; the browser never picks one.

maxLength: 64 bytes
noteEvents array of ref #noteEvent Optional

Every MIDI note struck during the jam, in the order they were released. Raw capture — `chords` and `cheat` are both derived from this array and nothing else.

title string Required

What Jake typed into the name box when he saved. Free text, and "untitled jam" when he saved without typing — which is what all three of the original 2026-08-12 jams did, so an untitled row is normal and not a defect.

maxLength: 200 bytes
transcript string Optional

The rough lyrics: every final Web Speech segment joined with spaces. Machine transcription of singing, so it is wrong often and interestingly — it is kept as a SEARCH INDEX for finding a jam again ("where was that one about the tractor"), never as a record of what was sung. Empty when the browser had no speech recognition or the mic caught nothing.

transcriptSegs array of ref #seg Optional

The same transcript, still cut into the segments the recognizer emitted, each stamped with its offset into the jam. This is what makes the words seekable against the audio; the joined `transcript` above is the flattening of it.

View raw schema
{
  "key": "any",
  "type": "record",
  "record": {
    "type": "object",
    "required": [
      "id",
      "title",
      "createdAt",
      "durationMs",
      "audio"
    ],
    "properties": {
      "id": {
        "type": "string",
        "maxLength": 64,
        "description": "The jam's id and its directory name under sessions/: \"jam-\" + the save time in base 36 + a short random tail, so ids sort chronologically as plain strings. Minted server-side; the browser never picks one."
      },
      "audio": {
        "type": "string",
        "maxLength": 300,
        "description": "Repo-relative path to the recording, always nodes/jam-capture/sessions/<id>/audio.webm. The file is GITIGNORED, so this path can point at nothing on a fresh clone — the page renders such a row with its text intact and no player rather than hiding it."
      },
      "cheat": {
        "type": "array",
        "items": {
          "ref": "#cheatRow",
          "type": "ref"
        },
        "description": "The cheat sheet: each distinct chord name once, in order of first appearance, with how many times it recurred. The shape a person actually wants when reopening a jam — the chord changes, without the timeline."
      },
      "title": {
        "type": "string",
        "maxLength": 200,
        "description": "What Jake typed into the name box when he saved. Free text, and \"untitled jam\" when he saved without typing — which is what all three of the original 2026-08-12 jams did, so an untitled row is normal and not a defect."
      },
      "chords": {
        "type": "array",
        "items": {
          "ref": "#chord",
          "type": "ref"
        },
        "description": "Notes struck within 180ms of each other, clustered and named. Derived at STOP by the page, then frozen into the row: re-deriving on read would let a change to the naming table silently rewrite what Jake played last month."
      },
      "createdAt": {
        "type": "string",
        "format": "datetime",
        "maxLength": 64,
        "description": "When the jam was SAVED, not when recording started — the two differ by however long the name box sat open. ISO-8601. The three migrated rows carry the save-time epoch the old server.mjs stored as a number, converted; nothing else was reconstructible."
      },
      "durationMs": {
        "type": "integer",
        "minimum": 0,
        "description": "How long the recording ran, measured by the page's own clock (performance.now deltas), not read back off the audio file. A jam whose audio failed to flush would still carry a duration here."
      },
      "noteEvents": {
        "type": "array",
        "items": {
          "ref": "#noteEvent",
          "type": "ref"
        },
        "description": "Every MIDI note struck during the jam, in the order they were released. Raw capture — `chords` and `cheat` are both derived from this array and nothing else."
      },
      "transcript": {
        "type": "string",
        "description": "The rough lyrics: every final Web Speech segment joined with spaces. Machine transcription of singing, so it is wrong often and interestingly — it is kept as a SEARCH INDEX for finding a jam again (\"where was that one about the tractor\"), never as a record of what was sung. Empty when the browser had no speech recognition or the mic caught nothing."
      },
      "transcriptSegs": {
        "type": "array",
        "items": {
          "ref": "#seg",
          "type": "ref"
        },
        "description": "The same transcript, still cut into the segments the recognizer emitted, each stamped with its offset into the jam. This is what makes the words seekable against the audio; the joined `transcript` above is the flattening of it."
      }
    }
  },
  "description": "One saved jam. Immutable once written — there is no edit path, only save and delete."
}
cheatRow object

One distinct chord in the jam, counted.

Properties

count integer Required

How many clusters in the jam carried this name.

minimum: 1
firstT integer Required

Milliseconds to its first appearance — the sort key for the sheet, so the list reads in playing order rather than by frequency.

name string Required

The chord name, matching a `chords[].name`.

maxLength: 64 bytes
View raw schema
{
  "type": "object",
  "required": [
    "name",
    "count",
    "firstT"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 64,
      "description": "The chord name, matching a `chords[].name`."
    },
    "count": {
      "type": "integer",
      "minimum": 1,
      "description": "How many clusters in the jam carried this name."
    },
    "firstT": {
      "type": "integer",
      "description": "Milliseconds to its first appearance — the sort key for the sheet, so the list reads in playing order rather than by frequency."
    }
  },
  "description": "One distinct chord in the jam, counted."
}
chord object

A cluster of notes struck together, with a best-effort name.

Properties

name string Required

The named chord — "F", "Gm7", "Bm/D". When no template matches, this falls back to the bare pitch classes joined by spaces ("C E F G"), which is the honest answer for a cluster that is not a chord; those appear in real rows and are not errors.

maxLength: 64 bytes
notes array of integer Required

The MIDI note numbers in the cluster, as struck — inversions preserved, so the voicing survives even though the name flattens it.

t integer Required

Milliseconds from the start of the recording to the first note of the cluster.

View raw schema
{
  "type": "object",
  "required": [
    "t",
    "name",
    "notes"
  ],
  "properties": {
    "t": {
      "type": "integer",
      "description": "Milliseconds from the start of the recording to the first note of the cluster."
    },
    "name": {
      "type": "string",
      "maxLength": 64,
      "description": "The named chord — \"F\", \"Gm7\", \"Bm/D\". When no template matches, this falls back to the bare pitch classes joined by spaces (\"C E F G\"), which is the honest answer for a cluster that is not a chord; those appear in real rows and are not errors."
    },
    "notes": {
      "type": "array",
      "items": {
        "type": "integer"
      },
      "description": "The MIDI note numbers in the cluster, as struck — inversions preserved, so the voicing survives even though the name flattens it."
    }
  },
  "description": "A cluster of notes struck together, with a best-effort name."
}
noteEvent object

One key, from press to release.

Properties

note integer Required

MIDI note number, 0-127. Middle C is 60.

minimum: 0maximum: 127
off integer Required

Milliseconds from the start of the recording to key-up, rounded.

on integer Required

Milliseconds from the start of the recording to key-down, rounded. Notes still held when STOP is pressed are flushed with `off` set to the stop time, so a held final chord is captured rather than dropped.

View raw schema
{
  "type": "object",
  "required": [
    "note",
    "on",
    "off"
  ],
  "properties": {
    "on": {
      "type": "integer",
      "description": "Milliseconds from the start of the recording to key-down, rounded. Notes still held when STOP is pressed are flushed with `off` set to the stop time, so a held final chord is captured rather than dropped."
    },
    "off": {
      "type": "integer",
      "description": "Milliseconds from the start of the recording to key-up, rounded."
    },
    "note": {
      "type": "integer",
      "maximum": 127,
      "minimum": 0,
      "description": "MIDI note number, 0-127. Middle C is 60."
    }
  },
  "description": "One key, from press to release."
}
seg object

One finalized run of recognized speech.

Properties

t integer Required

Milliseconds from the start of the recording to when the segment was finalized — which trails the words themselves by however long the recognizer took to commit them.

text string Required

The recognized words, trimmed. Interim results are never stored; only what the recognizer marked final.

View raw schema
{
  "type": "object",
  "required": [
    "t",
    "text"
  ],
  "properties": {
    "t": {
      "type": "integer",
      "description": "Milliseconds from the start of the recording to when the segment was finalized — which trails the words themselves by however long the recognizer took to commit them."
    },
    "text": {
      "type": "string",
      "description": "The recognized words, trimmed. Interim results are never stored; only what the recognizer marked final."
    }
  },
  "description": "One finalized run of recognized speech."
}

Lexicon Garden

@