quickmagic-cli 1.4.0 → 1.9.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 9 Claude **Agent Skills** and an **MCP server** integration for
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
@@ -127,7 +127,8 @@ Ghi chú chung trước khi xem bảng:
127
127
  | Lệnh | Mô tả | Credit |
128
128
  |---|---|---|
129
129
  | `qm cutout` | Tách nền / PNG trong suốt / remix scene | Có |
130
- | `qm edit <image> --tool <t>` | restore / upscale (2k,4k) / beauty / muscle / color_boost | Có |
130
+ | `qm edit <image> --tool <t>` | restore / beauty / muscle / color_boost (upscale → dùng `qm upscale`) | Có |
131
+ | `qm upscale <file...> [--video]` | Upscale ảnh (2k/4k/8k) hoặc video (nâng nét/tăng fps) — 1 lệnh nhiều file = mỗi file 1 job | Có |
131
132
 
132
133
  ### Video quảng cáo / Marketing
133
134
 
@@ -147,10 +148,14 @@ Ghi chú chung trước khi xem bảng:
147
148
  | `qm subtitle <video_url>` | Thêm phụ đề (+ lồng tiếng, dịch) | Có |
148
149
  | `qm split <video_url>` | Cắt video dài → clip ngắn | Có |
149
150
  | `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 tts (--text <t>\|--file <path>) --voice <v>` | Chuyển văn bản → giọng nói (6 model qimi_1.5/2.5/3/3.5/5/5.5; qimi_3.5 có kiểu đọc `--style`) | Có |
151
152
  | `qm voices list --model <m>` | Liệt kê giọng theo model + giá (`pricing`) | — |
152
153
  | `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
154
  | `qm voices delete <id>` | Xoá giọng đã clone | — |
155
+ | `qm music models` | Bảng model nhạc + giá + ETA + hàng chờ | — |
156
+ | `qm music lyrics --desc <d>` | AI viết lời bài hát | Không (0đ) |
157
+ | `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) |
158
+ | `qm music get <id>` | Xem chi tiết job nhạc | — |
154
159
 
155
160
  ### Nhập liệu (miễn phí)
156
161
 
@@ -160,6 +165,21 @@ Ghi chú chung trước khi xem bảng:
160
165
  | `qm import <url>` | Nhập ảnh/video từ link TikTok/Instagram | — |
161
166
  | `qm assets <kind>` | Liệt kê tài sản đã lưu: `products`\|`outfits`\|`kols` | — |
162
167
 
168
+ ### Ứng dụng web (React SSR trên `<slug>.quickmagic.app`)
169
+
170
+ | Lệnh | Mô tả | Credit |
171
+ |---|---|---|
172
+ | `qm apps create --title "…" [--subdomain s] [--from <app_id>]` | Tạo app mới từ template (`--from` = remix app công khai đã duyệt) | — |
173
+ | `qm apps list` | App của tôi | — |
174
+ | `qm apps status <app_id> [--json]` | Trạng thái deploy/duyệt/build | — |
175
+ | `qm apps repo-access <app_id>` | URL git clone có token 15 phút (push code) | — |
176
+ | `qm apps deploy <app_id> [--wait]` | Build + deploy nhánh main | — |
177
+ | `qm apps publish <app_id> [--unlist]` | Lên feed (cần cover; tự kiểm duyệt; app đầu tiên duyệt tay) | — |
178
+ | `qm apps remove <app_id> --yes` | Xoá vĩnh viễn (subdomain khoá mãi) | — |
179
+
180
+ Người dùng app trả credit của **chính họ** khi tạo ảnh/video trong app; bạn (creator) nhận 5% trên mỗi lượt.
181
+ Không có `apps db` / `apps secrets` trên CLI (chỉ agent dùng qua MCP).
182
+
163
183
  ## Tạo ảnh
164
184
 
165
185
  ```bash
@@ -182,8 +202,39 @@ qm generate video --prompt "..." --model grok-imagine --wait --out ./out
182
202
 
183
203
  - `--image` cũng nhận URL hoặc file local (như `--ref`).
184
204
  - `--mode reference|frames` — `reference` (nhiều ảnh tham chiếu) hoặc `frames` (khung
185
- đầu/cuối, tối đa 2). Một số model (họ Seedance 2.x) **từ chối ảnh chứa người thật** ở mọi
186
- `--mode` — xem cột `IMAGES` của `qm models list --type video` (đánh dấu `*`).
205
+ đầu/cuối, tối đa 2). Model họ Seedance 2.x (`seedance-2-0`, `-fast`, `-mini`) giờ **tự động
206
+ chấp nhận ảnh người thật** ở mọi `--mode` — xem mục "Ảnh người thật" ngay dưới.
207
+
208
+ ### Ảnh người thật (Seedance 2.x)
209
+
210
+ 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ý)
211
+ ả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 đó
212
+ `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
213
+ 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
214
+ `jobs get`/`wait_for_job` cũng áp dụng y hệt — xem `docs/mcp-integration-guide.md` phía repo
215
+ 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:
216
+ 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
217
+ + 1 `error_code` trong bảng dưới (credit hoàn), CLI tự in gợi ý xử lý:
218
+
219
+ | `error_code` | Ý nghĩa | Gợi ý |
220
+ |---|---|---|
221
+ | `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 |
222
+ | `PORTRAIT_MINOR` | Ảnh có trẻ em | Dùng ảnh người lớn |
223
+ | `PORTRAIT_NSFW` | Ảnh vi phạm nội dung nhạy cảm | Dùng ảnh khác |
224
+ | `PORTRAIT_GEMINI_UNAVAILABLE` | Chưa kiểm được ảnh (hệ thống bận) | Thử lại sau vài phút |
225
+ | `PORTRAIT_ASSET_FAILED` | Ảnh không được chấp nhận (mờ/nhiều người/định dạng lạ) | Thử ảnh khác |
226
+ | `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 |
227
+ | `PORTRAIT_TIMEOUT` | Chuẩn bị ảnh quá lâu (>10 phút) | Thử lại |
228
+ | `PORTRAIT_QUOTA_FULL` | Hệ thống đang bận (đầy quota tạm) | Thử lại sau vài phút |
229
+ | `PORTRAIT_IMAGE_HOST` | Ảnh không thuộc host Quick Magic | Dùng `--image <file local>` (CLI tự upload) thay URL ngoài |
230
+ | `PORTRAIT_IMAGE_FETCH_FAILED` | Không tải được ảnh | Thử lại |
231
+ | `PORTRAIT_PROVIDER_ERROR` | Hệ thống đang bận (lỗi tạm thời) | Thử lại sau ít phút |
232
+ | `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 |
233
+ | `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 |
234
+ | `PORTRAIT_DISABLED` | Tính năng ảnh người thật đang tắt | Đổi `--model gemini-omni` |
235
+ | `PORTRAIT_TOO_MANY_IMAGES` | Vượt số ảnh người thật cho phép của model | Bớt `--image` |
236
+ | `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 |
237
+ | `REAL_PERSON_BLOCKED` | (mã cũ) Model không nhận ảnh người thật | Đổi `--model gemini-omni` |
187
238
 
188
239
  ## Job
189
240
 
@@ -232,6 +283,18 @@ qm voices list --model qimi_1.5 --language "Tiếng Việt"
232
283
  qm tts --text "Xin chào Quick Magic" --voice <voice_id> --model qimi_3 --wait --out ./out
233
284
  qm voices clone --audio ./sample.wav --name "Giọng của tôi" --wait
234
285
 
286
+ # Qimi 3.5 — giọng của qimi_3, giá cố định mỗi lần (xem dòng "Giá:" của qm voices list --model qimi_3.5), tối đa 30.000 ký tự, chọn kiểu đọc:
287
+ # --style: tối đa 1 tốc độ (fast|default|slow) + 1 cảm xúc (cheerful|sad|storytelling|emotional), nối bằng "+"
288
+ # hoặc tự mô tả tiếng Việt "custom:<mô tả ≤200 ký tự>"; bỏ trống = đọc tự nhiên
289
+ qm voices list --model qimi_3.5
290
+ qm tts --file ./truyen.txt --voice <voice_id> --model qimi_3.5 --style "slow+storytelling" --wait --out ./out
291
+ qm tts --text "Ngày xửa ngày xưa..." --voice <voice_id> --model qimi_3.5 --style "custom:giọng trầm ấm, chậm rãi như kể chuyện đêm khuya" --wait --out ./out
292
+
293
+ # Tạo nhạc AI — xem giá trước khi chạy (thường miễn phí giai đoạn ra mắt)
294
+ qm music models
295
+ qm music lyrics --desc "bài hát vui về buổi sáng cà phê Sài Gòn"
296
+ qm music create --mode simple --desc "lofi chill guitar, rainy night" --instrumental --model melo-2 --wait --out ./out
297
+
235
298
  # Video marketing theo mode + phân tích video đối thủ → kịch bản
236
299
  qm marketing modes
237
300
  qm marketing video --mode 3 --product-id 42 --duration 10
@@ -267,7 +330,7 @@ qm credits # chạy không cần browser
267
330
 
268
331
  ## Agent Skills
269
332
 
270
- CLI này đi kèm 9 **Agent Skills** (`skills/quickmagic-*/SKILL.md`) để coding agent
333
+ CLI này đi kèm 10 **Agent Skills** (`skills/quickmagic-*/SKILL.md`) để coding agent
271
334
  (Claude Code...) dùng CLI trực tiếp — báo giá + xin xác nhận trước khi tốn credit,
272
335
  tự `jobs wait` thay vì poll tay, chủ động báo Qimi free-window. Danh sách đầy đủ và quy ước
273
336
  chung: xem `skills/README.md`.
@@ -278,7 +341,7 @@ Cách 1 — `npx skills` (nếu dùng công cụ [`skills`](https://www.npmjs.co
278
341
  quản lý Agent Skills từ GitHub):
279
342
 
280
343
  ```bash
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
344
+ 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
282
345
  ```
283
346
 
284
347
  Cách 2 — copy thủ công vào thư mục skills của Claude Code:
@@ -293,8 +356,8 @@ cp -r skills/quickmagic-* ~/.claude/skills/ # user (mọi project trên má
293
356
  Quick Magic cũng có MCP server dùng **chung tài khoản/ví credit** — cho phép Claude Code
294
357
  (và các MCP client khác) gọi thẳng `generate_image`, `generate_video`, `get_job`,
295
358
  `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.
