@flatkey-ai/cli 0.1.3 → 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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
- package/.github/pull_request_template.md +13 -0
- package/CONTRIBUTING.md +37 -0
- package/LICENSE +21 -0
- package/README.md +189 -13
- package/SECURITY.md +13 -0
- package/flatkey-output/audio-01.mp3 +1 -0
- package/package.json +1 -1
- package/src/api.js +69 -5
- package/src/cli.js +40 -4
- package/src/help.js +9 -2
- package/src/models.js +5 -2
- package/test/api.test.js +58 -8
- package/test/cli.test.js +53 -1
- package/test/help.test.js +4 -0
- package/test/integration.test.js +75 -5
- package/test/live-flatkey.test.js +30 -0
|
@@ -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
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
-
#
|
|
1
|
+
# Flatkey CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@flatkey-ai/cli)
|
|
4
|
+
[](https://www.npmjs.com/package/@flatkey-ai/cli)
|
|
5
|
+
[](https://github.com/flatkey-ai/flatkey-cli/actions/workflows/npm-publish.yml)
|
|
6
|
+
[](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
|
-
|
|
38
|
+
Run without installing:
|
|
12
39
|
|
|
13
40
|
```bash
|
|
14
41
|
npx @flatkey-ai/cli help --ai
|
|
15
42
|
```
|
|
16
43
|
|
|
17
|
-
|
|
44
|
+
Check local version:
|
|
18
45
|
|
|
19
46
|
```bash
|
|
20
|
-
flatkey
|
|
47
|
+
flatkey --version
|
|
48
|
+
flatkey -v
|
|
21
49
|
```
|
|
22
50
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
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
|
|
56
|
+
return requestBinaryArtifactFromPlan(options, planAudioRequest(options));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export function planAudioRequest(options) {
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
@@ -18,6 +18,9 @@ export function parseArgv(argv) {
|
|
|
18
18
|
if (!group) {
|
|
19
19
|
return { group: "help", action: undefined, options: {} };
|
|
20
20
|
}
|
|
21
|
+
if (group === "--version" || group === "-v") {
|
|
22
|
+
return { group: "version", action: undefined, options: {} };
|
|
23
|
+
}
|
|
21
24
|
if (!COMMANDS.has(group)) {
|
|
22
25
|
throw new Error(`Unknown command: ${group}`);
|
|
23
26
|
}
|
|
@@ -87,7 +90,8 @@ export async function runCommand(command, deps = {}) {
|
|
|
87
90
|
return command.options.ai ? getAiHelp() : getHumanHelp();
|
|
88
91
|
}
|
|
89
92
|
if (command.group === "version") {
|
|
90
|
-
|
|
93
|
+
const version = await readPackageVersion();
|
|
94
|
+
return command.options.json ? { version } : version;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
if (command.group === "models") {
|
|
@@ -99,7 +103,13 @@ export async function runCommand(command, deps = {}) {
|
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
if (["image", "video", "audio", "text"].includes(command.group)) {
|
|
102
|
-
if (command.action
|
|
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) {
|
|
103
113
|
throw new Error(`Unknown action for ${command.group}: ${command.action}`);
|
|
104
114
|
}
|
|
105
115
|
return handleGenerate(command, { ...deps, stdout, stderr });
|
|
@@ -112,10 +122,14 @@ async function handleGenerate(command, deps) {
|
|
|
112
122
|
const { resolveApiKey } = await import("./config.js");
|
|
113
123
|
const {
|
|
114
124
|
generateAudio,
|
|
125
|
+
generateAudioMusic,
|
|
126
|
+
generateAudioSfx,
|
|
115
127
|
generateImage,
|
|
116
128
|
generateText,
|
|
117
129
|
generateVideo,
|
|
130
|
+
planAudioMusicRequest,
|
|
118
131
|
planAudioRequest,
|
|
132
|
+
planAudioSfxRequest,
|
|
119
133
|
planImageRequest,
|
|
120
134
|
planTextRequest,
|
|
121
135
|
planVideoRequest,
|
|
@@ -140,7 +154,11 @@ async function handleGenerate(command, deps) {
|
|
|
140
154
|
: command.group === "video"
|
|
141
155
|
? planVideoRequest(options)
|
|
142
156
|
: command.group === "audio"
|
|
143
|
-
?
|
|
157
|
+
? command.action === "sfx"
|
|
158
|
+
? planAudioSfxRequest(options)
|
|
159
|
+
: command.action === "music"
|
|
160
|
+
? planAudioMusicRequest(options)
|
|
161
|
+
: planAudioRequest(options)
|
|
144
162
|
: planTextRequest(options);
|
|
145
163
|
return { dryRun: true, kind: command.group, request: redactRequest(request) };
|
|
146
164
|
}
|
|
@@ -155,7 +173,11 @@ async function handleGenerate(command, deps) {
|
|
|
155
173
|
: command.group === "video"
|
|
156
174
|
? await generateVideo(options)
|
|
157
175
|
: command.group === "audio"
|
|
158
|
-
?
|
|
176
|
+
? command.action === "sfx"
|
|
177
|
+
? await generateAudioSfx(options)
|
|
178
|
+
: command.action === "music"
|
|
179
|
+
? await generateAudioMusic(options)
|
|
180
|
+
: await generateAudio(options)
|
|
159
181
|
: await generateText(options);
|
|
160
182
|
if (command.group === "text") {
|
|
161
183
|
const text = extractText(response);
|
|
@@ -175,6 +197,20 @@ async function handleGenerate(command, deps) {
|
|
|
175
197
|
}
|
|
176
198
|
}
|
|
177
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
|
+
|
|
178
214
|
function extractText(response) {
|
|
179
215
|
return response?.choices?.[0]?.message?.content
|
|
180
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 "<
|
|
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
|
|
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: "
|
|
10
|
-
{ id: "
|
|
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: "
|
|
147
|
+
model: "eleven_multilingual_v2",
|
|
142
148
|
prompt: "voiceover",
|
|
143
|
-
|
|
144
|
-
|
|
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/
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
|
@@ -75,6 +113,19 @@ test("parses ai help", () => {
|
|
|
75
113
|
});
|
|
76
114
|
});
|
|
77
115
|
|
|
116
|
+
test("parses global version aliases", () => {
|
|
117
|
+
assert.deepEqual(parseArgv(["--version"]), {
|
|
118
|
+
group: "version",
|
|
119
|
+
action: undefined,
|
|
120
|
+
options: {},
|
|
121
|
+
});
|
|
122
|
+
assert.deepEqual(parseArgv(["-v"]), {
|
|
123
|
+
group: "version",
|
|
124
|
+
action: undefined,
|
|
125
|
+
options: {},
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
78
129
|
test("rejects unknown commands", () => {
|
|
79
130
|
assert.throws(
|
|
80
131
|
() => parseArgv(["wat"]),
|
|
@@ -85,7 +136,8 @@ test("rejects unknown commands", () => {
|
|
|
85
136
|
test("version command matches package version", async () => {
|
|
86
137
|
const pkg = JSON.parse(await (await import("node:fs/promises")).readFile("package.json", "utf8"));
|
|
87
138
|
|
|
88
|
-
assert.
|
|
139
|
+
assert.equal(await runCommand({ group: "version", options: {} }), pkg.version);
|
|
140
|
+
assert.deepEqual(await runCommand({ group: "version", options: { json: true } }), {
|
|
89
141
|
version: pkg.version,
|
|
90
142
|
});
|
|
91
143
|
});
|
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
|
});
|
package/test/integration.test.js
CHANGED
|
@@ -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/
|
|
26
|
-
response.
|
|
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.
|
|
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/
|
|
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"],
|