---
name: supermovie-subtitles
description: |
  transcript_fixed.jsonからテロップ（telopData.ts）とタイトル（titleData.ts）を
  自動生成するスキル。LLM意味分割 + 1行テロップ + 読了時間計算 + フォーマット連動で
  視聴者が自然に読めるテロップを生成。文字起こし自体は行わない。
  「テロップ生成」「字幕生成」「supermovie subtitles」と言われたときに使用。
argument-hint: [プロジェクトパス]
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Agent
effort: high
---

# SuperMovie Subtitles — テロップ＆タイトル自動生成

Senior video subtitle designer として、修正済み文字起こしデータを
LLM意味分割 → 読了時間チェック → スタイル割当の3段階で
視聴者が自然に読めるテロップに変換する。

**注意: このスキルは文字起こしを行わない。** `/supermovie-transcribe` → `/supermovie-transcript-fix` で事前に完了していること。

---

## ★標準モード（確定仕様・必ずこれを使う）

**整列・同期・キャップ・アニメ回転・検証のコードを自分で書かない。**
検証済みの正規スクリプト `scripts/align_telop.py` が全部やる。モデルの仕事は入力ファイル2つの作成だけ。

### 手順

```
1. Read: このスキルのフォルダの 改行ナレッジ.md（まさき式・節ベース改行の正本）
2. 作成: <PROJECT>/テロップ確定.txt
   - ★★**「フィラーだけ除去・ほぼ全発話カバー」（まさきFB 2026-06-13・最重要）**:
     transcript_fixed.json の本文を1行=1テロップに行分割。**除くのはフィラーだけ**
     （はい/えっと/あの/まあ/もうね/っていうところで/んですよね等の中身のない繋ぎ）。
     **意味のある発話・固有名詞・数字は1つも消さない。要点だけに絞りすぎない**
     （疎すぎると視聴者に「テロップ抜け＝失敗」に見える）。目標**密度90%前後**・全文逐語に近い行数。
   - 残りは自然に読めるよう**軽い意訳OK**。ただし align は音声照合（difflib・各行50%以上一致）なので
     喋ったワードから大きく外さない（語順大変更・新しい固有名詞/数字の追加は禁止）。
   - 1行最大18字。句読点は付けない（照合では無視されるがテロップは句読点なしが正）。
   - ★全編均一品質: 文字起こしを約120〜150語のセクションに分けて順に処理し、最後まで同じ粒度で改行する。
3. 作成: <PROJECT>/スタイル割当.json（フォーマットはスクリプト冒頭のdocstring参照）
   - 内容を読んで emphasis / warning / success / list_slide の行番号(1-origin)を決める
   - 章タイトル `titles`: 動画にスライドの焼き込みタイトルが既にあるなら **空 `[]`**（二重表示回避・まさきFB 2026-06-13）。
     無ければ最初の1件 word_start_ms: null = frame 0 ＋章見出し。
   - `hallucination_ranges`: ★**whisper幻聴だけ**を [start,endExclusive] で入れる（フィラーは入れない＝difflibが自動許容）。
     ①同一字の連発（数十〜200連続・ほぼ0尺）②同一句の二重出力の2回目（低conf/圧縮）。
     **difflib補集合を入れると重複句の1回目まで誤除去するので使わない**。
   - スタイル比率の目安: normal 65〜75% / emphasis 15〜25% / warning 3〜7% / success 4〜8%
     （normalだらけ=単調=NG。「重要・絶対・コツ」=emphasis、「注意・失敗・NG」=warning、
       「できる・解決・成果」=success、「1つ目/2つ目/章見出し」=list_slide）
4. 実行:
   python3 "<このスキルのフォルダ>/scripts/align_telop.py" "<PROJECT>"

   ★**確定.txtレビューモード（GATE_BEFORE_ALIGN）— まさきFB 2026-06-11**:
   オーケストレーターから「確定.txtレビューモード」「align前で停止」と指示された場合は、
   **ステップ2・3（テロップ確定.txt＋スタイル割当.json の作成）までで止め、この align_telop.py は実行しない。**
   完了報告に 確定.txt のパスと全文・削ったフィラー件数を含める。整列はユーザーOK後に
   オーケストレーターが align_telop.py を1発実行する（後工程はスクリプトのみでサブエージェント不要）。
   GATE指定が無い通常モードでは従来どおり 2→3→4 を一気に実行する。
5. エラーが出たら表示された行を直して再実行（エラー0になるまで）。
   - 「不一致」→ 確定.txt のその行が本文と違う。本文どおりに直す
   - 「幻聴スキップ」のログ → 正常動作（Whisperの幻聴を自動除去した報告。内容を確認だけする）
   - 「18字超」→ 該当行を最後の助詞（を/に/で/と/が/は/の）で2分割
   - 「後半1/3の密度低下」→ その区間の改行を前半と同じ粒度でやり直す
6. 完了報告（テロップ数・スタイル内訳・アニメ内訳・V6密度検証・無音キャップ最大値）
```

