akari-video 1.0.0 → 1.0.2

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 (141) hide show
  1. package/bin/akari.mjs +67 -43
  2. package/package.json +1 -1
  3. package/src/cli.mjs +20 -12
  4. package/src/first-arg-guard.mjs +53 -0
  5. package/src/kits.mjs +5 -1
  6. package/src/media-bin-loader.mjs +37 -0
  7. package/src/messages.mjs +11 -0
  8. package/src/narration-command.mjs +6 -3
  9. package/src/runtime-diagnostics.mjs +1 -16
  10. package/src/store-command.mjs +16 -4
  11. package/src/store-device-connect.mjs +2 -2
  12. package/src/vendor-sources.mjs +2 -0
  13. package/src/voice-command.mjs +6 -4
  14. package/vendor/.akari-capability-sources.json +2 -0
  15. package/vendor/docs/contract-2026-07-13-asset-library.md +3 -3
  16. package/vendor/docs/contract-2026-08-01-export-nle-beta.md +11 -4
  17. package/vendor/docs/contract-2026-08-28-osr-export-v0.md +2 -2
  18. package/vendor/docs/contract-2026-09-02-shape-item-v0.md +1 -0
  19. package/vendor/docs/contract-2026-10-02-fragment-font-subset-v0.md +44 -0
  20. package/vendor/docs/contract-2026-10-02-textstyle-v1-rich.md +3 -3
  21. package/vendor/docs/contract-2026-10-04-asset-tier-v1.md +44 -0
  22. package/vendor/packages/akari-launcher/package.json +1 -1
  23. package/vendor/packages/asset-resolver/bin/akari-assets.mjs +12 -11
  24. package/vendor/packages/asset-resolver/browse/app.js +5 -4
  25. package/vendor/packages/asset-resolver/src/add.mjs +1 -1
  26. package/vendor/packages/asset-resolver/src/browse-server.mjs +41 -7
  27. package/vendor/packages/asset-resolver/src/catalog.mjs +58 -5
  28. package/vendor/packages/asset-resolver/src/entitlements.mjs +20 -13
  29. package/vendor/packages/asset-resolver/src/errors.mjs +1 -1
  30. package/vendor/packages/asset-resolver/src/fetch-file.mjs +52 -3
  31. package/vendor/packages/asset-resolver/src/installed.mjs +16 -3
  32. package/vendor/packages/asset-resolver/src/library.mjs +2 -1
  33. package/vendor/packages/asset-resolver/src/paid-zip.mjs +6 -4
  34. package/vendor/packages/asset-resolver/src/project-references.mjs +102 -12
  35. package/vendor/packages/asset-resolver/src/resolve.mjs +115 -38
  36. package/vendor/packages/asset-resolver/src/state.mjs +23 -15
  37. package/vendor/packages/asset-resolver/src/tier.mjs +16 -0
  38. package/vendor/packages/asset-resolver/test/add.test.mjs +3 -2
  39. package/vendor/packages/asset-resolver/test/automatic-network-check.test.mjs +55 -0
  40. package/vendor/packages/asset-resolver/test/browse-server.test.mjs +132 -0
  41. package/vendor/packages/asset-resolver/test/catalog-and-state.test.mjs +58 -2
  42. package/vendor/packages/asset-resolver/test/fixtures/build-fixture-library.mjs +2 -2
  43. package/vendor/packages/asset-resolver/test/installed-assets.test.mjs +62 -4
  44. package/vendor/packages/asset-resolver/test/library-check.test.mjs +1 -1
  45. package/vendor/packages/asset-resolver/test/library-migration.test.mjs +1 -1
  46. package/vendor/packages/asset-resolver/test/local-library.test.mjs +15 -4
  47. package/vendor/packages/asset-resolver/test/project-references-concurrency.test.mjs +88 -0
  48. package/vendor/packages/asset-resolver/test/resolve-category-ref.test.mjs +52 -0
  49. package/vendor/packages/asset-resolver/test/resolve-fast.test.mjs +134 -0
  50. package/vendor/packages/asset-resolver/test/resolve-installed-offline.test.mjs +37 -0
  51. package/vendor/packages/asset-resolver/test/resolve-locked.test.mjs +24 -7
  52. package/vendor/packages/asset-resolver/test/resolve-paid-zip.test.mjs +113 -7
  53. package/vendor/packages/asset-resolver/test/resolve-validate-in-process.test.mjs +52 -0
  54. package/vendor/packages/asset-resolver/test/tier-access.test.mjs +86 -0
  55. package/vendor/packages/audio-library-setup/bin/beat-grid.mjs +18 -10
  56. package/vendor/packages/audio-library-setup/bin/declare-helper.mjs +15 -2
  57. package/vendor/packages/audio-library-setup/bin/fetch-akari-sounds.mjs +1 -0
  58. package/vendor/packages/audio-library-setup/bin/gallery-helper.mjs +15 -2
  59. package/vendor/packages/audio-library-setup/bin/generate-candidates-html.mjs +16 -3
  60. package/vendor/packages/audio-library-setup/bin/register-drop-folder.mjs +18 -4
  61. package/vendor/packages/audio-library-setup/bin/suggest-bgm.mjs +14 -6
  62. package/vendor/packages/audio-library-setup/gallery-server.mjs +1 -1
  63. package/vendor/packages/audio-library-setup/shared/akari-sounds.mjs +1 -0
  64. package/vendor/packages/audio-library-setup/shared/candidates.mjs +2 -0
  65. package/vendor/packages/audio-library-setup/test/akari-sounds.test.mjs +2 -0
  66. package/vendor/packages/audio-library-setup/test/cli-arguments-and-csp.test.mjs +63 -0
  67. package/vendor/packages/edit-lint/src/edit-lint.mjs +95 -6616
  68. package/vendor/packages/edit-lint/src/lint/audio.mjs +810 -0
  69. package/vendor/packages/edit-lint/src/lint/caption-style.mjs +416 -0
  70. package/vendor/packages/edit-lint/src/lint/captions.mjs +646 -0
  71. package/vendor/packages/edit-lint/src/lint/cuts-tracks.mjs +622 -0
  72. package/vendor/packages/edit-lint/src/lint/edit-v2.mjs +805 -0
  73. package/vendor/packages/edit-lint/src/lint/engine-capabilities.mjs +182 -0
  74. package/vendor/packages/edit-lint/src/lint/external.mjs +47 -0
  75. package/vendor/packages/edit-lint/src/lint/html-fragment.mjs +82 -0
  76. package/vendor/packages/edit-lint/src/lint/intake.mjs +217 -0
  77. package/vendor/packages/edit-lint/src/lint/look-adjust.mjs +229 -0
  78. package/vendor/packages/edit-lint/src/lint/media-checks.mjs +732 -0
  79. package/vendor/packages/edit-lint/src/lint/media-probe.mjs +230 -0
  80. package/vendor/packages/edit-lint/src/lint/overlay-css.mjs +359 -0
  81. package/vendor/packages/edit-lint/src/lint/overlays.mjs +564 -0
  82. package/vendor/packages/edit-lint/src/lint/review.mjs +467 -0
  83. package/vendor/packages/edit-lint/src/lint/shared.mjs +130 -0
  84. package/vendor/packages/edit-lint/src/lint/v2-projection.mjs +180 -0
  85. package/vendor/packages/edit-store/lib/caption-display.d.ts +3 -1
  86. package/vendor/packages/edit-store/lib/caption-display.js +92 -10
  87. package/vendor/packages/edit-store/lib/caption-store.d.ts +31 -0
  88. package/vendor/packages/edit-store/lib/caption-store.js +14 -0
  89. package/vendor/packages/edit-store/lib/cut-ranges.js +1 -1
  90. package/vendor/packages/edit-store/lib/edit-v2-item-write.js +13 -2
  91. package/vendor/packages/edit-store/lib/edit-v2.d.ts +2 -0
  92. package/vendor/packages/edit-store/lib/history-store.d.ts +3 -0
  93. package/vendor/packages/edit-store/lib/history-store.js +48 -2
  94. package/vendor/packages/edit-store/lib/project.js +0 -3
  95. package/vendor/packages/edit-store/lib/shape-markup-v1.js +62 -14
  96. package/vendor/packages/edit-store/lib/shape-markup.js +12 -9
  97. package/vendor/packages/edit-store/lib/shape-source-validation.js +9 -1
  98. package/vendor/packages/edit-store/lib/transform-keyframe-edit.js +9 -0
  99. package/vendor/packages/edit-store/lib/webview-kernel.js +165 -32
  100. package/vendor/packages/edit-store/lib/write-gate.js +16 -13
  101. package/vendor/packages/gpu-export/README.ja.md +3 -3
  102. package/vendor/packages/gpu-export/README.md +3 -3
  103. package/vendor/packages/media-bin/bin/audio-measure.mjs +0 -0
  104. package/vendor/packages/media-bin/src/binary-manifest.mjs +51 -0
  105. package/vendor/packages/media-bin/test/media-bin.test.mjs +15 -1
  106. package/vendor/packages/osr-export/README.md +1 -1
  107. package/vendor/packages/overlay-runtime/src/fragment-source-write.mjs +67 -0
  108. package/vendor/packages/overlay-runtime/src/handle-geometry.js +33 -9
  109. package/vendor/packages/overlay-runtime/src/interaction.js +508 -16
  110. package/vendor/packages/overlay-runtime/src/overlay-runtime.js +51 -3
  111. package/vendor/packages/render-cut/src/font-cmap.mjs +196 -0
  112. package/vendor/packages/render-cut/src/fragment-text.mjs +288 -0
  113. package/vendor/packages/schemas/asset-meta.schema.json +44 -8
  114. package/vendor/packages/schemas/bin/validate-asset.mjs +72 -20
  115. package/vendor/packages/schemas/bin/validate-sites.mjs +0 -0
  116. package/vendor/packages/schemas/edit.schema.json +4 -1
  117. package/vendor/packages/schemas/examples/kit-manifest-v1-with-asset/assets/overlay/sample-kit-frame/meta.json +1 -0
  118. package/vendor/packages/schemas/test/asset-meta-default.test.mjs +55 -5
  119. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +15 -0
  120. package/vendor/packages/schemas/test/fixtures/asset/invalid-matched-by/audio/whoosh-transition/meta.json +1 -0
  121. package/vendor/packages/schemas/test/fixtures/asset/invalid-scene3d-model-missing-glb/scene3d/hero-missing-glb/meta.json +1 -0
  122. package/vendor/packages/schemas/test/fixtures/asset/invalid-scene3d-neither/scene3d/hero-neither/meta.json +1 -0
  123. package/vendor/packages/schemas/test/fixtures/asset/valid-catalog/audio/whoosh-transition/meta.json +1 -0
  124. package/vendor/packages/schemas/test/fixtures/asset/valid-library/overlay/lower-third-clean/meta.json +2 -1
  125. package/vendor/packages/schemas/test/fixtures/asset/valid-scene3d-texts-only/scene3d/hero-texts-only/meta.json +2 -1
  126. package/vendor/packages/schemas/test/textstyle-allowlist-drift.test.mjs +20 -2
  127. package/vendor/packages/schemas/test/validate-asset.test.mjs +67 -1
  128. package/vendor/skills/analyze-footage/bin/face-expression/face-expression.mjs +35 -15
  129. package/vendor/skills/analyze-footage/bin/test/face-expression-availability.test.mjs +59 -0
  130. package/vendor/skills/analyze-footage/bin/vision-tracks/vision-tracks.mjs +49 -6
  131. package/vendor/skills/analyze-footage/test/vision-tracks-check.test.mjs +47 -6
  132. package/vendor/skills/export-nle/SKILL.md +9 -5
  133. package/vendor/skills/harvest-asset/SKILL.md +8 -7
  134. package/vendor/skills/overlay-authoring/SKILL.md +2 -0
  135. package/vendor/skills/overlay-authoring/telop.md +8 -0
  136. package/vendor/skills/setup-library/SKILL.md +1 -1
  137. package/vendor/skills/setup-library/fetch-and-validate.md +9 -3
  138. package/vendor/skills/setup-library/starter-pack.md +1 -0
  139. package/vendor/skills/verify/SKILL.md +1 -1
  140. package/vendor/packages/edit-store/lib/motion-position-write.d.ts +0 -6
  141. package/vendor/packages/edit-store/lib/motion-position-write.js +0 -14
