@flatkey-ai/cli 0.1.4 → 0.1.5

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.
@@ -0,0 +1,50 @@
1
+ name: Bug report
2
+ description: Report a Flatkey CLI bug.
3
+ title: "[Bug]: "
4
+ labels: ["bug"]
5
+ body:
6
+ - type: textarea
7
+ id: summary
8
+ attributes:
9
+ label: Summary
10
+ description: What happened?
11
+ validations:
12
+ required: true
13
+ - type: textarea
14
+ id: command
15
+ attributes:
16
+ label: Command
17
+ description: Paste the command. Redact API keys.
18
+ render: bash
19
+ validations:
20
+ required: true
21
+ - type: textarea
22
+ id: output
23
+ attributes:
24
+ label: Output
25
+ description: Paste relevant stdout/stderr. Redact secrets.
26
+ render: text
27
+ - type: input
28
+ id: version
29
+ attributes:
30
+ label: Version
31
+ description: Run `flatkey --version`.
32
+ placeholder: "0.1.4"
33
+ - type: dropdown
34
+ id: os
35
+ attributes:
36
+ label: OS
37
+ options:
38
+ - macOS
39
+ - Linux
40
+ - Windows
41
+ - Other
42
+ - type: checkboxes
43
+ id: checks
44
+ attributes:
45
+ label: Checks
46
+ options:
47
+ - label: I removed API keys and secrets from this report.
48
+ required: true
49
+ - label: I tried the command again with `--dry-run` when applicable.
50
+ required: false
@@ -0,0 +1,34 @@
1
+ name: Feature request
2
+ description: Request a Flatkey CLI feature.
3
+ title: "[Feature]: "
4
+ labels: ["enhancement"]
5
+ body:
6
+ - type: textarea
7
+ id: problem
8
+ attributes:
9
+ label: Problem
10
+ description: What workflow should be easier?
11
+ validations:
12
+ required: true
13
+ - type: textarea
14
+ id: proposal
15
+ attributes:
16
+ label: Proposal
17
+ description: What command or behavior do you want?
18
+ render: bash
19
+ validations:
20
+ required: true
21
+ - type: dropdown
22
+ id: area
23
+ attributes:
24
+ label: Area
25
+ options:
26
+ - image
27
+ - video
28
+ - audio
29
+ - text
30
+ - models
31
+ - credits/status
32
+ - packaging
33
+ - docs
34
+ - other
@@ -0,0 +1,13 @@
1
+ ## Summary
2
+
3
+ -
4
+
5
+ ## Test
6
+
7
+ ```bash
8
+ npm test
9
+ ```
10
+
11
+ ## Notes
12
+
13
+ - No API keys or generated media committed.
@@ -0,0 +1,37 @@
1
+ # Contributing
2
+
3
+ Thanks for improving Flatkey CLI.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/flatkey-ai/flatkey-cli.git
9
+ cd flatkey-cli
10
+ npm install
11
+ npm test
12
+ ```
13
+
14
+ ## Local CLI
15
+
16
+ ```bash
17
+ node bin/flatkey.js --version
18
+ node bin/flatkey.js help --ai
19
+ node bin/flatkey.js image generate --prompt "test" --dry-run --json
20
+ ```
21
+
22
+ ## Tests
23
+
24
+ Run all tests before opening a PR:
25
+
26
+ ```bash
27
+ npm test
28
+ ```
29
+
30
+ Live tests require `FLATKEY_API_KEY` and can spend credits. Keep routine tests mocked or `--dry-run`.
31
+
32
+ ## Pull Requests
33
+
34
+ - Keep changes small and focused.
35
+ - Add or update tests for behavior changes.
36
+ - Do not commit API keys, generated media, or local config.
37
+ - Use `--dry-run` examples when possible.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flatkey AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,33 @@
1
- # @flatkey-ai/cli
1
+ # Flatkey CLI
2
2
 
3
- Unified Flatkey CLI for media workers. Generate images, videos, and audio through Flatkey credits.
3
+ [![npm version](https://img.shields.io/npm/v/@flatkey-ai/cli.svg)](https://www.npmjs.com/package/@flatkey-ai/cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@flatkey-ai/cli.svg)](https://www.npmjs.com/package/@flatkey-ai/cli)
5
+ [![Publish npm](https://github.com/flatkey-ai/flatkey-cli/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/flatkey-ai/flatkey-cli/actions/workflows/npm-publish.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ Creative generation from the terminal for media teams and AI agents. Generate images, videos, audio, and text through one Flatkey credit balance.
9
+
10
+ Website: [flatkey.ai](https://flatkey.ai/?utm_source=github&utm_medium=readme&utm_campaign=flatkey_cli)
11
+
12
+ ```bash
13
+ npm install -g @flatkey-ai/cli
14
+ export FLATKEY_API_KEY=<your-flatkey-api-key>
15
+
16
+ flatkey image generate --prompt "editorial cover, neon city at dawn" -o cover.png
17
+ flatkey video generate --prompt "cinematic product launch clip" --model seedance2 -o launch.mp4
18
+ flatkey audio generate --prompt "你好,这是 Flatkey 的配音测试" -o speech.mp3
19
+ flatkey models --json
20
+ flatkey credits --json
21
+ ```
22
+
23
+ ## Why Flatkey CLI
24
+
25
+ - One CLI for image, video, audio, and text generation.
26
+ - One `FLATKEY_API_KEY`, one credit balance.
27
+ - Agent-friendly JSON mode with clean stdout.
28
+ - Local output support with `--output` / `-o`.
29
+ - Cross-platform npm package for macOS, Linux, and Windows.
30
+ - Cool terminal progress animation for human runs; disabled in `--json` mode.
4
31
 
5
32
  ## Install
6
33
 
@@ -8,46 +35,195 @@ Unified Flatkey CLI for media workers. Generate images, videos, and audio throug
8
35
  npm install -g @flatkey-ai/cli
9
36
  ```
