quickmagic-cli 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -7
- package/package.json +6 -3
- package/skills/README.md +18 -12
- package/skills/quickmagic-account/SKILL.md +10 -2
- package/skills/quickmagic-generate/SKILL.md +5 -1
- package/skills/quickmagic-music/SKILL.md +118 -0
- package/skills/quickmagic-tts/SKILL.md +167 -0
- package/src/api.js +3 -1
- package/src/commands/generate.js +2 -1
- package/src/commands/jobs.js +44 -0
- package/src/commands/models.js +12 -2
- package/src/commands/music.js +109 -0
- package/src/commands/voice.js +141 -0
- package/src/index.js +78 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
images/videos, product photoshoots, virtual try-on, hook ad videos, auto-subtitles/dubbing
|
|
9
9
|
and more, through the Quick Magic REST API. Sign-in is browser-based **OAuth PKCE** — no API
|
|
10
10
|
keys to copy around. Requires Node.js **≥ 18** (uses built-in `fetch`/`crypto`/`http`, no
|
|
11
|
-
`axios`). Also ships with
|
|
11
|
+
`axios`). Also ships with 10 Claude **Agent Skills** and an **MCP server** integration for
|
|
12
12
|
coding agents. Everything below this line is in Vietnamese (tiếng Việt) — quickstart only:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
@@ -147,6 +147,14 @@ Ghi chú chung trước khi xem bảng:
|
|
|
147
147
|
| `qm subtitle <video_url>` | Thêm phụ đề (+ lồng tiếng, dịch) | Có |
|
|
148
148
|
| `qm split <video_url>` | Cắt video dài → clip ngắn | Có |
|
|
149
149
|
| `qm motion <video_url> --image <url...>` | Áp chuyển động video vào ảnh (Kling) | Có |
|
|
150
|
+
| `qm tts (--text <t>\|--file <path>) --voice <v>` | Chuyển văn bản → giọng nói (5 model qimi_1.5/2.5/3/5/5.5) | Có |
|
|
151
|
+
| `qm voices list --model <m>` | Liệt kê giọng theo model + giá (`pricing`) | — |
|
|
152
|
+
| `qm voices clone --audio <file\|url> --name <n>` | Nhân bản giọng riêng (dùng cho qimi_5/5.5) | Có |
|
|
153
|
+
| `qm voices delete <id>` | Xoá giọng đã clone | — |
|
|
154
|
+
| `qm music models` | Bảng model nhạc + giá + ETA + hàng chờ | — |
|
|
155
|
+
| `qm music lyrics --desc <d>` | AI viết lời bài hát | Không (0đ) |
|
|
156
|
+
| `qm music create (--desc <d>\|--lyrics <l>)` | Tạo bài hát AI (mode simple/custom, 2 model melo-3/melo-2) | Có (thường miễn phí giai đoạn ra mắt) |
|
|
157
|
+
| `qm music get <id>` | Xem chi tiết job nhạc | — |
|
|
150
158
|
|
|
151
159
|
### Nhập liệu (miễn phí)
|
|
152
160
|
|
|
@@ -178,8 +186,39 @@ qm generate video --prompt "..." --model grok-imagine --wait --out ./out
|
|
|
178
186
|
|
|
179
187
|
- `--image` cũng nhận URL hoặc file local (như `--ref`).
|
|
180
188
|
- `--mode reference|frames` — `reference` (nhiều ảnh tham chiếu) hoặc `frames` (khung
|
|
181
|
-
đầu/cuối, tối đa 2).
|
|
182
|
-
|
|
189
|
+
đầu/cuối, tối đa 2). Model họ Seedance 2.x (`seedance-2-0`, `-fast`, `-mini`) giờ **tự động
|
|
190
|
+
chấp nhận ảnh người thật** ở mọi `--mode` — xem mục "Ảnh người thật" ngay dưới.
|
|
191
|
+
|
|
192
|
+
### Ảnh người thật (Seedance 2.x)
|
|
193
|
+
|
|
194
|
+
Model họ Seedance 2.x không còn chặn cứng ảnh có người thật: hệ thống tự chuẩn bị (đăng ký)
|
|
195
|
+
ảnh trước khi render, **có thể mất thêm ~1–3 phút** ở lần đầu dùng 1 ảnh cụ thể. Trong lúc đó
|
|
196
|
+
`qm jobs wait`/`--wait` in dòng `đang chuẩn bị ảnh có người thật (tối đa ~10 phút) — đừng tạo
|
|
197
|
+
job mới…` — **đừng bấm Ctrl+C rồi chạy lại lệnh**, cứ để CLI tự chờ tiếp (script/agent gọi
|
|
198
|
+
`jobs get`/`wait_for_job` cũng áp dụng y hệt — xem `docs/mcp-integration-guide.md` phía repo
|
|
199
|
+
backend). Ảnh bị bộ lọc nội dung của model từ chối (bản quyền/chính sách; khi bật kiểm tra sớm:
|
|
200
|
+
người nổi tiếng/nhân vật công chúng, trẻ em, nội dung nhạy cảm) → job `failed` kèm message rõ lý do
|
|
201
|
+
+ 1 `error_code` trong bảng dưới (credit hoàn), CLI tự in gợi ý xử lý:
|
|
202
|
+
|
|
203
|
+
| `error_code` | Ý nghĩa | Gợi ý |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `PORTRAIT_PUBLIC_FIGURE` | Ảnh có người nổi tiếng/nhân vật công chúng | Dùng ảnh người thường |
|
|
206
|
+
| `PORTRAIT_MINOR` | Ảnh có trẻ em | Dùng ảnh người lớn |
|
|
207
|
+
| `PORTRAIT_NSFW` | Ảnh vi phạm nội dung nhạy cảm | Dùng ảnh khác |
|
|
208
|
+
| `PORTRAIT_GEMINI_UNAVAILABLE` | Chưa kiểm được ảnh (hệ thống bận) | Thử lại sau vài phút |
|
|
209
|
+
| `PORTRAIT_ASSET_FAILED` | Ảnh không được chấp nhận (mờ/nhiều người/định dạng lạ) | Thử ảnh khác |
|
|
210
|
+
| `PORTRAIT_ASSET_REJECTED` | Ảnh vẫn bị bộ lọc nội dung từ chối sau khi xử lý (người nổi tiếng/trẻ em/nhạy cảm/bản quyền) | Dùng ảnh khác |
|
|
211
|
+
| `PORTRAIT_TIMEOUT` | Chuẩn bị ảnh quá lâu (>10 phút) | Thử lại |
|
|
212
|
+
| `PORTRAIT_QUOTA_FULL` | Hệ thống đang bận (đầy quota tạm) | Thử lại sau vài phút |
|
|
213
|
+
| `PORTRAIT_IMAGE_HOST` | Ảnh không thuộc host Quick Magic | Dùng `--image <file local>` (CLI tự upload) thay URL ngoài |
|
|
214
|
+
| `PORTRAIT_IMAGE_FETCH_FAILED` | Không tải được ảnh | Thử lại |
|
|
215
|
+
| `PORTRAIT_PROVIDER_ERROR` | Hệ thống đang bận (lỗi tạm thời) | Thử lại sau ít phút |
|
|
216
|
+
| `PORTRAIT_PROVIDER_FAILED` | Ảnh bị từ chối theo chính sách nội dung (người nổi tiếng/trẻ em/nhạy cảm/bản quyền) | Dùng ảnh khác |
|
|
217
|
+
| `PORTRAIT_COPYRIGHT` | Ảnh bị từ chối vì bản quyền/quyền hình ảnh (người nổi tiếng, nhân vật/tác phẩm/thương hiệu được bảo hộ) | Dùng ảnh khác |
|
|
218
|
+
| `PORTRAIT_DISABLED` | Tính năng ảnh người thật đang tắt | Đổi `--model gemini-omni` |
|
|
219
|
+
| `PORTRAIT_TOO_MANY_IMAGES` | Vượt số ảnh người thật cho phép của model | Bớt `--image` |
|
|
220
|
+
| `PORTRAIT_VIDEO_REF_UNSUPPORTED` | Video tham chiếu có người thật chưa hỗ trợ | Dùng `--image` thay `--video-ref`, hoặc đổi model |
|
|
221
|
+
| `REAL_PERSON_BLOCKED` | (mã cũ) Model không nhận ảnh người thật | Đổi `--model gemini-omni` |
|
|
183
222
|
|
|
184
223
|
## Job
|
|
185
224
|
|
|
@@ -223,6 +262,16 @@ qm split https://www.tiktok.com/@user/video/123 --mode auto
|
|
|
223
262
|
qm stt https://cdn.example.com/audio.mp3 --translate vi
|
|
224
263
|
qm motion https://cdn.example.com/dance.mp4 --image ./photo.jpg
|
|
225
264
|
|
|
265
|
+
# Đọc văn bản thành giọng nói — xem giá + chọn giọng trước khi chạy
|
|
266
|
+
qm voices list --model qimi_1.5 --language "Tiếng Việt"
|
|
267
|
+
qm tts --text "Xin chào Quick Magic" --voice <voice_id> --model qimi_3 --wait --out ./out
|
|
268
|
+
qm voices clone --audio ./sample.wav --name "Giọng của tôi" --wait
|
|
269
|
+
|
|
270
|
+
# Tạo nhạc AI — xem giá trước khi chạy (thường miễn phí giai đoạn ra mắt)
|
|
271
|
+
qm music models
|
|
272
|
+
qm music lyrics --desc "bài hát vui về buổi sáng cà phê Sài Gòn"
|
|
273
|
+
qm music create --mode simple --desc "lofi chill guitar, rainy night" --instrumental --model melo-2 --wait --out ./out
|
|
274
|
+
|
|
226
275
|
# Video marketing theo mode + phân tích video đối thủ → kịch bản
|
|
227
276
|
qm marketing modes
|
|
228
277
|
qm marketing video --mode 3 --product-id 42 --duration 10
|
|
@@ -258,7 +307,7 @@ qm credits # chạy không cần browser
|
|
|
258
307
|
|
|
259
308
|
## Agent Skills
|
|
260
309
|
|
|
261
|
-
CLI này đi kèm
|
|
310
|
+
CLI này đi kèm 10 **Agent Skills** (`skills/quickmagic-*/SKILL.md`) để coding agent
|
|
262
311
|
(Claude Code...) dùng CLI trực tiếp — báo giá + xin xác nhận trước khi tốn credit,
|
|
263
312
|
tự `jobs wait` thay vì poll tay, chủ động báo Qimi free-window. Danh sách đầy đủ và quy ước
|
|
264
313
|
chung: xem `skills/README.md`.
|
|
@@ -269,7 +318,7 @@ Cách 1 — `npx skills` (nếu dùng công cụ [`skills`](https://www.npmjs.co
|
|
|
269
318
|
quản lý Agent Skills từ GitHub):
|
|
270
319
|
|
|
271
320
|
```bash
|
|
272
|
-
npx skills add Tungbillee/cli-quickmagic --skills quickmagic-account,quickmagic-generate,quickmagic-product-photoshoot,quickmagic-fashion,quickmagic-cutout,quickmagic-hook-video,quickmagic-edit-image,quickmagic-subtitle-split
|
|
321
|
+
npx skills add Tungbillee/cli-quickmagic --skills quickmagic-account,quickmagic-generate,quickmagic-product-photoshoot,quickmagic-fashion,quickmagic-cutout,quickmagic-hook-video,quickmagic-edit-image,quickmagic-subtitle-split,quickmagic-tts,quickmagic-music
|
|
273
322
|
```
|
|
274
323
|
|
|
275
324
|
Cách 2 — copy thủ công vào thư mục skills của Claude Code:
|
|
@@ -283,8 +332,9 @@ cp -r skills/quickmagic-* ~/.claude/skills/ # user (mọi project trên má
|
|
|
283
332
|
|
|
284
333
|
Quick Magic cũng có MCP server dùng **chung tài khoản/ví credit** — cho phép Claude Code
|
|
285
334
|
(và các MCP client khác) gọi thẳng `generate_image`, `generate_video`, `get_job`,
|
|
286
|
-
`wait_for_job`, `list_models`, `get_credit_balance
|
|
287
|
-
|
|
335
|
+
`wait_for_job`, `list_models`, `get_credit_balance`, `list_voices`, `text_to_speech`,
|
|
336
|
+
`create_voice_clone`, `delete_voice_clone`, `generate_music`, `write_lyrics`... mà không cần
|
|
337
|
+
qua CLI. Xác thực OAuth 2.1 qua trình duyệt, không cần API key.
|
|
288
338
|
|
|
289
339
|
- MCP endpoint: `https://api.quickmagic.vn/mcp`
|
|
290
340
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quickmagic-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Quick Magic CLI — generate AI images/videos, product photoshoots, virtual try-on, hook ad videos, subtitles and more via the Quick Magic REST API, with browser-based OAuth PKCE login (no API keys to manage). Ships with
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Quick Magic CLI — generate AI images/videos, product photoshoots, virtual try-on, hook ad videos, subtitles, text-to-speech/voice cloning, AI song generation and more via the Quick Magic REST API, with browser-based OAuth PKCE login (no API keys to manage). Ships with 10 Claude Agent Skills and an MCP server integration.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"quickmagic": "src/index.js",
|
|
7
7
|
"qm": "src/index.js"
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
"claude-code",
|
|
33
33
|
"agent-skills",
|
|
34
34
|
"product-photography",
|
|
35
|
-
"virtual-try-on"
|
|
35
|
+
"virtual-try-on",
|
|
36
|
+
"text-to-speech",
|
|
37
|
+
"voice-cloning",
|
|
38
|
+
"music-generation"
|
|
36
39
|
],
|
|
37
40
|
"homepage": "https://github.com/Tungbillee/cli-quickmagic#readme",
|
|
38
41
|
"repository": {
|
package/skills/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Quick Magic CLI — Agent Skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
10 Agent Skills that wrap the `quickmagic` / `qm` CLI so a coding agent (Claude
|
|
4
4
|
Code and compatible tools) can drive Quick Magic directly from the terminal —
|
|
5
5
|
mirroring the `npx skills add higgsfield-ai/skills` pattern (`/higgsfield:generate`
|
|
6
6
|
etc.), tailored to Quick Magic's own REST API and CLI.
|
|
@@ -18,7 +18,7 @@ when to load the skill).
|
|
|
18
18
|
Option 1 — `npx skills` (if this repo is published as a skills package):
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npx skills add Tungbillee/cli-quickmagic --skills quickmagic-account,quickmagic-generate,quickmagic-product-photoshoot,quickmagic-fashion,quickmagic-cutout,quickmagic-hook-video,quickmagic-edit-image,quickmagic-subtitle-split
|
|
21
|
+
npx skills add Tungbillee/cli-quickmagic --skills quickmagic-account,quickmagic-generate,quickmagic-product-photoshoot,quickmagic-fashion,quickmagic-cutout,quickmagic-hook-video,quickmagic-edit-image,quickmagic-subtitle-split,quickmagic-tts,quickmagic-music
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Option 2 — copy directly into a project's or your user's Claude skills folder:
|
|
@@ -35,7 +35,7 @@ mkdir -p ~/.claude/skills && cp -r cli-quickmagic/skills/quickmagic-* ~/.claude/
|
|
|
35
35
|
|
|
36
36
|
| Skill | Wraps | Use for |
|
|
37
37
|
|---|---|---|
|
|
38
|
-
| `quickmagic-account` | `auth`, `credits`, `models list`, `jobs get/wait` | login/status, credit balance + Qimi free-window benefit, model catalog, job polling — **foundation** the other
|
|
38
|
+
| `quickmagic-account` | `auth`, `credits`, `models list`, `jobs get/wait` | login/status, credit balance + Qimi free-window benefit, model catalog, job polling — **foundation** the other 9 skills reference |
|
|
39
39
|
| `quickmagic-generate` | `generate image`, `generate video` | text/reference-to-image or -video |
|
|
40
40
|
| `quickmagic-product-photoshoot` | `product` | studio/marketing photos of a product (packshot/poster/infographic/composite) |
|
|
41
41
|
| `quickmagic-fashion` | `fashion`, `tryon` | outfit lookbook shoots vs. virtual try-on on a specific person photo |
|
|
@@ -43,23 +43,29 @@ mkdir -p ~/.claude/skills && cp -r cli-quickmagic/skills/quickmagic-* ~/.claude/
|
|
|
43
43
|
| `quickmagic-hook-video` | `hook presets`, `hook video` | ~10s comedy ad video from a character + product photo (18 presets) |
|
|
44
44
|
| `quickmagic-edit-image` | `edit` | restore / upscale (2k/4k) / beauty / muscle / color_boost on an existing image |
|
|
45
45
|
| `quickmagic-subtitle-split` | `subtitle`, `split` | auto subtitles/dubbing, long-video → short clips |
|
|
46
|
+
| `quickmagic-tts` | `tts`, `voices list/clone/delete` | text-to-speech (5 models) + custom voice cloning |
|
|
47
|
+
| `quickmagic-music` | `music create/lyrics/models/get` | AI song generation (vocals or instrumental) from a description or your own lyrics |
|
|
46
48
|
|
|
47
|
-
## Shared conventions (all
|
|
49
|
+
## Shared conventions (all 10 skills)
|
|
48
50
|
|
|
49
51
|
- Check `qm auth status` first; if not logged in, tell the user to run
|
|
50
52
|
`qm auth login`.
|
|
51
|
-
- Quote the credit cost (from `qm models list` / a preset's `credit_cost`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
- Quote the credit cost (from `qm models list` / a preset's `credit_cost` —
|
|
54
|
+
`tts`/`voices clone` instead read `pricing`/`clone_price` from
|
|
55
|
+
`qm voices list --model <m>`) and get explicit user confirmation **before**
|
|
56
|
+
running any command that spends credit — submission holds credit
|
|
57
|
+
immediately. `subtitle`/`split` are the one exception: price is
|
|
58
|
+
duration-based and only known after submission, so the skill asks for a
|
|
59
|
+
go-ahead instead of a number.
|
|
56
60
|
- Surface `insufficient_credit` (`Không đủ credit: ... thiếu Zcr`) with the exact
|
|
57
61
|
shortfall and a link to top up (https://quickmagic.vn/pricing).
|
|
58
62
|
- Never poll `jobs get` manually — use `qm jobs wait <id> [--out dir]`. Note that
|
|
59
63
|
`--out` only auto-downloads for job types with a single `result_url`
|
|
60
|
-
(image/video/tryon/edit/hook/subtitle); product/fashion/cutout/split
|
|
61
|
-
arrays and must be read back with `qm jobs get <id>` after
|
|
62
|
-
|
|
64
|
+
(image/video/tryon/edit/hook/subtitle/tts/music); product/fashion/cutout/split
|
|
65
|
+
return arrays and must be read back with `qm jobs get <id>` after
|
|
66
|
+
completion, and voice-clone jobs (`voc_`) have no `result_url` at all (read
|
|
67
|
+
`voice`/`sample_url`/`status` instead). See `quickmagic-account` for the
|
|
68
|
+
full job-id-prefix table.
|
|
63
69
|
- Always end with the final result URL(s) plus the credit amount already
|
|
64
70
|
charged/held.
|
|
65
71
|
|
|
@@ -66,6 +66,11 @@ qm models list --type video # columns: KEY | LABEL | CREDIT | IMAGES
|
|
|
66
66
|
`ref 9 · frames 2`. A trailing `*` means that mode **rejects photos containing
|
|
67
67
|
real people** for that model — switch models (e.g. `gemini-omni`, a `seedance
|
|
68
68
|
1.x` key, `wan-2-7`) if the user's reference photos have people in them.
|
|
69
|
+
No `*` = people photos are fine. Seedance 2.x currently accepts real-people
|
|
70
|
+
photos (the server prints an ℹ note under the table): the first use of a given
|
|
71
|
+
photo may add ~1–3 min of preparation — `qm jobs wait` handles it, do NOT
|
|
72
|
+
resubmit. If the model's content filter rejects a photo (copyright/policy), the
|
|
73
|
+
job fails with an explicit reason and the credit is refunded — use another photo.
|
|
69
74
|
- The table does not print quality tiers or max-ref-image counts for image
|
|
70
75
|
models — image quality levels are commonly `1K`/`2K`/`4K`, priced per level; if
|
|
71
76
|
the exact enum for a model is unknown, omit `--quality` and let the server
|
|
@@ -101,10 +106,13 @@ generating command's own `--wait` flag, where available).
|
|
|
101
106
|
| `mkt_` | `marketing video` | single result (no dedicated skill) |
|
|
102
107
|
| `mot_` | `motion` | single result (no dedicated skill) |
|
|
103
108
|
| `stt_` | `stt` | text result (no dedicated skill) |
|
|
109
|
+
| `tts_` | `tts` | single `result_url` (mp3) + `model`/`voice`/`character` — see `quickmagic-tts` |
|
|
110
|
+
| `voc_` | `voices clone` | **no** `result_url`: `voice` (name to use with `--voice`), `sample_url`, `is_free` — see `quickmagic-tts` |
|
|
104
111
|
|
|
105
112
|
**Important caveat**: `jobs wait --out <dir>` only **auto-downloads** for the
|
|
106
|
-
single-`result_url` types (`img_`/`vid_`/`vto_`/`edt_`/`hok_`/`sub_`). For the
|
|
107
|
-
array types (`pai_`/`fsh_`/`cut_`/`spl_`)
|
|
113
|
+
single-`result_url` types (`img_`/`vid_`/`vto_`/`edt_`/`hok_`/`sub_`/`tts_`). For the
|
|
114
|
+
array types (`pai_`/`fsh_`/`cut_`/`spl_`) and `voc_` (voice clone — no file, read
|
|
115
|
+
`voice`/`sample_url` via `qm jobs get`), `--out` still polls status correctly
|
|
108
116
|
but downloads nothing — after status is `completed`, call `qm jobs get <id>` and
|
|
109
117
|
read the array field yourself, then report each URL to the user.
|
|
110
118
|
|
|
@@ -36,7 +36,11 @@ studio photos see `quickmagic-product-photoshoot`.
|
|
|
36
36
|
`KEY | LABEL | CREDIT` (+ `IMAGES` column for video, showing max reference images
|
|
37
37
|
per mode, e.g. `ref 9 · frames 2`; a `*` after a number means that mode **rejects
|
|
38
38
|
real photos of real people** — pick a different model if the user's refs contain
|
|
39
|
-
people, e.g. `gemini-omni` / a `seedance 1.x` key / `wan-2-7`).
|
|
39
|
+
people, e.g. `gemini-omni` / a `seedance 1.x` key / `wan-2-7`). No `*` = people
|
|
40
|
+
photos are accepted; Seedance 2.x currently accepts them (first use of a photo may
|
|
41
|
+
add ~1–3 min of server-side preparation — keep waiting with `qm jobs wait`, do not
|
|
42
|
+
resubmit; a content-filter rejection — copyright/policy — fails the job with an
|
|
43
|
+
explicit reason and refunds the credit → try another photo).
|
|
40
44
|
|
|
41
45
|
- This table doesn't print quality tiers — image quality levels are commonly
|
|
42
46
|
`1K`/`2K`/`4K`, priced differently per level. If `--quality` matters, mention the
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quickmagic-music
|
|
3
|
+
description: >-
|
|
4
|
+
Generate an AI song (with vocals or instrumental) from a text description
|
|
5
|
+
or your own lyrics, via the Quick Magic CLI (`qm music create/lyrics/models/get`).
|
|
6
|
+
2 models (melo-3 default, melo-2), simple mode (AI writes lyrics) or custom
|
|
7
|
+
mode (your own lyrics), fair 1-song-per-account queue. Triggers:
|
|
8
|
+
"tạo nhạc AI", "sáng tác nhạc", "viết bài hát", "viết lời bài hát",
|
|
9
|
+
"làm nhạc nền", "generate a song", "AI music", "write song lyrics",
|
|
10
|
+
"compose a song", "make music", "text to music".
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# quickmagic-music — AI Song Generation (Xưởng Nhạc "Melo")
|
|
14
|
+
|
|
15
|
+
Wraps `qm music create/lyrics/models/get`. Use whenever the user wants an AI-
|
|
16
|
+
generated song (vocals or instrumental) or just wants lyrics written for them.
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
| Command | Required flags | Key optional flags |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `qm music models` | — | prints KEY/LABEL/GIÁ/ETA/HÀNG CHỜ table |
|
|
23
|
+
| `qm music lyrics` | `--desc <d>` | `--title <t>`, `--language <l>`, `--instrumental` |
|
|
24
|
+
| `qm music create` | `--desc <d>` (mode simple/instrumental) or `--lyrics <l>`/`--lyrics-file <path>` (mode custom) | `--mode <m>` (default `simple`), `--model <m>` (default `melo-3`), `--styles <s>`, `--title <t>`, `--instrumental`, `--gender <g>`, `--crid <id>`, `--wait`, `--out <dir>` |
|
|
25
|
+
| `qm music get <id>` | `<id>` (`mus_12` or bare `12`) | — |
|
|
26
|
+
|
|
27
|
+
## Two modes
|
|
28
|
+
|
|
29
|
+
- **`--mode simple`** (default): pass `--desc` describing the vibe/topic —
|
|
30
|
+
the server's internal AI songwriter writes the lyrics automatically. Good
|
|
31
|
+
default when the user just describes what they want.
|
|
32
|
+
- **`--mode custom`**: pass `--lyrics` (inline) or `--lyrics-file <path>`
|
|
33
|
+
(local UTF-8 text file, ≤3500 chars) with lyrics the user already has or
|
|
34
|
+
that you drafted with `qm music lyrics` first.
|
|
35
|
+
- **`--instrumental`**: no vocals at all — only `--styles`/`--desc` matter,
|
|
36
|
+
`--lyrics`/`--gender` are ignored.
|
|
37
|
+
|
|
38
|
+
## Pricing — always read at runtime, never hardcode
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
qm music models
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Prices are **runtime-only** — currently **free during the launch phase**
|
|
45
|
+
(check the `GIÁ` column / `free_launch` in the raw JSON), but this can change
|
|
46
|
+
at any time. `qm music create` already re-reads this internally right before
|
|
47
|
+
submitting and passes it as a price guard — if the price changed between your
|
|
48
|
+
quote and the actual submit, the command fails with `price_changed` instead
|
|
49
|
+
of silently charging a different amount. If that happens, just re-run
|
|
50
|
+
`qm music models`, tell the user the new price, and get confirmation again
|
|
51
|
+
before retrying `qm music create`.
|
|
52
|
+
|
|
53
|
+
## Queue — fair, 1 song running per account
|
|
54
|
+
|
|
55
|
+
Each account runs **1 song at a time**; extra requests queue fairly (FIFO
|
|
56
|
+
across users, not first-come-first-served within a single user hogging
|
|
57
|
+
slots). `qm music create` prints `queue_position`/`eta_seconds` right after
|
|
58
|
+
submitting. **Don't poll manually** — always use `--wait` (or
|
|
59
|
+
`qm jobs wait mus_<n> --out <dir>` afterwards), which polls automatically and
|
|
60
|
+
auto-downloads the mp3 once done (single `result_url`, like `tts`/`generate
|
|
61
|
+
image`). A song typically finishes in under 2 minutes once it starts running,
|
|
62
|
+
longer if queued behind other users' jobs. A long `queued` job is **not**
|
|
63
|
+
necessarily an error — check `queue_position`/`eta_seconds` in `qm music get`;
|
|
64
|
+
`status` only flips to `stalled` once the job stops making queue progress at
|
|
65
|
+
all (well past the ETA), not just because it has been waiting a while.
|
|
66
|
+
|
|
67
|
+
## Workflow
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
qm music models # 1. check current price + ETA
|
|
71
|
+
qm music lyrics --desc "lofi chill về mùa thu Hà Nội" # 2. optional — preview/edit lyrics first
|
|
72
|
+
# ... quote the price, get explicit user confirmation ...
|
|
73
|
+
qm music create --mode simple --desc "lofi chill về mùa thu Hà Nội" \
|
|
74
|
+
--model melo-3 --wait --out ./out # 3. submit + wait + auto-download mp3
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Custom lyrics example:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
qm music create --mode custom --lyrics-file ./loi-bai-hat.txt \
|
|
81
|
+
--styles "V-pop, Ballad, Piano" --title "Chiều Thu" --gender female --wait --out ./out
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Instrumental example:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
qm music create --mode simple --desc "epic cinematic trailer music, orchestral, rising tension" \
|
|
88
|
+
--instrumental --model melo-2 --wait --out ./out
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Errors
|
|
92
|
+
|
|
93
|
+
CLI prints `Lỗi [code]: message`. Common codes:
|
|
94
|
+
|
|
95
|
+
| Code | Meaning | What to do |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `rate_limited` | more than 3 song submits/minute for this account | wait, then retry |
|
|
98
|
+
| `feature_disabled` | Xưởng Nhạc temporarily off | tell the user, don't retry |
|
|
99
|
+
| `price_changed` | price shifted between quote and submit | re-run `qm music models`, re-quote, re-confirm |
|
|
100
|
+
| `invalid_input` | missing/invalid `--desc`/`--lyrics`/`--model`/`--mode` for the chosen mode | check the mode's required field (see "Two modes" above) |
|
|
101
|
+
| `insufficient_credit` / `negative_balance` | not enough balance (only matters once pricing is no longer free) | state the exact shortfall and https://quickmagic.vn/pricing |
|
|
102
|
+
| `busy` | `qm music lyrics` internal writer overloaded | wait a bit, then retry `qm music lyrics`, or just supply lyrics yourself |
|
|
103
|
+
| `lyrics_failed` | AI songwriter couldn't produce valid lyrics after retry | try a clearer/different `--desc`, or write lyrics yourself with `--lyrics` |
|
|
104
|
+
| `feature_unavailable` | account temporarily blocked from this feature | tell the user, don't retry |
|
|
105
|
+
| `concurrent_limit` | too many jobs already running at once for the account's plan | wait for one of the running jobs to finish, then retry |
|
|
106
|
+
| `INVALID_PROMPT` | (uppercase, REST-only) `--lyrics` over 3500 chars or `--desc` over 500 chars | shorten the text and resubmit |
|
|
107
|
+
|
|
108
|
+
Full auth/job/error-code reference: `quickmagic-account` skill.
|
|
109
|
+
|
|
110
|
+
## Examples
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
qm auth status
|
|
114
|
+
qm music models
|
|
115
|
+
qm music lyrics --desc "bài hát vui về buổi sáng cà phê Sài Gòn" --title "Sài Gòn Sáng Cà Phê"
|
|
116
|
+
qm music create --mode simple --desc "bài hát vui về buổi sáng cà phê Sài Gòn" --wait --out ./out
|
|
117
|
+
qm music get mus_12
|
|
118
|
+
```
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quickmagic-tts
|
|
3
|
+
description: >-
|
|
4
|
+
Convert text to speech, or clone a custom voice, via the Quick Magic CLI
|
|
5
|
+
(`qm tts`, `qm voices list/clone/delete`). Covers 5 TTS engines (qimi_1.5,
|
|
6
|
+
qimi_2.5, qimi_3, qimi_5, qimi_5.5) — the last two add 310 built-in system
|
|
7
|
+
voices across 16 languages plus optional user voice cloning. Triggers:
|
|
8
|
+
"đọc văn bản thành giọng nói", "chuyển text thành giọng nói", "clone giọng",
|
|
9
|
+
"tạo giọng riêng", "nhân bản giọng nói", "text to speech", "voice clone",
|
|
10
|
+
"TTS", "read this text aloud", "convert text to audio".
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# quickmagic-tts — Text-to-Speech & Voice Cloning
|
|
14
|
+
|
|
15
|
+
Wraps `qm tts` and `qm voices list/clone/delete`. Use whenever the user wants
|
|
16
|
+
text read aloud as audio, or wants a custom cloned voice for future TTS calls.
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
| Command | Required flags | Key optional flags |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `qm tts` | `--text <t>` or `--file <path>`, `--voice <v>` | `--model <m>` (default `qimi_3`), `--language <l>`, `--speed <n>` (0.5-2.0, qimi_5/qimi_5.5 only), `--style <s>`, `--title <t>`, `--crid <id>`, `--wait`, `--out <dir>` |
|
|
23
|
+
| `qm voices list` | `--model <m>` | `--language <l>`, `--search <s>`, `--limit <n>` (default 50, max 100 — qimi_1.5 has 535 voices and qimi_5 310+: use `--language`/`--search` to narrow instead of paging) |
|
|
24
|
+
| `qm voices clone` | `--audio <file\|url>`, `--name <n>` | `--crid <id>` (auto-generated if omitted), `--wait` |
|
|
25
|
+
| `qm voices delete <id>` | `<id>` (`voc_12` or bare `12`) | — |
|
|
26
|
+
|
|
27
|
+
## Models — always read the price at runtime
|
|
28
|
+
|
|
29
|
+
| Model | Billing | Char limit | Voices |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `qimi_1.5` | flat credits/call | 50,000 | fixed catalog |
|
|
32
|
+
| `qimi_2.5` | flat credits/call | 30,000 | fixed catalog |
|
|
33
|
+
| `qimi_3` (default) | flat credits/call | 30,000 | fixed catalog |
|
|
34
|
+
| `qimi_5` | credits/character, with a minimum charge | 10,000 | 310 system voices (16 languages) + your own clones |
|
|
35
|
+
| `qimi_5.5` | credits/character (higher quality), with a minimum charge | 10,000 | 310 system voices (16 languages) + your own clones |
|
|
36
|
+
|
|
37
|
+
Reference only, as of 260817 — **never quote these from memory**. Always run
|
|
38
|
+
`qm voices list --model <m>` and read the `pricing` object from its response:
|
|
39
|
+
roughly `qimi_1.5`/`qimi_2.5` ≈ 2cr/call, `qimi_3` ≈ 4cr/call, `qimi_5` ≈
|
|
40
|
+
0.09cr/char, `qimi_5.5` ≈ 0.15cr/char (min ~5cr) — rates can change. Voice
|
|
41
|
+
cloning is a separate, much larger flat charge (~2,230cr) — see "Cloning a
|
|
42
|
+
custom voice" below.
|
|
43
|
+
|
|
44
|
+
## Pick a voice
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
qm voices list --model qimi_1.5 --language "Tiếng Việt" # qimi_3 has no per-language labels (all "Đa ngôn ngữ (70+)") — omit --language for it
|
|
48
|
+
qm voices list --model qimi_5 --search Minh --limit 20
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Use the **`VOICE`** column value as `--voice` on `qm tts` — not `NAME`.
|
|
52
|
+
- Pass `--language` whenever a voice name repeats across languages (e.g. an
|
|
53
|
+
`Aarav` voice exists in both English and Hindi) so the server picks the
|
|
54
|
+
right one.
|
|
55
|
+
- `qimi_1.5`/`qimi_2.5`/`qimi_3` list a fixed catalog with a `STYLES` column
|
|
56
|
+
(labels like "Vui vẻ"/"Formal") — pass the exact label as `--style`.
|
|
57
|
+
`qimi_5`/`qimi_5.5` voices don't take `--style`.
|
|
58
|
+
- `qimi_5`/`qimi_5.5` output splits into **"Giọng của bạn"** (voices you
|
|
59
|
+
cloned) and **"Giọng hệ thống"** (310 system voices, 16 languages) —
|
|
60
|
+
cloning is optional, try a system voice first. Each system voice reads
|
|
61
|
+
most naturally in its own source language (it can still read all ~40
|
|
62
|
+
supported languages) — for Vietnamese text, prefer a `Tiếng Việt`-tagged
|
|
63
|
+
voice or a voice you cloned yourself.
|
|
64
|
+
|
|
65
|
+
## Reading text aloud
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
qm tts --text "Xin chào, đây là bản demo." --voice <voice_id> --model qimi_3 --wait --out ./out
|
|
69
|
+
qm tts --file ./script.txt --voice <voice_id> --model qimi_5 --language "Tiếng Việt" --speed 1.1 --wait --out ./out
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- `--file` reads local UTF-8 text — use it instead of `--text` for long
|
|
73
|
+
scripts.
|
|
74
|
+
- The CLI prints the character count before submitting; sanity-check it
|
|
75
|
+
against the model's char limit above to avoid a `char_limit_exceeded`.
|
|
76
|
+
- Without `--wait`, note the printed job id (`tts_<n>`) and run
|
|
77
|
+
`qm jobs wait tts_<n> --out ./out` separately — it auto-downloads the mp3
|
|
78
|
+
(single `result_url`).
|
|
79
|
+
|
|
80
|
+
## Cloning a custom voice (optional, for qimi_5/qimi_5.5)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
qm voices clone --audio ./sample.wav --name "Giọng của tôi" --wait
|
|
84
|
+
qm voices clone --audio https://files.quickmagic.cloud/... --name "Alt voice" --crid my-retry-key
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `--audio` accepts a **local audio file** (auto-uploaded to Quick Magic
|
|
88
|
+
first) or an **existing Quick Magic file URL** — an arbitrary external URL
|
|
89
|
+
is rejected (`invalid_source`).
|
|
90
|
+
- Sample length **10 seconds – 5 minutes**, size **≤ 20MB**.
|
|
91
|
+
- `--crid` is optional here: if omitted the CLI derives one from the file
|
|
92
|
+
bytes + name, so an accidental retry of the exact same file+name never
|
|
93
|
+
double-clones or double-charges.
|
|
94
|
+
- Before running, read `clone_price` and `first_free_available` from
|
|
95
|
+
`qm voices list --model qimi_5` — the **first clone may be free**, but only
|
|
96
|
+
for an account with an **active paid plan** (never on the free tier).
|
|
97
|
+
- Job id is `voc_<n>` and has **no `result_url`**: `qm jobs wait voc_<n>` only
|
|
98
|
+
polls status; once `completed`, run `qm jobs get voc_<n>` and read
|
|
99
|
+
`voice` / `sample_url` (or pass `--wait` to `qm voices clone`, which prints them). Use that same name as `--voice` on `qm tts` with
|
|
100
|
+
`--model qimi_5` (or `qimi_5.5`).
|
|
101
|
+
- Uploaded sample files live in temporary Quick Magic storage (auto-deleted
|
|
102
|
+
after ~5 days) — that's fine, they're only needed once, during cloning.
|
|
103
|
+
|
|
104
|
+
## Deleting a cloned voice
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
qm voices delete voc_12
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
No refund. Fails with `VOICE_BUSY` if the voice is still being processed, or
|
|
111
|
+
if a `qm tts` job is currently reading with it — wait for that job to finish,
|
|
112
|
+
then retry the delete.
|
|
113
|
+
|
|
114
|
+
## Pricing — quote, then confirm
|
|
115
|
+
|
|
116
|
+
1. `qm voices list --model <m> [...]` → read `pricing` (plus `clone_price` /
|
|
117
|
+
`first_free_available` for `qimi_5`/`qimi_5.5`).
|
|
118
|
+
2. State the estimated cost to the user (character count × per-char rate, or
|
|
119
|
+
the flat per-call price) and get explicit confirmation.
|
|
120
|
+
3. Only then run `qm tts` / `qm voices clone` — submission holds credit
|
|
121
|
+
immediately.
|
|
122
|
+
|
|
123
|
+
**Retries (same rule for both `qm tts --crid` and `qm voices clone`):**
|
|
124
|
+
retrying the *same* crid **after a failed** attempt (e.g.
|
|
125
|
+
`insufficient_credit`) starts a **new** job — a failed attempt does not keep
|
|
126
|
+
the crid reserved, so it's always safe to resubmit unchanged. Replaying the
|
|
127
|
+
crid of a job that's still **in progress** instead returns
|
|
128
|
+
`idempotent_replay: true` with the real `credits_held`/`status` — no new job,
|
|
129
|
+
no double charge. `qm voices clone` auto-generates its own crid from the
|
|
130
|
+
file bytes + name when `--crid` is omitted (see "Cloning a custom voice"
|
|
131
|
+
above) — the same failed-vs-in-progress behavior applies to that
|
|
132
|
+
auto-generated crid too.
|
|
133
|
+
|
|
134
|
+
## Errors
|
|
135
|
+
|
|
136
|
+
CLI prints `Lỗi [code]: message`. Common codes:
|
|
137
|
+
|
|
138
|
+
| Code | Meaning | What to do |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `invalid_model` | `--model` not one of the 5 keys (e.g. `v3`) | use `qimi_1.5|qimi_2.5|qimi_3|qimi_5|qimi_5.5` |
|
|
141
|
+
| `voice_not_found` | bad/unknown `--voice` | re-pick from `qm voices list`; if the name repeats across languages, add `--language` |
|
|
142
|
+
| `invalid_style` | `--style` isn't in that voice's `styles` | use the exact label from the `STYLES` column |
|
|
143
|
+
| `invalid_speed` | outside 0.5-2.0, or `--speed` used with qimi_1.5/2.5/3 | clamp to range; only pass `--speed` for qimi_5/qimi_5.5 |
|
|
144
|
+
| `char_limit_exceeded` | text longer than the model's char limit | split the text into smaller chunks |
|
|
145
|
+
| `insufficient_credit` / `negative_balance` | not enough balance | state the exact shortfall and https://quickmagic.vn/pricing |
|
|
146
|
+
| `rate_limited` / `concurrent_limit` / `busy` | too many requests/jobs right now | wait, then retry |
|
|
147
|
+
| `feature_disabled` | TTS or cloning temporarily off | tell the user, don't retry |
|
|
148
|
+
| `VOICE_BUSY` | voice is still cloning, or in use by an in-progress `qm tts` job | wait for that job to finish before deleting/reusing it |
|
|
149
|
+
| `duplicate_name` | clone `--name` already used | pick a different name |
|
|
150
|
+
| `invalid_source` | `--audio` isn't a local file or a Quick Magic URL | re-upload, or pass a valid Quick Magic file URL |
|
|
151
|
+
| `pending_limit` | too many jobs already queued | wait for some to finish |
|
|
152
|
+
|
|
153
|
+
Full auth/job/error-code reference: `quickmagic-account` skill.
|
|
154
|
+
|
|
155
|
+
## Examples
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
qm auth status
|
|
159
|
+
qm voices list --model qimi_1.5 --language "Tiếng Việt" # qimi_3 has no per-language labels (all "Đa ngôn ngữ (70+)") — omit --language for it
|
|
160
|
+
qm tts --text "Chào mừng đến với Quick Magic." --voice <voice_id> --model qimi_3 --wait --out ./out
|
|
161
|
+
|
|
162
|
+
qm voices list --model qimi_5
|
|
163
|
+
qm voices clone --audio ./mysample.wav --name "Giọng riêng" --wait
|
|
164
|
+
qm tts --text "Đọc bằng giọng của tôi." --voice "Giọng riêng" --model qimi_5 --wait --out ./out
|
|
165
|
+
|
|
166
|
+
qm voices delete voc_12
|
|
167
|
+
```
|
package/src/api.js
CHANGED
|
@@ -69,7 +69,9 @@ async function call(method, rest_path, { body } = {}) {
|
|
|
69
69
|
|
|
70
70
|
const json = await res.json().catch(() => ({}));
|
|
71
71
|
if (!res.ok || json.success === false) {
|
|
72
|
-
|
|
72
|
+
const err = new Error(json.message || `HTTP ${res.status}`);
|
|
73
|
+
err.code = json.code; // [plans/260817-2339 P4] giữ code REST để lệnh gọi phía trên in "Lỗi [code]: message"
|
|
74
|
+
throw err;
|
|
73
75
|
}
|
|
74
76
|
return json.data;
|
|
75
77
|
}
|
package/src/commands/generate.js
CHANGED
|
@@ -32,7 +32,8 @@ async function video(options) {
|
|
|
32
32
|
// --mode reference|frames (R2V 260706): server validate theo model; bỏ trống = default model.
|
|
33
33
|
if (options.mode) body.image_mode = options.mode;
|
|
34
34
|
// --video-ref (v2v 260723): file local tự upload presigned → URL host QM; server probe duration
|
|
35
|
-
// + validate (
|
|
35
|
+
// + validate theo cờ supports_video_ref per-model trong registry (260808: seedance-2-0/-fast/-2-5;
|
|
36
|
+
// danh sách ĐỘNG phía server — CLI không giữ list cứng), không mix ảnh, giá hạng with-video.
|
|
36
37
|
if (options.videoRef) body.video_ref_url = await resolveMediaInput(options.videoRef);
|
|
37
38
|
// [260726] 4 tham số nâng cao — server đã nhận sẵn, CLI chỉ chưa mở.
|
|
38
39
|
// --no-audio: commander set options.audio = false; mặc định undefined (giữ default của model).
|
package/src/commands/jobs.js
CHANGED
|
@@ -8,6 +8,38 @@ const POLL_INTERVAL_MS = 2500;
|
|
|
8
8
|
const TERMINAL_STATUSES = new Set(['completed', 'error', 'failed', 'success', 'stalled', 'timeout']);
|
|
9
9
|
const SUCCESS_STATUSES = new Set(['completed', 'success']);
|
|
10
10
|
|
|
11
|
+
// [P3 portrait, plans/260818-1521 bước 9] Ảnh người thật Seedance 2.x đi qua kho ảo pass-through —
|
|
12
|
+
// job có thể đứng "đang xử lý" tới ~10' ở lần đầu dùng 1 ảnh. R12: jobs-view có thể còn đánh dấu
|
|
13
|
+
// job xếp hàng >15' là 'stalled' (fix ở BE phase sau) → agent/script CHẠY --wait tuyệt đối KHÔNG
|
|
14
|
+
// được suy ra "job kẹt, tạo job mới thử lại" (tạo job đôi, trừ tiền 2 lần). portrait_state đọc trực
|
|
15
|
+
// tiếp từ response — override cách hiểu bất kể server còn gắn status gì.
|
|
16
|
+
const PORTRAIT_PREPARING_STATES = new Set(['awaiting_asset', 'asset_ready']);
|
|
17
|
+
|
|
18
|
+
// Gợi ý theo error_code B′ (đối chiếu backend-quick-magic/services/portrait/portrait-errors.js — 17 mã).
|
|
19
|
+
const PORTRAIT_HINTS = {
|
|
20
|
+
PORTRAIT_PUBLIC_FIGURE: 'dùng ảnh người thường, không phải người nổi tiếng',
|
|
21
|
+
PORTRAIT_MINOR: 'dùng ảnh người lớn, không phải trẻ em',
|
|
22
|
+
PORTRAIT_NSFW: 'dùng ảnh khác, tránh nội dung nhạy cảm',
|
|
23
|
+
PORTRAIT_GEMINI_UNAVAILABLE: 'thử lại sau vài phút',
|
|
24
|
+
PORTRAIT_ASSET_FAILED: 'thử ảnh khác, rõ mặt, chỉ 1 người',
|
|
25
|
+
PORTRAIT_ASSET_REJECTED: 'ảnh bị bộ lọc nội dung từ chối (người nổi tiếng/trẻ em/nhạy cảm/bản quyền) — dùng ảnh khác',
|
|
26
|
+
PORTRAIT_TIMEOUT: 'thử lại sau vài phút',
|
|
27
|
+
PORTRAIT_QUOTA_FULL: 'thử lại sau vài phút',
|
|
28
|
+
PORTRAIT_IMAGE_HOST: 'tải ảnh lên QM trước (dùng --image <file>)',
|
|
29
|
+
PORTRAIT_IMAGE_FETCH_FAILED: 'thử lại sau vài phút',
|
|
30
|
+
PORTRAIT_PROVIDER_ERROR: 'thử lại sau vài phút',
|
|
31
|
+
PORTRAIT_PROVIDER_FAILED: 'ảnh bị từ chối theo chính sách nội dung (người nổi tiếng/trẻ em/nhạy cảm/bản quyền) — dùng ảnh khác',
|
|
32
|
+
PORTRAIT_COPYRIGHT: 'ảnh bị từ chối vì bản quyền/quyền hình ảnh (người nổi tiếng, nhân vật/tác phẩm được bảo hộ) — dùng ảnh khác',
|
|
33
|
+
PORTRAIT_DISABLED: 'đổi --model gemini-omni (nhận ảnh người thật)',
|
|
34
|
+
PORTRAIT_TOO_MANY_IMAGES: 'bớt số ảnh có người thật trong yêu cầu',
|
|
35
|
+
PORTRAIT_VIDEO_REF_UNSUPPORTED: 'dùng --image thay --video-ref, hoặc đổi model',
|
|
36
|
+
REAL_PERSON_BLOCKED: 'đổi --model gemini-omni (nhận ảnh người thật)',
|
|
37
|
+
};
|
|
38
|
+
function hintFor(code) {
|
|
39
|
+
const h = PORTRAIT_HINTS[code];
|
|
40
|
+
return h ? ` — gợi ý: ${h}` : '';
|
|
41
|
+
}
|
|
42
|
+
|
|
11
43
|
function sleep(ms) {
|
|
12
44
|
return new Promise((r) => setTimeout(r, ms));
|
|
13
45
|
}
|
|
@@ -36,11 +68,23 @@ async function waitOne(id, out_dir) {
|
|
|
36
68
|
for (;;) {
|
|
37
69
|
const data = await api.call('GET', `/jobs/${id}`);
|
|
38
70
|
const status = data.status;
|
|
71
|
+
|
|
72
|
+
// [P3 portrait, R12] job đang chuẩn bị ảnh có người thật — KHÔNG coi là stalled/lỗi, KHÔNG gợi ý
|
|
73
|
+
// chạy lại lệnh dù đợi lâu. In thẳng rồi tiếp tục poll, bỏ qua nhánh TERMINAL_STATUSES lượt này
|
|
74
|
+
// (server có deadline riêng ~10' sẽ tự chuyển job sang failed thật nếu quá hạn — vòng lặp không treo mãi).
|
|
75
|
+
if (PORTRAIT_PREPARING_STATES.has(data.portrait_state)) {
|
|
76
|
+
console.log(`[${id}] đang chuẩn bị ảnh có người thật (tối đa ~10 phút) — đừng tạo job mới…`);
|
|
77
|
+
await sleep(POLL_INTERVAL_MS);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
39
81
|
const progress = data.progress != null ? ` (${data.progress}%)` : '';
|
|
40
82
|
console.log(`[${id}] ${status}${progress}`);
|
|
41
83
|
if (TERMINAL_STATUSES.has(status)) {
|
|
42
84
|
if (SUCCESS_STATUSES.has(status)) {
|
|
43
85
|
if (data.result_url && out_dir) await download(data.result_url, out_dir);
|
|
86
|
+
} else if (data.error_code) {
|
|
87
|
+
console.error(`[${id}] Lỗi [${data.error_code}]: ${data.error || status}${hintFor(data.error_code)}`);
|
|
44
88
|
} else {
|
|
45
89
|
console.error(`[${id}] job lỗi: ${data.error || status}`);
|
|
46
90
|
}
|
package/src/commands/models.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// commands/models.js — liệt kê model ảnh/video dạng bảng key | label | credit (+IMAGES cho
|
|
1
|
+
// commands/models.js — liệt kê model ảnh/video/nhạc dạng bảng key | label | credit (+IMAGES cho
|
|
2
|
+
// video). `qm models list --type music` dùng chung hàm này; bảng ĐẦY ĐỦ hơn (giá/ETA/hàng chờ) nằm
|
|
3
|
+
// ở `qm music models` (commands/music.js) — dành cho ai đã quen `qm models list` chung 1 lệnh.
|
|
2
4
|
const api = require('../api');
|
|
3
5
|
|
|
4
6
|
// Cột IMAGES (video): "ref 9* · frames 2" — theo image_max_by_mode; * = mode cấm ảnh người thật.
|
|
@@ -39,7 +41,15 @@ async function list(options) {
|
|
|
39
41
|
console.log(`${r.key.padEnd(key_w)} ${r.label.padEnd(label_w)} ${r.credit.padEnd(credit_w)}${is_video ? ` ${r.images}` : ''}`);
|
|
40
42
|
}
|
|
41
43
|
if (is_video && rows.some((r) => r.images.includes('*'))) {
|
|
42
|
-
console.log('\n* KHÔNG nhận ảnh
|
|
44
|
+
console.log('\n* KHÔNG nhận ảnh người upload/từ ngoài (gửi vào sẽ lỗi, không mất credit).');
|
|
45
|
+
console.log(' Muốn có nhân vật với model *: tạo ảnh bằng "qm generate image -m seedream-5.0-pro" ngay trong Quick Magic');
|
|
46
|
+
console.log(' rồi dùng ảnh đó (provider chấp nhận ảnh thuần Seedream 5.0 Pro — đã probe thật 260808).');
|
|
47
|
+
console.log(' Hoặc dùng model không dấu *: gemini-omni / wan-3-0 / wan-2-7 / seedance 1.x nhận ảnh người trực tiếp.');
|
|
48
|
+
}
|
|
49
|
+
// [Portrait 260819] server bật pass-through → model từng cấm người thật giờ trả real_person_note (không còn dấu *)
|
|
50
|
+
if (is_video && models.some((m) => m.real_person_note)) {
|
|
51
|
+
console.log('\nℹ Ảnh người thật: Seedance 2.x nhận bình thường — hệ thống chuẩn bị ảnh thêm ~1-3 phút ở lần đầu dùng 1 ảnh');
|
|
52
|
+
console.log(' (qm jobs wait tự chờ). Nếu bộ lọc nội dung của model từ chối (bản quyền/chính sách), job failed kèm lý do rõ, credit hoàn.');
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// commands/music.js — qm music create/lyrics/models/get (Xưởng Nhạc Melo: REST /music, /music/lyrics,
|
|
2
|
+
// /models?type=music, /jobs/mus_<n>). Tách khỏi tools.js vì cần đọc file lời + gộp --wait + in bảng.
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { call } = require('../api');
|
|
6
|
+
const jobs = require('./jobs');
|
|
7
|
+
|
|
8
|
+
const out = (data) => console.log(JSON.stringify(data, null, 2));
|
|
9
|
+
// [mirror voice.js] api.js đã gắn err.code từ REST {code,message} → in kèm code để rẽ nhánh (retryable
|
|
10
|
+
// 5xx/429 vs không 4xx).
|
|
11
|
+
const run = (fn) => (...a) => fn(...a).catch((e) => {
|
|
12
|
+
console.error(e.code ? `Lỗi [${e.code}]: ${e.message}` : `Lỗi: ${e.message}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Bảng text căn cột đơn giản — repo không có thư viện bảng, KHÔNG thêm dependency mới (mirror voice.js).
|
|
17
|
+
function printTable(headers, rows) {
|
|
18
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] == null ? '' : r[i]).length)));
|
|
19
|
+
const line = (cells) => cells.map((c, i) => String(c == null ? '' : c).padEnd(widths[i])).join(' ');
|
|
20
|
+
console.log(line(headers));
|
|
21
|
+
console.log(widths.map((w) => '-'.repeat(w)).join(' '));
|
|
22
|
+
for (const r of rows) console.log(line(r));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function priceLabel(m) {
|
|
26
|
+
return m.free_launch ? 'Miễn phí (giai đoạn ra mắt)' : `${m.credit} credit/bài`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── music models ──
|
|
30
|
+
const models = run(async () => {
|
|
31
|
+
const data = await call('GET', '/models?type=music');
|
|
32
|
+
const rows = data.models || [];
|
|
33
|
+
if (!rows.length) {
|
|
34
|
+
console.log('Không có model nào.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
printTable(
|
|
38
|
+
['KEY', 'LABEL', 'GIÁ', 'ETA (s)', 'HÀNG CHỜ'],
|
|
39
|
+
rows.map((m) => [m.key, m.label, priceLabel(m), m.eta_seconds, m.queue_capacity]),
|
|
40
|
+
);
|
|
41
|
+
if (rows.some((m) => m.free_launch)) {
|
|
42
|
+
console.log('\nGiá đang MIỄN PHÍ giai đoạn ra mắt — có thể đổi bất kỳ lúc nào, luôn kiểm tra lại trước khi tạo bài.');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── music lyrics — AI viết lời (0đ) ──
|
|
47
|
+
const lyrics = run(async (o) => {
|
|
48
|
+
if (!o.desc) throw new Error('Cần --desc');
|
|
49
|
+
const data = await call('POST', '/music/lyrics', {
|
|
50
|
+
body: { description: o.desc, title: o.title, language: o.language, instrumental: !!o.instrumental },
|
|
51
|
+
});
|
|
52
|
+
console.log(`Tiêu đề: ${data.title}`);
|
|
53
|
+
console.log(`Phong cách: ${data.style_tags}`);
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(data.lyrics || '(bản không lời — chỉ có tiêu đề + phong cách)');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── music create ──
|
|
59
|
+
const create = run(async (o) => {
|
|
60
|
+
// [P06b-L5 fix 260818] Server (validateMinimal, music-submit-service.js) CHẤP instrumental chỉ cần
|
|
61
|
+
// styles HOẶC description, không cần cả 2 — guard cục bộ cũ hẹp hơn hợp đồng, chặn oan
|
|
62
|
+
// `--instrumental --styles "lofi"` không --desc. Nới điều kiện cho khớp.
|
|
63
|
+
if (!o.desc && !o.lyrics && !o.lyricsFile && !(o.instrumental && o.styles)) {
|
|
64
|
+
throw new Error('Cần --desc (mode simple/instrumental) hoặc --lyrics/--lyrics-file (mode custom) hoặc --instrumental kèm --styles');
|
|
65
|
+
}
|
|
66
|
+
// [P06b-L5 fix 260818] --out mà thiếu --wait trước đây bị LỜ ÂM THẦM (waitAll chỉ chạy khi o.wait) —
|
|
67
|
+
// tự bật --wait để --out thực sự có tác dụng, kèm cảnh báo cho user biết.
|
|
68
|
+
if (o.out && !o.wait) {
|
|
69
|
+
console.log('Lưu ý: có --out nhưng thiếu --wait — tự động bật --wait để tải file khi bài xong.');
|
|
70
|
+
o.wait = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// [KI #11] Đọc giá + free_launch NGAY TRƯỚC submit — gửi kèm expected_free chống lệch giá giữa lúc
|
|
74
|
+
// quote và lúc submit thật (server fail-closed trả price_changed nếu lệch, KHÔNG âm thầm thu tiền).
|
|
75
|
+
const catalog = await call('GET', '/models?type=music');
|
|
76
|
+
const model_key = o.model || 'melo-3';
|
|
77
|
+
const m = (catalog.models || []).find((x) => x.key === model_key);
|
|
78
|
+
if (!m) {
|
|
79
|
+
console.log(`Cảnh báo: không tìm thấy model "${model_key}" trong catalog hiện tại — vẫn thử gửi, server sẽ báo lỗi nếu sai.`);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(priceLabel(m));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lyrics_text = o.lyricsFile ? fs.readFileSync(path.resolve(o.lyricsFile), 'utf8') : o.lyrics;
|
|
85
|
+
const data = await call('POST', '/music', {
|
|
86
|
+
body: {
|
|
87
|
+
mode: o.mode || 'simple', description: o.desc, lyrics: lyrics_text, title: o.title, model: model_key,
|
|
88
|
+
styles: o.styles ? o.styles.split(',').map((s) => s.trim()).filter(Boolean) : undefined,
|
|
89
|
+
instrumental: !!o.instrumental, vocal_gender: o.gender,
|
|
90
|
+
expected_free: m ? m.free_launch : undefined, client_request_id: o.crid,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
out(data);
|
|
94
|
+
console.log(`\n→ Theo dõi: qm music get ${data.job_id}`);
|
|
95
|
+
if (o.wait) {
|
|
96
|
+
// [KI #16] --out là THƯ MỤC (jobs.waitAll → fs.mkdirSync(out_dir) rồi ghi basename từ URL) —
|
|
97
|
+
// truyền --out song.mp3 sẽ tạo THƯ MỤC tên song.mp3, không phải file. Dùng --out ./out.
|
|
98
|
+
await jobs.waitAll([data.job_id], o.out);
|
|
99
|
+
out(await call('GET', `/jobs/${data.job_id}`));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── music get — chấp cả mus_<n> lẫn số trần ──
|
|
104
|
+
const get = run(async (id) => {
|
|
105
|
+
const job_id = String(id).startsWith('mus_') ? id : `mus_${id}`;
|
|
106
|
+
out(await call('GET', `/jobs/${job_id}`));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
module.exports = { create, lyrics, models, get };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// commands/voice.js — qm tts + qm voices list/clone/delete (4 endpoint REST P2: GET /voices,
|
|
2
|
+
// POST /tts, POST /voice-clones, DELETE /voice-clones/:voice_id). Tách khỏi tools.js vì 4 lệnh
|
|
3
|
+
// này cần đọc file text, đếm ký tự, in bảng, gộp --wait — nhét vào phá quy ước 1-dòng/lệnh.
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { call } = require('../api');
|
|
8
|
+
const { resolveMediaInput } = require('../media');
|
|
9
|
+
const jobs = require('./jobs');
|
|
10
|
+
|
|
11
|
+
const out = (data) => console.log(JSON.stringify(data, null, 2));
|
|
12
|
+
// [red-team #15] api.js đã gắn err.code từ REST {code,message} → in kèm code để agent rẽ nhánh
|
|
13
|
+
// (retryable 5xx/429 vs không 4xx như SKILL dặn); lỗi cục bộ (vd thiếu --text) không có code.
|
|
14
|
+
const run = (fn) => (...a) => fn(...a).catch((e) => {
|
|
15
|
+
console.error(e.code ? `Lỗi [${e.code}]: ${e.message}` : `Lỗi: ${e.message}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Query string cho GET /voices — bỏ param rỗng/undefined (chỉ model bắt buộc).
|
|
20
|
+
function buildQuery(params) {
|
|
21
|
+
const parts = Object.entries(params)
|
|
22
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
23
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`);
|
|
24
|
+
return parts.length ? `?${parts.join('&')}` : '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Bảng text căn cột đơn giản — repo không có thư viện bảng, KHÔNG thêm dependency mới.
|
|
28
|
+
function printTable(headers, rows) {
|
|
29
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] == null ? '' : r[i]).length)));
|
|
30
|
+
const line = (cells) => cells.map((c, i) => String(c == null ? '' : c).padEnd(widths[i])).join(' ');
|
|
31
|
+
console.log(line(headers));
|
|
32
|
+
console.log(widths.map((w) => '-'.repeat(w)).join(' '));
|
|
33
|
+
for (const r of rows) console.log(line(r));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printPricing(p) {
|
|
37
|
+
if (p.billing === 'flat') console.log(`Giá: ${p.credits} credit/lần`);
|
|
38
|
+
else console.log(`Giá: ${p.credits_per_char} credit/ký tự, tối thiểu ${p.min_charge}`);
|
|
39
|
+
console.log(`Trần ký tự: ${p.char_limit}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── tts ──
|
|
43
|
+
const tts = run(async (o) => {
|
|
44
|
+
if (!o.text && !o.file) throw new Error('Cần --text hoặc --file');
|
|
45
|
+
const text = o.text || fs.readFileSync(path.resolve(o.file), 'utf8');
|
|
46
|
+
console.log(`Số ký tự: ${text.length}`);
|
|
47
|
+
const data = await call('POST', '/tts', {
|
|
48
|
+
body: {
|
|
49
|
+
model: o.model, voice: o.voice, language: o.language, text,
|
|
50
|
+
speed: o.speed ? Number(o.speed) : undefined, style: o.style, title: o.title,
|
|
51
|
+
client_request_id: o.crid,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
out(data);
|
|
55
|
+
console.log(`\n→ Theo dõi: qm jobs get ${data.job_id}`);
|
|
56
|
+
if (o.wait) {
|
|
57
|
+
// waitAll KHÔNG trả data (chỉ poll + tự tải khi có result_url đơn) — gọi lại /jobs để in result_url.
|
|
58
|
+
await jobs.waitAll([data.job_id], o.out);
|
|
59
|
+
const final = await call('GET', `/jobs/${data.job_id}`);
|
|
60
|
+
console.log(`result_url: ${final.result_url || '(chưa có)'}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ── voices list ──
|
|
65
|
+
const voicesList = run(async (o) => {
|
|
66
|
+
const data = await call('GET', `/voices${buildQuery({ model: o.model, language: o.language, search: o.search, limit: o.limit })}`);
|
|
67
|
+
const pricing = data.pricing || {}; // [review C4] BE lỗi/shape cũ → không crash, in được phần còn lại
|
|
68
|
+
printPricing(pricing);
|
|
69
|
+
if (pricing.billing === 'per_char') {
|
|
70
|
+
if (data.clone_price != null) console.log(`Giá clone giọng riêng: ${data.clone_price} credit`);
|
|
71
|
+
// [review C2] false có 2 nghĩa: (a) đã dùng suất, (b) chưa có gói trả phí active (BE trả first_free_upsell=true).
|
|
72
|
+
if (data.first_free_available != null) {
|
|
73
|
+
const free_msg = data.first_free_available ? 'còn (giọng #1 = 0 credit)'
|
|
74
|
+
: data.first_free_upsell ? 'chỉ dành cho tài khoản có gói trả phí đang hoạt động (https://quickmagic.vn/pricing)'
|
|
75
|
+
: 'đã dùng';
|
|
76
|
+
console.log(`Suất clone miễn phí lần đầu: ${free_msg}`);
|
|
77
|
+
}
|
|
78
|
+
if (data.hint) console.log(data.hint);
|
|
79
|
+
}
|
|
80
|
+
console.log('');
|
|
81
|
+
if (pricing.billing === 'flat') {
|
|
82
|
+
printTable(
|
|
83
|
+
['VOICE', 'NAME', 'GENDER', 'LANGUAGE', 'STYLES'],
|
|
84
|
+
data.voices.map((v) => [v.voice, v.name, v.gender, v.language, (v.styles || []).join(', ').slice(0, 40)]),
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
const clones = data.voices.filter((v) => v.kind === 'clone');
|
|
88
|
+
const systems = data.voices.filter((v) => v.kind === 'system');
|
|
89
|
+
if (clones.length) {
|
|
90
|
+
console.log('Giọng của bạn:');
|
|
91
|
+
printTable(['VOICE', 'STATUS', 'SAMPLE'], clones.map((v) => [v.voice, v.status, v.sample_url]));
|
|
92
|
+
console.log('');
|
|
93
|
+
}
|
|
94
|
+
if (systems.length) {
|
|
95
|
+
console.log('Giọng hệ thống:');
|
|
96
|
+
printTable(['VOICE', 'NAME', 'NAME_EN', 'GENDER', 'LANGUAGE'], systems.map((v) => [v.voice, v.name, v.name_en, v.gender, v.language]));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
console.log(`\nNgôn ngữ: ${(data.languages || []).join(' · ')}`);
|
|
100
|
+
console.log(`Tổng: ${data.total} — Hiển thị: ${data.returned}`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// [red-team #14] auto-crid = hash(bytes file cục bộ | chuỗi URL) + hash(name) — retry vô tình
|
|
104
|
+
// (mất mạng, gõ lại lệnh) không tạo 2 voice/2 hold 2.230cr khi user không tự truyền --crid.
|
|
105
|
+
function autoCrid(audio_ref, name) {
|
|
106
|
+
const is_url = /^https?:\/\//i.test(audio_ref);
|
|
107
|
+
let source = audio_ref;
|
|
108
|
+
if (!is_url) {
|
|
109
|
+
const abs_path = path.resolve(audio_ref);
|
|
110
|
+
if (!fs.existsSync(abs_path)) throw new Error(`Không thấy file: ${audio_ref}`);
|
|
111
|
+
source = fs.readFileSync(abs_path);
|
|
112
|
+
}
|
|
113
|
+
const h1 = crypto.createHash('sha256').update(source).digest('hex').slice(0, 32);
|
|
114
|
+
const h2 = crypto.createHash('sha256').update(name).digest('hex').slice(0, 8);
|
|
115
|
+
return `cli-${h1}-${h2}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── voices clone ──
|
|
119
|
+
const voicesClone = run(async (o) => {
|
|
120
|
+
const crid = o.crid || autoCrid(o.audio, o.name);
|
|
121
|
+
const audio_url = await resolveMediaInput(o.audio);
|
|
122
|
+
const data = await call('POST', '/voice-clones', { body: { audio_url, name: o.name, client_request_id: crid } });
|
|
123
|
+
out(data);
|
|
124
|
+
console.log(`\nclient_request_id: ${crid}`);
|
|
125
|
+
console.log(`→ Dùng --voice "${o.name}" ở qm tts (model qimi_5/qimi_5.5) sau khi status completed`);
|
|
126
|
+
if (o.wait) {
|
|
127
|
+
// Job clone (voc_) không có result_url ⇒ --wait chỉ poll trạng thái; in voice/sample_url sau khi xong.
|
|
128
|
+
await jobs.waitAll([data.voice_id]);
|
|
129
|
+
const final = await call('GET', `/jobs/${data.voice_id}`);
|
|
130
|
+
console.log(`voice: ${final.voice || ''}`);
|
|
131
|
+
console.log(`sample_url: ${final.sample_url || '(chưa có)'}`);
|
|
132
|
+
console.log(`status: ${final.status}`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── voices delete ──
|
|
137
|
+
const voicesDelete = run(async (id) => {
|
|
138
|
+
out(await call('DELETE', `/voice-clones/${encodeURIComponent(String(id).replace(/^voc_/, ''))}`));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
module.exports = { tts, voicesList, voicesClone, voicesDelete };
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,8 @@ const jobs = require('./commands/jobs');
|
|
|
9
9
|
const models = require('./commands/models');
|
|
10
10
|
const credits = require('./commands/credits');
|
|
11
11
|
const tools = require('./commands/tools');
|
|
12
|
+
const voice = require('./commands/voice');
|
|
13
|
+
const music = require('./commands/music');
|
|
12
14
|
|
|
13
15
|
const collect = (v, acc) => { acc.push(v); return acc; };
|
|
14
16
|
|
|
@@ -50,7 +52,7 @@ gen_cmd
|
|
|
50
52
|
.option('--resolution <r>', 'Độ phân giải (vd 720p, 1080p)')
|
|
51
53
|
.option('--aspect-ratio <r>', 'Tỉ lệ khung')
|
|
52
54
|
.option('--image <i...>', 'Ảnh đầu vào (URL hoặc file local, lặp nhiều lần — max theo model+mode, xem qm models)')
|
|
53
|
-
.option('--mode <m>', 'Chế độ ảnh: reference (nhiều ảnh tham chiếu) | frames (khung đầu/cuối, max 2). Seedance 2.x
|
|
55
|
+
.option('--mode <m>', 'Chế độ ảnh: reference (nhiều ảnh tham chiếu) | frames (khung đầu/cuối, max 2). Seedance 2.x nhận ảnh người thật ở mọi chế độ (hệ thống chuẩn bị ảnh thêm ~1-3 phút lần đầu; nếu bộ lọc nội dung của model từ chối — bản quyền/chính sách — CLI in rõ lý do, credit hoàn; xem README mục "Ảnh người thật")')
|
|
54
56
|
.option('--video-ref <v>', 'Video tham chiếu (URL hoặc file local, max 15s/100MB) — CHỈ model supports_video_ref (seedance-2-0/-fast). Giá = rate with-video × (giây output + giây video); không dùng chung với --image')
|
|
55
57
|
.option('--seed <n>', 'Seed cố định — cùng seed + cùng prompt cho kết quả LẶP LẠI được (giữ nhất quán khi render nhiều cảnh)', parseInt)
|
|
56
58
|
.option('--negative <p>', 'Mô tả thứ KHÔNG muốn xuất hiện (watermark, chữ, tay thừa…) — model nào không hỗ trợ thì bỏ qua')
|
|
@@ -92,7 +94,7 @@ const models_cmd = program.command('models').description('Model AI');
|
|
|
92
94
|
models_cmd
|
|
93
95
|
.command('list')
|
|
94
96
|
.description('Liệt kê model')
|
|
95
|
-
.option('--type <t>', 'Loại: image | video', 'image')
|
|
97
|
+
.option('--type <t>', 'Loại: image | video | music', 'image')
|
|
96
98
|
.action(models.list);
|
|
97
99
|
|
|
98
100
|
// ── credits ──────────────────────────────────────────────────────────────────
|
|
@@ -148,6 +150,80 @@ program.command('motion <video_url>').description('Áp chuyển động video v
|
|
|
148
150
|
.requiredOption('--image <url...>', 'Ảnh áp motion (1-20)')
|
|
149
151
|
.option('--model <m>').option('--mode <m>', 'standard|professional').option('--prompt <p>').option('--crid <id>').action(tools.motion);
|
|
150
152
|
|
|
153
|
+
// ── TTS / Voices ─────────────────────────────────────────────────────────────
|
|
154
|
+
program
|
|
155
|
+
.command('tts')
|
|
156
|
+
.description('Chuyển văn bản → giọng nói (5 engine qimi_1.5/2.5/3/5/5.5)')
|
|
157
|
+
.option('--text <t>', 'Văn bản cần đọc')
|
|
158
|
+
.option('--file <path>', 'Đọc văn bản từ file local (thay --text)')
|
|
159
|
+
.option('--model <m>', 'qimi_1.5|qimi_2.5|qimi_3|qimi_5|qimi_5.5', 'qimi_3')
|
|
160
|
+
.requiredOption('--voice <v>', 'Tên giọng — xem cột VOICE của qm voices list')
|
|
161
|
+
.option('--language <l>', 'Ngôn ngữ — phân biệt giọng trùng tên (xem qm voices list)')
|
|
162
|
+
.option('--speed <n>', 'Tốc độ đọc 0.5-2.0 (mặc định 1) — CHỈ qimi_5/qimi_5.5')
|
|
163
|
+
.option('--style <s>', 'Phong cách đọc — nhãn từ cột STYLES của qm voices list')
|
|
164
|
+
.option('--title <t>', 'Tiêu đề job')
|
|
165
|
+
.option('--crid <id>', 'client_request_id')
|
|
166
|
+
.option('--wait', 'Chờ tới khi job hoàn tất')
|
|
167
|
+
.option('--out <dir>', 'Thư mục lưu file mp3 (dùng kèm --wait)')
|
|
168
|
+
.action(voice.tts);
|
|
169
|
+
|
|
170
|
+
const voices_cmd = program.command('voices').description('Giọng đọc: catalog + giọng clone của bạn');
|
|
171
|
+
voices_cmd
|
|
172
|
+
.command('list')
|
|
173
|
+
.description('Liệt kê giọng theo model')
|
|
174
|
+
.requiredOption('--model <m>', 'qimi_1.5|qimi_2.5|qimi_3|qimi_5|qimi_5.5')
|
|
175
|
+
.option('--language <l>', 'Lọc theo ngôn ngữ')
|
|
176
|
+
.option('--search <s>', 'Lọc theo tên (không phân biệt hoa/thường)')
|
|
177
|
+
.option('--limit <n>', 'Số lượng tối đa (mặc định 50, trần 100)')
|
|
178
|
+
.action(voice.voicesList);
|
|
179
|
+
voices_cmd
|
|
180
|
+
.command('clone')
|
|
181
|
+
.description('Nhân bản giọng nói từ file ghi âm mẫu (dùng cho qimi_5/qimi_5.5)')
|
|
182
|
+
.requiredOption('--audio <file|url>', 'File local mp3/wav/m4a 10s-5 phút ≤20MB (tự upload) hoặc URL do Quick Magic cấp (files.quickmagic.cloud)')
|
|
183
|
+
.requiredOption('--name <n>', 'Tên giọng — dùng lại ở --voice của qm tts')
|
|
184
|
+
.option('--crid <id>', 'client_request_id (mặc định tự sinh từ hash file+tên)')
|
|
185
|
+
.option('--wait', 'Chờ tới khi giọng xử lý xong')
|
|
186
|
+
.action(voice.voicesClone);
|
|
187
|
+
voices_cmd
|
|
188
|
+
.command('delete <id>')
|
|
189
|
+
.description('Xoá giọng đã clone')
|
|
190
|
+
.action(voice.voicesDelete);
|
|
191
|
+
|
|
192
|
+
// ── Xưởng Nhạc Melo ──────────────────────────────────────────────────────────
|
|
193
|
+
const music_cmd = program.command('music').description('Xưởng Nhạc Melo — tạo nhạc AI (mô tả/lời → mp3)');
|
|
194
|
+
music_cmd
|
|
195
|
+
.command('create')
|
|
196
|
+
.description('Tạo bài hát AI (mode simple: AI viết lời từ mô tả; mode custom: tự nhập lời)')
|
|
197
|
+
.option('--mode <m>', 'simple (mặc định, AI viết lời) | custom (tự nhập lời)', 'simple')
|
|
198
|
+
.option('--desc <d>', 'Mô tả bài hát — bắt buộc mode simple, hoặc bản không lời')
|
|
199
|
+
.option('--lyrics <l>', 'Lời bài hát (mode custom)')
|
|
200
|
+
.option('--lyrics-file <path>', 'Đọc lời từ file local UTF-8 (thay --lyrics)')
|
|
201
|
+
.option('--styles <s>', 'Phong cách nhạc, phân cách dấu phẩy (vd "V-pop, Ballad")')
|
|
202
|
+
.option('--title <t>', 'Tiêu đề bài hát')
|
|
203
|
+
.option('--model <m>', 'melo-3 (mặc định, mới nhất) | melo-2 (thế hệ trước)', 'melo-3')
|
|
204
|
+
.option('--instrumental', 'Bản không lời')
|
|
205
|
+
.option('--gender <g>', 'auto (mặc định) | male | female — chỉ áp dụng khi có lời')
|
|
206
|
+
.option('--crid <id>', 'client_request_id')
|
|
207
|
+
.option('--wait', 'Chờ tới khi job hoàn tất')
|
|
208
|
+
.option('--out <dir>', 'Thư mục lưu file mp3 (tự bật --wait nếu chưa có)')
|
|
209
|
+
.action(music.create);
|
|
210
|
+
music_cmd
|
|
211
|
+
.command('lyrics')
|
|
212
|
+
.description('AI viết lời bài hát (0đ) — xem trước/chỉnh trước khi dùng mode custom')
|
|
213
|
+
.requiredOption('--desc <d>', 'Mô tả bài hát')
|
|
214
|
+
.option('--title <t>', 'Tiêu đề gợi ý')
|
|
215
|
+
.option('--language <l>', 'Ngôn ngữ (mặc định auto)')
|
|
216
|
+
.option('--instrumental', 'Chỉ cần tiêu đề + phong cách, không cần lời')
|
|
217
|
+
.action(music.lyrics);
|
|
218
|
+
music_cmd
|
|
219
|
+
.command('models')
|
|
220
|
+
.description('Liệt kê model nhạc + giá + ETA + hàng chờ')
|
|
221
|
+
.action(music.models);
|
|
222
|
+
music_cmd
|
|
223
|
+
.command('get <id>')
|
|
224
|
+
.description('Xem chi tiết job nhạc (mus_<n> hoặc số trần)')
|
|
225
|
+
.action(music.get);
|
|
226
|
+
|
|
151
227
|
// ── Cutout Studio ────────────────────────────────────────────────────────────
|
|
152
228
|
program.command('cutout').description('Tách nền / tạo ảnh cutout trong suốt (PNG)')
|
|
153
229
|
.option('--operation <o>', 'generate|from_ref|remix_describe', 'from_ref')
|