akari-video 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/capture-command.mjs +46 -0
  3. package/src/cli.mjs +4 -0
  4. package/src/media-command.mjs +17 -0
  5. package/src/repo-assets.mjs +31 -10
  6. package/src/runtime-diagnostics.mjs +0 -3
  7. package/vendor/.akari-capability-sources.json +3 -0
  8. package/vendor/docs/contract-2026-07-18-edit-json-v1-sources.md +3 -0
  9. package/vendor/docs/contract-2026-08-02-preview-parity.md +14 -0
  10. package/vendor/docs/contract-2026-08-23-captions-emphasis-words-v0.md +1 -0
  11. package/vendor/docs/contract-2026-08-28-gpu-export-v0.md +210 -10
  12. package/vendor/docs/contract-2026-08-28-osr-export-v0.md +19 -1
  13. package/vendor/docs/contract-2026-08-28-v2-audio-roles-v0.md +19 -0
  14. package/vendor/docs/contract-2026-08-29-capture-v0.md +121 -0
  15. package/vendor/docs/contract-2026-08-29-media-inspect-cli-v0.md +236 -0
  16. package/vendor/packages/akari-launcher/package.json +1 -1
  17. package/vendor/packages/akari-tools/README.md +10 -0
  18. package/vendor/packages/akari-tools/package.json +9 -0
  19. package/vendor/packages/edit-lint/src/edit-lint.mjs +213 -12
  20. package/vendor/packages/edit-store/lib/audio-schedule.d.ts +44 -1
  21. package/vendor/packages/edit-store/lib/audio-schedule.js +156 -0
  22. package/vendor/packages/edit-store/lib/webview-kernel.d.ts +1 -0
  23. package/vendor/packages/edit-store/lib/webview-kernel.js +199 -0
  24. package/vendor/packages/gpu-export/README.ja.md +80 -5
  25. package/vendor/packages/gpu-export/README.md +84 -5
  26. package/vendor/packages/media-bin/src/proxy-recipe.json +8 -0
  27. package/vendor/packages/media-bin/src/proxy-recipe.mjs +54 -0
  28. package/vendor/packages/media-bin/src/speech-atempo.mjs +190 -0
  29. package/vendor/packages/media-bin/test/proxy-recipe.test.mjs +50 -0
  30. package/vendor/packages/media-bin/test/speech-atempo.test.mjs +68 -0
  31. package/vendor/packages/overlay-runtime/README.md +29 -0
  32. package/vendor/packages/schemas/analysis.schema.json +89 -0
  33. package/vendor/packages/schemas/test/analysis-observations.test.mjs +56 -0
  34. package/vendor/skills/render-cut/SKILL.md +6 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,46 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import { resolveLauncherAssets } from "./repo-assets.mjs";
7
+
8
+ export async function runCaptureCommand(argv, options = {}) {
9
+ const logError = options.error ?? options.logError ?? ((line) => console.error(line));
10
+ const assets = options.assets ?? resolveLauncherAssets();
11
+ if (!assets.captureScript || !existsSync(assets.captureScript)) {
12
+ logError("akari capture の実行スクリプトが見つかりません。AKARI Video を再インストールしてください。");
13
+ return { exitCode: 1 };
14
+ }
15
+
16
+ const isHelp = argv.includes("--help") || argv.includes("-h");
17
+ if (!isHelp) {
18
+ const browser = await resolveBrowserDiagnostics(assets, options);
19
+ if (!await browser.findChromePath()) {
20
+ logError(await browser.describeChromeNotFound());
21
+ return { exitCode: 1 };
22
+ }
23
+ }
24
+
25
+ const spawn = options.spawn ?? spawnSync;
26
+ const result = spawn(process.execPath, [assets.captureScript, ...argv], {
27
+ stdio: "inherit",
28
+ cwd: options.cwd ?? process.cwd(),
29
+ });
30
+ return { exitCode: typeof result.status === "number" ? result.status : 1 };
31
+ }
32
+
33
+ async function resolveBrowserDiagnostics(assets, options) {
34
+ if (options.findChromePath && options.describeChromeNotFound) {
35
+ return {
36
+ findChromePath: options.findChromePath,
37
+ describeChromeNotFound: options.describeChromeNotFound,
38
+ };
39
+ }
40
+ const modulePath = join(assets.repoRoot, "packages", "render-cut", "src", "render-cut.mjs");
41
+ const module = await import(pathToFileURL(modulePath).href);
42
+ return {
43
+ findChromePath: options.findChromePath ?? module.findChromePath,
44
+ describeChromeNotFound: options.describeChromeNotFound ?? module.describeChromeNotFound,
45
+ };
46
+ }
package/src/cli.mjs CHANGED
@@ -23,6 +23,8 @@ import {
23
23
  } from './update-check.mjs';
24
24
  import { applySelfUpdate, isRunningFromAppDir, rollbackSelfUpdate } from './self-update.mjs';
25
25
  import { runChromeCommand } from './chrome-command.mjs';
26
+ import { runCaptureCommand } from './capture-command.mjs';
27
+ import { runMediaCommand } from './media-command.mjs';
26
28
  import { resolveRuntimePaths } from './runtime-diagnostics.mjs';
27
29
 
28
30
  /**
@@ -36,6 +38,8 @@ import { resolveRuntimePaths } from './runtime-diagnostics.mjs';
36
38
  */
