akari-video 1.3.0-beta.5 → 1.3.0-beta.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.3.0-beta.5",
3
+ "version": "1.3.0-beta.6",
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": {
@@ -374,7 +374,7 @@ catalog に載せる素材は、取得元のライセンスが CC0 相当(帰
374
374
  | v1 category | 形の定義 | 判定 | 旧 category |
375
375
  |---|---|---|---|
376
376
  | `overlay` | 時間を持つ HTML 断片 | `fragment.html` を `overlays[]` から合成。断片ルートに時刻を置かず、素材本来の長さは `data-akari-natural-duration`(秒)に記録 | `telop` / `motion` |
377
- | `still` | 時間を持たない HTML シート | `fragment.html`。決定的スクショで画像に焼く | `thumbnail` |
377
+ | `still` | 時間を持たない静止画(画像 1 枚 + 任意でツマミ用の HTML シート) | **既定は直下の画像(`preview.png` 以外の 1 枚)を `sources[]` の media クリップとして置く**。`fragment.html` は色相などのツマミを使うとき・画像が 1 枚に決まらないときだけ(2026-10-12 改訂)。HTML シートは決定的スクショで画像に焼ける | `thumbnail` |
378
378
  | `scene3d` | 3D モデル + 表示断片、またはベイクレシピ | `fragment.html` + glTF、または `scene.py` | `3d` |
379
379
  | `audio` | 音声トラックに載るバイナリ | `edit.json` の `audio.bgm` / `sfx` が参照 | `audio`(変更なし) |
380
380
  | `broll` | 映像トラックに載る実写バイナリ | `sources[]` が参照 | `broll`(変更なし) |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.3.0-beta.5",
3
+ "version": "1.3.0-beta.6",
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": [
@@ -117,5 +117,7 @@ export function readLocalLibraryItem(env, category, id) {
117
117
  mediaFile: primaryMediaFile(category, files), libraryDir,
118
118
  addedAt: new Date(info.birthtimeMs > 0 ? info.birthtimeMs : info.mtimeMs).toISOString(),
119
119
  creditText, warnings, state: 'cached',
120
+ // meta.json の無い取り寄せ(AKARI Sounds は音源ファイルだけを配る)を合成側が見分ける。
121
+ metaOk: Object.keys(meta).length > 0,
120
122
  };
121
123
  }
@@ -82,11 +82,17 @@ export async function composeState({ env = process.env, fetchImpl = fetch, inten
82
82
  localItems.delete(key);
83
83
  // Preserve remote download descriptors/preview keys for existing consumers.
84
84
  // Local-only assets use the actual directory listing assembled above.
85
- return local ? { ...item, ...local, tier: item.tier ?? assetTier(item), files: item.files ?? local.files, preview: local.libraryDir && local.preview ? local.preview : item.preview ?? local.preview,
86
- ...(item.source === 'installed' ? { source: item.source } : {}) } : item;
85
+ if (!local) return item;
86
+ // meta.json の無い取り寄せは表示用の title / tags をカタログから引き継ぐ。空の tags で上書きすると
87
+ // 効果音の 'sfx' が消え、ライブラリの BGM 側へ混ざっていた(オーナー指摘 2026-10-12)。
88
+ const { metaOk, ...localFields } = local;
89
+ return { ...item, ...localFields,
90
+ ...(metaOk ? {} : { title: item.title ?? localFields.title, tags: item.tags ?? localFields.tags }),
91
+ tier: item.tier ?? assetTier(item), files: item.files ?? local.files, preview: local.libraryDir && local.preview ? local.preview : item.preview ?? local.preview,
92
+ ...(item.source === 'installed' ? { source: item.source } : {}) };
87
93
  });
88
94
  const localOnlyKeys = new Set(localItems.keys());
