@kernel.chat/kbot 2.15.2 → 2.17.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/README.md +23 -12
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +30 -16
- package/dist/agent.js.map +1 -1
- package/dist/auth.d.ts +3 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +20 -2
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +108 -3
- package/dist/cli.js.map +1 -1
- package/dist/inference.d.ts +49 -0
- package/dist/inference.d.ts.map +1 -0
- package/dist/inference.js +308 -0
- package/dist/inference.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/vfx.d.ts +2 -0
- package/dist/tools/vfx.d.ts.map +1 -0
- package/dist/tools/vfx.js +831 -0
- package/dist/tools/vfx.js.map +1 -0
- package/package.json +16 -3
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
// K:BOT VFX & Creative Production Tools — Houdini-inspired
|
|
2
|
+
// Procedural generation, video processing, image manipulation,
|
|
3
|
+
// 3D rendering, shader generation, and creative coding.
|
|
4
|
+
import { registerTool } from './index.js';
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
|
+
function shell(cmd, args, timeout = 60_000) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
execFile(cmd, args, { timeout, maxBuffer: 2 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
9
|
+
if (err)
|
|
10
|
+
reject(new Error(stderr || err.message));
|
|
11
|
+
else
|
|
12
|
+
resolve(stdout || stderr);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function registerVfxTools() {
|
|
17
|
+
// ── VEX Code Generation ───────────────────────────────────────────
|
|
18
|
+
registerTool({
|
|
19
|
+
name: 'vex_generate',
|
|
20
|
+
description: 'Generate Houdini VEX code for procedural effects. Creates point wrangles, volume wrangles, and attribute manipulation code.',
|
|
21
|
+
parameters: {
|
|
22
|
+
effect: { type: 'string', description: 'Effect type: noise, scatter, curl_noise, wave, fractal, vortex, erosion, growth', required: true },
|
|
23
|
+
target: { type: 'string', description: 'Target context: point, vertex, primitive, detail (default: point)' },
|
|
24
|
+
params: { type: 'string', description: 'JSON params like frequency, amplitude, octaves' },
|
|
25
|
+
},
|
|
26
|
+
tier: 'free',
|
|
27
|
+
async execute(args) {
|
|
28
|
+
const effect = String(args.effect).toLowerCase();
|
|
29
|
+
const target = String(args.target || 'point');
|
|
30
|
+
const params = args.params ? JSON.parse(String(args.params)) : {};
|
|
31
|
+
const templates = {
|
|
32
|
+
noise: `// ${target} wrangle — Perlin noise displacement
|
|
33
|
+
float freq = chf("frequency"); // ${params.frequency || 1.0}
|
|
34
|
+
float amp = chf("amplitude"); // ${params.amplitude || 0.5}
|
|
35
|
+
int oct = chi("octaves"); // ${params.octaves || 4}
|
|
36
|
+
|
|
37
|
+
vector pos = @P;
|
|
38
|
+
float n = 0;
|
|
39
|
+
float f = freq;
|
|
40
|
+
float a = amp;
|
|
41
|
+
|
|
42
|
+
for (int i = 0; i < oct; i++) {
|
|
43
|
+
n += a * noise(pos * f);
|
|
44
|
+
f *= 2.0;
|
|
45
|
+
a *= 0.5;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@P += @N * n;
|
|
49
|
+
@Cd = set(n, n * 0.8, n * 0.6);`,
|
|
50
|
+
curl_noise: `// ${target} wrangle — Curl noise for fluid-like motion
|
|
51
|
+
float freq = chf("frequency"); // ${params.frequency || 0.5}
|
|
52
|
+
float amp = chf("amplitude"); // ${params.amplitude || 1.0}
|
|
53
|
+
float time = @Time;
|
|
54
|
+
|
|
55
|
+
vector pos = @P * freq + time * 0.3;
|
|
56
|
+
|
|
57
|
+
// Compute curl via cross product of noise gradients
|
|
58
|
+
float eps = 0.001;
|
|
59
|
+
vector dx = set(eps, 0, 0);
|
|
60
|
+
vector dy = set(0, eps, 0);
|
|
61
|
+
vector dz = set(0, 0, eps);
|
|
62
|
+
|
|
63
|
+
float nx = noise(pos + dy).z - noise(pos - dy).z - noise(pos + dz).y + noise(pos - dz).y;
|
|
64
|
+
float ny = noise(pos + dz).x - noise(pos - dz).x - noise(pos + dx).z + noise(pos - dx).z;
|
|
65
|
+
float nz = noise(pos + dx).y - noise(pos - dx).y - noise(pos + dy).x + noise(pos - dy).x;
|
|
66
|
+
|
|
67
|
+
vector curl = set(nx, ny, nz) / (2.0 * eps);
|
|
68
|
+
@v = curl * amp;
|
|
69
|
+
@P += @v * @TimeInc;`,
|
|
70
|
+
scatter: `// ${target} wrangle — Poisson disk scatter
|
|
71
|
+
float density = chf("density"); // ${params.density || 100}
|
|
72
|
+
float radius = chf("min_radius"); // ${params.radius || 0.1}
|
|
73
|
+
float seed = chf("seed");
|
|
74
|
+
|
|
75
|
+
int npts = int(density * @Area);
|
|
76
|
+
for (int i = 0; i < npts; i++) {
|
|
77
|
+
vector2 uv = set(random(i + seed), random(i + seed + 0.5));
|
|
78
|
+
vector pos = primuv(0, "P", @primnum, uv);
|
|
79
|
+
int pt = addpoint(0, pos);
|
|
80
|
+
setpointattrib(0, "N", pt, @N);
|
|
81
|
+
setpointattrib(0, "Cd", pt, rand(set(i, seed, 0)));
|
|
82
|
+
}`,
|
|
83
|
+
wave: `// ${target} wrangle — Sine wave deformation
|
|
84
|
+
float freq = chf("frequency"); // ${params.frequency || 3.0}
|
|
85
|
+
float amp = chf("amplitude"); // ${params.amplitude || 0.3}
|
|
86
|
+
float speed = chf("speed"); // ${params.speed || 1.0}
|
|
87
|
+
|
|
88
|
+
float dist = length(@P.xz);
|
|
89
|
+
float wave = sin(dist * freq - @Time * speed) * amp;
|
|
90
|
+
wave *= exp(-dist * 0.1); // Falloff
|
|
91
|
+
|
|
92
|
+
@P.y += wave;
|
|
93
|
+
@Cd = fit01(wave / amp * 0.5 + 0.5, set(0.1, 0.2, 0.8), set(0.9, 0.95, 1.0));`,
|
|
94
|
+
fractal: `// ${target} wrangle — Mandelbrot fractal mapping
|
|
95
|
+
int max_iter = chi("iterations"); // ${params.iterations || 100}
|
|
96
|
+
float scale = chf("scale"); // ${params.scale || 2.0}
|
|
97
|
+
vector2 center = chu("center"); // ${params.center || '0, 0'}
|
|
98
|
+
|
|
99
|
+
vector2 c = (@P.xz - 0.5) * scale + set(center.x, center.y);
|
|
100
|
+
vector2 z = c;
|
|
101
|
+
int iter = 0;
|
|
102
|
+
|
|
103
|
+
for (int i = 0; i < max_iter; i++) {
|
|
104
|
+
if (length(z) > 2.0) break;
|
|
105
|
+
z = set(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
|
|
106
|
+
iter++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
float t = float(iter) / float(max_iter);
|
|
110
|
+
@P.y = t * chf("height");
|
|
111
|
+
@Cd = chramp("color", t);`,
|
|
112
|
+
vortex: `// ${target} wrangle — Vortex field
|
|
113
|
+
float strength = chf("strength"); // ${params.strength || 2.0}
|
|
114
|
+
float radius = chf("radius"); // ${params.radius || 1.0}
|
|
115
|
+
float falloff = chf("falloff"); // ${params.falloff || 2.0}
|
|
116
|
+
vector center = chv("center");
|
|
117
|
+
|
|
118
|
+
vector delta = @P - center;
|
|
119
|
+
float dist = length(delta);
|
|
120
|
+
float factor = strength * exp(-pow(dist / radius, falloff));
|
|
121
|
+
|
|
122
|
+
// Tangential velocity (perpendicular to radial direction)
|
|
123
|
+
vector tangent = normalize(cross(delta, set(0, 1, 0)));
|
|
124
|
+
@v += tangent * factor;
|
|
125
|
+
@P += @v * @TimeInc;`,
|
|
126
|
+
erosion: `// ${target} wrangle — Hydraulic erosion simulation step
|
|
127
|
+
float sediment = 0;
|
|
128
|
+
float water = chf("water_amount"); // ${params.water || 0.1}
|
|
129
|
+
float erosion_rate = chf("erosion_rate"); // ${params.erosion_rate || 0.01}
|
|
130
|
+
float deposition = chf("deposition"); // ${params.deposition || 0.005}
|
|
131
|
+
|
|
132
|
+
// Get height and neighbors
|
|
133
|
+
float h = @P.y;
|
|
134
|
+
int nbs[] = neighbours(0, @ptnum);
|
|
135
|
+
|
|
136
|
+
float min_h = h;
|
|
137
|
+
int min_nb = -1;
|
|
138
|
+
foreach (int nb; nbs) {
|
|
139
|
+
float nh = point(0, "P", nb).y;
|
|
140
|
+
if (nh < min_h) { min_h = nh; min_nb = nb; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (min_nb >= 0) {
|
|
144
|
+
float diff = h - min_h;
|
|
145
|
+
float erode = min(diff * erosion_rate * water, diff * 0.5);
|
|
146
|
+
@P.y -= erode;
|
|
147
|
+
sediment += erode;
|
|
148
|
+
// Deposit downstream
|
|
149
|
+
vector npos = point(0, "P", min_nb);
|
|
150
|
+
npos.y += sediment * deposition;
|
|
151
|
+
setpointattrib(0, "P", min_nb, npos);
|
|
152
|
+
}`,
|
|
153
|
+
growth: `// ${target} wrangle — Differential growth / space colonization
|
|
154
|
+
float search_radius = chf("search_radius"); // ${params.search_radius || 0.5}
|
|
155
|
+
float step_size = chf("step_size"); // ${params.step_size || 0.05}
|
|
156
|
+
float repel = chf("repulsion"); // ${params.repulsion || 0.02}
|
|
157
|
+
|
|
158
|
+
int nbs[] = pcfind(0, "P", @P, search_radius, 20);
|
|
159
|
+
vector avg_dir = set(0, 0, 0);
|
|
160
|
+
|
|
161
|
+
foreach (int nb; nbs) {
|
|
162
|
+
if (nb == @ptnum) continue;
|
|
163
|
+
vector delta = @P - point(0, "P", nb);
|
|
164
|
+
float dist = length(delta);
|
|
165
|
+
if (dist > 0.001) {
|
|
166
|
+
avg_dir += normalize(delta) * repel / (dist * dist);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Grow along normal + repulsion
|
|
171
|
+
@P += normalize(@N + avg_dir) * step_size;
|
|
172
|
+
@N = normalize(@N + avg_dir * 0.1);`,
|
|
173
|
+
};
|
|
174
|
+
const code = templates[effect];
|
|
175
|
+
if (!code) {
|
|
176
|
+
return `Unknown effect "${effect}". Available: ${Object.keys(templates).join(', ')}`;
|
|
177
|
+
}
|
|
178
|
+
return `\`\`\`vex\n${code}\n\`\`\`\n\nPaste into a ${target} wrangle in Houdini. Adjust channel references (chf/chi/chv) on the node parameters.`;
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
// ── FFmpeg Video Processing ───────────────────────────────────────
|
|
182
|
+
registerTool({
|
|
183
|
+
name: 'ffmpeg_process',
|
|
184
|
+
description: 'Process video/audio with FFmpeg. Encode, decode, filter, composite, extract frames, create timelapses, add effects.',
|
|
185
|
+
parameters: {
|
|
186
|
+
input: { type: 'string', description: 'Input file path', required: true },
|
|
187
|
+
output: { type: 'string', description: 'Output file path', required: true },
|
|
188
|
+
operation: { type: 'string', description: 'Operation: encode, extract_frames, gif, timelapse, stabilize, grayscale, reverse, speed, trim, concat, audio_extract, thumbnail', required: true },
|
|
189
|
+
options: { type: 'string', description: 'Additional options as JSON (e.g., {"fps": 30, "start": "00:01:00", "duration": "10"})' },
|
|
190
|
+
},
|
|
191
|
+
tier: 'free',
|
|
192
|
+
timeout: 300_000,
|
|
193
|
+
async execute(args) {
|
|
194
|
+
const input = String(args.input);
|
|
195
|
+
const output = String(args.output);
|
|
196
|
+
const op = String(args.operation);
|
|
197
|
+
const opts = args.options ? JSON.parse(String(args.options)) : {};
|
|
198
|
+
const commands = {
|
|
199
|
+
encode: ['-i', input, '-c:v', 'libx264', '-preset', 'medium', '-crf', String(opts.quality || 23), output],
|
|
200
|
+
extract_frames: ['-i', input, '-vf', `fps=${opts.fps || 1}`, `${output}_%04d.png`],
|
|
201
|
+
gif: ['-i', input, '-vf', `fps=${opts.fps || 10},scale=${opts.width || 480}:-1:flags=lanczos`, '-loop', '0', output],
|
|
202
|
+
timelapse: ['-i', input, '-vf', `setpts=${1 / (opts.speed || 10)}*PTS`, output],
|
|
203
|
+
grayscale: ['-i', input, '-vf', 'format=gray', output],
|
|
204
|
+
reverse: ['-i', input, '-vf', 'reverse', '-af', 'areverse', output],
|
|
205
|
+
speed: ['-i', input, '-vf', `setpts=${1 / (opts.speed || 2)}*PTS`, '-af', `atempo=${opts.speed || 2}`, output],
|
|
206
|
+
trim: ['-i', input, '-ss', opts.start || '0', '-t', opts.duration || '10', '-c', 'copy', output],
|
|
207
|
+
audio_extract: ['-i', input, '-vn', '-acodec', opts.codec || 'libmp3lame', output],
|
|
208
|
+
thumbnail: ['-i', input, '-vf', 'thumbnail', '-frames:v', '1', output],
|
|
209
|
+
stabilize: ['-i', input, '-vf', 'deshake', output],
|
|
210
|
+
};
|
|
211
|
+
const cmdArgs = commands[op];
|
|
212
|
+
if (!cmdArgs)
|
|
213
|
+
return `Unknown operation "${op}". Available: ${Object.keys(commands).join(', ')}`;
|
|
214
|
+
try {
|
|
215
|
+
const result = await shell('ffmpeg', ['-y', ...cmdArgs], 300_000);
|
|
216
|
+
return `FFmpeg ${op} complete: ${output}\n${result}`;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
return `FFmpeg error: ${err instanceof Error ? err.message : String(err)}`;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
// ── ImageMagick ───────────────────────────────────────────────────
|
|
224
|
+
registerTool({
|
|
225
|
+
name: 'imagemagick',
|
|
226
|
+
description: 'Manipulate images with ImageMagick. Resize, crop, composite, effects, format conversion, batch processing.',
|
|
227
|
+
parameters: {
|
|
228
|
+
input: { type: 'string', description: 'Input image path', required: true },
|
|
229
|
+
output: { type: 'string', description: 'Output image path', required: true },
|
|
230
|
+
operation: { type: 'string', description: 'Operation: resize, crop, rotate, blur, sharpen, grayscale, sepia, posterize, edge, emboss, negate, composite, montage, border, text', required: true },
|
|
231
|
+
value: { type: 'string', description: 'Operation value (e.g., "50%" for resize, "10x10" for blur, text for annotation)' },
|
|
232
|
+
},
|
|
233
|
+
tier: 'free',
|
|
234
|
+
async execute(args) {
|
|
235
|
+
const input = String(args.input);
|
|
236
|
+
const output = String(args.output);
|
|
237
|
+
const op = String(args.operation);
|
|
238
|
+
const val = String(args.value || '');
|
|
239
|
+
const commands = {
|
|
240
|
+
resize: ['convert', input, '-resize', val || '50%', output],
|
|
241
|
+
crop: ['convert', input, '-crop', val || '100x100+0+0', output],
|
|
242
|
+
rotate: ['convert', input, '-rotate', val || '90', output],
|
|
243
|
+
blur: ['convert', input, '-blur', val || '0x8', output],
|
|
244
|
+
sharpen: ['convert', input, '-sharpen', val || '0x3', output],
|
|
245
|
+
grayscale: ['convert', input, '-colorspace', 'Gray', output],
|
|
246
|
+
sepia: ['convert', input, '-sepia-tone', val || '80%', output],
|
|
247
|
+
posterize: ['convert', input, '-posterize', val || '4', output],
|
|
248
|
+
edge: ['convert', input, '-edge', val || '1', output],
|
|
249
|
+
emboss: ['convert', input, '-emboss', val || '2', output],
|
|
250
|
+
negate: ['convert', input, '-negate', output],
|
|
251
|
+
border: ['convert', input, '-border', val || '10x10', '-bordercolor', '#6B5B95', output],
|
|
252
|
+
text: ['convert', input, '-pointsize', '36', '-fill', 'white', '-gravity', 'south', '-annotate', '+0+10', val || 'kbot', output],
|
|
253
|
+
};
|
|
254
|
+
const cmdArgs = commands[op];
|
|
255
|
+
if (!cmdArgs)
|
|
256
|
+
return `Unknown operation "${op}". Available: ${Object.keys(commands).join(', ')}`;
|
|
257
|
+
try {
|
|
258
|
+
const result = await shell('magick', cmdArgs);
|
|
259
|
+
return `ImageMagick ${op} complete: ${output}\n${result}`;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Fallback: try without 'magick' prefix (older ImageMagick)
|
|
263
|
+
try {
|
|
264
|
+
const result = await shell(cmdArgs[0], cmdArgs.slice(1));
|
|
265
|
+
return `ImageMagick ${op} complete: ${output}\n${result}`;
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
return `ImageMagick error: ${err instanceof Error ? err.message : String(err)}. Is ImageMagick installed?`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
// ── Blender Script ────────────────────────────────────────────────
|
|
274
|
+
registerTool({
|
|
275
|
+
name: 'blender_run',
|
|
276
|
+
description: 'Execute a Blender Python script in background mode. Generate 3D models, render scenes, create animations.',
|
|
277
|
+
parameters: {
|
|
278
|
+
script: { type: 'string', description: 'Python script content or file path', required: true },
|
|
279
|
+
output: { type: 'string', description: 'Output file path for renders' },
|
|
280
|
+
blend_file: { type: 'string', description: 'Optional .blend file to open first' },
|
|
281
|
+
},
|
|
282
|
+
tier: 'pro',
|
|
283
|
+
timeout: 300_000,
|
|
284
|
+
async execute(args) {
|
|
285
|
+
const script = String(args.script);
|
|
286
|
+
const output = args.output ? String(args.output) : '';
|
|
287
|
+
// Write inline script to temp file if it's not a path
|
|
288
|
+
if (!script.endsWith('.py')) {
|
|
289
|
+
const { writeFileSync, mkdtempSync } = await import('fs');
|
|
290
|
+
const { join } = await import('path');
|
|
291
|
+
const tmpDir = mkdtempSync('/tmp/kbot-blender-');
|
|
292
|
+
const scriptPath = join(tmpDir, 'script.py');
|
|
293
|
+
writeFileSync(scriptPath, script);
|
|
294
|
+
const blenderArgs = ['--background', '--python', scriptPath];
|
|
295
|
+
if (args.blend_file)
|
|
296
|
+
blenderArgs.unshift(String(args.blend_file));
|
|
297
|
+
if (output)
|
|
298
|
+
blenderArgs.push('--render-output', output, '--render-frame', '1');
|
|
299
|
+
try {
|
|
300
|
+
return await shell('blender', blenderArgs, 300_000);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
return `Blender error: ${err instanceof Error ? err.message : String(err)}. Is Blender installed?`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const blenderArgs = ['--background', '--python', script];
|
|
307
|
+
if (args.blend_file)
|
|
308
|
+
blenderArgs.unshift(String(args.blend_file));
|
|
309
|
+
try {
|
|
310
|
+
return await shell('blender', blenderArgs, 300_000);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
return `Blender error: ${err instanceof Error ? err.message : String(err)}`;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
// ── Procedural Texture Generation ─────────────────────────────────
|
|
318
|
+
registerTool({
|
|
319
|
+
name: 'texture_generate',
|
|
320
|
+
description: 'Generate tileable procedural textures using Python/Pillow. Creates noise, marble, wood, brick, hex patterns.',
|
|
321
|
+
parameters: {
|
|
322
|
+
type: { type: 'string', description: 'Texture type: perlin, marble, wood, brick, hexagon, voronoi, checkerboard, gradient', required: true },
|
|
323
|
+
size: { type: 'number', description: 'Texture size in pixels (default: 512)' },
|
|
324
|
+
output: { type: 'string', description: 'Output file path (default: texture.png)', required: true },
|
|
325
|
+
seed: { type: 'number', description: 'Random seed for reproducibility' },
|
|
326
|
+
},
|
|
327
|
+
tier: 'free',
|
|
328
|
+
async execute(args) {
|
|
329
|
+
const type = String(args.type);
|
|
330
|
+
const size = Number(args.size) || 512;
|
|
331
|
+
const output = String(args.output);
|
|
332
|
+
const seed = args.seed !== undefined ? Number(args.seed) : 42;
|
|
333
|
+
const script = `
|
|
334
|
+
import random, math
|
|
335
|
+
random.seed(${seed})
|
|
336
|
+
|
|
337
|
+
# Simple procedural texture generator
|
|
338
|
+
size = ${size}
|
|
339
|
+
pixels = []
|
|
340
|
+
|
|
341
|
+
def noise2d(x, y):
|
|
342
|
+
n = int(x * 57 + y * 131 + ${seed})
|
|
343
|
+
n = (n << 13) ^ n
|
|
344
|
+
return 1.0 - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0
|
|
345
|
+
|
|
346
|
+
def smooth_noise(x, y):
|
|
347
|
+
corners = (noise2d(int(x)-1, int(y)-1) + noise2d(int(x)+1, int(y)-1) + noise2d(int(x)-1, int(y)+1) + noise2d(int(x)+1, int(y)+1)) / 16.0
|
|
348
|
+
sides = (noise2d(int(x)-1, int(y)) + noise2d(int(x)+1, int(y)) + noise2d(int(x), int(y)-1) + noise2d(int(x), int(y)+1)) / 8.0
|
|
349
|
+
center = noise2d(int(x), int(y)) / 4.0
|
|
350
|
+
return corners + sides + center
|
|
351
|
+
|
|
352
|
+
def lerp(a, b, t):
|
|
353
|
+
return a + t * (b - a)
|
|
354
|
+
|
|
355
|
+
for py in range(size):
|
|
356
|
+
for px in range(size):
|
|
357
|
+
u = px / size
|
|
358
|
+
v = py / size
|
|
359
|
+
t = '${type}'
|
|
360
|
+
if t == 'checkerboard':
|
|
361
|
+
c = int((u * 8) % 2) ^ int((v * 8) % 2)
|
|
362
|
+
val = c * 255
|
|
363
|
+
elif t == 'gradient':
|
|
364
|
+
val = int(u * 255)
|
|
365
|
+
elif t == 'hexagon':
|
|
366
|
+
hx = u * 8
|
|
367
|
+
hy = v * 8
|
|
368
|
+
if int(hy) % 2 == 1: hx += 0.5
|
|
369
|
+
fx = hx - int(hx)
|
|
370
|
+
fy = hy - int(hy)
|
|
371
|
+
d = min(abs(fx - 0.5), abs(fy - 0.5))
|
|
372
|
+
val = 255 if d > 0.1 else 100
|
|
373
|
+
elif t == 'voronoi':
|
|
374
|
+
min_d = 999
|
|
375
|
+
for i in range(16):
|
|
376
|
+
cx = random.Random(i + ${seed}).random()
|
|
377
|
+
cy = random.Random(i + ${seed} + 100).random()
|
|
378
|
+
d = math.sqrt((u - cx)**2 + (v - cy)**2)
|
|
379
|
+
min_d = min(min_d, d)
|
|
380
|
+
val = int(min(min_d * 500, 255))
|
|
381
|
+
else:
|
|
382
|
+
# Perlin-ish noise for perlin, marble, wood
|
|
383
|
+
n = smooth_noise(u * 8, v * 8)
|
|
384
|
+
if t == 'marble':
|
|
385
|
+
n = math.sin(u * 10 + n * 5) * 0.5 + 0.5
|
|
386
|
+
elif t == 'wood':
|
|
387
|
+
d = math.sqrt((u - 0.5)**2 + (v - 0.5)**2) * 20
|
|
388
|
+
n = math.sin(d + n * 2) * 0.5 + 0.5
|
|
389
|
+
else:
|
|
390
|
+
n = n * 0.5 + 0.5
|
|
391
|
+
val = int(max(0, min(255, n * 255)))
|
|
392
|
+
pixels.append(val)
|
|
393
|
+
|
|
394
|
+
# Write as PGM then convert
|
|
395
|
+
with open('${output.replace(/'/g, "\\'")}', 'wb') as f:
|
|
396
|
+
header = f'P5\\n${size} ${size}\\n255\\n'
|
|
397
|
+
f.write(header.encode())
|
|
398
|
+
f.write(bytes(pixels))
|
|
399
|
+
|
|
400
|
+
print(f'Generated ${type} texture: ${size}x${size} -> ${output}')
|
|
401
|
+
`;
|
|
402
|
+
try {
|
|
403
|
+
return await shell('python3', ['-c', script], 30_000);
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
return `Texture generation error: ${err instanceof Error ? err.message : String(err)}`;
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
// ── GLSL Shader Generation ────────────────────────────────────────
|
|
411
|
+
registerTool({
|
|
412
|
+
name: 'shader_generate',
|
|
413
|
+
description: 'Generate GLSL/HLSL shader code from descriptions. Creates vertex, fragment, compute shaders for effects like water, fire, displacement, post-processing.',
|
|
414
|
+
parameters: {
|
|
415
|
+
effect: { type: 'string', description: 'Shader effect: water, fire, plasma, raymarching, fog, bloom, chromatic_aberration, film_grain, outline, dissolve', required: true },
|
|
416
|
+
language: { type: 'string', description: 'Shader language: glsl, hlsl, wgsl (default: glsl)' },
|
|
417
|
+
},
|
|
418
|
+
tier: 'free',
|
|
419
|
+
async execute(args) {
|
|
420
|
+
const effect = String(args.effect).toLowerCase();
|
|
421
|
+
const lang = String(args.language || 'glsl').toLowerCase();
|
|
422
|
+
const shaders = {
|
|
423
|
+
water: `// Water surface shader — ${lang.toUpperCase()}
|
|
424
|
+
precision mediump float;
|
|
425
|
+
uniform float u_time;
|
|
426
|
+
uniform vec2 u_resolution;
|
|
427
|
+
|
|
428
|
+
float wave(vec2 p, float t) {
|
|
429
|
+
return sin(p.x * 3.0 + t) * 0.1 +
|
|
430
|
+
sin(p.y * 4.0 + t * 1.3) * 0.08 +
|
|
431
|
+
sin((p.x + p.y) * 5.0 + t * 0.7) * 0.05;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
void main() {
|
|
435
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
436
|
+
float w = wave(uv * 10.0, u_time);
|
|
437
|
+
|
|
438
|
+
// Fresnel-like edge darkening
|
|
439
|
+
float depth = 0.3 + w;
|
|
440
|
+
vec3 shallow = vec3(0.2, 0.7, 0.9);
|
|
441
|
+
vec3 deep = vec3(0.0, 0.1, 0.3);
|
|
442
|
+
vec3 color = mix(deep, shallow, depth);
|
|
443
|
+
|
|
444
|
+
// Specular highlight
|
|
445
|
+
float spec = pow(max(0.0, w * 5.0), 8.0) * 0.5;
|
|
446
|
+
color += vec3(spec);
|
|
447
|
+
|
|
448
|
+
gl_FragColor = vec4(color, 0.9);
|
|
449
|
+
}`,
|
|
450
|
+
fire: `// Procedural fire shader — ${lang.toUpperCase()}
|
|
451
|
+
precision mediump float;
|
|
452
|
+
uniform float u_time;
|
|
453
|
+
uniform vec2 u_resolution;
|
|
454
|
+
|
|
455
|
+
float hash(vec2 p) {
|
|
456
|
+
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
float noise(vec2 p) {
|
|
460
|
+
vec2 i = floor(p);
|
|
461
|
+
vec2 f = fract(p);
|
|
462
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
463
|
+
return mix(
|
|
464
|
+
mix(hash(i), hash(i + vec2(1,0)), f.x),
|
|
465
|
+
mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), f.x),
|
|
466
|
+
f.y
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
float fbm(vec2 p) {
|
|
471
|
+
float v = 0.0, a = 0.5;
|
|
472
|
+
for (int i = 0; i < 5; i++) {
|
|
473
|
+
v += a * noise(p);
|
|
474
|
+
p *= 2.0;
|
|
475
|
+
a *= 0.5;
|
|
476
|
+
}
|
|
477
|
+
return v;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
void main() {
|
|
481
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
482
|
+
uv.y = 1.0 - uv.y; // Flip Y
|
|
483
|
+
|
|
484
|
+
float n = fbm(uv * 5.0 + vec2(0, -u_time * 2.0));
|
|
485
|
+
float shape = 1.0 - uv.y;
|
|
486
|
+
shape *= smoothstep(0.0, 0.3, 0.5 - abs(uv.x - 0.5));
|
|
487
|
+
|
|
488
|
+
float fire = shape * n * 2.0;
|
|
489
|
+
|
|
490
|
+
vec3 col = vec3(1.5, 0.5, 0.1) * fire;
|
|
491
|
+
col += vec3(1.0, 0.9, 0.3) * pow(fire, 3.0);
|
|
492
|
+
col += vec3(0.3, 0.05, 0.0) * smoothstep(0.0, 0.5, fire);
|
|
493
|
+
|
|
494
|
+
gl_FragColor = vec4(col, fire);
|
|
495
|
+
}`,
|
|
496
|
+
plasma: `// Plasma effect shader — ${lang.toUpperCase()}
|
|
497
|
+
precision mediump float;
|
|
498
|
+
uniform float u_time;
|
|
499
|
+
uniform vec2 u_resolution;
|
|
500
|
+
|
|
501
|
+
void main() {
|
|
502
|
+
vec2 uv = gl_FragCoord.xy / u_resolution * 10.0;
|
|
503
|
+
float t = u_time;
|
|
504
|
+
|
|
505
|
+
float v = sin(uv.x + t);
|
|
506
|
+
v += sin((uv.y + t) * 0.5);
|
|
507
|
+
v += sin((uv.x + uv.y + t) * 0.5);
|
|
508
|
+
v += sin(sqrt(uv.x * uv.x + uv.y * uv.y) + t);
|
|
509
|
+
v *= 0.5;
|
|
510
|
+
|
|
511
|
+
vec3 color = vec3(
|
|
512
|
+
sin(v * 3.14159) * 0.5 + 0.5,
|
|
513
|
+
sin(v * 3.14159 + 2.094) * 0.5 + 0.5,
|
|
514
|
+
sin(v * 3.14159 + 4.189) * 0.5 + 0.5
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
gl_FragColor = vec4(color, 1.0);
|
|
518
|
+
}`,
|
|
519
|
+
raymarching: `// Raymarching SDF shader — ${lang.toUpperCase()}
|
|
520
|
+
precision mediump float;
|
|
521
|
+
uniform float u_time;
|
|
522
|
+
uniform vec2 u_resolution;
|
|
523
|
+
|
|
524
|
+
float sdSphere(vec3 p, float r) { return length(p) - r; }
|
|
525
|
+
float sdBox(vec3 p, vec3 b) { vec3 d = abs(p) - b; return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0)); }
|
|
526
|
+
|
|
527
|
+
float scene(vec3 p) {
|
|
528
|
+
float sphere = sdSphere(p - vec3(0, 0, 0), 1.0);
|
|
529
|
+
float box = sdBox(p - vec3(0, 0, 0), vec3(0.75));
|
|
530
|
+
float blend = mix(sphere, box, sin(u_time) * 0.5 + 0.5);
|
|
531
|
+
|
|
532
|
+
// Infinite repetition
|
|
533
|
+
vec3 q = mod(p + 2.0, 4.0) - 2.0;
|
|
534
|
+
float repeated = sdSphere(q, 0.3);
|
|
535
|
+
|
|
536
|
+
return min(blend, repeated);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
void main() {
|
|
540
|
+
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;
|
|
541
|
+
vec3 ro = vec3(0, 0, -3);
|
|
542
|
+
vec3 rd = normalize(vec3(uv, 1));
|
|
543
|
+
|
|
544
|
+
float t = 0.0;
|
|
545
|
+
for (int i = 0; i < 64; i++) {
|
|
546
|
+
vec3 p = ro + rd * t;
|
|
547
|
+
float d = scene(p);
|
|
548
|
+
if (d < 0.001 || t > 20.0) break;
|
|
549
|
+
t += d;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
vec3 col = vec3(0);
|
|
553
|
+
if (t < 20.0) {
|
|
554
|
+
vec3 p = ro + rd * t;
|
|
555
|
+
// Simple normal estimation
|
|
556
|
+
vec2 e = vec2(0.001, 0);
|
|
557
|
+
vec3 n = normalize(vec3(
|
|
558
|
+
scene(p + e.xyy) - scene(p - e.xyy),
|
|
559
|
+
scene(p + e.yxy) - scene(p - e.yxy),
|
|
560
|
+
scene(p + e.yyx) - scene(p - e.yyx)
|
|
561
|
+
));
|
|
562
|
+
float diff = max(dot(n, normalize(vec3(1, 1, -1))), 0.0);
|
|
563
|
+
col = vec3(0.4, 0.2, 0.6) * diff + vec3(0.1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
gl_FragColor = vec4(col, 1.0);
|
|
567
|
+
}`,
|
|
568
|
+
bloom: `// Post-processing bloom shader — ${lang.toUpperCase()}
|
|
569
|
+
precision mediump float;
|
|
570
|
+
uniform sampler2D u_texture;
|
|
571
|
+
uniform vec2 u_resolution;
|
|
572
|
+
uniform float u_threshold;
|
|
573
|
+
uniform float u_intensity;
|
|
574
|
+
|
|
575
|
+
vec3 sampleBlur(vec2 uv, float radius) {
|
|
576
|
+
vec3 sum = vec3(0);
|
|
577
|
+
float total = 0.0;
|
|
578
|
+
for (float x = -4.0; x <= 4.0; x += 1.0) {
|
|
579
|
+
for (float y = -4.0; y <= 4.0; y += 1.0) {
|
|
580
|
+
vec2 offset = vec2(x, y) * radius / u_resolution;
|
|
581
|
+
float weight = 1.0 - length(vec2(x, y)) / 5.66;
|
|
582
|
+
if (weight > 0.0) {
|
|
583
|
+
sum += texture2D(u_texture, uv + offset).rgb * weight;
|
|
584
|
+
total += weight;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return sum / total;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
void main() {
|
|
592
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
593
|
+
vec3 color = texture2D(u_texture, uv).rgb;
|
|
594
|
+
|
|
595
|
+
// Extract bright areas
|
|
596
|
+
float brightness = dot(color, vec3(0.2126, 0.7152, 0.0722));
|
|
597
|
+
vec3 bright = color * step(u_threshold, brightness);
|
|
598
|
+
|
|
599
|
+
// Blur bright areas
|
|
600
|
+
vec3 bloom = sampleBlur(uv, 3.0);
|
|
601
|
+
|
|
602
|
+
gl_FragColor = vec4(color + bloom * u_intensity, 1.0);
|
|
603
|
+
}`,
|
|
604
|
+
film_grain: `// Film grain post-processing — ${lang.toUpperCase()}
|
|
605
|
+
precision mediump float;
|
|
606
|
+
uniform sampler2D u_texture;
|
|
607
|
+
uniform vec2 u_resolution;
|
|
608
|
+
uniform float u_time;
|
|
609
|
+
uniform float u_intensity;
|
|
610
|
+
|
|
611
|
+
float rand(vec2 co) {
|
|
612
|
+
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
void main() {
|
|
616
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
617
|
+
vec3 color = texture2D(u_texture, uv).rgb;
|
|
618
|
+
|
|
619
|
+
float grain = rand(uv + fract(u_time)) * 2.0 - 1.0;
|
|
620
|
+
grain *= u_intensity;
|
|
621
|
+
|
|
622
|
+
// Vignette
|
|
623
|
+
float vignette = 1.0 - length(uv - 0.5) * 1.2;
|
|
624
|
+
vignette = smoothstep(0.0, 1.0, vignette);
|
|
625
|
+
|
|
626
|
+
color += grain;
|
|
627
|
+
color *= vignette;
|
|
628
|
+
|
|
629
|
+
gl_FragColor = vec4(color, 1.0);
|
|
630
|
+
}`,
|
|
631
|
+
dissolve: `// Dissolve transition shader — ${lang.toUpperCase()}
|
|
632
|
+
precision mediump float;
|
|
633
|
+
uniform sampler2D u_texture;
|
|
634
|
+
uniform vec2 u_resolution;
|
|
635
|
+
uniform float u_progress;
|
|
636
|
+
|
|
637
|
+
float hash(vec2 p) {
|
|
638
|
+
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
void main() {
|
|
642
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
643
|
+
vec3 color = texture2D(u_texture, uv).rgb;
|
|
644
|
+
|
|
645
|
+
float noise = hash(uv * 100.0);
|
|
646
|
+
float edge = smoothstep(u_progress - 0.05, u_progress + 0.05, noise);
|
|
647
|
+
|
|
648
|
+
// Glowing edge
|
|
649
|
+
float edgeGlow = 1.0 - abs(noise - u_progress) * 20.0;
|
|
650
|
+
edgeGlow = max(0.0, edgeGlow);
|
|
651
|
+
vec3 glowColor = vec3(1.0, 0.5, 0.1) * edgeGlow * 3.0;
|
|
652
|
+
|
|
653
|
+
color = mix(vec3(0), color, edge) + glowColor;
|
|
654
|
+
float alpha = step(0.01, edge + edgeGlow);
|
|
655
|
+
|
|
656
|
+
gl_FragColor = vec4(color, alpha);
|
|
657
|
+
}`,
|
|
658
|
+
};
|
|
659
|
+
const code = shaders[effect];
|
|
660
|
+
if (!code)
|
|
661
|
+
return `Unknown shader "${effect}". Available: ${Object.keys(shaders).join(', ')}`;
|
|
662
|
+
return `\`\`\`glsl\n${code}\n\`\`\``;
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
// ── Color Palette Generator ───────────────────────────────────────
|
|
666
|
+
registerTool({
|
|
667
|
+
name: 'color_palette',
|
|
668
|
+
description: 'Generate color palettes from descriptions, images, or color theory rules. Returns hex colors with names.',
|
|
669
|
+
parameters: {
|
|
670
|
+
source: { type: 'string', description: 'Description ("warm sunset"), hex color ("#6B5B95"), or image path', required: true },
|
|
671
|
+
count: { type: 'number', description: 'Number of colors (default: 5)' },
|
|
672
|
+
harmony: { type: 'string', description: 'Color harmony: complementary, analogous, triadic, split_complementary, tetradic, monochromatic' },
|
|
673
|
+
},
|
|
674
|
+
tier: 'free',
|
|
675
|
+
async execute(args) {
|
|
676
|
+
const source = String(args.source);
|
|
677
|
+
const count = Number(args.count) || 5;
|
|
678
|
+
// If source is a hex color, generate harmonies
|
|
679
|
+
if (source.startsWith('#') && source.length >= 7) {
|
|
680
|
+
const r = parseInt(source.slice(1, 3), 16);
|
|
681
|
+
const g = parseInt(source.slice(3, 5), 16);
|
|
682
|
+
const b = parseInt(source.slice(5, 7), 16);
|
|
683
|
+
// Convert to HSL
|
|
684
|
+
const rf = r / 255, gf = g / 255, bf = b / 255;
|
|
685
|
+
const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
|
|
686
|
+
let h = 0, s = 0;
|
|
687
|
+
const l = (max + min) / 2;
|
|
688
|
+
if (max !== min) {
|
|
689
|
+
const d = max - min;
|
|
690
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
691
|
+
if (max === rf)
|
|
692
|
+
h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
|
|
693
|
+
else if (max === gf)
|
|
694
|
+
h = ((bf - rf) / d + 2) / 6;
|
|
695
|
+
else
|
|
696
|
+
h = ((rf - gf) / d + 4) / 6;
|
|
697
|
+
}
|
|
698
|
+
const harmony = String(args.harmony || 'analogous');
|
|
699
|
+
const offsets = [];
|
|
700
|
+
switch (harmony) {
|
|
701
|
+
case 'complementary':
|
|
702
|
+
offsets.push(0, 0.5);
|
|
703
|
+
break;
|
|
704
|
+
case 'triadic':
|
|
705
|
+
offsets.push(0, 1 / 3, 2 / 3);
|
|
706
|
+
break;
|
|
707
|
+
case 'split_complementary':
|
|
708
|
+
offsets.push(0, 5 / 12, 7 / 12);
|
|
709
|
+
break;
|
|
710
|
+
case 'tetradic':
|
|
711
|
+
offsets.push(0, 0.25, 0.5, 0.75);
|
|
712
|
+
break;
|
|
713
|
+
case 'monochromatic':
|
|
714
|
+
for (let i = 0; i < count; i++)
|
|
715
|
+
offsets.push(0);
|
|
716
|
+
break;
|
|
717
|
+
default:
|
|
718
|
+
for (let i = 0; i < count; i++)
|
|
719
|
+
offsets.push(i * 30 / 360);
|
|
720
|
+
break; // analogous
|
|
721
|
+
}
|
|
722
|
+
// Pad to requested count
|
|
723
|
+
while (offsets.length < count)
|
|
724
|
+
offsets.push(offsets[offsets.length - 1] + 0.1);
|
|
725
|
+
const hslToHex = (h, s, l) => {
|
|
726
|
+
h = ((h % 1) + 1) % 1;
|
|
727
|
+
const hue2rgb = (p, q, t) => {
|
|
728
|
+
if (t < 0)
|
|
729
|
+
t += 1;
|
|
730
|
+
if (t > 1)
|
|
731
|
+
t -= 1;
|
|
732
|
+
if (t < 1 / 6)
|
|
733
|
+
return p + (q - p) * 6 * t;
|
|
734
|
+
if (t < 1 / 2)
|
|
735
|
+
return q;
|
|
736
|
+
if (t < 2 / 3)
|
|
737
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
738
|
+
return p;
|
|
739
|
+
};
|
|
740
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
741
|
+
const p = 2 * l - q;
|
|
742
|
+
const ri = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
|
|
743
|
+
const gi = Math.round(hue2rgb(p, q, h) * 255);
|
|
744
|
+
const bi = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
|
|
745
|
+
return `#${ri.toString(16).padStart(2, '0')}${gi.toString(16).padStart(2, '0')}${bi.toString(16).padStart(2, '0')}`;
|
|
746
|
+
};
|
|
747
|
+
const colors = offsets.slice(0, count).map((offset, i) => {
|
|
748
|
+
const newH = h + offset;
|
|
749
|
+
const newL = harmony === 'monochromatic' ? l - 0.15 + (i * 0.3 / count) : l;
|
|
750
|
+
return hslToHex(newH, s, Math.max(0.1, Math.min(0.9, newL)));
|
|
751
|
+
});
|
|
752
|
+
return `## ${harmony.replace('_', ' ')} palette from ${source}\n\n${colors.map((c, i) => `${i + 1}. \`${c}\` ${'█'.repeat(8)}`).join('\n')}`;
|
|
753
|
+
}
|
|
754
|
+
// Named palette descriptions
|
|
755
|
+
const palettes = {
|
|
756
|
+
'warm sunset': ['#FF6B35', '#F7C59F', '#EFEFD0', '#004E7C', '#1A2238'],
|
|
757
|
+
'ocean': ['#05445E', '#189AB4', '#75E6DA', '#D4F1F9', '#E8F8F5'],
|
|
758
|
+
'forest': ['#2D5016', '#4A7C2E', '#8FBC54', '#C5E17A', '#F0F4E4'],
|
|
759
|
+
'cyberpunk': ['#FF00FF', '#00FFFF', '#FF006E', '#8338EC', '#3A0CA3'],
|
|
760
|
+
'minimal': ['#2B2D42', '#8D99AE', '#EDF2F4', '#EF233C', '#D90429'],
|
|
761
|
+
'earth': ['#5C4033', '#8B6914', '#DAA520', '#F0E68C', '#FAF0E6'],
|
|
762
|
+
'pastel': ['#FFB3BA', '#FFDFBA', '#FFFFBA', '#BAFFC9', '#BAE1FF'],
|
|
763
|
+
'midnight': ['#0D1B2A', '#1B2838', '#324A5F', '#6B8F71', '#AAC0AA'],
|
|
764
|
+
'rubin': ['#6B5B95', '#8E7CC3', '#B8A9C9', '#F5F0EB', '#2A2A2A'],
|
|
765
|
+
};
|
|
766
|
+
const key = Object.keys(palettes).find(k => source.toLowerCase().includes(k));
|
|
767
|
+
if (key) {
|
|
768
|
+
const colors = palettes[key].slice(0, count);
|
|
769
|
+
return `## "${key}" palette\n\n${colors.map((c, i) => `${i + 1}. \`${c}\` ${'█'.repeat(8)}`).join('\n')}`;
|
|
770
|
+
}
|
|
771
|
+
// Default: generate based on hash of description
|
|
772
|
+
const hash = Array.from(source).reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
|
773
|
+
const baseHue = (hash % 360) / 360;
|
|
774
|
+
const colors = Array.from({ length: count }, (_, i) => {
|
|
775
|
+
const h = baseHue + i * (1 / count);
|
|
776
|
+
const s = 0.5 + (i % 2) * 0.2;
|
|
777
|
+
const l = 0.3 + i * (0.4 / count);
|
|
778
|
+
const hslToHex2 = (h, s, l) => {
|
|
779
|
+
h = ((h % 1) + 1) % 1;
|
|
780
|
+
const a = s * Math.min(l, 1 - l);
|
|
781
|
+
const f = (n) => {
|
|
782
|
+
const k = (n + h * 12) % 12;
|
|
783
|
+
return Math.round((l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)) * 255);
|
|
784
|
+
};
|
|
785
|
+
return `#${f(0).toString(16).padStart(2, '0')}${f(8).toString(16).padStart(2, '0')}${f(4).toString(16).padStart(2, '0')}`;
|
|
786
|
+
};
|
|
787
|
+
return hslToHex2(h, s, l);
|
|
788
|
+
});
|
|
789
|
+
return `## Palette for "${source}"\n\n${colors.map((c, i) => `${i + 1}. \`${c}\` ${'█'.repeat(8)}`).join('\n')}`;
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
// ── Audio Visualization ───────────────────────────────────────────
|
|
793
|
+
registerTool({
|
|
794
|
+
name: 'audio_visualize',
|
|
795
|
+
description: 'Generate audio visualization videos from audio files using FFmpeg. Creates waveform, spectrum, or vectorscope visualizations.',
|
|
796
|
+
parameters: {
|
|
797
|
+
input: { type: 'string', description: 'Input audio file path', required: true },
|
|
798
|
+
output: { type: 'string', description: 'Output video file path', required: true },
|
|
799
|
+
style: { type: 'string', description: 'Visualization style: waveform, spectrum, vectorscope, showcqt (default: showcqt)' },
|
|
800
|
+
size: { type: 'string', description: 'Video size (default: 1920x1080)' },
|
|
801
|
+
duration: { type: 'string', description: 'Duration in seconds (default: full audio)' },
|
|
802
|
+
},
|
|
803
|
+
tier: 'pro',
|
|
804
|
+
timeout: 300_000,
|
|
805
|
+
async execute(args) {
|
|
806
|
+
const input = String(args.input);
|
|
807
|
+
const output = String(args.output);
|
|
808
|
+
const style = String(args.style || 'showcqt');
|
|
809
|
+
const size = String(args.size || '1920x1080');
|
|
810
|
+
const filters = {
|
|
811
|
+
waveform: `showwaves=s=${size}:mode=cline:colors=0x6B5B95|0x8E7CC3`,
|
|
812
|
+
spectrum: `showspectrum=s=${size}:mode=combined:color=intensity:scale=cbrt`,
|
|
813
|
+
vectorscope: `avectorscope=s=${size}:mode=lissajous_xy:zoom=5`,
|
|
814
|
+
showcqt: `showcqt=s=${size}:sono_h=0:bar_h=${size.split('x')[1]}:sono_g=4:bar_g=4`,
|
|
815
|
+
};
|
|
816
|
+
const filter = filters[style];
|
|
817
|
+
if (!filter)
|
|
818
|
+
return `Unknown style "${style}". Available: ${Object.keys(filters).join(', ')}`;
|
|
819
|
+
const ffmpegArgs = ['-y', '-i', input, '-filter_complex', filter, '-pix_fmt', 'yuv420p', output];
|
|
820
|
+
if (args.duration)
|
|
821
|
+
ffmpegArgs.splice(3, 0, '-t', String(args.duration));
|
|
822
|
+
try {
|
|
823
|
+
return await shell('ffmpeg', ffmpegArgs, 300_000);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
return `Audio visualization error: ${err instanceof Error ? err.message : String(err)}`;
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
//# sourceMappingURL=vfx.js.map
|