37
39
  export async function run(args, options = {}) {
38
40
  if (args[0] === 'chrome') return runChromeCommand(args.slice(1), options);
41
+ if (args[0] === 'capture') return runCaptureCommand(args.slice(1), options);
42
+ if (args[0] === 'media') return runMediaCommand(args.slice(1), options);
39
43
 
40
44
  const log = options.log ?? ((line) => console.log(line));
41
45
  const assets = options.assets ?? resolveLauncherAssets();
@@ -0,0 +1,17 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ import { resolveLauncherAssets } from "./repo-assets.mjs";
4
+
5
+ export async function runMediaCommand(args, options = {}) {
6
+ const logError = options.logError ?? ((line) => console.error(line));
7
+ const assets = options.assets ?? resolveLauncherAssets();
8
+ const spawn = options.spawn ?? spawnSync;
9
+ if (!assets.mediaScript) {
10
+ logError("akari media の実行スクリプトが見つかりません。完全な AKARI Video を再導入してください:");
11
+ logError(" npm install -g akari-video");
12
+ return { exitCode: 1 };
13
+ }
14
+ const result = spawn(process.execPath, [assets.mediaScript, ...args], { stdio: "inherit" });
15
+ return { exitCode: typeof result?.status === "number" ? result.status : 1 };
16
+ }
17
+
@@ -33,6 +33,7 @@ const AUDIO_FETCH_SCRIPT_RELATIVE = path.join('packages', 'audio-library-setup',
33
33
  const ASSET_RESOLVER_CLI_RELATIVE = path.join('packages', 'asset-resolver', 'bin', 'akari-assets.mjs');
34
34
  const BEATMAP_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'beatmap.mjs');
35
35
  const PROBE_FRAME_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'probe-frame.mjs');
36
+ const CAPTURE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'capture.mjs');
36
37
  const RENDER_WHEN_IDLE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'render-when-idle.sh');
37
38
  const EYE_BAR_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'eye-bar.mjs');
38
39
 
@@ -53,6 +54,7 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
53
54
  const probeFrameScript = path.join(repoRoot, PROBE_FRAME_SCRIPT_RELATIVE);
54
55
  const renderWhenIdleScript = path.join(repoRoot, RENDER_WHEN_IDLE_SCRIPT_RELATIVE);
55
56
  const eyeBarScript = path.join(repoRoot, EYE_BAR_SCRIPT_RELATIVE);
57
+ const mediaScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'media.mjs');
56
58
 
57
59
  return {
58
60
  repoRoot,
@@ -66,24 +68,43 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
66
68
  assetResolverCliPath: existsSync(assetResolverCliPath) ? assetResolverCliPath : null,
67
69
  beatmapScript: existsSync(beatmapScript) ? beatmapScript : null,
68
70
  probeFrameScript: existsSync(probeFrameScript) ? probeFrameScript : null,
71
+ captureScript: existsSync(path.join(repoRoot, CAPTURE_SCRIPT_RELATIVE)) ? path.join(repoRoot, CAPTURE_SCRIPT_RELATIVE) : null,
69
72
  renderWhenIdleScript: existsSync(renderWhenIdleScript) ? renderWhenIdleScript : null,
70
- eyeBarScript: existsSync(eyeBarScript) ? eyeBarScript : null
73
+ eyeBarScript: existsSync(eyeBarScript) ? eyeBarScript : null,
74
+ mediaScript: existsSync(mediaScript) ? mediaScript : null
71
75
  };
72
76
  }
73
77
 
74
78
  /**
75
- * ランチャー実行時の同梱物解決: モノレポ checkout(開発時)→ vendor/(npm 配布時)の
76
- * 順で探し、最初に何かしら見つかったルートを採用する。テストからは candidateRoot /
77
- * vendorRoot を注入して両分岐を実ディレクトリ無しで検証できる。
79
+ * ランチャー実行時の同梱物解決: 資産ごとにモノレポ checkout(開発時)→ vendor/
80
+ * (npm 配布時)の順で探す。checkout が一部の資産しか持たないパッケージ構成でも、
81
+ * 欠けたフィールドだけを vendor から補完する。
78
82
  */
79
83
  export function resolveLauncherAssets({
80
84
  candidateRoot = DEFAULT_REPO_ROOT_CANDIDATE,
81
85
  vendorRoot = DEFAULT_VENDOR_ROOT
82
86
  } = {}) {
83
- const checkout = resolveRepoAssets(candidateRoot);
84
- const found = checkout.skillsSourceDir || checkout.templateDir || checkout.schemasSourceDir
85
- || checkout.doctorScript || checkout.scaffoldModulePath || checkout.creatorRootModulePath
86
- || checkout.beatmapScript || checkout.probeFrameScript || checkout.renderWhenIdleScript
87
- || checkout.eyeBarScript;
88
- return found ? checkout : resolveRepoAssets(vendorRoot);
87
+ const candidate = resolveRepoAssets(candidateRoot);
88
+ const vendor = resolveRepoAssets(vendorRoot);
89
+ const candidateHasAssets = Object.entries(candidate)
90
+ .some(([key, value]) => key !== 'repoRoot' && value !== null);
91
+
92
+ return {
93
+ repoRoot: candidateHasAssets ? candidateRoot : vendorRoot,
94
+ skillsSourceDir: candidate.skillsSourceDir ?? vendor.skillsSourceDir,
95
+ templateDir: candidate.templateDir ?? vendor.templateDir,
96
+ schemasSourceDir: candidate.schemasSourceDir ?? vendor.schemasSourceDir,
97
+ doctorScript: candidate.doctorScript ?? vendor.doctorScript,
98
+ scaffoldModulePath: candidate.scaffoldModulePath ?? vendor.scaffoldModulePath,
99
+ creatorRootModulePath: candidate.creatorRootModulePath ?? vendor.creatorRootModulePath,
100
+ audioFetchScriptPath: candidate.audioFetchScriptPath ?? vendor.audioFetchScriptPath,
101
+ assetResolverCliPath: candidate.assetResolverCliPath ?? vendor.assetResolverCliPath,
102
+ beatmapScript: candidate.beatmapScript ?? vendor.beatmapScript,
103
+ probeFrameScript: candidate.probeFrameScript ?? vendor.probeFrameScript,
104
+ // 既存の部分資産 fixture に capture marker が無いため、未解決時だけキー自体を省く。
105
+ ...(candidate.captureScript ?? vendor.captureScript ? { captureScript: candidate.captureScript ?? vendor.captureScript } : {}),
106
+ renderWhenIdleScript: candidate.renderWhenIdleScript ?? vendor.renderWhenIdleScript,
107
+ eyeBarScript: candidate.eyeBarScript ?? vendor.eyeBarScript,
108
+ mediaScript: candidate.mediaScript ?? vendor.mediaScript
109
+ };
89
110
  }
