@pixelvibing/cli 0.58.0 → 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 +140 -37
- 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);
|
|
@@ -4890,12 +4956,14 @@ function shareWords(credits, limit) {
|
|
|
4890
4956
|
function leftWords(usage) {
|
|
4891
4957
|
return `${100 - usedPercent(usage)}% of your limit left`;
|
|
4892
4958
|
}
|
|
4893
|
-
function costShareWords(prices, kind, usage) {
|
|
4959
|
+
function costShareWords(prices, kind, usage, body) {
|
|
4894
4960
|
const price = prices.kinds[kind];
|
|
4895
|
-
|
|
4896
|
-
|
|
4961
|
+
const most = costIn(prices, kind, body);
|
|
4962
|
+
if (price === void 0 || most === null) return null;
|
|
4897
4963
|
const share = `${shareWords(most, usage.limit)} of your limit`;
|
|
4898
|
-
|
|
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`;
|
|
4899
4967
|
}
|
|
4900
4968
|
const LARGEST_FOOTPRINT = {
|
|
4901
4969
|
cols: Math.min(Math.floor(WINDOW_PX.width / SCALES.at(-1)), Math.floor(MAX_ASSEMBLED_PX / TILE_SIZE)),
|
|
@@ -4917,31 +4985,6 @@ function drawCanvas(footprint) {
|
|
|
4917
4985
|
if (pxPerTile === void 0) return null;
|
|
4918
4986
|
return { width: footprint.cols * pxPerTile, height: footprint.rows * pxPerTile, pxPerTile };
|
|
4919
4987
|
}
|
|
4920
|
-
const SFX_SECONDS = { min: 0.5, max: 22 };
|
|
4921
|
-
const MAX_SPEECH_CHARACTERS = 500;
|
|
4922
|
-
const MAX_AUDIO_DESCRIPTION = 600;
|
|
4923
|
-
const VOICES = [
|
|
4924
|
-
"Aria",
|
|
4925
|
-
"Roger",
|
|
4926
|
-
"Sarah",
|
|
4927
|
-
"Laura",
|
|
4928
|
-
"Charlie",
|
|
4929
|
-
"George",
|
|
4930
|
-
"Callum",
|
|
4931
|
-
"River",
|
|
4932
|
-
"Liam",
|
|
4933
|
-
"Charlotte",
|
|
4934
|
-
"Alice",
|
|
4935
|
-
"Matilda",
|
|
4936
|
-
"Will",
|
|
4937
|
-
"Jessica",
|
|
4938
|
-
"Eric",
|
|
4939
|
-
"Chris",
|
|
4940
|
-
"Brian",
|
|
4941
|
-
"Daniel",
|
|
4942
|
-
"Lily",
|
|
4943
|
-
"Bill"
|
|
4944
|
-
];
|
|
4945
4988
|
const PATCH_CORNERS = [
|
|
4946
4989
|
[1, 0, 0, 0, 0],
|
|
4947
4990
|
[1, 0, 0, 1, 0],
|
|
@@ -5777,8 +5820,10 @@ const OWN_OPTIONS = {
|
|
|
5777
5820
|
terrain: ["terrain-animation"],
|
|
5778
5821
|
text: ["speech"],
|
|
5779
5822
|
voice: ["speech"],
|
|
5780
|
-
seconds: ["sfx"],
|
|
5823
|
+
seconds: ["sfx", "music"],
|
|
5781
5824
|
loop: ["sfx"],
|
|
5825
|
+
instrumental: ["music"],
|
|
5826
|
+
plan: ["music"],
|
|
5782
5827
|
out: ["sfx", "speech", "music", "vfx"],
|
|
5783
5828
|
preset: ["vfx"],
|
|
5784
5829
|
part: ["vfx"],
|
|
@@ -5819,8 +5864,18 @@ const MORE_OPTIONS = {
|
|
|
5819
5864
|
terrain: { type: "string", value: "<asset id|name>", description: "the terrain set an animated terrain set moves" },
|
|
5820
5865
|
text: { type: "string", value: "<text>", description: "what speech says" },
|
|
5821
5866
|
voice: { type: "string", value: "<voice>", description: `the voice speech is said in: ${VOICES.join(", ")}` },
|
|
5822
|
-
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
|
+
},
|
|
5823
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
|
+
},
|
|
5824
5879
|
out: {
|
|
5825
5880
|
type: "string",
|
|
5826
5881
|
value: "<path>",
|
|
@@ -5995,9 +6050,9 @@ async function moreRequest(context, kind, args, instruction, reference) {
|
|
|
5995
6050
|
if (!VOICES.includes(voice)) throw new CliError(`--voice is one of ${VOICES.join(", ")}`);
|
|
5996
6051
|
return { kind, body: { text: text2, voice }, words: `${words} in ${voice}: ${text2}` };
|
|
5997
6052
|
}
|
|
6053
|
+
if (kind === "music") return musicRequestOf(context, args, instruction);
|
|
5998
6054
|
if (instruction === "") throw new CliError(`describe ${words} with --instruction`);
|
|
5999
6055
|
if (instruction.length > MAX_AUDIO_DESCRIPTION) throw new CliError(`a description is at most ${MAX_AUDIO_DESCRIPTION} characters`);
|
|
6000
|
-
if (kind === "music") return { kind, body: { description: instruction }, words: `${words}: ${instruction}` };
|
|
6001
6056
|
const seconds = stringOf(args, "seconds");
|
|
6002
6057
|
const length = seconds === void 0 ? void 0 : Number(seconds);
|
|
6003
6058
|
if (length !== void 0 && !(length >= SFX_SECONDS.min && length <= SFX_SECONDS.max))
|
|
@@ -6008,6 +6063,54 @@ async function moreRequest(context, kind, args, instruction, reference) {
|
|
|
6008
6063
|
words: `${words}${length === void 0 ? "" : ` of ${length} s`}${flagOf(args, "loop") ? ", looping" : ""}: ${instruction}`
|
|
6009
6064
|
};
|
|
6010
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
|
+
}
|
|
6011
6114
|
async function withPatch(platform, project, request) {
|
|
6012
6115
|
const { terrain, ...body } = request.body;
|
|
6013
6116
|
const asset = await platform.web.asset(project, terrain);
|
|
@@ -6303,9 +6406,9 @@ async function shapedRequest(context, asked, args) {
|
|
|
6303
6406
|
if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a pixel-art concept art's");
|
|
6304
6407
|
return { kind, body, ...references.length > 0 ? { images: { references } } : {}, words };
|
|
6305
6408
|
}
|
|
6306
|
-
async function confirmed(context, platform, what, prices, kind, yes) {
|
|
6409
|
+
async function confirmed(context, platform, what, prices, kind, yes, body) {
|
|
6307
6410
|
const usage = usageOf$1(await platform.web.billing());
|
|
6308
|
-
const share = costShareWords(prices, kind, usage);
|
|
6411
|
+
const share = costShareWords(prices, kind, usage, body);
|
|
6309
6412
|
const priced = share === null ? "using what the platform will say of your limit" : `using ${share}`;
|
|
6310
6413
|
context.io.warn(`Making ${what}, ${priced}; ${leftWords(usage)}.
|
|
6311
6414
|
`);
|
|
@@ -6331,7 +6434,7 @@ async function followed(context, platform, jobId) {
|
|
|
6331
6434
|
const generate = {
|
|
6332
6435
|
name: "generate",
|
|
6333
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",
|
|
6334
|
-
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]",
|
|
6335
6438
|
options: {
|
|
6336
6439
|
instruction: {
|
|
6337
6440
|
type: "string",
|
|
@@ -6387,7 +6490,7 @@ const generate = {
|
|
|
6387
6490
|
const platform = await signedIn(context);
|
|
6388
6491
|
const request = asked.kind === "terrain-animation" ? await withPatch(platform, project, asked) : asked;
|
|
6389
6492
|
const prices = await platform.web.prices();
|
|
6390
|
-
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);
|
|
6391
6494
|
const jobId = await paying(platform, () => startGeneration(platform.web, project, request));
|
|
6392
6495
|
context.io.warn(`Started the job ${jobId}.
|
|
6393
6496
|
`);
|
|
@@ -7377,7 +7480,7 @@ function terminalOf(streams) {
|
|
|
7377
7480
|
step: (title, at = "step") => drawStep(streams.stderr, title, at)
|
|
7378
7481
|
};
|
|
7379
7482
|
}
|
|
7380
|
-
const version = "0.
|
|
7483
|
+
const version = "0.59.0";
|
|
7381
7484
|
const manifest = {
|
|
7382
7485
|
version
|
|
7383
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
|