@esquadrao/cli 0.0.0-stage → 1.6.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 -0
- package/README.md +366 -2
- package/assets/fonts/Anton-Regular.ttf +0 -0
- package/assets/fonts/BebasNeue-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +93 -0
- package/assets/fonts/Poppins-Bold.ttf +0 -0
- package/assets/fonts/README.md +13 -0
- package/bin/cli.js +126 -0
- package/package.json +58 -4
- package/scripts/ast_slice.js +73 -0
- package/scripts/cutout.py +197 -0
- package/scripts/dag_validator.js +78 -0
- package/scripts/lint.js +43 -0
- package/scripts/merkle_verify.js +83 -0
- package/scripts/wcag_check.js +77 -0
- package/skill-audit/SKILL.md +42 -0
- package/skill-audit/references/merkle-ledger-spec.md +27 -0
- package/skill-audit/references/receipt-ed25519-verification.md +17 -0
- package/skill-audit/references/time-travel-replay.md +10 -0
- package/skill-code-reviewer/SKILL.md +40 -0
- package/skill-context/SKILL.md +44 -0
- package/skill-context/references/ast-virtualization.md +10 -0
- package/skill-context/references/openapi-contract-synthesis.md +10 -0
- package/skill-engineering-manager/SKILL.md +40 -0
- package/skill-media-producer/SKILL.md +328 -0
- package/skill-media-producer/references/caption-templates.md +194 -0
- package/skill-media-producer/references/composicoes-especiais.md +99 -0
- package/skill-media-producer/references/configuracao-inicial.md +239 -0
- package/skill-media-producer/references/corte-podcast-3-niveis.md +179 -0
- package/skill-media-producer/references/llm-agent-guide.md +231 -0
- package/skill-orchestrate/SKILL.md +49 -0
- package/skill-orchestrate/references/mission-lifecycle.md +36 -0
- package/skill-orchestrate/references/swarm-coordination.md +33 -0
- package/skill-pipeline/SKILL.md +42 -0
- package/skill-pipeline/references/auto-healing-patterns.md +17 -0
- package/skill-pipeline/references/dag-decomposition-rules.md +18 -0
- package/skill-pipeline/references/topological-waves.md +16 -0
- package/skill-product-manager/SKILL.md +40 -0
- package/skill-qa-engineer/SKILL.md +40 -0
- package/skill-review/SKILL.md +46 -0
- package/skill-review/references/anti-slop-patterns.md +17 -0
- package/skill-review/references/dual-blind-adjudication.md +15 -0
- package/skill-review/references/voice-calibration.md +10 -0
- package/skill-security-engineer/SKILL.md +40 -0
- package/skill-software-architect/SKILL.md +40 -0
- package/skill-software-engineer/SKILL.md +40 -0
- package/skill-sre-engineer/SKILL.md +40 -0
- package/skill-system-analyst/SKILL.md +40 -0
- package/skill-tech-lead/SKILL.md +40 -0
- package/skill-technical-writer/SKILL.md +40 -0
- package/skill-ui/SKILL.md +45 -0
- package/skill-ui/references/design-tokens-spec.md +19 -0
- package/skill-ui/references/visual-tdd-methodology.md +10 -0
- package/skill-ui/references/wcag-target-sizes.md +9 -0
- package/src/captionTemplates.js +285 -0
- package/src/commands/onboard.js +618 -0
- package/src/commands/role.js +119 -0
- package/src/commands/setup.js +174 -0
- package/src/commands/studio.js +1822 -0
- package/src/roles/authority.js +129 -0
- package/src/roles/codeReviewer.js +65 -0
- package/src/roles/engineeringManager.js +53 -0
- package/src/roles/index.js +80 -0
- package/src/roles/mediaProducer.js +223 -0
- package/src/roles/productManager.js +50 -0
- package/src/roles/qaEngineer.js +54 -0
- package/src/roles/securityEngineer.js +53 -0
- package/src/roles/softwareArchitect.js +58 -0
- package/src/roles/softwareEngineer.js +76 -0
- package/src/roles/sreEngineer.js +60 -0
- package/src/roles/systemAnalyst.js +45 -0
- package/src/roles/techLead.js +46 -0
- package/src/roles/technicalWriter.js +46 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
// Onboarding Agent Command
|
|
2
|
+
// Guides client from zero to first rendered video artifact with green QA.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import readline from 'node:readline/promises';
|
|
8
|
+
import { stdin as defaultInput, stdout as defaultOutput } from 'node:process';
|
|
9
|
+
import {
|
|
10
|
+
checkBinary,
|
|
11
|
+
findSystemFont,
|
|
12
|
+
run as studioRun,
|
|
13
|
+
buildCaptionFilters,
|
|
14
|
+
buildClipFilters,
|
|
15
|
+
buildDarkTextFilters,
|
|
16
|
+
} from './studio.js';
|
|
17
|
+
import { tryAutoInstallFfmpeg } from './setup.js';
|
|
18
|
+
import { validateEditPlan } from '../roles/mediaProducer.js';
|
|
19
|
+
|
|
20
|
+
export function getFfmpegInstallInstructions(platform = process.platform) {
|
|
21
|
+
if (platform === 'win32') {
|
|
22
|
+
return 'winget install Gyan.FFmpeg (or: choco install ffmpeg)';
|
|
23
|
+
}
|
|
24
|
+
if (platform === 'darwin') {
|
|
25
|
+
return 'brew install ffmpeg';
|
|
26
|
+
}
|
|
27
|
+
return 'sudo apt install ffmpeg (Debian/Ubuntu) or your package manager';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function detectEnvironment() {
|
|
31
|
+
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
32
|
+
if (nodeMajor < 18) {
|
|
33
|
+
throw new Error(`Node.js >= 18 is required (current: ${process.versions.node}). Please upgrade Node.js.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let hasFfmpeg = checkBinary('ffmpeg') && checkBinary('ffprobe');
|
|
37
|
+
let autoInstalledFfmpeg = false;
|
|
38
|
+
if (!hasFfmpeg) {
|
|
39
|
+
autoInstalledFfmpeg = tryAutoInstallFfmpeg();
|
|
40
|
+
hasFfmpeg = checkBinary('ffmpeg') && checkBinary('ffprobe');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!hasFfmpeg) {
|
|
44
|
+
const missing = [!checkBinary('ffmpeg') && 'ffmpeg', !checkBinary('ffprobe') && 'ffprobe'].filter(Boolean).join(' and ');
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${missing} is not installed or not found on PATH.\nTo install ffmpeg on ${process.platform}:\n ${getFfmpegInstallInstructions()}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const font = findSystemFont();
|
|
51
|
+
return { nodeVersion: process.versions.node, ffmpeg: true, autoInstalledFfmpeg, font };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function loadProfile(dir) {
|
|
55
|
+
if (!dir) return null;
|
|
56
|
+
const uskFile = path.join(dir, '.esquadrao', 'profile.json');
|
|
57
|
+
if (fs.existsSync(uskFile)) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(uskFile, 'utf8'));
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
const studioFile = path.join(dir, 'studio.json');
|
|
63
|
+
if (fs.existsSync(studioFile)) {
|
|
64
|
+
try {
|
|
65
|
+
const manifest = JSON.parse(fs.readFileSync(studioFile, 'utf8'));
|
|
66
|
+
if (manifest.profile) return manifest.profile;
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function saveProfile(targetDir, profileData) {
|
|
73
|
+
const uskDir = path.join(targetDir, '.esquadrao');
|
|
74
|
+
fs.mkdirSync(uskDir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(path.join(uskDir, 'profile.json'), JSON.stringify(profileData, null, 2) + '\n');
|
|
76
|
+
|
|
77
|
+
const studioFile = path.join(targetDir, 'studio.json');
|
|
78
|
+
if (fs.existsSync(studioFile)) {
|
|
79
|
+
try {
|
|
80
|
+
const manifest = JSON.parse(fs.readFileSync(studioFile, 'utf8'));
|
|
81
|
+
manifest.profile = profileData;
|
|
82
|
+
fs.writeFileSync(studioFile, JSON.stringify(manifest, null, 2) + '\n');
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ponytail: minimal 2-clip / text templates; upgrade when interactive storyboard editing or custom caption styling is needed.
|
|
88
|
+
export function buildOnboardPlan({ mode, assets = [], format = '9:16' }) {
|
|
89
|
+
const isWidescreen = format === '16:9';
|
|
90
|
+
const width = isWidescreen ? 1920 : 1080;
|
|
91
|
+
const height = isWidescreen ? 1080 : 1920;
|
|
92
|
+
|
|
93
|
+
if (mode === 'dark') {
|
|
94
|
+
return {
|
|
95
|
+
schema_version: 1,
|
|
96
|
+
mode: 'dark',
|
|
97
|
+
clips: [
|
|
98
|
+
{
|
|
99
|
+
kind: 'text',
|
|
100
|
+
text: 'Esquadrão Onboarding',
|
|
101
|
+
duration_ms: 1000,
|
|
102
|
+
timeline_start_ms: 0,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
kind: 'text',
|
|
106
|
+
text: 'Autonomous Video Pipeline Ready',
|
|
107
|
+
duration_ms: 1000,
|
|
108
|
+
timeline_start_ms: 1000,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
output: {
|
|
112
|
+
width,
|
|
113
|
+
height,
|
|
114
|
+
fps_num: 30,
|
|
115
|
+
fps_den: 1,
|
|
116
|
+
video_codec: 'h264',
|
|
117
|
+
audio_codec: 'aac',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (mode === 'edit_only') {
|
|
123
|
+
if (!assets || assets.length === 0) {
|
|
124
|
+
throw new Error('edit_only mode requires at least one imported asset');
|
|
125
|
+
}
|
|
126
|
+
let clips = [];
|
|
127
|
+
if (assets.length === 1) {
|
|
128
|
+
const a = assets[0];
|
|
129
|
+
const durMs = Math.max(200, Math.floor((a.metadata?.duration_s || 2) * 1000));
|
|
130
|
+
const midMs = Math.max(100, Math.floor(durMs / 2));
|
|
131
|
+
clips = [
|
|
132
|
+
{
|
|
133
|
+
kind: 'video',
|
|
134
|
+
asset_id: a.id,
|
|
135
|
+
clip_id: 'c1',
|
|
136
|
+
in_ms: 0,
|
|
137
|
+
out_ms: midMs,
|
|
138
|
+
timeline_start_ms: 0,
|
|
139
|
+
speed: 1,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
kind: 'video',
|
|
143
|
+
asset_id: a.id,
|
|
144
|
+
clip_id: 'c2',
|
|
145
|
+
in_ms: midMs,
|
|
146
|
+
out_ms: durMs,
|
|
147
|
+
timeline_start_ms: midMs,
|
|
148
|
+
speed: 1,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
} else {
|
|
152
|
+
const a0 = assets[0];
|
|
153
|
+
const a1 = assets[1];
|
|
154
|
+
const dur0 = Math.max(100, Math.floor((a0.metadata?.duration_s || 1) * 1000));
|
|
155
|
+
const dur1 = Math.max(100, Math.floor((a1.metadata?.duration_s || 1) * 1000));
|
|
156
|
+
const len0 = Math.min(dur0, 1000);
|
|
157
|
+
const len1 = Math.min(dur1, 1000);
|
|
158
|
+
clips = [
|
|
159
|
+
{
|
|
160
|
+
kind: 'video',
|
|
161
|
+
asset_id: a0.id,
|
|
162
|
+
clip_id: 'c1',
|
|
163
|
+
in_ms: 0,
|
|
164
|
+
out_ms: len0,
|
|
165
|
+
timeline_start_ms: 0,
|
|
166
|
+
speed: 1,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
kind: 'video',
|
|
170
|
+
asset_id: a1.id,
|
|
171
|
+
clip_id: 'c2',
|
|
172
|
+
in_ms: 0,
|
|
173
|
+
out_ms: len1,
|
|
174
|
+
timeline_start_ms: len0,
|
|
175
|
+
speed: 1,
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
schema_version: 1,
|
|
181
|
+
mode: 'edit_only',
|
|
182
|
+
clips,
|
|
183
|
+
audio_tracks: [
|
|
184
|
+
{ kind: 'source', clip_id: 'c1', gain_db: 0 },
|
|
185
|
+
{ kind: 'source', clip_id: 'c2', gain_db: 0 },
|
|
186
|
+
],
|
|
187
|
+
output: {
|
|
188
|
+
width,
|
|
189
|
+
height,
|
|
190
|
+
fps_num: 30,
|
|
191
|
+
fps_den: 1,
|
|
192
|
+
video_codec: 'h264',
|
|
193
|
+
audio_codec: 'aac',
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (mode === 'podcast_clip') {
|
|
199
|
+
if (!assets || assets.length === 0) {
|
|
200
|
+
throw new Error('podcast_clip mode requires at least one imported asset');
|
|
201
|
+
}
|
|
202
|
+
let currentTimeline = 0;
|
|
203
|
+
const clips = [];
|
|
204
|
+
const audioTracks = [];
|
|
205
|
+
for (let i = 0; i < assets.length; i++) {
|
|
206
|
+
const a = assets[i];
|
|
207
|
+
const dur = Math.max(100, Math.floor((a.metadata?.duration_s || 1) * 1000));
|
|
208
|
+
const len = Math.min(dur, 1000);
|
|
209
|
+
const clipId = `c${i + 1}`;
|
|
210
|
+
clips.push({
|
|
211
|
+
kind: 'video',
|
|
212
|
+
asset_id: a.id,
|
|
213
|
+
clip_id: clipId,
|
|
214
|
+
in_ms: 0,
|
|
215
|
+
out_ms: len,
|
|
216
|
+
timeline_start_ms: currentTimeline,
|
|
217
|
+
speed: 1,
|
|
218
|
+
});
|
|
219
|
+
audioTracks.push({
|
|
220
|
+
kind: 'source',
|
|
221
|
+
clip_id: clipId,
|
|
222
|
+
gain_db: 0,
|
|
223
|
+
});
|
|
224
|
+
currentTimeline += len;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
schema_version: 1,
|
|
228
|
+
mode: 'podcast_clip',
|
|
229
|
+
clips,
|
|
230
|
+
audio_tracks: audioTracks,
|
|
231
|
+
captions: [
|
|
232
|
+
{
|
|
233
|
+
text: 'Esquadrão Podcast Clip',
|
|
234
|
+
timeline_start_ms: 0,
|
|
235
|
+
duration_ms: Math.min(currentTimeline, 1000),
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
output: {
|
|
239
|
+
width,
|
|
240
|
+
height,
|
|
241
|
+
fps_num: 30,
|
|
242
|
+
fps_den: 1,
|
|
243
|
+
video_codec: 'h264',
|
|
244
|
+
audio_codec: 'aac',
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
throw new Error(`Unsupported mode: ${mode}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function runSilentIfJson(isJson, fn) {
|
|
253
|
+
if (!isJson) return await fn();
|
|
254
|
+
const originalLog = console.log;
|
|
255
|
+
console.log = () => {};
|
|
256
|
+
try {
|
|
257
|
+
return await fn();
|
|
258
|
+
} finally {
|
|
259
|
+
console.log = originalLog;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function displayPath(p) {
|
|
264
|
+
const rel = path.relative(process.cwd(), p).replace(/\\/g, '/');
|
|
265
|
+
return rel.startsWith('..') ? p : rel;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function run({ positionals = [], flags = {}, io = {} } = {}) {
|
|
269
|
+
if (flags.help || positionals[0] === 'help') {
|
|
270
|
+
console.log(`Esquadrão Onboard Agent
|
|
271
|
+
|
|
272
|
+
Usage:
|
|
273
|
+
esquadrao onboard [options]
|
|
274
|
+
|
|
275
|
+
Guided onboarding agent that takes a client from zero to their first rendered video.
|
|
276
|
+
|
|
277
|
+
Options:
|
|
278
|
+
--mode <mode> Video mode: podcast_clip | edit_only | dark (chosen by user)
|
|
279
|
+
--format <format> Aspect ratio: 9:16 (vertical mobile) | 16:9 (widescreen) (default: 9:16)
|
|
280
|
+
--dest <path> Workspace destination directory (defaults to cwd or saved profile)
|
|
281
|
+
--file <path> Media file to import for edit_only or podcast_clip (can repeat)
|
|
282
|
+
--captions <path> SRT file for podcast_clip captions
|
|
283
|
+
--caption-template <id> Caption template (see studio caption-templates)
|
|
284
|
+
--openai Use OpenAI Whisper for podcast_clip captions (requires OPENAI_API_KEY)
|
|
285
|
+
--local Use local @xenova/transformers engine for podcast_clip captions
|
|
286
|
+
--no-captions Render podcast_clip cuts without captions
|
|
287
|
+
--json Output results in JSON format
|
|
288
|
+
-h, --help Show help message`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 1. Environment Detection
|
|
293
|
+
let env;
|
|
294
|
+
try {
|
|
295
|
+
env = detectEnvironment();
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error(`Error: ${err.message}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!flags.json) {
|
|
302
|
+
console.log(`\n👋 Olá! Bem-vindo ao Esquadrão de Conteúdo.`);
|
|
303
|
+
console.log(`Vamos preparar o seu ambiente e fazer o seu primeiro vídeo em poucos segundos.\n`);
|
|
304
|
+
console.log(`Status do Ambiente:`);
|
|
305
|
+
console.log(`- Node.js: ${env.nodeVersion} (OK)`);
|
|
306
|
+
console.log(`- FFmpeg & FFprobe: disponível (OK)`);
|
|
307
|
+
if (env.font) {
|
|
308
|
+
console.log(`- Fonte do sistema: ${path.basename(env.font)}`);
|
|
309
|
+
}
|
|
310
|
+
console.log(``);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let rl = null;
|
|
314
|
+
let isClosed = false;
|
|
315
|
+
const getRl = () => {
|
|
316
|
+
if (!rl) {
|
|
317
|
+
rl = readline.createInterface({
|
|
318
|
+
input: io.input || defaultInput,
|
|
319
|
+
output: flags.json ? undefined : (io.output || defaultOutput),
|
|
320
|
+
});
|
|
321
|
+
rl.on('close', () => { isClosed = true; });
|
|
322
|
+
}
|
|
323
|
+
return rl;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const ask = async (promptMsg, defaultVal = '') => {
|
|
327
|
+
if (isClosed || rl?.closed) return defaultVal;
|
|
328
|
+
try {
|
|
329
|
+
const promptStr = defaultVal ? `${promptMsg} [${defaultVal}]: ` : `${promptMsg}: `;
|
|
330
|
+
const r = getRl();
|
|
331
|
+
const res = await Promise.race([
|
|
332
|
+
r.question(promptStr),
|
|
333
|
+
new Promise((resolve) => r.once('close', () => resolve(defaultVal))),
|
|
334
|
+
]);
|
|
335
|
+
const answer = (res || '').trim();
|
|
336
|
+
return answer || defaultVal;
|
|
337
|
+
} catch {
|
|
338
|
+
return defaultVal;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const canPrompt = Boolean(io.input || process.stdin.isTTY || process.env.ESQUADRAO_INTERACTIVE);
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// 2. Directory Resolution
|
|
346
|
+
let targetDir = flags.dest || positionals[0];
|
|
347
|
+
const cwdProfile = loadProfile(process.cwd());
|
|
348
|
+
|
|
349
|
+
if (!targetDir && cwdProfile?.default_dir) {
|
|
350
|
+
targetDir = cwdProfile.default_dir;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!targetDir) {
|
|
354
|
+
if (!flags.mode && canPrompt) {
|
|
355
|
+
targetDir = await ask('1. Em qual pasta você deseja guardar seus vídeos e projetos? [Pressione Enter para a pasta atual]');
|
|
356
|
+
}
|
|
357
|
+
targetDir = targetDir || process.cwd();
|
|
358
|
+
}
|
|
359
|
+
targetDir = path.resolve(targetDir);
|
|
360
|
+
|
|
361
|
+
const wsProfile = loadProfile(targetDir);
|
|
362
|
+
const activeProfile = wsProfile || cwdProfile;
|
|
363
|
+
|
|
364
|
+
// 3. Mode & Format Resolution (User-driven choice: neither dark nor widescreen is forced default)
|
|
365
|
+
let mode = flags.mode;
|
|
366
|
+
if (!mode && activeProfile?.preferred_mode) {
|
|
367
|
+
mode = activeProfile.preferred_mode;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!mode) {
|
|
371
|
+
if (canPrompt) {
|
|
372
|
+
const modePrompt = `2. Que tipo de vídeo você deseja criar hoje?
|
|
373
|
+
[1] Corte de podcast ou aula com legendas automáticas (Recomendado)
|
|
374
|
+
[2] Canal sem rosto / Vídeo dinâmico de alta retenção (Dark)
|
|
375
|
+
[3] Fatiar e juntar partes de vídeos sem legendas (Edit Only)
|
|
376
|
+
Escolha`;
|
|
377
|
+
const ansMode = await ask(modePrompt, '1');
|
|
378
|
+
if (ansMode === '1' || ansMode.toLowerCase().includes('podcast')) mode = 'podcast_clip';
|
|
379
|
+
else if (ansMode === '2' || ansMode.toLowerCase().includes('dark')) mode = 'dark';
|
|
380
|
+
else if (ansMode === '3' || ansMode.toLowerCase().includes('edit')) mode = 'edit_only';
|
|
381
|
+
else mode = ansMode;
|
|
382
|
+
} else {
|
|
383
|
+
throw new Error('Non-interactive run requires --mode flag (edit_only, dark, podcast_clip)');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
mode = mode.toLowerCase().trim();
|
|
388
|
+
if (!['dark', 'edit_only', 'podcast_clip'].includes(mode)) {
|
|
389
|
+
throw new Error(`Invalid mode '${mode}'. Expected: dark, edit_only, or podcast_clip.`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let format = flags.format || activeProfile?.preferred_format || '9:16';
|
|
393
|
+
if (!flags.format && canPrompt) {
|
|
394
|
+
const fmtPrompt = `3. Onde você vai postar esse vídeo?
|
|
395
|
+
[1] Celular em pé (9:16 — Reels, TikTok, YouTube Shorts) [Padrão]
|
|
396
|
+
[2] Vídeo deitado (16:9 — YouTube tradicional)
|
|
397
|
+
Escolha`;
|
|
398
|
+
const ansFmt = await ask(fmtPrompt, '1');
|
|
399
|
+
if (ansFmt === '1' || ansFmt.includes('9:16')) format = '9:16';
|
|
400
|
+
else if (ansFmt === '2' || ansFmt.includes('16:9')) format = '16:9';
|
|
401
|
+
else format = ansFmt;
|
|
402
|
+
}
|
|
403
|
+
format = (format || '9:16').trim();
|
|
404
|
+
if (format !== '16:9' && format !== '9:16') {
|
|
405
|
+
format = '9:16';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 4. Media Files Resolution
|
|
409
|
+
let files = [];
|
|
410
|
+
if (flags.file) {
|
|
411
|
+
const raw = Array.isArray(flags.file) ? flags.file : [flags.file];
|
|
412
|
+
files = raw.flatMap((f) => f.split(',')).map((f) => f.trim()).filter(Boolean);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (mode !== 'dark' && files.length === 0) {
|
|
416
|
+
let existingAssets = [];
|
|
417
|
+
const manifestPath = path.join(targetDir, 'studio.json');
|
|
418
|
+
if (fs.existsSync(manifestPath)) {
|
|
419
|
+
try {
|
|
420
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
421
|
+
existingAssets = m.assets || [];
|
|
422
|
+
} catch (err) {
|
|
423
|
+
throw new Error(`Corrupted studio manifest at ${manifestPath}: ${err.message}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (existingAssets.length === 0) {
|
|
428
|
+
if (canPrompt) {
|
|
429
|
+
const ans = await ask('4. Qual é o arquivo de vídeo que você quer cortar? (Exemplo: ./assets/podcast.mp4)');
|
|
430
|
+
files = ans.split(',').map((s) => s.trim()).filter(Boolean);
|
|
431
|
+
}
|
|
432
|
+
if (files.length === 0) {
|
|
433
|
+
throw new Error(`Mode '${mode}' requires at least one media file.`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const f of files) {
|
|
439
|
+
if (!fs.existsSync(path.resolve(f))) {
|
|
440
|
+
throw new Error(`Media file not found: ${f}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (rl) {
|
|
445
|
+
rl.close();
|
|
446
|
+
rl = null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 5. Execution Pipeline
|
|
450
|
+
if (!flags.json) console.log(`\n⏳ [1/4] Preparando seu estúdio em ${targetDir}...`);
|
|
451
|
+
await runSilentIfJson(flags.json, () => studioRun({ positionals: ['init'], flags: { dest: targetDir, json: false, verbose: flags.verbose } }));
|
|
452
|
+
|
|
453
|
+
let planPath = null;
|
|
454
|
+
let artifact = null;
|
|
455
|
+
let artifactFullPath = null;
|
|
456
|
+
let qaReport = null;
|
|
457
|
+
let pipeline = mode === 'podcast_clip' ? 'auto-clip' : 'plan';
|
|
458
|
+
let captionMode = null;
|
|
459
|
+
let captionsCount = null;
|
|
460
|
+
|
|
461
|
+
if (mode === 'podcast_clip') {
|
|
462
|
+
if (!flags.json) console.log('🎬 [2/4] Executando o pipeline real de auto-clip...');
|
|
463
|
+
let sourceFile = files[0];
|
|
464
|
+
if (!sourceFile) {
|
|
465
|
+
const manifestPath = path.join(targetDir, 'studio.json');
|
|
466
|
+
if (fs.existsSync(manifestPath)) {
|
|
467
|
+
try {
|
|
468
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
469
|
+
const first = (m.assets || [])[0];
|
|
470
|
+
if (first) sourceFile = path.resolve(targetDir, first.path);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
throw new Error(`Corrupted studio manifest at ${manifestPath}: ${err.message}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!sourceFile) throw new Error('podcast_clip requires at least one media file.');
|
|
477
|
+
|
|
478
|
+
const autoFlags = {
|
|
479
|
+
dest: targetDir,
|
|
480
|
+
json: false,
|
|
481
|
+
verbose: flags.verbose,
|
|
482
|
+
format,
|
|
483
|
+
captions: flags.captions,
|
|
484
|
+
'caption-template': flags['caption-template'],
|
|
485
|
+
openai: flags.openai,
|
|
486
|
+
local: flags.local,
|
|
487
|
+
'no-captions': flags['no-captions'],
|
|
488
|
+
'silence-thresh': flags['silence-thresh'],
|
|
489
|
+
'silence-duration': flags['silence-duration'],
|
|
490
|
+
'caption-style': flags['caption-style'],
|
|
491
|
+
};
|
|
492
|
+
const hasCaptionChoice = autoFlags.captions || autoFlags.openai || autoFlags.local || autoFlags['no-captions'] || process.env.OPENAI_API_KEY;
|
|
493
|
+
if (!hasCaptionChoice) {
|
|
494
|
+
// A presença do pacote opcional não é uma escolha do usuário: o
|
|
495
|
+
// onboarding só usa legenda quando --captions/--openai/--local é
|
|
496
|
+
// explícito. Isso evita tentar baixar o modelo Whisper em CI/offline.
|
|
497
|
+
autoFlags['no-captions'] = true;
|
|
498
|
+
if (!flags.json) console.warn('⚠️ Nenhum motor de legenda escolhido; gerando corte real sem legendas. Use --captions, --openai ou --local para legendas.');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const autoResult = await runSilentIfJson(flags.json, () => studioRun({
|
|
502
|
+
positionals: ['auto-clip', path.resolve(sourceFile)],
|
|
503
|
+
flags: autoFlags,
|
|
504
|
+
}));
|
|
505
|
+
if (!autoResult || !autoResult.artifact) throw new Error('Failed to render auto-clip artifact');
|
|
506
|
+
artifact = autoResult.artifact;
|
|
507
|
+
artifactFullPath = path.resolve(targetDir, artifact.path);
|
|
508
|
+
qaReport = autoResult.qa;
|
|
509
|
+
captionMode = autoResult.caption_mode || null;
|
|
510
|
+
captionsCount = autoResult.captions_count ?? null;
|
|
511
|
+
planPath = autoResult.plan_path ? path.resolve(targetDir, autoResult.plan_path) : null;
|
|
512
|
+
if (!flags.json) console.log('✂️ [3/4] Cortes e legendas processados.');
|
|
513
|
+
if (!flags.json) console.log('🔍 [4/4] QA concluído.');
|
|
514
|
+
} else {
|
|
515
|
+
if (!flags.json) console.log('🎬 [2/4] Importando seus arquivos de vídeo...');
|
|
516
|
+
const importedAssets = [];
|
|
517
|
+
for (const f of files) {
|
|
518
|
+
const asset = await runSilentIfJson(flags.json, () => studioRun({ positionals: ['import', path.resolve(f)], flags: { dest: targetDir, json: false, verbose: flags.verbose } }));
|
|
519
|
+
if (asset) importedAssets.push(asset);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (importedAssets.length === 0 && mode !== 'dark') {
|
|
523
|
+
const manifestPath = path.join(targetDir, 'studio.json');
|
|
524
|
+
if (fs.existsSync(manifestPath)) {
|
|
525
|
+
try {
|
|
526
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
527
|
+
importedAssets.push(...(m.assets || []));
|
|
528
|
+
} catch (err) {
|
|
529
|
+
throw new Error(`Corrupted studio manifest at ${manifestPath}: ${err.message}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const plan = buildOnboardPlan({ mode, assets: importedAssets, format });
|
|
535
|
+
validateEditPlan(plan);
|
|
536
|
+
|
|
537
|
+
const packagesDir = path.join(targetDir, 'packages');
|
|
538
|
+
fs.mkdirSync(packagesDir, { recursive: true });
|
|
539
|
+
planPath = path.join(packagesDir, 'onboard-plan.json');
|
|
540
|
+
fs.writeFileSync(planPath, JSON.stringify(plan, null, 2) + '\n');
|
|
541
|
+
|
|
542
|
+
if (!flags.json) console.log(`✂️ [3/4] Fatiando silêncios e gerando vídeo (${mode})...`);
|
|
543
|
+
artifact = await runSilentIfJson(flags.json, () => studioRun({ positionals: ['render', planPath], flags: { dest: targetDir, json: false, verbose: flags.verbose } }));
|
|
544
|
+
if (!artifact || !artifact.path) {
|
|
545
|
+
throw new Error('Failed to render video artifact');
|
|
546
|
+
}
|
|
547
|
+
artifactFullPath = path.resolve(targetDir, artifact.path);
|
|
548
|
+
|
|
549
|
+
if (!flags.json) console.log('🔍 [4/4] Verificando conformidade técnica de qualidade (QA)...');
|
|
550
|
+
qaReport = await runSilentIfJson(flags.json, () => studioRun({ positionals: ['qa', artifactFullPath], flags: { dest: targetDir, plan: planPath, json: false, verbose: flags.verbose } }));
|
|
551
|
+
if (!qaReport || !qaReport.passed) {
|
|
552
|
+
throw new Error(`QA verification failed: ${qaReport?.errors?.join(', ') || 'QA check failed'}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!qaReport || !qaReport.passed) {
|
|
557
|
+
throw new Error(`QA verification failed: ${qaReport?.errors?.join(', ') || 'QA check failed'}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 6. Persist Profile
|
|
561
|
+
const profileData = {
|
|
562
|
+
default_dir: targetDir,
|
|
563
|
+
preferred_mode: mode,
|
|
564
|
+
preferred_format: format,
|
|
565
|
+
updated_at: new Date().toISOString(),
|
|
566
|
+
};
|
|
567
|
+
saveProfile(targetDir, profileData);
|
|
568
|
+
|
|
569
|
+
// 7. Delivery
|
|
570
|
+
if (flags.json) {
|
|
571
|
+
console.log(JSON.stringify({
|
|
572
|
+
status: 'ok',
|
|
573
|
+
workspace: targetDir,
|
|
574
|
+
mode,
|
|
575
|
+
pipeline,
|
|
576
|
+
caption_mode: captionMode,
|
|
577
|
+
captions_count: captionsCount,
|
|
578
|
+
artifact,
|
|
579
|
+
qa: qaReport,
|
|
580
|
+
profile: profileData,
|
|
581
|
+
next_steps: [
|
|
582
|
+
...(planPath ? [`esquadrao studio render ${displayPath(planPath)}`] : []),
|
|
583
|
+
`esquadrao studio qa ${displayPath(artifactFullPath)}`,
|
|
584
|
+
],
|
|
585
|
+
}, null, 2));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
console.log(`
|
|
590
|
+
=================================================================
|
|
591
|
+
🎉 Parabéns! Seu primeiro vídeo está pronto!
|
|
592
|
+
=================================================================
|
|
593
|
+
Arquivo gerado: ${artifactFullPath}
|
|
594
|
+
Identificador: ${artifact.id}
|
|
595
|
+
Resolução: ${qaReport.streams?.video?.width || '?'}x${qaReport.streams?.video?.height || '?'} (${format})
|
|
596
|
+
Áudio: ${qaReport.streams?.audio?.codec || 'aac'} (${qaReport.streams?.audio?.channels || 2} canais @ ${qaReport.streams?.audio?.sample_rate || 44100}Hz)
|
|
597
|
+
Duração: ${qaReport.duration_s?.toFixed(2) || '?'}s
|
|
598
|
+
Teste de QA: APROVADO ✓ (100% validado para postagem)
|
|
599
|
+
|
|
600
|
+
Perfil salvo em: ${path.join(targetDir, '.esquadrao', 'profile.json')}
|
|
601
|
+
Modo preferido: ${mode}
|
|
602
|
+
=================================================================
|
|
603
|
+
`);
|
|
604
|
+
return { status: 'ok', pipeline, caption_mode: captionMode, captions_count: captionsCount, artifact, qa: qaReport, profile: profileData };
|
|
605
|
+
} catch (err) {
|
|
606
|
+
if (rl) {
|
|
607
|
+
try { rl.close(); } catch {}
|
|
608
|
+
}
|
|
609
|
+
if (flags.verbose && err.stack) {
|
|
610
|
+
console.error(err.stack);
|
|
611
|
+
} else {
|
|
612
|
+
console.error(`Error: ${err.message}`);
|
|
613
|
+
}
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export default run;
|