89
- merged.push(...[...localItems.values()].filter(Boolean));
95
+ merged.push(...[...localItems.values()].filter(Boolean).map(({ metaOk, ...local }) => local));
90
96
  const items = merged.map((item) => {
91
97
  const key = `${item.category}/${item.id}`;
92
98
  const tier = localOnlyKeys.has(key) && !Object.hasOwn(item, 'tier')
@@ -185,3 +185,19 @@ test('cached Lab items retain remote preview and file descriptors', async t => {
185
185
  assert.deepEqual(item.files, remote.files);
186
186
  assert.ok(item.libraryDir);
187
187
  });
188
+
189
+ test('downloaded catalog audio without meta.json keeps the catalog tags and title (sfx stays out of BGM)', async t => {
190
+ const { env, catalog, catalogPath } = fixture(t);
191
+ catalog.items.push({ id: 'sfx-x', category: 'audio', title: 'ベルの効果音', tags: ['bell', 'sfx'],
192
+ license: { spdx: 'CC0-1.0' }, version: 1, tier: 'free', files: [{ name: 'sfx-x.mp3', key: 'audio/sfx-x/v1/sfx-x.mp3', bytes: 5 }] });
193
+ writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
194
+ // AKARI Sounds の取り寄せは音源ファイルだけで meta.json を持たない。
195
+ const dir = path.join(env.AKARI_LIBRARY_ROOT, 'audio', 'sfx-x');
196
+ mkdirSync(dir, { recursive: true });
197
+ writeFileSync(path.join(dir, 'sfx-x.mp3'), 'sound');
198
+ const item = (await composeState({ env })).items.find(row => row.id === 'sfx-x');
199
+ assert.equal(item.state, 'cached');
200
+ assert.ok(item.tags.includes('sfx'));
201
+ assert.equal(item.title, 'ベルの効果音');
202
+ assert.equal(Object.hasOwn(item, 'metaOk'), false);
203
+ });
@@ -359,7 +359,17 @@ export function validateCaptions(captions, edit, analysis, findings, paths, cuts
359
359
  if (caption.time_domain !== "output") {
360
360
  const kept = keptOverlap(caption.start, caption.end, edit?.cuts, caption.src);
361
361
  const ratio = kept / (caption.end - caption.start);
362
- if (ratio < 0.5 - EPSILON) {
362
+ if (kept <= EPSILON) {
363
+ // 話した区間ごとカットで消した字幕は出ないのが正しい結果。エラーにすると GUI で
364
+ // カットするたびに誤報になるため記録だけ残す(オーナー裁定 2026-10-12)。
365
+ addFinding(findings, {
366
+ severity: "info",
367
+ check: "captions.cut-removed",
368
+ message: "the caption is entirely inside removed sections and will not be shown",
369
+ path: itemPath,
370
+ range: { start: caption.start, end: caption.end },
371
+ });
372
+ } else if (ratio < 0.5 - EPSILON) {
363
373
  addFinding(findings, {
364
374
  severity: "error",
365
375
  check: "captions.cut-visibility",
@@ -22,6 +22,12 @@ export interface EditLintFinding {
22
22
  message?: string;
23
23
  check?: string;
24
24
  path?: string;
25
+ range?: {
26
+ start?: number;
27
+ end?: number;
28
+ };
29
+ /** 今回の保存より前の lint にも同じ指摘があった(この保存が起こした問題ではない)。 */
30
+ preexisting?: boolean;
25
31
  }
26
32
  export interface EditLintGateResult {
27
33
  pass: boolean;
@@ -95,6 +101,12 @@ export declare function assertNoCamelCaseTransitionOut(content: string): void;
95
101
  * これは render-cut が読む PASS ゲートを常に最新に保つ、意図した保存後 lint の副作用。
96
102
  */
97
103
  export declare function scheduleProjectLint(projectRoot: string, options?: DeferredLintOptions): void;
104
+ /**
105
+ * 保存前の lint にも有った指摘へ preexisting を付ける。字幕の挿入で配列の番号がずれても
106
+ * 同じ指摘と分かるよう、時刻範囲を持つ指摘は path の番号ではなく範囲で照合する。
107
+ * 基準が無い(初回・lint.json 不在)ときは従来どおり全部を今回の保存の指摘として返す。
108
+ */
109
+ export declare function markPreexistingLintFindings(result: EditLintGateResult, baseline: readonly EditLintFinding[] | undefined): EditLintGateResult;
98
110
  export declare function writeAtomic(destination: string, content: string): Promise<void>;
99
111
  /**
100
112
  * edit-lint をプロセス内実行する。inputOverrides は候補全文のメモリ差し替え。
@@ -27,6 +27,7 @@ exports.writeSavedByStamp = writeSavedByStamp;
27
27
  exports.writeProjectFilesGuarded = writeProjectFilesGuarded;
28
28
  exports.assertNoCamelCaseTransitionOut = assertNoCamelCaseTransitionOut;
29
29
  exports.scheduleProjectLint = scheduleProjectLint;
30
+ exports.markPreexistingLintFindings = markPreexistingLintFindings;
30
31
  exports.writeAtomic = writeAtomic;
31
32
  exports.runEditLint = runEditLint;
32
33
  exports.findEditLintModulePath = findEditLintModulePath;
@@ -72,6 +73,10 @@ const DEFAULT_LINT_DEBOUNCE_MS = 400;
72
73
  const lintTimers = new Map();
73
74
  const lintRevisions = new Map();
74
75
  const lintRunChains = new Map();
76
+ /** プロジェクトごとの直近の保存後 lint の指摘。次の保存の「保存前から有った指摘」の基準にする。 */
77
+ const lastLintFindings = new Map();
78
+ /** debounce 窓の最初の書き込みの直前に取った基準。窓の lint が走るときに消費する。 */
79
+ const pendingLintBaselines = new Map();
75
80
  const dynamicImport = new Function('specifier', 'return import(specifier)');
76
81
  /**
77
82
  * 実ファイルは変更せず、候補全文だけを options.inputOverrides で差し替えて検証する。
@@ -227,6 +232,10 @@ async function writeProjectFilesGuarded(projectRoot, candidates, options = {}) {
227
232
  if (typeof editCandidate === 'string') {
228
233
  assertNoCamelCaseTransitionOut(editCandidate);
229
234
  }
235
+ const lintKey = (0, path_1.resolve)(projectRoot);
236
+ if (!pendingLintBaselines.has(lintKey)) {
237
+ pendingLintBaselines.set(lintKey, lastLintFindings.get(lintKey) ?? await readPersistedLintFindings(projectRoot));
238
+ }
230
239
  for (const [name, text] of Object.entries(candidates)) {
231
240
  if (text === null) {
232
241
  continue;
@@ -288,10 +297,18 @@ function scheduleProjectLint(projectRoot, options = {}) {
288
297
  }
289
298
  const timer = setTimeout(() => {
290
299
  lintTimers.delete(key);
300
+ const baseline = pendingLintBaselines.get(key);
301
+ pendingLintBaselines.delete(key);
291
302
  const previousRun = lintRunChains.get(key) ?? Promise.resolve({ pass: true, errors: [], findings: [] });
292
303
  const currentRun = previousRun
293
304
  .catch(() => ({ pass: true, errors: [], findings: [] }))
294
- .then(() => (options.lintRunner ?? runEditLint)(key));
305
+ .then(() => (options.lintRunner ?? runEditLint)(key))
306
+ .then(result => {
307
+ if (!result.findings.some(finding => finding.check === 'edit-lint.execution')) {
308
+ lastLintFindings.set(key, result.findings);
309
+ }
310
+ return markPreexistingLintFindings(result, baseline);
311
+ });
295
312
  lintRunChains.set(key, currentRun);
296
313
  void currentRun.then(result => lintRevisions.get(key) === revision ? options.onLintResult?.(result) : undefined, error => {
297
314
  console.warn('[edit-store] 保存後 edit-lint の実行に失敗しました(保存は維持します)。', error);
@@ -303,6 +320,45 @@ function scheduleProjectLint(projectRoot, options = {}) {
303
320
  }, options.debounceMs ?? DEFAULT_LINT_DEBOUNCE_MS);
304
321
  lintTimers.set(key, timer);
305
322
  }
323
+ /**
324
+ * 保存前の lint にも有った指摘へ preexisting を付ける。字幕の挿入で配列の番号がずれても
325
+ * 同じ指摘と分かるよう、時刻範囲を持つ指摘は path の番号ではなく範囲で照合する。
326
+ * 基準が無い(初回・lint.json 不在)ときは従来どおり全部を今回の保存の指摘として返す。
327
+ */
328
+ function markPreexistingLintFindings(result, baseline) {
329
+ if (!baseline?.length)
330
+ return result;
331
+ const remaining = new Map();
332
+ for (const finding of baseline) {
333
+ const key = lintFindingIdentity(finding);
334
+ remaining.set(key, (remaining.get(key) ?? 0) + 1);
335
+ }
336
+ const findings = result.findings.map(finding => {
337
+ const key = lintFindingIdentity(finding);
338
+ const count = remaining.get(key) ?? 0;
339
+ if (count === 0)
340
+ return finding;
341
+ remaining.set(key, count - 1);
342
+ return { ...finding, preexisting: true };
343
+ });
344
+ return { ...result, findings };
345
+ }
346
+ function lintFindingIdentity(finding) {
347
+ const file = finding.path?.split('#', 1)[0] ?? '';
348
+ const range = finding.range;
349
+ const where = range && Number.isFinite(range.start) && Number.isFinite(range.end)
350
+ ? `${file}@${range.start}-${range.end}` : finding.path ?? '';
351
+ return JSON.stringify([finding.severity ?? '', finding.check ?? '', finding.message ?? '', where]);
352
+ }
353
+ async function readPersistedLintFindings(projectRoot) {
354
+ try {
355
+ const parsed = JSON.parse(await fs_1.promises.readFile((0, path_1.join)(projectRoot, '.akari', 'lint.json'), 'utf8'));
356
+ return Array.isArray(parsed.findings) ? parsed.findings : undefined;
357
+ }
358
+ catch {
359
+ return undefined;
360
+ }
361
+ }
306
362
  // 同じ宛先への書き込みは 1 本ずつ直列化する。
307
363
  // 一時ファイル名が PID だけだった頃は、同一プロセス内で書き込みが 2 本重なると同じ
308
364
  // 一時ファイルを奪い合い、短い方が truncate した後に長い方の書き込みがその先へ着地して
@@ -8,7 +8,7 @@
8
8
  "bundle:frame-engine": "node ../../scripts/gen-frame-engine-bundles.mjs",
9
9
  "check:frame-engine-drift": "node ../../scripts/gen-frame-engine-bundles.mjs --check",
10
10
  "assert-zero-readback": "node scripts/assert-zero-readback.mjs",
11
- "test": "node --test test/*.test.mjs",
11
+ "test": "node --import ../../scripts/ci/default-test-language.mjs --test test/*.test.mjs",
12
12
  "ci:required": "npm test && npm run assert-zero-readback"
13
13
  },
14
14
  "dependencies": {
@@ -3,6 +3,7 @@
3
3
  "lint.check.cuts.transitionOutNonAdjacent": "There is a gap before the clip after this transition. Close the gap or remove the transition.",
4
4
  "lint.check.cuts.transitionOutZeroOverlap": "This transition has no spare source footage to overlap, so it has no effect. Adjust the clip trim or remove the transition.",
5
5
  "lint.check.cuts.transitionOutLayerEvacuated": "This clip is routed through PiP, so its transition will not be exported. Resolve the overlap or remove the transition.",
6
+ "lint.check.captions.cutVisibility": "More than half of this caption's display time falls in a cut section, so it barely appears. Move its start and end into the kept section, or delete it.",
6
7
  "lint.check.captions.overlap": "Caption display times overlap. Move the caption start or end points on the timeline.",
7
8
  "lint.check.captions.outputDomainExceedsDuration": "An output-timed caption extends beyond the video duration. It will be clipped at the end during export.",
8
9
  "lint.check.cuts.trackOverlap": "Clips overlap on the same video track. Move them apart.",
@@ -1,4 +1,6 @@
1
1
  {
2
+ "preview.emphasisWords.validation.expectedArrayWarning": "[akari-preview] Ignored emphasis_words because it is not an array",
3
+ "preview.emphasisWords.validation.invalidItemWarning": "[akari-preview] Ignored invalid emphasis_words[{index}]",
2
4
  "preview.audioMeter.command.openAudioMeterAction": "Open audio meter",
3
5
  "preview.audioMeter.display.baseAudioUnavailableLegacyPreviewError": "Base audio is unavailable in legacy preview",
4
6
  "preview.audioMeter.display.clearClippingIndicatorAction": "Clear clipping indicator",
@@ -1735,7 +1735,7 @@
1735
1735
  "surfaces.onboarding.askConnectionCopyEvenConnectedAiSeeFlowExample.message": "You can watch the example without connecting an AI partner.",
1736
1736
  "surfaces.onboarding.askConnectionCopyConnectAiLaterList.message": "You can connect your AI partner from this list later.",
1737
1737
  "surfaces.onboarding.preparing.label": "Preparing...",
1738
- "surfaces.onboarding.inviteMarkupLikeMakeOneTogetherAddTitlesCaptions.message": "<h2>Make your first video together</h2><p>Add a title and captions to the prepared video, then export. It takes about five minutes, and you can stop anytime.</p>{error}<div class=\"ao-actions\"><button class=\"primary\" data-ao=\"start\">{action}</button></div><div class=\"ao-secondary\"><button data-ao=\"later\">Start on my own later</button></div>",
1738
+ "surfaces.onboarding.inviteMarkupLikeMakeOneTogetherAddTitlesCaptions.message": "<h2>Make your first video together</h2><p>Add a title and captions to the prepared video, then export. It takes about five minutes, and you can stop anytime.</p>{error}{switchNotice}<div class=\"ao-actions\"><button class=\"primary\" data-ao=\"start\">{action}</button></div><div class=\"ao-secondary\"><button data-ao=\"later\">Start on my own later</button></div>",
1739
1739
  "surfaces.home.homeDialogChannelName.label": "Channel name",
1740
1740
  "surfaces.home.welcomeCardOpenFolderAdvanced.button": "Open folder… (advanced)",
1741
1741
  "surfaces.home.welcomeCardOpenProjectLauncher.button": "Open project launcher",
@@ -1927,6 +1927,8 @@
1927
1927
  "surfaces.tools.detectSpeechAnalyzerCheckSpeechanalyzerTerms.label": "Check the SpeechAnalyzer terms of use",
1928
1928
  "surfaces.tools.defaultOpenPathOpenCommandFailed.message": "Could not run the open command.",
1929
1929
  "surfaces.onboarding.inviteMarkupOnceAgain.label": "Try again",
1930
+ "surfaces.onboarding.inviteMarkupAgreeAndStart.label": "Agree and start",
1931
+ "surfaces.onboarding.inviteMarkupPartnerSwitchWarning.message": "The guide will switch from your current project, ‘{current}’, to the guide project (the partner's current work will stop).",
1930
1932
  "surfaces.onboarding.inviteMarkupTry.label": "Try it",
1931
1933
  "surfaces.channelDesign.genre.title": "What do you film?",
1932
1934
  "surfaces.channelDesign.genre.hint": "Choose your main topic. You can change it later.",
@@ -3,6 +3,7 @@
3
3
  "lint.check.cuts.transitionOutNonAdjacent": "トランジションの次のクリップとの間にすき間があります。すき間を詰めるか、トランジションを削除してください。",
4
4
  "lint.check.cuts.transitionOutZeroOverlap": "トランジションを宣言していますが、のりしろにできる素材の余りがないため効きません。素材のトリムを調整するか、トランジションを削除してください。",
5
5
  "lint.check.cuts.transitionOutLayerEvacuated": "このクリップは PiP 経路へ退避されているため、宣言したトランジションは書き出されません。重なりを解消するか、トランジションを削除してください。",
6
+ "lint.check.captions.cutVisibility": "字幕の表示時間の半分以上がカットした区間にかかっていて、ほとんど映りません。字幕の開始・終了を残した区間に合わせるか、字幕を消してください。",
6
7
  "lint.check.captions.overlap": "字幕の表示時間が重なっています。タイムラインで字幕の開始・終了位置をずらしてください。",
7
8
  "lint.check.captions.outputDomainExceedsDuration": "出力時間の字幕が動画総尺を超えています。書き出しでは動画終端までにクランプして表示されます。",
8
9
  "lint.check.cuts.trackOverlap": "同じ映像トラック上でクリップが重なっています。クリップを重ならない位置へ移動してください。",
@@ -1,4 +1,6 @@
1
1
  {
2
+ "preview.emphasisWords.validation.expectedArrayWarning": "[akari-preview] emphasis_words を無視しました(配列ではありません)",
3
+ "preview.emphasisWords.validation.invalidItemWarning": "[akari-preview] emphasis_words[{index}] を無視しました(要素が不正です)",
2
4
  "preview.audioMeter.command.openAudioMeterAction": "音声メーターを開く",
3
5
  "preview.audioMeter.display.baseAudioUnavailableLegacyPreviewError": "legacy プレビューでは土台の音声は含まれません",
4
6
  "preview.audioMeter.display.clearClippingIndicatorAction": "クリップ表示を解除",
@@ -1735,7 +1735,7 @@
1735
1735
  "surfaces.onboarding.askConnectionCopyEvenConnectedAiSeeFlowExample.message": "AI とつながなくても、流れはお手本で見られます。",
1736
1736
  "surfaces.onboarding.askConnectionCopyConnectAiLaterList.message": "お使いの AI は、あとでこの一覧からつなげます。",
1737
1737
  "surfaces.onboarding.preparing.label": "準備しています…",
1738
- "surfaces.onboarding.inviteMarkupLikeMakeOneTogetherAddTitlesCaptions.message": "<h2>一緒に 1 本、つくってみませんか?</h2><p>用意した動画に、AI でタイトルと字幕を入れて、書き出すところまで。5 分ほどです。途中でやめても大丈夫です。</p>{error}<div class=\"ao-actions\"><button class=\"primary\" data-ao=\"start\">{action}</button></div><div class=\"ao-secondary\"><button data-ao=\"later\">後で自分で始める</button></div>",
1738
+ "surfaces.onboarding.inviteMarkupLikeMakeOneTogetherAddTitlesCaptions.message": "<h2>一緒に 1 本、つくってみませんか?</h2><p>用意した動画に、AI でタイトルと字幕を入れて、書き出すところまで。5 分ほどです。途中でやめても大丈夫です。</p>{error}{switchNotice}<div class=\"ao-actions\"><button class=\"primary\" data-ao=\"start\">{action}</button></div><div class=\"ao-secondary\"><button data-ao=\"later\">後で自分で始める</button></div>",
1739
1739
  "surfaces.home.homeDialogChannelName.label": "チャンネル名",
1740
1740
  "surfaces.home.welcomeCardOpenFolderAdvanced.button": "フォルダを開く…(上級者向け)",
1741
1741
  "surfaces.home.welcomeCardOpenProjectLauncher.button": "プロジェクト・ランチャーを開く",
@@ -1927,6 +1927,8 @@
1927
1927
  "surfaces.tools.detectSpeechAnalyzerCheckSpeechanalyzerTerms.label": "SpeechAnalyzer の利用条件を確認してください",
1928
1928
  "surfaces.tools.defaultOpenPathOpenCommandFailed.message": "open コマンドに失敗しました。",
1929
1929
  "surfaces.onboarding.inviteMarkupOnceAgain.label": "もう一度",
1930
+ "surfaces.onboarding.inviteMarkupAgreeAndStart.label": "同意してはじめる",
1931
+ "surfaces.onboarding.inviteMarkupPartnerSwitchWarning.message": "いまのプロジェクト『{current}』から、ガイド用のプロジェクトに切り替わります(作業中のパートナーは止まります)",
1930
1932
  "surfaces.onboarding.inviteMarkupTry.label": "やってみる",
1931
1933
  "surfaces.channelDesign.genre.title": "何を撮りますか?",
1932
1934
  "surfaces.channelDesign.genre.hint": "いちばん多いものを 1 つ。あとで変えられます。",
@@ -9,7 +9,7 @@
9
9
  "check:frame-engine-drift": "node ../../scripts/gen-frame-engine-bundles.mjs --check",
10
10
  "build-fixture": "node scripts/build-fixture.mjs",
11
11
  "assert-no-intermediates": "node scripts/assert-no-intermediates.mjs",
12
- "test": "node --test test/*.test.mjs",
12
+ "test": "node --import ../../scripts/ci/default-test-language.mjs --test test/*.test.mjs",
13
13
  "ci:required": "npm test",
14
14
  "ci:fixture": "node scripts/build-fixture.mjs"
15
15
  },
@@ -2132,7 +2132,7 @@ window.akari.threeRuntime = (() => {
2132
2132
  }
2133
2133
 
2134
2134
  // ── 標準ツマミ: モデルの姿勢(--akari-3d-rot-x/y/z)と動きのプリセット(--akari-3d-clip)──
2135
- // 契約: 内部リポ planning/contract-2026-10-11-3d-place-v0.md §2.1。
2135
+ // 契約: 内部リポの 3D 配置 v0 の設計契約 §2.1。
2136
2136
  // pan/zoom と同じく宣言キー不要で、draw のたびに canvas の computed style から読む。
2137
2137
  // - 軸は水平を保ったカメラ基準(モデル読み込み完了時点。X = 画面の横(水平)・Y = 真上・Z = 手前。levelledCameraFrame)
2138
2138
  // - 合成は「X → Y → Z の順にカメラ基準の固定軸で回す」(行列 Rz·Ry·Rx = Euler 順 "ZYX")