zdymak 0.10.0 ā 0.13.0
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/LICENSE +21 -21
- package/README.md +733 -288
- package/SKILL.md +183 -88
- package/bin/zdymak.mjs +3 -3
- package/package.json +66 -48
- package/src/canvas.mjs +116 -116
- package/src/capture/index.mjs +318 -285
- package/src/capture/web.mjs +137 -0
- package/src/cli.mjs +298 -215
- package/src/config.mjs +159 -121
- package/src/destinations.mjs +103 -0
- package/src/easings.mjs +74 -0
- package/src/effects.mjs +335 -0
- package/src/encode.mjs +70 -44
- package/src/fonts.mjs +42 -42
- package/src/frames.mjs +186 -168
- package/src/graphic.mjs +107 -77
- package/src/index.mjs +27 -12
- package/src/montage.mjs +428 -419
- package/src/png.mjs +50 -50
- package/src/premium.mjs +362 -361
- package/src/reel.mjs +366 -245
- package/src/screenshots.mjs +118 -66
- package/src/specs.mjs +123 -88
- package/src/statusbar.mjs +163 -0
- package/src/still.mjs +72 -62
- package/src/transitions.mjs +653 -0
- package/src/validate.mjs +98 -0
- package/src/video.mjs +243 -240
- package/types/index.d.ts +559 -0
package/src/cli.mjs
CHANGED
|
@@ -1,215 +1,298 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* zdymak CLI.
|
|
3
|
-
*
|
|
4
|
-
* zdymak build [--config <path>] [--out <dir>]
|
|
5
|
-
* zdymak video [--config <path>] [--target <ids>] [--out <dir>]
|
|
6
|
-
* zdymak screenshots [--config <path>] [--out <dir>]
|
|
7
|
-
* zdymak specs # print the store spec matrix
|
|
8
|
-
* zdymak capture --platform ios|android --name <screen> [--record] [--out <dir>]
|
|
9
|
-
* zdymak help
|
|
10
|
-
*/
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import fs from 'node:fs';
|
|
13
|
-
import { registerFonts } from './fonts.mjs';
|
|
14
|
-
import { loadConfig } from './config.mjs';
|
|
15
|
-
import { buildVideo } from './video.mjs';
|
|
16
|
-
import { buildReel } from './reel.mjs';
|
|
17
|
-
import { buildPremium } from './premium.mjs';
|
|
18
|
-
import { buildMontage } from './montage.mjs';
|
|
19
|
-
import { buildDeviceScreenshots } from './screenshots.mjs';
|
|
20
|
-
import { VIDEO_TARGETS, IMAGE_TARGETS, videoTarget } from './specs.mjs';
|
|
21
|
-
import { runCapture } from './capture/index.mjs';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
console.log(
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
1
|
+
/**
|
|
2
|
+
* zdymak CLI.
|
|
3
|
+
*
|
|
4
|
+
* zdymak build [--config <path>] [--out <dir>] [--locale <ids>] # EVERYTHING: videos + screenshots
|
|
5
|
+
* zdymak video [--config <path>] [--target <ids>] [--out <dir>]
|
|
6
|
+
* zdymak screenshots [--config <path>] [--out <dir>] [--locale <ids>] # per-device store screenshots
|
|
7
|
+
* zdymak specs # print the store spec matrix
|
|
8
|
+
* zdymak capture --platform ios|android --name <screen> [--record] [--out <dir>]
|
|
9
|
+
* zdymak help
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import { registerFonts } from './fonts.mjs';
|
|
14
|
+
import { loadConfig } from './config.mjs';
|
|
15
|
+
import { buildVideo } from './video.mjs';
|
|
16
|
+
import { buildReel } from './reel.mjs';
|
|
17
|
+
import { buildPremium } from './premium.mjs';
|
|
18
|
+
import { buildMontage } from './montage.mjs';
|
|
19
|
+
import { buildDeviceScreenshots, localizeScenes, localizeBrand, untranslatedScenes } from './screenshots.mjs';
|
|
20
|
+
import { VIDEO_TARGETS, IMAGE_TARGETS, videoTarget } from './specs.mjs';
|
|
21
|
+
import { runCapture } from './capture/index.mjs';
|
|
22
|
+
import { resolveVideo, paletteAt } from './destinations.mjs';
|
|
23
|
+
import { validateVideo, validateImage } from './validate.mjs';
|
|
24
|
+
|
|
25
|
+
function parseFlags(argv) {
|
|
26
|
+
const flags = {};
|
|
27
|
+
const rest = [];
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
if (a.startsWith('--')) {
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
// Boolean flag when followed by nothing or another --flag (e.g. `--build --project ā¦`); else it
|
|
33
|
+
// takes the next token as its value. Single-dash values like `-marketingScreen` ARE values.
|
|
34
|
+
if (next === undefined || next.startsWith('--')) flags[a.slice(2)] = true;
|
|
35
|
+
else flags[a.slice(2)] = argv[++i];
|
|
36
|
+
} else {
|
|
37
|
+
rest.push(a);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { flags, rest };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_CONFIG = 'zdymak.config.mjs';
|
|
44
|
+
|
|
45
|
+
async function open(flags) {
|
|
46
|
+
const cfg = await loadConfig(flags.config || DEFAULT_CONFIG);
|
|
47
|
+
registerFonts(cfg.brand.fontPaths);
|
|
48
|
+
const outDir = flags.out ? path.resolve(flags.out) : cfg.out;
|
|
49
|
+
// --clean wipes the output folder first, so a run can't leave stale assets from a removed target/scene
|
|
50
|
+
// behind (every screenshot/video in the folder is then freshly produced by THIS run).
|
|
51
|
+
if (flags.clean && fs.existsSync(outDir)) {
|
|
52
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
53
|
+
console.log(`š§¹ cleaned ${path.relative(process.cwd(), outDir) || outDir}`);
|
|
54
|
+
}
|
|
55
|
+
return { cfg, outDir };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Warn when the audio expectation and the config disagree: a video/reel that should carry a music score
|
|
59
|
+
* has none (Apple recommends a single score for continuity), or `music` is set where nothing consumes it. */
|
|
60
|
+
function warnAudio({ videoCount, hasMusic, label, silentByDesign }) {
|
|
61
|
+
// Never nag about a Play promo: our own guidance is to keep it silent unless the track is cleared,
|
|
62
|
+
// because a ContentID claim can force ads onto a listing video, which Play forbids.
|
|
63
|
+
if (silentByDesign) return;
|
|
64
|
+
if (videoCount > 0 && !hasMusic) {
|
|
65
|
+
console.warn(`ā ļø audio: ${label} ${videoCount === 1 ? 'plays' : 'play'} SILENT ā no music. Apple recommends a single music score for continuity; set \`music.path\`.`);
|
|
66
|
+
}
|
|
67
|
+
if (videoCount === 0 && hasMusic) {
|
|
68
|
+
console.warn('ā ļø audio: `music.path` is set but this run builds no video/reel ā the audio is ignored (screenshots have no sound).');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build one video. `entry` is either the `{ target }` shorthand or the split
|
|
74
|
+
* `{ destination, preset, transitions, effects }` form ā DESTINATION decides what's accepted, PRESET
|
|
75
|
+
* decides how it looks, and a `transitions`/`effects` list lets the caller supply its own vocabulary
|
|
76
|
+
* instead of the preset's defaults.
|
|
77
|
+
*/
|
|
78
|
+
async function buildVideoTarget({ entry, scenes, brand, cfg, outFile, size, theme, flags }) {
|
|
79
|
+
const r = resolveVideo(entry);
|
|
80
|
+
const base = r.destination;
|
|
81
|
+
const useSize = size || r.size;
|
|
82
|
+
const spec = useSize ? { ...base, w: useSize[0], h: useSize[1] } : base;
|
|
83
|
+
const build = r.engine === 'reel' ? buildReel : r.engine === 'premium' ? buildPremium : buildVideo;
|
|
84
|
+
const id = r.id;
|
|
85
|
+
// A caller-supplied palette overrides the per-scene default for scenes that don't name their own.
|
|
86
|
+
if (r.transitions || r.effects) {
|
|
87
|
+
scenes = scenes.map((sc, i) => ({
|
|
88
|
+
...sc,
|
|
89
|
+
cut: sc.cut ?? paletteAt(r.transitions, i, undefined),
|
|
90
|
+
effect: sc.effect ?? paletteAt(r.effects, i, undefined),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
const tag = r.preset !== 'full-bleed' ? `, ${r.preset}` : '';
|
|
94
|
+
process.stdout.write(` ⢠${id} (${spec.w}Ć${spec.h}${tag}${cfg.music ? ', āŖ' : ''}) ⦠`);
|
|
95
|
+
const { totalDur, warnings } = await build({
|
|
96
|
+
scenes, spec, brand, outFile,
|
|
97
|
+
sceneDur: cfg.sceneDur, xfade: cfg.xfade, timing: cfg.timing,
|
|
98
|
+
theme: theme ?? cfg.theme, music: cfg.music,
|
|
99
|
+
});
|
|
100
|
+
console.log(`${totalDur.toFixed(1)}s ā ${path.relative(process.cwd(), outFile)}`);
|
|
101
|
+
for (const w of warnings) console.warn(` ā ļø ${w}`);
|
|
102
|
+
// Measure the artefact, don't trust the intent: a renderer or encoder bug is caught here.
|
|
103
|
+
validateVideo({ file: outFile, destination: base, size: useSize, force: !!flags?.force });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function cmdVideo(flags) {
|
|
107
|
+
const { cfg, outDir } = await open(flags);
|
|
108
|
+
const targets = flags.target ? flags.target.split(',').map((s) => s.trim()) : cfg.targets;
|
|
109
|
+
console.log(`zdymak video ⢠${cfg.scenes.length} scenes ā ${targets.join(', ')}`);
|
|
110
|
+
for (const id of targets) {
|
|
111
|
+
await buildVideoTarget({ id, scenes: cfg.scenes, brand: cfg.brand, cfg, outFile: path.join(outDir, `${id}.mp4`) });
|
|
112
|
+
}
|
|
113
|
+
warnAudio({ videoCount: targets.length, hasMusic: !!cfg.music, label: 'videos' });
|
|
114
|
+
console.log('Done.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function cmdReel(flags) {
|
|
118
|
+
const { cfg, outDir } = await open(flags);
|
|
119
|
+
const reel = cfg.reel;
|
|
120
|
+
if (!reel?.segments?.length) {
|
|
121
|
+
console.warn('No `reel.segments` in the config ā nothing to build. (See README: live-footage reel.)');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const [w, h] = reel.size || [1080, 1920];
|
|
125
|
+
const spec = { w, h, fps: reel.fps || 30, profile: reel.profile || 'high', level: reel.level || '4.1' };
|
|
126
|
+
const outFile = path.join(outDir, `${flags.name || 'reel'}.mp4`);
|
|
127
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
128
|
+
console.log(`zdymak reel ⢠${reel.segments.length} segment(s) ā ${w}Ć${h}${reel.music ? ', āŖ' : ''}`);
|
|
129
|
+
const { totalDur } = await buildMontage({
|
|
130
|
+
segments: reel.segments, brand: cfg.brand, theme: reel.theme, spec, // reel theme (light default), NOT cfg.theme
|
|
131
|
+
music: reel.music, sceneDur: reel.sceneDur, bpm: reel.bpm, beatsPerCut: reel.beatsPerCut,
|
|
132
|
+
transition: reel.transition, xfadeDur: reel.xfadeDur, outFile,
|
|
133
|
+
});
|
|
134
|
+
console.log(` ā ${totalDur.toFixed(1)}s ā ${path.relative(process.cwd(), outFile)}`);
|
|
135
|
+
warnAudio({ videoCount: 1, hasMusic: !!reel.music, label: 'the reel' });
|
|
136
|
+
console.log('Done.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Which locales to render, from `captions` + an optional `--locale de,fr` filter. An unknown locale is an
|
|
141
|
+
* error, not a silent no-op ā a typo'd locale would otherwise look like a successful run that shipped
|
|
142
|
+
* nothing.
|
|
143
|
+
*/
|
|
144
|
+
function localesFor(cfg, flags) {
|
|
145
|
+
const configured = Object.keys(cfg.captions || {});
|
|
146
|
+
if (!flags.locale) return configured;
|
|
147
|
+
const wanted = String(flags.locale).split(',').map((s) => s.trim()).filter(Boolean);
|
|
148
|
+
const unknown = wanted.filter((l) => !configured.includes(l));
|
|
149
|
+
if (unknown.length) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`--locale: no captions configured for ${unknown.join(', ')}. ` +
|
|
152
|
+
(configured.length ? `Configured: ${configured.join(', ')}.` : 'The config has no `captions` block.'),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return wanted;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Per-locale screenshot sets ā `<out>/<locale>/<target>/ā¦`, leaving the base (source-language) set where
|
|
160
|
+
* it already is. Screenshots only: stores take localized stills far more often than localized previews,
|
|
161
|
+
* and re-encoding every video per locale costs minutes each.
|
|
162
|
+
*/
|
|
163
|
+
async function buildLocalizedScreenshots({ cfg, outDir, flags }) {
|
|
164
|
+
for (const locale of localesFor(cfg, flags)) {
|
|
165
|
+
const table = cfg.captions[locale];
|
|
166
|
+
console.log(`zdymak ⢠${locale} screenshots`);
|
|
167
|
+
const fellBack = new Set();
|
|
168
|
+
for (const device of cfg.devices) {
|
|
169
|
+
if (!device.screenshots.length) continue;
|
|
170
|
+
untranslatedScenes(device.scenes, table).forEach((id) => fellBack.add(id));
|
|
171
|
+
const written = await buildDeviceScreenshots({
|
|
172
|
+
device: { ...device, scenes: localizeScenes(device.scenes, table) },
|
|
173
|
+
brand: localizeBrand(cfg.brand, table),
|
|
174
|
+
theme: device.theme ?? cfg.stillTheme ?? cfg.theme,
|
|
175
|
+
outDir: path.join(outDir, locale),
|
|
176
|
+
force: !!flags.force,
|
|
177
|
+
});
|
|
178
|
+
console.log(` ⢠${device.name}: ${written.length}${written.length ? '' : ' ā no captures found, skipped'}`);
|
|
179
|
+
}
|
|
180
|
+
if (fellBack.size) {
|
|
181
|
+
console.log(` ā³ ${fellBack.size} scene(s) kept the base caption (no ${locale} translation): ${[...fellBack].join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function cmdScreenshots(flags) {
|
|
187
|
+
const { cfg, outDir } = await open(flags);
|
|
188
|
+
if (!cfg.devices.length) {
|
|
189
|
+
console.warn('No `devices` in the config ā nothing to screenshot. (See README: devices map.)');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
console.log(`zdymak screenshots ⢠${cfg.devices.length} device group(s)`);
|
|
193
|
+
for (const device of cfg.devices) {
|
|
194
|
+
const written = await buildDeviceScreenshots({ device, brand: cfg.brand, theme: device.theme ?? cfg.stillTheme ?? cfg.theme, outDir, force: !!flags.force });
|
|
195
|
+
console.log(` ⢠${device.name}: ${written.length} shot(s)${written[0] ? ` (${written[0].W}Ć${written[0].H}ā¦)` : ' ā no captures found, skipped'}`);
|
|
196
|
+
}
|
|
197
|
+
await buildLocalizedScreenshots({ cfg, outDir, flags });
|
|
198
|
+
warnAudio({ videoCount: 0, hasMusic: !!cfg.music, label: 'screenshots' });
|
|
199
|
+
console.log('Done.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function cmdBuild(flags) {
|
|
203
|
+
const { cfg, outDir } = await open(flags);
|
|
204
|
+
// 1) shared (phone) video targets from the top-level scenes
|
|
205
|
+
if (cfg.targets.length && cfg.scenes.length) {
|
|
206
|
+
console.log('zdymak build ⢠videos');
|
|
207
|
+
for (const id of cfg.targets) {
|
|
208
|
+
await buildVideoTarget({ entry: id, scenes: cfg.scenes, brand: cfg.brand, cfg, outFile: path.join(outDir, `${id}.mp4`), flags });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 2) per-device videos + screenshots
|
|
212
|
+
let deviceVideos = 0;
|
|
213
|
+
for (const device of cfg.devices) {
|
|
214
|
+
// A device the app doesn't ship (or hasn't captured yet) skips cleanly ā same contract as its
|
|
215
|
+
// screenshots. Without this, one uncaptured device aborts the whole build for every other one.
|
|
216
|
+
const captured = device.scenes.filter((s) => fs.existsSync(s.image));
|
|
217
|
+
if (device.videos.length) {
|
|
218
|
+
if (!captured.length) {
|
|
219
|
+
console.log(` ⢠${device.name} videos: skipped ā no captures found`);
|
|
220
|
+
} else {
|
|
221
|
+
console.log(`zdymak build ⢠${device.name} videos`);
|
|
222
|
+
for (const v of device.videos) {
|
|
223
|
+
const vid = resolveVideo(v);
|
|
224
|
+
await buildVideoTarget({ entry: v, scenes: captured, brand: cfg.brand, cfg, size: v.size, theme: v.theme ?? device.theme, outFile: path.join(outDir, `${device.name}-${vid.id}.mp4`), flags });
|
|
225
|
+
deviceVideos++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (device.screenshots.length) {
|
|
230
|
+
const written = await buildDeviceScreenshots({ device, brand: cfg.brand, theme: device.theme ?? cfg.stillTheme ?? cfg.theme, outDir, force: !!flags.force });
|
|
231
|
+
console.log(` ⢠${device.name} screenshots: ${written.length}${written.length ? '' : ' ā no captures found, skipped'}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// 3) localized screenshot sets (base set already written above)
|
|
235
|
+
await buildLocalizedScreenshots({ cfg, outDir, flags });
|
|
236
|
+
|
|
237
|
+
const videoCount = (cfg.targets.length && cfg.scenes.length ? cfg.targets.length : 0) + deviceVideos;
|
|
238
|
+
warnAudio({ videoCount, hasMusic: !!cfg.music, label: 'videos' });
|
|
239
|
+
console.log('Done.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function cmdSpecs() {
|
|
243
|
+
console.log('\nVIDEO targets (produce an .mp4):');
|
|
244
|
+
for (const [id, s] of Object.entries(VIDEO_TARGETS)) {
|
|
245
|
+
const dur = s.minSec ? `${s.minSec}ā${s.maxSec}s` : 'any length';
|
|
246
|
+
console.log(` ${id.padEnd(20)} ${s.w}Ć${s.h} @${s.fps} H.264 ${s.profile}@${s.level} ${dur} ā ${s.store}`);
|
|
247
|
+
}
|
|
248
|
+
console.log('\nIMAGE targets (store screenshots ā `zdymak screenshots` / `build`):');
|
|
249
|
+
for (const [id, s] of Object.entries(IMAGE_TARGETS)) {
|
|
250
|
+
const dim = s.accepts ? s.accepts.map((a) => a.join('Ć')).join(' | ') : `${s.w}Ć${s.h}`;
|
|
251
|
+
console.log(` ${id.padEnd(24)} ${dim}${s.alpha === false ? ' (no alpha)' : ''} ā ${s.store}`);
|
|
252
|
+
}
|
|
253
|
+
console.log('');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function cmdHelp() {
|
|
257
|
+
console.log(`zdymak ā premium App Store, Google Play & social videos + screenshots, from your captures
|
|
258
|
+
|
|
259
|
+
Usage:
|
|
260
|
+
zdymak build [--config <path>] [--out <dir>] [--clean] [--locale <ids>] [--force] # everything
|
|
261
|
+
zdymak video [--config <path>] [--target <ids>] [--out <dir>] [--clean]
|
|
262
|
+
zdymak reel [--config <path>] [--out <dir>] [--clean] # LIVE-FOOTAGE montage from clips/images
|
|
263
|
+
zdymak screenshots [--config <path>] [--out <dir>] [--clean] [--locale <ids>]
|
|
264
|
+
zdymak specs
|
|
265
|
+
zdymak capture --platform ios --bundle <id> --arg <handle> --states <a,b,c> [--suffix -light]
|
|
266
|
+
[--build --project <.xcodeproj> --scheme <name>] [--device <sim>] [--out <dir>] [--clean] [--keep]
|
|
267
|
+
# full workflow: start the app, drive each screen by a launch handle, snap store-ready PNGs
|
|
268
|
+
zdymak capture --platform ios|android --name <screen> # single snapshot of the booted device
|
|
269
|
+
zdymak help
|
|
270
|
+
|
|
271
|
+
Defaults: --config ${DEFAULT_CONFIG}. Needs ffmpeg on PATH (or $FFMPEG).
|
|
272
|
+
--clean: wipe the output folder first (capture clears stale PNGs but keeps the .dd build cache) ā so the
|
|
273
|
+
folder ends up holding ONLY this run's assets, never a stale screenshot from a removed target/scene.
|
|
274
|
+
README.md documents the config (brand, scenes, targets, theme, music, devices); SKILL.md is for agents.`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
278
|
+
const [cmd, ...rest0] = argv;
|
|
279
|
+
const { flags, rest } = parseFlags(rest0);
|
|
280
|
+
try {
|
|
281
|
+
switch (cmd) {
|
|
282
|
+
case 'build': await cmdBuild(flags); break;
|
|
283
|
+
case 'video': await cmdVideo(flags); break;
|
|
284
|
+
case 'reel': await cmdReel(flags); break;
|
|
285
|
+
case 'screenshots': await cmdScreenshots(flags); break;
|
|
286
|
+
case 'specs': cmdSpecs(); break;
|
|
287
|
+
case 'capture': await runCapture(flags, rest); break;
|
|
288
|
+
case 'help': case undefined: case '--help': case '-h': cmdHelp(); break;
|
|
289
|
+
default:
|
|
290
|
+
console.error(`Unknown command "${cmd}".\n`);
|
|
291
|
+
cmdHelp();
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
console.error(`\nā ${e.message}`);
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
}
|
|
298
|
+
}
|