@birdapi/velinstyle 1.2.2 → 1.3.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.
Files changed (109) hide show
  1. package/README.de.md +18 -4
  2. package/README.md +42 -7
  3. package/cli/cli-manifest.json +441 -174
  4. package/cli/docgen/extract-cli.js +2 -1
  5. package/cli/docs-generate.js +9 -0
  6. package/cli/experience.js +173 -0
  7. package/cli/index.js +1294 -417
  8. package/cli/motion.js +738 -0
  9. package/cli/production/extract.js +46 -6
  10. package/cli/production/report.js +104 -14
  11. package/cli/production/run.js +44 -1
  12. package/cli/security.js +190 -0
  13. package/cli/skills.js +53 -10
  14. package/cli/transparency.js +140 -41
  15. package/cli/workflow.js +32 -17
  16. package/components/velin-theme-toggle.js +15 -1
  17. package/core/a11y/component-contracts.json +391 -274
  18. package/core/attributes/registry.js +1 -1
  19. package/core/motion/analyze.js +38 -0
  20. package/core/motion/blueprint.js +51 -0
  21. package/core/motion/composer.js +114 -0
  22. package/core/motion/data/registry.json +352 -0
  23. package/core/motion/effects.js +17 -1
  24. package/core/motion/index.js +112 -9
  25. package/core/motion/optimize.js +44 -0
  26. package/core/motion/policy.js +77 -0
  27. package/core/motion/registry.js +131 -0
  28. package/core/motion/review.js +59 -0
  29. package/core/motion/scan.js +150 -0
  30. package/core/motion/timeline.js +68 -0
  31. package/core/motion/transitions.js +49 -0
  32. package/core/motion/triggers.js +168 -0
  33. package/core/security/checks/fs.js +92 -0
  34. package/core/security/detect.js +35 -0
  35. package/core/security/engine.js +116 -0
  36. package/core/security/index.js +18 -0
  37. package/core/security/registry.js +85 -0
  38. package/core/security/report/report.js +113 -0
  39. package/core/security/rules/ci/index.js +146 -0
  40. package/core/security/rules/consumer/index.js +174 -0
  41. package/core/security/rules/deps/index.js +182 -0
  42. package/core/security/rules/index.js +24 -0
  43. package/core/security/rules/publish/index.js +230 -0
  44. package/core/security/rules/repo/index.js +143 -0
  45. package/core/security/rules/secrets/index.js +133 -0
  46. package/core/security/score/dependency-health.js +53 -0
  47. package/core/security/score/release-health.js +66 -0
  48. package/core/security/score/score.js +102 -0
  49. package/core/security/timeline/timeline.js +76 -0
  50. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  51. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  52. package/dist/chunks/chunk-MYKUI364.js +116 -0
  53. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  54. package/dist/chunks/runtime-entry.js +1 -1
  55. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  56. package/dist/llms.txt +2 -2
  57. package/dist/search-index.json +166 -2
  58. package/dist/velin-agent.json +156 -32
  59. package/dist/velinstyle-components.iife.js +862 -17
  60. package/dist/velinstyle-components.js +862 -17
  61. package/dist/velinstyle-components.min.js +126 -126
  62. package/dist/velinstyle.css +42 -7
  63. package/dist/velinstyle.min.css +1 -1
  64. package/package.json +158 -143
  65. package/packages/velinstyle-cli-core/package.json +11 -0
  66. package/packages/velinstyle-cli-core/src/config.js +84 -0
  67. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  68. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  69. package/packages/velinstyle-cli-core/src/index.js +18 -0
  70. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  71. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  72. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  73. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  74. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  75. package/packages/velinstyle-cli-registry/package.json +13 -0
  76. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  77. package/packages/velinstyle-cli-renderer/package.json +8 -0
  78. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  79. package/packages/velinstyle-cli-theme/package.json +8 -0
  80. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  81. package/packages/velinstyle-cli-ui/package.json +8 -0
  82. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  83. package/packages/velinstyle-motion/README.md +81 -0
  84. package/packages/velinstyle-motion/package.json +37 -0
  85. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  86. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  87. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  88. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  89. package/packages/velinstyle-motion/src/config.js +257 -0
  90. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  91. package/packages/velinstyle-motion/src/index.js +304 -0
  92. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  93. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  94. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  95. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  96. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  97. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  98. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  99. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  100. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  101. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  102. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  103. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  104. package/packages/velinstyle-skills/catalog.json +1 -1
  105. package/packages/velinstyle-skills/registry.json +1 -142
  106. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  107. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  108. package/src/tokens/motion.css +7 -0
  109. package/src/utilities/scroll-animation.css +66 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Motion engines / AI providers catalog for `velinstyle motion providers`.