359
+ `create_voice_clone`, `delete_voice_clone`, `generate_music`, `write_lyrics`... mà không cần
360
+ qua CLI. Xác thực OAuth 2.1 qua trình duyệt, không cần API key.
298
361
 
299
362
  - MCP endpoint: `https://api.quickmagic.vn/mcp`
300
363
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "quickmagic-cli",
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.",
3
+ "version": "1.9.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, image upscaling and more via the Quick Magic REST API, with browser-based OAuth PKCE login (no API keys to manage). Ships with 11 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
- 9 Agent Skills that wrap the `quickmagic` / `qm` CLI so a coding agent (Claude
3
+ 11 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,quickmagic-tts
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-upscale,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,17 +35,19 @@ 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 8 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 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 |
42
42
  | `quickmagic-cutout` | `cutout` | transparent PNG cutouts / scene remix |
43
43
  | `quickmagic-hook-video` | `hook presets`, `hook video` | ~10s comedy ad video from a character + product photo (18 presets) |
44
- | `quickmagic-edit-image` | `edit` | restore / upscale (2k/4k) / beauty / muscle / color_boost on an existing image |
44
+ | `quickmagic-edit-image` | `edit` | restore / beauty / muscle / color_boost on an existing image |
45
+ | `quickmagic-upscale` | `upscale` | upscale images to 2K/4K/8K (crisp/standard/seedvr2/ultra) or videos (standard/pro/seedvr2/fps) |
45
46
  | `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-tts` | `tts`, `voices list/clone/delete` | text-to-speech (6 models; `qimi_3.5` adds reading styles — presets or your own Vietnamese description — at a flat price per call) + custom voice cloning |
48
+ | `quickmagic-music` | `music create/lyrics/models/get` | AI song generation (vocals or instrumental) from a description or your own lyrics |
47
49
 
48
- ## Shared conventions (all 9 skills)
50
+ ## Shared conventions (all 11 skills)
49
51
 
50
52
  - Check `qm auth status` first; if not logged in, tell the user to run
51
53
  `qm auth login`.
@@ -60,7 +62,7 @@ mkdir -p ~/.claude/skills && cp -r cli-quickmagic/skills/quickmagic-* ~/.claude/
60
62
  shortfall and a link to top up (https://quickmagic.vn/pricing).
61
63
  - Never poll `jobs get` manually — use `qm jobs wait <id> [--out dir]`. Note that
62
64
  `--out` only auto-downloads for job types with a single `result_url`
63
- (image/video/tryon/edit/hook/subtitle/tts); product/fashion/cutout/split
65
+ (image/video/tryon/edit/upscale/hook/subtitle/tts/music); product/fashion/cutout/split
64
66
  return arrays and must be read back with `qm jobs get <id>` after
65
67
  completion, and voice-clone jobs (`voc_`) have no `result_url` at all (read
66
68
  `voice`/`sample_url`/`status` instead). See `quickmagic-account` for the
@@ -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
@@ -103,13 +108,16 @@ generating command's own `--wait` flag, where available).
103
108
  | `stt_` | `stt` | text result (no dedicated skill) |
104
109
  | `tts_` | `tts` | single `result_url` (mp3) + `model`/`voice`/`character` — see `quickmagic-tts` |
105
110
  | `voc_` | `voices clone` | **no** `result_url`: `voice` (name to use with `--voice`), `sample_url`, `is_free` — see `quickmagic-tts` |
111
+ | `mus_` | `music create` | single `result_url` (mp3) + `title`/`duration_ms` — see `quickmagic-music` |
112
+ | `ups_` | `upscale` (image) | single `result_url` — see `quickmagic-upscale` |
113
+ | `vup_` | `upscale` (video, `--video`/auto-detected) | single `result_url` + `duration_sec` — see `quickmagic-upscale` |
106
114
 
107
115
  **Important caveat**: `jobs wait --out <dir>` only **auto-downloads** for the
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
111
- but downloads nothing — after status is `completed`, call `qm jobs get <id>` and
112
- read the array field yourself, then report each URL to the user.
116
+ single-`result_url` types (`img_`/`vid_`/`vto_`/`edt_`/`hok_`/`sub_`/`tts_`/`mus_`/
117
+ `ups_`/`vup_`). For the array types (`pai_`/`fsh_`/`cut_`/`spl_`) and `voc_` (voice
118
+ clone — no file, read `voice`/`sample_url` via `qm jobs get`), `--out` still polls
119
+ status correctly but downloads nothing — after status is `completed`, call
120
+ `qm jobs get <id>` and read the array field yourself, then report each URL to the user.
113
121
 
114
122
  Statuses: `queued` → `in_progress` → `completed` (success) or `failed` (relay the
115
123
  job's `error` field). A `queued` status stuck past ~15 minutes is reported as
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  name: quickmagic-edit-image
3
3
  description: >-
4
- Restore, upscale (2k/4k), beauty-retouch, muscle-enhance, or color-boost a
5
- single existing image via `qm edit`. Use for enhancing an existing photo,
6
- not generating a new one. Triggers: "upscale ảnh lên 4k", "phục hồi ảnh cũ",
7
- "làm đẹp da trong ảnh", "chỉnh màu ảnh", "restore old photo", "upscale
8
- image resolution".
4
+ Restore, beauty-retouch, muscle-enhance, or color-boost a single existing
5
+ image via `qm edit`. Use for enhancing an existing photo, not generating a new
6
+ one. For enlarging / sharpening (2k/4k/8k) use the `quickmagic-upscale` skill
7
+ (`qm upscale`) instead. Triggers: "phục hồi ảnh cũ", "làm đẹp da trong ảnh",
8
+ "chỉnh màu ảnh", "restore old photo", "retouch skin".
9
9
  ---
10
10
 
11
- # quickmagic-edit-image — Photo Restore / Upscale / Retouch
11
+ # quickmagic-edit-image — Photo Restore / Retouch
12
+
13
+ > Phóng to & làm nét ảnh đã tách sang skill `quickmagic-upscale` (`qm upscale`).
12
14
 
13
15
  Wraps `qm edit <image>`. Edits **one existing image** in place — restoring
14
16
  damage, upscaling resolution, beauty retouch, muscle enhancement, or color
@@ -18,22 +20,21 @@ grading. Not for generating a new image from a text prompt (use
18
20
  ## Command
19
21
 
20
22
  ```
21
- qm edit <image> --tool <t> [--model qimi_2.5|qimi_3] [--upscale-target 2k|4k] [--style <s...>] [--crid <id>]
23
+ qm edit <image> --tool <t> [--model qimi_2.5|qimi_3] [--style <s...>] [--crid <id>]
22
24
  ```
23
25
 
24
26
  | Flag | Notes |
25
27
  |---|---|
26
28
  | `<image>` **required** (positional) | URL or local file path |
27
- | `--tool <t>` **required** | one of `restore` \| `upscale` \| `beauty` \| `muscle` \| `color_boost` |
29
+ | `--tool <t>` **required** | one of `restore` \| `beauty` \| `muscle` \| `color_boost` |
28
30
  | `--model` | `qimi_2.5` or `qimi_3` (default `qimi_3`) |
29
- | `--upscale-target` | **required when `--tool upscale`** — `2k` or `4k` |
30
31
  | `--style <s...>` | **required when `--tool beauty`** — up to 3 style keywords, repeatable flag |
31
32
  | `--crid <id>` | idempotency key |
32
33
 
33
- Map the user's request to a tool: "ảnh cũ/rách/mờ" → `restore`; "phóng to/nét
34
- hơn/4K" → `upscale` (ask 2k or 4k if not specified); "làm đẹp da/mặt" → `beauty`
35
- (ask for up to 3 style keywords); "cơ bắp săn chắc hơn" → `muscle`; "màu sắc rực
36
- rỡ hơn" → `color_boost`.
34
+ Map the user's request to a tool: "ảnh cũ/rách/mờ" → `restore`; "làm đẹp da/mặt"
35
+ → `beauty` (ask for up to 3 style keywords); "cơ bắp săn chắc hơn" → `muscle`;
36
+ "màu sắc rực rỡ hơn" → `color_boost`. "Phóng to / nét hơn / 4K / 8K" KHÔNG thuộc
37
+ lệnh này — dùng skill `quickmagic-upscale` (`qm upscale`).
37
38
 
38
39
  ## Before running
39
40
 
@@ -55,15 +56,14 @@ No `--wait` flag. Copy the job id from the hint
55
56
  ## Errors
56
57
 
57
58
  `Không đủ credit: ...` → state shortfall + https://quickmagic.vn/pricing.
58
- Missing `--upscale-target` on `upscale`, or missing `--style` on `beauty` →
59
- plain `Lỗi: <message>`; add the missing flag and retry. Full reference:
59
+ Missing `--style` on `beauty` → plain `Lỗi: <message>`; add the flag and retry.
60
+ `tool_moved` → tool `upscale` đã dời sang `qm upscale` (skill `quickmagic-upscale`). Full reference:
60
61
  `quickmagic-account` skill.
61
62
 
62
63
  ## Example
63
64
 
64
65
  ```bash
65
66
  qm edit ./old-photo.jpg --tool restore --model qimi_3
66
- qm edit ./portrait.jpg --tool upscale --upscale-target 4k
67
67
  qm edit ./selfie.jpg --tool beauty --style smooth-skin soft-light
68
68
  qm jobs wait edt_3 --out ./out
69
69
  ```
@@ -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
+ ```
@@ -2,12 +2,15 @@
2
2
  name: quickmagic-tts
3
3
  description: >-
4
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".
5
+ (`qm tts`, `qm voices list/clone/delete`). Covers 6 TTS engines (qimi_1.5,
6
+ qimi_2.5, qimi_3, qimi_3.5, qimi_5, qimi_5.5) — qimi_3.5 reads the qimi_3
7
+ voices with a chosen reading style (speed/emotion presets or your own
8
+ Vietnamese description), at a flat price per call; qimi_5/qimi_5.5 add 310
9
+ built-in system voices across 16 languages plus optional user voice
10
+ cloning. Triggers: "đọc văn bản thành giọng nói", "chuyển text thành giọng
11
+ nói", "đọc truyện", "giọng kể chuyện", "đọc chậm", "giọng vui tươi",
12
+ "clone giọng", "tạo giọng riêng", "nhân bản giọng nói", "text to speech",
13
+ "voice clone", "TTS", "read this text aloud", "convert text to audio".
11
14
  ---
12
15
 
13
16
  # quickmagic-tts — Text-to-Speech & Voice Cloning
@@ -19,7 +22,7 @@ text read aloud as audio, or wants a custom cloned voice for future TTS calls.
19
22
 
20
23
  | Command | Required flags | Key optional flags |
21
24
  |---|---|---|
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>` |
25
+ | `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>` (voice label for qimi_1.5/2.5/3; preset or `custom:<mô tả>` for qimi_3.5 — see "Reading styles"), `--title <t>`, `--crid <id>`, `--wait`, `--out <dir>` |
23
26
  | `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
27
  | `qm voices clone` | `--audio <file\|url>`, `--name <n>` | `--crid <id>` (auto-generated if omitted), `--wait` |
25
28
  | `qm voices delete <id>` | `<id>` (`voc_12` or bare `12`) | — |
@@ -31,15 +34,18 @@ text read aloud as audio, or wants a custom cloned voice for future TTS calls.
31
34
  | `qimi_1.5` | flat credits/call | 50,000 | fixed catalog |
32
35
  | `qimi_2.5` | flat credits/call | 30,000 | fixed catalog |
33
36
  | `qimi_3` (default) | flat credits/call | 30,000 | fixed catalog |
37
+ | `qimi_3.5` | flat credits/call | 30,000 | the `qimi_3` catalog (multilingual) + reading styles — no clones |
34
38
  | `qimi_5` | credits/character, with a minimum charge | 10,000 | 310 system voices (16 languages) + your own clones |
35
39
  | `qimi_5.5` | credits/character (higher quality), with a minimum charge | 10,000 | 310 system voices (16 languages) + your own clones |
36
40
 
37
- Reference only, as of 260817 — **never quote these from memory**. Always run
41
+ Reference only, as of 260927 — **never quote these from memory**. Always run
38
42
  `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
+ roughly `qimi_1.5`/`qimi_2.5` ≈ 2cr/call, `qimi_3` ≈ 4cr/call, `qimi_3.5` ≈
44
+ 10cr/call (any length up to 30,000 chars), `qimi_5` ≈ 0.09cr/char,
45
+ `qimi_5.5` ≈ 0.15cr/char (min ~5cr) — rates can change. Per-character models
46
+ count the text **after** your saved pronunciation replacements. Voice cloning
47
+ is a separate, much larger flat charge (~2,230cr) — see "Cloning a custom
48
+ voice" below.
43
49
 
44
50
  ## Pick a voice
45
51
 
@@ -55,6 +61,9 @@ qm voices list --model qimi_5 --search Minh --limit 20
55
61
  - `qimi_1.5`/`qimi_2.5`/`qimi_3` list a fixed catalog with a `STYLES` column
56
62
  (labels like "Vui vẻ"/"Formal") — pass the exact label as `--style`.
57
63
  `qimi_5`/`qimi_5.5` voices don't take `--style`.
64
+ - `qimi_3.5` lists the same voices as `qimi_3` and
65
+ prints a `Kiểu đọc (style)` line — its `--style` is a reading style, not a
66
+ voice label (see "Reading styles" below).
58
67
  - `qimi_5`/`qimi_5.5` output splits into **"Giọng của bạn"** (voices you
59
68
  cloned) and **"Giọng hệ thống"** (310 system voices, 16 languages) —
60
69
  cloning is optional, try a system voice first. Each system voice reads
@@ -74,8 +83,28 @@ qm tts --file ./script.txt --voice <voice_id> --model qimi_5 --language "Tiếng
74
83
  - The CLI prints the character count before submitting; sanity-check it
75
84
  against the model's char limit above to avoid a `char_limit_exceeded`.
76
85
  - 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`).
86
+ `qm jobs wait tts_<n> --out ./out` separately — it auto-downloads the audio
87
+ file (mp3, or wav for qimi_3/qimi_3.5; single `result_url`).
88
+
89
+ ## Reading styles (qimi_3.5 only)
90
+
91
+ ```bash
92
+ qm voices list --model qimi_3.5 # prints the price line + the live "Kiểu đọc (style)" hint
93
+ qm tts --file ./truyen.txt --voice <voice_id> --model qimi_3.5 --style "slow+storytelling" --wait --out ./out
94
+ qm tts --text "Ngày xửa ngày xưa..." --voice <voice_id> --model qimi_3.5 --style "custom:giọng trầm ấm, chậm rãi như kể chuyện đêm khuya" --wait --out ./out
95
+ ```
96
+
97
+ - Preset: at most **one speed** (`fast` | `default` | `slow`) and **one
98
+ emotion** (`cheerful` | `sad` | `storytelling` | `emotional`), joined
99
+ with `+` — e.g. `slow`, `cheerful`, `slow+storytelling`. The server's
100
+ current list is the `Kiểu đọc (style)` line of `qm voices list --model qimi_3.5`.
101
+ - Custom: `custom:<Vietnamese description>`, ≤200 characters after the
102
+ server removes quotes/brackets and turns line breaks into spaces. It only
103
+ changes tone, pace and emotion — the text is still read word for word.
104
+ - Omit `--style` for a natural read. `--speed` does not apply to qimi_3.5 —
105
+ put the speed in `--style` instead.
106
+ - An unknown key, two speeds, two emotions, or a custom text over 200
107
+ characters fails with `invalid_style` **before** any credit is held.
79
108
 
80
109
  ## Cloning a custom voice (optional, for qimi_5/qimi_5.5)
81
110
 
@@ -137,10 +166,10 @@ CLI prints `Lỗi [code]: message`. Common codes:
137
166
 
138
167
  | Code | Meaning | What to do |
139
168
  |---|---|---|
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` |
169
+ | `invalid_model` | `--model` not one of the 6 keys (e.g. `v3`) | use `qimi_1.5|qimi_2.5|qimi_3|qimi_3.5|qimi_5|qimi_5.5` |
141
170
  | `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 |
171
+ | `invalid_style` | qimi_1.5/2.5/3: `--style` isn't in that voice's `styles`; qimi_3.5: unknown preset key, two speeds/emotions, or `custom:` text over 200 chars | use the exact `STYLES` label; for qimi_3.5 follow "Reading styles" |
172
+ | `invalid_speed` | `--speed` outside 0.5-2.0 on qimi_5/qimi_5.5 (other models ignore `--speed`) | clamp to range; for qimi_3.5 use a speed preset in `--style` |
144
173
  | `char_limit_exceeded` | text longer than the model's char limit | split the text into smaller chunks |
145
174
  | `insufficient_credit` / `negative_balance` | not enough balance | state the exact shortfall and https://quickmagic.vn/pricing |
146
175
  | `rate_limited` / `concurrent_limit` / `busy` | too many requests/jobs right now | wait, then retry |
@@ -159,6 +188,9 @@ qm auth status
159
188
  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
189
  qm tts --text "Chào mừng đến với Quick Magic." --voice <voice_id> --model qimi_3 --wait --out ./out
161
190
 
191
+ qm voices list --model qimi_3.5
192
+ qm tts --file ./truyen.txt --voice <voice_id> --model qimi_3.5 --style "slow+storytelling" --wait --out ./out
193
+
162
194
  qm voices list --model qimi_5
163
195
  qm voices clone --audio ./mysample.wav --name "Giọng riêng" --wait
164
196
  qm tts --text "Đọc bằng giọng của tôi." --voice "Giọng riêng" --model qimi_5 --wait --out ./out
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: quickmagic-upscale
3
+ description: >-
4
+ Upscale (sharpen + enlarge) one or more existing images to 2K/4K/8K, or
5
+ denoise/sharpen existing videos, via `qm upscale`. Use when the user wants a
6
+ sharper/higher-resolution version of an existing photo or video — not for
7
+ generating new media, and not for restore/beauty/muscle/color_boost (use
8
+ `quickmagic-edit-image` for those). Triggers: "upscale ảnh lên 4k/8k",
9
+ "phóng to ảnh nét hơn", "nâng độ phân giải ảnh", "upscale image to 4k",
10
+ "sharpen this photo", "upscale video", "làm nét video", "video mờ quá làm
11
+ nét lại", "tăng fps video".
12
+ ---
13
+
14
+ # quickmagic-upscale — Image (2K/4K/8K) + Video Upscale
15
+
16
+ Wraps `qm upscale <image...>`. Takes **one or more existing images or
17
+ videos** and returns a sharper, higher-resolution (or higher-frame-rate)
18
+ version of each — one credit hold and one job per file. Not for restoring
19
+ damage, beauty retouch, muscle enhancement, or color grading
20
+ (`quickmagic-edit-image` handles those tools), and not for generating new
21
+ media from a text prompt (`quickmagic-generate`).
22
+
23
+ ## Command
24
+
25
+ ```
26
+ qm upscale <file...> [--model <m>] [--resolution <r>] [--video] [--crid <id>]
27
+ ```
28
+
29
+ | Flag | Notes |
30
+ |---|---|
31
+ | `<file...>` **required** (positional, 1+) | URL(s) or local file path(s) — repeatable, one job submitted per file |
32
+ | `--model <m>` | Image: `crisp`\|`standard`\|`seedvr2`\|`ultra`. Video: `standard`\|`pro`\|`seedvr2`\|`fps`. Default `standard` (valid for both) |
33
+ | `--resolution <r>` | Image: `2k`\|`4k`\|`8k`, default `4k`. Video: `720p`\|`1080p`\|`2k`\|`4k`, **no CLI default** — omit it and the server picks `1080p` (do NOT pass `4k` "to be safe", it is the most expensive video tier). **Ignored by image `--model crisp`** and **video `--model fps`** (no resolution option); video `--model pro` does not accept `720p` |
34
+ | `--video` | Force every file in this command to be treated as **video** — only needed when a URL hides its extension (e.g. a signed URL); local paths and URLs ending in `.mp4`/`.mov`/`.webm` are auto-detected, no flag needed |
35
+ | `--crid <id>` | idempotency key; the CLI appends `-0`, `-1`, ... per file so retrying the exact same multi-file command is safe |
36
+
37
+ Image model choice (ask the user if unclear, or default to `standard`):
38
+
39
+ | Model | Best for | Notes |
40
+ |---|---|---|
41
+ | `crisp` | quick sharpen, fixed output size | cheapest; does **not** accept `--resolution` |
42
+ | `standard` | general 2K/4K/8K upscale | default |
43
+ | `seedvr2` | best detail retention on faces/text | pricier than `standard` |
44
+ | `ultra` | highest quality, large enlargements | most expensive — confirm cost before running |
45
+
46
+ Video model choice (ask the user if unclear, or default to `standard`):
47
+
48
+ | Model | Resolutions | Best for | Notes |
49
+ |---|---|---|---|
50
+ | `standard` | 720p/1080p/2k/4k | general video upscale | default; wait ≈10× the clip length |
51
+ | `pro` | 1080p/2k/4k (no 720p) | highest-fidelity tier | **slowest**, wait ≈25-30× the clip length |
52
+ | `seedvr2` | 720p/1080p/2k/4k | best detail retention | wait ≈6-27× the clip length (higher resolutions are slower) |
53
+ | `fps` | none (ignores `--resolution`) | double the frame rate | **fastest**, wait ≈2-3× the clip length |
54
+
55
+ Video caps: **300MB / 10 minutes** input. Billing is per second of input,
56
+ rounded up to the next whole second; if that is still below the model
57
+ minimum (`standard`/`seedvr2`: 3s, `pro`/`fps`: 1s) it bills at the minimum
58
+ instead — e.g. a 2s clip on `standard` bills as 3s (30 credits, not 20).
59
+
60
+ ## Before running
61
+
62
+ 1. `qm auth status` → not logged in → `qm auth login`.
63
+ 2. `qm models list --type image` does **not** list upscale pricing (it's a
64
+ separate catalog) — quote credit cost from the table below instead.
65
+
66
+ ## Quote, then confirm
67
+
68
+ **Image** — state the per-image credit cost **× number of images** and get
69
+ confirmation before running — submitting holds credit immediately, one hold
70
+ per image:
71
+
72
+ | Model | Credits/image |
73
+ |---|---|
74
+ | `crisp` | 10 |
75
+ | `standard` | 20 |
76
+ | `seedvr2` | 20 |
77
+ | `ultra` | 116 |
78
+
79
+ **Video** — credits are **per second of input** (not per file); estimate
80
+ from the clip's duration (`ffprobe`, or ask the user): round the seconds up
81
+ to the next whole second, floor that at the model's minimum (`standard`/
82
+ `seedvr2`: 3s, `pro`/`fps`: 1s), then × the rate below — confirm the result
83
+ before running (do not quote a lower number for clips under the minimum):
84
+
85
+ | Model | 720p | 1080p | 2k | 4k |
86
+ |---|---|---|---|---|
87
+ | `standard` | 10 | 10 | 20 | 39 |
88
+ | `pro` | — (unsupported) | 14 | 28 | 56 |
89
+ | `seedvr2` | 39 | 77 | 96 | 116 |
90
+ | `fps` | 16 (flat, no resolution) | | | |
91
+
92
+ ## Run + track
93
+
94
+ No `--wait` flag. The CLI prints one job hint per file
95
+ (`→ Theo dõi: quickmagic jobs get ups_5` for images, `vup_5` for videos);
96
+ collect every id from the output, then `qm jobs wait <id> --out ./out` for
97
+ each — single `result_url` per job, `--out` auto-downloads. Video processing
98
+ is slow (see per-model wait multipliers above) — a `processing` status
99
+ (including while the finished file is still being saved) is normal, keep
100
+ waiting, do not resubmit. Report every final URL + total `credits_charged`.
101
+ Result files follow the account plan retention (free plan: 5 days —
102
+ download promptly; paid plans: long-term).
103
+
104
+ ## Errors
105
+
106
+ `Không đủ credit: ...` → state shortfall + https://quickmagic.vn/pricing.
107
+ Any other failure prints `Lỗi: <message>` and stops the whole command (a
108
+ prior file's job may already be submitted/charged — check with
109
+ `qm jobs get ups_<n>` / `qm jobs get vup_<n>` before retrying that one
110
+ file). Full reference: `quickmagic-account` skill.
111
+
112
+ ## Example
113
+
114
+ ```bash
115
+ qm upscale ./old-photo.jpg --model standard --resolution 4k
116
+ qm upscale ./a.jpg ./b.jpg ./c.jpg --model seedvr2 --resolution 8k
117
+ qm jobs wait ups_5 --out ./out
118
+
119
+ qm upscale ./clip.mp4 --model standard --resolution 1080p
120
+ qm upscale https://files.quickmagic.cloud/media-tools/clip.mp4 --model fps
121
+ qm jobs wait vup_12 --out ./out
122
+ ```
@@ -0,0 +1,76 @@
1
+ // commands/apps.js — Ứng dụng React SSR trên <slug>.quickmagic.app (Apps đợt B): create / list / status / deploy / publish / remove.
2
+ // KHÔNG có `apps db` / `apps secrets` (bề mặt nhạy cảm — chỉ agent qua MCP). repo-access in clone_url ra stdout theo yêu cầu tường minh.
3
+ const api = require('../api');
4
+
5
+ const SLEEP = (ms) => new Promise((r) => setTimeout(r, ms));
6
+
7
+ function printApp(a) {
8
+ console.log(`${a.title || '(chưa tên)'} [${a.id}]`);
9
+ console.log(` URL: ${a.url || '—'}`);
10
+ console.log(` Deploy: ${a.deploy_status || '—'}${a.last_build ? ` (build #${a.last_build.id} ${a.last_build.status}${a.last_build.error_code ? ' ' + a.last_build.error_code : ''})` : ''}`);
11
+ console.log(` Duyệt: ${a.review_status || '—'} · công khai: ${a.is_public ? 'có' : 'không'} · cover: ${a.thumb_url ? 'có' : 'chưa'} · DB: ${a.has_db ? 'có' : 'không'}`);
12
+ if (a.has_unbuilt_commits) console.log(' ⚠️ Có commit chưa deploy — chạy: qm apps deploy <app_id>');
13
+ if (a.last_build && a.last_build.error_text) console.log(` Lỗi build: ${String(a.last_build.error_text).slice(0, 300)}`);
14
+ }
15
+
16
+ async function create(options) {
17
+ const body = { title: options.title, subdomain: options.subdomain, category: options.category, description: options.description };
18
+ if (options.from) body.from_app_id = options.from;
19
+ const data = await api.call('POST', '/apps', { body });
20
+ console.log(`Đã tạo app ${data.id}${data.remixed_from ? ` (remix từ "${data.remixed_from.title}")` : ''}`);
21
+ printApp(data);
22
+ console.log('Tiếp: qm apps repo-access <app_id> → git clone → sửa → push → qm apps deploy <app_id>');
23
+ }
24
+ async function list() {
25
+ const data = await api.call('GET', '/apps');
26
+ const apps = data.apps || [];
27
+ if (!apps.length) { console.log('Chưa có app nào. Tạo: qm apps create --title "Tên app"'); return; }
28
+ const id_w = 36, slug_w = Math.max(4, ...apps.map((a) => String(a.slug || '').length));
29
+ console.log(`${'APP_ID'.padEnd(id_w)} ${'SLUG'.padEnd(slug_w)} DEPLOY DUYỆT PUBLIC TITLE`);
30
+ for (const a of apps) console.log(`${a.id.padEnd(id_w)} ${String(a.slug || '').padEnd(slug_w)} ${String(a.deploy_status || '').padEnd(9)} ${String(a.review_status || '').padEnd(8)} ${a.is_public ? 'có ' : 'không'} ${a.title || ''}`);
31
+ }
32
+ async function status(app_id, options) {
33
+ const data = await api.call('GET', `/apps/${encodeURIComponent(app_id)}`);
34
+ if (options && options.json) { console.log(JSON.stringify(data, null, 2)); return; }
35
+ printApp(data);
36
+ }
37
+ async function repoAccess(app_id) {
38
+ const data = await api.call('POST', `/apps/${encodeURIComponent(app_id)}/repo-access`, { body: {} });
39
+ // In clone_url có token TTL 15' — người dùng CLI chủ động xin, KHÔNG log ở nơi khác.
40
+ console.log(`Quyền push repo (hết hạn ${data.expires_at}):`);
41
+ console.log(data.clone_url);
42
+ console.log(`git clone ${data.clone_url} app && cd app`);
43
+ }
44
+ async function deploy(app_id, options) {
45
+ const data = await api.call('POST', `/apps/${encodeURIComponent(app_id)}/deploy`, { body: {} });
46
+ console.log(`Đã xếp hàng build #${data.build_id} → ${data.url}`);
47
+ if (!options || !options.wait) { console.log('Theo dõi: qm apps status <app_id>'); return; }
48
+ for (let i = 0; i < 60; i++) {
49
+ await SLEEP(5000);
50
+ const s = await api.call('GET', `/apps/${encodeURIComponent(app_id)}`);
51
+ const b = s.last_build;
52
+ if (b && String(b.id) === String(data.build_id) && ['succeeded', 'failed'].includes(b.status)) {
53
+ if (b.status === 'succeeded') { console.log(`✅ Deploy xong (${Math.round((b.duration_ms || 0) / 1000)}s): ${s.url}`); return; }
54
+ console.error(`❌ Build thất bại (${b.error_code || 'lỗi'}): ${String(b.error_text || '').slice(0, 400)}`); process.exit(1);
55
+ }
56
+ process.stdout.write(` … ${b ? b.step || b.status : 'queued'}\r`);
57
+ }
58
+ console.error('Hết 5 phút chưa xong — kiểm: qm apps status <app_id>'); process.exit(1);
59
+ }
60
+ async function publish(app_id, options) {
61
+ try {
62
+ const data = await api.call('POST', `/apps/${encodeURIComponent(app_id)}/publish`, { body: { is_public: !(options && options.unlist) } });
63
+ console.log(data.is_public ? `✅ Đã công khai lên feed Ứng dụng: ${data.url}` : 'Đã gỡ khỏi feed (link trực tiếp vẫn hoạt động).');
64
+ } catch (e) {
65
+ const m = String(e.message || '');
66
+ if (/APP_REVIEW_PENDING|chờ duyệt/.test(m)) { console.log('⏳ App đang chờ duyệt nội dung (app đầu tiên của tài khoản được duyệt tay). Link trực tiếp vẫn dùng được.'); return; }
67
+ throw e;
68
+ }
69
+ }
70
+ async function remove(app_id, options) {
71
+ if (!options || !options.yes) { console.error('Xoá VĨNH VIỄN app (Worker + DB + repo, subdomain khoá mãi). Xác nhận bằng --yes.'); process.exit(1); }
72
+ const data = await api.call('DELETE', `/apps/${encodeURIComponent(app_id)}`);
73
+ console.log(`Đã xoá app ${data.slug || app_id}.`);
74
+ }
75
+
76
+ module.exports = { create, list, status, repoAccess, deploy, publish, remove };
@@ -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
  }
@@ -1,4 +1,6 @@
1
- // commands/models.js — liệt kê model ảnh/video dạng bảng key | label | credit (+IMAGES cho video).
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.
@@ -44,6 +46,11 @@ async function list(options) {
44
46
  console.log(' rồi dùng ảnh đó (provider chấp nhận ảnh thuần Seedream 5.0 Pro — đã probe thật 260808).');
45
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.');
46
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.');
53
+ }
47
54
  }
48
55
 
49
56
  module.exports = { list };
@@ -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 };
@@ -12,7 +12,9 @@ const jobHint = (data) => {
12
12
  const id = data.job_id || (Array.isArray(data.job_ids) && data.job_ids[0]);
13
13
  if (id) console.log(`\n→ Theo dõi: quickmagic jobs get ${id}`);
14
14
  };
