@helix3/helix-cli 0.1.13-helix3.74 → 0.1.13-helix3.75

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.
@@ -1,6 +1,30 @@
1
1
  import type { Command } from 'commander';
2
2
  import { type GenerateAudioInput, type StandaloneGeneratedAsset } from './assets';
3
3
  import type { CliCredentials } from './config';
4
+ type GenerateAudioOptions = {
5
+ mode: string;
6
+ additionalPrompt?: string;
7
+ model?: string;
8
+ outputFormat?: string;
9
+ projectId?: string;
10
+ sessionId?: string;
11
+ title?: string;
12
+ durationSeconds?: string;
13
+ loop?: boolean;
14
+ promptInfluence?: string;
15
+ forceInstrumental?: boolean;
16
+ text?: string;
17
+ voiceId?: string;
18
+ languageCode?: string;
19
+ seed?: string;
20
+ stability?: string;
21
+ similarityBoost?: string;
22
+ style?: string;
23
+ speed?: string;
24
+ speakerBoost?: boolean;
25
+ pronunciationDictionaryIds?: string;
26
+ output?: string;
27
+ };
4
28
  export type GenerateAudioCommandDependencies = {
5
29
  requireAuth: () => CliCredentials;
6
30
  generateAudio: (creds: CliCredentials, input: GenerateAudioInput, options?: {
@@ -12,4 +36,30 @@ export type GenerateAudioCommandDependencies = {
12
36
  printJson: (value: unknown) => void;
13
37
  fail: (message: string) => never;
14
38
  };
39
+ export declare const AUDIO_MODES: readonly ["sound_effect", "music", "text_to_speech"];
40
+ export declare const AUDIO_OPTIONS: {
41
+ flags: string;
42
+ description: string;
43
+ default?: string;
44
+ shared?: boolean;
45
+ }[];
46
+ /** Attach the audio flags. `skipShared` omits the ones the target command already declares. */
47
+ export declare function addAudioOptions(command: Command, opts?: {
48
+ skipShared?: boolean;
49
+ }): Command;
50
+ export type AudioFlagValues = Omit<GenerateAudioOptions, 'output'>;
51
+ /**
52
+ * Parse the audio flags into the library's input shape. Shared by both routes so a flag can never
53
+ * mean one thing synchronously and another durably.
54
+ */
55
+ export declare function buildAudioInput(prompt: string | undefined, opts: AudioFlagValues, fail: (message: string) => never): GenerateAudioInput;
56
+ /**
57
+ * Everything that can be known to be wrong BEFORE any LIX moves. Returns the problems rather than
58
+ * throwing, so callers can render them all at once with the fix named.
59
+ *
60
+ * The voice check is deliberately NOT done here — it needs the catalog, and a network lookup that
61
+ * fails must not become a new way to block a valid job.
62
+ */
63
+ export declare function validateAudioRequest(prompt: string | undefined, opts: AudioFlagValues): string[];
15
64
  export declare function registerGenerateAudioCommand(assets: Command, dependencies: GenerateAudioCommandDependencies): void;
65
+ export {};
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AUDIO_OPTIONS = exports.AUDIO_MODES = void 0;
4
+ exports.addAudioOptions = addAudioOptions;
5
+ exports.buildAudioInput = buildAudioInput;
6
+ exports.validateAudioRequest = validateAudioRequest;
3
7
  exports.registerGenerateAudioCommand = registerGenerateAudioCommand;
4
8
  const api_1 = require("./api");
5
9
  const assets_1 = require("./assets");
6
- const AUDIO_MODES = [
10
+ exports.AUDIO_MODES = [
7
11
  'sound_effect',
8
12
  'music',
9
13
  'text_to_speech',
@@ -24,73 +28,140 @@ function commaList(value) {
24
28
  .filter(Boolean);
25
29
  return values?.length ? values : undefined;
26
30
  }
31
+ // The audio flags, as DATA, so the synchronous route (`generate-audio`) and the durable route
32
+ // (`assets start audio`) attach the SAME names from one definition. They drifted once already:
33
+ // `start` accepted no mode/text/voice flags at all, so the async path agents are told to prefer
34
+ // could only ever produce a sound effect, and music and speech were reachable only by blocking the
35
+ // call with no job id to poll.
36
+ //
37
+ // `start` already declares --title/--additional-prompt/--project-id/--session-id/--duration-seconds
38
+ // /--loop for every kind, so those are marked shared and skipped when attaching there.
39
+ exports.AUDIO_OPTIONS = [
40
+ { flags: '--mode <mode>', description: 'sound_effect, music, or text_to_speech', default: 'sound_effect' },
41
+ { flags: '--additional-prompt <text>', description: 'additional generation guidance', shared: true },
42
+ { flags: '--model <model>', description: 'an allowlisted ElevenLabs model id for the selected --mode' },
43
+ { flags: '--output-format <format>', description: 'portable MP3 output format, e.g. mp3_44100_128' },
44
+ { flags: '--project-id <id>', description: 'Dreamer project id', shared: true },
45
+ { flags: '--session-id <id>', description: 'Dreamer session id', shared: true },
46
+ { flags: '--title <title>', description: 'asset title', shared: true },
47
+ { flags: '--duration-seconds <n>', description: 'requested duration — sound_effect: 0.5-30s; music: 3-600s', shared: true },
48
+ { flags: '--loop', description: 'sound_effect only: request a seamless loop', shared: true },
49
+ { flags: '--prompt-influence <n>', description: 'sound_effect only: how closely to follow the prompt (0-1)' },
50
+ { flags: '--force-instrumental', description: 'music only: request an instrumental-only composition' },
51
+ { flags: '--text <text>', description: 'text_to_speech only: the text to convert to speech' },
52
+ { flags: '--voice-id <id>', description: 'text_to_speech only: the voice id to speak with (see `helix assets voices`)' },
53
+ { flags: '--language-code <code>', description: 'text_to_speech only: ISO 639-1 language code' },
54
+ { flags: '--seed <n>', description: 'text_to_speech only: a seed for deterministic output' },
55
+ { flags: '--stability <n>', description: 'text_to_speech only: voice stability (0-1)' },
56
+ { flags: '--similarity-boost <n>', description: 'text_to_speech only: voice similarity boost (0-1)' },
57
+ { flags: '--style <n>', description: 'text_to_speech only: voice style (0-1)' },
58
+ { flags: '--speed <n>', description: 'text_to_speech only: voice speed (0.7-1.2)' },
59
+ { flags: '--speaker-boost', description: 'text_to_speech only: enable speaker boost' },
60
+ {
61
+ flags: '--pronunciation-dictionary-ids <a,b,c>',
62
+ description: 'text_to_speech only: up to 3 comma-separated pronunciation dictionary ids',
63
+ },
64
+ ];
65
+ /** Attach the audio flags. `skipShared` omits the ones the target command already declares. */
66
+ function addAudioOptions(command, opts = {}) {
67
+ for (const option of exports.AUDIO_OPTIONS) {
68
+ if (opts.skipShared && option.shared)
69
+ continue;
70
+ if (option.default === undefined)
71
+ command.option(option.flags, option.description);
72
+ else
73
+ command.option(option.flags, option.description, option.default);
74
+ }
75
+ return command;
76
+ }
77
+ /**
78
+ * Parse the audio flags into the library's input shape. Shared by both routes so a flag can never
79
+ * mean one thing synchronously and another durably.
80
+ */
81
+ function buildAudioInput(prompt, opts, fail) {
82
+ const stability = optionalNumber(opts.stability, '--stability', fail);
83
+ const similarityBoost = optionalNumber(opts.similarityBoost, '--similarity-boost', fail);
84
+ const style = optionalNumber(opts.style, '--style', fail);
85
+ const speed = optionalNumber(opts.speed, '--speed', fail);
86
+ const voiceSettings = stability === undefined &&
87
+ similarityBoost === undefined &&
88
+ style === undefined &&
89
+ speed === undefined &&
90
+ opts.speakerBoost === undefined
91
+ ? undefined
92
+ : { stability, similarityBoost, style, speed, speakerBoost: opts.speakerBoost };
93
+ return {
94
+ prompt,
95
+ audioMode: opts.mode,
96
+ additionalPrompt: opts.additionalPrompt,
97
+ model: opts.model,
98
+ outputFormat: opts.outputFormat,
99
+ projectId: opts.projectId,
100
+ sessionId: opts.sessionId,
101
+ title: opts.title,
102
+ durationSeconds: optionalNumber(opts.durationSeconds, '--duration-seconds', fail),
103
+ loop: opts.loop,
104
+ promptInfluence: optionalNumber(opts.promptInfluence, '--prompt-influence', fail),
105
+ forceInstrumental: opts.forceInstrumental,
106
+ text: opts.text,
107
+ voiceId: opts.voiceId,
108
+ languageCode: opts.languageCode,
109
+ seed: optionalNumber(opts.seed, '--seed', fail),
110
+ voiceSettings,
111
+ pronunciationDictionaryIds: commaList(opts.pronunciationDictionaryIds),
112
+ };
113
+ }
114
+ /**
115
+ * Everything that can be known to be wrong BEFORE any LIX moves. Returns the problems rather than
116
+ * throwing, so callers can render them all at once with the fix named.
117
+ *
118
+ * The voice check is deliberately NOT done here — it needs the catalog, and a network lookup that
119
+ * fails must not become a new way to block a valid job.
120
+ */
121
+ function validateAudioRequest(prompt, opts) {
122
+ const problems = [];
123
+ const mode = opts.mode;
124
+ if (!exports.AUDIO_MODES.includes(mode)) {
125
+ problems.push(`--mode must be ${exports.AUDIO_MODES.join(', ')}; got '${opts.mode}'`);
126
+ return problems; // every other rule is mode-specific
127
+ }
128
+ if (mode === 'text_to_speech') {
129
+ if (!opts.text?.trim()) {
130
+ problems.push('text_to_speech needs --text "<what to say>" (the prompt argument is not used for speech)');
131
+ }
132
+ if (!opts.voiceId?.trim()) {
133
+ problems.push('text_to_speech needs --voice-id <id> — list them with `helix assets voices`');
134
+ }
135
+ }
136
+ else if (!prompt?.trim()) {
137
+ problems.push(`${mode} needs a prompt argument describing the audio to generate`);
138
+ }
139
+ if (mode !== 'music' && opts.forceInstrumental) {
140
+ problems.push('--force-instrumental applies to music only');
141
+ }
142
+ if (mode !== 'sound_effect' && opts.loop) {
143
+ problems.push('--loop applies to sound_effect only');
144
+ }
145
+ if (mode !== 'sound_effect' && opts.promptInfluence !== undefined) {
146
+ problems.push('--prompt-influence applies to sound_effect only');
147
+ }
148
+ return problems;
149
+ }
27
150
  function registerGenerateAudioCommand(assets, dependencies) {
28
- assets
151
+ const command = assets
29
152
  .command('generate-audio')
30
- .description('Generate a sound effect, music track, or spoken line via ElevenLabs (through Dreamer), auto-publish it to Vault, and optionally save it locally')
31
- .argument('[prompt]', 'generation prompt — required for sound_effect/music; text_to_speech uses --text instead')
32
- .option('--mode <mode>', 'sound_effect, music, or text_to_speech', 'sound_effect')
33
- .option('--additional-prompt <text>', 'additional generation guidance')
34
- .option('--model <model>', 'an allowlisted ElevenLabs model id for the selected --mode')
35
- .option('--output-format <format>', 'portable MP3 output format, e.g. mp3_44100_128')
36
- .option('--project-id <id>', 'Dreamer project id')
37
- .option('--session-id <id>', 'Dreamer session id')
38
- .option('--title <title>', 'asset title')
39
- .option('--duration-seconds <n>', 'requested duration — sound_effect: 0.5-30s; music: 3-600s')
40
- .option('--loop', 'sound_effect only: request a seamless loop')
41
- .option('--prompt-influence <n>', 'sound_effect only: how closely to follow the prompt (0-1)')
42
- .option('--force-instrumental', 'music only: request an instrumental-only composition')
43
- .option('--text <text>', 'text_to_speech only: the text to convert to speech')
44
- .option('--voice-id <id>', 'text_to_speech only: the ElevenLabs voice id to speak with')
45
- .option('--language-code <code>', 'text_to_speech only: ISO 639-1 language code')
46
- .option('--seed <n>', 'text_to_speech only: a seed for deterministic output')
47
- .option('--stability <n>', 'text_to_speech only: voice stability (0-1)')
48
- .option('--similarity-boost <n>', 'text_to_speech only: voice similarity boost (0-1)')
49
- .option('--style <n>', 'text_to_speech only: voice style (0-1)')
50
- .option('--speed <n>', 'text_to_speech only: voice speed (0.7-1.2)')
51
- .option('--speaker-boost', 'text_to_speech only: enable speaker boost')
52
- .option('--pronunciation-dictionary-ids <a,b,c>', 'text_to_speech only: up to 3 comma-separated pronunciation dictionary ids')
153
+ .description('Generate a sound effect, music track, or spoken line via ElevenLabs (through Dreamer), auto-publish it to Vault, and optionally save it locally. Blocks until done — use `helix assets start audio` for a pollable job.')
154
+ .argument('[prompt]', 'generation prompt — required for sound_effect/music; text_to_speech uses --text instead');
155
+ addAudioOptions(command)
53
156
  .option('--output <path>', 'save the generated audio file to this path')
54
157
  .action(async (prompt, opts) => {
55
- if (!AUDIO_MODES.includes(opts.mode)) {
56
- dependencies.fail(`✖ --mode must be ${AUDIO_MODES.join(', ')}; got '${opts.mode}'`);
158
+ const problems = validateAudioRequest(prompt, opts);
159
+ if (problems.length) {
160
+ dependencies.fail(`✖ ${problems.join('\n ✖ ')}`);
57
161
  }
58
- const durationSeconds = optionalNumber(opts.durationSeconds, '--duration-seconds', dependencies.fail);
59
- const promptInfluence = optionalNumber(opts.promptInfluence, '--prompt-influence', dependencies.fail);
60
- const seed = optionalNumber(opts.seed, '--seed', dependencies.fail);
61
- const stability = optionalNumber(opts.stability, '--stability', dependencies.fail);
62
- const similarityBoost = optionalNumber(opts.similarityBoost, '--similarity-boost', dependencies.fail);
63
- const style = optionalNumber(opts.style, '--style', dependencies.fail);
64
- const speed = optionalNumber(opts.speed, '--speed', dependencies.fail);
65
- const voiceSettings = stability === undefined &&
66
- similarityBoost === undefined &&
67
- style === undefined &&
68
- speed === undefined &&
69
- opts.speakerBoost === undefined
70
- ? undefined
71
- : { stability, similarityBoost, style, speed, speakerBoost: opts.speakerBoost };
72
162
  const creds = dependencies.requireAuth();
73
163
  try {
74
- const job = await dependencies.generateAudio(creds, {
75
- prompt,
76
- audioMode: opts.mode,
77
- additionalPrompt: opts.additionalPrompt,
78
- model: opts.model,
79
- outputFormat: opts.outputFormat,
80
- projectId: opts.projectId,
81
- sessionId: opts.sessionId,
82
- title: opts.title,
83
- durationSeconds,
84
- loop: opts.loop,
85
- promptInfluence,
86
- forceInstrumental: opts.forceInstrumental,
87
- text: opts.text,
88
- voiceId: opts.voiceId,
89
- languageCode: opts.languageCode,
90
- seed,
91
- voiceSettings,
92
- pronunciationDictionaryIds: commaList(opts.pronunciationDictionaryIds),
93
- }, { timeoutMs: assets_1.AUDIO_GENERATION_TIMEOUT_MS });
164
+ const job = await dependencies.generateAudio(creds, buildAudioInput(prompt, opts, dependencies.fail), { timeoutMs: assets_1.AUDIO_GENERATION_TIMEOUT_MS });
94
165
  if (opts.output && job.status === 'succeeded' && job.asset) {
95
166
  await dependencies.saveGeneratedAsset(job.asset, opts.output, {
96
167
  apiUrl: creds.apiUrl,
@@ -1 +1 @@
1
- {"version":3,"file":"audioCommand.js","sourceRoot":"","sources":["../src/audioCommand.ts"],"names":[],"mappings":";;AA+EA,oEA0HC;AAxMD,+BAAiC;AACjC,qCAMkB;AA4ClB,MAAM,WAAW,GAAG;IAClB,cAAc;IACd,OAAO;IACP,gBAAgB;CAC8B,CAAC;AAEjD,SAAS,cAAc,CACrB,KAAyB,EACzB,IAAY,EACZ,IAAgC;IAEhC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI,2BAA2B,KAAK,GAAG,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,SAAS,CAAC,KAAyB;IAC1C,MAAM,MAAM,GAAG,KAAK;QAClB,EAAE,KAAK,CAAC,GAAG,CAAC;SACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,OAAO,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7C,CAAC;AAED,SAAgB,4BAA4B,CAC1C,MAAe,EACf,YAA8C;IAE9C,MAAM;SACH,OAAO,CAAC,gBAAgB,CAAC;SACzB,WAAW,CACV,iJAAiJ,CAClJ;SACA,QAAQ,CACP,UAAU,EACV,yFAAyF,CAC1F;SACA,MAAM,CAAC,eAAe,EAAE,wCAAwC,EAAE,cAAc,CAAC;SACjF,MAAM,CAAC,4BAA4B,EAAE,gCAAgC,CAAC;SACtE,MAAM,CAAC,iBAAiB,EAAE,4DAA4D,CAAC;SACvF,MAAM,CAAC,0BAA0B,EAAE,gDAAgD,CAAC;SACpF,MAAM,CAAC,mBAAmB,EAAE,oBAAoB,CAAC;SACjD,MAAM,CAAC,mBAAmB,EAAE,oBAAoB,CAAC;SACjD,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAAC;SACxC,MAAM,CACL,wBAAwB,EACxB,2DAA2D,CAC5D;SACA,MAAM,CAAC,QAAQ,EAAE,4CAA4C,CAAC;SAC9D,MAAM,CACL,wBAAwB,EACxB,2DAA2D,CAC5D;SACA,MAAM,CACL,sBAAsB,EACtB,sDAAsD,CACvD;SACA,MAAM,CAAC,eAAe,EAAE,oDAAoD,CAAC;SAC7E,MAAM,CAAC,iBAAiB,EAAE,4DAA4D,CAAC;SACvF,MAAM,CAAC,wBAAwB,EAAE,8CAA8C,CAAC;SAChF,MAAM,CAAC,YAAY,EAAE,sDAAsD,CAAC;SAC5E,MAAM,CAAC,iBAAiB,EAAE,4CAA4C,CAAC;SACvE,MAAM,CAAC,wBAAwB,EAAE,mDAAmD,CAAC;SACrF,MAAM,CAAC,aAAa,EAAE,wCAAwC,CAAC;SAC/D,MAAM,CAAC,aAAa,EAAE,4CAA4C,CAAC;SACnE,MAAM,CAAC,iBAAiB,EAAE,2CAA2C,CAAC;SACtE,MAAM,CACL,wCAAwC,EACxC,2EAA2E,CAC5E;SACA,MAAM,CAAC,iBAAiB,EAAE,4CAA4C,CAAC;SACvE,MAAM,CAAC,KAAK,EAAE,MAA0B,EAAE,IAA0B,EAAE,EAAE;QACvE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAwB,CAAC,EAAE,CAAC;YACzD,YAAY,CAAC,IAAI,CAAC,oBAAoB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACtF,CAAC;QACD,MAAM,eAAe,GAAG,cAAc,CACpC,IAAI,CAAC,eAAe,EACpB,oBAAoB,EACpB,YAAY,CAAC,IAAI,CAClB,CAAC;QACF,MAAM,eAAe,GAAG,cAAc,CACpC,IAAI,CAAC,eAAe,EACpB,oBAAoB,EACpB,YAAY,CAAC,IAAI,CAClB,CAAC;QACF,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;QACpE,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;QACnF,MAAM,eAAe,GAAG,cAAc,CACpC,IAAI,CAAC,eAAe,EACpB,oBAAoB,EACpB,YAAY,CAAC,IAAI,CAClB,CAAC;QACF,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;QACvE,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;QACvE,MAAM,aAAa,GACjB,SAAS,KAAK,SAAS;YACvB,eAAe,KAAK,SAAS;YAC7B,KAAK,KAAK,SAAS;YACnB,KAAK,KAAK,SAAS;YACnB,IAAI,CAAC,YAAY,KAAK,SAAS;YAC7B,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;QAEpF,MAAM,KAAK,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,aAAa,CAC1C,KAAK,EACL;gBACE,MAAM;gBACN,SAAS,EAAE,IAAI,CAAC,IAAwB;gBACxC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;gBACvC,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,YAAY,EAAE,IAAI,CAAC,YAAY;gBAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,eAAe;gBACf,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,eAAe;gBACf,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;gBACzC,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,YAAY,EAAE,IAAI,CAAC,YAAY;gBAC/B,IAAI;gBACJ,aAAa;gBACb,0BAA0B,EAAE,SAAS,CAAC,IAAI,CAAC,0BAA0B,CAAC;aACvE,EACD,EAAE,SAAS,EAAE,oCAA2B,EAAE,CAC3C,CAAC;YAEF,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,KAAK,WAAW,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;gBAC3D,MAAM,YAAY,CAAC,kBAAkB,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE;oBAC5D,MAAM,EAAE,KAAK,CAAC,MAAM;iBACrB,CAAC,CAAC;gBACH,YAAY,CAAC,SAAS,CAAC,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC3D,OAAO;YACT,CAAC;YACD,YAAY,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAY,CAAC,IAAI,CACf,GAAG,YAAY,cAAQ;gBACrB,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE;gBACd,CAAC,CAAC,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC5D,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
1
+ {"version":3,"file":"audioCommand.js","sourceRoot":"","sources":["../src/audioCommand.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -299,7 +299,25 @@ program
299
299
  .argument('[directory]', 'bundle directory', '.')
300
300
  .option('--thumbnail <file>', 'cover image (png, jpg/jpeg, webp, avif, or gif); otherwise uses thumbnail.<ext> at the bundle root')
301
301
  .option('--skip-subpath-check', 'skip the pre-publish sub-path load check (not recommended: it is the only gate that serves the bundle the way production does)')
302
+ .option('--description <text>', `catalog description, max ${lib_1.WORLD_DESCRIPTION_MAX} chars (pass "" to clear). helix.json cannot carry this — the manifest schema forbids unknown fields`)
303
+ .option('--description-file <path>', 'read the catalog description from a file')
302
304
  .action(async (directory, opts) => {
305
+ // Pure input checks first — bad flags should fail before auth, the network, or any upload.
306
+ if (opts.description !== undefined && opts.descriptionFile) {
307
+ fail('✖ Pass --description or --description-file, not both');
308
+ }
309
+ let description = opts.description;
310
+ if (opts.descriptionFile) {
311
+ try {
312
+ description = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(opts.descriptionFile), 'utf8').trim();
313
+ }
314
+ catch (err) {
315
+ fail(`✖ could not read --description-file: ${err instanceof Error ? err.message : String(err)}`);
316
+ }
317
+ }
318
+ if (description !== undefined && description.length > lib_1.WORLD_DESCRIPTION_MAX) {
319
+ fail(`✖ description is ${description.length} chars; the limit is ${lib_1.WORLD_DESCRIPTION_MAX}. Nothing was published.`);
320
+ }
303
321
  const creds = requireAuth();
304
322
  const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
305
323
  if (mismatch)
@@ -325,7 +343,10 @@ program
325
343
  }
326
344
  }
327
345
  try {
328
- const result = await (0, lib_1.publishWorld)((0, node_path_1.resolve)(directory), creds, (msg) => console.log(msg), { thumbnail: opts.thumbnail ? (0, node_path_1.resolve)(opts.thumbnail) : undefined });
346
+ const result = await (0, lib_1.publishWorld)((0, node_path_1.resolve)(directory), creds, (msg) => console.log(msg), {
347
+ thumbnail: opts.thumbnail ? (0, node_path_1.resolve)(opts.thumbnail) : undefined,
348
+ description,
349
+ });
329
350
  console.log(`✔ Published "${result.title}" build ${result.buildNumber}`);
330
351
  if (result.playUrl)
331
352
  console.log(` play: ${result.playUrl}`);
@@ -816,11 +837,14 @@ function buildQuoteInput(kind, prompt, opts) {
816
837
  * A quote failure never blocks the generation (the server prices it anyway); it warns, because a
817
838
  * hard dependency on an advisory call would make the estimate a new way to fail.
818
839
  */
819
- async function showEstimateBeforeSpending(creds, kind, prompt, opts) {
840
+ async function showEstimateBeforeSpending(creds,
841
+ // The EXACT body the generation call will send. Passing the real request (rather than rebuilding
842
+ // an approximation of it) is what stops the quoted price and the charged price drifting apart.
843
+ input, opts) {
820
844
  if (opts.estimate === false)
821
845
  return;
822
846
  try {
823
- const quote = await (0, lib_1.quoteGeneration)(creds, buildQuoteInput(kind, prompt, opts));
847
+ const quote = await (0, lib_1.quoteGeneration)(creds, input);
824
848
  const balance = await (0, lib_1.getLixBalance)(creds).catch(() => null);
825
849
  console.log((0, lib_1.formatQuote)(quote, balance));
826
850
  const maxLix = optionalNumber(typeof opts.maxLix === 'string' ? opts.maxLix : undefined, '--max-lix');
@@ -1140,11 +1164,14 @@ assets
1140
1164
  fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
1141
1165
  }
1142
1166
  });
1143
- assets
1167
+ const assetsStart = assets
1144
1168
  .command('start')
1145
- .description('Start one generation job without waiting; prop/character use Dreamer, other supported kinds use the shared asset pipeline')
1169
+ .description('Start one generation job without waiting; prop/character use Dreamer, other supported kinds use the shared asset pipeline. For audio, --mode selects sound_effect (default), music, or text_to_speech.')
1146
1170
  .argument('<kind>', 'prop, character, image, material, audio, gaussian_splat, or animation (explicit unavailable stub)')
1147
- .argument('<prompt>', 'asset prompt')
1171
+ // Optional at the parser level ON PURPOSE: text_to_speech takes its content from --text, and a
1172
+ // commander-level "missing required argument 'prompt'" would be actively misleading there.
1173
+ // Required-ness is enforced below, per kind, with a message that names the right flag.
1174
+ .argument('[prompt]', 'asset prompt (sound_effect/music/other kinds; speech uses --text)')
1148
1175
  .option('--title <title>', 'asset title')
1149
1176
  .option('--additional-prompt <text>', 'additional generation guidance')
1150
1177
  .option('--target-polycount <n>', 'prop/character triangle target')
@@ -1156,7 +1183,11 @@ assets
1156
1183
  .option('--repeat-per-meter <n>', 'material physical scale: tile repeats per metre')
1157
1184
  .option('--splat-scope <scope>', 'Gaussian-splat scope: object or environment', 'environment')
1158
1185
  .option('--max-lix <n>', 'refuse to start if the estimate exceeds this many LIX')
1159
- .option('--no-estimate', 'skip the up-front cost estimate')
1186
+ .option('--no-estimate', 'skip the up-front cost estimate');
1187
+ // The audio surface, attached from the SAME definition `generate-audio` uses (minus the flags
1188
+ // `start` already declares for every kind), so the durable route reaches music and speech with
1189
+ // identical flag names instead of only ever producing a sound effect.
1190
+ (0, audioCommand_1.addAudioOptions)(assetsStart, { skipShared: true })
1160
1191
  .action(async (kind, prompt, opts) => {
1161
1192
  const supported = [
1162
1193
  'prop',
@@ -1170,6 +1201,13 @@ assets
1170
1201
  if (!supported.includes(kind)) {
1171
1202
  fail(`✖ kind must be ${supported.join(', ')}; got '${kind}'`);
1172
1203
  }
1204
+ // Every kind except audio takes its subject from the prompt argument.
1205
+ if (kind !== 'audio' && !prompt?.trim()) {
1206
+ fail(`✖ ${kind} needs a prompt argument describing what to generate`);
1207
+ }
1208
+ // Past the guard above, only the audio path can still have an absent prompt, and that path
1209
+ // never reads this binding (speech uses --text; sound_effect/music are checked separately).
1210
+ const promptText = prompt ?? '';
1173
1211
  const resolution = optionalNumber(opts.resolution, '--resolution');
1174
1212
  if (resolution !== 1024 && resolution !== 2048) {
1175
1213
  fail('✖ --resolution must be 1024 or 2048');
@@ -1178,22 +1216,70 @@ assets
1178
1216
  !['object', 'environment'].includes(opts.splatScope)) {
1179
1217
  fail('✖ --splat-scope must be object or environment');
1180
1218
  }
1219
+ // Audio is the one kind whose request shape is mode-dependent, so build and check it before
1220
+ // anything else. Until now `start` accepted no audio flags at all, which meant the durable
1221
+ // route agents are told to prefer could only ever emit a sound effect: music and speech were
1222
+ // reachable only by blocking the call, with no job id to poll.
1223
+ let audioInput = null;
1224
+ if (kind === 'audio') {
1225
+ const problems = (0, audioCommand_1.validateAudioRequest)(prompt, opts);
1226
+ if (problems.length) {
1227
+ fail(`✖ ${problems.join('\n ✖ ')}`);
1228
+ }
1229
+ audioInput = {
1230
+ kind,
1231
+ ...(0, audioCommand_1.buildAudioInput)(prompt, opts, fail),
1232
+ };
1233
+ }
1181
1234
  const creds = requireAuth();
1182
- await showEstimateBeforeSpending(creds, kind, prompt, opts);
1235
+ // A voice id that is not in the catalog fails HERE, before the quote and before any LIX
1236
+ // moves. A lookup that cannot run only warns — an advisory check must not become a new way
1237
+ // to block a valid job.
1238
+ if (audioInput && typeof audioInput.voiceId === 'string') {
1239
+ const voiceId = audioInput.voiceId;
1240
+ try {
1241
+ const known = await (0, lib_1.listDreamerVoices)(creds, { search: voiceId, pageSize: 100 });
1242
+ const match = known.voices.find((v) => v.id === voiceId);
1243
+ if (!match) {
1244
+ const nearby = known.voices.slice(0, 5).map((v) => `${v.id} (${v.name})`);
1245
+ fail(`✖ No voice "${voiceId}" in the catalog. Nothing was charged.\n` +
1246
+ ` List them with: helix assets voices\n` +
1247
+ (nearby.length ? ` Close matches: ${nearby.join(', ')}` : ''));
1248
+ }
1249
+ const languageCode = audioInput.languageCode;
1250
+ if (typeof languageCode === 'string' &&
1251
+ match.languages.length &&
1252
+ !match.languages.some((l) => l.code === languageCode)) {
1253
+ fail(`✖ Voice "${match.name}" does not support language "${languageCode}". Nothing was charged.\n` +
1254
+ ` It supports: ${match.languages.map((l) => l.code).join(', ')}`);
1255
+ }
1256
+ }
1257
+ catch (err) {
1258
+ if (err instanceof lib_1.ApiError) {
1259
+ console.warn(` • could not verify the voice id against the catalog: ${err.message}`);
1260
+ }
1261
+ else {
1262
+ throw err;
1263
+ }
1264
+ }
1265
+ }
1266
+ const generationInput = audioInput ?? buildQuoteInput(kind, promptText, opts);
1267
+ await showEstimateBeforeSpending(creds, generationInput, opts);
1183
1268
  try {
1184
1269
  if (kind === 'prop' || kind === 'character') {
1185
1270
  printJson(await (0, lib_1.startDreamerAssetGeneration)(creds, {
1186
1271
  kind,
1187
- prompt,
1272
+ prompt: promptText,
1188
1273
  title: opts.title,
1189
1274
  additionalPrompt: opts.additionalPrompt,
1190
1275
  targetPolycount: optionalNumber(opts.targetPolycount, '--target-polycount'),
1191
1276
  }));
1192
1277
  return;
1193
1278
  }
1194
- printJson(await (0, lib_1.startStandaloneAssetGeneration)(creds, {
1279
+ // For audio, send the SAME object that was just quoted — not a re-derived one.
1280
+ printJson(await (0, lib_1.startStandaloneAssetGeneration)(creds, audioInput ?? {
1195
1281
  kind: kind,
1196
- prompt,
1282
+ prompt: promptText,
1197
1283
  title: opts.title,
1198
1284
  additionalPrompt: opts.additionalPrompt,
1199
1285
  projectId: opts.projectId,
@@ -1210,6 +1296,36 @@ assets
1210
1296
  }
1211
1297
  });
1212
1298
  // ---- Money: what you have, and what a job will cost BEFORE it runs ---------
1299
+ program
1300
+ .command('jobs')
1301
+ .description('List your generation jobs with what each one cost — the answer to "what was I charged for?" after a failure')
1302
+ .option('--state <state>', 'all, unfinished, failed, published, or awaiting_approval', 'all')
1303
+ .option('--limit <n>', 'how many to show', '20')
1304
+ .option('--offset <n>', 'skip this many')
1305
+ .option('--json', 'emit the raw rows')
1306
+ .action(async (opts) => {
1307
+ const states = ['all', 'unfinished', 'failed', 'published', 'awaiting_approval'];
1308
+ if (opts.state && !states.includes(opts.state)) {
1309
+ fail(`✖ --state must be ${states.join(', ')}; got '${opts.state}'`);
1310
+ }
1311
+ const creds = requireAuth();
1312
+ try {
1313
+ const result = await (0, lib_1.listJobs)(creds, {
1314
+ state: opts.state,
1315
+ limit: optionalNumber(opts.limit, '--limit'),
1316
+ offset: optionalNumber(opts.offset, '--offset'),
1317
+ });
1318
+ if (opts.json)
1319
+ printJson(result);
1320
+ else
1321
+ console.log((0, lib_1.formatJobs)(result));
1322
+ }
1323
+ catch (err) {
1324
+ fail(err instanceof lib_1.ApiError
1325
+ ? err.render()
1326
+ : `✖ ${err instanceof Error ? err.message : String(err)}`);
1327
+ }
1328
+ });
1213
1329
  program
1214
1330
  .command('balance')
1215
1331
  .description('Show your LIX balance: available, held by open jobs, and total')
@@ -1412,7 +1528,7 @@ assets
1412
1528
  fail(`✖ --visibility must be public, unlisted, or private`);
1413
1529
  }
1414
1530
  const creds = requireAuth();
1415
- await showEstimateBeforeSpending(creds, kind, prompt, opts);
1531
+ await showEstimateBeforeSpending(creds, buildQuoteInput(kind, prompt, opts), opts);
1416
1532
  try {
1417
1533
  printJson(await (0, lib_1.generateAndPublishAsset)(creds, {
1418
1534
  kind,