スクリプトが自動で保証すること（supermovie-0609 で実証済み・本番出力と完全一致を確認）:
- 幻聴重複ワードの整列駆動除去（確定.txt=正解テキストと一致しない圧縮タイムスタンプ語だけスキップ）
- cutData.ts の keepSegmentsMs を読んで playback タイムラインへリマップ
- start = round(word.start) **+2f** / end = min(次start, 発話終わり**+0.4s**)（持続表示+無音キャップ）
- 最終テロップは end=TOTAL_FRAMES（Whisperの末尾タイムスタンプ圧縮対策。カットの末尾無音≦0.5s保証で安全）
- normal=none / 冒頭アニメ無し / アニメプール回転・3連続禁止 / リストはslideFromLeft統一
- 検証 V1〜V6 + 無音キャップ検査（≦0.55s）

以降の Phase 1〜6 は、改行・スタイル判断の質を上げるための知識ベース。
**フレーム計算やバリデーションを手で再実装する必要はない。**

---

## ワークフロー概要

```
┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│ 1. 設定   │→│ 2. LLM    │→│ 3. 読了時間│→│ 4. スタイル│→│ 5. 検証   │
│ 読み込み  │  │ 意味分割  │  │ チェック  │  │ 割り当て  │  │ +書出し  │
└──────────┘   └──────────┘   └──────────┘   └──────────┘   └──────────┘
                  ↑ ★核心
```

---

## 前提条件チェックリスト

- [ ] `/supermovie-init` でプロジェクト生成済み
- [ ] `/supermovie-transcribe` → `/supermovie-transcript-fix` 済み
- [ ] `transcript_fixed.json` が存在し `words` 配列がある
- [ ] `project-config.json` が存在（format/tone参照）
- [ ] `/supermovie-cut` でカット済み（任意：cutData.tsがあればカット後タイミングで生成）

---

## Phase 1: 設定読み込み

### 1-1. フォーマット別パラメータ決定

`project-config.json` の `format` から全パラメータを決定:

```
┌─────────────┬───────────┬───────────┬───────────┐
│ パラメータ    │ youtube   │ short     │ square    │
│              │ (16:9)    │ (9:16)    │ (1:1)     │
├─────────────┼───────────┼───────────┼───────────┤
│ 1行最大文字数 │ ★18文字   │ 15文字    │ 18文字    │
│ 目標文字数   │ 12〜18文字 │ 8〜12文字  │ 9〜14文字  │
│ 行数         │ 1行固定   │ 1行固定   │ 1行固定   │
│ 読了速度     │ 7文字/秒  │ 5文字/秒  │ 6文字/秒  │
│ 最小表示時間  │ 0.8秒     │ 1.0秒     │ 0.9秒     │
│ 最大表示時間  │ 5秒       │ 5秒       │ 5秒       │
└─────────────┴───────────┴───────────┴───────────┘
```

★youtube の最大は **18字ハード**（盤面で2行に折り返さない上限。旧25字・旧19字は廃止）。

**★重要: テロップは常に1行。改行(\n)は一切挿入しない。**
文字数が多い場合はテロップを分割して2つにする。

### 1-2. その他の設定読み込み

- `Root.tsx` → `FPS`, `TOTAL_FRAMES`
- `project-config.json` → `tone`, `notes`（キーワード）
- `transcript_fixed.json` → `words`, `segments`
- `src/cutData.ts` → カットデータ（存在する場合は Phase 2.5 で必須使用）

---

## Phase 2: LLM意味分割（★核心・/kaigyou 内蔵）

**機械的な文字数カウントではなく、Claude LLMで「話し言葉の呼吸」に合わせた分割を行う。**

### 2-0. 分割思想（最重要）

> ★★★ このPhaseは **/kaigyou（まさき式改行）と同一処理**。`/kaigyou` を別途呼ぶ必要はなく、
> ここで必ず `改行ナレッジ.md`（このスキルと同じフォルダ）を Read し、文字起こしを節分割する。
> 生成した改行は `<PROJECT>/テロップ確定.txt` に保存し、それを音声整列の入力にする
> （ユーザーが事前に テロップ確定.txt を用意/編集済みなら、それを優先して使う）。
> 改行ナレッジは、まさき本人の改行スタイルとの差分を言語化した文字数・改行頻度の正本。

テロップ分割の本質は「話し手の呼吸に合わせる」こと。
1テロップ = 話し手が一息で言う1フレーズ。

**まさき式の核心（改行ナレッジ.mdより）:**
1. 1行は長めに使う（**最大18字**）。短い断片は隣の行に統合する。
2. 改行頻度を下げる。AIは切りすぎる傾向 → 切る回数を減らす。
3. 助詞ごとに切らない。**意味の塊（主語〜述語/修飾語〜被修飾語/節）がまとまるまで切らない。**

```
✅ 良い分割（節の塊・長め）:
  「サイトごとに固有の文字列が発行されて」（17文字）
  「ものすごく大きな金銭的損害が」（13文字）

❌ 悪い分割（切りすぎ・断片）:
  「サイトごとに」「固有の文字列が発行されて」← 助詞で細切れ
  「ものすごく大きな」「金銭的損害が」← 修飾語と被修飾語を分断
```

### 2-1. テロップ分割の数値目標（★まさき式キャリブレーション済み）

```
┌──────────────────────────────────────────┐
│ 目標文字数: 12〜18文字（youtube 16:9）      │
│ 最大:       18文字ハード（V4で機械検証）      │
│ 許容範囲:   5〜18文字（決め台詞は5〜9でも可） │
│ 平均目標:   11〜14文字                       │
│ 改行:       テロップ内は改行なし（1行固定）    │
│ 改行頻度:   低め（短い断片を作らず隣に統合）   │
│ 読了速度:   平均6〜7文字/秒                  │
└──────────────────────────────────────────┘
```