@@ -164,9 +164,6 @@ export async function resolveDoctorReport(options = {}) {
164
164
 
165
165
  export async function resolveGpuExportAvailability(options = {}) {
166
166
  const platform = options.platform ?? process.platform;
167
- if (platform !== 'darwin') {
168
- return { available: false, reason: 'GPU hardware export v0 is available on macOS only', launcher_tier: null };
169
- }
170
167
  try {
171
168
  const resolver = options.resolveGpuLauncher
172
169
  ?? (await import('../../gpu-export/src/runner.mjs')).resolveGpuLauncher;
@@ -46,9 +46,12 @@
46
46
  "docs/contract-2026-08-28-osr-export-v0.md",
47
47
  "docs/contract-2026-08-28-v2-approximation-ledger.md",
48
48
  "docs/contract-2026-08-28-v2-audio-roles-v0.md",
49
+ "docs/contract-2026-08-29-capture-v0.md",
50
+ "docs/contract-2026-08-29-media-inspect-cli-v0.md",
49
51
  "packages/akari-launcher/package.json",
50
52
  "packages/akari-launcher/README.md",
51
53
  "packages/akari-tools/package.json",
54
+ "packages/akari-tools/README.md",
52
55
  "packages/analysis-report/package.json",
53
56
  "packages/analysis-report/README.md",
54
57
  "packages/asset-resolver/package.json",
@@ -42,6 +42,8 @@
42
42
  | `cuts[].src` | v1 では必須。`sources[].id` を参照する |
43
43
  | `cuts[].in`, `cuts[].out` | 対象ソースの秒。`0 <= in < out` を満たす |
44
44
 
45
+ `sources[].proxy` の生成規格は [プレビュー用プロキシの規格](./contract-2026-08-02-preview-parity.md#55-プレビュー用プロキシの規格) に従う。
46
+
45
47
  参照は path ではなく安定した `id` で行う。これにより素材の差し替えや path 変更で cut や
46
48
  サイドカーの参照が壊れない。JSON Schema は将来の任意フィールドを許容する tolerant reader とし、
47
49
  既知フィールドの型、version ごとの必須形、`source` / `sources[]` の排他を検証する。
@@ -76,6 +78,7 @@ source 秒アンカー規則の対象外である。
76
78
  - `review.json` の `annotations[]` は任意フィールド `src` を持てる。値は `sources[].id` への参照で、
77
79
  `sourceT` または `sourceRange` と組にして source 座標を表す
78
80
  - `src` の省略は単一ソース互換を意味する
81
+ - 字幕の重なり(`captions.overlap`)と並び順(`captions.order`)の判定は `src` ごとに行う
79
82
 
80
83
  analysis サイドカーは素材単位のままとし、構造は変更しない。参照時に `src` から source path を
81
84
  解決する。
@@ -195,6 +195,7 @@ SHA-256 一致を要求する。GPU は同一マシン一致率を診断値と
195
195
  | GPU 直結・engine 区間 | OSR decode 比較の per-frame MAD ≤ 1.0 | 超過は不合格 |
196
196
  | GPU 直結・字幕 | cue 代表 5 時刻の下半分 MAD ≤ 1.0 | 超過は不合格 |
197
197
  | GPU 直結・3D | 3D active 区間 MAD ≤ 1.0 | 超過は不合格 |
198
+ | GPU 直結・DOM 層 | overlay 外接矩形内 MAD ≤ 1.0、t=0 を含む代表 5 時刻 | いずれかの超過または sentinel 不一致は不合格 |
198
199
 
199
200
  OSR の比較は H.264 を再 decode した画像ではなく捕捉時の raw BGRA を使う。ソフト描画の検収と GPU の
200
201
  診断値を混同しない。
@@ -253,3 +254,16 @@ lint 実行系が見つからない場合は **fail-open**(2026-08-02 オー
253
254
  ペン描画の単一正本は `packages/pen-visuals` の `PEN_TUNING` と描画プリミティブである。器や overlay
254
255
  sheet が独自の補間、太さ、透明度、消去規則を持ってはならない。フェード時間は **600ms** を正とする
255
256
  (2026-08-02 オーナー裁定)。
257
+
258
+ ### 5.5 プレビュー用プロキシの規格
259
+
260
+ frame-engine がランダムアクセスするプレビュー用プロキシは、H.264 High Profile の 8bit
261
+ `yuv420p`、GOP 1 秒以下、B フレームなし、faststart とする。GOP はソースの実測 fps を丸めた
262
+ フレーム数を使い、`-g <fps> -keyint_min <fps> -sc_threshold 0 -bf 0` を指定する。変換後も尺と
263
+ コマ数はソースと一致させる。
264
+ 29.97 fps の GOP は 30 コマで 1.001 秒になるため、doctor / lint などで機械照合するときの閾値は 1.05 秒に置く。
265
+
266
+ 生成経路は shell の HEVC フォールバックと preview-server の HEVC プロキシの 2 系統であり、
267
+ いずれも `packages/media-bin/src/proxy-recipe.mjs` を唯一の定義として使う。レシピ版
268
+ `gop1s-v1` は shell のキャッシュキーと preview-server の出力名へ含め、旧規格のキャッシュを
269
+ 次回参照時に再利用しない。
@@ -74,3 +74,4 @@
74
74
  比較できない `t_end > t_start` と、配列内の `id` 一意性は `validate-captions.mjs` が検査する。
75
75
  語と `captions[].words[]` の実測値の突き合わせは v1 契約と同じく書き手の規律であり、静的検証では
76
76
  行わない。
77
+ edit-lint は object ルートの `emphasis_words` を受理し、同じ規則で検証する。
@@ -16,7 +16,7 @@
16
16
 
17
17
  | 分類 | 適格 | 意味 |
18
18
  |---|---|---|
19
- | `same` | はい | 静的 HTML または対応済み字幕。起動時に 1 回だけスプライト化する |
19
+ | `same` | はい | 静的 HTML は起動時、対応済み字幕は unit の初回活性時に 1 回だけスプライト化する |
20
20
  | `three` | はい | JSON の宣言型 3D scene と描画先 canvas を持つ overlay。毎コマ Three.js canvas を更新する |
21
21
  | `degraded` | いいえ | raster 自体は可能でも live DOM と同じ時間変化を保証できない |
22
22
  | `unsupported` | いいえ | v0 の表現範囲外であり、正しい完成画を生成できない |
@@ -27,14 +27,68 @@ CSS animation/transition/keyframes、filter/mask/clip-path 等を検出する。
27
27
  `<script type="application/json" data-akari-3d-scene>` 宣言を属性順にかかわらずちょうど 1 個持ち、
28
28
  それ以外の script と video を持たない宣言型 3D は `three` とする。3D の描画先である canvas は許可する。
29
29
 
30
- 字幕は cue ごとに 1 回 rasterize し、出現・loop・消失を解析的な opacity と中心基準 affine 変換で
31
- 再現する。次は v0 `unsupported` とする。
30
+ 字幕は cue ごとに rasterize し、出現・loop・消失を解析的な opacity と中心基準 affine 変換で
31
+ 再現する。v2 では `words[]` を持つ karaoke、pop、reveal、reveal-word と `emphasis_words`
32
+ 語矩形タイルとして GPU-native に合成する。次は引き続き `unsupported` とする。
32
33
 
33
- - `words[]` を持つ karaokepop、reveal、reveal-word 等の語単位表示。
34
- - `emphasis_words`。
34
+ - 同一 unit karaoke の色補間と、pop / one-char-bang / one-char-jumble / size-pulse の幾何変形が混在するもの。
35
+ - 語矩形タイルで再現できない縦書きの語単位字幕。
35
36
  - 未知の motion、および clip-path を使う push/typewriter/wipe/glitch。
36
37
  - `transform-origin: top center` を必要とする swing。
37
38
 
39
+ ### 2.1 語単位字幕(v2)
40
+
41
+ 語単位字幕の意味論は `packages/render-cut/src/captions.mjs` が生成する DOM と CSS を正本とする。
42
+ GPU 出口はその DOM を別実装で組み直さず、同じ DOM から `getClientRects()` で各語の矩形を採寸する。
43
+ 1 語が改行をまたいで複数矩形を返す場合は全矩形を保持する。縦長出力で style 無指定かつ
44
+ `words[]` があり複数行になる cue は、正本と同じ行分割関数で reveal へ自動昇格する。
45
+ 採寸は独立した DOM root の挿入ごとに行い、全 variant の全測定値が 2 回連続で厳密一致するまで
46
+ 測り直す。上限は 32 回とし、収束しなければ `caption-measure-unstable` を理由コードと warning に残して
47
+ fail-closed とする。HW で max 6、soft で max 7 だった実測に対する余裕として 32 回を採る。
48
+ 許容差、平均、丸めによって走間の揺らぎを隠さない。
49
+
50
+ | 対象 | 毎コマの状態 |
51
+ |---|---|
52
+ | karaoke | 語の start まで基本色、start〜end は語全体の色を linear 補間、end 以後は highlight 色 |
53
+ | pop | start から 0.2 秒、語矩形中心で `translateY(0→-0.08em→0)` と `scale(1→1.12→1)` |
54
+ | reveal-word | start から 0.01 秒で opacity 0→1 |
55
+ | reveal | 行群を 0/12/99.99/100% の opacity・translateY keyframe で順送りし、前群は終了時に消す |
56
+ | one-char-bang / one-char-jumble | 文字ごとに opacity 0→1、scale 1.6→1。jumble の静的変形はラスタへ保持 |
57
+ | size-pulse | 語矩形中心で scale 1→1.25→1 |
58
+ | 色・太さ・縁取りだけの emphasis | 静的な基本ラスタへ焼き込み、毎コマの状態評価はしない |
59
+
60
+ karaoke は左から右へのワイプではない。正本の keyframes は `color` の from/to だけなので、基本色と
61
+ highlight 色の 2 ラスタを語タイル内で時間比率により mix する。CSS timing function は keyframe 区間ごとに
62
+ 適用し、pop と size-pulse の 0〜50% / 50〜100% をそれぞれ ease してから補間する。
63
+
64
+ ラスタ単位を unit と呼ぶ。通常は cue 1 個、reveal は重なった `.akari-caption__reveal-group` 1 個を
65
+ unit 1 個とする。unit ごとのラスタは最大 2 枚で、色モードは基本色 / highlight 色、幾何モードは
66
+ プレート背景 / 透明背景の文字、状態なしは基本 1 枚だけを持つ。2 状態 DOM の全語矩形は各成分差
67
+ 0.01 px 以下であることを起動時に検査し、超過は fail-closed とする。ラスタは unit ごとに 1 回だけ作り、毎コマは
68
+ 行ストリップと語境界でフレームを隙間なく分割した整数タイルを配列順に合成する。3 枚目や毎コマ rasterize は行わない。
69
+
70
+ ラスタ SVG はフレーム全面ではなく、出力幅を維持した字幕帯だけの `viewBox` を持つ。開始時刻順の
71
+ 連続する最大 8 unit、かつ全バンド高 4096 px 以下を 1 バッチとし、各 unit の 1〜2 状態を縦方向の
72
+ バンドとして 1 枚の SVG に積み、デコードはバッチにつき 1 回だけ行う。variant CSS は
73
+ `data-akari-band` ごとにスコープし、埋め込みフォントの `@font-face` は SVG 内に 1 本だけ置く。
74
+ 初めて必要になった unit のバッチをまとめて登録する一方、GPU texture は活性区間の終了時に unit ごとに
75
+ 解放し、バッチのために寿命を延ばさない。upload 後の切り出し用 CPU canvas とデコード画像も破棄する。
76
+
77
+ SVG の入力は data URL に固定する。Blob URL と同一オリジン HTTP URL は SVG 内フォントを含む canvas を
78
+ 汚染し、`getImageData` だけでなく `texImage2D(canvas)` も `SecurityError` になるため使用しない。
79
+ フォント data URL は `encodeURIComponent` 済みの文字列を 1 度だけ作って再利用し、生の base64 を
80
+ キャッシュしない。
81
+
82
+ 適格な cue は理由 `words-native` とし、receipt に `mode`(`sprite` / `words-native`)、style、unit 数、
83
+ 語数、ラスタ枚数、バンド数、タイル数と `captionLayoutMaxDeltaPx`、採寸試行の count / p50 / max、
84
+ バッチ実測、字幕ラスタ合計時間を記録する。未知 style は
85
+ `caption-style-unsupported:<value>`、色と幾何の混在は `words-native-color-and-geometry-mixed` とする。
86
+
87
+ 検収は style 5 種 × 各 5 時刻について GPU / OSR decode の画面下 1/4 MAD 1.0 以下、語境界の対比画像
88
+ 6 枚以内の目視、語状態評価と合成の追加コスト中央値 1 ms/コマ以下、hardware / software の指定 fixture
89
+ 2 走一致、製品経路の読み戻し 0 を要求する。性能 gate は cue ラスタ p50 500 ms 以下、karaoke 44 cue の
90
+ akari-video-pv 18 ms/コマ以下、小 fixture(360 コマ・3 cue)の RSS peak 900 MB 以下とする。
91
+
38
92
  `--engine auto` は macOS で全件適格なら `gpu`、不適格なら `osr` を選ぶ。他 OS の `auto` は従来の
39
93
  選択を維持する。明示 `--engine gpu` と不適格の組み合わせは理由を全件表示して fail-closed とし、
40
94
  黙って OSR へ変更しない。GPU launcher が利用できない明示指定も fail-closed とする。
@@ -67,7 +121,8 @@ raw frame 読み戻しではない。
67
121
 
68
122
  映像は H.264 `avc1.640028`、2 秒ごとの keyframe とし、製品は hardware preference、
69
123
  `--soft` は software preference を指定する。ビットレートは render-cut の quality プリセットにある
70
- VideoToolbox 値を正本とし、`high = 12 Mbps`、`standard = 8 Mbps`、`light = 5 Mbps` とする。
124
+ GPU ビットレート値(mac では VideoToolbox 用の値と共用)を正本とし、`high = 12 Mbps`、
125
+ `standard = 8 Mbps`、`light = 5 Mbps` とする。
71
126
  `--bitrate` の明示値は quality より優先する。`master` は VideoToolbox ビットレートを宣言しないため、
72
127
  GPU 出口では `--bitrate` が無ければ理由付きで fail-closed にする。
73
128
 
@@ -108,6 +163,10 @@ GPU と OSR の decode 比較は、engine-only 区間の per-frame MAD 1.0 以
108
163
  "video_reencode": false
109
164
  },
110
165
  "gpu": {
166
+ "platform": "win32",
167
+ "chromium": "140.0.0.0",
168
+ "renderer": { "vendor": "NVIDIA Corporation", "renderer": "ANGLE (NVIDIA GeForce RTX)" },
169
+ "encoder_support": { "prefer-hardware": true, "prefer-software": false },
111
170
  "encoder": "WebCodecsH264Encoder",
112
171
  "hardware": "prefer-hardware",
113
172
  "uploadPath": "direct",
@@ -125,10 +184,151 @@ GPU と OSR の decode 比較は、engine-only 区間の per-frame MAD 1.0 以
125
184
  OSR receipt と同じ warning/hard-stop 語彙を使う。`--engine osr` と `--engine gpu` は receipt 以外の
126
185
  最終成果物パス・命名と `.akari/render.json` の置き場を共有する。
127
186
 
128
- ## 8. v0 の限界
187
+ `gpu.platform` と `gpu.chromium` Electron main process の実行環境、`gpu.renderer` は renderer process
188
+ で `WEBGL_debug_renderer_info` から取得した vendor / renderer(取得不能なら `null`)を記録する。
189
+ `gpu.encoder_support` は製品と同じ H.264 config の `hardwareAcceleration` だけを差し替えた
190
+ `prefer-hardware` / `prefer-software` の対応可否を記録し、取得不能なら `null` とする。WebCodecs は
191
+ 実際に使われた encoder 実装を露出しないため、「hardware encoder が使われた」という主張は
192
+ `encoder_support` と書き出し速度を組み合わせて裏取りする。
193
+
194
+ ## 8. v0 / v2 の限界
129
195
 
130
- - karaoke、語単位 style、`emphasis_words` は GPU-native glyph/word texture 実装まで非対応。
196
+ - 語矩形で表せない演出、色補間と幾何変形が同居する cue、縦書きの語単位字幕は glyph atlas 等の次段が必要。
131
197
  - 動的自由 HTML は OSR または事前ベイクが必要。
132
- - Windows hardware H.264 encoder v0 の提供対象外。
198
+ - Windows / Linux launcher tier 1 / 2 があれば `--engine gpu` 明示で利用できる。`auto` の GPU 候補は
199
+ macOS のみとし、Windows の既定切替は実機実測後の別契約で扱う。
133
200
  - 長尺の区間並列、複数 process 並列は非対応。
134
- - **インストール済みデスクトップアプリ経由(launcher tier 1)の GPU 書き出しは未配線**(v0.1.25 で判明)。shell の `--render` は OSR ランタイムしか読まず、`buildElectronArguments` は tier 2 にしか mainScript を渡さない。v0.1.26 から `resolveGpuLauncher` は tier 1 を候補から外す(fail-closed): `auto` は OSR へ(provenance に `engine_fallback` と理由)、`--engine gpu` 明示は拒否。tier 1 の配線(shell contribution に GPU ランタイム選択を足す)は別票。
201
+ - ~~インストール済みデスクトップアプリ経由(launcher tier 1)の GPU 書き出しは未配線~~ **2026-08-29 解消**: shell の `electron-entry.js` が `--akari-main packages/gpu-export/src/electron-main.mjs` を受け、`buildElectronArguments` が tier 1 にそれを渡す(osr 契約 §6)。`resolveGpuLauncher` の fail-closed(allowDesktop 既定 false)は v0.1.28 で解除。以下は v0.1.26〜v0.1.27 の記録: (v0.1.25 で判明)。shell の `--render` は OSR ランタイムしか読まず、`buildElectronArguments` は tier 2 にしか mainScript を渡さない。v0.1.26 から `resolveGpuLauncher` は tier 1 を候補から外す(fail-closed): `auto` は OSR へ(provenance に `engine_fallback` と理由)、`--engine gpu` 明示は拒否。tier 1 の配線(shell contribution に GPU ランタイム選択を足す)は別票。
202
+
203
+ ## 8.1 プラットフォーム
204
+
205
+ | platform | `--engine gpu` | `--engine auto` | launcher |
206
+ |---|---|---|---|
207
+ | macOS | 明示利用可 | 適格なら GPU、不適格なら OSR | tier 1 / 2(ただし tier 1 は現状未配線で fail-closed) |
208
+ | Windows | 明示利用可 | legacy のまま | tier 1 / 2(同上) |
209
+ | Linux | 明示利用可 | legacy のまま | tier 1 / 2(同上) |
210
+
211
+ npm Electron の tier 2 は `node_modules/electron/path.txt` を必須とする。値は win32 が
212
+ `electron.exe`、darwin が `Electron.app/Contents/MacOS/Electron`、linux が `electron` である。
213
+ インストール済みアプリの tier 1 を配布する場合は shell の `extraResources` に
214
+ `packages/gpu-export` が同梱されていることを前提とする。ただし現行 tier 1 は
215
+ `GPU_DESKTOP_TIER_UNWIRED_REASON` のとおり候補から外し、誤って OSR を GPU receipt として記録しない。
216
+
217
+ ## 9. v1 — HTML-in-Canvas DOM 層
218
+
219
+ v1 は、CSS animation、transition、`@keyframes`、Web Animations、`@property` で時間変化する自由 HTML を
220
+ `dom` 分類として適格化する。書き出し時だけ動的に生成した `canvas[layoutsubtree]` の子へ DOM を mount し、
221
+ エンジン時計で元の animation を pause・seek してから `drawElementImage` で透過 2D canvas へ転写する。
222
+ その canvas は `SpriteCompositor.updateSprite` で直接 texture 化する。Three.js canvas は従来の別 texture の
223
+ ままとし、DOM host へ入れない。
224
+
225
+ GPU 出口だけに `--enable-features=CanvasDrawElement`、`--disable-gpu-vsync`、
226
+ `--disable-frame-rate-limit` を付け、`--force-device-scale-factor=1` を維持する。DOM ランは `overlays[]` の
227
+ 宣言順で連続する項目をまとめ、静的 HTML、3D、DOM ランを元の index 順に合成したあと、字幕を最後に載せる。
228
+ すべて LUT の外である。
229
+
230
+ 次の条件は fail-closed のまま `degraded` とし、receipt に overlay id、理由、検出条件を全件残す。
231
+
232
+ - `iframe`、`object`、`embed` の埋め込み context。
233
+ - `perspective`、`preserve-3d`、`rotateX/Y/3d`、`matrix3d`、`translateZ/3d` のいずれかを含む
234
+ CSS 3D transform。先行実験では `translateZ` 単独・`perspective` 単独は正しく転写できたため、
235
+ 将来はこの粒度まで緩和できる余地があるが、v1 では緩和しない。
236
+ - `requestAnimationFrame`、`setTimeout`、`setInterval`、`Date.now`、`performance.now` で自走する時計。
237
+ - `video`、`audio`、canvas/宣言型 3D 以外の runtime、JSON 以外の script。
238
+ - 絶対 URL と外部 font/image/background resource。
239
+ - `drawElementImage` が利用できない実行環境、または device pixel ratio が 1 でない環境。
240
+
241
+ settle は mount 時に一度だけ決める。`canvas.requestPaint` がある Chromium では rAF 2 回の後に
242
+ `requestPaint()` と `paint` event(上限 250 ms)を待つ。API がない Chromium では computed style、
243
+ bounding rect、host height を同期読みして layout を確定し、直ちに転写する。採用 policy、API probe、
244
+ DOM 層の固定・待機・転写・upload の p50/p95 は receipt の `gpu.domLayer` に記録する。
245
+
246
+ `--verify-frames` では各 DOM ラン左上の 8×8 sentinel を frame number から決定論的に着色し、転写後
247
+ texture の左上 4×4 が期待 RGB の ±8 に一致するかを毎コマ検査する。CSS `mod()` の自己検査に失敗した
248
+ 環境では JS channel 指定へ切り替え、その mode も記録する。pixel read は
249
+ `src/verify-readback.js` に隔離した検証経路だけに許可し、製品経路の読み戻しゼロ契約は変えない。
250
+
251
+ DOM 層の OSR decode 比較は overlay 外接矩形内 MAD 1.0 以下を、animation 開始時刻を含む代表 5 時刻で
252
+ 要求する。sentinel は全要求 frame 一致を必須とする。既知の限界は karaoke の word texture、CSS 3D、
253
+ 自走時計、3D scene の DOM 入場 animation、OSR/legacy に残る `@property` animation の時刻不整合である。
254
+
255
+ 決定論には長尺時の既知の限界がある。短い書き出し(実測 450 / 678 / 900 コマ)は 2 走の全コマ SHA と
256
+ MP4 SHA が一致した。一方、大きな文字を持つ DOM overlay を多数含む長い書き出し(実測 5400 コマ)では、
257
+ 1 つの overlay 区間に閉じた 180 コマ前後で文字の縁のアンチエイリアスが走ごとに変わり、全コマ SHA 一致が
258
+ 確率的に崩れた(MAD 0.0001〜0.0003、差分画素 11〜41 個)。sentinel は全走一致しており 1 コマ遅れではない。
259
+ ラスタライズ関連の起動フラグでは解消しなかった。
260
+
261
+ **検収裁定(2026-08-29・司令塔)**: DOM 層を含む書き出しの HW 決定論 gate は「2 走の全コマ SHA 一致」ではなく
262
+ **「全コマの per-frame MAD ≤ 0.001 かつ sentinel 全一致」を一致とみなす**。上記の文字縁アンチエイリアス差(最大 0.0003)は
263
+ この範囲内であり不可視・1 コマ遅れでもないため受け入れる。DOM 層を含まない書き出し(エンジン層・静的スプライト・
264
+ 語単位字幕のみ)は従来どおり SHA 一致を要求する。
265
+
266
+ (2026-08-28 v1 時点の記述)字幕 cue はページ起動時に 1 枚ずつ SVG へ焼いており、30 cue の焼き込みが
267
+ 900 コマの書き出しに約 47 秒(約 52 ms/コマ相当)を上乗せした。字幕を含む短い書き出しでは GPU 出口が
268
+ OSR より遅く、同じ題材から字幕を外した実測は GPU 19.2 ms/コマ、OSR 40.9 ms/コマだった。
269
+
270
+ 2026-08-29 の #120f で、土台以外のスプライトを種別ごとのインスタンス描画へまとめた。karaoke の
271
+ `drawArrays` は字幕の本数によらず 1 コマ 2 回で一定となり、3 cue 同時と字幕なしの追加 GPU 時間は
272
+ +23.4 ms/コマから +1.65 ms/コマへ縮小した。毎コマの字幕描画費用という限界は解消し、実素材
273
+ `akari-video-pv`(5,999 コマ)では GPU / OSR 7.2〜8.2 倍へ到達した。
274
+
275
+ 残る字幕差は毎コマの合成費用ではなく、cue 採寸と SVG ラスタの起動費用である。実素材 PV の字幕ありは
276
+ 150.7 秒(25.1 ms/コマ)、字幕なし対照は 88.0 秒(14.7 ms/コマ)で 1.71 倍だった。30 cue の
277
+ `akari-project/dynamic` では字幕ラスタが約 47 秒から 9.95 秒へ短縮したが、30 秒級の短い題材では
278
+ 起動費用の比率が大きく、GPU / OSR は 1.07〜1.14 倍にとどまる。
279
+
280
+ ## 10. v3 — 宣言型 3D の登場曲線
281
+
282
+ v3 は、宣言型 Three.js scene のルート要素にある 1 回きりの登場 CSS animation を時刻の関数へ解析し、
283
+ 3D canvas の sprite draw state として GPU-native に合成する。Three.js 自体は従来どおり engine clock の
284
+ local seconds を `threeRuntime.render(container, t)` へ直接渡す。scene 内部の animation、動画 texture、
285
+ ready 判定は変更しない。
286
+
287
+ `three` 分類・理由 `three-scene-entrance-curve` にできるのは、次の条件をすべて満たす overlay だけである。
288
+
289
+ - `<script type="application/json" data-akari-3d-scene>` が属性順にかかわらずちょうど 1 個あり、他の
290
+ script がない。
291
+ - animation を持つのは HTML のルート要素 1 個だけで、selector は
292
+ `[data-akari-active] .root, [data-no-timeline] .root` の対である。canvas と fallback は動かさない。
293
+ - animation は 1 本、iteration count は 1、direction は normal、delay は 0 以上、fill mode は
294
+ `both` または `forwards` である。timing は `linear`、`ease`、`ease-in`、`ease-out`、`ease-in-out`、
295
+ または妥当な `cubic-bezier(x1,y1,x2,y2)` に限る。
296
+ - keyframe は `from` / `to` または 0% / 100% の 2 点だけで、両端に opacity と transform がある。
297
+ transform は `translate()` / `translateX()` / `translateY()` / `scale()` / `scaleX()` / `scaleY()`
298
+ だけを使う。px 平行移動と単位なし scale に加え、`var(--name, fallback)`、
299
+ `calc(var(--name) + Npx)`、`calc(var(--name) * N)` を受理する。
300
+
301
+ transition、`@property`、複数 animation、複数の animated element、中間 keyframe、iteration count が
302
+ 1 以外、alternate / reverse、負の delay、fill mode の欠落、未知の timing、rotate / skew / 3D transform、
303
+ filter、clip-path、解決不能な値は不可とし、`three-entrance-*` の具体的な理由で fail-closed にする。
304
+ animation のない従来の宣言型 3D は理由 `three-scene-canvas-direct` と manifest 形を変えない。
305
+
306
+ 解析時は overlay の `vars` と `transform.x / y / scale` を CSS 変数へ解決する。未定義変数の既定値は
307
+ 平行移動が 0 px、scale が 1 である。manifest の `entrance` は次の additive な形を持ち、from / to は
308
+ 変数解決後の絶対値である。
309
+
310
+ ```json
311
+ {
312
+ "durationSec": 1.1,
313
+ "delaySec": 0.05,
314
+ "timing": { "x1": 0.16, "y1": 1, "x2": 0.3, "y2": 1 },
315
+ "fill": "both",
316
+ "from": { "opacity": 0, "tx": -380, "ty": 140, "sx": 0.817, "sy": 0.817 },
317
+ "to": { "opacity": 1, "tx": 0, "ty": 0, "sx": 0.95, "sy": 0.95 }
318
+ }
319
+ ```
320
+
321
+ 毎コマの意味論は次式を正本とする。delay 前は from、終了後は to とし、opacity / tx / ty / sx / sy の
322
+ すべてへ同じ eased progress を線形補間で適用する。
323
+
324
+ ```text
325
+ local = seconds - overlay.start
326
+ progress = clamp((local - delaySec) / durationSec, 0, 1)
327
+ eased = timing == linear ? progress : cubicBezierAt(progress, x1, y1, x2, y2)
328
+ value = from + (to - from) * eased
329
+ ```
330
+
331
+ `cubicBezierAt` は frame-engine の既存 export を使用する。実素材 3 種を Chrome headless の
332
+ `getComputedStyle` と delay 前・登場中 3 点・終了後の 5 時刻で突き合わせた実測差は、translate 最大
333
+ 0.00043 px、scale 最大 0.000001、opacity 0 だった。検収閾値は translate 0.5 px 以下、opacity 0.005
334
+ 以下、3D 登場区間の GPU / OSR 外接矩形内 MAD 1.0 以下とする。
@@ -81,7 +81,7 @@ seek → ready → invalidate → paint → verify → write
81
81
 
82
82
  第1段・第2段とも`--user-data-dir=<run 一時ディレクトリ>/electron-user-data`を渡し、本体アプリの単一インスタンスロック(userData単位)と分離する。これによりアプリ起動中でも書き出せる。子がexit 0で終了して出力を作らなかった場合は、launcherが失敗として扱う。
83
83
 
84
- パッケージ版のTheiaではelectron-main contributionが`--render`を捕捉する。contribution開始は初期ウィンドウ表示とbackend起動の後なので、v0ではスプラッシュが一瞬表示され得る。通常起動で`--render`が無い場合、contributionは何もしない。
84
+ `--render`は`package.json`の`main`(`electron-entry.js`)がTheiaより前に捕捉する。backend fork・初期ウィンドウ・contribution・単一インスタンスロックを起動せず、`--akari-main`で指定したランタイム(既定はosr-export、gpu-exportも指定可能)へ直行するため、スプラッシュは表示されない。通常起動で`--render`が無い場合は従来どおりTheiaを起動する。
85
85
 
86
86
  Linux v0は第3段を使用する。将来の差し替え席として、Chrome headlessと`HeadlessExperimental.beginFrame`を使うlauncherを第1段と第2段の間へ追加できるものとする。この契約では実装しない。
87
87
 
@@ -148,6 +148,24 @@ media time に合わせて評価するため、従来の一定 2 コマ手前に
148
148
 
149
149
  v0.1.24以前はTheiaの`singleInstance`により、AKARI Videoデスクトップアプリの起動中に第1段を開始すると、子プロセスがexit 0・無出力で終了していた。launcherが出力を検査しなかったため、後続処理ではこの失敗がffmpegのENOENTに化けていた。runごとにuserDataを分離し、exit 0でも出力が無い場合を失敗として扱うことで根治した。Windowsのelectron-builder NSIS per-user既定導入先は`%LOCALAPPDATA%\Programs\@akari-videoshell`である。
150
150
 
151
+ v0.1.25では、contribution方式がランタイムの同梱漏れに遭遇すると、起動途中の`app.exit(1)`がSIGTRAP / Windowsの`0x80000003`に化けた。同梱が揃っていても、Theiaの`window-all-closed`から始まるquitとOSRランタイムのウィンドウ生成が競合し得る構造だった。2026-08-29に`electron-entry.js`方式へ移行し、`--render`をTheia起動前に捕捉してこの競合を除去した。
152
+
153
+ ### 11.5 インストール済みアプリ経由の第 1 段は起動処理と競合して落ちる(2026-08-29 追記)
154
+
155
+ v0.1.26 の実ビルド(署名有効・未改変)で実証: インストール済みの AKARI Video を `--render` 付きで起動すると、
156
+ `akari-osr-export` contribution が初期ウィンドウを destroy した直後に Theia の `handleMainCommand` →
157
+ `openDefaultWindow` が destroy 済みウィンドウへ `loadURL` して `TypeError: Object has been destroyed`(未処理 rejection)→
158
+ V8 fatal → SIGTRAP(exit 133 / Windows `0x80000003`)となり、PROGRESS 0 行で終わる。§11.4 の単一インスタンスロック解消後も
159
+ 残る、contribution 方式の構造的な競合(`window-all-closed` → `app.quit()` とも競合し得る)。この経路(tier 1 の既定候補)は
160
+ fieldtest / 検収に receipt が 1 件も無く、一度も動いていない。
161
+
162
+ v0.1.27 からの挙動: `resolveOsrLauncher`(製品入口)はインストール済みアプリを既定で候補から外す
163
+ (`allowInstalledDesktop: false`)。`AKARI_OSR_ELECTRON` の明示指定は従来どおり tier 1。npm Electron(tier 2)が無い
164
+ パッケージ版では tier 3 = legacy へ警告付きで落ち、provenance に `engine_fallback` と理由が残る(`auto` / `osr` とも)。
165
+ 根本修正 = `--render` を Theia より前に捕捉する書き出し専用の Electron 入口(別票)。入口が入ったら既定を戻す。
166
+
167
+ **2026-08-29 追記(根治)**: 書き出し専用の入口 `apps/shell/electron-entry.js` が合流した(§6 / §11.4)。`resolveOsrLauncher` の既定を戻し、インストール済みアプリを再び tier 1 の候補にする(v0.1.28〜)。`allowInstalledDesktop: false` は明示の opt-out として残す。
168
+
151
169
  ## 12. GPU 直結出口との共有境界(2026-08-28 追記)
152
170
 
153
171
  [GPU 直結書き出し v0](./contract-2026-08-28-gpu-export-v0.md) は、本契約の launcher 3 段、static
@@ -121,3 +121,22 @@ ms へ換算し、絶対値で集計する。全 59 点の最大は 16.667 ms、
121
121
 
122
122
  これらの近似を残しても、タイムライン上の開始、trim、loop、gain、fade、ducking 対象区間は
123
123
  同じ決定論的予定表から再現できなければならない。
124
+
125
+ ## 6. 撮影素材の台詞と ducking 側鎖
126
+
127
+ `speech` は cuts[] の撮影素材に含まれる音声を表し、各カットの trim / speed / freeze を
128
+ 出力タイムラインへ投影して Web Audio プレビューへ供給する。ただし `speech` は ducking の
129
+ 側鎖へ入れない。書き出しの `sidechaincompress` で鍵入力になるのは narration のみであり、
130
+ 土台カット音声 `[0:a]` は鍵ではないため、プレビューも同じ対象区間を維持する。
131
+
132
+ ## 7. 速度変更した台詞のピッチ保持サイドカー
133
+
134
+ `speed` が 1 以外の撮影素材の台詞は、Web Audio の `playbackRate` による近似から外す。
135
+ プレビュー前に対象カットの `[in, out)` を ffmpeg で切り出し、render-cut と同じ
136
+ `atempo` チェーンを適用した 48 kHz / `pcm_s16le` WAV を一度だけ生成する。チャンネル数は
137
+ 元音声を保ち、プレビューではこの短い WAV を offset 0・`playbackRate = 1` で再生する。
138
+
139
+ 生成物は案件内の `.akari/cache/speech-atempo/` に置く。キーは素材の実体情報、trim、speed、
140
+ `atempo` チェーンから決まり、同じ入力は再生成しない。生成またはデコードに失敗した項目だけは
141
+ 警告を 1 行出して従来の `playbackRate` 経路へ退避し、映像および他の音声のプレビューを止めない。
142
+ したがって §5 の「残す近似」に速度変更した台詞のピッチ差は含めない。