10
37
 
11
- Or run without installing:
38
+ Run without installing:
12
39
 
13
40
  ```bash
14
41
  npx @flatkey-ai/cli help --ai
15
42
  ```
16
43
 
17
- ## Auth
44
+ Check local version:
18
45
 
19
46
  ```bash
20
- flatkey onboard --api-key <key>
47
+ flatkey --version
48
+ flatkey -v
21
49
  ```
22
50
 
23
- Or use:
51
+ ## Auth
52
+
53
+ Use an environment variable:
24
54
 
25
55
  ```bash
26
56
  export FLATKEY_API_KEY=<key>
27
57
  ```
28
58
 
59
+ Or save it locally:
60
+
61
+ ```bash
62
+ flatkey onboard --api-key <key>
63
+ ```
64
+
65
+ Get a key from [flatkey.ai](https://flatkey.ai/?utm_source=github&utm_medium=readme&utm_campaign=flatkey_cli_auth).
66
+
29
67
  ## Commands
30
68
 
69
+ ### Generate Image
70
+
71
+ ```bash
72
+ flatkey image generate \
73
+ --prompt "magazine cover, reflective typography, studio lighting" \
74
+ --model gpt-image-2 \
75
+ -o cover.png
76
+ ```
77
+
78
+ ### Generate Video
79
+
80
+ ```bash
81
+ flatkey video generate \
82
+ --prompt "8 second cinematic product reveal, glossy black background" \
83
+ --model seedance2 \
84
+ -o launch.mp4
85
+ ```
86
+
87
+ ### Generate Audio
88
+
89
+ Text to speech:
90
+
91
+ ```bash
92
+ flatkey audio generate \
93
+ --prompt "你好,这是 Flatkey 网关的语音测试。" \
94
+ --voice-id EXAVITQu4vr4xnSDxMaL \
95
+ --model eleven_multilingual_v2 \
96
+ --stability 0.5 \
97
+ --similarity-boost 0.75 \
98
+ --style 0 \
99
+ -o speech.mp3
100
+ ```
101
+
102
+ List voices:
103
+
104
+ ```bash
105
+ flatkey audio voices --json
106
+ ```
107
+
108
+ Sound effects:
109
+
110
+ ```bash
111
+ flatkey audio sfx \
112
+ --prompt "glass shattering on the floor" \
113
+ --duration 3 \
114
+ -o sfx.mp3
115
+ ```
116
+
117
+ Music:
118
+
119
+ ```bash
120
+ flatkey audio music \
121
+ --prompt "calm ambient piano, sad mood" \
122
+ --music-length-ms 10000 \
123
+ -o music.mp3
124
+ ```
125
+
126
+ ### Generate Text
127
+
128
+ ```bash
129
+ flatkey text generate \
130
+ --prompt "write 5 sharp headlines for a creator tool launch" \
131
+ --model gpt-5.5 \
132
+ -o headlines.txt
133
+ ```
134
+
135
+ ### Credits, Status, Models
136
+
31
137
  ```bash
32
- flatkey image generate --prompt "magazine cover" --json
33
- flatkey image generate --prompt "magazine cover" --output cover.png --json
34
- flatkey video generate --prompt "product launch clip" --json
35
- flatkey audio generate --prompt "30 second voiceover" --json
36
- flatkey text generate --prompt "write a headline" --model gpt-5.5 --json
37
138
  flatkey credits --json
38
139
  flatkey status --json
39
140
  flatkey models --json
141
+ flatkey models --type image --json
142
+ flatkey help --ai
143
+ ```
144
+
145
+ ## Models
146
+
147
+ Live model list:
148
+
149
+ ```bash
150
+ flatkey models --json
151
+ ```
152
+
153
+ Useful current defaults:
154
+
155
+ | Type | Models |
156
+ | --- | --- |
157
+ | Image | `gpt-image-2`, `nano-banana-pro-preview`, Gemini image models |
158
+ | Video | `seedance2`, `veo-3`, `veo-3-fast` |
159
+ | Audio | `eleven_multilingual_v2`, ElevenLabs voices, sound effects, music |
160
+ | Text | `gpt-5.5`, Claude, Gemini, GLM, Grok models |
161
+
162
+ `models` reads from the Flatkey console model registry. Generation calls use the Flatkey router.
163
+
164
+ ## Agent Protocol
165
+
166
+ Use this when wiring Flatkey into Codex, Claude Code, Cursor, or other agents:
167
+
168
+ ```bash
40
169
  flatkey help --ai
41
170
  ```
42
171
 
43
- Default router: `https://router.flatkey.ai`.
172
+ Agent rules:
173
+
174
+ - Prefer `FLATKEY_API_KEY`.
175
+ - Always pass `--json` for machine-readable output.
176
+ - Use `--output` / `-o` when the generated file path matters.
177
+ - Call `flatkey models --json` before choosing a model.
178
+ - Call `flatkey audio voices --json` before choosing a TTS `voice_id`.
179
+ - Use `--dry-run` to inspect request shape without spending credits.
180
+
181
+ Example dry run:
182
+
183
+ ```bash
184
+ flatkey video generate \
185
+ --prompt "fashion campaign hero shot" \
186
+ --model seedance2 \
187
+ --dry-run \
188
+ --json
189
+ ```
190
+
191
+ ## Routing
192
+
193
+ - Generation router: `https://router.flatkey.ai`
194
+ - Model registry: `https://console.flatkey.ai/v1/available_models`
195
+ - Voice registry: `https://router.flatkey.ai/v1/voices`
196
+
197
+ Override router only when developing or testing:
198
+
199
+ ```bash
200
+ flatkey image generate --prompt "test" --base-url http://127.0.0.1:3000 --json
201
+ ```
44
202
 
45
203
  ## Release Channels
46
204
 
47
- Primary release is npm package `@flatkey-ai/cli`.
205
+ Primary release:
206
+
207
+ - npm package: [`@flatkey-ai/cli`](https://www.npmjs.com/package/@flatkey-ai/cli)
48
208
 
49
209
  Planned wrappers:
50
210
 
51
211
  - Homebrew formula.
52
212
  - Debian `.deb` package for apt-based install.
53
213
  - Windows MSI package.
214
+
215
+ ## Development
216
+
217
+ ```bash
218
+ git clone https://github.com/flatkey-ai/flatkey-cli.git
219
+ cd flatkey-cli
220
+ npm install
221
+ npm test
222
+ node bin/flatkey.js --version
223
+ ```
224
+
225
+ ## Links
226
+
227
+ - Website: [flatkey.ai](https://flatkey.ai/?utm_source=github&utm_medium=readme&utm_campaign=flatkey_cli_links)
228
+ - npm: [`@flatkey-ai/cli`](https://www.npmjs.com/package/@flatkey-ai/cli)
229
+ - Issues: [github.com/flatkey-ai/flatkey-cli/issues](https://github.com/flatkey-ai/flatkey-cli/issues)
package/SECURITY.md ADDED
@@ -0,0 +1,13 @@
1
+ # Security
2
+
3
+ ## Reporting
4
+
5
+ Please do not open public issues for security vulnerabilities.
6
+
7
+ Report security concerns through Flatkey support at [flatkey.ai](https://flatkey.ai/?utm_source=github&utm_medium=security&utm_campaign=flatkey_cli).
8
+
9
+ ## API Keys
10
+
11
+ - Prefer `FLATKEY_API_KEY` for automation.
12
+ - Never commit API keys.
13
+ - Rotate keys if they appear in logs, screenshots, shell history, or public CI.
@@ -0,0 +1 @@
1
+ audio-file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Flatkey media generation CLI for image, video, audio, credits, status, and models.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -53,18 +53,52 @@ export function planVideoRequest(options) {
53
53
  }
54
54
 
55
55
  export function generateAudio(options) {
56
- return requestJsonFromPlan(options, planAudioRequest(options));
56
+ return requestBinaryArtifactFromPlan(options, planAudioRequest(options));
57
57
  }
58
58
 
59
59
  export function planAudioRequest(options) {
60
- return planJsonPost(options, "/v1/audio/generations", cleanObject({
61
- model: options.model ?? "tts-1",
60
+ const voiceId = optionValue(options, "voiceId", "voice_id") ?? "EXAVITQu4vr4xnSDxMaL";
61
+ return planJsonPost(options, `/v1/text-to-speech/${encodeURIComponent(voiceId)}`, cleanObject({
62
+ text: options.text ?? options.prompt,
63
+ model_id: options.model ?? options.model_id ?? "eleven_multilingual_v2",
64
+ voice_settings: cleanObject({
65
+ stability: parseOptionalFloat(options.stability),
66
+ similarity_boost: parseOptionalFloat(optionValue(options, "similarityBoost", "similarity_boost")),
67
+ style: parseOptionalFloat(options.style),
68
+ }),
69
+ }));
70
+ }
71
+
72
+ export function generateAudioSfx(options) {
73
+ return requestBinaryArtifactFromPlan(options, planAudioSfxRequest(options));
74
+ }
75
+
76
+ export function planAudioSfxRequest(options) {
77
+ return planJsonPost(options, "/v1/sound-generation", cleanObject({
78
+ text: options.text ?? options.prompt,
79
+ duration_seconds: parseOptionalFloat(optionValue(options, "durationSeconds", "duration_seconds", "duration")),
80
+ }));
81
+ }
82
+
83
+ export function generateAudioMusic(options) {
84
+ return requestBinaryArtifactFromPlan(options, planAudioMusicRequest(options));
85
+ }
86
+
87
+ export function planAudioMusicRequest(options) {
88
+ return planJsonPost(options, "/v1/music", cleanObject({
62
89
  prompt: options.prompt,
63
- voice: options.voice,
64
- format: options.format,
90
+ music_length_ms: parseOptionalInteger(optionValue(options, "musicLengthMs", "music_length_ms")),
65
91
  }));
66
92
  }
67
93
 
94
+ export function getVoices(options) {
95
+ return requestJson(options, "/v1/voices");
96
+ }
97
+
98
+ export function planVoicesRequest(options) {
99
+ return planRequest(options, "/v1/voices");
100
+ }
101
+
68
102
  export function generateText(options) {
69
103
  return requestJsonFromPlan(options, planTextRequest(options));
70
104
  }
@@ -128,6 +162,24 @@ async function requestJsonFromPlan(options, plan) {
128
162
  return body;
129
163
  }
130
164
 
165
+ async function requestBinaryArtifactFromPlan(options, plan) {
166
+ const fetchImpl = options.fetch ?? fetch;
167
+ const response = await fetchImpl(plan.url, {
168
+ method: plan.method,
169
+ headers: plan.headers,
170
+ body: plan.body === undefined ? undefined : JSON.stringify(plan.body),
171
+ });
172
+ if (!response.ok) {
173
+ const body = await readJson(response);
174
+ throw new FlatkeyError(extractErrorMessage(body, response.status), {
175
+ status: response.status,
176
+ });
177
+ }
178
+ return {
179
+ data: [{ data: Buffer.from(await response.arrayBuffer()).toString("base64") }],
180
+ };
181
+ }
182
+
131
183
  async function requestJson(options, path, init = {}) {
132
184
  return requestJsonFromPlan(options, planRequest(options, path, {
133
185
  method: init.method ?? "GET",
@@ -159,6 +211,18 @@ function parseOptionalInteger(value) {
159
211
  return Number.parseInt(value, 10);
160
212
  }
161
213
 
214
+ function parseOptionalFloat(value) {
215
+ if (value === undefined) return undefined;
216
+ return Number.parseFloat(value);
217
+ }
218
+
219
+ function optionValue(options, ...keys) {
220
+ for (const key of keys) {
221
+ if (options[key] !== undefined) return options[key];
222
+ }
223
+ return undefined;
224
+ }
225
+
162
226
  function cleanObject(value) {
163
227
  return Object.fromEntries(
164
228
  Object.entries(value).filter(([, entry]) => entry !== undefined),
package/src/cli.js CHANGED
@@ -103,7 +103,13 @@ export async function runCommand(command, deps = {}) {
103
103
  }
104
104
 
105
105
  if (["image", "video", "audio", "text"].includes(command.group)) {
106
- if (command.action !== "generate") {
106
+ if (command.group === "audio" && command.action === "voices") {
107
+ return handleVoices(command, deps);
108
+ }
109
+ const validAction = command.group === "audio"
110
+ ? ["generate", "sfx", "music"].includes(command.action)
111
+ : command.action === "generate";
112
+ if (!validAction) {
107
113
  throw new Error(`Unknown action for ${command.group}: ${command.action}`);
108
114
  }
109
115
  return handleGenerate(command, { ...deps, stdout, stderr });
@@ -116,10 +122,14 @@ async function handleGenerate(command, deps) {
116
122
  const { resolveApiKey } = await import("./config.js");
117
123
  const {
118
124
  generateAudio,
125
+ generateAudioMusic,
126
+ generateAudioSfx,
119
127
  generateImage,
120
128
  generateText,
121
129
  generateVideo,
130
+ planAudioMusicRequest,
122
131
  planAudioRequest,
132
+ planAudioSfxRequest,
123
133
  planImageRequest,
124
134
  planTextRequest,
125
135
  planVideoRequest,
@@ -144,7 +154,11 @@ async function handleGenerate(command, deps) {
144
154
  : command.group === "video"
145
155
  ? planVideoRequest(options)
146
156
  : command.group === "audio"
147
- ? planAudioRequest(options)
157
+ ? command.action === "sfx"
158
+ ? planAudioSfxRequest(options)
159
+ : command.action === "music"
160
+ ? planAudioMusicRequest(options)
161
+ : planAudioRequest(options)
148
162
  : planTextRequest(options);
149
163
  return { dryRun: true, kind: command.group, request: redactRequest(request) };
150
164
  }
@@ -159,7 +173,11 @@ async function handleGenerate(command, deps) {
159
173
  : command.group === "video"
160
174
  ? await generateVideo(options)
161
175
  : command.group === "audio"
162
- ? await generateAudio(options)
176
+ ? command.action === "sfx"
177
+ ? await generateAudioSfx(options)
178
+ : command.action === "music"
179
+ ? await generateAudioMusic(options)
180
+ : await generateAudio(options)
163
181
  : await generateText(options);
164
182
  if (command.group === "text") {
165
183
  const text = extractText(response);
@@ -179,6 +197,20 @@ async function handleGenerate(command, deps) {
179
197
  }
180
198
  }
181
199
 
200
+ async function handleVoices(command, deps) {
201
+ const { resolveApiKey } = await import("./config.js");
202
+ const { getVoices } = await import("./api.js");
203
+ const apiKey = await resolveApiKey({
204
+ apiKey: command.options.api_key,
205
+ env: deps.env ?? process.env,
206
+ });
207
+ return getVoices({
208
+ apiKey,
209
+ baseUrl: command.options.base_url,
210
+ fetch: deps.fetch,
211
+ });
212
+ }
213
+
182
214
  function extractText(response) {
183
215
  return response?.choices?.[0]?.message?.content
184
216
  ?? response?.output_text
package/src/help.js CHANGED
@@ -14,7 +14,10 @@ JSON mode:
14
14
  Commands:
15
15
  - flatkey image generate --prompt "<prompt>" --json [--model <model>] [--output image.png]
16
16
  - flatkey video generate --prompt "<prompt>" --json [--model seedance2] [--output video.mp4]
17
- - flatkey audio generate --prompt "<prompt>" --json [--model tts-1] [--output audio.mp3]
17
+ - flatkey audio generate --prompt "<text>" --json [--voice-id <voice_id>] [--model eleven_multilingual_v2] [--output speech.mp3]
18
+ - flatkey audio sfx --prompt "<sound>" --json [--duration <seconds>] [--output sfx.mp3]
19
+ - flatkey audio music --prompt "<music prompt>" --json [--music-length-ms <ms>] [--output music.mp3]
20
+ - flatkey audio voices --json
18
21
  - flatkey text generate --prompt "<prompt>" --json [--model gpt-5.5] [--output text.txt]
19
22
  - flatkey credits --json
20
23
  - flatkey status --json
@@ -28,6 +31,7 @@ Environment:
28
31
  Recovery:
29
32
  - Missing key: run flatkey onboard --api-key <key> or set FLATKEY_API_KEY.
30
33
  - Unknown model: run flatkey models --json, then retry with a listed model id.
34
+ - Unknown voice: run flatkey audio voices --json, then retry with a listed voice_id.
31
35
  - API failure: inspect stderr JSON or HTTP message, then retry only after fixing request or credits.
32
36
  `;
33
37
  }
@@ -39,7 +43,10 @@ Commands:
39
43
  onboard --api-key <key> Save Flatkey API key
40
44
  image generate --prompt <txt> Generate image
41
45
  video generate --prompt <txt> Generate video
42
- audio generate --prompt <txt> Generate audio
46
+ audio generate --prompt <txt> Generate speech with ElevenLabs voices
47
+ audio sfx --prompt <txt> Generate sound effects
48
+ audio music --prompt <txt> Generate music
49
+ audio voices List available voices
43
50
  text generate --prompt <txt> Generate text
44
51
  credits Show remaining credits
45
52
  status Show usage/status
package/src/models.js CHANGED
@@ -6,8 +6,11 @@ const BUNDLED_MODELS = [
6
6
  { id: "veo-3-fast", type: "video" },
7
7
  { id: "seedance2", type: "video" },
8
8
  { id: "gpt-5.5", type: "text" },
9
- { id: "tts-1", type: "audio" },
10
- { id: "gpt-4o-mini-tts", type: "audio" },
9
+ { id: "eleven_multilingual_v2", type: "audio" },
10
+ { id: "eleven_turbo_v2_5", type: "audio" },
11
+ { id: "eleven_flash_v2_5", type: "audio" },
12
+ { id: "sound-generation", type: "audio" },
13
+ { id: "music", type: "audio" },
11
14
  ];
12
15
 
13
16
  export function getBundledModels(type) {
package/test/api.test.js CHANGED
@@ -11,7 +11,10 @@ import {
11
11
  getModels,
12
12
  getStatus,
13
13
  planImageRequest,
14
+ planAudioMusicRequest,
14
15
  planTextRequest,
16
+ planAudioSfxRequest,
17
+ planVoicesRequest,
15
18
  planVideoRequest,
16
19
  } from "../src/api.js";
17
20
 
@@ -22,6 +25,9 @@ function fetchRecorder(responseBody = { ok: true }) {
22
25
  return {
23
26
  ok: true,
24
27
  status: 200,
28
+ async arrayBuffer() {
29
+ return Buffer.from("binary");
30
+ },
25
31
  async json() {
26
32
  return responseBody;
27
33
  },
@@ -138,20 +144,64 @@ test("builds audio generation request", async () => {
138
144
  await generateAudio({
139
145
  apiKey: "key",
140
146
  baseUrl: "https://router.test",
141
- model: "tts-1",
147
+ model: "eleven_multilingual_v2",
142
148
  prompt: "voiceover",
143
- voice: "alloy",
144
- format: "mp3",
149
+ voiceId: "voice-123",
150
+ stability: "0.5",
151
+ similarityBoost: "0.75",
152
+ style: "0",
145
153
  fetch,
146
154
  });
147
155
 
148
- assert.equal(calls[0].url, "https://router.test/v1/audio/generations");
156
+ assert.equal(calls[0].url, "https://router.test/v1/text-to-speech/voice-123");
149
157
  assert.deepEqual(JSON.parse(calls[0].init.body), {
150
- model: "tts-1",
151
- prompt: "voiceover",
152
- voice: "alloy",
153
- format: "mp3",
158
+ text: "voiceover",
159
+ model_id: "eleven_multilingual_v2",
160
+ voice_settings: {
161
+ stability: 0.5,
162
+ similarity_boost: 0.75,
163
+ style: 0,
164
+ },
165
+ });
166
+ });
167
+
168
+ test("builds audio sfx, music, and voices requests", () => {
169
+ assert.equal(planAudioSfxRequest({
170
+ apiKey: "key",
171
+ baseUrl: "https://router.test",
172
+ prompt: "glass shattering",
173
+ duration: "3",
174
+ }).url, "https://router.test/v1/sound-generation");
175
+ assert.deepEqual(planAudioSfxRequest({
176
+ apiKey: "key",
177
+ baseUrl: "https://router.test",
178
+ prompt: "glass shattering",
179
+ duration: "3",
180
+ }).body, {
181
+ text: "glass shattering",
182
+ duration_seconds: 3,
154
183
  });
184
+
185
+ assert.equal(planAudioMusicRequest({
186
+ apiKey: "key",
187
+ baseUrl: "https://router.test",
188
+ prompt: "calm ambient piano",
189
+ musicLengthMs: "10000",
190
+ }).url, "https://router.test/v1/music");
191
+ assert.deepEqual(planAudioMusicRequest({
192
+ apiKey: "key",
193
+ baseUrl: "https://router.test",
194
+ prompt: "calm ambient piano",
195
+ musicLengthMs: "10000",
196
+ }).body, {
197
+ prompt: "calm ambient piano",
198
+ music_length_ms: 10000,
199
+ });
200
+
201
+ assert.equal(planVoicesRequest({
202
+ apiKey: "key",
203
+ baseUrl: "https://router.test",
204
+ }).url, "https://router.test/v1/voices");
155
205
  });
156
206
 
157
207
  test("builds credits, status, and models requests", async () => {
package/test/cli.test.js CHANGED
@@ -63,6 +63,44 @@ test("parses output aliases", () => {
63
63
  );
64
64
  });
65
65
 
66
+ test("parses audio voice generation controls", () => {
67
+ const command = parseArgv([
68
+ "audio",
69
+ "generate",
70
+ "--prompt",
71
+ "hello",
72
+ "--voice-id",
73
+ "voice-123",
74
+ "--stability",
75
+ "0.5",
76
+ "--similarity-boost",
77
+ "0.75",
78
+ "--style",
79
+ "0",
80
+ "-o",
81
+ "speech.mp3",
82
+ ]);
83
+
84
+ assert.deepEqual(command, {
85
+ group: "audio",
86
+ action: "generate",
87
+ options: {
88
+ prompt: "hello",
89
+ voice_id: "voice-123",
90
+ stability: "0.5",
91
+ similarity_boost: "0.75",
92
+ style: "0",
93
+ output: "speech.mp3",
94
+ },
95
+ });
96
+ });
97
+
98
+ test("parses audio sfx, music, and voices actions", () => {
99
+ assert.equal(parseArgv(["audio", "sfx", "--prompt", "glass", "--duration", "3"]).action, "sfx");
100
+ assert.equal(parseArgv(["audio", "music", "--prompt", "piano", "--music-length-ms", "10000"]).action, "music");
101
+ assert.equal(parseArgv(["audio", "voices", "--json"]).action, "voices");
102
+ });
103
+
66
104
  test("parses ai help", () => {
67
105
  const command = parseArgv(["help", "--ai"]);
68
106
 
package/test/help.test.js CHANGED
@@ -11,6 +11,9 @@ test("ai help teaches agents setup and command usage", () => {
11
11
  assert.match(help, /flatkey image generate/);
12
12
  assert.match(help, /flatkey video generate/);
13
13
  assert.match(help, /flatkey audio generate/);
14
+ assert.match(help, /flatkey audio sfx/);
15
+ assert.match(help, /flatkey audio music/);
16
+ assert.match(help, /flatkey audio voices/);
14
17
  assert.match(help, /flatkey text generate/);
15
18
  assert.match(help, /flatkey credits --json/);
16
19
  assert.match(help, /flatkey status --json/);
@@ -25,5 +28,6 @@ test("human help lists core commands", () => {
25
28
  assert.match(help, /Usage: flatkey/);
26
29
  assert.match(help, /image generate/);
27
30
  assert.match(help, /models/);
31
+ assert.match(help, /audio voices/);
28
32
  assert.match(help, /text generate/);
29
33
  });
@@ -17,26 +17,36 @@ test("runs generation and utility commands in json mode", async (t) => {
17
17
  });
18
18
  request.on("end", () => {
19
19
  requests.push({ url: request.url, method: request.method, body });
20
- response.setHeader("content-type", "application/json");
21
20
  if (request.url.startsWith("/v1beta/models/")) {
21
+ response.setHeader("content-type", "application/json");
22
22
  response.end(JSON.stringify({ data: [{ url: "https://cdn.test/image.png" }] }));
23
23
  } else if (request.url === "/v1/video/generations") {
24
+ response.setHeader("content-type", "application/json");
24
25
  response.end(JSON.stringify({ data: [{ url: "https://cdn.test/video.mp4" }] }));
25
- } else if (request.url === "/v1/audio/generations") {
26
- response.end(JSON.stringify({ data: [{ url: "https://cdn.test/audio.mp3" }] }));
26
+ } else if (request.url === "/v1/text-to-speech/EXAVITQu4vr4xnSDxMaL") {
27
+ response.setHeader("content-type", "audio/mpeg");
28
+ response.end("audio-file");
27
29
  } else if (request.url === "/v1/chat/completions") {
30
+ response.setHeader("content-type", "application/json");
28
31
  response.end(JSON.stringify({ choices: [{ message: { content: "headline" } }] }));
29
32
  } else if (request.url === "/v1/credits") {
33
+ response.setHeader("content-type", "application/json");
30
34
  response.end(JSON.stringify({ remaining: 42, used: 8 }));
31
35
  } else if (request.url === "/v1/status") {
36
+ response.setHeader("content-type", "application/json");
32
37
  response.end(JSON.stringify({ status: "ok" }));
33
38
  } else if (request.url === "/v1/available_models") {
39
+ response.setHeader("content-type", "application/json");
34
40
  response.end(JSON.stringify({
35
41
  success: true,
36
42
  object: "list",
37
43
  data: [{ id: "remote-image", object: "model", owned_by: "flatkey" }],
38
44
  }));
45
+ } else if (request.url === "/v1/voices") {
46
+ response.setHeader("content-type", "application/json");
47
+ response.end(JSON.stringify({ voices: [{ voice_id: "voice-123", name: "Rachel" }] }));
39
48
  } else {
49
+ response.setHeader("content-type", "application/json");
40
50
  response.statusCode = 404;
41
51
  response.end(JSON.stringify({ error: { message: "not found" } }));
42
52
  }
@@ -54,20 +64,23 @@ test("runs generation and utility commands in json mode", async (t) => {
54
64
  const credits = await runCli(["credits", ...common]);
55
65
  const status = await runCli(["status", ...common]);
56
66
  const models = await runCli(["models", ...common]);
67
+ const voices = await runCli(["audio", "voices", ...common]);
57
68
 
58
69
  assert.deepEqual(JSON.parse(image.stdout).artifacts, [{ url: "https://cdn.test/image.png" }]);
59
70
  assert.deepEqual(JSON.parse(video.stdout).artifacts, [{ url: "https://cdn.test/video.mp4" }]);
60
- assert.deepEqual(JSON.parse(audio.stdout).artifacts, [{ url: "https://cdn.test/audio.mp3" }]);
71
+ assert.equal(JSON.parse(audio.stdout).artifacts.length, 1);
72
+ assert.match(JSON.parse(audio.stdout).artifacts[0].path, /audio-01\.mp3$/);
61
73
  assert.equal(JSON.parse(text.stdout).text, "headline");
62
74
  assert.equal(JSON.parse(credits.stdout).remaining, 42);
63
75
  assert.equal(JSON.parse(status.stdout).status, "ok");
64
76
  assert.deepEqual(JSON.parse(models.stdout).models, [
65
77
  { id: "remote-image", type: "image", source: "remote" },
66
78
  ]);
79
+ assert.deepEqual(JSON.parse(voices.stdout).voices, [{ voice_id: "voice-123", name: "Rachel" }]);
67
80
  assert.equal(image.stderr, "");
68
81
  assert.ok(requests.some((request) => request.url.startsWith("/v1beta/models/")));
69
82
  assert.ok(requests.some((request) => request.url === "/v1/video/generations"));
70
- assert.ok(requests.some((request) => request.url === "/v1/audio/generations"));
83
+ assert.ok(requests.some((request) => request.url === "/v1/text-to-speech/EXAVITQu4vr4xnSDxMaL"));
71
84
  assert.ok(requests.some((request) => request.url === "/v1/chat/completions"));
72
85
  });
73
86
 
@@ -91,18 +104,63 @@ test("dry-run returns planned request without calling network", async () => {
91
104
  assert.equal(result.stderr, "");
92
105
  });
93
106
 
107
+ test("audio dry-run supports tts, sfx, and music routes", async () => {
108
+ const tts = await runCli([
109
+ "audio",
110
+ "generate",
111
+ "--prompt",
112
+ "hello",
113
+ "--voice-id",
114
+ "voice-123",
115
+ "--dry-run",
116
+ "--json",
117
+ ]);
118
+ const sfx = await runCli([
119
+ "audio",
120
+ "sfx",
121
+ "--prompt",
122
+ "glass shattering",
123
+ "--duration",
124
+ "3",
125
+ "--dry-run",
126
+ "--json",
127
+ ]);
128
+ const music = await runCli([
129
+ "audio",
130
+ "music",
131
+ "--prompt",
132
+ "calm ambient piano",
133
+ "--music-length-ms",
134
+ "10000",
135
+ "--dry-run",
136
+ "--json",
137
+ ]);
138
+
139
+ assert.equal(JSON.parse(tts.stdout).request.url, "https://router.flatkey.ai/v1/text-to-speech/voice-123");
140
+ assert.equal(JSON.parse(sfx.stdout).request.url, "https://router.flatkey.ai/v1/sound-generation");
141
+ assert.equal(JSON.parse(sfx.stdout).request.body.duration_seconds, 3);
142
+ assert.equal(JSON.parse(music.stdout).request.url, "https://router.flatkey.ai/v1/music");
143
+ assert.equal(JSON.parse(music.stdout).request.body.music_length_ms, 10000);
144
+ });
145
+
94
146
  test("generation commands write explicit output files", async (t) => {
95
147
  const server = createServer((request, response) => {
96
148
  request.on("data", () => {});
97
149
  request.on("end", () => {
98
150
  response.setHeader("content-type", "application/json");
99
151
  if (request.url === "/v1/images/generations") {
152
+ response.setHeader("content-type", "application/json");
100
153
  response.end(JSON.stringify({
101
154
  data: [{ b64_json: Buffer.from("image-file").toString("base64") }],
102
155
  }));
156
+ } else if (request.url === "/v1/text-to-speech/EXAVITQu4vr4xnSDxMaL") {
157
+ response.setHeader("content-type", "audio/mpeg");
158
+ response.end("audio-file");
103
159
  } else if (request.url === "/v1/chat/completions") {
160
+ response.setHeader("content-type", "application/json");
104
161
  response.end(JSON.stringify({ choices: [{ message: { content: "text-file" } }] }));
105
162
  } else {
163
+ response.setHeader("content-type", "application/json");
106
164
  response.statusCode = 404;
107
165
  response.end(JSON.stringify({ error: { message: "not found" } }));
108
166
  }
@@ -113,6 +171,7 @@ test("generation commands write explicit output files", async (t) => {
113
171
  const baseUrl = `http://127.0.0.1:${server.address().port}`;
114
172
  const dir = await mkdtemp(join(tmpdir(), "flatkey-output-"));
115
173
  const imageOutput = join(dir, "poster.png");
174
+ const audioOutput = join(dir, "speech.mp3");
116
175
  const textOutput = join(dir, "headline.txt");
117
176
 
118
177
  const common = ["--base-url", baseUrl, "--api-key", "test-key", "--json"];
@@ -136,9 +195,20 @@ test("generation commands write explicit output files", async (t) => {
136
195
  textOutput,
137
196
  ...common,
138
197
  ]);
198
+ const audio = await runCli([
199
+ "audio",
200
+ "generate",
201
+ "--prompt",
202
+ "hello",
203
+ "-o",
204
+ audioOutput,
205
+ ...common,
206
+ ]);
139
207
 
140
208
  assert.deepEqual(JSON.parse(image.stdout).artifacts, [{ path: imageOutput }]);
141
209
  assert.equal(await readFile(imageOutput, "utf8"), "image-file");
210
+ assert.deepEqual(JSON.parse(audio.stdout).artifacts, [{ path: audioOutput }]);
211
+ assert.equal(await readFile(audioOutput, "utf8"), "audio-file");
142
212
  assert.equal(JSON.parse(text.stdout).output, textOutput);
143
213
  assert.equal(await readFile(textOutput, "utf8"), "text-file");
144
214
  });
@@ -18,6 +18,36 @@ const cases = [
18
18
  assert.ok(payload.models.length > 0);
19
19
  },
20
20
  },
21
+ {
22
+ name: "audio voices",
23
+ args: ["audio", "voices", "--json"],
24
+ paid: false,
25
+ expect: (payload) => {
26
+ assert.ok(Array.isArray(payload.voices));
27
+ assert.ok(payload.voices.length > 0);
28
+ },
29
+ },
30
+ {
31
+ name: "audio tts",
32
+ args: ["audio", "generate", "--prompt", "flatkey voice test", "--json"],
33
+ paid: true,
34
+ dryRun: true,
35
+ expect: (payload) => assert.equal(payload.request.url, "https://router.flatkey.ai/v1/text-to-speech/EXAVITQu4vr4xnSDxMaL"),
36
+ },
37
+ {
38
+ name: "audio sfx",
39
+ args: ["audio", "sfx", "--prompt", "glass shattering", "--duration", "3", "--json"],
40
+ paid: true,
41
+ dryRun: true,
42
+ expect: (payload) => assert.equal(payload.request.url, "https://router.flatkey.ai/v1/sound-generation"),
43
+ },
44
+ {
45
+ name: "audio music",
46
+ args: ["audio", "music", "--prompt", "calm ambient piano", "--music-length-ms", "10000", "--json"],
47
+ paid: true,
48
+ dryRun: true,
49
+ expect: (payload) => assert.equal(payload.request.url, "https://router.flatkey.ai/v1/music"),
50
+ },
21
51
  {
22
52
  name: "image gpt",
23
53
  args: ["image", "generate", "--model", "gpt-image-2", "--prompt", "one tiny red square icon", "--json"],