quickmagic-cli 1.2.0 → 1.4.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 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 8 Claude **Agent Skills** and an **MCP server** integration for
11
+ `axios`). Also ships with 9 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,10 @@ 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 | — |
150
154
 
151
155
  ### Nhập liệu (miễn phí)
152
156
 
@@ -223,6 +227,11 @@ qm split https://www.tiktok.com/@user/video/123 --mode auto
223
227
  qm stt https://cdn.example.com/audio.mp3 --translate vi
224
228
  qm motion https://cdn.example.com/dance.mp4 --image ./photo.jpg
225
229
 
230
+ # Đọc văn bản thành giọng nói — xem giá + chọn giọng trước khi chạy
231
+ qm voices list --model qimi_1.5 --language "Tiếng Việt"
232
+ qm tts --text "Xin chào Quick Magic" --voice <voice_id> --model qimi_3 --wait --out ./out
233
+ qm voices clone --audio ./sample.wav --name "Giọng của tôi" --wait
234
+
226
235
  # Video marketing theo mode + phân tích video đối thủ → kịch bản
227
236
  qm marketing modes
228
237
  qm marketing video --mode 3 --product-id 42 --duration 10
@@ -258,7 +267,7 @@ qm credits # chạy không cần browser
258
267
 
259
268
  ## Agent Skills
260
269
 
261
- CLI này đi kèm 8 **Agent Skills** (`skills/quickmagic-*/SKILL.md`) để coding agent
270
+ CLI này đi kèm 9 **Agent Skills** (`skills/quickmagic-*/SKILL.md`) để coding agent
262
271
  (Claude Code...) dùng CLI trực tiếp — báo giá + xin xác nhận trước khi tốn credit,
263
272
  tự `jobs wait` thay vì poll tay, chủ động báo Qimi free-window. Danh sách đầy đủ và quy ước
264
273
  chung: xem `skills/README.md`.
