# edit.json v1 見せ場マーカー（beats）データ契約

- 日付: 2026-07-22
- 状態: 実装ラウンドの SSOT（`beats` フィールドのみ確定）
- 前提: `contract-2026-07-17-data-contract-versioning.md`（版必須・追加のみ進化・明示マイグレの三原則）、
  `contract-2026-07-18-edit-json-v1-sources.md`（§3 座標系 — source 秒アンカー）、
  `contract-2026-07-20-edit-json-v1-narration.md`（id 規約・検証分担の先例）
- スコープ: edit.json のトップレベル `beats` フィールド（見せ場マーカー）のみ。
  beats を**生成する側**（解析・抽出）と**消費する側**（演出同期・編集マップ表示）の実装は
  **別タスク**。本書はデータの器と検証責務の正文化のみを行う

## 0. version 運用（後方互換）

`contract-2026-07-14-edit-json-v1-audio.md` §0 / `contract-2026-07-20-edit-json-v1-narration.md` §0
と同じ運用を踏襲する。**`version` は bump しない。**

- `beats` は edit.json の**トップレベル任意フィールド**（`Option`）。存在しなければ従来
  （見せ場マーカーなし）と完全に同じ挙動
- 既存の `edit.json`（`beats` フィールド無し）は一切影響を受けない。既存の読み手は未知フィールドと
  して素通しできる（tolerant reader）
- `contract-2026-07-17-data-contract-versioning.md` 原則 1（版必須・追加のみ進化）どおり、
  任意フィールドの追加のみであり `version` の bump を要しない
- v0（単一 `source`）と v1（`sources[]`）の双方で使用できる。差は `src` の扱いのみ（§2）

## 1. 呼称

| 文脈 | 呼称 |
|---|---|
| データモデル（edit.json のフィールド名・スキーマ・コード・エラーメッセージ） | `beats[]` |
| 人間向け（レポート・UI・ドキュメント本文・オーナーとの会話） | **見せ場マーカー** |

この 2 つを正文とし、他の呼称（ハイライト、チャプター、山場マーカー等）を新設しない。
データ名を英語 1 語（`beats`）に、人間向けを日本語（見せ場マーカー）に固定することで、
コードとレポートのどちらから読んでも同じものを指していると分かるようにする。

## 2. 確定スキーマ

```jsonc
{
  "version": 1,
  "output": { "width": 1920, "height": 1080, "fps": 30 },
  "sources": [
    { "id": "s1", "path": "assets/intro.mp4", "proxy": null },
    { "id": "s2", "path": "assets/main.mov", "proxy": null }
  ],
  "cuts": [ /* 既存のまま */ ],

  "beats": [                    // 省略可。配列
    {
      "id": "b-0001",           // 必須。^b-\d{4}$。edit.json 内で一意
      "src": "s2",              // 任意。sources[].id への参照。省略 = 単一 source 互換
      "t": 42.0,                // 必須。source 秒。0 以上
      "kind": "reveal",         // 必須。空でない文字列
      "strength": 0.8,          // 必須。number。[0, 1]
      "basis": "音声エネルギーのピーク + 『登場』発話"  // 任意。分析根拠の自由記述
    }
  ]
}
```

### フィールド表

| フィールド | 型 | 必須 | 既定値 | 単位・座標系 |
|---|---|---|---|---|
| `beats` | array \| 省略 | 否 | 省略 = 見せ場マーカーなし | — |
| `beats[].id` | string | **必須** | — | `^b-\d{4}$`。edit.json 内で一意（`audio.narration[].id` と同型の規約） |
| `beats[].src` | string | 否 | 省略 = 単一 source 互換 | `sources[].id` への参照。v0（単一 `source`）では**使用できない**（参照先が定義できないため） |
| `beats[].t` | number | **必須** | — | **source 秒**（§3）。0 以上 |
| `beats[].kind` | string | **必須** | — | 空でない文字列。`hook` / `turn` / `punchline` / `reveal` / `emotion` を例示（enum 強制はしない） |
| `beats[].strength` | number | **必須** | — | `[0, 1]`。1 に近いほど強い見せ場 |
| `beats[].basis` | string | 否 | — | 分析根拠の自由記述（§5） |

