@remixmate/cli 0.1.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/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/argv.d.ts +15 -0
- package/dist/argv.js +51 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +83 -0
- package/dist/handlers/gen-image.d.ts +13 -0
- package/dist/handlers/gen-image.js +161 -0
- package/dist/handlers/gen-voice.d.ts +16 -0
- package/dist/handlers/gen-voice.js +110 -0
- package/dist/handlers/index.d.ts +13 -0
- package/dist/handlers/index.js +14 -0
- package/dist/handlers/template-bind.d.ts +15 -0
- package/dist/handlers/template-bind.js +149 -0
- package/dist/http.d.ts +52 -0
- package/dist/http.js +110 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/manifest.json +260 -0
- package/dist/progress.d.ts +23 -0
- package/dist/progress.js +15 -0
- package/dist/registry.d.ts +33 -0
- package/dist/registry.js +72 -0
- package/dist/runner.d.ts +17 -0
- package/dist/runner.js +63 -0
- package/package.json +54 -0
- package/skills/export-jianying/SKILL.md +251 -0
- package/skills/export-jianying/scripts/gen_jianying_draft.py +1099 -0
- package/skills/export-jianying/skill.json +24 -0
- package/skills/export-jianying/version.json +7 -0
- package/skills/gen-digital-human/SKILL.md +211 -0
- package/skills/gen-digital-human/scripts/gen_digital_human_video.py +574 -0
- package/skills/gen-digital-human/skill.json +23 -0
- package/skills/gen-digital-human/version.json +7 -0
- package/skills/gen-image/SKILL.md +130 -0
- package/skills/gen-image/skill.json +22 -0
- package/skills/gen-image/version.json +7 -0
- package/skills/gen-script/SKILL.md +283 -0
- package/skills/gen-script/scripts/gen_script.py +1208 -0
- package/skills/gen-script/skill.json +41 -0
- package/skills/gen-script/version.json +7 -0
- package/skills/gen-video/SKILL.md +170 -0
- package/skills/gen-video/scripts/gen_video.py +591 -0
- package/skills/gen-video/skill.json +24 -0
- package/skills/gen-video/version.json +7 -0
- package/skills/gen-voice/SKILL.md +104 -0
- package/skills/gen-voice/skill.json +21 -0
- package/skills/gen-voice/version.json +7 -0
- package/skills/prepare-video-assets/SKILL.md +192 -0
- package/skills/prepare-video-assets/scripts/prepare_video_assets.py +57 -0
- package/skills/prepare-video-assets/skill.json +23 -0
- package/skills/prepare-video-assets/version.json +7 -0
- package/skills/render-video/SKILL.md +187 -0
- package/skills/render-video/scripts/_chrome_vendor.py +305 -0
- package/skills/render-video/scripts/_video_probe.py +174 -0
- package/skills/render-video/scripts/_vod_polling.py +86 -0
- package/skills/render-video/scripts/remote_renderer_client.py +236 -0
- package/skills/render-video/scripts/render_video.py +2288 -0
- package/skills/render-video/scripts/upload_video.py +193 -0
- package/skills/render-video/skill.json +21 -0
- package/skills/render-video/version.json +7 -0
- package/skills/template-bind/README.md +63 -0
- package/skills/template-bind/SKILL.md +234 -0
- package/skills/template-bind/scripts/check_contracts.py +196 -0
- package/skills/template-bind/scripts/match_template.py +215 -0
- package/skills/template-bind/scripts/registry_loader.py +379 -0
- package/skills/template-bind/scripts/render_job_client.py +162 -0
- package/skills/template-bind/scripts/sync_registry.py +44 -0
- package/skills/template-bind/scripts/template_paths.py +72 -0
- package/skills/template-bind/skill.json +20 -0
- package/skills/template-bind/version.json +7 -0
- package/skills/template-bind/video_dsl/README.md +97 -0
- package/skills/template-bind/video_dsl/runtime/__init__.py +11 -0
- package/skills/template-bind/video_dsl/runtime/dsl_validator.py +495 -0
- package/skills/template-bind/video_dsl/runtime/prompt_enhancer.py +274 -0
- package/skills/template-bind/video_dsl/runtime/template_binder.py +87 -0
- package/skills/template-bind/video_dsl/runtime/timeline_compiler.py +312 -0
- package/skills/template-bind/video_dsl/schema/render-plan-v1alpha1.json +193 -0
- package/skills/template-bind/video_dsl/schema/template-binding-v1alpha1.json +122 -0
- package/skills/template-bind/video_dsl/schema/template-definition-v1alpha1.json +247 -0
- package/skills/template-bind/video_dsl/schema/video-dsl-v1alpha1.json +378 -0
- package/skills/video-parser/SKILL.md +238 -0
- package/skills/video-parser/scripts/analyze_video.py +1038 -0
- package/skills/video-parser/scripts/deconstruct_video.py +602 -0
- package/skills/video-parser/scripts/parse_via_render.py +148 -0
- package/skills/video-parser/skill.json +21 -0
- package/skills/video-parser/version.json +7 -0
- package/skills/web-capture/SKILL.md +183 -0
- package/skills/web-capture/scripts/web_capture.py +461 -0
- package/skills/web-capture/skill.json +29 -0
- package/skills/web-capture/version.json +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aibearer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# @remixmate/cli
|
|
2
|
+
|
|
3
|
+
> åå `ab-skill-cli`ļ¼bin `ab-skill`ļ¼ļ¼ē°å·²ę“åäøŗ **`@remixmate/cli`**ļ¼bin **`remixmate`**ļ¼ćę§å
å·²åØ npm ę č®°å¼ēØļ¼čÆ·čæē§»å°ę°å
ć
|
|
4
|
+
|
|
5
|
+
AI media generation skills for Claude Code / Codex.
|
|
6
|
+
|
|
7
|
+
9 skills covering the full short-video production pipeline: image / video / voice / digital-human asset generation, script planning, template binding, Remotion rendering, and Jianying (CapCut) draft export.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @remixmate/cli
|
|
13
|
+
remixmate --help
|
|
14
|
+
remixmate --list
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Use in Claude Code
|
|
18
|
+
|
|
19
|
+
Copy the bundled `skills/` into your project-level or global `.claude/skills/` directory and Claude Code will pick them up:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# project-level
|
|
23
|
+
mkdir -p .claude/skills
|
|
24
|
+
cp -R "$(npm root -g)/@remixmate/cli/skills/." .claude/skills/
|
|
25
|
+
|
|
26
|
+
# or global
|
|
27
|
+
mkdir -p ~/.claude/skills
|
|
28
|
+
cp -R "$(npm root -g)/@remixmate/cli/skills/." ~/.claude/skills/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use in Codex
|
|
32
|
+
|
|
33
|
+
Codex does not load `SKILL.md` natively. Tell Codex about the CLI in your system prompt and let it invoke `remixmate <skill> ...` when it needs media generation:
|
|
34
|
+
|
|
35
|
+
> When you need AI media generation (image / video / voice / talking-head / Jianying export), prefer running `remixmate <skill-id> --help` first to discover the flags, then `remixmate <skill-id> --json-output ...` to execute. Available skills: `remixmate --list`.
|
|
36
|
+
|
|
37
|
+
See the full intro page (Claude Code / Codex setup, env vars, end-to-end workflow examples) at the project homepage.
|
|
38
|
+
|
|
39
|
+
## Environment
|
|
40
|
+
|
|
41
|
+
Most skills authenticate against ab-api with a `PRIV_TOKEN`. Without a valid token (and network access to ab-api) the template / media-generation skills are unavailable ā the code is open-source, but the generation capabilities are hosted on the ab-api service. Copy `.env.example` to `.env` and source it before running:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cp .env.example .env
|
|
45
|
+
# edit values
|
|
46
|
+
source .env
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Python skills (6 of 9) require `python3 >= 3.10`. `video-parser` additionally requires `ffmpeg`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## AI åŖä½ēę Agent Skillsļ¼äøęļ¼
|
|
54
|
+
|
|
55
|
+
ę¬é”¹ē®å
å« 9 äøŖ AI åŖä½ēęęč½ļ¼č¦ēä»ē“ ęēęćčę¬ē¼ęć樔ęæē»å®ćč§é¢ęø²ęå°åŖę 导åŗēå®ę“č§é¢å
容ēäŗ§é¾č·Æć
|
|
56
|
+
|
|
57
|
+
### ęč½åå±
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
61
|
+
ā ē¼ęå± Skills ā
|
|
62
|
+
ā gen-script äø»é¢ ā Video DSLļ¼čę¬ēęļ¼ ā
|
|
63
|
+
ā template-bind DSL ā TemplateBindingļ¼ęØ”ęæåč”Øļ¼ ā
|
|
64
|
+
ā prepare-video-assets DSL + Binding ā ē“ ę蔄é½ļ¼Phase 1ļ¼ ā
|
|
65
|
+
ā render-video RenderPlan ā Remotion ęø²ęļ¼Phase 3ļ¼ ā
|
|
66
|
+
ā export-jianying ē“ ę ā åŖę č稿 ZIP ā
|
|
67
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
|
|
68
|
+
ā ååå± Skills ā
|
|
69
|
+
ā gen-image ęēå¾ / å¾ēå¾ļ¼Seedream / Geminiļ¼ ā
|
|
70
|
+
ā gen-video ęēč§é¢ļ¼Seedance / Veoļ¼ ā
|
|
71
|
+
ā gen-voice čÆé³åęļ¼Minimax TTSļ¼ ā
|
|
72
|
+
ā gen-digital-human ę°åäŗŗå£ęļ¼å³ę¢¦ / é£å½±ļ¼ ā
|
|
73
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
|
|
74
|
+
ā å·„å
·å± Skills ā
|
|
75
|
+
ā video-parser č§é¢č§£ęļ¼é³é¢ęå / ASR / å
³é®åø§ļ¼ ā
|
|
76
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### ē®å½ē»ę
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
āāā docs/
|
|
83
|
+
ā āāā video-production-architecture.md # ę¶ęę攣
|
|
84
|
+
āāā skills/
|
|
85
|
+
ā āāā gen-image/ # åå: AI ēå¾
|
|
86
|
+
ā āāā gen-video/ # åå: AI ēč§é¢
|
|
87
|
+
ā āāā gen-voice/ # åå: čÆé³åę
|
|
88
|
+
ā āāā gen-digital-human/ # åå: ę°åäŗŗå£ę
|
|
89
|
+
ā āāā gen-script/ # ē¼ę: äø»é¢ ā Video DSL
|
|
90
|
+
ā āāā template-bind/ # ē¼ę: 樔ęæå蔨ļ¼å«ęØ”ęæę³Øå蔨 + DSL Schemaļ¼
|
|
91
|
+
ā āāā prepare-video-assets/ # ē¼ę: Phase 1 ē“ ęåå¤ļ¼thin wrapperļ¼
|
|
92
|
+
ā āāā render-video/ # ē¼ę: Phase 3 Remotion ęø²ęļ¼å« render_video.py å®ē°ļ¼
|
|
93
|
+
ā āāā export-jianying/ # ē¼ę: 导åŗåŖę č稿 ZIP
|
|
94
|
+
ā āāā video-parser/ # å·„å
·: č§é¢č§£ęäøåę
|
|
95
|
+
āāā README.md
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### ęč½čÆ“ę
|
|
99
|
+
|
|
100
|
+
| ęč½ | ē±»å | 诓ę | ę øåæčę¬ |
|
|
101
|
+
|------|------|------|----------|
|
|
102
|
+
| gen-image | åå | ęēå¾ / å¾ēå¾ļ¼SeedreamćGeminiļ¼ | `gen_image.py` |
|
|
103
|
+
| gen-video | åå | ęēč§é¢ļ¼SeedancećVeoļ¼ | `gen_video.py` |
|
|
104
|
+
| gen-voice | åå | čÆé³åęļ¼Minimax TTSļ¼ | `gen_voice.py` |
|
|
105
|
+
| gen-digital-human | åå | ę°åäŗŗå£ęļ¼å³ę¢¦ / é£å½±ļ¼ | `gen_digital_human_video.py` |
|
|
106
|
+
| gen-script | ē¼ę | äø»é¢ ā Video DSL JSON | `gen_script.py` |
|
|
107
|
+
| template-bind | ē¼ę | 樔ęæå蔨ļ¼ē»å®é»č¾å
åµäŗ prepare-video-assetsļ¼ | `match_template.py` |
|
|
108
|
+
| prepare-video-assets | ē¼ę | DSL + Binding ā ē“ ęč”„é½ ā č½åŗ RenderPlan | `render_video.py --resolve-only`ļ¼wrapperļ¼ |
|
|
109
|
+
| render-video | ē¼ę | job_id ā Remotion ęø²ę ā äøä¼ | `render_video.py` |
|
|
110
|
+
| export-jianying | ē¼ę | ē“ ę URL ā åŖę č稿 ZIPļ¼ęÆęä» RenderPlan čŖåØč½¬ę¢ļ¼ | `gen_jianying_draft.py` |
|
|
111
|
+
| video-parser | å·„å
· | č§é¢ ā é³é¢ + ASR + å
³é®åø§ + åŗęÆåꮵ | `deconstruct_video.py` |
|
|
112
|
+
|
|
113
|
+
### ę øåæé¾č·Æ
|
|
114
|
+
|
|
115
|
+
čÆ¦č§ [č§é¢å
容ēäŗ§ę¶ęę攣](docs/video-production-architecture.md) å [ē¼ęęµēØęå](docs/orchestration-guide.md)ć
|
|
116
|
+
|
|
117
|
+
**é¾č·Æ Aļ¼äø»é¢ ā Remotion č§é¢**
|
|
118
|
+
```
|
|
119
|
+
gen-script ā ā
ēØę·ē”®č®¤čę¬ ā prepare-video-assets(--template-id)
|
|
120
|
+
ā ā
ēØę·ē”®č®¤ē“ ę ā render-video(--job-id) ā MP4
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**é¾č·Æ Bļ¼äø»é¢ ā åŖę č稿**
|
|
124
|
+
```
|
|
125
|
+
gen-script ā ā
ēØę·ē”®č®¤čę¬ ā prepare-video-assets(--template-id)
|
|
126
|
+
ā ā
ēØę·ē”®č®¤ē“ ę ā export-jianying(--from-job-id) ā åŖę ZIP
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### ęµčÆ
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# åčę¬ --help åÆę£åøøčæč”
|
|
133
|
+
python3 skills/gen-image/scripts/gen_image.py --help
|
|
134
|
+
python3 skills/gen-video/scripts/gen_video.py --help
|
|
135
|
+
python3 skills/gen-voice/scripts/gen_voice.py --help
|
|
136
|
+
python3 skills/gen-digital-human/scripts/gen_digital_human_video.py --help
|
|
137
|
+
python3 skills/gen-script/scripts/gen_script.py --help
|
|
138
|
+
python3 skills/template-bind/scripts/match_template.py --help
|
|
139
|
+
python3 skills/render-video/scripts/render_video.py --help
|
|
140
|
+
python3 skills/prepare-video-assets/scripts/prepare_video_assets.py --help
|
|
141
|
+
python3 skills/export-jianying/scripts/gen_jianying_draft.py --help
|
|
142
|
+
python3 skills/video-parser/scripts/deconstruct_video.py --help
|
|
143
|
+
|
|
144
|
+
# ę„ēåÆēØé³č²
|
|
145
|
+
python3 skills/gen-voice/scripts/gen_voice.py --list-voices
|
|
146
|
+
|
|
147
|
+
# ę„ēåÆēØęØ”ęæ
|
|
148
|
+
python3 skills/template-bind/scripts/match_template.py --list-templates
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### å¼åęµēØ
|
|
152
|
+
|
|
153
|
+
- å
éę¤é”¹ē®å°ę¬å°
|
|
154
|
+
- åØ skills ē®å½äøåęč½ę件夹äøå¼å
|
|
155
|
+
- ęÆäøŖęč½å
å« `SKILL.md`ļ¼ęč½čÆ“ęļ¼ć`version.json`ļ¼å
ę°ę®ļ¼å `scripts/`ļ¼čę¬ļ¼
|
|
156
|
+
|
|
157
|
+
### åæ«éä½éŖ
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
@skills/gen-image/SKILL.md ēęäøå¼ ēē«ēå¾ēļ¼9:16ļ¼č°ēØgemini 3.1ļ¼å½ē»é£ + äø„ę ¼ę评ę攣ę§č”
|
|
161
|
+
@skills/gen-video/SKILL.md ēęäøę®µēē«åØē«¹ęå„č·ēč§é¢ļ¼9:16ļ¼éæåŗ¦6ē§ļ¼č°ēØveo + äø„ę ¼ę评ę攣ę§č”
|
|
162
|
+
@skills/gen-voice/SKILL.md ēęäøę®µčÆé³ļ¼ä»ē»ēē«ēä¹ ę§ļ¼å¤§ę¦100åå·¦å³ + äø„ę ¼ę评ę攣ę§č”
|
|
163
|
+
@skills/gen-digital-human/SKILL.md č·åę°åäŗŗå蔨 + äø„ę ¼ę评ę攣ę§č”
|
|
164
|
+
@skills/template-bind/SKILL.md č·å樔ēå蔨 + äø„ę ¼ę评ę攣ę§č”
|
|
165
|
+
@skills/gen-script/SKILL.md åŗäŗęØ”ē image-slideļ¼åä½äøäøŖå
³äŗAIå¦ä¹ ę¹ę³ēč§é¢ + äø„ę ¼ę评ę攣ę§č”
|
|
166
|
+
```
|
package/dist/argv.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argv parser shared by cli.ts and python passthrough.
|
|
3
|
+
*
|
|
4
|
+
* Convention (matches ab-agent's buildCliArgs):
|
|
5
|
+
* --foo-bar value ā foo_bar: 'value'
|
|
6
|
+
* --foo-bar ā foo_bar: true
|
|
7
|
+
* --foo-bar a --foo-bar b ā foo_bar: ['a', 'b']
|
|
8
|
+
* -p value ā short flags also supported as-is (kebab ā snake)
|
|
9
|
+
*
|
|
10
|
+
* Booleans are detected when the next token starts with '--' or is absent.
|
|
11
|
+
* Negative number values (-1.5) are NOT supported as flag values in this
|
|
12
|
+
* parser ā Python skills handle them via argparse downstream when needed.
|
|
13
|
+
*/
|
|
14
|
+
export type ParsedArgs = Record<string, string | boolean | string[]>;
|
|
15
|
+
export declare function parseArgv(argv: string[]): ParsedArgs;
|
package/dist/argv.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argv parser shared by cli.ts and python passthrough.
|
|
3
|
+
*
|
|
4
|
+
* Convention (matches ab-agent's buildCliArgs):
|
|
5
|
+
* --foo-bar value ā foo_bar: 'value'
|
|
6
|
+
* --foo-bar ā foo_bar: true
|
|
7
|
+
* --foo-bar a --foo-bar b ā foo_bar: ['a', 'b']
|
|
8
|
+
* -p value ā short flags also supported as-is (kebab ā snake)
|
|
9
|
+
*
|
|
10
|
+
* Booleans are detected when the next token starts with '--' or is absent.
|
|
11
|
+
* Negative number values (-1.5) are NOT supported as flag values in this
|
|
12
|
+
* parser ā Python skills handle them via argparse downstream when needed.
|
|
13
|
+
*/
|
|
14
|
+
function flagToKey(flag) {
|
|
15
|
+
return flag.replace(/^--?/, '').replace(/-/g, '_');
|
|
16
|
+
}
|
|
17
|
+
export function parseArgv(argv) {
|
|
18
|
+
const out = {};
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < argv.length) {
|
|
21
|
+
const token = argv[i];
|
|
22
|
+
if (!token.startsWith('-')) {
|
|
23
|
+
i++;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const key = flagToKey(token);
|
|
27
|
+
const next = argv[i + 1];
|
|
28
|
+
if (next === undefined || next.startsWith('--')) {
|
|
29
|
+
out[key] = true;
|
|
30
|
+
i++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Treat next token as value.
|
|
34
|
+
const existing = out[key];
|
|
35
|
+
if (existing === undefined) {
|
|
36
|
+
out[key] = next;
|
|
37
|
+
}
|
|
38
|
+
else if (Array.isArray(existing)) {
|
|
39
|
+
existing.push(next);
|
|
40
|
+
}
|
|
41
|
+
else if (typeof existing === 'string') {
|
|
42
|
+
out[key] = [existing, next];
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Was boolean ā overwrite (last write wins; unusual case)
|
|
46
|
+
out[key] = next;
|
|
47
|
+
}
|
|
48
|
+
i += 2;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* remixmate CLI entrypoint.
|
|
4
|
+
*
|
|
5
|
+
* remixmate --list list registered skills
|
|
6
|
+
* remixmate <name> --help forward --help to underlying skill
|
|
7
|
+
* remixmate <name> [--flag value ...] invoke the skill
|
|
8
|
+
*
|
|
9
|
+
* For python skills (entry.type === 'python') we spawn the script and inherit
|
|
10
|
+
* its stdio so progress / errors flow through unchanged.
|
|
11
|
+
* For http / builtin skills we dispatch in-process to handlers/.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* remixmate CLI entrypoint.
|
|
4
|
+
*
|
|
5
|
+
* remixmate --list list registered skills
|
|
6
|
+
* remixmate <name> --help forward --help to underlying skill
|
|
7
|
+
* remixmate <name> [--flag value ...] invoke the skill
|
|
8
|
+
*
|
|
9
|
+
* For python skills (entry.type === 'python') we spawn the script and inherit
|
|
10
|
+
* its stdio so progress / errors flow through unchanged.
|
|
11
|
+
* For http / builtin skills we dispatch in-process to handlers/.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { loadSkills, SKILLS_DIR } from './registry.js';
|
|
15
|
+
import { parseArgv } from './argv.js';
|
|
16
|
+
import { runSkill } from './runner.js';
|
|
17
|
+
/**
|
|
18
|
+
* SKILL_BASE_DIR can arrive as a relative path (e.g. from a developer .env or
|
|
19
|
+
* from ab-agent's spawn env). The cli script's cwd isn't a stable anchor for
|
|
20
|
+
* that ā it's whatever the caller set, often the bundled `skills-cli/`
|
|
21
|
+
* directory. Resolve against process.cwd() (= the caller's intended root) up
|
|
22
|
+
* front so any later existsSync / readdirSync call sees an absolute,
|
|
23
|
+
* predictable path.
|
|
24
|
+
*/
|
|
25
|
+
function resolveBaseDir() {
|
|
26
|
+
const raw = process.env.SKILL_BASE_DIR;
|
|
27
|
+
if (!raw)
|
|
28
|
+
return SKILLS_DIR;
|
|
29
|
+
return path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
|
|
30
|
+
}
|
|
31
|
+
function describeEntryTail(s) {
|
|
32
|
+
if (s.entry.type === 'python') {
|
|
33
|
+
return `(scripts/${s.entry.scriptPath.replace(/^scripts\//, '')})`;
|
|
34
|
+
}
|
|
35
|
+
return `[${s.entry.handler}]`;
|
|
36
|
+
}
|
|
37
|
+
function printUsage() {
|
|
38
|
+
// Match runSkill: honor SKILL_BASE_DIR so `remixmate --help` and the
|
|
39
|
+
// subsequent `remixmate <name>` invocation list / run from the same tree.
|
|
40
|
+
const skills = loadSkills(resolveBaseDir());
|
|
41
|
+
process.stdout.write('Usage:\n');
|
|
42
|
+
process.stdout.write(' remixmate --list List all skills\n');
|
|
43
|
+
process.stdout.write(' remixmate <name> [--flag value ...] Invoke a skill\n');
|
|
44
|
+
process.stdout.write(' remixmate <name> --help Show skill-specific help\n');
|
|
45
|
+
process.stdout.write('\nAvailable skills:\n');
|
|
46
|
+
for (const s of skills) {
|
|
47
|
+
const summary = s.description.split('\n')[0].slice(0, 60);
|
|
48
|
+
process.stdout.write(` ${s.name.padEnd(22)} ${s.entry.type.padEnd(8)} ${summary}\n`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function printList() {
|
|
52
|
+
for (const s of loadSkills(resolveBaseDir())) {
|
|
53
|
+
process.stdout.write(`${s.name}\t${s.toolName}\t${s.entry.type}\t${describeEntryTail(s)}\n`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function main() {
|
|
57
|
+
const argv = process.argv.slice(2);
|
|
58
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
59
|
+
printUsage();
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
if (argv[0] === '--list' || argv[0] === 'list') {
|
|
63
|
+
printList();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
if (argv[0].startsWith('-')) {
|
|
67
|
+
process.stderr.write(`ā unknown option: ${argv[0]}\n`);
|
|
68
|
+
printUsage();
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
const skillName = argv[0];
|
|
72
|
+
const rest = argv.slice(1);
|
|
73
|
+
const code = await runSkill(skillName, {
|
|
74
|
+
baseDir: resolveBaseDir(),
|
|
75
|
+
rawArgs: rest,
|
|
76
|
+
parsedArgs: parseArgv(rest),
|
|
77
|
+
});
|
|
78
|
+
process.exit(code);
|
|
79
|
+
}
|
|
80
|
+
main().catch((err) => {
|
|
81
|
+
process.stderr.write(`ā fatal: ${err?.stack ?? err?.message ?? String(err)}\n`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-image handler ā POST ab-api /model/genImg.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the previous `scripts/gen_image.py` behavior:
|
|
5
|
+
* - Seedream models accept width/height (via SIZE_PRESETS lookup or WxH parse)
|
|
6
|
+
* - Gemini models accept aspectRatio + size (1K/2K/4K)
|
|
7
|
+
* - Reference images: HTTPS URLs and data: URIs pass through; local paths
|
|
8
|
+
* are read and base64-encoded into a data: URI
|
|
9
|
+
* - Async response (generationId + status:'generating'/'pending') is polled
|
|
10
|
+
* against /model/getImgStatus until completed/failed/timeout
|
|
11
|
+
*/
|
|
12
|
+
import type { HandlerContext, HandlerInput } from './index.js';
|
|
13
|
+
export declare function genImage(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-image handler ā POST ab-api /model/genImg.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the previous `scripts/gen_image.py` behavior:
|
|
5
|
+
* - Seedream models accept width/height (via SIZE_PRESETS lookup or WxH parse)
|
|
6
|
+
* - Gemini models accept aspectRatio + size (1K/2K/4K)
|
|
7
|
+
* - Reference images: HTTPS URLs and data: URIs pass through; local paths
|
|
8
|
+
* are read and base64-encoded into a data: URI
|
|
9
|
+
* - Async response (generationId + status:'generating'/'pending') is polled
|
|
10
|
+
* against /model/getImgStatus until completed/failed/timeout
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
15
|
+
import { emitProgress } from '../progress.js';
|
|
16
|
+
const DEFAULT_IMAGE_MODEL = 'doubao/doubao-seedream-5-0-260128';
|
|
17
|
+
// Maps aspect-ratio shorthand ā (width, height) for Seedream models.
|
|
18
|
+
const SIZE_PRESETS = {
|
|
19
|
+
'1:1': [2048, 2048],
|
|
20
|
+
'16:9': [2848, 1600],
|
|
21
|
+
'9:16': [1600, 2848],
|
|
22
|
+
'4:3': [2304, 1728],
|
|
23
|
+
'3:4': [1728, 2304],
|
|
24
|
+
'2:3': [1664, 2496],
|
|
25
|
+
};
|
|
26
|
+
const MIME_BY_EXT = {
|
|
27
|
+
'.png': 'image/png',
|
|
28
|
+
'.jpg': 'image/jpeg',
|
|
29
|
+
'.jpeg': 'image/jpeg',
|
|
30
|
+
'.webp': 'image/webp',
|
|
31
|
+
'.gif': 'image/gif',
|
|
32
|
+
};
|
|
33
|
+
// Per-model reference-image caps (matches ab-api validation).
|
|
34
|
+
const SEEDREAM_REF_LIMIT = 14;
|
|
35
|
+
const GEMINI_REF_LIMIT = 4;
|
|
36
|
+
function isSeedreamModel(model) {
|
|
37
|
+
const lower = model.toLowerCase();
|
|
38
|
+
return lower.includes('seedream') || lower.includes('seedance');
|
|
39
|
+
}
|
|
40
|
+
function resolveImageInput(pathOrUrl) {
|
|
41
|
+
const trimmed = pathOrUrl.trim();
|
|
42
|
+
if (!trimmed)
|
|
43
|
+
return '';
|
|
44
|
+
const lower = trimmed.toLowerCase();
|
|
45
|
+
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:')) {
|
|
46
|
+
return trimmed;
|
|
47
|
+
}
|
|
48
|
+
if (!existsSync(trimmed) || !statSync(trimmed).isFile()) {
|
|
49
|
+
throw new SkillError(`ā image path does not exist or is not a file: ${trimmed}`);
|
|
50
|
+
}
|
|
51
|
+
const ext = path.extname(trimmed).toLowerCase();
|
|
52
|
+
const mime = MIME_BY_EXT[ext] ?? 'image/jpeg';
|
|
53
|
+
const b64 = readFileSync(trimmed).toString('base64');
|
|
54
|
+
return `data:${mime};base64,${b64}`;
|
|
55
|
+
}
|
|
56
|
+
/** Coerce a CLI flag value (string|string[]|boolean) into an array of strings. */
|
|
57
|
+
function toStringArray(value) {
|
|
58
|
+
if (value === undefined || value === null)
|
|
59
|
+
return [];
|
|
60
|
+
if (Array.isArray(value))
|
|
61
|
+
return value.map(String);
|
|
62
|
+
return [String(value)];
|
|
63
|
+
}
|
|
64
|
+
function toNumber(value, name) {
|
|
65
|
+
if (value === undefined || value === null)
|
|
66
|
+
return undefined;
|
|
67
|
+
const n = Number(value);
|
|
68
|
+
if (!Number.isFinite(n))
|
|
69
|
+
throw new SkillError(`ā ${name} must be a number, received: ${value}`);
|
|
70
|
+
return n;
|
|
71
|
+
}
|
|
72
|
+
/** Resolve {width, height} for Seedream from --size (preset key or WxH). */
|
|
73
|
+
function resolveSeedreamSize(size) {
|
|
74
|
+
if (SIZE_PRESETS[size]) {
|
|
75
|
+
const [w, h] = SIZE_PRESETS[size];
|
|
76
|
+
return { width: w, height: h };
|
|
77
|
+
}
|
|
78
|
+
if (/^\d+x\d+$/i.test(size)) {
|
|
79
|
+
const [w, h] = size.toLowerCase().split('x').map((v) => parseInt(v, 10));
|
|
80
|
+
return { width: w, height: h };
|
|
81
|
+
}
|
|
82
|
+
throw new SkillError(`ā Seedream model does not support size format: ${size} (use 1:1 / 9:16 / 16:9 ... or WxH like 1600x2848)`);
|
|
83
|
+
}
|
|
84
|
+
async function pollImageStatus(ctx, generationId) {
|
|
85
|
+
return pollUntil(async () => {
|
|
86
|
+
const data = await mmPost(ctx, '/model/getImgStatus', { generationId }, { timeoutMs: 30_000 });
|
|
87
|
+
const status = data.status ?? 'unknown';
|
|
88
|
+
emitProgress({ phase: 'gen-image:poll', generationId, status });
|
|
89
|
+
if (status === 'completed' || status === 'succeeded') {
|
|
90
|
+
return (data.imageUrls ?? []).filter((u) => !!u);
|
|
91
|
+
}
|
|
92
|
+
if (status === 'failed') {
|
|
93
|
+
const err = data.errorMsg ?? data.error ?? 'unknown error';
|
|
94
|
+
throw new SkillError(`ā image generation failed: ${err}`);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}, { intervalMs: 5_000, timeoutMs: 120_000 });
|
|
98
|
+
}
|
|
99
|
+
export async function genImage(input, ctxIn) {
|
|
100
|
+
const prompt = input.prompt;
|
|
101
|
+
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
102
|
+
throw new SkillError('ā Please provide --prompt to describe the image');
|
|
103
|
+
}
|
|
104
|
+
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
105
|
+
apiBaseUrl: input.api_base_url,
|
|
106
|
+
privateToken: input.priv_token,
|
|
107
|
+
});
|
|
108
|
+
const model = input.model ?? process.env.MM_IMAGE_MODEL ?? DEFAULT_IMAGE_MODEL;
|
|
109
|
+
const size = input.size ?? '1:1';
|
|
110
|
+
const n = toNumber(input.n, 'n') ?? 1;
|
|
111
|
+
const resolution = input.resolution ?? '1K';
|
|
112
|
+
const seed = toNumber(input.seed, 'seed');
|
|
113
|
+
const imageStrength = toNumber(input.image_strength, 'image_strength');
|
|
114
|
+
const guidanceScale = toNumber(input.guidance_scale, 'guidance_scale');
|
|
115
|
+
const negativePrompt = input.negative_prompt;
|
|
116
|
+
// --watermark / --no-watermark ā parseArgv only sees `--watermark true` style;
|
|
117
|
+
// when ab-agent's buildCliArgs passes booleans, only the truthy flag appears.
|
|
118
|
+
// Treat presence as true; explicit `--no-watermark` isn't supported here.
|
|
119
|
+
const watermark = input.watermark === true ? true : undefined;
|
|
120
|
+
const seedream = isSeedreamModel(model);
|
|
121
|
+
const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
|
|
122
|
+
const refLimit = seedream ? SEEDREAM_REF_LIMIT : GEMINI_REF_LIMIT;
|
|
123
|
+
if (references.length > refLimit) {
|
|
124
|
+
throw new SkillError(`ā ${seedream ? 'Seedream' : 'Gemini'} model accepts at most ${refLimit} reference images, got ${references.length}`);
|
|
125
|
+
}
|
|
126
|
+
const payload = { model, prompt, imageCount: n };
|
|
127
|
+
if (guidanceScale !== undefined)
|
|
128
|
+
payload.guidanceScale = guidanceScale;
|
|
129
|
+
if (references.length > 0)
|
|
130
|
+
payload.referenceImages = references;
|
|
131
|
+
if (imageStrength !== undefined)
|
|
132
|
+
payload.imageStrength = imageStrength;
|
|
133
|
+
if (negativePrompt)
|
|
134
|
+
payload.negativePrompt = negativePrompt;
|
|
135
|
+
if (seed !== undefined)
|
|
136
|
+
payload.seed = seed;
|
|
137
|
+
if (watermark !== undefined)
|
|
138
|
+
payload.watermark = watermark;
|
|
139
|
+
if (seedream) {
|
|
140
|
+
Object.assign(payload, resolveSeedreamSize(size));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
payload.aspectRatio = size;
|
|
144
|
+
payload.size = resolution;
|
|
145
|
+
}
|
|
146
|
+
emitProgress({ phase: 'gen-image:request', model, size, n });
|
|
147
|
+
const data = await mmPost(ctx, '/model/genImg', payload, { timeoutMs: 120_000 });
|
|
148
|
+
let imageUrls = (data.imageUrls ?? []).filter((u) => !!u);
|
|
149
|
+
if (imageUrls.length === 0 && data.generationId && (data.status === 'generating' || data.status === 'pending')) {
|
|
150
|
+
emitProgress({ phase: 'gen-image:async', generationId: data.generationId });
|
|
151
|
+
imageUrls = await pollImageStatus(ctx, data.generationId);
|
|
152
|
+
}
|
|
153
|
+
if (imageUrls.length === 0) {
|
|
154
|
+
throw new SkillError(`ā API response did not contain any image URL: ${JSON.stringify(data)}`);
|
|
155
|
+
}
|
|
156
|
+
process.stdout.write(`\nšø Generated ${imageUrls.length} image(s)\n`);
|
|
157
|
+
process.stdout.write(`\nš Image URLs (renders directly, no download needed):\n`);
|
|
158
|
+
for (const url of imageUrls)
|
|
159
|
+
process.stdout.write(`${url}\n`);
|
|
160
|
+
process.stdout.write(`\nš Image generation done!\n`);
|
|
161
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-voice handler ā Minimax TTS via ab-api /tool/minimaxT2aV2.
|
|
3
|
+
*
|
|
4
|
+
* Operations:
|
|
5
|
+
* --list-voices true ā GET available voice IDs from /voice/page (live)
|
|
6
|
+
* --list-voices true --local true ā print the built-in language-tagged
|
|
7
|
+
* fallback catalog (offline, no API call)
|
|
8
|
+
* default ā synthesize text into an MP3 URL
|
|
9
|
+
*
|
|
10
|
+
* The previous Python script also offered `--download` to write the MP3 to
|
|
11
|
+
* disk. That's intentionally dropped here: the agent flow never used it (the
|
|
12
|
+
* audio URL is persisted in cloud) and removing local file I/O keeps the
|
|
13
|
+
* handler stateless.
|
|
14
|
+
*/
|
|
15
|
+
import type { HandlerContext, HandlerInput } from './index.js';
|
|
16
|
+
export declare function genVoice(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-voice handler ā Minimax TTS via ab-api /tool/minimaxT2aV2.
|
|
3
|
+
*
|
|
4
|
+
* Operations:
|
|
5
|
+
* --list-voices true ā GET available voice IDs from /voice/page (live)
|
|
6
|
+
* --list-voices true --local true ā print the built-in language-tagged
|
|
7
|
+
* fallback catalog (offline, no API call)
|
|
8
|
+
* default ā synthesize text into an MP3 URL
|
|
9
|
+
*
|
|
10
|
+
* The previous Python script also offered `--download` to write the MP3 to
|
|
11
|
+
* disk. That's intentionally dropped here: the agent flow never used it (the
|
|
12
|
+
* audio URL is persisted in cloud) and removing local file I/O keeps the
|
|
13
|
+
* handler stateless.
|
|
14
|
+
*/
|
|
15
|
+
import { mmPost, resolveHttpContext, SkillError } from '../http.js';
|
|
16
|
+
import { emitProgress } from '../progress.js';
|
|
17
|
+
const DEFAULT_VOICE_ID = 'Chinese (Mandarin)_Male_Announcer';
|
|
18
|
+
/**
|
|
19
|
+
* Fallback voice catalog printed when `--list-voices --local` is passed.
|
|
20
|
+
* Mirrors the language-keyed defaults that gen_script.py uses, kept in sync
|
|
21
|
+
* by hand because the two callers live on different runtimes.
|
|
22
|
+
*
|
|
23
|
+
* Promote to a JSON config once this grows beyond a handful of entries.
|
|
24
|
+
*/
|
|
25
|
+
const LOCAL_VOICE_CATALOG = [
|
|
26
|
+
{ id: 'Chinese (Mandarin)_Male_Announcer', language: 'zh', name: 'Mandarin Male Announcer (legacy default)' },
|
|
27
|
+
// TODO: replace once gen-voice service confirms an English default voice id.
|
|
28
|
+
{ id: 'english-male-friendly-01', language: 'en', name: 'Friendly Male (English, TBD)' },
|
|
29
|
+
];
|
|
30
|
+
function toNumber(value, name, defaultValue) {
|
|
31
|
+
if (value === undefined || value === null || value === '')
|
|
32
|
+
return defaultValue;
|
|
33
|
+
const n = Number(value);
|
|
34
|
+
if (!Number.isFinite(n))
|
|
35
|
+
throw new SkillError(`ā ${name} must be a number, received: ${value}`);
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
function isTrue(value) {
|
|
39
|
+
return value === true || value === 'true';
|
|
40
|
+
}
|
|
41
|
+
async function listVoicesRemote(ctx) {
|
|
42
|
+
const data = await mmPost(ctx, '/voice/page', { current: 1, pageSize: 100 }, { timeoutMs: 30_000 });
|
|
43
|
+
const voices = data.list ?? [];
|
|
44
|
+
if (voices.length === 0) {
|
|
45
|
+
process.stdout.write('ā ļø No voices available\n');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
process.stdout.write(`\n${'Voice ID'.padEnd(30)} Name\n`);
|
|
49
|
+
process.stdout.write(`${'-'.repeat(30)} ${'-'.repeat(20)}\n`);
|
|
50
|
+
for (const v of voices) {
|
|
51
|
+
process.stdout.write(`${(v.uniqId ?? '').padEnd(30)} ${v.name ?? ''}\n`);
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write(`\n${voices.length} voice(s) available\n`);
|
|
54
|
+
process.stdout.write(`š” Use --voice-id <id> to select a voice for synthesis\n`);
|
|
55
|
+
}
|
|
56
|
+
function listVoicesLocal() {
|
|
57
|
+
for (const v of LOCAL_VOICE_CATALOG) {
|
|
58
|
+
process.stdout.write(`${v.id}\t${v.language}\t${v.name}\n`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function genVoice(input, ctxIn) {
|
|
62
|
+
// āā --list-voices: catalog mode (no synthesis) āāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
63
|
+
if (isTrue(input.list_voices)) {
|
|
64
|
+
if (isTrue(input.local))
|
|
65
|
+
return listVoicesLocal();
|
|
66
|
+
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
67
|
+
apiBaseUrl: input.api_base_url,
|
|
68
|
+
privateToken: input.priv_token,
|
|
69
|
+
});
|
|
70
|
+
return listVoicesRemote(ctx);
|
|
71
|
+
}
|
|
72
|
+
// āā synthesize āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
73
|
+
const text = input.text;
|
|
74
|
+
if (!text || typeof text !== 'string' || !text.trim()) {
|
|
75
|
+
throw new SkillError('ā Please provide --text to specify the text to synthesize');
|
|
76
|
+
}
|
|
77
|
+
const voiceId = input.voice_id ?? DEFAULT_VOICE_ID;
|
|
78
|
+
const speed = toNumber(input.speed, 'speed', 1.0);
|
|
79
|
+
if (speed < 0.5 || speed > 2.0) {
|
|
80
|
+
throw new SkillError('ā speed must be in the range 0.5~2.0');
|
|
81
|
+
}
|
|
82
|
+
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
83
|
+
apiBaseUrl: input.api_base_url,
|
|
84
|
+
privateToken: input.priv_token,
|
|
85
|
+
});
|
|
86
|
+
emitProgress({ phase: 'gen-voice:request', voiceId, speed });
|
|
87
|
+
const data = await mmPost(ctx, '/tool/minimaxT2aV2', { text, voiceSetting: { voiceId, speed }, outputFormat: 'url' }, { timeoutMs: 120_000 });
|
|
88
|
+
const audioUrl = data.audio;
|
|
89
|
+
if (!audioUrl) {
|
|
90
|
+
throw new SkillError(`ā API response did not contain an audio URL: ${JSON.stringify(data)}`);
|
|
91
|
+
}
|
|
92
|
+
if (isTrue(input.json_output)) {
|
|
93
|
+
process.stdout.write(JSON.stringify({
|
|
94
|
+
url: audioUrl,
|
|
95
|
+
audio_length_ms: data.extraInfo?.audioLength ?? null,
|
|
96
|
+
subtitles: (data.subtitles ?? []).map((s) => ({
|
|
97
|
+
text: s.text ?? '',
|
|
98
|
+
timeBegin: s.timeBegin ?? 0,
|
|
99
|
+
timeEnd: s.timeEnd ?? 0,
|
|
100
|
+
})),
|
|
101
|
+
}) + '\n');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const lengthMs = data.extraInfo?.audioLength;
|
|
105
|
+
if (typeof lengthMs === 'number') {
|
|
106
|
+
process.stdout.write(`\nš Audio length: ${(lengthMs / 1000).toFixed(1)} s\n`);
|
|
107
|
+
}
|
|
108
|
+
process.stdout.write(`\nš Audio URL (plays directly, no download needed):\n${audioUrl}\n`);
|
|
109
|
+
process.stdout.write(`\nš TTS synthesis done!\n`);
|
|
110
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler registry ā maps `entry.handler` strings to their implementation.
|
|
3
|
+
*
|
|
4
|
+
* Only `entry.type` in ('http' | 'builtin') uses this registry; python
|
|
5
|
+
* skills go through spawn() in runner.ts and bypass handlers entirely.
|
|
6
|
+
*/
|
|
7
|
+
export type HandlerInput = Record<string, unknown>;
|
|
8
|
+
export interface HandlerContext {
|
|
9
|
+
/** ISO-style skill name from the registry (e.g. `gen-image`). Used for x-invoke-skill. */
|
|
10
|
+
skillName: string;
|
|
11
|
+
}
|
|
12
|
+
export type SkillHandler = (input: HandlerInput, ctx: HandlerContext) => Promise<void>;
|
|
13
|
+
export declare const HANDLERS: Record<string, SkillHandler>;
|