@pixelvibing/cli 0.57.1 → 0.59.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/dist/pixel-vibing-cli.js +144 -38
- package/package.json +1 -1
- package/skills/pixelvibing-sound/SKILL.md +31 -4
package/dist/pixel-vibing-cli.js
CHANGED
|
@@ -418,6 +418,66 @@ function apiAddress(environment = process.env) {
|
|
|
418
418
|
if (!PLAIN_PATH.test(url.pathname)) throw new ApiAddressError("PIXELVIBING_API_URL's path may hold only letters, digits, and . _ ~ - /");
|
|
419
419
|
return url.href.replace(/\/+$/, "");
|
|
420
420
|
}
|
|
421
|
+
const SFX_SECONDS = { min: 0.5, max: 22 };
|
|
422
|
+
const MAX_SPEECH_CHARACTERS = 500;
|
|
423
|
+
const MAX_AUDIO_DESCRIPTION = 600;
|
|
424
|
+
const VOICES = [
|
|
425
|
+
"Aria",
|
|
426
|
+
"Roger",
|
|
427
|
+
"Sarah",
|
|
428
|
+
"Laura",
|
|
429
|
+
"Charlie",
|
|
430
|
+
"George",
|
|
431
|
+
"Callum",
|
|
432
|
+
"River",
|
|
433
|
+
"Liam",
|
|
434
|
+
"Charlotte",
|
|
435
|
+
"Alice",
|
|
436
|
+
"Matilda",
|
|
437
|
+
"Will",
|
|
438
|
+
"Jessica",
|
|
439
|
+
"Eric",
|
|
440
|
+
"Chris",
|
|
441
|
+
"Brian",
|
|
442
|
+
"Daniel",
|
|
443
|
+
"Lily",
|
|
444
|
+
"Bill"
|
|
445
|
+
];
|
|
446
|
+
const MUSIC_SECONDS = { min: 3, max: 600, unasked: 60 };
|
|
447
|
+
const SECTION_SECONDS = { min: 3, max: 120 };
|
|
448
|
+
const toMs = (seconds) => Math.round(seconds * 1e3) / 1e3;
|
|
449
|
+
function musicSeconds(body) {
|
|
450
|
+
const sections = body.plan?.sections;
|
|
451
|
+
if (Array.isArray(sections))
|
|
452
|
+
return sections.reduce((sum, each) => sum + (typeof each?.seconds === "number" && Number.isFinite(each.seconds) ? toMs(each.seconds) : 0), 0);
|
|
453
|
+
return typeof body.seconds === "number" && Number.isFinite(body.seconds) ? toMs(body.seconds) : MUSIC_SECONDS.unasked;
|
|
454
|
+
}
|
|
455
|
+
function musicMinutes(body) {
|
|
456
|
+
return Math.max(1, Math.ceil(musicSeconds(body) / 60));
|
|
457
|
+
}
|
|
458
|
+
function musicProblem(body) {
|
|
459
|
+
const described = typeof body.description === "string" && body.description.trim() !== "";
|
|
460
|
+
if (body.plan === void 0) {
|
|
461
|
+
if (!described) return "describe the music, or give a composition plan";
|
|
462
|
+
if (body.seconds !== void 0 && !(typeof body.seconds === "number" && body.seconds >= MUSIC_SECONDS.min && body.seconds <= MUSIC_SECONDS.max))
|
|
463
|
+
return `music runs ${MUSIC_SECONDS.min} to ${MUSIC_SECONDS.max} seconds`;
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
if (body.description !== void 0) return "music takes a description or a composition plan, not both";
|
|
467
|
+
if (body.seconds !== void 0 || body.instrumental !== void 0)
|
|
468
|
+
return "a composition plan's length is its sections', and its lyrics say whether it is sung";
|
|
469
|
+
const sections = body.plan.sections;
|
|
470
|
+
if (!Array.isArray(sections) || sections.length === 0) return "a composition plan has at least one section";
|
|
471
|
+
for (const section2 of sections) {
|
|
472
|
+
const { name, seconds: seconds2 } = typeof section2 === "object" && section2 !== null ? section2 : {};
|
|
473
|
+
if (typeof name !== "string" || name.trim() === "") return "each section has a name";
|
|
474
|
+
if (!(typeof seconds2 === "number" && seconds2 >= SECTION_SECONDS.min && seconds2 <= SECTION_SECONDS.max))
|
|
475
|
+
return `a section runs ${SECTION_SECONDS.min} to ${SECTION_SECONDS.max} seconds, not ${String(seconds2)}`;
|
|
476
|
+
}
|
|
477
|
+
const seconds = musicSeconds(body);
|
|
478
|
+
if (seconds > MUSIC_SECONDS.max) return `a composition plan runs at most ${MUSIC_SECONDS.max} seconds in all, not ${seconds}`;
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
421
481
|
class GuideConflict extends Error {
|
|
422
482
|
constructor(current) {
|
|
423
483
|
super(`the style guide is now at version ${current}`);
|
|
@@ -433,6 +493,12 @@ class TooFewCredits extends Error {
|
|
|
433
493
|
this.credits = credits;
|
|
434
494
|
}
|
|
435
495
|
}
|
|
496
|
+
function costIn(prices, kind, body) {
|
|
497
|
+
const price = prices.kinds[kind];
|
|
498
|
+
if (price === void 0) return null;
|
|
499
|
+
if ("credits" in price) return price.credits;
|
|
500
|
+
return price.perMinute !== void 0 && body !== void 0 ? price.perMinute * musicMinutes(body) : price.upTo;
|
|
501
|
+
}
|
|
436
502
|
const id = (value) => encodeURIComponent(value);
|
|
437
503
|
function createWebApi(fetcher = (input, init2) => fetch(input, init2), base = API_BASE) {
|
|
438
504
|
const call = createCaller(fetcher, base);
|
|
@@ -447,9 +513,10 @@ function createWebApi(fetcher = (input, init2) => fetch(input, init2), base = AP
|
|
|
447
513
|
throw error;
|
|
448
514
|
}
|
|
449
515
|
};
|
|
516
|
+
const address = new URL(base, globalThis.location?.origin ?? "http://localhost").href.replace(/\/+$/, "");
|
|
450
517
|
return {
|
|
451
518
|
/** The API's own address, whole, for the MCP server's next to it (specs/web-app/ R2.14). */
|
|
452
|
-
address
|
|
519
|
+
address,
|
|
453
520
|
/** The project's own token, to the signed-in person alone (specs/accounts-and-sign-in/ R3.12). */
|
|
454
521
|
projectToken: (projectId) => json2(`/projects/${id(projectId)}/token`),
|
|
455
522
|
projects: (before = null) => json2(`/projects${before === null ? "" : `?before=${id(before)}`}`),
|
|
@@ -524,6 +591,8 @@ function createWebApi(fetcher = (input, init2) => fetch(input, init2), base = AP
|
|
|
524
591
|
redo: (jobId) => paying2(async () => (await json2(`/jobs/${id(jobId)}/redo`, { method: "POST" })).id),
|
|
525
592
|
/** Cancel a job no worker has taken, answering it and the credits that came back; 409 once one has (specs/generation-jobs/ R5). */
|
|
526
593
|
cancelJob: (jobId) => json2(`/jobs/${id(jobId)}/cancel`, { method: "POST" }),
|
|
594
|
+
/** The address of a project's job changes, as server-sent events (specs/live-generations/ R1.3). */
|
|
595
|
+
jobEventsUrl: (projectId) => `${address}/jobs/events?project=${id(projectId)}`,
|
|
527
596
|
jobs: (projectId, before = null, kinds = null) => json2(
|
|
528
597
|
`/jobs?project=${id(projectId)}${before === null ? "" : `&before=${id(before)}`}${kinds === null ? "" : `&kinds=${kinds.map(id).join(",")}`}`
|
|
529
598
|
),
|
|
@@ -4887,12 +4956,14 @@ function shareWords(credits, limit) {
|
|
|
4887
4956
|
function leftWords(usage) {
|
|
4888
4957
|
return `${100 - usedPercent(usage)}% of your limit left`;
|
|
4889
4958
|
}
|
|
4890
|
-
function costShareWords(prices, kind, usage) {
|
|
4959
|
+
function costShareWords(prices, kind, usage, body) {
|
|
4891
4960
|
const price = prices.kinds[kind];
|
|
4892
|
-
|
|
4893
|
-
|
|
4961
|
+
const most = costIn(prices, kind, body);
|
|
4962
|
+
if (price === void 0 || most === null) return null;
|
|
4894
4963
|
const share = `${shareWords(most, usage.limit)} of your limit`;
|
|
4895
|
-
|
|
4964
|
+
if ("credits" in price) return share;
|
|
4965
|
+
if (price.perMinute !== void 0 && body !== void 0) return `${share}, by the minute of its length`;
|
|
4966
|
+
return `up to ${share}, charged for what it uses once done`;
|
|
4896
4967
|
}
|
|
4897
4968
|
const LARGEST_FOOTPRINT = {
|
|
4898
4969
|
cols: Math.min(Math.floor(WINDOW_PX.width / SCALES.at(-1)), Math.floor(MAX_ASSEMBLED_PX / TILE_SIZE)),
|
|
@@ -4914,31 +4985,6 @@ function drawCanvas(footprint) {
|
|
|
4914
4985
|
if (pxPerTile === void 0) return null;
|
|
4915
4986
|
return { width: footprint.cols * pxPerTile, height: footprint.rows * pxPerTile, pxPerTile };
|
|
4916
4987
|
}
|
|
4917
|
-
const SFX_SECONDS = { min: 0.5, max: 22 };
|
|
4918
|
-
const MAX_SPEECH_CHARACTERS = 500;
|
|
4919
|
-
const MAX_AUDIO_DESCRIPTION = 600;
|
|
4920
|
-
const VOICES = [
|
|
4921
|
-
"Aria",
|
|
4922
|
-
"Roger",
|
|
4923
|
-
"Sarah",
|
|
4924
|
-
"Laura",
|
|
4925
|
-
"Charlie",
|
|
4926
|
-
"George",
|
|
4927
|
-
"Callum",
|
|
4928
|
-
"River",
|
|
4929
|
-
"Liam",
|
|
4930
|
-
"Charlotte",
|
|
4931
|
-
"Alice",
|
|
4932
|
-
"Matilda",
|
|
4933
|
-
"Will",
|
|
4934
|
-
"Jessica",
|
|
4935
|
-
"Eric",
|
|
4936
|
-
"Chris",
|
|
4937
|
-
"Brian",
|
|
4938
|
-
"Daniel",
|
|
4939
|
-
"Lily",
|
|
4940
|
-
"Bill"
|
|
4941
|
-
];
|
|
4942
4988
|
const PATCH_CORNERS = [
|
|
4943
4989
|
[1, 0, 0, 0, 0],
|
|
4944
4990
|
[1, 0, 0, 1, 0],
|
|
@@ -5774,8 +5820,10 @@ const OWN_OPTIONS = {
|
|
|
5774
5820
|
terrain: ["terrain-animation"],
|
|
5775
5821
|
text: ["speech"],
|
|
5776
5822
|
voice: ["speech"],
|
|
5777
|
-
seconds: ["sfx"],
|
|
5823
|
+
seconds: ["sfx", "music"],
|
|
5778
5824
|
loop: ["sfx"],
|
|
5825
|
+
instrumental: ["music"],
|
|
5826
|
+
plan: ["music"],
|
|
5779
5827
|
out: ["sfx", "speech", "music", "vfx"],
|
|
5780
5828
|
preset: ["vfx"],
|
|
5781
5829
|
part: ["vfx"],
|
|
@@ -5816,8 +5864,18 @@ const MORE_OPTIONS = {
|
|
|
5816
5864
|
terrain: { type: "string", value: "<asset id|name>", description: "the terrain set an animated terrain set moves" },
|
|
5817
5865
|
text: { type: "string", value: "<text>", description: "what speech says" },
|
|
5818
5866
|
voice: { type: "string", value: "<voice>", description: `the voice speech is said in: ${VOICES.join(", ")}` },
|
|
5819
|
-
seconds: {
|
|
5867
|
+
seconds: {
|
|
5868
|
+
type: "string",
|
|
5869
|
+
value: "<seconds>",
|
|
5870
|
+
description: `a sound effect's length, ${SFX_SECONDS.min} to ${SFX_SECONDS.max}; music's, ${MUSIC_SECONDS.min} to ${MUSIC_SECONDS.max}, ${MUSIC_SECONDS.unasked} unless given`
|
|
5871
|
+
},
|
|
5820
5872
|
loop: { type: "boolean", description: "a sound effect that loops seamlessly" },
|
|
5873
|
+
instrumental: { type: "boolean", description: "music with no voice" },
|
|
5874
|
+
plan: {
|
|
5875
|
+
type: "string",
|
|
5876
|
+
value: "<json>",
|
|
5877
|
+
description: `music section by section, instead of --instruction: a JSON file of { styles, avoid, sections: [{ name, seconds (${SECTION_SECONDS.min}-${SECTION_SECONDS.max}), styles, avoid, lyrics }], flexibleLengths }, at most ${MUSIC_SECONDS.max} seconds in all`
|
|
5878
|
+
},
|
|
5821
5879
|
out: {
|
|
5822
5880
|
type: "string",
|
|
5823
5881
|
value: "<path>",
|
|
@@ -5992,9 +6050,9 @@ async function moreRequest(context, kind, args, instruction, reference) {
|
|
|
5992
6050
|
if (!VOICES.includes(voice)) throw new CliError(`--voice is one of ${VOICES.join(", ")}`);
|
|
5993
6051
|
return { kind, body: { text: text2, voice }, words: `${words} in ${voice}: ${text2}` };
|
|
5994
6052
|
}
|
|
6053
|
+
if (kind === "music") return musicRequestOf(context, args, instruction);
|
|
5995
6054
|
if (instruction === "") throw new CliError(`describe ${words} with --instruction`);
|
|
5996
6055
|
if (instruction.length > MAX_AUDIO_DESCRIPTION) throw new CliError(`a description is at most ${MAX_AUDIO_DESCRIPTION} characters`);
|
|
5997
|
-
if (kind === "music") return { kind, body: { description: instruction }, words: `${words}: ${instruction}` };
|
|
5998
6056
|
const seconds = stringOf(args, "seconds");
|
|
5999
6057
|
const length = seconds === void 0 ? void 0 : Number(seconds);
|
|
6000
6058
|
if (length !== void 0 && !(length >= SFX_SECONDS.min && length <= SFX_SECONDS.max))
|
|
@@ -6005,6 +6063,54 @@ async function moreRequest(context, kind, args, instruction, reference) {
|
|
|
6005
6063
|
words: `${words}${length === void 0 ? "" : ` of ${length} s`}${flagOf(args, "loop") ? ", looping" : ""}: ${instruction}`
|
|
6006
6064
|
};
|
|
6007
6065
|
}
|
|
6066
|
+
const MAX_PLAN_BYTES = 128 * 1024;
|
|
6067
|
+
async function planOf(context, path) {
|
|
6068
|
+
const file = resolve(context.io.cwd, path);
|
|
6069
|
+
let text2;
|
|
6070
|
+
try {
|
|
6071
|
+
const found = await stat(file);
|
|
6072
|
+
if (!found.isFile()) throw new CliError(`the plan ${path} is not a file`);
|
|
6073
|
+
if (found.size > MAX_PLAN_BYTES) throw new CliError(`the plan ${path} is larger than ${MAX_PLAN_BYTES} bytes`);
|
|
6074
|
+
text2 = await readFile(file, "utf8");
|
|
6075
|
+
} catch (error) {
|
|
6076
|
+
if (error instanceof CliError) throw error;
|
|
6077
|
+
throw new CliError(`the plan ${path} cannot be read`);
|
|
6078
|
+
}
|
|
6079
|
+
let plan;
|
|
6080
|
+
try {
|
|
6081
|
+
plan = JSON.parse(text2);
|
|
6082
|
+
} catch {
|
|
6083
|
+
throw new CliError(`the plan ${path} is not JSON`);
|
|
6084
|
+
}
|
|
6085
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan))
|
|
6086
|
+
throw new CliError(`the plan ${path} is not a plan: { "styles": [...], "sections": [{ "name": "intro", "seconds": 20 }] }`);
|
|
6087
|
+
return plan;
|
|
6088
|
+
}
|
|
6089
|
+
async function musicRequestOf(context, args, instruction) {
|
|
6090
|
+
const path = stringOf(args, "plan");
|
|
6091
|
+
const given2 = stringOf(args, "seconds");
|
|
6092
|
+
if (path !== void 0) {
|
|
6093
|
+
if (instruction !== "") throw new CliError("music takes --instruction or --plan, not both");
|
|
6094
|
+
if (given2 !== void 0 || flagOf(args, "instrumental"))
|
|
6095
|
+
throw new CliError("a plan's length is its sections', and its lyrics say whether it is sung: --seconds and --instrumental are not a plan's");
|
|
6096
|
+
const body = { plan: await planOf(context, path) };
|
|
6097
|
+
const problem = musicProblem(body);
|
|
6098
|
+
if (problem !== null) throw new CliError(`the plan ${path}: ${problem}`);
|
|
6099
|
+
const sections = body.plan.sections.map((section2) => String(section2.name)).join(", ");
|
|
6100
|
+
return { kind: "music", body, words: `music of ${musicMinutes(body)} min from ${path}: ${sections}` };
|
|
6101
|
+
}
|
|
6102
|
+
if (instruction === "") throw new CliError("describe the music with --instruction, or give its sections with --plan");
|
|
6103
|
+
if (instruction.length > MAX_AUDIO_DESCRIPTION) throw new CliError(`a description is at most ${MAX_AUDIO_DESCRIPTION} characters`);
|
|
6104
|
+
const seconds = given2 === void 0 ? void 0 : Number(given2);
|
|
6105
|
+
if (seconds !== void 0 && !(seconds >= MUSIC_SECONDS.min && seconds <= MUSIC_SECONDS.max))
|
|
6106
|
+
throw new CliError(`--seconds is ${MUSIC_SECONDS.min} to ${MUSIC_SECONDS.max} for music`);
|
|
6107
|
+
const instrumental = flagOf(args, "instrumental");
|
|
6108
|
+
return {
|
|
6109
|
+
kind: "music",
|
|
6110
|
+
body: { description: instruction, ...seconds === void 0 ? {} : { seconds }, ...instrumental ? { instrumental: true } : {} },
|
|
6111
|
+
words: `music of ${seconds ?? MUSIC_SECONDS.unasked} s${instrumental ? ", instrumental" : ""}: ${instruction}`
|
|
6112
|
+
};
|
|
6113
|
+
}
|
|
6008
6114
|
async function withPatch(platform, project, request) {
|
|
6009
6115
|
const { terrain, ...body } = request.body;
|
|
6010
6116
|
const asset = await platform.web.asset(project, terrain);
|
|
@@ -6300,9 +6406,9 @@ async function shapedRequest(context, asked, args) {
|
|
|
6300
6406
|
if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a pixel-art concept art's");
|
|
6301
6407
|
return { kind, body, ...references.length > 0 ? { images: { references } } : {}, words };
|
|
6302
6408
|
}
|
|
6303
|
-
async function confirmed(context, platform, what, prices, kind, yes) {
|
|
6409
|
+
async function confirmed(context, platform, what, prices, kind, yes, body) {
|
|
6304
6410
|
const usage = usageOf$1(await platform.web.billing());
|
|
6305
|
-
const share = costShareWords(prices, kind, usage);
|
|
6411
|
+
const share = costShareWords(prices, kind, usage, body);
|
|
6306
6412
|
const priced = share === null ? "using what the platform will say of your limit" : `using ${share}`;
|
|
6307
6413
|
context.io.warn(`Making ${what}, ${priced}; ${leftWords(usage)}.
|
|
6308
6414
|
`);
|
|
@@ -6328,7 +6434,7 @@ async function followed(context, platform, jobId) {
|
|
|
6328
6434
|
const generate = {
|
|
6329
6435
|
name: "generate",
|
|
6330
6436
|
summary: "Generate an asset, a change of a picture, terrain, a sound or a visual effect on the platform, then pull it, keep it or write it",
|
|
6331
|
-
usage: "<object|creature|concept-art|icons|ui|variant|edit|animate|pose|rotate|state|terrain|terrain-animation|sfx|speech|music|vfx> [--instruction <text>] [--source <image id>] [--state <state>] [--into <sprite> --name <name>] [--upper <ground> --lower <ground>] [--terrain <asset>] [--text <text> --voice <voice>] [--preset <preset> | --part <part>…] [--subject <image id>] [--out <path>] [--size <c>x<r>|<w>x<h>|<px>] [--transparent] [--pixel-art] [--tiles <c>x<r>] [--tag <tag>]… [--mask <png>] [--names <list>] [--purpose skill|item] [--sketch <json>] [--reference <png>]… [--no-enrich] [--yes]",
|
|
6437
|
+
usage: "<object|creature|concept-art|icons|ui|variant|edit|animate|pose|rotate|state|terrain|terrain-animation|sfx|speech|music|vfx> [--instruction <text>] [--source <image id>] [--state <state>] [--into <sprite> --name <name>] [--upper <ground> --lower <ground>] [--terrain <asset>] [--text <text> --voice <voice>] [--seconds <s>] [--loop] [--instrumental] [--plan <json>] [--preset <preset> | --part <part>…] [--subject <image id>] [--out <path>] [--size <c>x<r>|<w>x<h>|<px>] [--transparent] [--pixel-art] [--tiles <c>x<r>] [--tag <tag>]… [--mask <png>] [--names <list>] [--purpose skill|item] [--sketch <json>] [--reference <png>]… [--no-enrich] [--yes]",
|
|
6332
6438
|
options: {
|
|
6333
6439
|
instruction: {
|
|
6334
6440
|
type: "string",
|
|
@@ -6384,7 +6490,7 @@ const generate = {
|
|
|
6384
6490
|
const platform = await signedIn(context);
|
|
6385
6491
|
const request = asked.kind === "terrain-animation" ? await withPatch(platform, project, asked) : asked;
|
|
6386
6492
|
const prices = await platform.web.prices();
|
|
6387
|
-
const uses = await confirmed(context, platform, request.words, prices, request.kind, flagOf(args, "yes"));
|
|
6493
|
+
const uses = await confirmed(context, platform, request.words, prices, request.kind, flagOf(args, "yes"), request.body);
|
|
6388
6494
|
const jobId = await paying(platform, () => startGeneration(platform.web, project, request));
|
|
6389
6495
|
context.io.warn(`Started the job ${jobId}.
|
|
6390
6496
|
`);
|
|
@@ -7374,7 +7480,7 @@ function terminalOf(streams) {
|
|
|
7374
7480
|
step: (title, at = "step") => drawStep(streams.stderr, title, at)
|
|
7375
7481
|
};
|
|
7376
7482
|
}
|
|
7377
|
-
const version = "0.
|
|
7483
|
+
const version = "0.59.0";
|
|
7378
7484
|
const manifest = {
|
|
7379
7485
|
version
|
|
7380
7486
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelvibing/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"description": "PixelVibing: pixel art game assets from the command line, and the Skills that teach coding assistants to make them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://pixelvibing.com",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pixelvibing-sound
|
|
3
|
-
description: Generate sounds for this game on PixelVibing — a sound effect such as a coin picked up or a door creaking, a line of speech in a voice, or a piece of music — with pixel-vibing-cli or the PixelVibing MCP server, written as MP3 files. Use when the game needs audio.
|
|
3
|
+
description: Generate sounds for this game on PixelVibing — a sound effect such as a coin picked up or a door creaking, a line of speech in a voice, or a piece of music up to ten minutes long, from a description or section by section — with pixel-vibing-cli or the PixelVibing MCP server, written as MP3 files. Use when the game needs audio.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Sound
|
|
@@ -14,7 +14,23 @@ Load the `pixelvibing` Skill first: who is signed in, the limit.
|
|
|
14
14
|
```bash
|
|
15
15
|
pixel-vibing-cli generate sfx --instruction "<what it sounds like>" [--seconds <s>] [--loop] [--out <file>] --yes --json
|
|
16
16
|
pixel-vibing-cli generate speech --text "<what is said>" --voice <voice> [--out <file>] --yes --json
|
|
17
|
-
pixel-vibing-cli generate music --instruction "<the piece>" [--out <file>] --yes --json
|
|
17
|
+
pixel-vibing-cli generate music --instruction "<the piece>" [--seconds <3-600>] [--instrumental] [--out <file>] --yes --json
|
|
18
|
+
pixel-vibing-cli generate music --plan <plan.json> [--out <file>] --yes --json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A plan's file is the track section by section, in order:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"styles": ["chiptune", "heroic", "140 bpm"],
|
|
26
|
+
"avoid": ["vocals"],
|
|
27
|
+
"sections": [
|
|
28
|
+
{ "name": "intro", "seconds": 15, "styles": ["soft pads"] },
|
|
29
|
+
{ "name": "theme", "seconds": 90, "lyrics": ["Sail on", "the harbour waits"] },
|
|
30
|
+
{ "name": "outro", "seconds": 20, "avoid": ["drums"] }
|
|
31
|
+
],
|
|
32
|
+
"flexibleLengths": false
|
|
33
|
+
}
|
|
18
34
|
```
|
|
19
35
|
|
|
20
36
|
Each writes its sound as an MP3 into `pixelvibing/sounds/`, or to the file `--out` names, once
|
|
@@ -25,7 +41,8 @@ its job is done. `pixel-vibing-cli generate --help` lists the voices.
|
|
|
25
41
|
```json
|
|
26
42
|
{ "project": "<id>", "kind": "sfx", "description": "a coin picked up, bright and short", "seconds": 1, "max_percent": 30 }
|
|
27
43
|
{ "project": "<id>", "kind": "speech", "text": "Welcome aboard, traveller.", "voice": "<voice>", "max_percent": 30 }
|
|
28
|
-
{ "project": "<id>", "kind": "music", "description": "a calm harbour theme on strings", "max_percent": 30 }
|
|
44
|
+
{ "project": "<id>", "kind": "music", "description": "a calm harbour theme on strings", "seconds": 180, "instrumental": true, "max_percent": 30 }
|
|
45
|
+
{ "project": "<id>", "kind": "music", "plan": { "styles": ["chiptune"], "sections": [{ "name": "intro", "seconds": 15 }, { "name": "loop", "seconds": 90 }] }, "max_percent": 30 }
|
|
29
46
|
```
|
|
30
47
|
|
|
31
48
|
with the tool `generate_sound`. `get_job` follows the job and, once done, names its `sounds`;
|
|
@@ -39,7 +56,17 @@ with the tool `generate_sound`. `get_job` follows the job and, once done, names
|
|
|
39
56
|
- **Speech.** The words exactly as they are said, in a voice. Keep a line short; one line a
|
|
40
57
|
generation.
|
|
41
58
|
- **Music.** The mood, the instruments and the tempo: "a calm harbour theme on strings and a
|
|
42
|
-
soft accordion".
|
|
59
|
+
soft accordion". It runs 60 seconds unless `--seconds` (`seconds`) says otherwise, from 3 to
|
|
60
|
+
600. A game's theme, a level's loop or a menu track is asked at its full length: a short one
|
|
61
|
+
ends where it stops. `--instrumental` (`instrumental`) makes one with no voice.
|
|
62
|
+
- **Music section by section.** Give a plan instead of a description when the track has parts
|
|
63
|
+
that must land in order: an intro, a loop, a boss phase, an ending. Each section is 3 to 120
|
|
64
|
+
seconds, with its own styles, styles to avoid, and lyrics for its sung lines (none for an
|
|
65
|
+
instrumental part); the track is at most 600 seconds in all. `flexibleLengths`
|
|
66
|
+
(`flexible_lengths` on the MCP server) lets each section's length move while the track's stays.
|
|
67
|
+
- **What music uses.** Music uses the limit by the minute of its length, rounded up: a
|
|
68
|
+
four-minute theme uses four times a one-minute one. Say so before asking for a long track, and
|
|
69
|
+
keep `max_percent` to what the person agreed.
|
|
43
70
|
- **Original sound.** Never another game's music or theme, and never a real person's voice:
|
|
44
71
|
when the person names one, make original sound in its spirit and say so.
|
|
45
72
|
- **Where it goes.** The game loads the MP3 from where it was written; copy it into the game's
|