**アルゴリズム分割のパラメータ（まさき式）:**
`MIN=10 / PART_MIN=14 / HARD_MAX=18`（旧 MIN=7/PART_MIN=12/HARD=24 は切りすぎ）。
MIN_BEFORE_BREAK を上げて短い断片での改行を抑制するのが、まさき式に近づける最大のレバー。

### 2-2. 分割位置の優先順位（★最重要ルール）

テロップの区切り位置は以下の優先度で判定する:

```
優先度1 ★★★ 文末（最強の区切り）
  ─────────────────────────────────
  「〜ます」「〜です」「〜ません」
  「〜ですね」「〜ですよね」「〜んですね」
  → 文が完結するので必ずここで切る

優先度2 ★★☆ 接続助詞の後ろ（自然な呼吸）
  ─────────────────────────────────
  「〜して」「〜していて」「〜たり」
  「〜ので」「〜から」「〜ため」
  「〜けど」「〜けれど」「〜のですけど」
  「〜っていう」「〜という」
  → 前の節が完結し次の節に移る切れ目

優先度3 ★☆☆ 助詞の後ろ（許容される区切り）
  ─────────────────────────────────
  「〜は」「〜が」「〜を」
  「〜に」「〜で」「〜と」「〜も」
  「〜のは」「〜には」「〜では」「〜とか」
  → 助詞の「後ろ」で切る。助詞の「前」では絶対に切らない。

  ✅ OK: 「案件を探せばいいの｜」
  ❌ NG: 「案件｜を探せばいいの」
```

### 2-3. 絶対に切ってはいけない位置

```
❌ 助詞の直前
   「時間 | を」→ NG   →  「時間を | 」→ OK

❌ 用言の途中
   「自動 | 化する」→ NG   →  「自動化する | 」→ OK

❌ 固有名詞の途中
   「モッ | ピー」→ NG   →  「モッピー | 」→ OK
   「プライム | 市場」→ NG  →  「プライム市場 | 」→ OK

❌ 数字と助数詞の間
   「3 | つ」→ NG   →  「3つ | 」→ OK
   「2 | 種類」→ NG   →  「2種類 | 」→ OK

❌ 「〜っていう」の途中
   「っていう | 会社」→ OK
   「って | いう会社」→ NG
```

### 2-4. LLMへのプロンプト

```
あなたはYouTube動画のテロップ分割の専門家です。
以下のテキストを「話し手の呼吸」に合わせてテロップに分割してください。

## 核心ルール
テロップ = 話し手が一息で言う1フレーズ。
改行は使わない。1テロップ = 1行。

## 文字数ガイドライン
- 目標: 12〜18文字（フォーマット: <format>）
- 許容: 5〜18文字（18字ハード上限・盤面折り返し防止）
- 平均: 11〜14文字を目指す
- 短い方が読みやすいが、意味が宙ぶらりんになるほど短くしない

## 分割位置の優先順位
1. 文末（ます/です/ですね/ですよ）で必ず切る
2. 接続助詞の後ろ（して/ので/から/けど/っていう）で切る
3. 助詞の後ろ（は/が/を/に/で/と/も）で切れる
4. 助詞の前では絶対に切らない

## 切ってはいけない位置
- 助詞の直前（「時間 | を」NG、「時間を | 」OK）
- 用言の途中（「自動 | 化する」NG）
- 固有名詞の途中（「モッ | ピー」NG）
- 数字+助数詞の間（「3 | つ」NG）

## テロップ分割の良い例
「それでは今回は」（7文字）
「ポイ活サイトのモッピーとハピタス」（16文字）
「というテーマで解説していきたいと思います」（19文字）
「前回ポイ活は時間対効果最強の副業」（16文字）
「というテーマでお話しさせてもらったんですけど」（21文字）
「その中で仕組み化型と」（10文字）
「単発型っていう」（7文字）
「やる価値のあるポイ活の2種類を」（15文字）
「お伝えしましたよね」（9文字）

## 句読点ルール
- 「。」「、」はテロップでは省略する
- 「！」「？」は残す

## 入力テキスト
<セグメントテキスト>

## 入力words（タイムスタンプ参照用）
<words JSON配列>

## 出力形式
以下のJSON配列で返してください:
[
  {
    "text": "ポイ活サイトのモッピーとハピタス",
    "wordIndices": [0, 1, 2, 3, 4, 5]
  },
  {
    "text": "というテーマで解説していきたいと思います",
    "wordIndices": [6, 7, 8, 9, 10, 11, 12]
  }
]
```

**wordIndices でタイムスタンプと紐付け（★音声頭ぴったり）:**

```
SYNC_DELAY = +2フレーム（★まさきFB: PRE=0でもまだ早い→+2f(67ms)後ろ倒しでピッタリ）
SYNC_POST_BUFFER = 2フレーム（最終テロップの余韻のみ）

【startFrame ★音声頭ぴったり】
  startFrame = mapOriginalToPlayback( round(words[wordIndices[0]].start /1000*FPS) ) + SYNC_DELAY

  ※ ★まさきFB（最重要・2段階で学習）:
    ① 先出し100ms（PRE=3）→「早すぎて気になる」
    ② PRE=0（round）→「やっぱり若干早い」
    ③ ✅ round + SYNC_DELAY(+2f=67ms)→ピッタリ。
    視覚は聴覚より速く感知するため、word.startちょうどだと早く見える。+2fで一致。
  ※ SEもこの startFrame に置くため、SEも同じく+2fで音声頭に揃う。
  ※ cutData がある場合は round(rawFrame) を mapOriginalToPlayback() に通してから +SYNC_DELAY。
```

