docs: user guide for configuring media (hand-authoring + validating the catalog)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# Human Experience Filter — User Guide
|
||||
|
||||
> **Scope (current build state).** Only the catalog + selection core is built so
|
||||
> far. This guide covers the one thing you can do today: **configure media by
|
||||
> hand-authoring and validating catalog records.** The automated ingest tool,
|
||||
> the tagging/review CLI, and the room player are separate, not-yet-built
|
||||
> sub-projects and are not covered here.
|
||||
|
||||
---
|
||||
|
||||
## What "configuring media" means
|
||||
|
||||
The installation plays media chosen by where a viewer sets the control knobs.
|
||||
Every piece of media is one **record** in a catalog file. A record says *what the
|
||||
file is*, *where it lives on the drive*, *its license*, and — the important part —
|
||||
*its coordinate* in the experience-space. At run time the player will pick the
|
||||
record nearest the knob position; for now, you build and validate that catalog.
|
||||
|
||||
The catalog is a single file: **`catalog/library.jsonl`**. It is
|
||||
[JSON Lines](https://jsonlines.org/) — **one JSON object per line**, one line per
|
||||
piece of media. Blank lines are ignored.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The commands in this guide need **Python 3.11+** and must be run **from the repo
|
||||
root**, where `import hef` resolves with no install step. Examples use
|
||||
`.venv/bin/python`; if you made the project virtualenv they work as-is, otherwise
|
||||
substitute your own `python3`.
|
||||
|
||||
Creating the virtualenv is optional but recommended:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The record schema
|
||||
|
||||
Each line is a JSON object. **Twelve fields are required**; the rest have
|
||||
defaults and may be omitted.
|
||||
|
||||
### Required fields
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|------------------|--------|----------------------------------------------------------------|
|
||||
| `id` | string | Stable unique id (non-empty). Your choice; keep it unique. |
|
||||
| `title` | string | Human title. |
|
||||
| `source_url` | string | Where the piece came from. |
|
||||
| `source_archive` | string | Origin label, e.g. `internet_archive`, `musopen`, `librivox`. |
|
||||
| `license` | string | One of: `public_domain`, `cc0`, `cc_by`, `cc_by_nc`. |
|
||||
| `mode` | string | One of: `audio`, `video`, `av`. |
|
||||
| `left` | int | Analytical / verbal axis, **0–4** (see rubric). |
|
||||
| `right` | int | Artistic / abstract axis, **0–4**. |
|
||||
| `dark` | int | Somber / heavy axis, **0–4**. |
|
||||
| `light` | int | Uplifting / serene axis, **0–4**. |
|
||||
| `duration_s` | int | Segment length in seconds (≥ 0). |
|
||||
| `file_path` | string | Path to the media file on the player's drive. |
|
||||
|
||||
### Optional fields (defaults shown)
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|------------------|--------------|------------------------------------------------------------|
|
||||
| `review_status` | `"proposed"` | `proposed` or `approved` — whether a human has blessed it. |
|
||||
| `attribution` | `""` | **Required text when `license` is `cc_by` or `cc_by_nc`.** |
|
||||
| `resolution` | `""` | e.g. `1920x1080` (video). |
|
||||
| `dominant_color` | `""` | Hex color, used by the side walls later. |
|
||||
| `rationale` | `""` | One-line note on why you chose the coordinate. |
|
||||
| `reviewed_at` | `null` | Timestamp when approved. |
|
||||
| `notes` | `""` | Free text. |
|
||||
|
||||
### Validation rules (enforced when you load/save)
|
||||
|
||||
- `id` must be non-empty.
|
||||
- `mode`, `license`, `review_status` must be one of the allowed values above.
|
||||
- `left`, `right`, `dark`, `light` must be **integers** in `0..4` (booleans are
|
||||
rejected).
|
||||
- `duration_s` must be `≥ 0`.
|
||||
- `cc_by` and `cc_by_nc` require a non-empty `attribution`.
|
||||
- Unknown fields and missing required fields are rejected.
|
||||
|
||||
> The file referenced by `file_path` is **not** checked for existence yet — only
|
||||
> the record's structure is validated.
|
||||
|
||||
---
|
||||
|
||||
## The coordinate rubric
|
||||
|
||||
The four coordinates are the curatorial heart of the work. Each is a felt
|
||||
judgment on a **0–4** scale. The two pairs are **independent knobs**, not opposite
|
||||
ends of one slider — a piece can be high on both `left` and `right` ("whole
|
||||
brain"), or high on both `dark` and `light` ("bittersweet").
|
||||
|
||||
| Axis | 0 means… | 4 means… |
|
||||
|---------|---------------------|-------------------------------------------------------------|
|
||||
| `left` | not analytical | strongly analytical / verbal / structured — narration, language, logic, sequence, instruction, documentary, spoken word |
|
||||
| `right` | not artistic | strongly artistic / emotional / abstract — music, abstract visuals, dance, nature, awe, non-verbal, dreamlike |
|
||||
| `dark` | not somber | strongly somber / heavy — ominous, melancholic, night, decay, minor-key, storms, noir |
|
||||
| `light` | not light | strongly uplifting / serene — joyful, hopeful, bright, sunrise, gardens, major-key, calm |
|
||||
|
||||
`mode` describes the media's channels: `audio` (sound only), `video` (image
|
||||
only), `av` (both). There is no `none` record — "None" is a control state the
|
||||
player handles by going dark, not a kind of media.
|
||||
|
||||
**License stance:** prefer `public_domain` / `cc0`; `cc_by` is fine if you record
|
||||
`attribution`; `cc_by_nc` only while the installation stays non-commercial.
|
||||
|
||||
---
|
||||
|
||||
## Adding media — two ways
|
||||
|
||||
### Option A: edit the JSONL file by hand
|
||||
|
||||
Append one line per piece to `catalog/library.jsonl`. A complete example record
|
||||
(formatted across lines here for readability — **it must be a single line in the
|
||||
file**):
|
||||
|
||||
```json
|
||||
{"id": "nasa-earthrise", "title": "Earthrise", "source_url": "https://archive.org/details/earthrise", "source_archive": "internet_archive", "license": "public_domain", "mode": "video", "left": 0, "right": 4, "dark": 1, "light": 3, "duration_s": 600, "file_path": "/media/nasa-earthrise.mp4", "resolution": "1920x1080", "rationale": "wordless awe, bright Earth on black — strongly right-brain, gently light"}
|
||||
```
|
||||
|
||||
### Option B: use the Python API (validates before writing)
|
||||
|
||||
```bash
|
||||
.venv/bin/python -c '
|
||||
from hef.catalog import Record, append_record
|
||||
r = Record(
|
||||
id="librivox-meditations",
|
||||
title="Meditations, Book II (excerpt)",
|
||||
source_url="https://librivox.org/meditations/",
|
||||
source_archive="librivox",
|
||||
license="public_domain",
|
||||
mode="audio",
|
||||
left=4, right=1, dark=2, light=2,
|
||||
duration_s=720,
|
||||
file_path="/media/librivox-meditations-ii.mp3",
|
||||
rationale="spoken philosophy, verbal and reflective — strongly left-brain",
|
||||
)
|
||||
append_record(r, "catalog/library.jsonl")
|
||||
print("added", r.id)
|
||||
'
|
||||
```
|
||||
|
||||
`append_record` validates the record and raises `CatalogError` (printing what is
|
||||
wrong) before it writes, so a bad record never lands in the file.
|
||||
|
||||
---
|
||||
|
||||
## Validating the whole catalog
|
||||
|
||||
Loading the catalog validates **every** record. Run this after editing by hand:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -c "from hef.catalog import load_catalog; print(len(load_catalog('catalog/library.jsonl')), 'records OK')"
|
||||
```
|
||||
|
||||
- Prints e.g. `12 records OK` if everything is valid.
|
||||
- Raises `CatalogError` describing the problem if not. A validation error names
|
||||
the field and reason, e.g. `coordinate left=9 out of range 0..4`; malformed
|
||||
JSON additionally names the line, e.g. `line 4: invalid JSON: ...`.
|
||||
|
||||
---
|
||||
|
||||
## Sanity-checking selection (optional)
|
||||
|
||||
To feel how tagging drives playback, you can ask the selection core which record
|
||||
a given knob position would pick. Coordinates are `Coordinate(left, right, dark,
|
||||
light)`; the second argument is the content-mode selector
|
||||
(`audio` / `video` / `av` / `none`).
|
||||
|
||||
```bash
|
||||
.venv/bin/python -c "
|
||||
from hef.catalog import load_catalog
|
||||
from hef.selection import Coordinate, select
|
||||
lib = load_catalog('catalog/library.jsonl')
|
||||
pick = select(lib, Coordinate(left=0, right=4, dark=1, light=3), 'video')
|
||||
print(pick.id if pick else 'nothing')
|
||||
"
|
||||
```
|
||||
|
||||
Notes that shape how you tag:
|
||||
|
||||
- Selection is **nearest-match** — every knob position resolves to the closest
|
||||
record, so a sparse catalog still works; you do not need to fill every
|
||||
coordinate.
|
||||
- `select(..., 'none')` always returns nothing (the void state).
|
||||
- Pass `approved_only=True` to restrict to records whose `review_status` is
|
||||
`approved`.
|
||||
|
||||
---
|
||||
|
||||
## Where the media files go
|
||||
|
||||
Records point at files via `file_path`. Those files live on the player's drive,
|
||||
**not** in this git repo (the repo holds only the catalog metadata). Keep your
|
||||
`file_path` values consistent with wherever you mount that drive on the machine
|
||||
that will eventually run the room.
|
||||
Reference in New Issue
Block a user