@bitmagic/cli 0.1.71 → 0.1.72
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 +2 -2
- package/dist/auth/subscribe-flow.d.ts +10 -1
- package/dist/auth/subscribe-flow.js +13 -4
- package/dist/auth/subscribe-flow.js.map +1 -1
- package/dist/cli.d.ts +8 -0
- package/dist/commands/generate.d.ts +17 -32
- package/dist/commands/generate.js +58 -91
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +16 -10
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/subscribe.d.ts +4 -0
- package/dist/commands/subscribe.js +6 -1
- package/dist/commands/subscribe.js.map +1 -1
- package/dist/forge/edit-target.js +2 -1
- package/dist/forge/edit-target.js.map +1 -1
- package/dist/generate/model.d.ts +29 -0
- package/dist/generate/model.js +7 -28
- package/dist/generate/model.js.map +1 -1
- package/dist/generate/music.js +2 -2
- package/dist/generate/prop.d.ts +29 -0
- package/dist/generate/prop.js +53 -27
- package/dist/generate/prop.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -185,8 +185,8 @@ browser reloads itself on every change and the assets panel shows each generatio
|
|
|
185
185
|
|
|
186
186
|
| Command | What it does |
|
|
187
187
|
|---|---|
|
|
188
|
-
| `bitmagic login [--env <env>]` | Device-code auth against Auth0; stores credentials per environment. |
|
|
189
|
-
| `bitmagic subscribe [--no-wait]` | Start the Bitmagic Pro subscription the CLI requires; opens checkout and waits for it. |
|
|
188
|
+
| `bitmagic login [--env <env>] [--ref <campaign>]` | Device-code auth against Auth0; stores credentials per environment. `--ref` is set by a landing page's prompt and passed on to the Bitmagic Pro page for attribution. |
|
|
189
|
+
| `bitmagic subscribe [--no-wait] [--ref <campaign>]` | Start the Bitmagic Pro subscription the CLI requires; opens checkout and waits for it. |
|
|
190
190
|
| `bitmagic usage [--days N]` | Where this account's sparks went, and what Pro allowance is left. |
|
|
191
191
|
| `bitmagic allowance [--allow-sparks \| --block-sparks]` | The two Pro allowance windows, and whether your purchased sparks may fund work once they run out. `--allow-sparks` needs a real terminal — it authorises spending your money, so an agent cannot run it for you. |
|
|
192
192
|
| `bitmagic logout [--env <env>]` | Remove stored credentials for an environment. |
|
|
@@ -56,6 +56,8 @@ export interface SubscribeOptions {
|
|
|
56
56
|
noOpen?: boolean;
|
|
57
57
|
/** Skip the wait entirely and return as soon as checkout is open. */
|
|
58
58
|
noWait?: boolean;
|
|
59
|
+
/** The campaign that sent the creator (`--ref`), passed on to the storefront. See storefrontUrlFor. */
|
|
60
|
+
ref?: string;
|
|
59
61
|
}
|
|
60
62
|
export interface SubscribeResult {
|
|
61
63
|
/** True when Pro was confirmed active before we returned. */
|
|
@@ -88,8 +90,15 @@ export interface SubscribeResult {
|
|
|
88
90
|
* there is no session to resume it comes straight back and nothing is shown.
|
|
89
91
|
* The portal treats a bare `cli=1` as the same request, so every CLI already
|
|
90
92
|
* installed is fixed by the portal deploy alone — this only states it.
|
|
93
|
+
*
|
|
94
|
+
* `ref` names the paid campaign whose landing page handed the creator the
|
|
95
|
+
* prompt that ran `bitmagic login --ref <campaign>`. The storefront reports it
|
|
96
|
+
* to analytics (portal/src/campaign-ref.ts): the prompt is the one thing that
|
|
97
|
+
* reliably crosses from the landing page's browser to wherever this opens. Only
|
|
98
|
+
* a short slug is passed on; anything else is dropped rather than refused,
|
|
99
|
+
* because a marketing tag must never stand between a creator and signing in.
|
|
91
100
|
*/
|
|
92
|
-
export declare function storefrontUrlFor(subscribeUrl: string, userId: string): string;
|
|
101
|
+
export declare function storefrontUrlFor(subscribeUrl: string, userId: string, ref?: string): string;
|
|
93
102
|
/**
|
|
94
103
|
* Explain that Pro is required, open the storefront, and wait for it to land.
|
|
95
104
|
*
|
|
@@ -82,10 +82,19 @@ export function fetchWhoami(environment, token, deps) {
|
|
|
82
82
|
* there is no session to resume it comes straight back and nothing is shown.
|
|
83
83
|
* The portal treats a bare `cli=1` as the same request, so every CLI already
|
|
84
84
|
* installed is fixed by the portal deploy alone — this only states it.
|
|
85
|
+
*
|
|
86
|
+
* `ref` names the paid campaign whose landing page handed the creator the
|
|
87
|
+
* prompt that ran `bitmagic login --ref <campaign>`. The storefront reports it
|
|
88
|
+
* to analytics (portal/src/campaign-ref.ts): the prompt is the one thing that
|
|
89
|
+
* reliably crosses from the landing page's browser to wherever this opens. Only
|
|
90
|
+
* a short slug is passed on; anything else is dropped rather than refused,
|
|
91
|
+
* because a marketing tag must never stand between a creator and signing in.
|
|
85
92
|
*/
|
|
86
|
-
export function storefrontUrlFor(subscribeUrl, userId) {
|
|
93
|
+
export function storefrontUrlFor(subscribeUrl, userId, ref) {
|
|
87
94
|
const separator = subscribeUrl.includes('?') ? '&' : '?';
|
|
88
|
-
|
|
95
|
+
const slug = ref?.trim().toLowerCase();
|
|
96
|
+
const campaign = slug && /^[a-z0-9][a-z0-9-]{0,31}$/.test(slug) ? `&ref=${slug}` : '';
|
|
97
|
+
return `${subscribeUrl}${separator}cli=1&signin=1&u=${encodeURIComponent(userId)}${campaign}`;
|
|
89
98
|
}
|
|
90
99
|
/**
|
|
91
100
|
* Explain that Pro is required, open the storefront, and wait for it to land.
|
|
@@ -98,7 +107,7 @@ export async function runSubscribeFlow(environment, token, me, output, options =
|
|
|
98
107
|
// An api-server too old to serve a storefront still has to be able to sell a
|
|
99
108
|
// subscription, so fall back to minting Checkout the way this flow used to.
|
|
100
109
|
const url = me.subscribeUrl
|
|
101
|
-
? storefrontUrlFor(me.subscribeUrl, me.userId)
|
|
110
|
+
? storefrontUrlFor(me.subscribeUrl, me.userId, options.ref)
|
|
102
111
|
: (await apiPost(environment, '/api/cli/v1/pro/checkout', token, {}, { fetch: deps.fetch })).url;
|
|
103
112
|
output.info('');
|
|
104
113
|
output.info('The Bitmagic CLI needs Bitmagic Pro.');
|
|
@@ -126,7 +135,7 @@ export async function runSubscribeFlow(environment, token, me, output, options =
|
|
|
126
135
|
// A transient failure mid-poll must not end the flow — the creator may
|
|
127
136
|
// still be typing their card in. Only a definitive "yes" stops the loop.
|
|
128
137
|
// This answers for a redeemed voucher too: `pro` is entitlement, not billing.
|
|
129
|
-
let current
|
|
138
|
+
let current;
|
|
130
139
|
try {
|
|
131
140
|
current = await fetchWhoami(environment, token, deps);
|
|
132
141
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscribe-flow.js","sourceRoot":"","sources":["../../src/auth/subscribe-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAInD,kFAAkF;AAClF,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,MAAM,CAAC;AACrD,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AA6BhD;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAkB;IAClD,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9C,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC;QACX,IAAI,CAAC,EAAE,CAAC,QAAQ;YAAE,OAAO,yBAAyB,CAAC;QACnD,OAAO,EAAE,CAAC,iBAAiB,KAAK,IAAI;YAClC,CAAC,CAAC,gCAAgC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,0DAA0D;YAC5G,CAAC,CAAC,kCAAkC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;IAC5D,CAAC;IACD,OAAO,EAAE,CAAC,QAAQ;QAChB,CAAC,CAAC,yBAAyB,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG;QAC9C,CAAC,CAAC,+BAA+B,CAAC;AACtC,CAAC;AAcD,MAAM,CAAC,MAAM,oBAAoB,GAAkB;IACjD,KAAK,EAAE,UAAU,CAAC,KAAK;IACvB,KAAK,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClE,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;IACrB,aAAa;CACd,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,WAAwB,EAAE,KAAa,EAAE,IAAmB;IACtF,OAAO,MAAM,CAAiB,WAAW,EAAE,oBAAoB,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AACjG,CAAC;
|
|
1
|
+
{"version":3,"file":"subscribe-flow.js","sourceRoot":"","sources":["../../src/auth/subscribe-flow.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -42,6 +42,10 @@ declare const rawCommands: {
|
|
|
42
42
|
readonly type: "boolean";
|
|
43
43
|
readonly description: "Print the result as JSON on stdout; all progress goes to stderr.";
|
|
44
44
|
};
|
|
45
|
+
readonly ref: {
|
|
46
|
+
readonly type: "string";
|
|
47
|
+
readonly description: "The campaign that sent you, passed on to the Bitmagic Pro page. Set by a landing page's prompt.";
|
|
48
|
+
};
|
|
45
49
|
}>;
|
|
46
50
|
logout: CommandDef<{
|
|
47
51
|
readonly env: {
|
|
@@ -94,6 +98,10 @@ declare const rawCommands: {
|
|
|
94
98
|
readonly type: "boolean";
|
|
95
99
|
readonly description: "Print the result as JSON on stdout; all progress goes to stderr.";
|
|
96
100
|
};
|
|
101
|
+
readonly ref: {
|
|
102
|
+
readonly type: "string";
|
|
103
|
+
readonly description: "The campaign that sent you, passed on to the Bitmagic Pro page. Set by a landing page's prompt.";
|
|
104
|
+
};
|
|
97
105
|
}>;
|
|
98
106
|
usage: CommandDef<{
|
|
99
107
|
readonly days: {
|
|
@@ -221,25 +221,6 @@ export interface MusicRunResult {
|
|
|
221
221
|
file?: string;
|
|
222
222
|
bytes?: number;
|
|
223
223
|
}
|
|
224
|
-
/**
|
|
225
|
-
* `generate music` is the one `generate` lane with two outputs a creator may want separately: the
|
|
226
|
-
* world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a
|
|
227
|
-
* video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game
|
|
228
|
-
* for the file-only case, so a trailer bed is not registered as a track the game never plays.
|
|
229
|
-
*
|
|
230
|
-
* Cannot go through {@link runAssetGeneration} because that always applies the patches and never
|
|
231
|
-
* downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the
|
|
232
|
-
* "world.json is unchanged" failure contract are the same as every other lane's.
|
|
233
|
-
*/
|
|
234
|
-
export declare function runMusicGeneration(options: {
|
|
235
|
-
body: Record<string, unknown>;
|
|
236
|
-
/** Save the MP3 here (relative to the current directory). */
|
|
237
|
-
out?: string;
|
|
238
|
-
/** Apply the asset to world.json. The default; `--no-world` turns it off. */
|
|
239
|
-
world: boolean;
|
|
240
|
-
json: boolean;
|
|
241
|
-
deps?: GenerateRunDeps;
|
|
242
|
-
}): Promise<MusicRunResult>;
|
|
243
224
|
export interface SpeechRunResult {
|
|
244
225
|
speechUrl: string;
|
|
245
226
|
originalUrl: string;
|
|
@@ -250,18 +231,7 @@ export interface SpeechRunResult {
|
|
|
250
231
|
file?: string;
|
|
251
232
|
bytes?: number;
|
|
252
233
|
}
|
|
253
|
-
|
|
254
|
-
* `generate speech` has the same two-outputs shape as `generate music`, and for a sharper reason:
|
|
255
|
-
* the world.json asset is a line an NPC speaks (`engine.playSound`), while the MP3 is the input
|
|
256
|
-
* `tools/lipsync/make_line.py --audio` needs to cut mouth cues from. A creator rigging a talking
|
|
257
|
-
* character wants the file and NOT the asset, so `--no-world` is a first-class path here rather
|
|
258
|
-
* than an escape hatch.
|
|
259
|
-
*
|
|
260
|
-
* Cannot go through {@link runAssetGeneration} because that always applies the patches and never
|
|
261
|
-
* downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the
|
|
262
|
-
* "world.json is unchanged" failure contract are the same as every other lane's.
|
|
263
|
-
*/
|
|
264
|
-
export declare function runSpeechGeneration(options: {
|
|
234
|
+
export interface AudioRunOptions {
|
|
265
235
|
body: Record<string, unknown>;
|
|
266
236
|
/** Save the MP3 here (relative to the current directory). */
|
|
267
237
|
out?: string;
|
|
@@ -269,7 +239,22 @@ export declare function runSpeechGeneration(options: {
|
|
|
269
239
|
world: boolean;
|
|
270
240
|
json: boolean;
|
|
271
241
|
deps?: GenerateRunDeps;
|
|
272
|
-
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* `generate music` is the one `generate` lane with two outputs a creator may want separately: the
|
|
245
|
+
* world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a
|
|
246
|
+
* video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game
|
|
247
|
+
* for the file-only case, so a trailer bed is not registered as a track the game never plays.
|
|
248
|
+
*/
|
|
249
|
+
export declare function runMusicGeneration(options: AudioRunOptions): Promise<MusicRunResult>;
|
|
250
|
+
/**
|
|
251
|
+
* `generate speech` has the same two-outputs shape as `generate music`, and for a sharper reason:
|
|
252
|
+
* the world.json asset is a line an NPC speaks (`engine.playSound`), while the MP3 is the input
|
|
253
|
+
* `tools/lipsync/make_line.py --audio` needs to cut mouth cues from. A creator rigging a talking
|
|
254
|
+
* character wants the file and NOT the asset, so `--no-world` is a first-class path here rather
|
|
255
|
+
* than an escape hatch.
|
|
256
|
+
*/
|
|
257
|
+
export declare function runSpeechGeneration(options: AudioRunOptions): Promise<SpeechRunResult>;
|
|
273
258
|
/**
|
|
274
259
|
* Refuses `--voxel-grid` alongside `--mesh`, rather than ignoring it.
|
|
275
260
|
*
|
|
@@ -24,7 +24,6 @@ import { smartObjectSpecSchema, formatZodIssues } from '@bitmagic/asset-core';
|
|
|
24
24
|
import { DEFAULT_MASTER_GRID, generateAndInstallProp } from '../generate/prop.js';
|
|
25
25
|
import { generateAndInstallVehicle } from '../generate/vehicle.js';
|
|
26
26
|
import { generateAndInstallModel } from '../generate/model.js';
|
|
27
|
-
import { downloadMusicFile } from '../generate/music.js';
|
|
28
27
|
import { downloadAudioFile } from '../generate/audio-file.js';
|
|
29
28
|
import { MUSIC_MAX_SECONDS, MUSIC_MIN_SECONDS, SPEECH_MAX_CHARS, SPEECH_VOICES, VIDEO_MAX_SECONDS, VIDEO_MIN_SECONDS, } from '@bitmagic/asset-core';
|
|
30
29
|
import { generateAndInstallAnimationFromVideo, isLocomotionState, isVideoToMotionModel, } from '../generate/animation-from-video.js';
|
|
@@ -567,24 +566,24 @@ export async function requestAssetWithJobRecord(type, label, body, runDeps, onSu
|
|
|
567
566
|
});
|
|
568
567
|
}
|
|
569
568
|
/**
|
|
570
|
-
*
|
|
571
|
-
* world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a
|
|
572
|
-
* video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game
|
|
573
|
-
* for the file-only case, so a trailer bed is not registered as a track the game never plays.
|
|
569
|
+
* The lane both audio generators share: request, apply (or deliberately not), download (or not).
|
|
574
570
|
*
|
|
575
|
-
*
|
|
576
|
-
* downloads
|
|
577
|
-
* "world.json is unchanged" failure contract
|
|
571
|
+
* Neither can go through {@link runAssetGeneration} — that always applies the patches and never
|
|
572
|
+
* downloads — so both share {@link requestAssetWithJobRecord} instead, which is what keeps the
|
|
573
|
+
* request, the job record and the "world.json is unchanged" failure contract identical to every
|
|
574
|
+
* other lane's. Written once because the two differed only in nouns, and a message about a
|
|
575
|
+
* generation that was billed and then half-lost is the last thing a creator reads.
|
|
578
576
|
*/
|
|
579
|
-
|
|
577
|
+
async function runAudioGeneration(lane, options) {
|
|
580
578
|
const output = createOutput(options.json);
|
|
581
579
|
const runDeps = options.deps ?? defaultDeps(options.json);
|
|
582
580
|
const outPath = options.out === undefined ? undefined : path.resolve(options.out);
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
581
|
+
const shared = await requestAssetWithJobRecord(lane.type, lane.type, options.body, runDeps, async (outcome, worldPath, log) => {
|
|
582
|
+
const urls = lane.read(outcome);
|
|
583
|
+
if (!urls) {
|
|
584
|
+
throw new CliError(`The server reported the ${lane.noun} as generated but sent ${lane.missingUrls}. world.json is unchanged.`);
|
|
586
585
|
}
|
|
587
|
-
const
|
|
586
|
+
const generated = `Generated ${lane.noun} (${urls.durationSeconds.toFixed(1)}s): ${outcome.message}`;
|
|
588
587
|
let patchesApplied = 0;
|
|
589
588
|
if (options.world) {
|
|
590
589
|
let applyResult;
|
|
@@ -592,120 +591,88 @@ export async function runMusicGeneration(options) {
|
|
|
592
591
|
applyResult = applyPatchesToWorld(worldPath, outcome.patches);
|
|
593
592
|
}
|
|
594
593
|
catch (error) {
|
|
595
|
-
throw decoratedError(error, (message) => `Generated
|
|
596
|
-
+
|
|
597
|
-
+ `yet in your project. It is at ${
|
|
594
|
+
throw decoratedError(error, (message) => `Generated ${lane.noun} successfully, but could not write it into world.json: ${message} `
|
|
595
|
+
+ `world.json is unchanged — ${lane.output.toLowerCase()} was generated (and may have been billed) `
|
|
596
|
+
+ `but is not yet in your project. It is at ${urls.originalUrl}.`);
|
|
598
597
|
}
|
|
599
598
|
patchesApplied = applyResult.applied;
|
|
600
|
-
log(
|
|
599
|
+
log(generated);
|
|
601
600
|
for (const line of applyResult.summary)
|
|
602
601
|
runDeps.log(` - ${line}`);
|
|
603
602
|
runDeps.log(worldUpdatedLine(patchesApplied));
|
|
604
603
|
}
|
|
605
604
|
else {
|
|
606
|
-
log(
|
|
605
|
+
log(generated);
|
|
607
606
|
runDeps.log('world.json is unchanged (--no-world).');
|
|
608
607
|
}
|
|
609
608
|
let bytes;
|
|
610
609
|
if (outPath !== undefined) {
|
|
611
610
|
try {
|
|
612
|
-
bytes = await
|
|
611
|
+
bytes = await downloadAudioFile(urls.originalUrl, outPath, runDeps.fetch, lane.fileLabel);
|
|
613
612
|
}
|
|
614
613
|
catch (error) {
|
|
615
|
-
// The asset is generated, billed, and (unless --no-world) already in world.json; only
|
|
616
|
-
// local copy is missing, and the message has to say so or the creator regenerates.
|
|
617
|
-
throw decoratedError(error, (message) => `${message}
|
|
614
|
+
// The asset is generated, billed, and (unless --no-world) already in world.json; only
|
|
615
|
+
// the local copy is missing, and the message has to say so or the creator regenerates.
|
|
616
|
+
throw decoratedError(error, (message) => `${message} ${lane.output} was generated${options.world ? ' and is in world.json' : ''}; only saving `
|
|
618
617
|
+ 'the local copy failed.');
|
|
619
618
|
}
|
|
620
619
|
runDeps.log(`Saved ${outPath} (${Math.round(bytes / 1024)} KB).`);
|
|
621
620
|
}
|
|
622
621
|
return {
|
|
623
|
-
|
|
624
|
-
originalUrl: music.originalUrl,
|
|
625
|
-
musicId: music.musicId,
|
|
626
|
-
durationSeconds: music.durationSeconds,
|
|
622
|
+
...urls,
|
|
627
623
|
patchesApplied,
|
|
628
624
|
...(outPath !== undefined ? { file: outPath } : {}),
|
|
629
625
|
...(bytes !== undefined ? { bytes } : {}),
|
|
630
626
|
};
|
|
631
627
|
});
|
|
632
|
-
|
|
628
|
+
const result = lane.rename(shared);
|
|
629
|
+
output.result({ ok: true, kind: lane.type, ...result });
|
|
633
630
|
if (options.world) {
|
|
634
|
-
runDeps.log(
|
|
631
|
+
runDeps.log(lane.playHint(shared.id));
|
|
635
632
|
runDeps.log(DEV_VIEW_RELOADS);
|
|
636
633
|
}
|
|
637
634
|
return result;
|
|
638
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* `generate music` is the one `generate` lane with two outputs a creator may want separately: the
|
|
638
|
+
* world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a
|
|
639
|
+
* video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game
|
|
640
|
+
* for the file-only case, so a trailer bed is not registered as a track the game never plays.
|
|
641
|
+
*/
|
|
642
|
+
export function runMusicGeneration(options) {
|
|
643
|
+
return runAudioGeneration({
|
|
644
|
+
type: 'music',
|
|
645
|
+
noun: 'music',
|
|
646
|
+
output: 'The track',
|
|
647
|
+
missingUrls: 'no track URLs',
|
|
648
|
+
fileLabel: 'track',
|
|
649
|
+
read: ({ music }) => (music
|
|
650
|
+
? { url: music.musicUrl, originalUrl: music.originalUrl, id: music.musicId, durationSeconds: music.durationSeconds }
|
|
651
|
+
: undefined),
|
|
652
|
+
rename: ({ url, id, ...rest }) => ({ ...rest, musicUrl: url, musicId: id }),
|
|
653
|
+
playHint: (musicId) => `Play it from game code with engine.playMusic('${musicId}').`,
|
|
654
|
+
}, options);
|
|
655
|
+
}
|
|
639
656
|
/**
|
|
640
657
|
* `generate speech` has the same two-outputs shape as `generate music`, and for a sharper reason:
|
|
641
658
|
* the world.json asset is a line an NPC speaks (`engine.playSound`), while the MP3 is the input
|
|
642
659
|
* `tools/lipsync/make_line.py --audio` needs to cut mouth cues from. A creator rigging a talking
|
|
643
660
|
* character wants the file and NOT the asset, so `--no-world` is a first-class path here rather
|
|
644
661
|
* than an escape hatch.
|
|
645
|
-
*
|
|
646
|
-
* Cannot go through {@link runAssetGeneration} because that always applies the patches and never
|
|
647
|
-
* downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the
|
|
648
|
-
* "world.json is unchanged" failure contract are the same as every other lane's.
|
|
649
662
|
*/
|
|
650
|
-
export
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
applyResult = applyPatchesToWorld(worldPath, outcome.patches);
|
|
664
|
-
}
|
|
665
|
-
catch (error) {
|
|
666
|
-
throw decoratedError(error, (message) => `Generated speech successfully, but could not write it into world.json: ${message} `
|
|
667
|
-
+ 'world.json is unchanged — the line was generated (and may have been billed) but is not '
|
|
668
|
-
+ `yet in your project. It is at ${speech.originalUrl}.`);
|
|
669
|
-
}
|
|
670
|
-
patchesApplied = applyResult.applied;
|
|
671
|
-
log(`Generated speech (${speech.durationSeconds.toFixed(1)}s): ${outcome.message}`);
|
|
672
|
-
for (const line of applyResult.summary)
|
|
673
|
-
runDeps.log(` - ${line}`);
|
|
674
|
-
runDeps.log(worldUpdatedLine(patchesApplied));
|
|
675
|
-
}
|
|
676
|
-
else {
|
|
677
|
-
log(`Generated speech (${speech.durationSeconds.toFixed(1)}s): ${outcome.message}`);
|
|
678
|
-
runDeps.log('world.json is unchanged (--no-world).');
|
|
679
|
-
}
|
|
680
|
-
let bytes;
|
|
681
|
-
if (outPath !== undefined) {
|
|
682
|
-
try {
|
|
683
|
-
bytes = await downloadAudioFile(speech.originalUrl, outPath, runDeps.fetch, 'line');
|
|
684
|
-
}
|
|
685
|
-
catch (error) {
|
|
686
|
-
// The line is generated, billed, and (unless --no-world) already in world.json; only the
|
|
687
|
-
// local copy is missing, and the message has to say so or the creator regenerates.
|
|
688
|
-
throw decoratedError(error, (message) => `${message} The line was generated${options.world ? ' and is in world.json' : ''}; only saving `
|
|
689
|
-
+ 'the local copy failed.');
|
|
690
|
-
}
|
|
691
|
-
runDeps.log(`Saved ${outPath} (${Math.round(bytes / 1024)} KB).`);
|
|
692
|
-
}
|
|
693
|
-
return {
|
|
694
|
-
speechUrl: speech.speechUrl,
|
|
695
|
-
originalUrl: speech.originalUrl,
|
|
696
|
-
speechId: speech.speechId,
|
|
697
|
-
durationSeconds: speech.durationSeconds,
|
|
698
|
-
patchesApplied,
|
|
699
|
-
...(outPath !== undefined ? { file: outPath } : {}),
|
|
700
|
-
...(bytes !== undefined ? { bytes } : {}),
|
|
701
|
-
};
|
|
702
|
-
});
|
|
703
|
-
output.result({ ok: true, kind: 'speech', ...result });
|
|
704
|
-
if (options.world) {
|
|
705
|
-
runDeps.log(`Play it from game code with engine.playSound('${result.speechId}').`);
|
|
706
|
-
runDeps.log(DEV_VIEW_RELOADS);
|
|
707
|
-
}
|
|
708
|
-
return result;
|
|
663
|
+
export function runSpeechGeneration(options) {
|
|
664
|
+
return runAudioGeneration({
|
|
665
|
+
type: 'speech',
|
|
666
|
+
noun: 'speech',
|
|
667
|
+
output: 'The line',
|
|
668
|
+
missingUrls: 'no audio URLs',
|
|
669
|
+
fileLabel: 'line',
|
|
670
|
+
read: ({ speech }) => (speech
|
|
671
|
+
? { url: speech.speechUrl, originalUrl: speech.originalUrl, id: speech.speechId, durationSeconds: speech.durationSeconds }
|
|
672
|
+
: undefined),
|
|
673
|
+
rename: ({ url, id, ...rest }) => ({ ...rest, speechUrl: url, speechId: id }),
|
|
674
|
+
playHint: (speechId) => `Play it from game code with engine.playSound('${speechId}').`,
|
|
675
|
+
}, options);
|
|
709
676
|
}
|
|
710
677
|
/**
|
|
711
678
|
* Refuses `--voxel-grid` alongside `--mesh`, rather than ignoring it.
|