akari-video 1.5.2-beta.1 → 1.5.2-beta.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.
- package/README.md +6 -1
- package/bin/akari.mjs +2 -0
- package/package.json +1 -1
- package/src/geo-command.mjs +18 -0
- package/src/messages.mjs +1 -1
- package/src/project-state.mjs +1 -1
- package/src/repo-assets.mjs +2 -0
- package/src/status-core/status.mjs +3 -3
- package/src/status-core/workspace.mjs +1 -1
- package/src/vendor-sources.mjs +4 -0
- package/vendor/.akari-capability-sources.json +43 -43
- package/vendor/packages/akari-launcher/README.md +6 -1
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-tools/README.md +1 -0
- package/vendor/packages/akari-tools/bin/geo.mjs +176 -0
- package/vendor/packages/akari-tools/package.json +1 -0
- package/vendor/packages/akari-tools/src/geo/png.mjs +164 -0
- package/vendor/packages/akari-tools/src/geo/point.mjs +202 -0
- package/vendor/packages/akari-tools/src/geo/raster.mjs +177 -0
- package/vendor/packages/akari-tools/src/geo/terrain.mjs +243 -0
- package/vendor/packages/akari-tools/src/geo/tiles.mjs +114 -0
- package/vendor/packages/audio-library-setup/bin/beat-grid.mjs +3 -3
- package/vendor/packages/audio-library-setup/bin/suggest-bgm.mjs +2 -2
- package/vendor/packages/edit-lint/src/lint/fragment-css-collision.mjs +2 -2
- package/vendor/packages/edit-lint/src/lint/overlays.mjs +396 -5
- package/vendor/packages/i18n/locales/en/cli.json +1 -0
- package/vendor/packages/i18n/locales/en/skills.json +14 -14
- package/vendor/packages/i18n/locales/en/tools.json +3 -1
- package/vendor/packages/i18n/locales/ja/cli.json +1 -0
- package/vendor/packages/i18n/locales/ja/skills.json +14 -14
- package/vendor/packages/i18n/locales/ja/tools.json +3 -1
- package/vendor/packages/overlay-runtime/runtimes.mjs +67 -2
- package/vendor/packages/overlay-runtime/src/fragment-css-scope.mjs +660 -0
- package/vendor/packages/overlay-runtime/src/interaction.js +2 -2
- package/vendor/packages/overlay-runtime/src/motion-vocab.css +1 -1
- package/vendor/packages/overlay-runtime/src/overlay-runtime.js +21 -2
- package/vendor/packages/overlay-runtime/src/three-runtime.js +1141 -4
- package/vendor/packages/overlay-runtime/src/viewport-units.js +1 -1
- package/vendor/packages/project-scaffold/src/index.mjs +1 -1
- package/vendor/packages/schemas/skill-renames.mjs +48 -5
- package/vendor/packages/schemas/test/validate-cut-candidates.test.mjs +2 -2
- package/vendor/packages/schemas/test/validate-semantic-keep-plan.test.mjs +2 -2
- package/vendor/skills/akari/SKILL.md +3 -3
- package/vendor/skills/analyze-footage/SKILL.md +1 -1
- package/vendor/skills/analyze-footage/events-and-hooks.md +1 -1
- package/vendor/skills/{declare-audio → analyze-music}/SKILL.md +4 -4
- package/vendor/skills/{declare-audio → analyze-music}/after-save.md +2 -2
- package/vendor/skills/analyze-project/SKILL.md +3 -3
- package/vendor/skills/analyze-project/collect.md +1 -1
- package/vendor/skills/analyze-project/decision-record.md +8 -8
- package/vendor/skills/analyze-project/interpretation.md +3 -3
- package/vendor/skills/analyze-project/interview.md +1 -1
- package/vendor/skills/analyze-project/references/open-questions-examples.md +4 -4
- package/vendor/skills/{overlay-authoring → author-overlay}/3d.md +1 -1
- package/vendor/skills/{overlay-authoring → author-overlay}/SKILL.md +2 -2
- package/vendor/skills/{overlay-authoring → author-overlay}/thumbnail.md +1 -1
- package/vendor/skills/author-scene3d/SKILL.md +1 -1
- package/vendor/skills/author-scene3d/live.md +105 -1
- package/vendor/skills/{design-world → author-world}/SKILL.md +2 -2
- package/vendor/skills/edit-to-beat/SKILL.md +5 -5
- package/vendor/skills/edit-to-beat/generator.md +1 -1
- package/vendor/skills/generate-image/SKILL.md +1 -1
- package/vendor/skills/generate-image/image.md +1 -1
- package/vendor/skills/generate-narration/avatar-voice.md +2 -2
- package/vendor/skills/generate-video/SKILL.md +1 -1
- package/vendor/skills/generate-video/video.md +1 -1
- package/vendor/skills/harvest-asset/SKILL.md +1 -1
- package/vendor/skills/{record-playbook → harvest-playbook}/SKILL.md +4 -4
- package/vendor/skills/{record-playbook → harvest-playbook}/bin/record-playbook.mjs +2 -2
- package/vendor/skills/{record-playbook → harvest-playbook}/test/record-beats.test.mjs +2 -2
- package/vendor/skills/{edit-plan → plan-edit}/SKILL.md +4 -4
- package/vendor/skills/{edit-plan → plan-edit}/approvals-and-generation.md +2 -2
- package/vendor/skills/{edit-plan → plan-edit}/avatar-resolution.md +1 -1
- package/vendor/skills/{edit-plan → plan-edit}/beat-sync.md +3 -3
- package/vendor/skills/{edit-plan → plan-edit}/execution.md +2 -2
- package/vendor/skills/{edit-plan → plan-edit}/expression-selection.md +1 -1
- package/vendor/skills/{edit-plan → plan-edit}/recipe.md +1 -1
- package/vendor/skills/{edit-plan → plan-edit}/report-guide.md +9 -9
- package/vendor/skills/{edit-plan → plan-edit}/workflow.md +3 -3
- package/vendor/skills/{research-plan → plan-video}/SKILL.md +4 -4
- package/vendor/skills/{research-plan → plan-video}/ideate.md +1 -1
- package/vendor/skills/{research-plan → plan-video}/storyboard.md +1 -1
- package/vendor/skills/renames.json +7 -0
- package/vendor/skills/render-cut/SKILL.md +1 -1
- package/vendor/skills/review-cut/SKILL.md +3 -3
- package/vendor/skills/review-cut/workflow.md +1 -1
- package/vendor/skills/setup-connections/SKILL.md +4 -4
- package/vendor/skills/setup-library/SKILL.md +2 -2
- package/vendor/skills/setup-library/audio-gallery.md +1 -1
- package/vendor/skills/setup-library/fetch-and-validate.md +1 -1
- package/vendor/skills/{create-project → setup-project}/SKILL.md +2 -2
- package/vendor/templates/project-default/.claude/skills/README.md +2 -2
- package/vendor/templates/project-default/.opencode/hooks/session-start.mjs +2 -2
- package/vendor/templates/project-default/.opencode/skills/analyze-footage.yml +1 -1
- package/vendor/templates/project-default/.opencode/skills/{edit-plan.yml → plan-edit.yml} +2 -2
- package/vendor/templates/project-default/.opencode/skills/{create-project.yml → setup-project.yml} +2 -2
- package/vendor/templates/project-default/AGENTS.md +2 -2
- package/vendor/templates/project-default/CLAUDE.md +2 -2
- /package/vendor/skills/{declare-audio → analyze-music}/launch.md +0 -0
- /package/vendor/skills/{declare-audio → analyze-music}/what-to-declare.md +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/dev-fixtures/test_device_materials.py +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/glass.md +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/lang/en.md +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/lang/ja.md +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/motion.md +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/table.md +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/telop.md +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/text-behind-person.md +0 -0
- /package/vendor/skills/{overlay-authoring → author-overlay}/world.md +0 -0
- /package/vendor/skills/{design-world → author-world}/bin/expand-template.mjs +0 -0
- /package/vendor/skills/{design-world → author-world}/bin/test/expand-template.test.mjs +0 -0
- /package/vendor/skills/{design-world → author-world}/bin/test/package.json +0 -0
- /package/vendor/skills/{design-world → author-world}/guide.md +0 -0
- /package/vendor/skills/{design-world → author-world}/templates/browser-to-chat.json +0 -0
- /package/vendor/skills/{design-world → author-world}/templates/paper-to-browser.json +0 -0
- /package/vendor/skills/{design-world → author-world}/templates/street-to-room.json +0 -0
- /package/vendor/skills/{design-world → author-world}/world.md +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/autonomy.md +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/beats.md +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/candidate-core.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/canonical-json.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/contract-semantics.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/errors.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/generated/contract-validators.cjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/generated/runtime/equal.cjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/generated/runtime/fast-deep-equal-index-9a360c74.cjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/generated/runtime/licenses/ajv.txt +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/generated/runtime/licenses/fast-deep-equal.txt +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/generated/runtime/ucs2length.cjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/propose-cut-candidates.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/runtime-support.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/test/candidate-core.test.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/test/contract-validators-regen.test.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/test/errors.test.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/test/l1-real-media.test.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/test/propose-cut-candidates.test.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/test/runtime-support.test.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/bin/test/scale.test.mjs +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/emphasis-detection.md +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/lang/en.md +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/lang/ja.md +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/plan-json.md +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/references/asset.decisions.example.json +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/references/cut-candidate-policy.a4-conversation-v1.json +0 -0
- /package/vendor/skills/{edit-plan → plan-edit}/references/event.example.json +0 -0
- /package/vendor/skills/{research-plan → plan-video}/competitor.md +0 -0
- /package/vendor/skills/{research-plan → plan-video}/lang/en.md +0 -0
- /package/vendor/skills/{research-plan → plan-video}/lang/ja.md +0 -0
- /package/vendor/skills/{research-plan → plan-video}/shotlist.md +0 -0
- /package/vendor/skills/{research-plan → plan-video}/trends.md +0 -0
package/README.md
CHANGED
|
@@ -58,7 +58,12 @@ zip を使う。`src/store-command.mjs`)/
|
|
|
58
58
|
`packages/asset-resolver` の CLI への薄い委譲で、カタログ合成・entitlements 判定・
|
|
59
59
|
sha256 検証・fail-closed は resolver 側の責務のまま。`src/assets-command.mjs`)/
|
|
60
60
|
`akari clean [project-dir] [--dry-run] [--yes] [--json]`(使い捨ての中間ファイル、保持する
|
|
61
|
-
|
|
61
|
+
正本、判断が必要なものを容量付きで一覧する。既定は一覧のみで、削除可能なものだけを承認後に削除)/
|
|
62
|
+
`akari geo terrain --bbox <西,南,東,北> --out <dir> [--zoom 1-14] [--max-tiles 64]`(国土地理院の
|
|
63
|
+
標高・写真タイルから 3D 地形の `heightmap.png`・`texture.png`・`terrain.json` を作る)/
|
|
64
|
+
`akari geo point --terrain <terrain.json> --lonlat <経度,緯度>[,高さm]`(terrain.json の
|
|
65
|
+
declaration と同じ縮尺で経緯度を 3D 座標 `[x, y, z]` に直す。出典「国土地理院(地理院タイル)」の
|
|
66
|
+
表示が利用条件)。
|
|
62
67
|
|
|
63
68
|
## 拡張キット
|
|
64
69
|
|
package/bin/akari.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { runDoctorCommand } from '../src/doctor-command.mjs';
|
|
|
17
17
|
import { runGenerateCommand } from '../src/generate-command.mjs';
|
|
18
18
|
import { runStoryboardCommand } from '../src/storyboard-command.mjs';
|
|
19
19
|
import { runWorldCommand } from '../src/world-command.mjs';
|
|
20
|
+
import { runGeoCommand } from '../src/geo-command.mjs';
|
|
20
21
|
import { runLintCommand } from '../src/lint-command.mjs';
|
|
21
22
|
import { runPreviewCommand } from '../src/preview-command.mjs';
|
|
22
23
|
import { runSkillsCommand, refreshEntrySkillOnLaunch } from '../src/skills-command.mjs';
|
|
@@ -79,6 +80,7 @@ const commands = {
|
|
|
79
80
|
generate: runGenerateCommand,
|
|
80
81
|
storyboard: runStoryboardCommand,
|
|
81
82
|
world: runWorldCommand,
|
|
83
|
+
geo: runGeoCommand,
|
|
82
84
|
lint: runLintCommand,
|
|
83
85
|
preview: runPreviewCommand,
|
|
84
86
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.5.2-beta.
|
|
3
|
+
"version": "1.5.2-beta.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 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { cliText } from './i18n.mjs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { GEO_CLI_RELATIVE, resolveLauncherAssets } from './repo-assets.mjs';
|
|
7
|
+
|
|
8
|
+
export async function runGeoCommand(args, options = {}) {
|
|
9
|
+
const logError = options.logError ?? ((line) => console.error(line));
|
|
10
|
+
const assets = options.assets ?? resolveLauncherAssets();
|
|
11
|
+
const script = assets.repoRoot ? path.join(assets.repoRoot, GEO_CLI_RELATIVE) : null;
|
|
12
|
+
if (!script || !existsSync(script)) {
|
|
13
|
+
logError(cliText('cli.internal.error.scriptMissing', { command: 'geo' }));
|
|
14
|
+
return { exitCode: 1 };
|
|
15
|
+
}
|
|
16
|
+
const result = (options.spawn ?? spawnSync)(process.execPath, [script, ...args], { stdio: 'inherit' });
|
|
17
|
+
return { exitCode: typeof result?.status === 'number' ? result.status : 1 };
|
|
18
|
+
}
|
package/src/messages.mjs
CHANGED
|
@@ -335,7 +335,7 @@ export function describeCliHelp() {
|
|
|
335
335
|
cliText('cli.help.header.usage'),
|
|
336
336
|
'',
|
|
337
337
|
cliText('cli.help.commands.heading'),
|
|
338
|
-
...['launch', 'preview', 'store', 'sounds', 'doctor', 'speakers', 'toolsVoice', 'update', 'status', 'migrate', 'migrateLook', 'generate', 'storyboard', 'project', 'playbook', 'recipe', 'lint', 'clean'].map((name) => cliText(`cli.help.commands.${name}`)),
|
|
338
|
+
...['launch', 'preview', 'store', 'sounds', 'doctor', 'speakers', 'toolsVoice', 'update', 'status', 'migrate', 'migrateLook', 'generate', 'storyboard', 'project', 'playbook', 'recipe', 'geo', 'lint', 'clean'].map((name) => cliText(`cli.help.commands.${name}`)),
|
|
339
339
|
'',
|
|
340
340
|
cliText('cli.help.developer.heading'),
|
|
341
341
|
...['opencode', 'claude', 'yes', 'version', 'new', 'init', 'other', 'help'].map((name) => cliText(`cli.help.developer.${name}`)),
|
package/src/project-state.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
/**
|
|
5
5
|
* カレントプロジェクトが AKARI Video プロジェクトとしてセットアップ済みかを判定する。
|
|
6
6
|
* `.akari/connections.json` の有無を「セットアップ済み」の判定基準にする
|
|
7
|
-
* (`
|
|
7
|
+
* (`setup-project` / `project-scaffold` が必ずこのファイルを雛形から生成するため)。
|
|
8
8
|
*
|
|
9
9
|
* 例外: `.akari/root.json`(作業場マニフェスト)を持つディレクトリは作業場ルートであって
|
|
10
10
|
* プロジェクトではない。作業場層 `.akari/connections.json`(`packages/creator-root` の
|
package/src/repo-assets.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export const WORD_BOOK_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'b
|
|
|
47
47
|
export const GENERATE_CLI_RELATIVE = path.join('packages', 'generate', 'src', 'cli', 'index.mjs');
|
|
48
48
|
export const STORYBOARD_CLI_RELATIVE = path.join('packages', 'decision-cards', 'render-storyboard-print.mjs');
|
|
49
49
|
export const WORLD_CLI_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'world.mjs');
|
|
50
|
+
export const GEO_CLI_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'geo.mjs');
|
|
50
51
|
export const WORLD_VALIDATOR_RELATIVE = path.join('packages', 'schemas', 'bin', 'validate-world-map.mjs');
|
|
51
52
|
// ブラウザプレビューの実体。`akari preview` はこれを子プロセスで起動するだけで、
|
|
52
53
|
// サーバーを再実装しない(アプリでは extraResources、モノレポでは checkout から解決)。
|
|
@@ -77,6 +78,7 @@ export const LAUNCHER_SUBCOMMAND_EXECUTABLES = [
|
|
|
77
78
|
{ command: 'akari generate', relative: GENERATE_CLI_RELATIVE, resolution: 'launcher-assets' },
|
|
78
79
|
{ command: 'akari storyboard', relative: STORYBOARD_CLI_RELATIVE, resolution: 'launcher-assets' },
|
|
79
80
|
{ command: 'akari world', relative: WORLD_CLI_RELATIVE, resolution: 'resources' },
|
|
81
|
+
{ command: 'akari geo', relative: GEO_CLI_RELATIVE, resolution: 'launcher-assets' },
|
|
80
82
|
{ command: 'akari world check', relative: WORLD_VALIDATOR_RELATIVE, resolution: 'resources' },
|
|
81
83
|
{ command: 'akari preview', relative: PREVIEW_SERVER_SCRIPT_RELATIVE, resolution: 'resources' }
|
|
82
84
|
];
|
|
@@ -418,12 +418,12 @@ function sha256Text(value) {
|
|
|
418
418
|
function routeForStage(stage, review, materials) {
|
|
419
419
|
const routes = {
|
|
420
420
|
state_inconclusive: [null, null],
|
|
421
|
-
not_scaffolded: [{ kind: "human", action: "
|
|
421
|
+
not_scaffolded: [{ kind: "human", action: "setup-project", reason: "project is not scaffolded" }, "setup-project"],
|
|
422
422
|
intake_pending: [{ kind: "human", action: "complete-intake", reason: "project intake is not submitted" }, null],
|
|
423
423
|
analysis_pending: [{ kind: "agent", action: "analyze-footage", reason: `analysis coverage ${materials.covered.size}/${materials.sources.length}` }, "analyze-footage"],
|
|
424
424
|
interpretation_pending: [{ kind: "agent", action: "analyze-project", reason: "multiple materials require project interpretation" }, "analyze-project"],
|
|
425
|
-
planning_pending: [{ kind: "agent", action: "edit
|
|
426
|
-
edit_pending: [{ kind: "agent", action: "edit
|
|
425
|
+
planning_pending: [{ kind: "agent", action: "plan-edit", reason: materials.sources.length === 0 ? "zero-material planning has not started" : "plan.json is absent" }, "plan-edit"],
|
|
426
|
+
edit_pending: [{ kind: "agent", action: "plan-edit", reason: "edit.json is absent" }, "plan-edit"],
|
|
427
427
|
review_pending: [{ kind: "agent", action: "edit-from-review", reason: `open review tickets: ${review.open}` }, "edit-from-review"],
|
|
428
428
|
human_review_pending: [{ kind: "human", action: "resolve-review", reason: `addressed review tickets: ${review.addressed}` }, null],
|
|
429
429
|
lint_pending: [{ kind: "agent", action: "verify-edit", reason: "current edit/review has no fresh PASS lint" }, "verify-edit"],
|
|
@@ -113,7 +113,7 @@ function resolveNextAction(projects, inboxNewCount) {
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
if (projects.length === 0) {
|
|
116
|
-
return { kind: "human", action: "
|
|
116
|
+
return { kind: "human", action: "setup-project", reason: "workspace has no projects yet" };
|
|
117
117
|
}
|
|
118
118
|
return null;
|
|
119
119
|
}
|
package/src/vendor-sources.mjs
CHANGED
|
@@ -84,6 +84,10 @@ export const VENDOR_SOURCES = [
|
|
|
84
84
|
'packages/akari-tools/src/media/whisper-model-candidates.mjs',
|
|
85
85
|
'packages/akari-tools/src/i18n.mjs',
|
|
86
86
|
'packages/akari-tools/package.json',
|
|
87
|
+
// `akari geo terrain` は launcher-assets(npm 配布の vendor)から解決する。
|
|
88
|
+
// 実行に必要な CLI と src/geo だけを追加し、test/ は配布しない。
|
|
89
|
+
'packages/akari-tools/bin/geo.mjs',
|
|
90
|
+
'packages/akari-tools/src/geo',
|
|
87
91
|
'packages/akari-voice/src',
|
|
88
92
|
'packages/akari-voice/package.json',
|
|
89
93
|
// テストは実行時に不要で、分析レポートのテスト専用モジュールを参照するため除外する。
|
|
@@ -169,6 +169,10 @@
|
|
|
169
169
|
"skills/analyze-footage/SKILL.md",
|
|
170
170
|
"skills/analyze-footage/vision-tracks.md",
|
|
171
171
|
"skills/analyze-footage/workflow.md",
|
|
172
|
+
"skills/analyze-music/after-save.md",
|
|
173
|
+
"skills/analyze-music/launch.md",
|
|
174
|
+
"skills/analyze-music/SKILL.md",
|
|
175
|
+
"skills/analyze-music/what-to-declare.md",
|
|
172
176
|
"skills/analyze-project/collect.md",
|
|
173
177
|
"skills/analyze-project/decision-record.md",
|
|
174
178
|
"skills/analyze-project/interpretation.md",
|
|
@@ -177,34 +181,25 @@
|
|
|
177
181
|
"skills/analyze-project/references/open-questions-examples.md",
|
|
178
182
|
"skills/analyze-project/SKILL.md",
|
|
179
183
|
"skills/analyze-project/validate-and-render.md",
|
|
184
|
+
"skills/author-overlay/3d.md",
|
|
185
|
+
"skills/author-overlay/glass.md",
|
|
186
|
+
"skills/author-overlay/lang/en.md",
|
|
187
|
+
"skills/author-overlay/lang/ja.md",
|
|
188
|
+
"skills/author-overlay/motion.md",
|
|
189
|
+
"skills/author-overlay/SKILL.md",
|
|
190
|
+
"skills/author-overlay/table.md",
|
|
191
|
+
"skills/author-overlay/telop.md",
|
|
192
|
+
"skills/author-overlay/text-behind-person.md",
|
|
193
|
+
"skills/author-overlay/thumbnail.md",
|
|
194
|
+
"skills/author-overlay/world.md",
|
|
180
195
|
"skills/author-scene3d/bake.md",
|
|
181
196
|
"skills/author-scene3d/device-materials.md",
|
|
182
197
|
"skills/author-scene3d/live.md",
|
|
183
198
|
"skills/author-scene3d/SKILL.md",
|
|
184
|
-
"skills/
|
|
185
|
-
"skills/
|
|
186
|
-
"skills/
|
|
187
|
-
"skills/declare-audio/SKILL.md",
|
|
188
|
-
"skills/declare-audio/what-to-declare.md",
|
|
189
|
-
"skills/design-world/guide.md",
|
|
190
|
-
"skills/design-world/SKILL.md",
|
|
191
|
-
"skills/design-world/world.md",
|
|
199
|
+
"skills/author-world/guide.md",
|
|
200
|
+
"skills/author-world/SKILL.md",
|
|
201
|
+
"skills/author-world/world.md",
|
|
192
202
|
"skills/edit-from-review/SKILL.md",
|
|
193
|
-
"skills/edit-plan/approvals-and-generation.md",
|
|
194
|
-
"skills/edit-plan/autonomy.md",
|
|
195
|
-
"skills/edit-plan/avatar-resolution.md",
|
|
196
|
-
"skills/edit-plan/beat-sync.md",
|
|
197
|
-
"skills/edit-plan/beats.md",
|
|
198
|
-
"skills/edit-plan/emphasis-detection.md",
|
|
199
|
-
"skills/edit-plan/execution.md",
|
|
200
|
-
"skills/edit-plan/expression-selection.md",
|
|
201
|
-
"skills/edit-plan/lang/en.md",
|
|
202
|
-
"skills/edit-plan/lang/ja.md",
|
|
203
|
-
"skills/edit-plan/plan-json.md",
|
|
204
|
-
"skills/edit-plan/recipe.md",
|
|
205
|
-
"skills/edit-plan/report-guide.md",
|
|
206
|
-
"skills/edit-plan/SKILL.md",
|
|
207
|
-
"skills/edit-plan/workflow.md",
|
|
208
203
|
"skills/edit-to-beat/generator.md",
|
|
209
204
|
"skills/edit-to-beat/pitfalls.md",
|
|
210
205
|
"skills/edit-to-beat/SKILL.md",
|
|
@@ -226,27 +221,31 @@
|
|
|
226
221
|
"skills/generate-video/still.md",
|
|
227
222
|
"skills/generate-video/video.md",
|
|
228
223
|
"skills/harvest-asset/SKILL.md",
|
|
229
|
-
"skills/
|
|
230
|
-
"skills/
|
|
231
|
-
"skills/
|
|
232
|
-
"skills/
|
|
233
|
-
"skills/
|
|
234
|
-
"skills/
|
|
235
|
-
"skills/
|
|
236
|
-
"skills/
|
|
237
|
-
"skills/
|
|
238
|
-
"skills/
|
|
239
|
-
"skills/
|
|
240
|
-
"skills/
|
|
224
|
+
"skills/harvest-playbook/SKILL.md",
|
|
225
|
+
"skills/plan-edit/approvals-and-generation.md",
|
|
226
|
+
"skills/plan-edit/autonomy.md",
|
|
227
|
+
"skills/plan-edit/avatar-resolution.md",
|
|
228
|
+
"skills/plan-edit/beat-sync.md",
|
|
229
|
+
"skills/plan-edit/beats.md",
|
|
230
|
+
"skills/plan-edit/emphasis-detection.md",
|
|
231
|
+
"skills/plan-edit/execution.md",
|
|
232
|
+
"skills/plan-edit/expression-selection.md",
|
|
233
|
+
"skills/plan-edit/lang/en.md",
|
|
234
|
+
"skills/plan-edit/lang/ja.md",
|
|
235
|
+
"skills/plan-edit/plan-json.md",
|
|
236
|
+
"skills/plan-edit/recipe.md",
|
|
237
|
+
"skills/plan-edit/report-guide.md",
|
|
238
|
+
"skills/plan-edit/SKILL.md",
|
|
239
|
+
"skills/plan-edit/workflow.md",
|
|
240
|
+
"skills/plan-video/competitor.md",
|
|
241
|
+
"skills/plan-video/ideate.md",
|
|
242
|
+
"skills/plan-video/lang/en.md",
|
|
243
|
+
"skills/plan-video/lang/ja.md",
|
|
244
|
+
"skills/plan-video/shotlist.md",
|
|
245
|
+
"skills/plan-video/SKILL.md",
|
|
246
|
+
"skills/plan-video/storyboard.md",
|
|
247
|
+
"skills/plan-video/trends.md",
|
|
241
248
|
"skills/render-cut/SKILL.md",
|
|
242
|
-
"skills/research-plan/competitor.md",
|
|
243
|
-
"skills/research-plan/ideate.md",
|
|
244
|
-
"skills/research-plan/lang/en.md",
|
|
245
|
-
"skills/research-plan/lang/ja.md",
|
|
246
|
-
"skills/research-plan/shotlist.md",
|
|
247
|
-
"skills/research-plan/SKILL.md",
|
|
248
|
-
"skills/research-plan/storyboard.md",
|
|
249
|
-
"skills/research-plan/trends.md",
|
|
250
249
|
"skills/review-cut/SKILL.md",
|
|
251
250
|
"skills/review-cut/workflow.md",
|
|
252
251
|
"skills/review-from-recording/compilation-rules.md",
|
|
@@ -269,6 +268,7 @@
|
|
|
269
268
|
"skills/setup-library/SKILL.md",
|
|
270
269
|
"skills/setup-library/starter-pack.md",
|
|
271
270
|
"skills/setup-library/tools-check.md",
|
|
271
|
+
"skills/setup-project/SKILL.md",
|
|
272
272
|
"skills/setup-remote/doctor-and-install.md",
|
|
273
273
|
"skills/setup-remote/serve-preview.md",
|
|
274
274
|
"skills/setup-remote/SKILL.md",
|
|
@@ -58,7 +58,12 @@ zip を使う。`src/store-command.mjs`)/
|
|
|
58
58
|
`packages/asset-resolver` の CLI への薄い委譲で、カタログ合成・entitlements 判定・
|
|
59
59
|
sha256 検証・fail-closed は resolver 側の責務のまま。`src/assets-command.mjs`)/
|
|
60
60
|
`akari clean [project-dir] [--dry-run] [--yes] [--json]`(使い捨ての中間ファイル、保持する
|
|
61
|
-
|
|
61
|
+
正本、判断が必要なものを容量付きで一覧する。既定は一覧のみで、削除可能なものだけを承認後に削除)/
|
|
62
|
+
`akari geo terrain --bbox <西,南,東,北> --out <dir> [--zoom 1-14] [--max-tiles 64]`(国土地理院の
|
|
63
|
+
標高・写真タイルから 3D 地形の `heightmap.png`・`texture.png`・`terrain.json` を作る)/
|
|
64
|
+
`akari geo point --terrain <terrain.json> --lonlat <経度,緯度>[,高さm]`(terrain.json の
|
|
65
|
+
declaration と同じ縮尺で経緯度を 3D 座標 `[x, y, z]` に直す。出典「国土地理院(地理院タイル)」の
|
|
66
|
+
表示が利用条件)。
|
|
62
67
|
|
|
63
68
|
## 拡張キット
|
|
64
69
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.5.2-beta.
|
|
3
|
+
"version": "1.5.2-beta.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": [
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- `akari media transcribe-diff <target> [--engines a,b,c]` — 最新のエンジン別文字起こしを文字単位で比較し、`diff.json` と JSON 要約を返す。句読点だけの差は除外し、1 エンジンなら比較を省略する。
|
|
11
11
|
- `akari media transcribe-cuts <target> [--basis b] [--silence-min 0.45] [--silence-break 3.0] [--silence-keep 0.15]` — フィラー・言い直し・無音・未認識を `cuts.json` に候補として保存し、JSON 要約を返す。既存の採否を引き継ぎ、手直しした字幕に重なる候補は既定 OFF にする。
|
|
12
12
|
- `akari media audio-level <projectDir>` — `gain_db` 未指定の音声素材を測り、役割別の提案値を表にする。`--write` で `edit.json` へ数値を保存し、`--json` で JSON 配列を返す。
|
|
13
|
+
- `akari geo terrain --bbox <西,南,東,北> --out <dir>` — 国土地理院の標高・写真タイルを結合し、`heightmap.png`・`texture.png`・`terrain.json` を作る。出典表示は [`docs/guides/geo-terrain.ja.md`](../../docs/guides/geo-terrain.ja.md) を参照。
|
|
13
14
|
|
|
14
15
|
## 音声素材の挿入レベル
|
|
15
16
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `akari geo` — 国土地理院の公開タイルから 3D 地形の素材を作り、地点を 3D 座標へ直す。
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { isMainModule } from "../src/common/main-module.mjs";
|
|
6
|
+
|
|
7
|
+
const terrainUsage = [
|
|
8
|
+
"使い方: akari geo terrain --bbox <西,南,東,北> --out <dir> [--zoom 1-14] [--max-tiles 64] [--cache-dir <dir>] [--json]",
|
|
9
|
+
"",
|
|
10
|
+
"国土地理院の標高タイル(dem_png)と写真タイル(seamlessphoto)を結合し、",
|
|
11
|
+
"heightmap.png・texture.png・terrain.json を出力します。",
|
|
12
|
+
"",
|
|
13
|
+
" --bbox 西,南,東,北(世界測地系の度)",
|
|
14
|
+
" --out 出力先ディレクトリ(必須)",
|
|
15
|
+
" --zoom タイルのズーム(既定 14)",
|
|
16
|
+
" --max-tiles 取得するタイル数の上限(既定 64)",
|
|
17
|
+
" --cache-dir タイルのキャッシュ先(既定 ~/.akari/cache/geo)",
|
|
18
|
+
" --json 結果を JSON で出力",
|
|
19
|
+
"",
|
|
20
|
+
"terrain.json の declaration を 3D の terrain 宣言にそのまま貼れます。",
|
|
21
|
+
].join("\n");
|
|
22
|
+
|
|
23
|
+
const pointUsage = [
|
|
24
|
+
"使い方: akari geo point --terrain <terrain.json> --lonlat <経度,緯度>[,高さm] [--exaggeration <n>] [--json]",
|
|
25
|
+
"",
|
|
26
|
+
"地形の declaration(size・meters・baseline・exaggeration)に合わせて、",
|
|
27
|
+
"経緯度から 3D 座標 [x, y, z] を出力します。高さを省略すると高さマップから引きます。",
|
|
28
|
+
"",
|
|
29
|
+
" --terrain akari geo terrain が出力した terrain.json",
|
|
30
|
+
" --lonlat 経度,緯度(,高さm は任意)",
|
|
31
|
+
" --exaggeration シーンの terrain.exaggeration を上書き(シーンで変えたら同じ値を渡す)",
|
|
32
|
+
" --json 結果を JSON で出力(既定も JSON 配列)",
|
|
33
|
+
"",
|
|
34
|
+
"出力は lines[].points と data-akari-3d-anchor にそのまま使えます。",
|
|
35
|
+
"線は lines[].drape: true にすると y を気にせず地表に沿わせられます。",
|
|
36
|
+
].join("\n");
|
|
37
|
+
|
|
38
|
+
const usage = [terrainUsage, "", pointUsage, "",
|
|
39
|
+
"生成物を使うときは「出典: 国土地理院(地理院タイル)」を必ず表示してください。"].join("\n");
|
|
40
|
+
|
|
41
|
+
function parseTerrainArgs(args) {
|
|
42
|
+
const parsed = { json: false };
|
|
43
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
44
|
+
const arg = args[index];
|
|
45
|
+
if (arg === "--json") parsed.json = true;
|
|
46
|
+
else if (arg === "--help" || arg === "-h") parsed.help = true;
|
|
47
|
+
else if (["--bbox", "--out", "--zoom", "--max-tiles", "--cache-dir"].includes(arg)) {
|
|
48
|
+
const value = args[index + 1];
|
|
49
|
+
if (value === undefined || value.startsWith("--")) return { ok: false, reason: `${arg} の値がありません` };
|
|
50
|
+
parsed[arg.slice(2)] = value;
|
|
51
|
+
index += 1;
|
|
52
|
+
} else if (arg.startsWith("--bbox=")) parsed.bbox = arg.slice(7);
|
|
53
|
+
else if (arg.startsWith("--out=")) parsed.out = arg.slice(6);
|
|
54
|
+
else if (arg.startsWith("--zoom=")) parsed.zoom = arg.slice(7);
|
|
55
|
+
else if (arg.startsWith("--max-tiles=")) parsed["max-tiles"] = arg.slice(12);
|
|
56
|
+
else if (arg.startsWith("--cache-dir=")) parsed["cache-dir"] = arg.slice(12);
|
|
57
|
+
else return { ok: false, reason: `不明な引数です: ${arg}` };
|
|
58
|
+
}
|
|
59
|
+
if (parsed.help) return { ok: true, parsed };
|
|
60
|
+
if (!parsed.bbox) return { ok: false, reason: "--bbox を指定してください" };
|
|
61
|
+
if (!parsed.out) return { ok: false, reason: "--out を指定してください" };
|
|
62
|
+
return { ok: true, parsed };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parsePointArgs(args) {
|
|
66
|
+
const parsed = { json: false };
|
|
67
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
68
|
+
const arg = args[index];
|
|
69
|
+
if (arg === "--json") parsed.json = true;
|
|
70
|
+
else if (arg === "--help" || arg === "-h") parsed.help = true;
|
|
71
|
+
else if (["--terrain", "--lonlat", "--exaggeration"].includes(arg)) {
|
|
72
|
+
const value = args[index + 1];
|
|
73
|
+
if (value === undefined || value.startsWith("--")) return { ok: false, reason: `${arg} の値がありません` };
|
|
74
|
+
parsed[arg.slice(2)] = value;
|
|
75
|
+
index += 1;
|
|
76
|
+
} else if (arg.startsWith("--terrain=")) parsed.terrain = arg.slice(10);
|
|
77
|
+
else if (arg.startsWith("--lonlat=")) parsed.lonlat = arg.slice(9);
|
|
78
|
+
else return { ok: false, reason: `不明な引数です: ${arg}` };
|
|
79
|
+
}
|
|
80
|
+
if (parsed.help) return { ok: true, parsed };
|
|
81
|
+
if (!parsed.terrain) return { ok: false, reason: "--terrain を指定してください" };
|
|
82
|
+
if (!parsed.lonlat) return { ok: false, reason: "--lonlat を指定してください" };
|
|
83
|
+
return { ok: true, parsed };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function runTerrain(args, options, log, logError) {
|
|
87
|
+
const parsed = parseTerrainArgs(args);
|
|
88
|
+
if (!parsed.ok) {
|
|
89
|
+
logError(parsed.reason);
|
|
90
|
+
logError(terrainUsage);
|
|
91
|
+
return { exitCode: 2 };
|
|
92
|
+
}
|
|
93
|
+
if (parsed.parsed.help) {
|
|
94
|
+
log(terrainUsage);
|
|
95
|
+
return { exitCode: 0 };
|
|
96
|
+
}
|
|
97
|
+
const build = options.buildTerrain ?? (await import("../src/geo/terrain.mjs")).buildTerrain;
|
|
98
|
+
try {
|
|
99
|
+
const result = await build({
|
|
100
|
+
bbox: parsed.parsed.bbox,
|
|
101
|
+
outDir: path.resolve(parsed.parsed.out),
|
|
102
|
+
zoom: parsed.parsed.zoom,
|
|
103
|
+
maxTiles: parsed.parsed["max-tiles"],
|
|
104
|
+
cacheDir: parsed.parsed["cache-dir"],
|
|
105
|
+
env: options.env,
|
|
106
|
+
log: parsed.parsed.json ? () => {} : log,
|
|
107
|
+
});
|
|
108
|
+
if (parsed.parsed.json) {
|
|
109
|
+
log(JSON.stringify({
|
|
110
|
+
out: result.outDir,
|
|
111
|
+
files: result.files,
|
|
112
|
+
terrain: result.terrain,
|
|
113
|
+
size: result.size,
|
|
114
|
+
tiles: result.tiles,
|
|
115
|
+
missing: result.missing,
|
|
116
|
+
filled: result.filled,
|
|
117
|
+
}));
|
|
118
|
+
} else {
|
|
119
|
+
log(`出力先: ${result.outDir}`);
|
|
120
|
+
}
|
|
121
|
+
return { exitCode: 0 };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
124
|
+
return { exitCode: 1 };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runPoint(args, options, log, logError) {
|
|
129
|
+
const parsed = parsePointArgs(args);
|
|
130
|
+
if (!parsed.ok) {
|
|
131
|
+
logError(parsed.reason);
|
|
132
|
+
logError(pointUsage);
|
|
133
|
+
return { exitCode: 2 };
|
|
134
|
+
}
|
|
135
|
+
if (parsed.parsed.help) {
|
|
136
|
+
log(pointUsage);
|
|
137
|
+
return { exitCode: 0 };
|
|
138
|
+
}
|
|
139
|
+
const module = await import("../src/geo/point.mjs");
|
|
140
|
+
const loadTerrainFiles = options.loadTerrainFiles ?? module.loadTerrainFiles;
|
|
141
|
+
const terrainPoint = options.terrainPoint ?? module.terrainPoint;
|
|
142
|
+
const roundPoint = options.roundPoint ?? module.roundPoint;
|
|
143
|
+
const parseLonLat = options.parseLonLat ?? module.parseLonLat;
|
|
144
|
+
try {
|
|
145
|
+
const { lon, lat, height } = parseLonLat(parsed.parsed.lonlat);
|
|
146
|
+
const { terrain, image } = await loadTerrainFiles(parsed.parsed.terrain);
|
|
147
|
+
const point = terrainPoint({
|
|
148
|
+
terrain, image, lon, lat, height, exaggeration: parsed.parsed.exaggeration,
|
|
149
|
+
});
|
|
150
|
+
log(JSON.stringify(roundPoint(point)));
|
|
151
|
+
return { exitCode: 0 };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
154
|
+
return { exitCode: 1 };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function runGeoCommand(args, options = {}) {
|
|
159
|
+
const log = options.log ?? console.log;
|
|
160
|
+
const logError = options.logError ?? console.error;
|
|
161
|
+
const subcommand = args[0];
|
|
162
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
163
|
+
log(usage);
|
|
164
|
+
return { exitCode: 0 };
|
|
165
|
+
}
|
|
166
|
+
if (subcommand === "terrain") return runTerrain(args.slice(1), options, log, logError);
|
|
167
|
+
if (subcommand === "point") return runPoint(args.slice(1), options, log, logError);
|
|
168
|
+
logError(`不明な geo サブコマンドです: ${subcommand}`);
|
|
169
|
+
logError(usage);
|
|
170
|
+
return { exitCode: 2 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (isMainModule(import.meta.url)) {
|
|
174
|
+
const result = await runGeoCommand(process.argv.slice(2));
|
|
175
|
+
process.exitCode = result.exitCode;
|
|
176
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// 地理院タイルの PNG 読み書き。外部パッケージは使わず Node 標準の zlib だけで扱う。
|
|
2
|
+
// 標高タイル(dem_png)は 8bit RGB、写真タイルは JPEG なので、ここでは PNG だけを担う。
|
|
3
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
4
|
+
|
|
5
|
+
const SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
6
|
+
const CHANNELS = new Map([
|
|
7
|
+
[0, 1], // grayscale
|
|
8
|
+
[2, 3], // RGB
|
|
9
|
+
[3, 1], // palette
|
|
10
|
+
[4, 2], // grayscale + alpha
|
|
11
|
+
[6, 4], // RGBA
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function crc32(buffer) {
|
|
15
|
+
let crc = 0xffffffff;
|
|
16
|
+
for (const byte of buffer) {
|
|
17
|
+
crc ^= byte;
|
|
18
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
19
|
+
crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function chunk(type, data) {
|
|
26
|
+
const typeBuffer = Buffer.from(type, "ascii");
|
|
27
|
+
const out = Buffer.alloc(12 + data.length);
|
|
28
|
+
out.writeUInt32BE(data.length, 0);
|
|
29
|
+
typeBuffer.copy(out, 4);
|
|
30
|
+
data.copy(out, 8);
|
|
31
|
+
out.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 8 + data.length);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** RGB のバイト列(幅 * 高さ * 3)を PNG にする。 */
|
|
36
|
+
export function encodeRgbPng(rgb, width, height) {
|
|
37
|
+
if (!Buffer.isBuffer(rgb)) throw new TypeError("PNG の画素は Buffer で渡してください");
|
|
38
|
+
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
|
|
39
|
+
throw new TypeError("PNG の幅・高さは正の整数で指定してください");
|
|
40
|
+
}
|
|
41
|
+
if (rgb.length !== width * height * 3) {
|
|
42
|
+
throw new RangeError(`PNG の画素数が足りません: ${rgb.length} バイト(期待 ${width * height * 3})`);
|
|
43
|
+
}
|
|
44
|
+
const stride = width * 3;
|
|
45
|
+
const scanlines = Buffer.alloc((stride + 1) * height);
|
|
46
|
+
for (let row = 0; row < height; row += 1) {
|
|
47
|
+
rgb.copy(scanlines, row * (stride + 1) + 1, row * stride, (row + 1) * stride);
|
|
48
|
+
}
|
|
49
|
+
const header = Buffer.alloc(13);
|
|
50
|
+
header.writeUInt32BE(width, 0);
|
|
51
|
+
header.writeUInt32BE(height, 4);
|
|
52
|
+
header[8] = 8; // bit depth
|
|
53
|
+
header[9] = 2; // color type: RGB
|
|
54
|
+
header[10] = 0; // compression
|
|
55
|
+
header[11] = 0; // filter
|
|
56
|
+
header[12] = 0; // interlace
|
|
57
|
+
return Buffer.concat([
|
|
58
|
+
SIGNATURE,
|
|
59
|
+
chunk("IHDR", header),
|
|
60
|
+
chunk("IDAT", deflateSync(scanlines)),
|
|
61
|
+
chunk("IEND", Buffer.alloc(0)),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readChunks(bytes) {
|
|
66
|
+
if (!bytes.subarray(0, 8).equals(SIGNATURE)) throw new Error("PNG の署名がありません");
|
|
67
|
+
const chunks = [];
|
|
68
|
+
let offset = 8;
|
|
69
|
+
while (offset + 12 <= bytes.length) {
|
|
70
|
+
const length = bytes.readUInt32BE(offset);
|
|
71
|
+
const type = bytes.toString("ascii", offset + 4, offset + 8);
|
|
72
|
+
const data = bytes.subarray(offset + 8, offset + 8 + length);
|
|
73
|
+
if (data.length !== length) throw new Error(`PNG の ${type} チャンクが途中で切れています`);
|
|
74
|
+
chunks.push({ type, data });
|
|
75
|
+
offset += 12 + length;
|
|
76
|
+
if (type === "IEND") break;
|
|
77
|
+
}
|
|
78
|
+
return chunks;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function paeth(left, up, upperLeft) {
|
|
82
|
+
const p = left + up - upperLeft;
|
|
83
|
+
const pa = Math.abs(p - left);
|
|
84
|
+
const pb = Math.abs(p - up);
|
|
85
|
+
const pc = Math.abs(p - upperLeft);
|
|
86
|
+
if (pa <= pb && pa <= pc) return left;
|
|
87
|
+
return pb <= pc ? up : upperLeft;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 8bit PNG を読み、RGB のバイト列(幅 * 高さ * 3)にする。 */
|
|
91
|
+
export function decodeRgbPng(bytes) {
|
|
92
|
+
if (!Buffer.isBuffer(bytes)) bytes = Buffer.from(bytes);
|
|
93
|
+
const chunks = readChunks(bytes);
|
|
94
|
+
const ihdr = chunks.find((entry) => entry.type === "IHDR")?.data;
|
|
95
|
+
if (!ihdr || ihdr.length < 13) throw new Error("PNG の IHDR がありません");
|
|
96
|
+
const width = ihdr.readUInt32BE(0);
|
|
97
|
+
const height = ihdr.readUInt32BE(4);
|
|
98
|
+
const bitDepth = ihdr[8];
|
|
99
|
+
const colorType = ihdr[9];
|
|
100
|
+
const compression = ihdr[10];
|
|
101
|
+
const filter = ihdr[11];
|
|
102
|
+
const interlace = ihdr[12];
|
|
103
|
+
if (compression !== 0 || filter !== 0) throw new Error("PNG の圧縮・フィルタ方式が未対応です");
|
|
104
|
+
if (interlace !== 0) throw new Error("インターレース PNG は未対応です");
|
|
105
|
+
if (bitDepth !== 8) throw new Error(`PNG のビット深度 ${bitDepth} は未対応です(8bit のみ)`);
|
|
106
|
+
const channels = CHANNELS.get(colorType);
|
|
107
|
+
if (!channels) throw new Error(`PNG の色種別 ${colorType} は未対応です`);
|
|
108
|
+
const palette = chunks.find((entry) => entry.type === "PLTE")?.data;
|
|
109
|
+
if (colorType === 3 && !palette) throw new Error("パレット PNG に PLTE がありません");
|
|
110
|
+
|
|
111
|
+
const idat = Buffer.concat(chunks.filter((entry) => entry.type === "IDAT").map((entry) => entry.data));
|
|
112
|
+
if (!idat.length) throw new Error("PNG の IDAT がありません");
|
|
113
|
+
const raw = inflateSync(idat);
|
|
114
|
+
const stride = width * channels;
|
|
115
|
+
if (raw.length < (stride + 1) * height) throw new Error("PNG の画素データが足りません");
|
|
116
|
+
|
|
117
|
+
const scanlines = Buffer.alloc(stride * height);
|
|
118
|
+
let source = 0;
|
|
119
|
+
for (let row = 0; row < height; row += 1) {
|
|
120
|
+
const filterType = raw[source];
|
|
121
|
+
source += 1;
|
|
122
|
+
const target = row * stride;
|
|
123
|
+
const previous = target - stride;
|
|
124
|
+
for (let column = 0; column < stride; column += 1) {
|
|
125
|
+
const value = raw[source + column];
|
|
126
|
+
const left = column >= channels ? scanlines[target + column - channels] : 0;
|
|
127
|
+
const up = row > 0 ? scanlines[previous + column] : 0;
|
|
128
|
+
const upperLeft = row > 0 && column >= channels ? scanlines[previous + column - channels] : 0;
|
|
129
|
+
let restored;
|
|
130
|
+
if (filterType === 0) restored = value;
|
|
131
|
+
else if (filterType === 1) restored = value + left;
|
|
132
|
+
else if (filterType === 2) restored = value + up;
|
|
133
|
+
else if (filterType === 3) restored = value + Math.floor((left + up) / 2);
|
|
134
|
+
else if (filterType === 4) restored = value + paeth(left, up, upperLeft);
|
|
135
|
+
else throw new Error(`PNG のフィルタ種別 ${filterType} は未対応です`);
|
|
136
|
+
scanlines[target + column] = restored & 0xff;
|
|
137
|
+
}
|
|
138
|
+
source += stride;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const rgb = Buffer.alloc(width * height * 3);
|
|
142
|
+
for (let index = 0, target = 0; index < width * height; index += 1, target += 3) {
|
|
143
|
+
const base = index * channels;
|
|
144
|
+
if (colorType === 2) {
|
|
145
|
+
rgb[target] = scanlines[base];
|
|
146
|
+
rgb[target + 1] = scanlines[base + 1];
|
|
147
|
+
rgb[target + 2] = scanlines[base + 2];
|
|
148
|
+
} else if (colorType === 6) {
|
|
149
|
+
rgb[target] = scanlines[base];
|
|
150
|
+
rgb[target + 1] = scanlines[base + 1];
|
|
151
|
+
rgb[target + 2] = scanlines[base + 2];
|
|
152
|
+
} else if (colorType === 0 || colorType === 4) {
|
|
153
|
+
rgb[target] = scanlines[base];
|
|
154
|
+
rgb[target + 1] = scanlines[base];
|
|
155
|
+
rgb[target + 2] = scanlines[base];
|
|
156
|
+
} else {
|
|
157
|
+
const paletteIndex = scanlines[base] * 3;
|
|
158
|
+
rgb[target] = palette[paletteIndex];
|
|
159
|
+
rgb[target + 1] = palette[paletteIndex + 1];
|
|
160
|
+
rgb[target + 2] = palette[paletteIndex + 2];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { width, height, rgb };
|
|
164
|
+
}
|