Canonical schema of record for the ActiveInferenceJournal repository: uniform, flat,
machine-readable item folders with a canonical metadata.json, no placeholder cruft,
audio kept off main, and top-level indexes. The repository was migrated to this shape
non-destructively by the Journal-Utilities refactor (see "Layout history").
<Series>/<Series>_<NNN[.E]>/
metadata.json # canonical record (see below) — single source of truth
README.md # generated human nav: title(s), date, links, contents
transcript.txt # derived text view ("## <video_id>" headers when multi-part)
transcript.json # raw timestamped segments (array; part/session-tagged) — see "Transcripts"
captions/ # original-language *.srt
translations/ # translated *.srt (one per language) — preserved verbatim
assets/
images/ html/ prose/ appendices/ bibliography/ csv/ # curated content, by type;
# csv/ = legacy AssemblyAI sentence exports
# audio/ is NOT on main (gitignored). The `audio` branch carries audio/<video_id>.64k.m4a
{
"series": "GuestStream",
"item": "GuestStream_094",
"title": "ActInf GuestStream 094 ~ ...",
"source": "youtube",
"channel": "ActiveInferenceInstitute",
"parts": [
{"part": "094.1", "video_id": "1Qr7um2W-Rc",
"url": "https://www.youtube.com/watch?v=1Qr7um2W-Rc",
"title": "...", "published": "2026-..", "duration": 0,
"has_transcript": true, "has_captions": true, "has_audio": true}
]
}Note: the example above shows aspirational per-part flags (published,
has_transcript, …). Files currently on main carry the core keys
series, item, source, channel, category, episode and parts[] entries with
video_id, url, title (plus duration, upload_date where known).
transcript.json is the immutable raw layer; transcript.txt is a
derived view. Human speaker names live only in metadata.json.
transcript.json— raw diarization output: an array of{"video_id", "segments"}blocks, each segment{start, end, text, speaker}in seconds. Speaker labels stay machine labels (SPEAKER_NN) forever — never rewritten with human names, so re-mapping is always possible (two labels identified as the same person remain distinguishable). Legacy AssemblyAI items carry that tool's response verbatim inside the block instead; treat those as frozen.parts[].speakers(metadata.json, journal-owned) — the only place human speaker identifications are recorded:{"SPEAKER_NN": "Name", ...}per video.transcript.txt— canonical human-facing text, regenerated fromtranscript.json+parts[].speakers; unmapped labels remainSPEAKER_NN. Multi-part items use## <video_id>headers; split sessions## <video_id>_sessNN. Items without diarization keep their YouTube-caption text until WhisperX runs; original captions always remain undercaptions/.
Provenance is encoded by file shape — no metadata flag:
| Files present | Meaning |
|---|---|
transcript.json with speaker fields in segments |
WhisperX-diarized |
parts[].speakers non-empty |
names mapped by a human |
assets/csv/*.sentences.csv (± AssemblyAI-shaped transcript.json) |
legacy AssemblyAI diarization |
transcript.txt only |
YouTube captions (not yet diarized) |
Items are enriched from the Institute's Coda table (now Superhuman Docs), the
session split-files, and the channel manifest by
Journal-Utilities/scripts/enrich_metadata.py. Regenerate with (run from the
Journal-Utilities checkout; enrichment is dry-run unless --apply is passed):
python scripts/enrich_metadata.py --journal ../ActiveInferenceJournal
python scripts/enrich_metadata.py --journal ../ActiveInferenceJournal --apply
python scripts/generate_journal_indexes.py --journal ../ActiveInferenceJournal
python scripts/validate_journal.py --journal ../ActiveInferenceJournal \
--manifest data/output/channel_videos.jsonEnrichment-owned keys — the script only ever sets these; it never deletes or rewrites other keys, empty values are omitted, and re-running is idempotent. Invalid values in enrichment-owned URL fields are normalized into their corresponding display fields without losing the source text:
| Field | Type | Source | Level |
|---|---|---|---|
title |
string | Coda "<Unique event name> ~ <Title or name of stream>" |
item |
date |
ISO date | Coda "Date" | item (or per-part when parts differ) |
guests |
string[] | Coda "Guests" | item (multi-row lists union) |
other_participants |
string[] | Coda "Other Participants" | item (union) |
description |
string | split-file session description (shared text) | item |
github |
url | generated: canonical link to the item's folder in this repo | item |
slides_url |
url | Coda "Slides" / legacy session DB; real http(s) URL only |
item / per-part |
slides_label |
string | Coda "Slides" display text when no URL is available | item / per-part |
paper_link |
url | Coda "Paper link" when it is a real http(s) URL |
item / per-part |
paper_title |
string | Coda "Paper link" display text when no URL is available | item / per-part |
doi |
string | Coda "DOI" | item / per-part |
zenodo |
url | Coda "Zenodo Link" | item / per-part |
keywords |
string[] | Coda "Keywords" | item (union) |
thumbnails |
object | Coda "Thumbnail Image" / "Cover image" | item |
summaries |
object | Coda summaries: human, ai, word_300, abstract |
item |
enriched_from |
string[] | provenance: coda, split_file, youtube, db (legacy session DB), generated, curated (human-verified correction) |
item |
sessions |
object[] | split-files (multi-talk videos) | item |
duplicate_of |
string | item path this uncategorized item duplicates | item |
titlevsparts[].title: the item-leveltitleis the Coda event title;parts[].titlestays the verbatim YouTube video title. They differ by design — do not "fix" one to match the other.sessions[](talks within one long video):{index, session_name ("<video_id>_sessNN"), start ("H:MM:SS"), title?, guests[]?, other_participants[]?, video_id?}.video_idlinks a session to its standalone per-talk upload when one exists; that upload's own item carriesduplicate_ofback to the full recording.titlemay be a segment label ("Roundtable") when the talk has no distinct title. Ownership: this repo. The enrichment script seedssessions[]— from YouTube chapter lists (timestamped video descriptions, cached byfetch_chapters.py) or curated data — only when an item has none, and never overwrites. Edit sessions directly here (add speakers, fix titles); regeneration preserves your edits.- Split transcript identity: when source transcripts are split into sessions,
corresponding
transcript.txtheadings andtranscript.jsonvideo_idfields use the stable<video_id>_sessNNsession name rather than an empty placeholder. parts[].speakers(journal-owned): human speaker identifications for WhisperX-diarized transcripts —{"SPEAKER_NN": "Name", ...}per video. Record the mapping here, then runJournal-Utilities/scripts/apply_speaker_names.pyto regenerate transcript.txt with names (transcript.json keeps raw labels; idempotent; unmapped labels stay SPEAKER_NN). See "Transcripts — raw vs derived".duplicate_ofmarks anOther/<video_id>item whose content duplicates a curated item (currently 34 records: individual symposium talks whose canonical home is the full-recording item). The duplicate intentionally repeats the canonical video's ID; coverage reconciliation excludes the secondary record andINDEX.jsonexposes both record and unique video counts.data/video/activeinferenceinstitute/private_videos.jsondocuments private/unlisted channel videos known to the Institute (from the legacy session database) that are deliberately absent from this public corpus.
INDEX.json— machine index: every item + parts + paths + flags.videoscounts part records, including deliberate duplicates;unique_videoscounts distinct video IDs.INDEX.md— human index grouped by series.SCHEMA.md— this spec; mirror structural changes into Journal-Utilitiesdocs/JOURNAL_SCHEMA.md.
The v2 layout above was produced from the legacy flat structure (per-item
Metadata/, Captions/, Translations/, Images/, Prose/, Appendices/
folders, Audio/ on main) by the non-destructive Journal-Utilities refactor:
placeholders were dropped, Metadata/<item>.json became metadata.json parts,
*.simple.txt became transcript.txt, Captions//Translations/ became
captions//translations/ (translations preserved verbatim), curated folders
moved under assets/ by type, and Audio/*.m4a moved to the audio branch as
audio/<video_id>.64k.m4a. The converter's --dry-run reported every unmapped
source file before any in-place apply. See
Journal-Utilities docs/REFACTOR_READINESS.md.
- Coverage invariant (still in force): every non-duplicate channel video is a
part in exactly one canonical item. Records with
duplicate_ofare deliberate secondary copies and are excluded from missing/duplicate coverage counts; reconcile tomissing == 0against the channel manifest (seePIPELINE.md).