3
+ */
4
+ import {
5
+ PIXVERSE_AFFILIATE_URL,
6
+ formatPixVerseSignupHelp,
7
+ } from './ai/providers/pixverse-setup.js';
8
+
9
+ export function listMotionProviders() {
10
+ return [
11
+ {
12
+ id: 'local',
13
+ kind: 'local',
14
+ title: 'LOCAL',
15
+ description: 'Kostenlos, lokal',
16
+ requiresApiKey: false,
17
+ },
18
+ {
19
+ id: 'pixverse',
20
+ kind: 'ai',
21
+ title: 'PIXVERSE',
22
+ description: 'AI Image-to-Video',
23
+ requiresApiKey: true,
24
+ setupCommand: 'velinstyle motion setup pixverse',
25
+ signupUrl: PIXVERSE_AFFILIATE_URL,
26
+ note: 'API erforderlich',
27
+ },
28
+ ];
29
+ }
30
+
31
+ /**
32
+ * Human-readable providers listing (German, matches plan copy).
33
+ */
34
+ export function formatMotionProvidersText() {
35
+ return [
36
+ 'VelinStyle Motion Providers',
37
+ '',
38
+ 'LOCAL',
39
+ 'Kostenlos, lokal',
40
+ '',
41
+ formatPixVerseSignupHelp(),
42
+ '',
43
+ ].join('\n');
44
+ }
@@ -0,0 +1,25 @@
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+
5
+ const ROOT = dirname(fileURLToPath(import.meta.url));
6
+ const DATA = JSON.parse(readFileSync(join(ROOT, 'presets.json'), 'utf8'));
7
+
8
+ export function listClipPresets() {
9
+ return DATA.presets.slice();
10
+ }
11
+
12
+ export function getClipPreset(id) {
13
+ if (!id) return null;
14
+ const key = String(id).toLowerCase();
15
+ return DATA.presets.find((p) => p.id === key || p.title.toLowerCase() === key) || null;
16
+ }
17
+
18
+ export function listWizardChoices() {
19
+ return listClipPresets().map((p, i) => ({
20
+ index: i + 1,
21
+ id: p.id,
22
+ title: p.title,
23
+ engine: p.engine,
24
+ }));
25
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "schema": "velinstyle.motion.clip.presets",
3
+ "version": 1,
4
+ "presets": [
5
+ {
6
+ "id": "soft-zoom",
7
+ "title": "Soft Zoom",
8
+ "engine": "local",
9
+ "effects": ["zoom"],
10
+ "durationMs": 4000,
11
+ "easing": "ease-in-out",
12
+ "status": "stable",
13
+ "description": "Gentle Ken Burns zoom-in on the image."
14
+ },
15
+ {
16
+ "id": "cinematic",
17
+ "title": "Cinematic",
18
+ "engine": "local",
19
+ "effects": ["pan", "zoom"],
20
+ "durationMs": 5000,
21
+ "easing": "ease-in-out",
22
+ "status": "stable",
23
+ "description": "Slow pan with subtle zoom for hero/showcase frames."
24
+ },
25
+ {
26
+ "id": "atelier",
27
+ "title": "Atelier Reveal",
28
+ "engine": "local",
29
+ "effects": ["intro", "soft-zoom"],
30
+ "durationMs": 4500,
31
+ "easing": "cubic-bezier(0.22, 1, 0.36, 1)",
32
+ "status": "stable",
33
+ "description": "Atelier-style fade-in then soft zoom — good for batch reels."
34
+ },
35
+ {
36
+ "id": "rotate",
37
+ "title": "Rotate",
38
+ "engine": "local",
39
+ "effects": ["rotate"],
40
+ "durationMs": 4000,
41
+ "easing": "ease-in-out",
42
+ "status": "stable",
43
+ "description": "Subtle rotation with settle."
44
+ },
45
+ {
46
+ "id": "particle-reveal",
47
+ "title": "Particle Reveal",
48
+ "engine": "local",
49
+ "effects": ["particles", "fade"],
50
+ "durationMs": 5000,
51
+ "easing": "ease-out",
52
+ "status": "stable",
53
+ "description": "Particle overlay fade revealing the image."
54
+ },
55
+ {
56
+ "id": "rotate-explode",
57
+ "title": "Rotate + Explosion",
58
+ "engine": "local",
59
+ "effects": ["rotate", "particles"],
60
+ "durationMs": 4500,
61
+ "easing": "cubic-bezier(0.34, 1.56, 0.64, 1)",
62
+ "status": "stable",
63
+ "description": "Spin then particle burst."
64
+ },
65
+ {
66
+ "id": "ai-reveal",
67
+ "title": "AI Animation",
68
+ "engine": "ai",
69
+ "effects": ["ai"],
70
+ "durationMs": 5000,
71
+ "easing": null,
72
+ "status": "beta",
73
+ "description": "Provider-driven image-to-video (PixVerse by default). Requires credits."
74
+ },
75
+ {
76
+ "id": "custom-prompt",
77
+ "title": "Eigener Prompt / Custom Prompt",
78
+ "engine": "hybrid",
79
+ "effects": ["mini-prompt"],
80
+ "durationMs": 5000,
81
+ "status": "beta",
82
+ "description": "Mini-prompt → structured motion profile (local or AI)."
83
+ }
84
+ ]
85
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Cost confirmation gate before AI jobs.
3
+ */
4
+ import { createInterface } from 'readline';
5
+
6
+ /**
7
+ * @param {object} opts
8
+ * @param {string} opts.provider
9
+ * @param {string} opts.model
10
+ * @param {number} opts.duration
11
+ * @param {string} opts.quality
12
+ * @param {boolean} [opts.yes]
13
+ * @param {boolean} [opts.quiet]
14
+ */
15
+ export async function confirmAiCost(opts) {
16
+ if (opts.yes || opts.quiet || !process.stdin.isTTY) return true;
17
+
18
+ const lines = [
19
+ '',
20
+ `${opts.provider || 'AI'}`,
21
+ `Modell: ${opts.model}`,
22
+ `Dauer: ${opts.duration} Sekunden`,
23
+ `Qualität: ${opts.quality}`,
24
+ '',
25
+ `Dieser Auftrag verwendet ${opts.provider || 'Provider'}-Credits.`,
26
+ '',
27
+ ];
28
+ process.stdout.write(lines.join('\n'));
29
+
30
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
31
+ const answer = await new Promise((resolve) => {
32
+ rl.question('Starten? [J/n] ', resolve);
33
+ });
34
+ rl.close();
35
+ const a = (answer || 'j').trim().toLowerCase();
36
+ return a === '' || a === 'j' || a === 'y' || a === 'ja' || a === 'yes';
37
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Ensure PixVerse API key before AI jobs.
3
+ * Hard rules: --yes → no browser/prompt; setup_completed → no auto affiliate reopen.
4
+ * Affiliate-first: open partner link / copy link / enter key — never bare platform bypass.
5
+ */
6
+ import { createInterface } from 'readline';
7
+ import {
8
+ hasPixVerseApiKey,
9
+ isPixVerseSetupCompleted,
10
+ loadMotionConfig,
11
+ loadMotionDotEnv,
12
+ markPixVerseSetupCompleted,
13
+ } from '../config.js';
14
+ import {
15
+ PIXVERSE_DOCS_URL,
16
+ PIXVERSE_KEY_MISSING_HINT,
17
+ copyPixVersePartnerLink,
18
+ formatPixVerseSignupHelp,
19
+ formatPixVerseTrackerHint,
20
+ runPixVersePartnerOpenFlow,
21
+ promptAndSaveApiKey,
22
+ } from '../ai/providers/pixverse-setup.js';
23
+
24
+ function ask(rl, q) {
25
+ return new Promise((resolve) => {
26
+ rl.question(q, resolve);
27
+ });
28
+ }
29
+
30
+ function writeCopied(result) {
31
+ if (result.ok) {
32
+ process.stdout.write(`\nPartnerlink kopiert:\n${result.url}\n`);
33
+ } else {
34
+ process.stdout.write(`\nPartnerlink (manuell kopieren):\n${result.url}\n`);
35
+ }
36
+ process.stdout.write(`\n${formatPixVerseTrackerHint()}\n`);
37
+ }
38
+
39
+ /**
40
+ * @param {object} opts
41
+ * @param {string} [opts.cwd]
42
+ * @param {boolean} [opts.yes]
43
+ * @param {boolean} [opts.quiet]
44
+ * @param {boolean} [opts.json]
45
+ * @param {boolean} [opts.dryRun]
46
+ * @param {(url: string, o?: object) => object|string} [opts.openImpl]
47
+ * @returns {Promise<{ ok: boolean, dryRun?: boolean }>}
48
+ */
49
+ export async function ensurePixVerseReady(opts = {}) {
50
+ const cwd = opts.cwd || process.cwd();
51
+ loadMotionDotEnv(cwd);
52
+ loadMotionConfig(cwd);
53
+
54
+ if (hasPixVerseApiKey(undefined, cwd)) {
55
+ return { ok: true };
56
+ }
57
+
58
+ const nonInteractive = Boolean(opts.yes || opts.quiet || opts.json || !process.stdin.isTTY);
59
+
60
+ if (opts.dryRun || process.env.VELIN_MOTION_DRY_RUN === '1') {
61
+ return { ok: true, dryRun: true };
62
+ }
63
+
64
+ if (nonInteractive) {
65
+ throw new Error(PIXVERSE_KEY_MISSING_HINT);
66
+ }
67
+
68
+ const completed = isPixVerseSetupCompleted(cwd);
69
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
70
+
71
+ try {
72
+ if (!completed) {
73
+ process.stdout.write(`\nVelinStyle Motion\n\n${formatPixVerseSignupHelp()}\n\n`);
74
+ const choice = (await ask(rl, 'Auswahl [1/2/3/4]: ')).trim();
75
+ if (choice === '4' || choice.toLowerCase() === 'q' || choice.toLowerCase() === 'c') {
76
+ throw new Error('Abgebrochen');
77
+ }
78
+ if (choice === '1' || choice === '') {
79
+ const outcome = await runPixVersePartnerOpenFlow({
80
+ rl,
81
+ openImpl: opts.openImpl,
82
+ spawnImpl: opts.spawnImpl,
83
+ copyImpl: opts.copyImpl,
84
+ forceFail: opts.forceFail,
85
+ });
86
+ if (outcome === 'aborted') throw new Error('Abgebrochen');
87
+ process.stdout.write(
88
+ '\nRichte den API-Zugang im PixVerse-Dashboard ein, dann füge den Key hier ein.\n',
89
+ );
90
+ await promptAndSaveApiKey({ cwd, rl });
91
+ markPixVerseSetupCompleted(cwd);
92
+ return { ok: true };
93
+ }
94
+ if (choice === '2') {
95
+ writeCopied(copyPixVersePartnerLink({ copyImpl: opts.copyImpl }));
96
+ process.stdout.write(
97
+ '\nÖffne den kopierten Partnerlink, richte den API-Zugang ein, dann Key hier einfügen.\n',
98
+ );
99
+ await promptAndSaveApiKey({ cwd, rl });
100
+ markPixVerseSetupCompleted(cwd);
101
+ return { ok: true };
102
+ }
103
+ if (choice === '3') {
104
+ process.stdout.write(
105
+ '\nAPI-Key aus dem PixVerse-Dashboard einfügen (Env / .velin/motion/.env).\n',
106
+ );
107
+ await promptAndSaveApiKey({ cwd, rl });
108
+ markPixVerseSetupCompleted(cwd);
109
+ return { ok: true };
110
+ }
111
+ throw new Error('Abgebrochen');
112
+ }
113
+
114
+ // setup_completed but key missing — same affiliate-first menu, no auto browser
115
+ process.stdout.write(
116
+ [
117
+ '',
118
+ 'VelinStyle Motion',
119
+ '',
120
+ 'PixVerse Setup',
121
+ '',
122
+ 'PixVerse API-Key fehlt (Env / .velin/motion/.env).',
123
+ 'Oder erneut: velinstyle motion setup pixverse',
124
+ '',
125
+ formatPixVerseSignupHelp().split('\n').slice(6).join('\n'),
126
+ '',
127
+ ].join('\n'),
128
+ );
129
+ const choice = (await ask(rl, 'Auswahl [1/2/3/4]: ')).trim();
130
+ if (choice === '4' || choice.toLowerCase() === 'q' || choice.toLowerCase() === 'c') {
131
+ throw new Error('Abgebrochen');
132
+ }
133
+ if (choice === '1' || choice === '') {
134
+ const outcome = await runPixVersePartnerOpenFlow({
135
+ rl,
136
+ openImpl: opts.openImpl,
137
+ spawnImpl: opts.spawnImpl,
138
+ copyImpl: opts.copyImpl,
139
+ forceFail: opts.forceFail,
140
+ });
141
+ if (outcome === 'aborted') throw new Error('Abgebrochen');
142
+ } else if (choice === '2') {
143
+ writeCopied(copyPixVersePartnerLink({ copyImpl: opts.copyImpl }));
144
+ } else if (choice !== '3') {
145
+ throw new Error('Abgebrochen');
146
+ }
147
+ process.stdout.write(`Docs: ${PIXVERSE_DOCS_URL}\n`);
148
+ await promptAndSaveApiKey({ cwd, rl });
149
+ return { ok: true };
150
+ } finally {
151
+ rl.close();
152
+ }
153
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Interactive TTY wizard for motion create / atelier.
3
+ */
4
+ import { createInterface } from 'readline';
5
+ import { listWizardChoices, getClipPreset } from '../registry/presets.js';
6
+ import { listAiProviders, loadBuiltinAiProviders } from '../ai/provider.js';
7
+ import { suggestFromMiniPrompt } from '../ai/mini-prompt.js';
8
+
9
+ function rl() {
10
+ return createInterface({ input: process.stdin, output: process.stdout });
11
+ }
12
+
13
+ function ask(r, q) {
14
+ return new Promise((resolve) => r.question(q, resolve));
15
+ }
16
+
17
+ /**
18
+ * @param {object} ctx
19
+ * @param {string} ctx.image
20
+ * @param {boolean} [ctx.ai]
21
+ * @param {boolean} [ctx.yes]
22
+ * @param {object} [ctx.flags] — prefilled non-interactive
23
+ */
24
+ export async function runCreateWizard(ctx = {}) {
25
+ if (ctx.yes || ctx.flags?.preset || !process.stdin.isTTY) {
26
+ return resolveNonInteractive(ctx);
27
+ }
28
+
29
+ const r = rl();
30
+ try {
31
+ process.stdout.write(`\nVelinStyle Motion\n\nBild: ${ctx.image}\n\nAnimation auswählen:\n\n`);
32
+ const choices = listWizardChoices();
33
+ for (const c of choices) {
34
+ process.stdout.write(`${c.index}. ${c.title}${c.engine === 'ai' ? ' (AI)' : ''}\n`);
35
+ }
36
+ process.stdout.write(`${choices.length + 1}. Eigener Prompt / Mini-Prompt\n`);
37
+ const raw = await ask(r, '\nAuswahl: ');
38
+ const n = Number(raw);
39
+ let preset = null;
40
+ let profile = null;
41
+ let engine = 'local';
42
+ let provider = null;
43
+ let promptText = '';
44
+
45
+ if (n === choices.length + 1 || ctx.ai) {
46
+ const phrase = await ask(r, '\nWas soll passieren?\n> ');
47
+ const suggestions = suggestFromMiniPrompt(phrase);
48
+ if (suggestions.length) {
49
+ process.stdout.write('\nVelinStyle schlägt vor:\n\n');
50
+ suggestions.forEach((s, i) => process.stdout.write(`[${i + 1}] ${s.label}\n`));
51
+ const pick = Number(await ask(r, '\nAuswahl: ')) || 1;
52
+ profile = suggestions[Math.max(0, Math.min(suggestions.length, pick) - 1)];
53
+ } else {
54
+ profile = suggestFromMiniPrompt(phrase)[0];
55
+ }
56
+ engine = ctx.ai || (await ask(r, 'Engine [local/ai] (local): ')) === 'ai' ? 'ai' : (ctx.ai ? 'ai' : 'local');
57
+ if (ctx.ai) engine = 'ai';
58
+ preset = getClipPreset(engine === 'ai' ? 'ai-reveal' : 'custom-prompt');
59
+ } else {
60
+ const choice = choices[n - 1] || choices[0];
61
+ preset = getClipPreset(choice.id);
62
+ engine = preset.engine === 'ai' || preset.engine === 'hybrid' ? (choice.engine === 'ai' ? 'ai' : 'local') : 'local';
63
+ if (preset.engine === 'ai') engine = 'ai';
64
+ }
65
+
66
+ if (engine === 'ai') {
67
+ await loadBuiltinAiProviders();
68
+ const providers = listAiProviders();
69
+ process.stdout.write('\nAI Provider\n\n');
70
+ providers.forEach((p, i) => process.stdout.write(`${i + 1}. ${p.title}\n`));
71
+ process.stdout.write(`${providers.length + 1}. Weitere Provider (bald)\n`);
72
+ const pi = Number(await ask(r, '\nAuswahl: ')) || 1;
73
+ if (pi > providers.length) {
74
+ throw new Error('Weitere Provider sind in 1.3.0 noch nicht verfügbar — bitte PixVerse wählen.');
75
+ }
76
+ provider = providers[pi - 1];
77
+ const model = (await ask(r, `Modell [${provider.models[0]}]: `)) || provider.models[0];
78
+ const quality = (await ask(r, 'Qualität [720p]: ')) || '720p';
79
+ const duration = Number(await ask(r, 'Dauer Sekunden [5]: ')) || 5;
80
+ promptText = profile?.aiPrompt || (await ask(r, 'Prompt (Enter = Profil): ')) || profile?.aiPrompt || '';
81
+
82
+ process.stdout.write(`\n${provider.title} Animation\n\n`);
83
+ process.stdout.write(`Bild: ${ctx.image}\n`);
84
+ process.stdout.write(`Preset: ${preset?.title || 'AI'}\n`);
85
+ process.stdout.write(`Dauer: ${duration}s\n`);
86
+ process.stdout.write(`Modell: ${model}\n`);
87
+ process.stdout.write(`Qualität: ${quality}\n`);
88
+ process.stdout.write(`Prompt: ${promptText}\n`);
89
+
90
+ return {
91
+ image: ctx.image,
92
+ preset: preset?.id || 'ai-reveal',
93
+ engine: 'ai',
94
+ provider: provider.id,
95
+ model,
96
+ quality,
97
+ duration,
98
+ prompt: promptText,
99
+ profile,
100
+ };
101
+ }
102
+
103
+ const duration = Number(await ask(r, 'Dauer Sekunden [4]: ')) || (preset?.durationMs || 4000) / 1000;
104
+ process.stdout.write(`\nLocal Animation\nBild: ${ctx.image}\nPreset: ${preset?.title}\nDauer: ${duration}s\n`);
105
+ const ok = await ask(r, '\nStarten? [J/n] ');
106
+ if (ok && !/^(j|y|ja|yes)?$/i.test(ok.trim() || 'j')) {
107
+ throw new Error('Abgebrochen');
108
+ }
109
+ return {
110
+ image: ctx.image,
111
+ preset: preset?.id || 'soft-zoom',
112
+ engine: 'local',
113
+ duration,
114
+ profile,
115
+ };
116
+ } finally {
117
+ r.close();
118
+ }
119
+ }
120
+
121
+ function resolveNonInteractive(ctx) {
122
+ const flags = ctx.flags || {};
123
+ const presetId = flags.preset || (ctx.ai ? 'ai-reveal' : 'soft-zoom');
124
+ const preset = getClipPreset(presetId);
125
+ return {
126
+ image: ctx.image,
127
+ preset: preset?.id || presetId,
128
+ engine: flags.style || (ctx.ai || preset?.engine === 'ai' ? 'ai' : 'local'),
129
+ provider: flags.provider || 'pixverse',
130
+ model: flags.model,
131
+ quality: flags.quality || '720p',
132
+ duration: flags.duration ? Number(flags.duration) : undefined,
133
+ prompt: flags.prompt,
134
+ profile: flags.profile,
135
+ outDir: flags.out,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Atelier batch wizard (simplified interactive).
141
+ */
142
+ export async function runAtelierWizard(ctx = {}) {
143
+ if (ctx.yes || !process.stdin.isTTY) {
144
+ return {
145
+ count: ctx.count || 10,
146
+ preset: ctx.preset || 'atelier',
147
+ engine: ctx.ai ? 'ai' : 'local',
148
+ provider: 'pixverse',
149
+ duration: ctx.duration || 5,
150
+ quality: '720p',
151
+ outDir: ctx.outDir || './motion-output',
152
+ };
153
+ }
154
+ const r = rl();
155
+ try {
156
+ process.stdout.write(`\nVelinStyle Atelier Motion\n\n${ctx.assetCount ?? '?'} Assets gefunden.\n\n`);
157
+ const count = Number(await ask(r, 'Wie viele Animationen?\n> ')) || 10;
158
+ const preset = (await ask(r, 'Preset [atelier]: ')) || 'atelier';
159
+ const engine = (await ask(r, 'Engine [local/ai]: ')) || 'local';
160
+ let provider = 'pixverse';
161
+ let quality = '720p';
162
+ if (engine === 'ai') {
163
+ provider = (await ask(r, 'Provider [pixverse]: ')) || 'pixverse';
164
+ quality = (await ask(r, 'Qualität [720p]: ')) || '720p';
165
+ }
166
+ const duration = Number(await ask(r, 'Dauer [5]: ')) || 5;
167
+ return { count, preset, engine, provider, duration, quality, outDir: './motion-output' };
168
+ } finally {
169
+ r.close();
170
+ }
171
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Validate still-image inputs for Motion Clip CLI (local + AI).
3
+ * Rejects missing, empty, wrong-extension, and non-image payloads.
4
+ */
5
+ import { existsSync, statSync, openSync, readSync, closeSync } from 'fs';
6
+ import { extname, basename } from 'path';
7
+
8
+ const ALLOWED_EXT = new Set(['.webp', '.png', '.jpg', '.jpeg']);
9
+
10
+ /**
11
+ * @param {string} filePath absolute path
12
+ * @param {string} [label]
13
+ */
14
+ export function assertMotionImageFile(filePath, label = 'Image') {
15
+ if (!filePath || typeof filePath !== 'string') {
16
+ throw new Error(`${label} path is required`);
17
+ }
18
+ if (!existsSync(filePath)) {
19
+ throw new Error(`${label} not found: ${filePath}`);
20
+ }
21
+ let st;
22
+ try {
23
+ st = statSync(filePath);
24
+ } catch {
25
+ throw new Error(`${label} not readable: ${filePath}`);
26
+ }
27
+ if (!st.isFile()) {
28
+ throw new Error(`${label} must be a file: ${filePath}`);
29
+ }
30
+ if (st.size <= 0) {
31
+ throw new Error(`${label} is empty (0 bytes): ${basename(filePath)}`);
32
+ }
33
+
34
+ const ext = extname(filePath).toLowerCase();
35
+ if (!ALLOWED_EXT.has(ext)) {
36
+ throw new Error(
37
+ `${label} has unsupported extension "${ext || '(none)'}" — use .webp, .png, .jpg, or .jpeg`,
38
+ );
39
+ }
40
+
41
+ const head = readHead(filePath, 16);
42
+ if (!looksLikeRasterImage(head)) {
43
+ throw new Error(
44
+ `${label} is not a valid PNG/JPEG/WebP file: ${basename(filePath)}`,
45
+ );
46
+ }
47
+
48
+ return { path: filePath, size: st.size, ext };
49
+ }
50
+
51
+ /**
52
+ * @param {string} filePath
53
+ * @param {number} n
54
+ */
55
+ function readHead(filePath, n) {
56
+ const buf = Buffer.alloc(n);
57
+ const fd = openSync(filePath, 'r');
58
+ try {
59
+ readSync(fd, buf, 0, n, 0);
60
+ } finally {
61
+ closeSync(fd);
62
+ }
63
+ return buf;
64
+ }
65
+
66
+ /**
67
+ * Accept PNG / JPEG / WebP magic regardless of which allowed extension was used
68
+ * (fixtures often store PNG bytes under a .webp name).
69
+ * @param {Buffer} buf
70
+ */
71
+ function looksLikeRasterImage(buf) {
72
+ if (!buf || buf.length < 3) return false;
73
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return true;
74
+ if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return true;
75
+ if (
76
+ buf.length >= 12 &&
77
+ buf[0] === 0x52 &&
78
+ buf[1] === 0x49 &&
79
+ buf[2] === 0x46 &&
80
+ buf[3] === 0x46 &&
81
+ buf[8] === 0x57 &&
82
+ buf[9] === 0x45 &&
83
+ buf[10] === 0x42 &&
84
+ buf[11] === 0x50
85
+ ) {
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ export { ALLOWED_EXT as MOTION_IMAGE_EXTENSIONS };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "generatedAt": "2026-08-04T10:26:09.763Z",
3
+ "generatedAt": "2026-08-09T18:36:20.293Z",
4
4
  "totals": {
5
5
  "skills": 40,
6
6
  "packs": 6,