@@ -269,7 +278,7 @@ Cách 1 — `npx skills` (nếu dùng công cụ [`skills`](https://www.npmjs.co
269
278
  quản lý Agent Skills từ GitHub):
270
279
 
271
280
  ```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
281
+ 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
273
282
  ```
274
283
 
275
284
  Cách 2 — copy thủ công vào thư mục skills của Claude Code:
@@ -283,8 +292,9 @@ cp -r skills/quickmagic-* ~/.claude/skills/ # user (mọi project trên má
283
292
 
284
293
  Quick Magic cũng có MCP server dùng **chung tài khoản/ví credit** — cho phép Claude Code
285
294
  (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`... mà không cần qua CLI. Xác thực OAuth
287
- 2.1 qua trình duyệt, không cần API key.
295
+ `wait_for_job`, `list_models`, `get_credit_balance`, `list_voices`, `text_to_speech`,
296
+ `create_voice_clone`, `delete_voice_clone`... mà không cần qua CLI. Xác thực OAuth 2.1 qua
297
+ trình duyệt, không cần API key.
288
298
 
289
299
  - MCP endpoint: `https://api.quickmagic.vn/mcp`
290
300
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "quickmagic-cli",
3
- "version": "1.2.0",
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 8 Claude Agent Skills and an MCP server integration.",
3
+ "version": "1.4.0",
4
+ "description": "Quick Magic CLI — generate AI images/videos, product photoshoots, virtual try-on, hook ad videos, subtitles, text-to-speech/voice cloning and more via the Quick Magic REST API, with browser-based OAuth PKCE login (no API keys to manage). Ships with 9 Claude Agent Skills and an MCP server integration.",
5
5
  "bin": {
6
6
  "quickmagic": "src/index.js",
7
7
  "qm": "src/index.js"
package/skills/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Quick Magic CLI — Agent Skills
2
2
 
3
- 8 Agent Skills that wrap the `quickmagic` / `qm` CLI so a coding agent (Claude
3
+ 9 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
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 7 skills reference |
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 8 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,28 @@ 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 |
46
47
 
47
- ## Shared conventions (all 8 skills)
48
+ ## Shared conventions (all 9 skills)
48
49
 
49
50
  - Check `qm auth status` first; if not logged in, tell the user to run
50
51
  `qm auth login`.
51
- - Quote the credit cost (from `qm models list` / a preset's `credit_cost`) and
52
- get explicit user confirmation **before** running any command that spends
53
- credit — submission holds credit immediately. `subtitle`/`split` are the one
54
- exception: price is duration-based and only known after submission, so the
55
- skill asks for a go-ahead instead of a number.
52
+ - Quote the credit cost (from `qm models list` / a preset's `credit_cost` —
53
+ `tts`/`voices clone` instead read `pricing`/`clone_price` from
54
+ `qm voices list --model <m>`) and get explicit user confirmation **before**
55
+ running any command that spends credit — submission holds credit
56
+ immediately. `subtitle`/`split` are the one exception: price is
57
+ duration-based and only known after submission, so the skill asks for a
58
+ go-ahead instead of a number.
56
59
  - Surface `insufficient_credit` (`Không đủ credit: ... thiếu Zcr`) with the exact
57
60
  shortfall and a link to top up (https://quickmagic.vn/pricing).
58
61
  - Never poll `jobs get` manually — use `qm jobs wait <id> [--out dir]`. Note that
59
62
  `--out` only auto-downloads for job types with a single `result_url`
60
- (image/video/tryon/edit/hook/subtitle); product/fashion/cutout/split return
61
- arrays and must be read back with `qm jobs get <id>` after completion.
62
- See `quickmagic-account` for the full job-id-prefix table.
63
+ (image/video/tryon/edit/hook/subtitle/tts); product/fashion/cutout/split
64
+ return arrays and must be read back with `qm jobs get <id>` after
65
+ completion, and voice-clone jobs (`voc_`) have no `result_url` at all (read
66
+ `voice`/`sample_url`/`status` instead). See `quickmagic-account` for the
67
+ full job-id-prefix table.
63
68
  - Always end with the final result URL(s) plus the credit amount already
64
69
  charged/held.
65
70
 
@@ -101,10 +101,13 @@ generating command's own `--wait` flag, where available).
101
101
  | `mkt_` | `marketing video` | single result (no dedicated skill) |
102
102
  | `mot_` | `motion` | single result (no dedicated skill) |
103
103
  | `stt_` | `stt` | text result (no dedicated skill) |
104
+ | `tts_` | `tts` | single `result_url` (mp3) + `model`/`voice`/`character` — see `quickmagic-tts` |
105
+ | `voc_` | `voices clone` | **no** `result_url`: `voice` (name to use with `--voice`), `sample_url`, `is_free` — see `quickmagic-tts` |
104
106
 
105
107
  **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_`), `--out` still polls status correctly
108
+ single-`result_url` types (`img_`/`vid_`/`vto_`/`edt_`/`hok_`/`sub_`/`tts_`). For the
109
+ array types (`pai_`/`fsh_`/`cut_`/`spl_`) and `voc_` (voice clone — no file, read
110
+ `voice`/`sample_url` via `qm jobs get`), `--out` still polls status correctly
108
111
  but downloads nothing — after status is `completed`, call `qm jobs get <id>` and
109
112
  read the array field yourself, then report each URL to the user.
110
113
 
@@ -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
- throw new Error(json.message || `HTTP ${res.status}`);
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
  }
@@ -0,0 +1,42 @@
1
+ // commands/elements.js — thư viện element tái dùng: xem `@tag` để trỏ trong prompt video, và tạo mới.
2
+ // BE dịch `@tag` trong prompt thành "Image N (mô tả)" lúc gửi provider → giữ nhân vật/sản phẩm
3
+ // nhất quán qua nhiều cảnh. Nhớ đưa ảnh element vào `--image` kèm `--mode reference`.
4
+ const api = require('../api');
5
+ const { resolveMediaInput } = require('../media');
6
+
7
+ async function list(options) {
8
+ const limit = (options && options.limit) || 50;
9
+ const data = await api.call('GET', `/elements?limit=${encodeURIComponent(limit)}`);
10
+ const items = (data && data.elements) || [];
11
+ if (!items.length) {
12
+ console.log('Chưa có element nào. Tạo bằng: qm elements create --name "Áo dài đỏ" --image ./ao.jpg --desc "..."');
13
+ return;
14
+ }
15
+
16
+ const tag_w = Math.max(3, ...items.map((e) => `@${e.tag}`.length));
17
+ const name_w = Math.max(4, ...items.map((e) => (e.name || '').length));
18
+ console.log(`${'TAG'.padEnd(tag_w)} ${'TÊN'.padEnd(name_w)} MÔ TẢ`);
19
+ console.log('─'.repeat(tag_w + name_w + 30));
20
+ for (const e of items) {
21
+ const desc = (e.description || '').replace(/\s+/g, ' ');
22
+ const short = desc.length > 60 ? `${desc.slice(0, 57)}...` : desc;
23
+ console.log(`${`@${e.tag}`.padEnd(tag_w)} ${(e.name || '').padEnd(name_w)} ${short}`);
24
+ }
25
+ console.log(`\nDùng: qm generate video --prompt "cô gái mặc @${items[0].tag} đi dạo" --image <url-element> --mode reference --model seedance-2-0`);
26
+ }
27
+
28
+ async function create(options) {
29
+ // File local → tự upload presigned lên host Quick Magic (server chỉ nhận URL của mình).
30
+ const media_url = await resolveMediaInput(options.image);
31
+ const body = { name: options.name, media_url };
32
+ if (options.desc) body.description = options.desc;
33
+ if (options.category) body.category = options.category;
34
+ if (options.type) body.media_type = options.type;
35
+
36
+ const data = await api.call('POST', '/elements', { body });
37
+ console.log(`Đã tạo element: @${data.tag} (${data.name})`);
38
+ if (data.description) console.log(`Mô tả gửi kèm model: ${data.description}`);
39
+ console.log(`Dùng trong prompt: "@${data.tag}"`);
40
+ }
41
+
42
+ module.exports = { list, create };
@@ -32,8 +32,15 @@ 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 (chỉ seedance-2-0/-fast, max 15s, không mix ảnh, giá hạng with-video).
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);
38
+ // [260726] 4 tham số nâng cao — server đã nhận sẵn, CLI chỉ chưa mở.
39
+ // --no-audio: commander set options.audio = false; mặc định undefined (giữ default của model).
40
+ if (Number.isInteger(options.seed)) body.seed = options.seed;
41
+ if (options.negative) body.negative_prompt = options.negative;
42
+ if (options.cameraFixed) body.camera_fixed = true;
43
+ if (options.audio === false) body.audio = false;
37
44
 
38
45
  const data = await api.call('POST', '/videos', { body });
39
46
  const job_id = data.job_id;
@@ -39,7 +39,10 @@ async function list(options) {
39
39
  console.log(`${r.key.padEnd(key_w)} ${r.label.padEnd(label_w)} ${r.credit.padEnd(credit_w)}${is_video ? ` ${r.images}` : ''}`);
40
40
  }
41
41
  if (is_video && rows.some((r) => r.images.includes('*'))) {
42
- console.log('\n* KHÔNG nhận ảnh chứa người thật (gửi vào sẽ lỗi) — dùng gemini-omni / seedance 1.x / wan-2-7 cho ảnh người.');
42
+ 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).');
43
+ 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');
44
+ console.log(' rồi dùng ảnh đó (provider chấp nhận ảnh thuần Seedream 5.0 Pro — đã probe thật 260808).');
45
+ 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.');
43
46
  }
44
47
  }
45
48
 
@@ -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
@@ -4,10 +4,12 @@ const { program } = require('commander');
4
4
 
5
5
  const auth = require('./commands/auth');
6
6
  const generate = require('./commands/generate');
7
+ const elements = require('./commands/elements');
7
8
  const jobs = require('./commands/jobs');
8
9
  const models = require('./commands/models');
9
10
  const credits = require('./commands/credits');
10
11
  const tools = require('./commands/tools');
12
+ const voice = require('./commands/voice');
11
13
 
12
14
  const collect = (v, acc) => { acc.push(v); return acc; };
13
15
 
@@ -51,10 +53,32 @@ gen_cmd
51
53
  .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)')
52
54
  .option('--mode <m>', 'Chế độ ảnh: reference (nhiều ảnh tham chiếu) | frames (khung đầu/cuối, max 2). Seedance 2.x KHÔNG nhận ảnh người thật ở MỌI chế độ (xem qm models --type video)')
53
55
  .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')
56
+ .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)
57
+ .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')
58
+ .option('--camera-fixed', 'Khoá máy quay đứng yên (không pan/zoom) — hợp cảnh xoay sản phẩm')
59
+ .option('--no-audio', 'Tắt tiếng (không đổi giá)')
54
60
  .option('--wait', 'Chờ tới khi job hoàn tất')
55
61
  .option('--out <dir>', 'Thư mục lưu kết quả (dùng kèm --wait)')
56
62
  .action(generate.video);
57
63
 
64
+ // ── elements ─────────────────────────────────────────────────────────────────
65
+ // [260726] Thư viện element tái dùng: `@tag` trong prompt → BE dịch thành "Image N (mô tả)".
66
+ const elements_cmd = program.command('elements').description('Thư viện element tái dùng (@tag trong prompt)');
67
+ elements_cmd
68
+ .command('list')
69
+ .description('Liệt kê element + tag để trỏ trong prompt')
70
+ .option('--limit <n>', 'Số lượng tối đa (mặc định 50)')
71
+ .action(elements.list);
72
+ elements_cmd
73
+ .command('create')
74
+ .description('Lưu 1 ảnh thành element tái dùng')
75
+ .requiredOption('--name <n>', 'Tên element, vd "Áo dài đỏ" (tag tự sinh từ tên)')
76
+ .requiredOption('--image <i>', 'Ảnh (URL hoặc file local — tự upload)')
77
+ .option('--desc <d>', 'Mô tả chi tiết — CHÍNH là thứ được chèn kèm ảnh vào prompt, viết càng rõ càng giữ được nhân vật')
78
+ .option('--category <c>', 'character | location | prop | product | style | other')
79
+ .option('--type <t>', 'image (mặc định) | video')
80
+ .action(elements.create);
81
+
58
82
  // ── jobs ─────────────────────────────────────────────────────────────────────
59
83
  const jobs_cmd = program.command('jobs').description('Theo dõi job');
60
84
  jobs_cmd.command('get <id>').description('Xem chi tiết job (JSON)').action(jobs.get);
@@ -125,6 +149,45 @@ program.command('motion <video_url>').description('Áp chuyển động video v
125
149
  .requiredOption('--image <url...>', 'Ảnh áp motion (1-20)')
126
150
  .option('--model <m>').option('--mode <m>', 'standard|professional').option('--prompt <p>').option('--crid <id>').action(tools.motion);
127
151
 
152
+ // ── TTS / Voices ─────────────────────────────────────────────────────────────
153
+ program
154
+ .command('tts')
155
+ .description('Chuyển văn bản → giọng nói (5 engine qimi_1.5/2.5/3/5/5.5)')
156
+ .option('--text <t>', 'Văn bản cần đọc')
157
+ .option('--file <path>', 'Đọc văn bản từ file local (thay --text)')
158
+ .option('--model <m>', 'qimi_1.5|qimi_2.5|qimi_3|qimi_5|qimi_5.5', 'qimi_3')
159
+ .requiredOption('--voice <v>', 'Tên giọng — xem cột VOICE của qm voices list')
160
+ .option('--language <l>', 'Ngôn ngữ — phân biệt giọng trùng tên (xem qm voices list)')
161
+ .option('--speed <n>', 'Tốc độ đọc 0.5-2.0 (mặc định 1) — CHỈ qimi_5/qimi_5.5')
162
+ .option('--style <s>', 'Phong cách đọc — nhãn từ cột STYLES của qm voices list')
163
+ .option('--title <t>', 'Tiêu đề job')
164
+ .option('--crid <id>', 'client_request_id')
165
+ .option('--wait', 'Chờ tới khi job hoàn tất')
166
+ .option('--out <dir>', 'Thư mục lưu file mp3 (dùng kèm --wait)')
167
+ .action(voice.tts);
168
+
169
+ const voices_cmd = program.command('voices').description('Giọng đọc: catalog + giọng clone của bạn');
170
+ voices_cmd
171
+ .command('list')
172
+ .description('Liệt kê giọng theo model')
173
+ .requiredOption('--model <m>', 'qimi_1.5|qimi_2.5|qimi_3|qimi_5|qimi_5.5')
174
+ .option('--language <l>', 'Lọc theo ngôn ngữ')
175
+ .option('--search <s>', 'Lọc theo tên (không phân biệt hoa/thường)')
176
+ .option('--limit <n>', 'Số lượng tối đa (mặc định 50, trần 100)')
177
+ .action(voice.voicesList);
178
+ voices_cmd
179
+ .command('clone')
180
+ .description('Nhân bản giọng nói từ file ghi âm mẫu (dùng cho qimi_5/qimi_5.5)')
181
+ .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)')
182
+ .requiredOption('--name <n>', 'Tên giọng — dùng lại ở --voice của qm tts')
183
+ .option('--crid <id>', 'client_request_id (mặc định tự sinh từ hash file+tên)')
184
+ .option('--wait', 'Chờ tới khi giọng xử lý xong')
185
+ .action(voice.voicesClone);
186
+ voices_cmd
187
+ .command('delete <id>')
188
+ .description('Xoá giọng đã clone')
189
+ .action(voice.voicesDelete);
190
+
128
191
  // ── Cutout Studio ────────────────────────────────────────────────────────────
129
192
  program.command('cutout').description('Tách nền / tạo ảnh cutout trong suốt (PNG)')
130
193
  .option('--operation <o>', 'generate|from_ref|remix_describe', 'from_ref')