**★カバー保証（必須・「テロップが出てない」をゼロにする）**
テロップ確定.txt が音声を完全にカバーしない（文を省略/圧縮した）と、その発話区間に
テロップが無く「声だけ流れる」状態になる（まさきFB）。これを構造的に防ぐ:
- 各テロップ i は **fw[i]（自分の先頭ワード）〜 fw[i+1]-1（次テロップの直前ワード）を必ず受け持つ**。
- fw は単調非減少に補正し、fw[0]=0（先頭から漏らさない）。最後のテロップは末尾ワードまで。
- 併せて、確定.txt は**喋った内容を省略しない**（用語の表記修正はOK、文の削除はNG）。

**★セリフが終わる前に切り替えない（まさきFB）**
- difflibでの各テロップの「実マッチ終端ワード」を lw_match[i] とし、
  **fw を非重複・順次に補正**: `fw[i] = max(fw[i], lw_match[i-1]+1)`。
  → 次テロップが前テロップの発話途中の語から始まることが無くなり、セリフ完了前の切替が消える。
- endFrame は最低でも自分のマッチ発話の終わりまで: `endFrame[i] ≥ pe(words[lw_match[i]].end)`。

**★1行18字厳守（ハード強制）**
- 確定.txt 生成時に最大18字。生成後に **>18字の行があれば自動で最後の助詞(を/に/で/と/が/は/の)で2分割**。
- 検証で 19字以上が1つでもあれば分割し直してから出力（盤面で2行に折り返すため厳守）。

**★★ 全編均一品質（まさきFB・最重要）: 後半ほど雑になるのを禁止する**
長い文字起こしを一気に改行すると、前半は丁寧・後半は粗く（圧縮/粒度低下）なりがち。
**最初から最後（締めの「ありがとうございました」まで）を同じ集中度・同じ粒度で改行する。**
- 文字起こしを**セクション（話題の切れ目 or 約120〜150語）ごとに分割**し、各セクションを
  独立に・同じルールで節分割する（1パスで流し書きしない）。中盤以降も省略・圧縮しない。
- 各セクションの後に「ここは前半と同じ粒度か？」を自問してから次へ進む。

**★書き出し前の必須検証パス（チェックを増やしても時間はほぼ増えない）:**
- V1 未カバーワード = 0（全発話にテロップ）
- V2 表示の空白は「実測無音中」のみ（発話中の空白ゼロ）
- V3 各テロップ endFrame ≥ 自分のマッチ発話終わり（セリフ完了前に切り替わらない）
- V4 全テロップ ≤18字
- V5 同期: テロップ start は round + SYNC_DELAY(+2f)
- **V6 均一性: テロップを尺で3分割し、各1/3の「1分あたりテロップ数」と「平均文字数」を比較。
  後半が前半より粗い（テロップ/分が2割以上減 or 平均文字数が2割以上増）なら、その区間を
  前半と同じ粒度で改行し直す。SE密度も各1/3で同程度（前半だけ賑やか＝NG）。**
- いずれか違反なら直してから render する。

**★前処理（必須）: 幻聴重複ワードの除去**
Whisperは同一フレーズを連続で2〜3回出力することがある（2回目以降は0.7秒に十数語＝幻聴）。
**これを words 配列から除去してから整列する。** 残すとテロップが幻聴位置に貼られ、
実発話のテロップが1フレームに潰れて「テロップが入っていない」状態になる（実例あり）。
判定: 連続する同一テキストブロックで、2番目以降の合計尺が1番目の0.45倍未満なら幻聴→削除。

【endFrame ★持続表示モード（無音キャップ付き）】

  speechEnd  = ceil(words[wordIndices[最後]].end /1000*FPS)  // このテロップの発話終わり(playback)
  nextStart  = 次のテロップの startFrame
  HOLD_TAIL  = 12フレーム（0.4秒）= 発話後に残す自然な余韻

  endFrame = min(nextStart, speechEnd + HOLD_TAIL)

  → 次の発話までの間が短い（連続発話）なら nextStart まで持続＝バトンタッチ
  → 次の発話まで間が空く（ポーズ/無音）なら speechEnd+0.4秒で消す
  ※ これにより「声が無いのにテロップだけ出ている」状態を構造的に防ぐ。

■ 最後のテロップだけ:
  endFrame = min(TOTAL_FRAMES, speechEnd + SYNC_POST_BUFFER)

