@seanmozeik/vicon 0.1.0 → 0.1.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/package.json +1 -1
- package/src/index.ts +24 -2
- package/src/lib/prompt.ts +46 -9
- package/src/lib/tools.ts +23 -14
- package/src/types.ts +2 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
1
3
|
import * as p from "@clack/prompts";
|
|
2
4
|
import boxen from "boxen";
|
|
3
5
|
import { generate, ValidationError } from "./lib/ai.js";
|
|
@@ -190,7 +192,8 @@ function renderToolSummary(ctx: ToolCtx): string {
|
|
|
190
192
|
|
|
191
193
|
if (ctx.ffmpeg.installed) {
|
|
192
194
|
const ver = ctx.ffmpeg.version ?? "?";
|
|
193
|
-
const enc =
|
|
195
|
+
const enc =
|
|
196
|
+
ctx.ffmpeg.videoEncoders.length + ctx.ffmpeg.audioEncoders.length;
|
|
194
197
|
const dec = ctx.ffmpeg.decoders.length;
|
|
195
198
|
parts.push(
|
|
196
199
|
theme.muted(`ffmpeg ${ver} (${enc} encoders · ${dec} decoders)`),
|
|
@@ -395,13 +398,25 @@ async function handleEditCommandsAction(
|
|
|
395
398
|
}
|
|
396
399
|
|
|
397
400
|
async function handleRunAction(commands: string[]): Promise<void> {
|
|
401
|
+
const preExisting = new Set(
|
|
402
|
+
await Promise.all(
|
|
403
|
+
inferInputFiles(commands).map(async (f) => {
|
|
404
|
+
const file = Bun.file(f);
|
|
405
|
+
return (await file.exists()) ? f : null;
|
|
406
|
+
}),
|
|
407
|
+
).then((results) => results.filter((f): f is string => f !== null)),
|
|
408
|
+
);
|
|
409
|
+
|
|
398
410
|
const success = await runCommands(commands, {
|
|
399
411
|
onBefore: (cmd, i, total) => p.log.step(`▶ [${i + 1}/${total}] ${cmd}`),
|
|
400
412
|
onSuccess: () => p.log.success("All commands completed successfully."),
|
|
401
413
|
onError: (cmd, exitCode) =>
|
|
402
414
|
p.log.error(`Command exited with code ${exitCode}: ${cmd}`),
|
|
403
415
|
});
|
|
404
|
-
|
|
416
|
+
|
|
417
|
+
if (success) {
|
|
418
|
+
await runCleanup([...preExisting]);
|
|
419
|
+
}
|
|
405
420
|
process.exit(success ? 0 : 1);
|
|
406
421
|
}
|
|
407
422
|
|
|
@@ -417,6 +432,13 @@ async function runConversion(
|
|
|
417
432
|
toolSpinner.stop("Tools detected.");
|
|
418
433
|
p.log.info(renderToolSummary(ctx));
|
|
419
434
|
|
|
435
|
+
if (!ctx.ffmpeg.installed && !ctx.magick.installed) {
|
|
436
|
+
p.log.error(
|
|
437
|
+
"No media tools found. Install ffmpeg or ImageMagick and try again.",
|
|
438
|
+
);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
|
|
420
442
|
let { result: currentResult, userRequest } = await generateUntilSuccess(
|
|
421
443
|
initialRequest,
|
|
422
444
|
ctx,
|
package/src/lib/prompt.ts
CHANGED
|
@@ -1,26 +1,63 @@
|
|
|
1
1
|
import type { ToolContext } from "../types.js";
|
|
2
2
|
|
|
3
|
+
// Common output format → required ffmpeg encoder.
|
|
4
|
+
// Used to help the AI cross-reference before generating commands.
|
|
5
|
+
const FORMAT_ENCODER_REF = `\
|
|
6
|
+
## Output Format → Required ffmpeg Encoder
|
|
7
|
+
webp → libwebp
|
|
8
|
+
hevc/h265 → libx265
|
|
9
|
+
h264/mp4/mov/mkv → libx264
|
|
10
|
+
vp8/webm → libvpx
|
|
11
|
+
vp9 → libvpx-vp9
|
|
12
|
+
av1 → libsvtav1 or libaom-av1
|
|
13
|
+
gif → gif (built-in)
|
|
14
|
+
png → png (built-in)
|
|
15
|
+
jpeg/jpg → mjpeg (built-in)
|
|
16
|
+
mp3 → libmp3lame
|
|
17
|
+
aac → aac (built-in)
|
|
18
|
+
opus → libopus
|
|
19
|
+
flac → flac (built-in)
|
|
20
|
+
Use this table to identify which encoder is needed, then verify it appears in the available encoder lists before generating any command.`;
|
|
21
|
+
|
|
3
22
|
export function buildSystemPrompt(ctx: ToolContext): string {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
23
|
+
const lines: string[] = [];
|
|
24
|
+
|
|
25
|
+
lines.push("## Available Tools");
|
|
26
|
+
|
|
27
|
+
if (ctx.ffmpeg.installed) {
|
|
28
|
+
lines.push(`ffmpeg ${ctx.ffmpeg.version ?? "unknown"}`);
|
|
29
|
+
lines.push(
|
|
30
|
+
` Video encoders: ${ctx.ffmpeg.videoEncoders.join(", ") || "none"}`,
|
|
31
|
+
);
|
|
32
|
+
lines.push(
|
|
33
|
+
` Audio encoders: ${ctx.ffmpeg.audioEncoders.join(", ") || "none"}`,
|
|
34
|
+
);
|
|
35
|
+
} else {
|
|
36
|
+
lines.push("ffmpeg: NOT installed — do not generate ffmpeg commands");
|
|
37
|
+
}
|
|
7
38
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
: "
|
|
39
|
+
if (ctx.magick.installed) {
|
|
40
|
+
lines.push(`magick ${ctx.magick.version ?? "unknown"}`);
|
|
41
|
+
lines.push(` Formats: ${ctx.magick.formats.join(", ") || "none"}`);
|
|
42
|
+
} else {
|
|
43
|
+
lines.push("magick: NOT installed — do not generate magick commands");
|
|
44
|
+
}
|
|
11
45
|
|
|
12
|
-
const environment =
|
|
46
|
+
const environment = lines.join("\n");
|
|
13
47
|
|
|
14
48
|
const rules = `## Rules
|
|
15
49
|
Return ONLY valid JSON in this exact shape: { "commands": string[], "explanation": string }
|
|
16
50
|
- explanation: plain prose only — no shell syntax, no backticks, no code
|
|
17
51
|
- commands: complete, copy-pasteable shell strings — no placeholders, no &&, no loops
|
|
18
|
-
-
|
|
52
|
+
- BEFORE generating any command: identify the required encoder/format using the reference table below, then verify it appears in the available lists above
|
|
53
|
+
- NEVER rely on ffmpeg auto-selection — always specify -c:v for video output and -c:a for audio output explicitly
|
|
54
|
+
- If a required ffmpeg encoder is not available, use magick if the format is in its format list
|
|
55
|
+
- If neither tool can handle the task, return { "commands": [], "explanation": "<reason why it cannot be done with available tools>" }
|
|
19
56
|
- Prefer non-destructive output: append _converted to output filenames, use -n flag to avoid overwriting
|
|
20
57
|
- For batch tasks, emit one command per file
|
|
21
58
|
IMPORTANT: Reply with ONLY the JSON object — no markdown fences, no extra text`;
|
|
22
59
|
|
|
23
|
-
return [environment, rules].join("\n\n");
|
|
60
|
+
return [environment, FORMAT_ENCODER_REF, rules].join("\n\n");
|
|
24
61
|
}
|
|
25
62
|
|
|
26
63
|
export function buildUserPrompt(request: string): string {
|
package/src/lib/tools.ts
CHANGED
|
@@ -13,7 +13,13 @@ async function run(cmd: string[]): Promise<string> {
|
|
|
13
13
|
|
|
14
14
|
async function probeFfmpeg(): Promise<ToolContext["ffmpeg"]> {
|
|
15
15
|
const versionOut = await run(["ffmpeg", "-version"]);
|
|
16
|
-
if (!versionOut)
|
|
16
|
+
if (!versionOut)
|
|
17
|
+
return {
|
|
18
|
+
installed: false,
|
|
19
|
+
videoEncoders: [],
|
|
20
|
+
audioEncoders: [],
|
|
21
|
+
decoders: [],
|
|
22
|
+
};
|
|
17
23
|
|
|
18
24
|
const versionMatch = versionOut.split("\n")[0]?.match(/ffmpeg version (\S+)/);
|
|
19
25
|
const version = versionMatch?.[1];
|
|
@@ -21,21 +27,24 @@ async function probeFfmpeg(): Promise<ToolContext["ffmpeg"]> {
|
|
|
21
27
|
const encodersOut = await run(["ffmpeg", "-encoders"]);
|
|
22
28
|
const decodersOut = await run(["ffmpeg", "-decoders"]);
|
|
23
29
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const parseEncoderLines = (out: string) =>
|
|
31
|
+
out
|
|
32
|
+
.split("\n")
|
|
33
|
+
.filter((l) => /^ [VAS][A-Z.]{5} \S/.test(l))
|
|
34
|
+
.map((l) => ({ type: l[1], name: l.trim().split(/\s+/)[1] ?? "" }))
|
|
35
|
+
.filter((e) => e.name);
|
|
30
36
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
.filter(
|
|
37
|
+
const encoderEntries = parseEncoderLines(encodersOut);
|
|
38
|
+
const videoEncoders = encoderEntries
|
|
39
|
+
.filter((e) => e.type === "V")
|
|
40
|
+
.map((e) => e.name);
|
|
41
|
+
const audioEncoders = encoderEntries
|
|
42
|
+
.filter((e) => e.type === "A")
|
|
43
|
+
.map((e) => e.name);
|
|
44
|
+
|
|
45
|
+
const decoders = parseEncoderLines(decodersOut).map((e) => e.name);
|
|
37
46
|
|
|
38
|
-
return { installed: true, version,
|
|
47
|
+
return { installed: true, version, videoEncoders, audioEncoders, decoders };
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
async function probeMagick(): Promise<ToolContext["magick"]> {
|