com.jakesimonds.streetlight.jam.session

jakesimonds.com

{
  "id": "com.jakesimonds.streetlight.jam.session",
  "defs": {
    "seg": {
      "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."
    },
    "main": {
      "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."
    },
    "chord": {
      "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."
    },
    "cheatRow": {
      "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."
    },
    "noteEvent": {
      "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."
    }
  },
  "$type": "com.atproto.lexicon.schema",
  "lexicon": 1,
  "description": "One jam captured at the piano: the room audio, the MIDI Jake actually played, and a rough machine transcript of whatever he sang over it. Written by the viewer's POST /api/jam-session (local-viewer/server.mjs) through mintRecord, one row appended per save to nodes/jam-capture/sessions.jsonl, read back by GET /api/jam-sessions and rendered by the jam-capture node page. THE SPLIT BETWEEN ROW AND BLOB: everything in this record is text and numbers and lives in git; the audio itself is a webm blob under nodes/jam-capture/sessions/<id>/audio.webm, which is gitignored and referenced by the `audio` path field. That split is the whole reason this store is browsable — before 2026-08-15 a jam was a per-session session.json sitting beside its audio inside an ignored directory, carrying no $type, so the lexicon scan read three separate orphan stores and could show a count and nothing else. SCALING CAVEAT, stated rather than discovered: `noteEvents` is the raw MIDI stream and runs ~1000 entries for a four-minute jam, so a row is tens of KB and a browse page fetches them 60 at a time. At a few dozen jams that is fine; if the library ever reaches hundreds, the fix is to move noteEvents beside the audio as a sibling blob and leave the derived `chords`/`cheat` in the row, NOT to start truncating a record that says it holds everything. Kept whole for now because chords and cheat are both derived from noteEvents and dropping it would make them unre-derivable."
}

Validate Record

Validate a record against com.jakesimonds.streetlight.jam.session

Validation Options
Treat any remaining unresolved references as valid

Metadata

DID
did:plc:aurnkk6uy6axy66uqaq6dqy6
CID
bafyreicqtxr7ov64p7mmovh7lhb2ltebkffuu76m4s6wzjtbvscf5u52ee
Indexed At
2026-08-16 04:29 UTC
AT-URI
at://did:plc:aurnkk6uy6axy66uqaq6dqy6/com.atproto.lexicon.schema/com.jakesimonds.streetlight.jam.session

Lexicon Garden

@