※ endFrame > TOTAL_FRAMES はクランプ。endFrame < startFrame+最小表示(15f) は最小表示を確保。
```

**なぜ無音キャップが必要か（★重要）:**
純粋な「次のテロップまで持続」だと、文と文の間の自然なポーズ（0.5〜1.0秒）の上に
前のテロップが居座り、「テロップは出ているのに声が無い」状態になる。
カットで詰めきれない短いポーズ（<1秒）でこれが起きるため、
**発話終わり+0.4秒でキャップ**して、無音区間にはテロップを残さない。

**★同期の鉄則（まさきFB反映版）:**
- **start = round・先出し0** → テロップは音声の頭ぴったり（早すぎ＝NG。100ms先出しは気になると判明）
- **end = ceil**（遅い側） → テロップは音声より早く消えない
- SEもstartに置く＝音声頭に揃う。テロップが早いとSEも早く鳴って気になるため、ここを最優先で合わせる。

**なぜ「持続表示モード」にしたか:**
従来は word.end で消える設計だったが、テロップが頻繁に出たり消えたりして
画面がチカチカし、視聴者の目が疲れる問題があった。
「常に何かしらテロップが出ている」状態にすることで:
- フェードイン・アウトの繰り返しを排除（後述のアニメーションルールと連動）
- 次のテロップが出る瞬間まで前のテロップを読み続けられる
- TV字幕に近い、落ち着いた視聴体験

**なぜ先出しバッファが必要か:**
Whisperのワードタイムスタンプには100-300msの誤差がある。
バッファなしだと「話し始めてからテロップが出る」遅延が発生し、
視聴者に違和感を与える。先出しすることで「テロップと音声が同時」に感じる。

### 2-5. 並列処理

- セグメント数が10以上 → Agentツールで3並列
- 10未満 → 直列実行

### 2-6. フォールバック

LLMが不正なJSONを返した場合 → 機械的分割にフォールバック:
- 句読点で分割
- 最大文字数で分割
- 助詞の後で分割（正規表現: `/(を|に|で|が|は|と|も|から|まで|より)/`）

---

## Phase 2.5: cutDataリマッピング（★必須・スキップ不可）

**cutData.ts が存在する場合、全テロップのフレームを再生タイムラインに変換する。**
このフェーズを飛ばすと音声とテロップが確実にズレる。

### 2.5-1. リマッピングの必要性

```
transcript_fixed.json の word.start/end → 元動画のタイムスタンプ（ms）
cutData.ts → 元動画の区間 → 再生タイムラインへのマッピング

テロップフレームは「再生タイムライン」で表示されるため、
元動画タイムスタンプから直接変換したフレームでは
カット分だけズレが蓄積する。
```

### 2.5-2. リマッピングアルゴリズム

```typescript
function mapOriginalToPlayback(originalFrame: number, cutData: CutSegment[]): number {
  for (const seg of cutData) {
    if (originalFrame >= seg.originalStart && originalFrame <= seg.originalEnd) {
      const offset = originalFrame - seg.originalStart;
      return seg.playbackStart + offset;
    }
  }
  // どのセグメントにも属さない場合（カット区間内）
  // → 直前のセグメントの playbackEnd を返す
  for (let i = cutData.length - 1; i >= 0; i--) {
    if (originalFrame > cutData[i].originalEnd) {
      return cutData[i].playbackEnd;
    }
  }
  return 0;
}
```

### 2.5-3. 適用手順

```
1. cutData.ts を読み込む
2. 全テロップの startFrame / endFrame に mapOriginalToPlayback() を適用
3. titleData の startFrame / endFrame にも同様に適用
4. 適用後、フレーム順序の再検証
```

### 2.5-4. cutData が存在しない場合

カットなし（cutData.ts が存在しないか、セグメント1つで全区間保持）の場合、
リマッピングは不要。元動画タイムスタンプ → フレーム変換のみ。

### 2.5-5. 検証（必須）

```
リマッピング後、以下を確認:
- 全テロップの startFrame < endFrame
- startFrame >= 0
- endFrame <= TOTAL_FRAMES（カット後）
- テロップの時系列順序が保持されている
- サンプル5件を手動検算: word.start(ms) → originalFrame → playbackFrame == telopStartFrame
```

---

## Phase 3: 読了時間チェック

### 3-1. 読了時間の計算

```
必要表示時間 = テキスト文字数 / 読了速度（文字/秒）

youtube: 「ポイ活サイトのモッピーとハピタス」(16文字) → 16/7 = 2.3秒
short:   「ポイ活サイトのモッピーとハピタス」(16文字) → 16/5 = 3.2秒
```

### 3-2. 表示時間の調整

```
┌─────────────────────────────────────────────────────┐
│ 計算した必要表示時間 vs 実際のword区間                   │
│                                                      │
│ 実際 > 必要 → そのまま（音声に余裕がある）              │
│ 実際 < 必要 → 以下の対応:                              │
│   a. endFrameを延長（次のテロップのstartFrameまで）     │
│   b. それでも足りない → テロップを2つに分割              │
│   c. 分割できない → 最小表示時間を保証して次に重ねる     │
└─────────────────────────────────────────────────────┘
```

### 3-3. テロップ密度チェック

```
動画全体の表示率 = テロップ表示フレーム数 / TOTAL_FRAMES