`kind` は `audio.narration[].provenance.provider` と同じ流儀で、**文書上の例示に留め enum 強制はしない**。
見せ場の種類は素材ジャンルによって語彙が増えるため、契約側で列挙を固定すると新しい語彙を
足すたびにスキーマ改訂が要る。書き手が新しい `kind` を使っても検証は通り、消費側は未知の
`kind` を既定の扱い（§6）へフォールバックする。

`strength` は 0〜1 の連続値で、演出の強度（SE の音量、トランジションの派手さ等）へ写像する
ための入力ヒントである。上限・下限を持つ正規化値としたのは、消費側が素材ごとの絶対スケール
を知らずに相対比較・しきい値判定できるようにするためである。

## 3. 座標系 — source 秒アンカー

**`beats[].t` は (`src`, source 秒) で永続化し、timeline 秒へ変換した結果を永続化してはならない。**

これは `contract-2026-07-18-edit-json-v1-sources.md` §3 の次の規則を beats に適用したものである:

> 字幕、注釈、解析結果は (`src`, source 秒) で永続化し、timeline 秒へ変換した結果を
> 永続化してはならない。表示や書き出しのたびに、その時点の `cuts[]` から timeline 秒へ
> 射影する。

beats は**解析結果**（素材のどこに見せ場があるかという素材固有の事実）であり、編集の結果
（どこに置いたか）ではない。したがって上記規則の適用対象そのものである。具体的な帰結:

- 消費側（演出同期・編集マップ表示・書き出し）は、表示・書き出しのたびに `cuts[]` から
  timeline 秒へ射影する。射影結果を edit.json へ書き戻さない
- 同一 source 区間がタイムライン上に複数回現れ得るため、source 秒 → timeline 秒の対応は
  **一対多**である（同§3）。1 つの beat が複数の timeline 位置へ射影されることは正常であり、
  エラーではない
- どの cut にも含まれない source 秒の beat（カットで落とした区間の見せ場）は timeline 上に
  射影先を持たない。これも正常であり、消費側は単に射影結果 0 件として扱う
- カットの再配置・同一区間の再利用を行っても beats を書き換える必要がない。焼き込んだ時刻との
  ずれが原理的に発生しない

`overlays[].start` / `audio.bgm` / `audio.sfx` / `audio.narration[].t` が**タイムライン秒**で
あることとは対照的である（同§3 の「オーバーレイの `start`、BGM、SFX は従来どおりアウトプット
タイムライン座標であり、この source 秒アンカー規則の対象外である」）。beats は「素材の事実」、
narration や SFX は「編集で置いた演出」であり、座標系の違いはこの性質の違いに対応する。

`src` の省略は `captions.json` の `items[].src` / `review.json` の `annotations[].src` と同じく
**単一ソース互換**を意味する（同§4）。v0（単一 `source`）では参照先を定義できないため、
`src` の存在自体を不正とする（§7 の検証で**エラー**）。

## 4. 劣化規約

`contract-2026-07-14-edit-json-v1-audio.md` §5「音声は装飾であり、映像本体の書き出し成否を
左右してはならない」と同じ設計哲学を beats にも適用する。**beats は演出の入力ヒントであり、
映像本体の書き出し成否を左右してはならない。**

| 状況 | 挙動 |
|---|---|
| `beats` フィールドなし | 従来どおり（見せ場マーカーなし）。エラーにしない |
| `beats` が配列でない | 消費側は beats 全体を無いものとして扱い warning。書き出しは継続する |
| ある要素の `id` / `kind` / `strength` / `t` が不正 | **その 1 要素のみ**無視 + warning。他の beat・映像本体・音声には影響しない |
| ある要素の `src` が `sources[].id` に解決できない | 同上（その 1 要素のみ無視 + warning）。`cuts[].src` の劣化規約（`contract-2026-07-18-edit-json-v1-sources.md` §6）と同型 |
| `t` がどの cut にも含まれない | エラーでも warning でもない。射影結果 0 件として扱う（§3） |
| 未知の `kind` | エラーにしない。消費側は既定の扱いへフォールバックする（§6） |