@@ -89,7 +89,7 @@ seek → ready → invalidate → paint → verify → write
89
89
 
90
90
  第1段・第2段とも`--user-data-dir`を渡し、本体アプリの単一インスタンスロック(userData単位)と分離する。既定は`launchElectronExport`が書き出しごとに`os.tmpdir()`直下へ`mkdtemp("akari-osr-")`で作る短い一意なディレクトリで、子のclose後に`finally`で削除する。呼び出し側が`userDataDir`を明示した場合はそれを使い、作らず消さない。これによりアプリ起動中でも書き出せる。子がexit 0で終了して出力を作らなかった場合は、launcherが失敗として扱う。**2026-09-28 改訂(#114)**: 出力の隣に置くとプロジェクトのパス長とChromiumのキャッシュ階層がWindowsのMAX_PATHを超え、キャッシュ作成エラーがstderrに出ていた。
91
91
 
92
- **Windows のアプリ別 GPU 設定の一時上書き(2026-09-01 追記・§11.7)**: `platform === "win32"` かつソフト描画でないとき、第1段・第2段とも launcher(`launchElectronExport`・gpu / osr 共通の spawn 点)は、`auto` では GPU 出口(`options.exit === "gpu"`・gpu-export の electron-main)のときだけ、`force` では OSR 出口でも、spawn の直前に `HKCU\Software\Microsoft\DirectX\UserGpuPreferences` へ「値名 = 実行体のフルパス(`path.win32.resolve` で正規化)・REG_SZ `GpuPreference=2;`」を書き、子の `close` 後(exit code に関わらず・spawn エラーでも)に `finally` で必ず 1 回復元する(無かったなら削除・あったなら元の値へ)。方針は呼び出し側の `gpuPreference` → env `AKARI_EXPORT_GPU_PREFERENCE` → `auto` の順に解決し、`auto` は利用者が明示した値(`GpuPreference=1;` 等)を黙って上書きしない(`force` だけが上書き + 復元する)。書く前に sidecar `<AKARI_HOME ?? ~/.akari>/gpu-preference-override.json` を置き、復元後に削除する。`launchElectronExport` は毎回冒頭で sidecar があれば先に復元する。記録は戻り値の `gpuPreference`(`exit` 込み)と receipt の `provenance.gpu_preference`。OSR 出口を `auto` で外す根拠は §11.7(2026-09-02 改訂)。他 OS はバイト同一の no-op(記録に `reason: platform` だけ残す)。開発用に `AKARI_EXPORT_ALLOW_DESKTOP=0` で第1段(インストール済みアプリ)を候補から外せる(明示引数 `allowDesktop` が env より優先)。
92
+ **Windows のアプリ別 GPU 設定の一時上書き(2026-09-01 追記・§11.7)**: `platform === "win32"` かつソフト描画でないとき、第1段・第2段とも launcher(`launchElectronExport`・gpu / osr 共通の spawn 点)は、`auto` では GPU 出口(`options.exit === "gpu"`・gpu-export の electron-main)のときだけ、`force` では OSR 出口でも、spawn の直前に `HKCU\Software\Microsoft\DirectX\UserGpuPreferences` へ「値名 = 実行体のフルパス(`path.win32.resolve` で正規化)・REG_SZ `GpuPreference=2;`」を書き、子の `close` 後(exit code に関わらず・spawn エラーでも)に `finally` で必ず 1 回復元する(無かったなら削除・あったなら元の値へ)。方針は呼び出し側の `gpuPreference` → env `AKARI_EXPORT_GPU_PREFERENCE` → 保存済みの許可があれば `auto`、なければ `off` の順に解決し、`auto` は利用者が明示した値(`GpuPreference=1;` 等)を黙って上書きしない(`force` だけが上書き + 復元する)。書く前に sidecar `<AKARI_HOME ?? ~/.akari>/gpu-preference-override.json` を置き、復元後に削除する。`launchElectronExport` は毎回冒頭で sidecar があれば先に復元する。記録は戻り値の `gpuPreference`(`exit` 込み)と receipt の `provenance.gpu_preference`。OSR 出口を `auto` で外す根拠は §11.7(2026-09-02 改訂)。他 OS はバイト同一の no-op(記録に `reason: platform` だけ残す)。開発用に `AKARI_EXPORT_ALLOW_DESKTOP=0` で第1段(インストール済みアプリ)を候補から外せる(明示引数 `allowDesktop` が env より優先)。
93
93
 
94
94
  `--render`は`package.json`の`main`(`electron-entry.js`)がTheiaより前に捕捉する。backend fork・初期ウィンドウ・contribution・単一インスタンスロックを起動せず、`--akari-main`で指定したランタイム(既定はosr-export、gpu-exportも指定可能)へ直行するため、スプラッシュは表示されない。通常起動で`--render`が無い場合は従来どおりTheiaを起動する。
95
95
 
@@ -237,7 +237,7 @@ OS のアプリ別 GPU 設定だけが効き、プロセス生成時に評価さ
237
237
  裁定(実装 = `packages/osr-export/src/gpu-preference.mjs` / `gpu-adapters.mjs`、`packages/gpu-export/src/gpu-diagnostics.mjs`):
238
238
 
239
239
  1. **適用点(2026-09-02 改訂・feedback-r1)**: `launchElectronExport`(gpu / osr 共通の spawn 点)。`platform === "win32"` かつ `options.soft` でないときだけ動く。他 OS / soft は no-op(記録に理由だけ残す)。**`auto` は GPU 出口だけ**(gpu-export の electron-main を起動する launch = export / capture。`launchGpuExport` が `options.exit = "gpu"` を渡す)。OSR 出口(osr-export の electron-main = export / capture。`exportWithOsr` / `captureFramesWithOsr` が `exit: "osr"` を渡す)は `auto` では書かず(skip・reason `not-gpu-exit`)、**`force` のときだけ**書く(終了後の復元は同じ)。`exit` 未指定は `"osr"` 扱い(保守的)。
240
- 2. **方針値 `gpuPreference`**: `"auto"`(既定)| `"off"` | `"force"`。解決順 = 呼び出し側の `options.gpuPreference` → env `AKARI_EXPORT_GPU_PREFERENCE` → `"auto"`。不正値は許容値を含むメッセージで throw。render-cut は `--gpu-preference auto|off|force`。
240
+ 2. **方針値 `gpuPreference`**: `"auto"` | `"off"`(許可なしの既定)| `"force"`。解決順 = 呼び出し側の `options.gpuPreference` → env `AKARI_EXPORT_GPU_PREFERENCE` → 保存済みの許可があれば `"auto"`、なければ `"off"`。不正値は許容値を含むメッセージで throw。render-cut は `--gpu-preference auto|off|force`。
241
241
  3. **対象 exe** = `launcher.executable` を `path.win32.resolve` で正規化(`/` → `\`、Windows 設定アプリが書く形式)。レジストリは `HKCU\Software\Microsoft\DirectX\UserGpuPreferences`、値名 = exe フルパス、REG_SZ `GpuPreference=2;`。読み書きは `%SystemRoot%\System32\reg.exe`(`query` / `add ... /f` / `delete ... /f`)を `spawnSync` で叩く。ネイティブモジュール禁止・管理者権限不要・`reg query` は値の部分(ASCII)だけ parse する。`registry` 依存 `{ read, write, remove }` は注入可能。
242
242
  4. **判定は純関数** `planGpuPreference({ platform, policy, soft, current, exit })` → `{ action: "write" | "skip", value, restore, reason }`: platform ≠ win32 → skip `platform` / soft → skip `soft` / off → skip `policy-off` / **auto かつ exit ≠ gpu → skip `not-gpu-exit`(2026-09-02 改訂)** / current === `GpuPreference=2;` → skip `already-high-performance` / current === null → write + 終了後 remove / current がそれ以外(`GpuPreference=1;` 等): `auto` → skip `user-preference-respected`(利用者の明示設定を黙って上書きしない)、`force` → write + 終了後 current へ戻す。defensive な skip 理由 4 つ(いずれも stderr に warning を出して spawn は続ける・r0 受理): `executable-missing`(正規化後の exe が存在しない)/ `registry-unavailable`(`reg query` を spawn できない)/ `sidecar-unavailable`(sidecar が書けないのでレジストリも書かない)/ `write-failed`(`reg add` 失敗・sidecar を消して続行)。
243
243
  5. **順序と復元**: write → spawn → 子の `close`(exit code に関わらず・spawn エラーでも)→ `finally` で restore を必ず 1 回。復元に失敗したら stderr に `[gpu-preference] restore failed: ...` を出し記録に `restored: false` を残す(throw しない)。
@@ -23,6 +23,7 @@
23
23
  ```
24
24
 
25
25
  - `shape`: `rect | rounded-rect | ellipse | line | arrow | speech-bubble`
26
+ - `line` / `arrow` の `params.startCapScale` と `params.endCapScale` は任意の数値(0.5〜3.0、未指定時 1.0)。始点・終点の飾りを個別に拡大縮小する。既存データへの追記は不要。
26
27
  - `params`(全部 optional・additionalProperties false で開始 — 広げる方向は互換):
27
28
  `width`(>0・既定 600)/ `height`(>0・既定 340。line / arrow は既定 80)/
28
29
  `fill`(既定 `#f97316`)/ `stroke`(既定なし = 描かない)/ `strokeWidth`(≥0・既定 0。line / arrow は 8)/
@@ -0,0 +1,44 @@
1
+ # contract — 断片書体の部分化と字形検査 v0
2
+
3
+ - 決定日: 2026-10-02
4
+ - 対象: HTML 断片の `@font-face`
5
+ - 状態: 段 1 の字形検査を実装。段 2 の書き出し時の自動部分化と段 3 のキャッシュ掃除は予定
6
+
7
+ ## 1. 目的と順序
8
+
9
+ 断片の文言を直すたびに手で部分フォントを作り直す負担と、追加した字だけが別書体に切り替わる見落としを減らす。段 1 は `edit-lint` の字形検査、段 2 は書き出し時の自動部分化、段 3 は不要になったキャッシュの掃除とする。プレビューは原本の書体をそのまま読む。3D テキストと字幕の書体は対象外。
10
+
11
+ ## 2. 使う字の集合
12
+
13
+ lint は断片から**描画される字**を集める。テキストノード(コメント・`<style>`・通常の `<script>` を除く)、文字参照を解いた字、CSS `content:` の文字列と CSS エスケープを解いた字、`content: attr(x)` の属性値、`params` / `vars` の文字列値、`type="application/json"` の宣言内の文字列値、`data-akari-font-chars`、`text-transform` の変換後の字を含む。数値文字参照と HTML 4.01 の名前付き文字参照 252 個を解く。`text-transform` は `uppercase`・`lowercase`・`capitalize`・`full-width` の対応する字を足し、`none` では足さない。空白・制御文字・既定で描画されない字は数えない。通常の `alt`・`title` などの属性値や、CSS のカスタムプロパティに置いた文字列は描画集合に直接は入れない。
14
+
15
+ 段 2 の部分化は**ソース全体の字**を使う予定である。書体の `data:` URI の中身を除くソース、文字参照と CSS エスケープを解いた字、`params` / `vars`、`data-akari-font-chars` に、ASCII 印字可能文字・和文約物・省略記号などの安全余白を足す。**部分化用の広い集合 ⊇ lint 用の狭い集合**を保つ。
16
+
17
+ 実行時の script が字を生成する断片は、ルート要素に `data-akari-font-chars="追加され得る字"` を置く。この値は lint の描画集合にも入る。宣言のない通常の `<script>` について lint は静的収集が不完全であることを info で知らせ、段 2 では書体を絞らない予定である。
18
+
19
+ ```html
20
+ <div data-akari-font-chars="晴雨雪">
21
+ <style>@font-face { font-family: MyFont; src: url("../assets/font/my-font.ttf"); }</style>
22
+ <span>今日の天気</span>
23
+ </div>
24
+ ```
25
+
26
+ ## 3. 字形検査
27
+
28
+ `overlays.fragment-font-glyphs` は `@font-face` を持つファイル参照の断片を対象とする。プロジェクト内の断片と共有ライブラリ参照の断片を同じように扱い、相対パスと `data:` URI の書体を読む。断片が読む書体の cmap の**和集合**に描画集合の字が無ければ、欠けた字の先頭 20 字と件数、コードポイント、断片と書体のパスを含む warning を 1 件出す。通常の `<script>` があり宣言のない断片には静的に集めきれない旨の info を出す。書体を読めない場合やプロジェクト外への参照には info を出し、その断片で 1 本でも書体を読めなければ欠けた字の warning は出さない。`@font-face` のない断片は対象外。
29
+
30
+ ファイル参照で読める拡張子は `.ttf` / `.otf` / `.woff` / `.woff2` で、TTC は `data:` URI のときだけ読める。クエリつきの参照(`a.woff2?v=2`)や他の拡張子、`local()` や `https:` だけの `src` は「読める書体参照がありません」の info になる。Unicode cmap の format 4 / 12 を読み、異体字セレクタ用の format 14 は見ない。Unicode cmap がない書体(記号用の符号化だけの書体など)、壊れた書体、WOFF2 コレクションは読めない旨を info で知らせる。
31
+
32
+ 和集合の検査は、ある要素に指定された書体に字が無くても、別の参照書体にその字があれば見逃す。`src` の代替リスト(`url(a), url(b)`)も全部の和集合で数えるが、ブラウザは使える先頭しか使わないため見逃しがあり得る。`unicode-range` は見ない。体験版など字数を制限した書体でも見逃しが起こり得る。書き出しでは全断片を 1 枚の文書に並べるため、他の断片の `@font-face` で描かれる字も、その断片の検査では警告になり得る。部品アイテムの `source.text` による文字の差し替えは数えない。結合文字(NFD。例: `か` + U+3099)は、合成済みの字だけを持つ書体だと誤警告になる。CSS の `font-family`、変数、代替書体を静的に確定しないため、warning は表示上の欠けを断定せず、書き出しを止めない。
33
+
34
+ ## 4. 自動部分化とキャッシュ(予定)
35
+
36
+ 段 2 では書き出し時に断片と書体ごとに部分化し、プレビューでは原本を読む。部分化器は版を固定した hb-subset の WASM を同梱する予定である。TTF / OTF は元と同じ sfnt 形式で、レイアウト機能、ヒンティング、可変軸、`.notdef` を保持する。通常の `<script>` があり `data-akari-font-chars` のない断片、WOFF2 の書体、部分化に失敗した書体は絞らず、理由を報告する。既存の手作業の部分フォントはそのまま利用できる。`AKARI_FONT_SUBSET=off` で部分化を外せるようにする。16MiB の埋め込み上限は部分化**後**の大きさに適用する。
37
+
38
+ 一時ファイルはプロジェクトの `.akari/cache/font-subset/` に置く。鍵は書体内容の SHA-256、使う字、部分化器の版と設定から作る。段 3 では書き出し後に今回使わなかった期限切れの鍵だけを掃除する。`akari clean` の既存の `.akari/cache/**` 規則を使い、`assets/` の原本は削除しない。未参照の原本書体の一覧化は別途検討する。
39
+
40
+ 出力プレビューで同じ書体が断片ごとに別 URL になる可能性は、実測してから別途扱う。この契約ではプレビューの配信方法を変えない。
41
+
42
+ ## 5. 利用許諾
43
+
44
+ 書き出し時の一時的な部分化は、書体の種類を問わずプロジェクト内のキャッシュだけに置き、部分フォントを頒布しない運用として扱う。字形自体は変更しない。利用者が部分化を外す手段を設ける。
@@ -62,7 +62,7 @@ v1.1(2026-10-02)では pattern に限り `id` に `heart` / `thunder` を追
62
62
 
63
63
  ## 3. 柄の定義と変換元
64
64
 
65
- 初期 7 id の図形、タイルの位相・基準寸法・重ね順は素材側 `assets/overlay/telop-pattern-*/fragment.html` の `--i-fill` と SVG データ URI を正本として固定する。`diamond` は斜め筋と菱形、`dot` はずれた水玉、`stripe` は帯、`gingham` は格子、`skull` は髑髏、`hazard` は警告三角、`night` は星点と星形を持つ。描画側は id から固定テンプレートを選び、検証済み `fg` / `bg` と `scale` を埋めて自己完結した CSS 画像を作る。同じ id と 3 値は 4 経路で同じ画素入力とする。素材の多色グラデや複数の独立した色を単一 `fg` / `bg` に縮約できないときは、変換プリセットに別の展開済み色を追加する改訂契約を起こすか §9 の残留とする。CSS `mask` による切り抜きはこの v1 pattern に含めない。
65
+ 初期 7 id の図形、タイルの位相・基準寸法・重ね順は素材側 `assets/overlay/telop-pattern-*/fragment.html` の `--i-fill` と SVG データ URI を正本として固定する。`diamond` は 45deg / -45deg の筋(各 2px / 13px 周期、`fg` 32%)を菱形 SVG(幅 26px、図形 opacity .5)の上に重ねる。`dot` は 16px タイルの radial 2 層で、中心 (8,8) は 2→3px のフェードと `fg` 50%、中心 (0,0) は 1.6→2.6px のフェードと `fg` 35% を持つ。`gingham` は 90deg / 0deg の格子 2 層(各 9px / 22px 周期、`fg` 55%)で、0deg 層は下端起点、交差部の合成 opacity は .7975 とする。`stripe` は帯、`skull` は髑髏、`hazard` は警告三角、`night` は星点と星形を持つ。描画側は id から固定テンプレートを選び、検証済み `fg` / `bg` と `scale` を埋めて自己完結した CSS 画像を作る。これら 3 柄の筋・水玉・格子は fragment と同じ CSS gradient 層で作り、菱形だけ SVG データ URI を使う。同じ id と 3 値は 4 経路で同じ画素入力とする。素材の多色グラデや複数の独立した色を単一 `fg` / `bg` に縮約できないときは、変換プリセットに別の展開済み色を追加する改訂契約を起こすか §9 の残留とする。CSS `mask` による切り抜きはこの v1 pattern に含めない。
66
66
 
67
67
  v1.1 の `heart` / `thunder` は別の正本 URI を §11 で追加する。透過柄の下に `bg` のグラデーションを敷く 2 層合成を用いるため、上記の 2 色縮約条件はこの 2 id には適用しない。
68
68
 
@@ -263,14 +263,14 @@ TODO(B-2 所有外): インスペクタの「文字色」操作は `fill`
263
263
 
264
264
  ## 11. v1.1(2026-10-02)
265
265
 
266
- 素材側の仕分けで、`telop-pop-heart` と `telop-pop-thunder` は地がグラデーションであり、単色 `pattern.bg` では原画を表せないと確定した。`pattern.bg` の gradient は保存時に停止点と角度を展開し、描画時は `background-image: <透過 pattern SVG>, linear-gradient(...)` の順に合成する。**`bg` 文字列は v1 の 1 層(SVG 内の `<rect>`)をバイト不変で維持し、2 層は `bg` がグラデーションオブジェクトのときだけ使う。**第 1 層のサイズは基準タイル × `scale`、第 2 層のグラデーションは行全体の描画矩形とする。両層の位置は行の共通原点に合わせ、`words[]` 分割後も位相を戻さない。2 層の SVG に地色を焼き込まない。
266
+ 素材側の仕分けで、`telop-pop-heart` は地がグラデーション、`telop-pop-thunder` は地が繰り返し縞(`repeating-linear-gradient`)であり、単色 `pattern.bg` では原画を表せないと確定した。v1.1 の `thunder` は縞を 2 色グラデーションで近似する。`pattern.bg` の gradient は保存時に停止点と角度を展開し、描画時は `background-image: <透過 pattern SVG>, linear-gradient(...)` の順に合成する。**`diamond` / `dot` / `gingham` 以外の `bg` 文字列は v1 の 1 層(SVG 内の `<rect>`)をバイト不変で維持し、2 層は `bg` がグラデーションオブジェクトのときだけ使う。**3 柄は §3 の CSS gradient 層を保つため、`bg` が文字列でも最背面に単色の CSS gradient を敷く。第 1 層のサイズは基準タイル × `scale`、地のグラデーションは行全体の描画矩形とする。各層の位置は行の共通原点に合わせ、`words[]` 分割後も位相を戻さない。透過 SVG に地色を焼き込まない。
267
267
 
268
268
  | 項目 | v1 | v1.1 |
269
269
  |---|---|---|
270
270
  | `pattern.id` enum | `diamond` / `dot` / `stripe` / `gingham` / `skull` / `hazard` / `night` | 左記に `heart` / `thunder` を追加 |
271
271
  | `pattern.bg` の型 | `hexColor` 文字列 | `hexColor` 文字列または `{stops,angle_deg}` |
272
272
  | `fg` の alpha | `hexColor` の `#RRGGBBAA` を受理 | 9 id とも SVG の `fill` に alpha を保持し、図形固有の opacity と合成 |
273
- | CSS 合成式 `background-image` | `url("<rect を持つ pattern SVG>")` の 1 層 | `bg` 文字列は v1 とバイト一致。`bg` グラデのみ `<透過 pattern>, linear-gradient(...)` の 2 層 |
273
+ | CSS 合成式 `background-image` | `url("<rect を持つ pattern SVG>")` の 1 層 | `diamond` / `dot` / `gingham` は §3 の CSS gradient 層 + 地の CSS gradient。他の id は `bg` 文字列で v1 とバイト一致し、`bg` グラデのみ `<透過 pattern>, linear-gradient(...)` の 2 層 |
274
274
  | 位相・タイル寸法 | 初期 7 id の既定タイルと位相 | `heart` は 14px、`thunder` は 30px。`thunder` の位相は 4px 2px。1 層は位置 1 組・サイズ 1 組、2 層は位置 2 組・サイズはタイルと行全体 |
275
275
  | 対象外の `night` / `skull` / `stripe` | id は受理 | 素材の textstyle 変換は対象外。`night` は星点が別色、`skull` は SVG 2 色、`stripe` は独立した斜線グラデーションが必要 |
276
276
 
@@ -0,0 +1,44 @@
1
+ # 素材 tier 契約 v1
2
+
3
+ - 日付: 2026-10-04
4
+ - 状態: 実装契約 v1
5
+ - 関連: [素材ライブラリ契約](contract-2026-07-13-asset-library.md)
6
+
7
+ `meta.json` の `tier: "free" | "pro"` は必須。`price` は廃止予定の任意フィールドとして残すが、`tier` がある場合は無視し、1 版後に削除する。旧メタを読むときだけ、`tier` が無ければ `price === 0` の場合に限り `free` と解釈し、`null`・欠落・非数値を含むそれ以外は `pro` とする(fail-closed)。新規メタと v1 validator には `tier` が必要。
8
+
9
+ `tier: "pro"` と `license.spdx: "CC0-1.0"` の組み合わせは validator のエラー。`tier: "free"` には CC0 を推奨し、それ以外のライセンスには warning を出す。tier はライセンスの許諾内容を置き換えない。
10
+
11
+ 切替前に CC0 で取得されたコピーは CC0 のまま。後から tier を変更しても、既に配布された CC0 コピーの利用条件は変更されない。
12
+
13
+ - 鍵はライブラリ側に置く。`composeState` は `tier === "pro" && !entitled` を `locked` とし、fetch も同じ条件で認可する。解錠は (a) 店応答の `pass` が非 null(Lifetime パス保有。`all-access-pass` id も同値)なら全 Pro、または (b) item の `product_id` が `entitlements[].product_id` に含まれる場合。店はパス対象商品を entitlement 行へ展開するため、応答に `all-access-pass` id は入らない。item と無関係に全 Pro を解錠できるのは (a) だけ。取得済み素材は `cached` のまま使える。
14
+ - resolver の一覧出力は各 item に `tier` と `machineTags` の `tier:<tier>` を持たせる。
15
+ - 公開 catalog の Pro 項目は `files` を持たない。非空の `files` があれば `invalid_catalog_item` として取得を拒否する。locked 項目の一覧・`/api/items` 応答にも `files` を含めない。
16
+ - 利用者取り込み(`akari assets add`)は `tier: "free"` とする。
17
+
18
+ ## § 取得時の素材参照(2026-10-05)
19
+
20
+ resolver・CLI・shell からの取得参照は `category/id` とする。bare id はカタログ内で一意な場合だけ互換解決する。複数の category に同じ id があれば、候補の `category/id` を含む `ambiguous_id` エラーを返し、先頭の item を選ばない。
21
+
22
+ ## § `source` の union(2026-10-04)
23
+
24
+ `source` は **external**(既存 v0)または **akari-r2** の一方だけを取る。external は `url` が必須で、`acquisition`・`license_at_source`・`attribution_required` は従来どおり。akari-r2 は絶対 URL または R2 key の `image` と `preview` が必須で、`width`・`height`・`bytes` は任意の正整数。`url` があれば external、`image` があれば akari-r2 と判別する。両方ある形、どちらもない形は不正。
25
+
26
+ `remote: true` の素材は実体がディレクトリに無いため akari-r2 の `source` を必須とする。`remote` が無い、または false の素材では `source` は任意。既存の公開 `catalog/` は external + `remote: true` の旧形式のため、validator は `catalog/` 配下に限って互換性を維持する。新規メタは akari-r2 に従う。
27
+ JSON Schema は `remote: true` なら `source` 必須までを表し、akari-r2 必須と既存 `catalog/` の external 互換は validator が経路を見て判定する。
28
+
29
+ ## § textstyle(字幕スタイル)の Pro item(2026-10-05)
30
+
31
+ 公開カタログの textstyle Pro item は `id`、`category: "textstyle"`、`tier: "pro"`、
32
+ `product_id: "telop-rich-pack-01"`、`version`、`preview`、`license`、`tags`、`title` を持つ。
33
+ `files` は持たせない。
34
+
35
+ 束 zip は `<product_id>-v<version>/` の直下に `README.md`、`LICENSE.md`、
36
+ `checksums.txt` を置き、各素材を次の形で収める。
37
+
38
+ `assets/textstyle/<id>/{meta.json,preset.json,preview.png}`
39
+
40
+ `resolve` は対象の 1 件だけを `<ライブラリ>/textstyle/<id>/` へ配置し、
41
+ `validate-asset` で `preset.json` の `format: "akari-textstyle"` と `id` の一致を検証する。
42
+ `preview` URL は `https://akari.video/lab/media/telop-rich-pack/textstyle/<id>.png` の形とする。
43
+ 配置後は既存の textstyle ライブラリ読み込みが `preset.json` を読み、
44
+ 「テキスト > スタイル」へ並べる。未取得 textstyle の棚カード表示は別票で扱う。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
5
5
  "type": "module",
6
6
  "files": [
@@ -4,7 +4,7 @@
4
4
  // akari-assets list [--category <c>] [--source <lab|site|own>] [--json]
5
5
  // akari-assets add <path...> --plan [--json]
6
6
  // akari-assets add --apply <plan.json> [--json]
7
- // akari-assets fetch <id> [--project <dir>] [--reference] [--force]
7
+ // akari-assets fetch <ref> [--project <dir>] [--reference] [--force] (ref は category/id)
8
8
  // akari-assets bundle --project <dir> [--dry-run]
9
9
  // akari-assets sync
10
10
  // akari-assets browse [--port <n>]
@@ -58,16 +58,17 @@ function validateArgs(sub, args) {
58
58
  if (sub === 'bundle' && !seen.has('--project')) throw new Error('--project <dir> が必要です');
59
59
  if (sub === 'credits' && !seen.has('--project')) throw new Error('--project <dir> が必要です');
60
60
  if (sub === 'fetch') {
61
- if (args[0] !== positional[0]) throw new Error('fetch の先頭には素材 ID を指定してください');
61
+ if (args[0] !== positional[0]) throw new Error('fetch の先頭には素材参照 category/id を指定してください');
62
62
  if (seen.has('--reference') && !seen.has('--project')) throw new Error('--reference には --project <dir> が必要です');
63
63
  }
64
64
  }
65
65
 
66
- const STATE_BADGE = { cached: '✓', locked: '¥', available: '☁' };
66
+ const STATE_BADGE = { cached: '✓', locked: '🔒 Pro', available: '☁' };
67
67
 
68
68
  function badgeOf(item) {
69
69
  if (item.source === 'installed') return '[installed]';
70
- if (item.state === 'locked') return `¥${(item.price ?? 0).toLocaleString()}`;
70
+ if (item.state === 'locked') return STATE_BADGE.locked;
71
+ if (item.state === 'available' && item.tier === 'pro') return '✓ Pro';
71
72
  return STATE_BADGE[item.state] ?? '?';
72
73
  }
73
74
 
@@ -87,7 +88,7 @@ async function cmdList(args, env) {
87
88
 
88
89
  console.log(`使える素材 ${filtered.length} 件(ライブラリ: ${libraryRoots.write})`);
89
90
  for (const item of filtered) {
90
- console.log(` ${badgeOf(item)} ${item.id}\t${item.sourceKind}\t[${item.category}]\t${item.title}`);
91
+ console.log(` ${badgeOf(item)} ${item.category}/${item.id}\t${item.sourceKind}\t[${item.category}]\t${item.title}`);
91
92
  }
92
93
  }
93
94
 
@@ -107,9 +108,9 @@ async function cmdAdd(args, env) {
107
108
  }
108
109
 
109
110
  async function cmdFetch(args, env) {
110
- const id = args[0];
111
- if (!id || id.startsWith('--')) {
112
- console.error('使い方: akari-assets fetch <id> [--project <dir>] [--reference] [--force]');
111
+ const ref = args[0];
112
+ if (!ref || ref.startsWith('--')) {
113
+ console.error('使い方: akari-assets fetch <ref> [--project <dir>] [--reference] [--force](ref は category/id)');
113
114
  process.exitCode = 1;
114
115
  return;
115
116
  }
@@ -122,7 +123,7 @@ async function cmdFetch(args, env) {
122
123
  return;
123
124
  }
124
125
  try {
125
- const result = await resolveAsset(id, { env, project, force, reference });
126
+ const result = await resolveAsset(ref, { env, project, force, reference });
126
127
  console.log(`${result.cached ? '取得済み(キャッシュ)を使用' : '取得しました'}: ${result.dir}`);
127
128
  if (result.projectDir) console.log(` プロジェクトへコピー: ${result.projectDir}`);
128
129
  if (result.referenced) console.log(` 参照を記帳: ${result.category}/${result.id}`);
@@ -202,8 +203,8 @@ function printUsage() {
202
203
  出どころ・取得状態つき素材一覧
203
204
  add <path...> --plan [--json] ローカル素材の取り込み計画(書き込みなし)
204
205
  add --apply <plan.json> [--json] 計画で選択した素材を複製して登録
205
- fetch <id> [--project <dir>] [--reference] [--force]
206
- 素材を解決して登録(--reference はコピーせず参照を記帳)
206
+ fetch <ref> [--project <dir>] [--reference] [--force]
207
+ ref は category/id(bare id は一意な場合のみ)。--reference はコピーせず参照を記帳
207
208
  bundle --project <dir> [--dry-run] 参照素材をプロジェクトへ実体化(素材をまとめる)
208
209
  migrate [--dry-run] ライブラリを作業場へ移行
209
210
  sync カタログを取得してローカルにキャッシュ(オフライン用)
@@ -43,9 +43,10 @@
43
43
 
44
44
  /* ---- グリッド ---- */
45
45
  const grid = $('#grid');
46
+ const tierLabel = (item) => item.tier === 'free' ? '無料' : 'Pro';
46
47
  function badgeOf(item) {
47
48
  if (item.state === 'cached') return '<span class="badge got">✓ 取得済み</span>';
48
- if (item.state === 'locked') return `<span class="badge paid">¥${(item.price ?? 0).toLocaleString()}</span>`;
49
+ if (item.state === 'locked') return `<span class="badge paid">${tierLabel(item)}</span>`;
49
50
  return '<span class="badge">☁ 未取得</span>';
50
51
  }
51
52
  function renderGrid() {
@@ -76,9 +77,9 @@
76
77
  const isAudio = item.category === 'audio';
77
78
  detail.append(el(`<div>
78
79
  ${item.preview && !isAudio ? `<img class="big" src="/thumb/${encodeURIComponent(item.id)}" alt="">` : ''}
79
- ${item.preview && isAudio ? `<audio controls preload="none" src="/media/${encodeURIComponent(item.id)}"></audio>` : ''}
80
+ ${isAudio && item.mediaAvailable ? `<audio controls preload="none" src="/media/${encodeURIComponent(item.id)}"></audio>` : ''}
80
81
  <h2>${esc(item.title)}</h2>
81
- <p class="id">${esc(item.id)} ・ ${esc(item.license?.spdx ?? '')}${item.price ? ` ・ ¥${item.price.toLocaleString()}` : ' ・ 無料'}</p>
82
+ <p class="id">${esc(item.id)} ・ ${esc(item.license?.spdx ?? '')} ・ ${tierLabel(item)}</p>
82
83
  <div class="tagrow">${(item.tags || []).map((t) => `<span>${esc(t)}</span>`).join('')}</div>
83
84
  ${item.provenance?.prompt ? `<details class="prompt"><summary>生成プロンプト</summary><p>${esc(item.provenance.prompt)}</p></details>` : ''}
84
85
  <div class="actions" id="actions"></div>
@@ -89,7 +90,7 @@
89
90
  const say = (ok, text) => { msg.className = `msg ${ok ? 'ok' : 'err'}`; msg.textContent = text; };
90
91
 
91
92
  if (item.state === 'locked') {
92
- actions.append(el(`<button class="btn" disabled>未購入(¥${(item.price ?? 0).toLocaleString()}) — AKARI Video Lab で購入してください</button>`));
93
+ actions.append(el('<button class="btn" disabled>Pro — AKARI Video Lab で利用資格を確認してください</button>'));
93
94
  } else if (item.state === 'cached') {
94
95
  actions.append(el('<p class="note">✓ ライブラリに取得済みです。</p>'));
95
96
  } else {
@@ -162,7 +162,7 @@ export function createImportMeta(entry, options = {}) {
162
162
  requires: [], provenance: { origin: '利用者がローカルから取り込み', generator: null },
163
163
  author: 'user',
164
164
  license: { spdx: 'LicenseRef-user-owned', scope: 'private-owned', attribution_required: Boolean(credit), ai_training_allowed: false },
165
- price: 0,
165
+ tier: 'free',
166
166
  };
167
167
  if (origin === 'site') {
168
168
  meta.source = {
@@ -12,6 +12,7 @@ import { loadCatalog, resolveEffectiveBase } from './catalog.mjs';
12
12
  import { resolvePreviewLocation } from './fetch-file.mjs';
13
13
  import { AssetResolverError, resolve as resolveAsset } from './resolve.mjs';
14
14
  import { composeState } from './state.mjs';
15
+ import { readLocalLibraryItem } from './library.mjs';
15
16
 
16
17
  const here = path.dirname(fileURLToPath(import.meta.url));
17
18
  const BROWSE_DIR = path.resolve(here, '..', 'browse');
@@ -61,6 +62,27 @@ function statusForError(error) {
61
62
  return 400;
62
63
  }
63
64
 
65
+ function audioFile(files) {
66
+ return files?.find((file) => /\.(mp3|wav|m4a|ogg|aac|flac|aif|aiff)$/i.test(file.name ?? ''));
67
+ }
68
+
69
+ function audioPreview(item) {
70
+ return typeof item.preview === 'string' && /\.(mp3|wav|m4a|ogg|aac|flac|aif|aiff)(?:[?#]|$)/i.test(item.preview)
71
+ ? item.preview : null;
72
+ }
73
+
74
+ function localAudioPath(env, item) {
75
+ const local = readLocalLibraryItem(env, item.category, item.id);
76
+ return local?.mediaFile ? path.join(local.libraryDir, local.mediaFile) : null;
77
+ }
78
+
79
+ function canServeAudio(env, item, base) {
80
+ return item.category === 'audio' && item.state !== 'locked'
81
+ && Boolean(localAudioPath(env, item) || audioFile(item.files)?.local_path
82
+ || audioFile(item.files)?.url || (base && audioFile(item.files)?.key)
83
+ || (audioPreview(item) && (base || /^https?:\/\//i.test(item.preview))));
84
+ }
85
+
64
86
  export async function startBrowseServer({
65
87
  env = process.env,
66
88
  fetchImpl = fetch,
@@ -82,20 +104,32 @@ export async function startBrowseServer({
82
104
 
83
105
  if (pathname === '/api/items' && req.method === 'GET') {
84
106
  const state = await composeState({ env, fetchImpl });
85
- return sendJson(res, 200, state);
107
+ return sendJson(res, 200, {
108
+ ...state,
109
+ items: state.items.map((item) => {
110
+ const visibleItem = { ...item, mediaAvailable: canServeAudio(env, item, state.base) };
111
+ if (item.state !== 'locked') return visibleItem;
112
+ const { files, ...visible } = item;
113
+ return { ...visible, mediaAvailable: false };
114
+ }),
115
+ });
86
116
  }
87
117
 
88
118
  // 試聴: 音源など実体ファイルへの参照。リモート(url 型 / リモート base)は 302 で
89
119
  // 実体 URL へ委ね、ローカル base は静的配信する。/thumb と同じ解決規則(fetch-file 共有)。
90
120
  if (pathname.startsWith('/media/') && req.method === 'GET') {
91
121
  const id = decodeURIComponent(pathname.slice('/media/'.length));
92
- const catalog = await loadCatalog({ env, fetchImpl });
93
- const item = catalog.items.find((entry) => entry.id === id);
94
- const file = item?.files?.find((f) => /\.(mp3|wav|m4a|ogg)$/i.test(f.name ?? '')) ?? item?.files?.[0];
95
- const ref = file?.url ?? file?.key;
122
+ const state = await composeState({ env, fetchImpl });
123
+ const item = state.items.find((entry) => entry.id === id && entry.category === 'audio');
124
+ if (!item || item.state === 'locked') return res.writeHead(404).end();
125
+ const localPath = localAudioPath(env, item);
126
+ if (localPath) return await serveStaticFile(res, localPath);
127
+ const file = audioFile(item.files);
128
+ if (file?.local_path) return await serveStaticFile(res, file.local_path);
129
+ const ref = file?.url ?? file?.key ?? audioPreview(item);
96
130
  if (!ref) return res.writeHead(404).end();
97
- const base = resolveEffectiveBase(env, catalog);
98
- const resolved = resolvePreviewLocation(base, ref);
131
+ if (!state.base && !/^https?:\/\//i.test(ref)) return res.writeHead(404).end();
132
+ const resolved = resolvePreviewLocation(state.base, ref);
99
133
  if (!resolved) return res.writeHead(404).end();
100
134
  if (resolved.remote) {
101
135
  res.writeHead(302, { location: resolved.location });
@@ -4,7 +4,10 @@
4
4
 
5
5
  import { readFile, mkdir, writeFile } from 'node:fs/promises';
6
6
  import { catalogCachePath, resolveAkariHome, resolveCatalogSource } from './env.mjs';
7
+ import { join } from 'node:path';
7
8
  import { loadInstalledItems, mergeInstalledItems } from './installed.mjs';
9
+ import { fetchTimed, readTimedJson } from './fetch-file.mjs';
10
+ import { assetTier } from './tier.mjs';
8
11
 
9
12
  function normalizeCatalog(catalog) {
10
13
  if (!catalog || !Array.isArray(catalog.items)) {
@@ -15,37 +18,68 @@ function normalizeCatalog(catalog) {
15
18
 
16
19
  export async function readCatalogCache(env = process.env) {
17
20
  try {
18
- return normalizeCatalog(JSON.parse(await readFile(catalogCachePath(env), 'utf8')));
21
+ return publicCatalog(normalizeCatalog(JSON.parse(await readFile(catalogCachePath(env), 'utf8'))));
19
22
  } catch {
20
23
  return null;
21
24
  }
22
25
  }
23
26
 
27
+ function publicCatalog(catalog) {
28
+ return {
29
+ ...catalog,
30
+ items: catalog.items.map(item => {
31
+ if (item.state !== 'locked' && assetTier(item) !== 'pro') return item;
32
+ const { files, ...visible } = item;
33
+ return visible;
34
+ }),
35
+ };
36
+ }
37
+
38
+ /** The update preference is the shared source for shell and resolver automatic requests. */
39
+ export async function automaticChecksEnabled(env = process.env) {
40
+ try {
41
+ const settings = JSON.parse(await readFile(join(resolveAkariHome(env), 'update-preferences.json'), 'utf8'));
42
+ return settings.autoCheck !== false;
43
+ } catch {
44
+ return true;
45
+ }
46
+ }
47
+
48
+ export async function catalogNetworkAllowed(intent = 'user', env = process.env) {
49
+ return intent !== 'automatic' || await automaticChecksEnabled(env);
50
+ }
51
+
24
52
  export async function cacheCatalog(env = process.env, catalog) {
25
53
  const home = resolveAkariHome(env);
26
54
  await mkdir(home, { recursive: true });
27
- await writeFile(catalogCachePath(env), `${JSON.stringify(catalog, null, 2)}\n`);
55
+ await writeFile(catalogCachePath(env), `${JSON.stringify(publicCatalog(normalizeCatalog(catalog)), null, 2)}\n`);
28
56
  }
29
57
 
30
58
  /**
31
59
  * カタログを読む。リモート取得が失敗した場合(オフライン等)はローカルキャッシュへ
32
60
  * フォールバックする(黙って劣化させるのではなく、キャッシュが無ければ明示的に失敗する)。
33
61
  */
34
- export async function loadCatalog({ env = process.env, fetchImpl = fetch, includeInstalled = true } = {}) {
62
+ export async function loadCatalog({ env = process.env, fetchImpl = fetch, includeInstalled = true, intent = 'user', timeouts, fallbackToCache = true } = {}) {
35
63
  const source = resolveCatalogSource(env);
36
64
  const installedItems = includeInstalled ? await loadInstalledItems(env) : [];
37
65
  let catalog;
38
66
 
67
+ if (source.kind === 'url' && !await catalogNetworkAllowed(intent, env)) {
68
+ catalog = await readCatalogCache(env) ?? { schema: 'akari-assets-catalog/v0', version: null, base: null, items: [] };
69
+ return includeInstalled ? mergeInstalledItems(catalog, installedItems) : catalog;
70
+ }
71
+
39
72
  if (source.kind === 'file') {
40
73
  const raw = await readFile(source.value, 'utf8');
41
74
  catalog = normalizeCatalog(JSON.parse(raw));
42
75
  } else {
43
76
  try {
44
- const res = await fetchImpl(source.value);
77
+ const { response: res, controller } = await fetchTimed(source.value, { fetchImpl, timeouts, label: 'カタログ' });
45
78
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
46
- catalog = normalizeCatalog(await res.json());
79
+ catalog = normalizeCatalog(await readTimedJson(res, controller, { timeouts, label: 'カタログ' }));
47
80
  await cacheCatalog(env, catalog);
48
81
  } catch (error) {
82
+ if (!fallbackToCache) throw new Error('カタログの取得に失敗しました: ' + (error instanceof Error ? error.message : String(error)));
49
83
  const cached = await readCatalogCache(env);
50
84
  if (cached) {
51
85
  catalog = cached;
@@ -64,4 +98,23 @@ export async function loadCatalog({ env = process.env, fetchImpl = fetch, includ
64
98
  return includeInstalled ? mergeInstalledItems(catalog, installedItems) : catalog;
65
99
  }
66
100
 
101
+ /** Resolve uses a valid local cache first; listing/sync remains responsible for freshness. */
102
+ export async function loadCatalogForResolve(ref, { env = process.env, fetchImpl = fetch, timeouts } = {}) {
103
+ // ref は category/id か bare id(resolve.mjs の findCatalogItem と同じ受け方)
104
+ const matches = typeof ref === 'string' && ref.includes('/')
105
+ ? item => `${item.category}/${item.id}` === ref
106
+ : item => item.id === ref;
107
+ const installedItems = await loadInstalledItems(env);
108
+ if (installedItems.some(matches)) {
109
+ return { schema: 'akari-assets-catalog/v0', version: null, base: null, items: installedItems };
110
+ }
111
+ if (resolveCatalogSource(env).kind === 'url') {
112
+ const cached = await readCatalogCache(env);
113
+ if (cached && cached.items.some(matches)) {
114
+ return mergeInstalledItems(cached, installedItems);
115
+ }
116
+ }
117
+ return loadCatalog({ env, fetchImpl, timeouts, fallbackToCache: false });
118
+ }
119
+
67
120
  export { resolveEffectiveBase } from './env.mjs';
@@ -6,7 +6,9 @@
6
6
  // 自体は別途 fail-closed で拒否するので、ここで例外を投げて全体を止める必要はない)。
7
7
 
8
8
  import { readFile } from 'node:fs/promises';
9
+ import { automaticChecksEnabled } from './catalog.mjs';
9
10
  import { resolveCredentialsPath, resolveEntitlementsUrl } from './env.mjs';
11
+ import { fetchTimed, readTimedJson } from './fetch-file.mjs';
10
12
 
11
13
  export async function readStoreCredentials(env = process.env) {
12
14
  try {
@@ -19,34 +21,39 @@ export async function readStoreCredentials(env = process.env) {
19
21
  }
20
22
 
21
23
  /**
22
- * 購入済み商品 id の Set + 取得結果。取得できない場合も ids は空集合
24
+ * 購入済み商品 id の Set + パス情報 + 取得結果。取得できない場合も ids は空集合、pass は null
23
25
  * (= 無料のみ使える)のままで、既存のフォールバックを変えない。
24
- * @returns {Promise<{ ids: Set<string>, status: 'ok'|'no_credentials'|'unauthorized'|'error' }>}
26
+ * @returns {Promise<{ ids: Set<string>, status: 'ok'|'no_credentials'|'unauthorized'|'error', pass: { tier: unknown, seat_no: unknown } | null }>}
25
27
  */
26
- export async function fetchEntitlements({ env = process.env, fetchImpl = fetch } = {}) {
28
+ export async function fetchEntitlements({ env = process.env, fetchImpl = fetch, intent = 'user', timeouts } = {}) {
29
+ if (intent === 'automatic' && !await automaticChecksEnabled(env)) {
30
+ return { ids: new Set(), status: 'no_credentials', pass: null };
31
+ }
27
32
  const credentials = await readStoreCredentials(env);
28
- if (!credentials) return { ids: new Set(), status: 'no_credentials' };
33
+ if (!credentials) return { ids: new Set(), status: 'no_credentials', pass: null };
29
34
 
30
35
  const url = resolveEntitlementsUrl(env, credentials);
31
36
  try {
32
- const res = await fetchImpl(url, { headers: { authorization: `Bearer ${credentials.token}` } });
37
+ const { response: res, controller } = await fetchTimed(url, { fetchImpl, request: { headers: { authorization: `Bearer ${credentials.token}` } }, timeouts, label: '権利確認' });
33
38
  if (res.status === 401 || res.status === 403) {
34
- return { ids: new Set(), status: 'unauthorized' };
39
+ return { ids: new Set(), status: 'unauthorized', pass: null };
35
40
  }
36
41
  let data;
37
42
  try {
38
- data = await res.json();
43
+ data = await readTimedJson(res, controller, { timeouts, label: '権利確認' });
39
44
  } catch {
40
- return { ids: new Set(), status: 'error' };
45
+ return { ids: new Set(), status: 'error', pass: null };
41
46
  }
42
47
  if (data?.error === 'token_revoked') {
43
- return { ids: new Set(), status: 'unauthorized' };
48
+ return { ids: new Set(), status: 'unauthorized', pass: null };
44
49
  }
45
- if (!res.ok) return { ids: new Set(), status: 'error' };
50
+ if (!res.ok) return { ids: new Set(), status: 'error', pass: null };
46
51
  const list = Array.isArray(data?.entitlements) ? data.entitlements : [];
47
52
  const ids = list.map((entry) => (typeof entry === 'string' ? entry : entry?.product_id ?? entry?.id)).filter(Boolean);
48
- return { ids: new Set(ids), status: 'ok' };
49
- } catch {
50
- return { ids: new Set(), status: 'error' };
53
+ const pass = data?.pass !== null && typeof data?.pass === 'object' && !Array.isArray(data.pass)
54
+ ? { tier: data.pass.tier, seat_no: data.pass.seat_no } : null;
55
+ return { ids: new Set(ids), status: 'ok', pass };
56
+ } catch (error) {
57
+ return { ids: new Set(), status: 'error', pass: null, error: error instanceof Error ? error.message : String(error) };
51
58
  }
52
59
  }
@@ -1,5 +1,5 @@
1
1
  // resolve() が投げるエラーの型。code で分岐できるようにする
2
- // ('locked' | 'not_found' | 'invalid_catalog_item' | 'integrity' | 'validation' | 'download_failed')。
2
+ // ('locked' | 'not_found' | 'ambiguous_id' | 'invalid_catalog_item' | 'integrity' | 'validation' | 'download_failed')。
3
3
 
4
4
  export class AssetResolverError extends Error {
5
5
  constructor(message, code) {