推奨: 60-85%（常に表示ではなく、息つく間がある）
90%超: 警告「テロップが多すぎます。間を空けることを推奨」
50%未満: 警告「テロップが少ないかもしれません」
```

---

## Phase 4: スタイル自動割り当て

### ★ 大原則: アニメーションは「SEが付くテロップ」だけ

視聴体験を落ち着かせるため、**アニメーションは emphasis / warning / success のみ**。
normal は **必ず `animation: 'none'`**（ハードカット表示）にする。

これは SE スキルの配置ルールと完全連動している:
- emphasis / warning / success → **SEあり + アニメーションあり**（注意を引きたい瞬間）
- normal → **SEなし + アニメーションなし**（読み続けてもらう瞬間）

この一致により「目がチカチカする問題」を排除し、必要な瞬間だけ視聴者の意識を引き戻す。

### ★ 大原則2: アニメーションは「入口だけ」、出口は無し（ハードカット）

アニメーション付きテロップ（emphasis/warning/success）でも、
**フェードイン・スライドインなど入口の演出だけを適用し、出口（フェードアウト）は一切付けない。**

理由: 持続表示モード（endFrame = 次のstartFrame）では、出口にフェードアウトを入れると
「消える数フレーム前から薄くなる」＝テロップが音声より早く消えて見える。
出口をハードカット（opacity=1のまま次のテロップへ瞬時に切替）にすると、
表示時間いっぱいまで読めて、かつ音声と完全に一致する。

**実装（Telop.tsx）:**
- opacity は `interpolate(localFrame, [0, fadeIn], [0, 1], {extrapolateRight:'clamp'})` ＝ フェードインのみ、その後 1 を維持
- 出口フェードアウト（`... duration, [0,1,1,0]`）は使わない
- スライド/charByChar も入口で settle 後そのまま維持（出口で動かさない）

### 4-1. スタイル配分テーブル

| 判定条件 | style | animation | 比率目安 | template |
|---------|-------|-----------|---------|---------|
| 通常の文 | normal | **none**（必須） | 60-70% | 2 |
| 疑問文（？） | normal | **none**（必須） | 5-10% | 2 |
| 強調キーワード含む | emphasis | slideIn | 10-15% | 1 or 6 |
| ネガティブ表現 | warning | slideIn | 5-10% | 4 or 5 |
| ポジティブ表現 | success | fadeOnly | 5-10% | 3 |
| 最重要メッセージ | emphasis | charByChar | 1-3% | 1 |

### 4-2. キーワード辞書

**強調（emphasis）:**
```
重要, ポイント, すごい, 注目, 革命, 最強, 必見, 衝撃, 驚き,
本質, 秘密, 真実, 鍵, コツ, 裏技, 必須, 絶対
+ project-config.json の notes のキーワード
```

**ネガティブ（warning）:**
```
問題, 失敗, 難しい, 危険, 注意, やばい, 最悪, 損, 無駄,
間違い, リスク, 落とし穴, 罠, 搾取, 地獄
```

**ポジティブ（success）:**
```
解決, 成功, できる, 簡単, 効果, 結果, 実現, 達成, 完成,
自動化, 効率, 時短, 無料, 利益, 成長
```

### 4-3. アニメーション多様化ルール（★最重要・emphasis / warning / success のみ適用）

**normal は常に `'none'`。** 以下はアニメーション付きテロップに対する選び方。

#### ★ 大原則: 同じアニメーションを連続させない

「全部 fadeOnly」「全部 slideIn」のような単調な割当は禁止。
**直前2個と同じアニメーションを選ばない**ことを最優先制約とする。

#### 4-3-A. 使えるアニメーション9種（用途タグ付き）

| アニメーション | 動きの強さ | 雰囲気 | おすすめstyle |
|---|---|---|---|
| `none` | 静止 | 落ち着き | normal（強制） |
| `fadeOnly` | 弱 | しっとり | success・落ち着いた emphasis |
| `fadeFromLeft` | 弱 | やわらか | emphasis・success |
| `fadeFromRight` | 弱 | やわらか | emphasis・success |
| `fadeBlurFromBottom` | 中 | 上品 | warning・emphasis |
| `slideFromLeft` | 中 | テンポ | emphasis・リスト項目 |
| `slideIn`（上から） | 中 | 注目 | emphasis・warning |
| `slideLeftFadeBlur` | 強 | 派手 | emphasis（最重要）・エンタメwarning |
| `charByChar` | 強 | インパクト | emphasis（決め台詞）・エンタメ |

#### 4-3-B. style別アニメーションプール（ローテーション元）

各 style ごとに**3〜5種のプール**から循環選択する。

| style | 標準プール |
|-------|-----------|
| emphasis | `slideIn` → `slideFromLeft` → `fadeFromRight` → `fadeBlurFromBottom` → `charByChar` |
| warning | `fadeBlurFromBottom` → `slideIn` → `slideLeftFadeBlur` → `fadeFromRight` |
| success | `fadeFromLeft` → `fadeOnly` → `fadeFromRight` → `slideFromLeft` |

#### 4-3-C. 割当アルゴリズム

```
for 各 emphasis/warning/success テロップ in 時系列順:
    pool = style別プール（4-3-B）
    pool = トーン別フィルタ（4-3-D で絞り込み）
    # 連続禁止
    pool = pool から「直前2個と同じアニメーション」を除外
    # それでも空なら直前1個との重複だけ避ける
    if pool 空: pool を再構築し、直前1個のみ除外
    # ローテーション
    animation = pool[(該当styleの累積出現回数) % len(pool)]
