# Conventions and gotchas

The things you'll get wrong on the first try. Skim this once; refer back when something behaves strangely.

## Time is in seconds, not frames

Every time-shaped field (`in`, `out`, `trackOffset`, `duration`, keyframe `time`, `fadeIn`, `fadeOut`) is **seconds, float**. The composition's `fps` is locked at create-time and used only by the render pipeline. Don't multiply by fps anywhere.

Bare seconds and `Ns` / `MmNs` are interchangeable in `autoviral seek`:

```bash
autoviral seek 12.5      # 12.5 seconds
autoviral seek 12.5s     # same
autoviral seek 1m30s     # 90 seconds
```

`comp show` always returns numeric seconds — no string conversion. Don't try to feed `"12.5s"` into a clip body.

## `trackOffset` is absolute on the track

Not relative to the previous clip. If clip A is `trackOffset: 0, in: 0, out: 5`, the next clip starts at `trackOffset: 5`, not `trackOffset: 0`.

When inserting a clip into the middle of a track, you usually need to bump every subsequent clip's `trackOffset`. The bridge does **not** auto-shift; that's your job.

## Clip overlap is allowed (and required for crossfade)

The schema does not enforce non-overlapping clips. Two video clips on the same track can occupy the same `[trackOffset, trackOffset + (out - in)]` window. The render pipeline compositest hem with the later clip on top (per `clips[]` array order).

This is **how crossfades work**: clip A ends at t=5.18, clip B starts at t=5.0, with paired opacity keyframes on both. See `recipes/video/crossfade-between-clips.md`.

## Keyframe `time` is clip-local

A keyframe with `time: 0.18` fires 0.18 seconds **after the clip starts on the track** — not 0.18 seconds into the composition. Always think clip-local.

A clip with `in: 0.5, out: 4.5` runs for 4.0s on the track. Keyframes live in `[0, 4.0]` (clip-local), not `[0.5, 4.5]` (source-local).

## `clip set` patches replace nested objects wholesale

`clip set vc_s07 --filters '{"brightness": 0.1}'` will replace the entire `filters` object — `contrast` and `saturation` get lost.

To tweak one field inside a nested object, fetch first:

```bash
filters=$(autoviral comp show --format json | jq '.tracks[].clips[] | select(.id=="vc_s07") | .filters')
new_filters=$(echo "$filters" | jq '.brightness = 0.1')
autoviral clip set vc_s07 --filters "$new_filters"
```

This is annoying enough that we may add `--patch-deep` later; for now, expect it.

## Asset paths are relative to the workspace root

```yaml
# Correct
src: assets/clips/s01.mp4

# Wrong — absolute paths leak the user's filesystem layout into composition.yaml
src: /Users/nanjiayan/.autoviral/works/.../assets/clips/s01.mp4
```

The render pipeline resolves relative paths against the workspace root. If you must reference an asset outside the workspace, copy it in first.

## IDs are agent-chosen but must be unique

Clip ids (`vc_s07`, `ac_bgm`, etc.) are not generated by the bridge — you pass them on `clip add` via the schema body, or the server picks `c_<random>` if you don't. **Always pick a meaningful id** when you can: it makes downstream `clip set` / `select` / log lines readable.

Convention:

- `vc_*` — video clips
- `ac_*` — audio clips
- `tc_*` — text clips
- `ov_*` — overlay clips
- `trk_*` — tracks
- `a_*` — assets (registry entries)
- `p_*` — export presets

The bridge doesn't enforce the prefix; it's just human-friendly.

## YAML output ≠ JSON output (intentionally)

When stdout is a TTY (user running the command interactively):

```yaml
- id: vc_s01
  kind: video
  src: assets/clips/s01.mp4
```

When stdout is piped to another command (you're an agent):

```json
[{"id":"vc_s01","kind":"video","src":"assets/clips/s01.mp4",...}]
```

If you want JSON in an interactive shell (e.g. for `jq`), pass `--format json` explicitly. Don't assume.

## Loopback only, no auth

The bridge binds to `127.0.0.1` only. There's no token, no header check beyond `X-AutoViral-Work-Id`. If your agent tries to hit the bridge from a remote host, expect a refused connection — that's by design.

## The CLI exits 2 outside a Studio terminal

If you run `autoviral whoami` from a plain shell prompt (not inside a Studio terminal panel), it exits 2 with:

```
autoviral: AUTOVIRAL_WORK_ID is not set; run from a Studio terminal panel
```

This is intentional. The CLI is safe to leave on the global PATH; it just won't do anything without the panel env.

## Schema validation is the safety net

Every write goes through zod. If the bridge rejects your patch, the on-disk `composition.yaml` is untouched. You'll see HTTP 400 + an issues array (exit code 4). Read it; don't retry blindly.

Common rejections:

- `out` not greater than `in`
- `trackOffset` negative
- `speed` keyframe value outside `[0.1, 4.0]`
- `kind` mismatch on a track (text clip in a video track, etc.)
- `aspect` not one of the four enums

## What's NOT enforced by the schema

These are landmines you'll step in if you ignore them:

- **Clip end past composition duration** — schema allows it; render pipeline clamps it. The Studio timeline shows the clip overflowing.
- **Two clips with the same id** — schema doesn't enforce uniqueness; the watcher loads the second one and `clip set` updates whichever it finds first.
- **Missing asset file** — `clip add --src does-not-exist.mp4` succeeds; the render will fail later. Validate paths exist before adding when you can.
- **Audio clip on a video track** — discriminated union catches `kind` mismatch but only at parse time; if you somehow craft a clip in a way that confuses the union, behavior is undefined.

## Versioning

The bridge path is `/api/bridge/v1/`. Breaking changes ship as `/v2/`. `autoviral whoami` returns the bridge version it speaks; the CLI checks compat on startup and prints an "upgrade autoviral" message if the server is newer.