**書き手は厳格・読み手は寛容。** 静的検証（`validate-edit.mjs` / `edit-lint`）が形式不正・範囲外を
**エラー**として弾くことと、消費側（書き出し・プレビュー）が実行時に不正な 1 要素だけを無視して
継続することは矛盾しない。前者は「壊れたファイルを書かせない」ためのゲート、後者は
「壊れたファイルを渡されても映像を出す」ための保険であり、役割が異なる。この二段構えは
`audio.narration`（`contract-2026-07-20-edit-json-v1-narration.md` §4/§8）および
`audio.bgm` / `audio.sfx`（`contract-2026-07-14-edit-json-v1-audio.md` §5）で確立済みの先例に従う。

## 5. `basis` の命名経緯

分析根拠の自由記述フィールドは、内部設計時の仮名が `source`（分析根拠、の意）であった。
しかし edit.json には既に `source`（v0 の単一素材オブジェクト）、`sources[]`（v1 の素材配列）、
`src`（`cuts[].src` / `beats[].src` などの素材参照）という 3 つの語が存在し、いずれも
「映像素材」を意味する。同じファイル内で `source` が「素材」と「分析根拠」の 2 義になると、
スキーマの読み手・実装者・エラーメッセージの読み手すべてが取り違える。そこで衝突しない語として
**`basis`**（根拠）へ改名した。以後、分析根拠を指す語は `basis` であり、`source` 系の語は
すべて映像素材を指す。

## 6. 消費側の期待（本契約が定めること・定めないこと）

本契約が定めるのは **beats がどう書かれるか**（形式・座標系・検証）までである。beats を読んで
実際にどんな演出を発火するか（SE の選択、トランジションの種類、BGM 変化のカーブ、`strength` から
音量・尺への写像関数、未知 `kind` の既定の扱いの具体）は**本契約のスコープ外**であり、
消費側の別契約で定める。

本契約が消費側に課す不変条件は次の 2 点のみである:

1. **source 秒アンカーを尊重する**（§3）。timeline 秒への射影は消費のたびに `cuts[]` から行い、
   結果を永続化しない
2. **劣化規約を尊重する**（§4）。不正・解決不能な要素は 1 件単位で無視し、書き出し全体を止めない

## 7. 検証

| 層 | 検証すること |
|---|---|
| `packages/schemas/edit.schema.json` | `$defs/beatItem` として構造（型・`id` パターン・必須項目・`strength` の範囲・`kind` の最小長）を定義する。`editV0` / `editV1` 双方の `properties.beats` から参照する。`additionalProperties: true`（tolerant reader）を維持する |
| `packages/schemas/bin/validate-edit.mjs` | 上記に加え、JSON Schema 単体では表せない **`id` のファイル内一意性**と **`src` の参照整合**（v1: `sources[].id` に存在しない場合エラー / v0: `src` の存在自体がエラー）を検証する。`cuts[].src` と同じ扱い |
| `packages/edit-lint`（`src/edit-lint.mjs`） | validate-edit と同一の構造チェック（エラー）を、依存ゼロ・手書き写しの流儀で実装する |

`id` の一意性と `src` の参照整合を JSON Schema に載せないのは、どちらも**兄弟要素・兄弟フィールドの
値を突き合わせる必要があり、JSON Schema 標準の語彙では表現できない**ためである
（`cutV1.src` / `audio.narration[].id` と同じ理由・同じ分担）。

**edit-lint は source の実尺と `beats[].t` を突き合わせない。** `--media` なしでメディアを
デコードしない規律を維持するためである。「`t` が素材の実尺を超えている」という検査は
**将来課題**とし、必要になった時点で実尺解析（ffprobe 等）を伴う別契約として設計する。
`beats` および `analysis.json` の不在は、edit-lint の既存原則どおり**エラーにしない**
（skipped として報告する）。

## 8. 将来拡張の席（本契約のスコープ外）

- **`emphasis_words[]`**（語レベルの強調指定）: beats より細かい粒度で「どの語を強調するか」を
  持つデータ。本契約では席のみ言及し、フィールドは定義しない
- **`direction {}`**（演出プリセット・強度の明示指定）: beats を「何が起きたか」の観測に留め、
  「どう演出するか」を分離して持つためのデータ。本契約では席のみ言及し、フィールドは定義しない

いずれも別契約で扱う。本契約は `beats[]` の器だけを確定し、これらの席が将来開く可能性がある
ことを記録するに留める。