```

これにより、**同一styleのテロップが続いても毎回違うアニメーション**が選ばれる。

#### 4-3-D. トーン別の絞り込み（プールからどれを使うか）

トーンに応じて「強い動き」を含むかどうかをフィルタする。

| トーン | charByChar | slideLeftFadeBlur | プールの傾向 |
|--------|-----------|-------------------|------------|
| プロフェッショナル | 除外 | 除外 | fade系中心・控えめ |
| 教育的 | 除外 | 除外 | fade系中心・落ち着き |
| カジュアル | たまにOK | 含む | バランス |
| エンタメ | 積極使用 | 積極使用 | 動きを最大化 |

**最重要メッセージのみ特例:**
- emphasis テロップで、テキストに「絶対」「最強」「必見」「衝撃」が含まれる場合
  → トーンに関わらず `charByChar` を許可（1動画あたり最大2回まで）

#### 4-3-E. リスト項目の一貫性（特例）

「1つ目」「2つ目」「3つ目」のような**並列リスト**は、
あえて**同じアニメーションで揃える**と統一感が出る。

```
判定: テロップテキストが /[0-9一二三四五]つ目|第[0-9]/ にマッチ
処理: 連番リスト内では同じアニメーションを使う（連続禁止ルールの例外）
推奨: slideFromLeft（左から並ぶイメージ）
```

### 4-4. 句読点の除去

テロップ表示用テキストから句読点を除去:
```
「今日は、AIについてお話しします。」
→ 「今日はAIについてお話しします」

例外: ！？は残す（感情表現として有効）
```

---

## Phase 5: タイトルデータ生成

transcript_fixed.json の `segments` を俯瞰し、話題の転換点を検出。
（フレーム計算と書き出しは align_telop.py が行う。ここで決めるのは
スタイル割当.json の `titles` 配列 = テキストと切替位置の word_start_ms だけ）

### 実証済みの構成パターン（解説動画の正本・supermovie-0609）
```json
"titles": [
  { "text": "スキルを育てる運用",       "word_start_ms": null },   // 冒頭=動画テーマ（frame 0）
  { "text": "①スキルは育てるもの",     "word_start_ms": 45080 },  // 「ではまず1つ目」の頭
  { "text": "②小さく始める階段",       "word_start_ms": 131560 }, // 「では続いて2つ目」の頭
  { "text": "③全部スキル化しなくていい", "word_start_ms": 228880 }, // 「では3つ目」の頭
  { "text": "④7章のまとめ",            "word_start_ms": 287380 }  // 「最後4つ目」の頭
]
```
- 冒頭1件 + 章見出し（①②③…の連番プレフィックス）が基本形
- 切替位置 = 章の導入フレーズ（「ではまず1つ目」「続いて」「次に」「最後に」等）の先頭ワードの start(ms)
- word_start_ms は該当ワードの start を transcript_fixed.json からそのまま転記（±200msまで自動で最近傍補正）

### 分割基準
- 話題の切り替わり（「次に」「それでは」「ここからは」等）
- 目安: 冒頭1 + 章3〜6（多すぎると煩い）

### タイトル生成ルール
- 最大15文字
- キャッチーかつ内容を的確に表現（話者の章タイトル発言があればそれに忠実に）

---

## Phase 6: ファイル書き込み＆検証

### 6-1. telopData.ts 出力

```typescript
import type { TelopSegment } from './telopTypes';

export const FPS = 60;
export const TOTAL_FRAMES = 41447;

export const telopData: TelopSegment[] = [
  {
    id: 1,
    startFrame: 196,
    endFrame: 253,
    text: 'それでは今回は',    // ★1行・改行なし
    style: 'normal',
    animation: 'fadeOnly',
  },
  {
    id: 2,
    startFrame: 253,
    endFrame: 412,
    text: 'ポイ活サイトのモッピーとハピタス',  // ★1行・改行なし
    style: 'normal',
    animation: 'fadeOnly',
  },
];
```

**保存先:** `src/テロップテンプレート/telopData.ts`

### 6-2. titleData.ts 出力

**保存先:** `src/Title/titleData.ts`

### 6-3. バリデーション

| チェック項目 | 条件 | 失敗時の対応 |
|-------------|------|------------|
| フレーム順序 | startFrame < endFrame | endFrameを+1補正 |
| 持続表示+無音キャップ | endFrame[i] == min(startFrame[i+1], speechEnd+12f) | 再キャップ |
| 範囲超過 | endFrame ≤ TOTAL_FRAMES | カット |
| 1行文字数 | ≤ 18文字（youtube・ハード） | 最後の助詞で2分割 |
| 改行なし | \n が含まれていない | \n を除去して分割し直し |
| ID連番 | 1から連番 | 採番し直し |
| テキスト空 | text.length > 0 | 空エントリ削除 |
| 句読点除去 | 。、が残っていない | 除去 |
| 平均文字数 | 11〜15文字 | 範囲外は警告 |
| 同期 | startFrame = round(word.start) **+2f**（旧-3f/先出しは廃止） | 未適用なら再計算 |
| cutDataリマッピング | cutData存在時、全フレームがplayback timeline | 未適用なら Phase 2.5 を実行 |
| 同期スポットチェック | テロップ5件をword.startと照合、差が5フレーム以内 | 5フレーム超のズレがあればリマッピング再検証 |
| ★無音上テロップ検査（必須） | 全テロップで `endFrame - speechEnd(playback) ≤ 0.55秒` | 超過は endFrame=min(nextStart, speechEnd+12f) で再キャップ。「声が無いのにテロップが出る」箇所をゼロにしてから出力する |
| **normal アニメ統一** | **style:'normal' は全件 animation:'none'** | **'none'以外は強制的に'none'に矯正** |
| **アニメ多様性** | **emphasis/warning/success 内で「同一アニメ3連続」がない** | **連続箇所をプール内の別アニメに差し替え（4-3-Cアルゴリズム再実行）** |
| **アニメ偏り率** | **同一アニメが全体の50%超を占めない**（emphasis/warning/success合算） | **50%超なら差し替えで分散** |

---

## 完了時の報告フォーマット

```
✅ テロップ＆タイトル生成完了