15
- const run = (fn) => (...a) => fn(...a).catch((e) => { console.error('Lỗi:', e.message); process.exit(1); });
15
+ // [code-review-p03.md L8] api.js#call gán e.code = json.code (REST giữ nguyên code cho mục đích này) —
16
+ // bản cũ chỉ in message, script/agent đọc stderr KHÔNG phân biệt được 'feature_disabled' với lỗi khác.
17
+ const run = (fn) => (...a) => fn(...a).catch((e) => { console.error('Lỗi:', e.code ? `[${e.code}] ${e.message}` : e.message); process.exit(1); });
16
18
 
17
19
  // ── Nhập liệu (không tốn credit) ──
18
20
  const scrape = run(async (url) => out(await call('POST', '/scrape', { body: { url } })));
@@ -29,7 +31,39 @@ const product = run(async (o) => jobHint(await call('POST', '/product/images', {
29
31
  const fashion = run(async (o) => jobHint(await call('POST', '/fashion/images', { body: { mode: o.mode, outfit_ids: (o.outfit || []).map(Number), kol_kind: o.kolKind, kol_id: o.kolId ? Number(o.kolId) : undefined, model: o.model, client_request_id: o.crid } })));
30
32
 
31
33
  // ── Ảnh ──
32
- const edit = run(async (image, o) => jobHint(await call('POST', '/edit', { body: { image: await rmi(image), tool: o.tool, model: o.model, upscale_target: o.upscaleTarget, style: o.style, client_request_id: o.crid } })));
34
+ const edit = run(async (image, o) => jobHint(await call('POST', '/edit', { body: { image: await rmi(image), tool: o.tool, model: o.model, style: o.style, client_request_id: o.crid } })));
35
+ // [260825, plans/260825-1344-upscale-studio-hf-clone P04] 1 lệnh CLI = N file = N job (mỗi file 1
36
+ // job/hold riêng, REST /upscale(-video) chỉ nhận 1 file/call). --crid dùng CHUNG y nguyên cho cả vòng
37
+ // lặp sẽ khiến BE coi file 2+ TRÙNG file 1 (idempotency theo client_request_id) → chỉ file đầu có job
38
+ // thật, các file sau lặng lẽ trả về CÙNG job đó (thiếu file, không lỗi rõ ràng) — hậu tố `-${i}` cho
39
+ // mỗi file 1 khoá riêng, vẫn giữ được idempotency khi retry NGUYÊN LỆNH (cùng --crid, cùng thứ tự file).
40
+ //
41
+ // [260826, plans/260826-1200-upscale-video-hf-clone-byteplus-vcube P03] mở rộng ẢNH → VIDEO cùng lệnh:
42
+ // --video ép mọi file coi là video; không truyền thì tự nhận theo đuôi .mp4/.mov/.webm (KHÔNG .m4v —
43
+ // media.js/get_upload_url không có mime cho đuôi này). data:video/ hoặc data:image/ (file ≤64KB đã
44
+ // inline) tự nhận theo mime, KHÔNG cần đuôi.
45
+ const VIDEO_EXT_RE = /\.(mp4|mov|webm)(\?.*)?$/i;
46
+ function looksLikeVideo(ref) {
47
+ if (/^data:video\//i.test(ref)) return true;
48
+ if (/^data:image\//i.test(ref)) return false;
49
+ if (/^https?:\/\//i.test(ref)) {
50
+ try { return VIDEO_EXT_RE.test(new URL(ref).pathname); } catch { return false; }
51
+ }
52
+ return VIDEO_EXT_RE.test(ref);
53
+ }
54
+ const upscale = run(async (files, o) => {
55
+ for (const [i, f] of files.entries()) { // LẶP — không phải files[0]
56
+ const crid = o.crid ? `${o.crid}-${i}` : undefined;
57
+ const is_video = !!o.video || looksLikeVideo(f);
58
+ // [code-review-p03.md H3] --resolution KHÔNG còn default ở commander (index.js). Ảnh: giữ default
59
+ // '4k' CŨ (P04, chỉ áp khi user không truyền). Video: KHÔNG tự thêm default — để trống thì server
60
+ // (createMediaJobs) tự chọn default_resolution 1080p, tránh CLI âm thầm gửi bậc giá đắt nhất.
61
+ const res = is_video
62
+ ? await call('POST', '/upscale-video', { body: { video: await rmi(f), model: o.model, resolution: o.resolution, client_request_id: crid } })
63
+ : await call('POST', '/upscale', { body: { image: await rmi(f), model: o.model, resolution: o.resolution || '4k', client_request_id: crid } });
64
+ jobHint(res); // in job_id từng file
65
+ }
66
+ });
33
67
  const tryon = run(async (o) => jobHint(await call('POST', '/tryon', { body: { type: o.type, model: o.model, model_image: await rmi(o.model_image), garment_image: await rmi(o.garment), upper_image: await rmi(o.upper), lower_image: await rmi(o.lower), background_image: await rmi(o.background), prompt: o.prompt, client_request_id: o.crid } })));
34
68
 
35
69
  // ── Video nặng / audio ──
@@ -43,4 +77,4 @@ const cutout = run(async (o) => jobHint(await call('POST', '/cutout', { body: {
43
77
  const hookPresets = run(async () => out(await call('GET', '/hook/presets')));
44
78
  const hookVideo = run(async (o) => jobHint(await call('POST', '/hook/videos', { body: { preset_id: o.preset, character_url: await rmi(o.character), product_url: await rmi(o.product), aspect: o.aspect, speech_lang: o.speechLang, custom_cta: o.cta, location_url: o.location, accessory_url: o.accessory, style: o.style, format: o.format, resolution: o.resolution, client_request_id: o.crid } })));
45
79
 
46
- module.exports = { scrape, importSocial, assets, analyze, marketingModes, marketingVideo, product, fashion, edit, tryon, stt, subtitle, split, motion, cutout, hookPresets, hookVideo };
80
+ module.exports = { scrape, importSocial, assets, analyze, marketingModes, marketingVideo, product, fashion, edit, upscale, tryon, stt, subtitle, split, motion, cutout, hookPresets, hookVideo };
@@ -66,6 +66,8 @@ const voicesList = run(async (o) => {
66
66
  const data = await call('GET', `/voices${buildQuery({ model: o.model, language: o.language, search: o.search, limit: o.limit })}`);
67
67
  const pricing = data.pricing || {}; // [review C4] BE lỗi/shape cũ → không crash, in được phần còn lại
68
68
  printPricing(pricing);
69
+ // qimi_3.5: server builds this hint from its own preset list — print it verbatim instead of hard-coding keys here.
70
+ if (data.style_hint) console.log(data.style_hint);
69
71
  if (pricing.billing === 'per_char') {
70
72
  if (data.clone_price != null) console.log(`Giá clone giọng riêng: ${data.clone_price} credit`);
71
73
  // [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).
@@ -93,7 +95,12 @@ const voicesList = run(async (o) => {
93
95
  }
94
96
  if (systems.length) {
95
97
  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]));
98
+ // qimi_3.5 system voices (the qimi_3 catalog) have no English name — drop the empty column.
99
+ const has_en = systems.some((v) => v.name_en);
100
+ printTable(
101
+ has_en ? ['VOICE', 'NAME', 'NAME_EN', 'GENDER', 'LANGUAGE'] : ['VOICE', 'NAME', 'GENDER', 'LANGUAGE'],
102
+ systems.map((v) => (has_en ? [v.voice, v.name, v.name_en, v.gender, v.language] : [v.voice, v.name, v.gender, v.language])),
103
+ );
97
104
  }
98
105
  }
99
106
  console.log(`\nNgôn ngữ: ${(data.languages || []).join(' · ')}`);
package/src/index.js CHANGED
@@ -10,6 +10,8 @@ const models = require('./commands/models');
10
10
  const credits = require('./commands/credits');
11
11
  const tools = require('./commands/tools');
12
12
  const voice = require('./commands/voice');
13
+ const music = require('./commands/music');
14
+ const apps = require('./commands/apps');
13
15
 
14
16
  const collect = (v, acc) => { acc.push(v); return acc; };
15
17
 
@@ -51,7 +53,7 @@ gen_cmd
51
53
  .option('--resolution <r>', 'Độ phân giải (vd 720p, 1080p)')
52
54
  .option('--aspect-ratio <r>', 'Tỉ lệ khung')
53
55
  .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)')
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)')
56
+ .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")')
55
57
  .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
58
  .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
59
  .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')
@@ -93,7 +95,7 @@ const models_cmd = program.command('models').description('Model AI');
93
95
  models_cmd
94
96
  .command('list')
95
97
  .description('Liệt kê model')
96
- .option('--type <t>', 'Loại: image | video', 'image')
98
+ .option('--type <t>', 'Loại: image | video | music', 'image')
97
99
  .action(models.list);
98
100
 
99
101
  // ── credits ──────────────────────────────────────────────────────────────────
@@ -129,9 +131,22 @@ program.command('fashion').description('Tạo ảnh thời trang AI')
129
131
  .action(tools.fashion);
130
132
 
131
133
  // ── Ảnh ────────────────────────────────────────────────────────────────────────
132
- program.command('edit <image>').description('Sửa ảnh: restore/upscale/beauty/muscle/color_boost')
134
+ program.command('edit <image>').description('Sửa ảnh: restore/beauty/muscle/color_boost (upscale → dùng lệnh `qm upscale`)')
133
135
  .requiredOption('--tool <t>').option('--model <m>', 'qimi_2.5|qimi_3', 'qimi_3')
134
- .option('--upscale-target <t>', '2k|4k').option('--style <s...>').option('--crid <id>').action(tools.edit);
136
+ .option('--style <s...>').option('--crid <id>').action(tools.edit);
137
+ // [260826, plans/260826-1200-upscale-video-hf-clone-byteplus-vcube P03] mở rộng ẢNH → VIDEO cùng lệnh
138
+ // (giữ nguyên tên arg <image...> — đã pin cứng trong test BE mcp-upscale-wiring.test.js). Video tự
139
+ // nhận theo đuôi .mp4/.mov/.webm (KHÔNG .m4v — media.js/get_upload_url không có mime cho đuôi này),
140
+ // hoặc ép bằng --video khi URL không lộ đuôi (vd link ký chữ ký).
141
+ program.command('upscale <image...>').description('Upscale ảnh (crisp|standard|seedvr2|ultra) hoặc video (standard|pro|seedvr2|fps) — 1 lệnh nhiều file = mỗi file 1 job. File video: .mp4/.mov/.webm, tự nhận theo đuôi hoặc ép bằng --video')
142
+ .option('--model <m>', 'Ảnh: crisp|standard|seedvr2|ultra — Video: standard|pro|seedvr2|fps', 'standard')
143
+ // [code-review-p03.md H3] KHÔNG đặt default ở đây — default '4k' hợp lý cho ẢNH (giữ nguyên hành vi
144
+ // cũ P04) nhưng SAI cho VIDEO (server default là 1080p, rẻ hơn 4k standard 3.9×). Default lệch theo
145
+ // media_type xử lý TRONG tools.upscale (ảnh mới rơi về '4k' khi không truyền; video để trống, server tự chọn).
146
+ .option('--resolution <r>', 'Ảnh: 2k|4k|8k, mặc định 4k (bỏ qua crisp) — Video: 720p|1080p|2k|4k, mặc định 1080p do server chọn khi không truyền (bỏ qua fps, pro không nhận 720p)')
147
+ .option('--video', 'Ép MỌI file trong lệnh này là VIDEO (mặc định tự nhận theo đuôi .mp4/.mov/.webm)')
148
+ .option('--crid <id>', 'client_request_id (tự thêm hậu tố theo từng file)')
149
+ .action(tools.upscale);
135
150
  program.command('tryon').description('Thử đồ ảo (virtual try-on)')
136
151
  .requiredOption('--model-image <img>', 'Ảnh người mẫu (URL/base64)')
137
152
  .option('--type <t>', 'full|upper', 'full').option('--model <m>', 'qimi_1.5|qimi_2.5|qimi_3', 'qimi_3')
@@ -149,28 +164,46 @@ program.command('motion <video_url>').description('Áp chuyển động video v
149
164
  .requiredOption('--image <url...>', 'Ảnh áp motion (1-20)')
150
165
  .option('--model <m>').option('--mode <m>', 'standard|professional').option('--prompt <p>').option('--crid <id>').action(tools.motion);
151
166
 
167
+ // ── Media-tools mở rộng [260902] — cùng kho model Spaces "Bàn Sáng Tạo" ─────
168
+ program.command('sfx <prompt>').description('Tạo hiệu ứng âm thanh từ mô tả (whoosh/nổ/mưa/bước chân…)')
169
+ .option('--model <m>', 'sfx-lite (mặc định) | sfx-pro').option('--duration <s>', 'Giây: lite 3/5/10/15/30, pro 3/5/10/15 (mặc định 5)').option('--crid <id>').action(tools.sfx);
170
+ program.command('remove-bg <image>').description('Tách nền ảnh → PNG trong suốt')
171
+ .option('--crid <id>').action(tools.removeBg);
172
+ const t3d = program.command('3d').description('Model 3D (.glb): tạo từ ảnh/mô tả + hoạt hình');
173
+ t3d.command('create').description('Tạo model 3D từ ảnh hoặc mô tả')
174
+ .option('--image <i>', 'Ảnh tham chiếu (file/URL)').option('--prompt <p>', 'Mô tả model')
175
+ .option('--model <m>', 'tripo-v3 (mặc định) | tripo-v2-5').option('--texture <t>', 'standard | textureless').option('--crid <id>').action(tools.gen3d);
176
+ t3d.command('animate <model_url>').description('Hoạt hình model 3D đã tạo (rig + preset chuyển động)')
177
+ .option('--preset <p>', 'Tên preset (vd dance_01, wave_goodbye_01)').option('--crid <id>').action(tools.animate3d);
178
+ program.command('talking-photo').description('Ảnh chân dung + audio → video nói chuyện (khớp môi)')
179
+ .requiredOption('--image <i>', 'Ảnh chân dung (file/URL)').requiredOption('--audio <url>', 'Audio giọng nói (vd kết quả qm tts)')
180
+ .option('--model <m>', 'talk-fast (mặc định) | talk-standard').option('--resolution <r>', '480p | 720p (talk-standard)').option('--prompt <p>', 'Gợi ý biểu cảm').option('--crid <id>').action(tools.talkingPhoto);
181
+ program.command('extend-video <video_url>').description('Kéo dài video — sinh tiếp N giây sau khung cuối')
182
+ .requiredOption('--duration <s>', 'Giây đoạn mới: wan 3-10, seedance 4-15')
183
+ .option('--model <m>', 'vext-wan-2-5 (mặc định) | vext-seedance-2-0').option('--resolution <r>', '480p/720p/1080p (+4k seedance)').option('--prompt <p>', 'Điều xảy ra tiếp theo').option('--crid <id>').action(tools.extendVideo);
184
+
152
185
  // ── TTS / Voices ─────────────────────────────────────────────────────────────
153
186
  program
154
187
  .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)')
188
+ .description('Chuyển văn bản → giọng nói (6 engine qimi_1.5/2.5/3/3.5/5/5.5)')
156
189
  .option('--text <t>', 'Văn bản cần đọc')
157
190
  .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')
191
+ .option('--model <m>', 'qimi_1.5|qimi_2.5|qimi_3|qimi_3.5|qimi_5|qimi_5.5 (qimi_3.5: giọng qimi_3, giá cố định mỗi lần — xem giá ở qm voices list --model qimi_3.5, tối đa 30.000 ký tự)', 'qimi_3')
159
192
  .requiredOption('--voice <v>', 'Tên giọng — xem cột VOICE của qm voices list')
160
193
  .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')
194
+ .option('--speed <n>', 'Tốc độ đọc 0.5-2.0 (mặc định 1) — CHỈ qimi_5/qimi_5.5 (qimi_3.5: chỉnh tốc độ bằng --style)')
195
+ .option('--style <s>', 'Kiểu đọc. qimi_1.5/2.5/3: nhãn cột STYLES của qm voices list. qimi_3.5: tối đa 1 tốc độ (fast|default|slow) + 1 cảm xúc (cheerful|sad|storytelling|emotional) nối bằng "+", vd "slow+storytelling"; hoặc tự mô tả tiếng Việt "custom:<mô tả ≤200 ký tự>" — danh sách mới nhất: qm voices list --model qimi_3.5')
163
196
  .option('--title <t>', 'Tiêu đề job')
164
197
  .option('--crid <id>', 'client_request_id')
165
198
  .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)')
199
+ .option('--out <dir>', 'Thư mục lưu file audio mp3/wav (dùng kèm --wait)')
167
200
  .action(voice.tts);
168
201
 
169
202
  const voices_cmd = program.command('voices').description('Giọng đọc: catalog + giọng clone của bạn');
170
203
  voices_cmd
171
204
  .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')
205
+ .description('Liệt kê giọng theo model (+ giá, và gợi ý kiểu đọc với qimi_3.5)')
206
+ .requiredOption('--model <m>', 'qimi_1.5|qimi_2.5|qimi_3|qimi_3.5|qimi_5|qimi_5.5')
174
207
  .option('--language <l>', 'Lọc theo ngôn ngữ')
175
208
  .option('--search <s>', 'Lọc theo tên (không phân biệt hoa/thường)')
176
209
  .option('--limit <n>', 'Số lượng tối đa (mặc định 50, trần 100)')
@@ -188,6 +221,41 @@ voices_cmd
188
221
  .description('Xoá giọng đã clone')
189
222
  .action(voice.voicesDelete);
190
223
 
224
+ // ── Xưởng Nhạc Melo ──────────────────────────────────────────────────────────
225
+ const music_cmd = program.command('music').description('Xưởng Nhạc Melo — tạo nhạc AI (mô tả/lời → mp3)');
226
+ music_cmd
227
+ .command('create')
228
+ .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)')
229
+ .option('--mode <m>', 'simple (mặc định, AI viết lời) | custom (tự nhập lời)', 'simple')
230
+ .option('--desc <d>', 'Mô tả bài hát — bắt buộc mode simple, hoặc bản không lời')
231
+ .option('--lyrics <l>', 'Lời bài hát (mode custom)')
232
+ .option('--lyrics-file <path>', 'Đọc lời từ file local UTF-8 (thay --lyrics)')
233
+ .option('--styles <s>', 'Phong cách nhạc, phân cách dấu phẩy (vd "V-pop, Ballad")')
234
+ .option('--title <t>', 'Tiêu đề bài hát')
235
+ .option('--model <m>', 'melo-3 (mặc định, mới nhất) | melo-2 (thế hệ trước)', 'melo-3')
236
+ .option('--instrumental', 'Bản không lời')
237
+ .option('--gender <g>', 'auto (mặc định) | male | female — chỉ áp dụng khi có lời')
238
+ .option('--crid <id>', 'client_request_id')
239
+ .option('--wait', 'Chờ tới khi job hoàn tất')
240
+ .option('--out <dir>', 'Thư mục lưu file mp3 (tự bật --wait nếu chưa có)')
241
+ .action(music.create);
242
+ music_cmd
243
+ .command('lyrics')
244
+ .description('AI viết lời bài hát (0đ) — xem trước/chỉnh trước khi dùng mode custom')
245
+ .requiredOption('--desc <d>', 'Mô tả bài hát')
246
+ .option('--title <t>', 'Tiêu đề gợi ý')
247
+ .option('--language <l>', 'Ngôn ngữ (mặc định auto)')
248
+ .option('--instrumental', 'Chỉ cần tiêu đề + phong cách, không cần lời')
249
+ .action(music.lyrics);
250
+ music_cmd
251
+ .command('models')
252
+ .description('Liệt kê model nhạc + giá + ETA + hàng chờ')
253
+ .action(music.models);
254
+ music_cmd
255
+ .command('get <id>')
256
+ .description('Xem chi tiết job nhạc (mus_<n> hoặc số trần)')
257
+ .action(music.get);
258
+
191
259
  // ── Cutout Studio ────────────────────────────────────────────────────────────
192
260
  program.command('cutout').description('Tách nền / tạo ảnh cutout trong suốt (PNG)')
193
261
  .option('--operation <o>', 'generate|from_ref|remix_describe', 'from_ref')
@@ -220,6 +288,20 @@ hook_cmd
220
288
  .option('--crid <id>', 'client_request_id')
221
289
  .action(tools.hookVideo);
222
290
 
291
+ // ── apps (Ứng dụng React SSR trên <slug>.quickmagic.app — Apps đợt B) ─────────
292
+ const apps_cmd = program.command('apps').description('Ứng dụng web đầy đủ (React SSR, subdomain riêng, Sign in with Quick Magic)');
293
+ apps_cmd.command('create').description('Tạo app mới (repo từ template) — cần duyệt nếu gọi từ agent')
294
+ .requiredOption('--title <t>', 'Tên app').option('--subdomain <s>', 'Subdomain mong muốn (tự thêm hậu tố 4 ký tự)')
295
+ .option('--category <c>', 'Danh mục').option('--description <d>', 'Mô tả 1 câu')
296
+ .option('--from <app_id>', 'Remix: chép mã nguồn app công khai đã duyệt (không chép secrets/DB/cover)')
297
+ .action(apps.create);
298
+ apps_cmd.command('list').description('Liệt kê app của tôi').action(apps.list);
299
+ apps_cmd.command('status <app_id>').description('Trạng thái deploy/duyệt/build gần nhất').option('--json', 'In JSON').action(apps.status);
300
+ apps_cmd.command('repo-access <app_id>').description('Lấy URL git clone có token 15 phút (in ra stdout)').action(apps.repoAccess);
301
+ apps_cmd.command('deploy <app_id>').description('Build + deploy nhánh main').option('--wait', 'Chờ tới khi xong (≤5 phút)').action(apps.deploy);
302
+ apps_cmd.command('publish <app_id>').description('Đưa lên feed Ứng dụng (cần deployed + cover; tự kiểm duyệt nội dung)').option('--unlist', 'Gỡ khỏi feed').action(apps.publish);
303
+ apps_cmd.command('remove <app_id>').description('Xoá vĩnh viễn app').option('--yes', 'Xác nhận').action(apps.remove);
304
+
223
305
  program.parseAsync(process.argv).catch((e) => {
224
306
  console.error('Lỗi:', e.message);
225
307
  process.exit(1);