📝 テロップ: <N>個（全て1行テロップ）
   - normal: <n> / emphasis: <n> / warning: <n> / success: <n>
📏 平均文字数: <X>文字/テロップ（目標: 13文字前後）
⏱️ 平均表示時間: <X>秒/テロップ
📊 テロップ密度: <X>%
🏷️ タイトル: <N>セグメント

次のステップ:
→ /supermovie-image-gen で画像生成
```

---

## エラーハンドリング

| エラー | 対応 |
|--------|------|
| transcript_fixed.json なし | `/supermovie-transcribe` → `/supermovie-transcript-fix` を促す |
| transcript.json のみ（fix未実施） | `/supermovie-transcript-fix` を促す |
| words 配列が空 | 音声なし動画の可能性を通知 |
| project-config.json なし | デフォルト（youtube, プロフェッショナル）で続行 |
| LLM意味分割が不正JSON | 機械的分割にフォールバック |
| 読了時間が全体的に不足 | テロップ数を減らす提案 |
| テロップ密度 90%超 | 間引き提案（低優先度テロップを非表示に） |
| 平均文字数 20超 | 分割が足りない。LLMプロンプト調整 |
| 平均文字数 8未満 | 分割しすぎ。統合を検討 |

---

## 付録A: テロップ分割パターン集（実データ分析結果）

> ⚠️ この付録の実例には18字超のものが混在する（18字ハード化以前のデータ）。
> 分割位置の感覚を掴む参考であり、**19字以上の例は現在は最後の助詞で2分割する**。

### 語尾パターン別の出現頻度

326件の実テロップから分析した分割パターン:

```
助詞で区切り(は/が/を/に/で/と):  124件 (38%)  ← 最多
その他(名詞/動詞止め):            66件 (20%)
文末(ます/です):                  44件 (13%)
文末(ですね/ですよ):              34件 (10%)
接続(して/ていて/たり):           16件 (5%)
引用(っていう/という):            15件 (5%)
理由(ので/から):                  14件 (4%)
逆接(けど/けれど):                13件 (4%)
```

### 文字数分布（実績値）

```
 5〜 9文字:  66件 (20%)  ← 短いフレーズ
10〜13文字: 101件 (31%)  ← ★コアゾーン
14〜16文字:  90件 (28%)  ← ★コアゾーン
17〜19文字:  51件 (16%)  ← やや長め
20〜25文字:  18件 (6%)   ← 最長級
```

### 良い分割の具体例（30件）

```
「それでは今回は」（7文字）
「ポイ活サイトのモッピーとハピタス」（16文字）
「というテーマで解説していきたいと思います」（19文字）
「前回ポイ活は時間対効果最強の副業」（16文字）
「というテーマでお話しさせてもらったんですけど」（21文字）
「その中で仕組み化型と」（10文字）
「単発型っていう」（7文字）
「やる価値のあるポイ活の2種類を」（15文字）
「お伝えしましたよね」（9文字）
「今回はそのうちの」（8文字）
「単発型を実際にやろうとした時に」（15文字）
「じゃあ具体的にどこで」（10文字）
「案件を探せばいいの」（9文字）
「っていうところを」（8文字）
「しっかり押さえていく回になります」（15文字）
「結論からお伝えすると」（10文字）
「僕がみなさんにおすすめしたいのは」（16文字）
「モッピーとハピタスっていう」（13文字）
「2つのポイ活サイトなんですね」（14文字）
「両方とも僕が普段から使っていて」（15文字）
「子育て世帯の皆さんにも」（11文字）
「全く同じものをおすすめしています」（16文字）
「ただ初めて聞いた方からすると」（14文字）
「そもそもそれって安全なの」（12文字）
「とか怪しいサイトじゃないの」（13文字）
「なので今回はこの2つのサイトが」（15文字）
「どういう会社が運営していて」（13文字）
「安心して使えるのか」（9文字）
「っていうところから」（9文字）
「しっかり順番に見ていきたいと思います」（18文字）
```

---

## 連携マップ

```
/supermovie-init              ← ヒアリング → プロジェクト作成
    ↓
/supermovie-transcribe        ← 文字起こし（ローカル無料）
    ↓ transcript.json
/supermovie-transcript-fix    ← 誤字修正（辞書 + Claude LLM）
    ↓ transcript_fixed.json
/supermovie-cut               ← 不要区間カット（VAD + LLM分析）
    ↓ cutData.ts
/supermovie-subtitles         ← ★ここ: テロップ＆タイトル生成
    ↓ telopData.ts + titleData.ts
/supermovie-image-gen         ← 画像生成 + 配置データ
    ↓ insertImageData.ts
/supermovie-se                ← SE自動配置
    ↓
npm run dev                   ← プレビュー
```
