@acedatacloud/skills 2026.712.1 → 2026.713.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/AGENTS.md +1 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/skills/_shared/mcp-servers.md +1 -0
- package/skills/hashnode/SKILL.md +79 -131
- package/skills/hashnode/scripts/hashnode.py +364 -0
- package/skills/maestro-video/SKILL.md +271 -0
package/AGENTS.md
CHANGED
|
@@ -24,6 +24,7 @@ Skills are located in the `skills/` directory (also mirrored to `.agents/skills/
|
|
|
24
24
|
- **hailuo-video** — Generate videos with Hailuo / MiniMax
|
|
25
25
|
- **seedance-video** — Generate dance/motion videos with ByteDance Seedance
|
|
26
26
|
- **wan-video** — Generate videos with Alibaba Wan
|
|
27
|
+
- **maestro-video** — Produce complete videos from a brief with scripting, media, voiceover, editing, captions, and multilingual variants
|
|
27
28
|
|
|
28
29
|
### AI Chat & Tools
|
|
29
30
|
- **ai-chat** — Unified LLM gateway — GPT, Claude, Gemini, Kimi, Grok (50+ models)
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
Compatible with **30+ AI coding agents** via the [agentskills.io](https://agentskills.io/) open standard: Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, Cursor, Roo Code, Goose, and more.
|
|
14
14
|
|
|
15
|
-
## Available Skills
|
|
15
|
+
## Available Skills
|
|
16
16
|
|
|
17
17
|
### AI Music & Audio
|
|
18
18
|
|
|
@@ -40,6 +40,7 @@ Compatible with **30+ AI coding agents** via the [agentskills.io](https://agents
|
|
|
40
40
|
| [kling-video](skills/kling-video/) | Generate videos with Kuaishou Kling (motion control) |
|
|
41
41
|
| [hailuo-video](skills/hailuo-video/) | Generate videos with Hailuo / MiniMax |
|
|
42
42
|
| [seedance-video](skills/seedance-video/) | Generate dance/motion videos with ByteDance Seedance |
|
|
43
|
+
| [maestro-video](skills/maestro-video/) | Produce complete videos from a brief with script, media, voiceover, captions, and editing |
|
|
43
44
|
|
|
44
45
|
### AI Chat & Tools
|
|
45
46
|
|
|
@@ -208,6 +209,7 @@ Skills provide **knowledge** (when to use, parameters, gotchas). MCP servers pro
|
|
|
208
209
|
| veo-video | [mcp-veo](https://pypi.org/project/mcp-veo/) | `pip install mcp-veo` | `https://veo.mcp.acedata.cloud/mcp` |
|
|
209
210
|
| seedream-image | [mcp-seedream](https://pypi.org/project/mcp-seedream/) | `pip install mcp-seedream` | `https://seedream.mcp.acedata.cloud/mcp` |
|
|
210
211
|
| seedance-video | [mcp-seedance](https://pypi.org/project/mcp-seedance/) | `pip install mcp-seedance` | `https://seedance.mcp.acedata.cloud/mcp` |
|
|
212
|
+
| maestro-video | [mcp-maestro](https://pypi.org/project/mcp-maestro/) | `pip install mcp-maestro` | `https://maestro.mcp.acedata.cloud/mcp` |
|
|
211
213
|
| nano-banana-image | [mcp-nano-banana](https://pypi.org/project/mcp-nano-banana/) | `pip install mcp-nano-banana` | `https://nano-banana.mcp.acedata.cloud/mcp` |
|
|
212
214
|
| short-url | [mcp-shorturl](https://pypi.org/project/mcp-shorturl/) | `pip install mcp-shorturl` | `https://short-url.mcp.acedata.cloud/mcp` |
|
|
213
215
|
| wan-video | [mcp-wan](https://pypi.org/project/mcp-wan/) | `pip install mcp-wan` | `https://wan.mcp.acedata.cloud/mcp` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.713.0",
|
|
4
4
|
"description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -15,6 +15,7 @@ Each AceDataCloud service has a corresponding MCP server that provides tool-use
|
|
|
15
15
|
| veo-video | `pip install mcp-veo` | `https://veo.mcp.acedata.cloud/mcp` |
|
|
16
16
|
| seedream-image | `pip install mcp-seedream` | `https://seedream.mcp.acedata.cloud/mcp` |
|
|
17
17
|
| seedance-video | `pip install mcp-seedance` | `https://seedance.mcp.acedata.cloud/mcp` |
|
|
18
|
+
| maestro-video | `pip install mcp-maestro` | `https://maestro.mcp.acedata.cloud/mcp` |
|
|
18
19
|
| nano-banana-image | `pip install mcp-nano-banana` | `https://nano-banana.mcp.acedata.cloud/mcp` |
|
|
19
20
|
| short-url | `pip install mcp-shorturl` | `https://short-url.mcp.acedata.cloud/mcp` |
|
|
20
21
|
| wan-video | `pip install mcp-wan` | `https://wan.mcp.acedata.cloud/mcp` |
|
package/skills/hashnode/SKILL.md
CHANGED
|
@@ -1,171 +1,119 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hashnode
|
|
3
|
-
description: Publish,
|
|
3
|
+
description: Publish, draft and read blog posts on the user's Hashnode blog using their own login cookies (BYOC) — no Hashnode Pro plan required. Use when the user mentions Hashnode, publishing/cross-posting an article to their Hashnode blog, saving a Hashnode draft, or listing their Hashnode publications.
|
|
4
4
|
when_to_use: |
|
|
5
|
-
Trigger when the user wants to publish a Markdown article to their
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
account access — confirm before publishing publicly (you can save a
|
|
10
|
-
draft first with the createDraft mutation).
|
|
5
|
+
Trigger when the user wants to publish a Markdown article to their Hashnode
|
|
6
|
+
blog, save it as a private draft, or list their Hashnode publications. This
|
|
7
|
+
drives the user's REAL blog, so publishing is gated behind an explicit
|
|
8
|
+
confirmation (save a draft first if unsure).
|
|
11
9
|
connections: [hashnode]
|
|
12
|
-
allowed_tools: [Bash]
|
|
10
|
+
allowed_tools: [Bash, publish_artifact]
|
|
13
11
|
license: Apache-2.0
|
|
14
12
|
metadata:
|
|
15
13
|
author: acedatacloud
|
|
16
|
-
version: "
|
|
14
|
+
version: "2.0"
|
|
17
15
|
---
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
Token is in `$HASHNODE_TOKEN`. Single endpoint: `https://gql-beta.hashnode.com`
|
|
21
|
-
(always `POST` a JSON body `{query, variables}`).
|
|
17
|
+
# hashnode — publish to your Hashnode blog via your own cookies (no Pro)
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
Hashnode moved its **public GraphQL API behind a paid Pro plan** (2026-05-13), so
|
|
20
|
+
a Personal Access Token can no longer publish for free. This skill instead drives
|
|
21
|
+
the **same first-party REST API the Hashnode web editor uses**
|
|
22
|
+
(`https://hashnode.com/api/...`), authenticated by the user's login **session
|
|
23
|
+
cookie** — which is free and needs no Pro plan.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
The connector injects the cookie jar as `HASHNODE_COOKIES` (a JSON array; the
|
|
26
|
+
session cookie is `hashnode-session`). **Secret — full account access. Never echo
|
|
27
|
+
or print it.**
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
GraphQL always returns HTTP `200`; real failures live in the JSON `errors`
|
|
34
|
-
array — always inspect it and show it verbatim. Two error cases matter:
|
|
35
|
-
|
|
36
|
-
- `Unauthorized` / `not authenticated` → the token is invalid → the user must
|
|
37
|
-
re-connect the Hashnode connector.
|
|
38
|
-
- `extensions.code == "FORBIDDEN"` with a message like *"Publication does not have
|
|
39
|
-
an active Pro plan"* → this is **not** a token problem. As of 2026-05-13 Hashnode
|
|
40
|
-
moved its API behind a paid plan: **every write mutation** (`publishPost`,
|
|
41
|
-
`createDraft`, `updatePost`) **and publication-scoped reads require the target
|
|
42
|
-
publication to be on [Hashnode Pro](https://hashnode.com/pro)**. Do NOT tell the
|
|
43
|
-
user to reconnect and do NOT retry in a loop — tell them the publication owner
|
|
44
|
-
must upgrade to Hashnode Pro (blog dashboard → Billing → Upgrade to Pro), after
|
|
45
|
-
which the API works immediately. Public reads (posts/feeds/users/tags) stay free.
|
|
46
|
-
|
|
47
|
-
Helper — send a query/mutation (`$1` = query string, `$2` = variables JSON):
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
gql() {
|
|
51
|
-
jq -n --arg q "$1" --argjson v "${2:-null}" '{query:$q, variables:$v}' \
|
|
52
|
-
| curl -sS -X POST https://gql-beta.hashnode.com \
|
|
53
|
-
-H "Authorization: Bearer $HASHNODE_TOKEN" \
|
|
54
|
-
-H "Content-Type: application/json" \
|
|
55
|
-
-d @-
|
|
56
|
-
}
|
|
57
|
-
```
|
|
29
|
+
> E2E-verified 2026-07-12 on a real connected blog: list publications, create +
|
|
30
|
+
> save draft (title / subtitle / cover / tags / Markdown), publish, and delete —
|
|
31
|
+
> all via cookie auth, no Pro.
|
|
58
32
|
|
|
59
|
-
##
|
|
33
|
+
## Setup — anchor on our own script
|
|
60
34
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
| jq '{me: .data.me.username, publications: [.data.me.publications.edges[].node | {id, title, url}], errors: .errors}'
|
|
35
|
+
```sh
|
|
36
|
+
# $SKILL_DIR can point at another skill loaded this turn — anchor on our script.
|
|
37
|
+
H="$SKILL_DIR/scripts/hashnode.py"; [ -f "$H" ] || H=$(find /tmp -maxdepth 8 -path '*/skills/*/hashnode/scripts/hashnode.py' 2>/dev/null | head -1)
|
|
38
|
+
[ -f "$H" ] || { echo "hashnode script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
|
|
39
|
+
python3 "$H" publications # list the user's blogs (verifies the cookie)
|
|
67
40
|
```
|
|
68
41
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
## Publish a post
|
|
42
|
+
On an auth error the cookie is expired — have the user reconnect at
|
|
43
|
+
<https://auth.acedata.cloud/user/connections>. Do **not** loop-retry.
|
|
73
44
|
|
|
74
|
-
|
|
75
|
-
a draft first (see below). `tags` is required — supply 1–5 tags, each as
|
|
76
|
-
`{name, slug}` (slug lowercase, no spaces).
|
|
45
|
+
## ⚠️ Non-Pro blogs are automoderated — write EDITORIAL, not ads
|
|
77
46
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
47
|
+
This is the single most important rule. A **non-Pro** publication is subject to
|
|
48
|
+
Hashnode **automoderation**: an overtly promotional post (hypey language,
|
|
49
|
+
"free credits!!", the *same* CTA link repeated 3–4 times) is **auto-removed
|
|
50
|
+
within seconds** — the publish call still returns success, but the live URL then
|
|
51
|
+
404s. (Pro's one benefit here is "protection from automoderation removal".)
|
|
82
52
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
publicationId: $p,
|
|
86
|
-
title: $t,
|
|
87
|
-
contentMarkdown: $b,
|
|
88
|
-
tags: [{name:"AI", slug:"ai"}, {name:"Web Development", slug:"web-development"}]
|
|
89
|
-
}
|
|
90
|
-
}')
|
|
53
|
+
To stay live, write a genuinely useful **how-to / guide** that happens to feature
|
|
54
|
+
the product:
|
|
91
55
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
56
|
+
- Educational framing and a concrete title (e.g. *"… (Beginner's Guide)"*).
|
|
57
|
+
- At most a couple of **tasteful** links, not the same CTA repeated.
|
|
58
|
+
- No "limited-time!! grab now!!" marketing voice.
|
|
96
59
|
|
|
97
|
-
|
|
60
|
+
`publish` re-fetches the live URL after publishing and returns a `warning` if the
|
|
61
|
+
post was moderated away — if you see that, rewrite it editorially and republish.
|
|
98
62
|
|
|
99
|
-
|
|
100
|
-
- `slug` — custom URL slug (otherwise derived from the title).
|
|
101
|
-
- `canonicalUrl` / `originalArticleURL` — when cross-posting, point SEO back to
|
|
102
|
-
the original source. Set this whenever the same article also lives elsewhere.
|
|
103
|
-
- `coverImageOptions: { coverImageURL: "https://..." }` — cover image.
|
|
104
|
-
- `publishedAt` — ISO-8601 timestamp to backdate; `disableComments` etc. live
|
|
105
|
-
under `settings`.
|
|
63
|
+
## Read
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
65
|
+
```sh
|
|
66
|
+
python3 "$H" publications # {publications:[{id,title,url}]}
|
|
67
|
+
```
|
|
110
68
|
|
|
111
|
-
|
|
112
|
-
VARS=$(jq -n --arg p "$PUB_ID" --arg t "$TITLE" --arg b "$BODY_MD" '{
|
|
113
|
-
input: { publicationId: $p, title: $t, contentMarkdown: $b }
|
|
114
|
-
}')
|
|
69
|
+
## Save a private draft (safe, non-public)
|
|
115
70
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
71
|
+
```sh
|
|
72
|
+
python3 "$H" draft \
|
|
73
|
+
--title "My Title" \
|
|
74
|
+
--subtitle "Optional subtitle" \
|
|
75
|
+
--content-file article.md \
|
|
76
|
+
--cover "https://cdn.example.com/cover.jpg" \
|
|
77
|
+
--tags ai,apis
|
|
78
|
+
# → {draft_id, editor_url}. Nothing is public yet.
|
|
119
79
|
```
|
|
120
80
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`
|
|
81
|
+
- `--content-file` is the Markdown body (use a file for long posts; `--content`
|
|
82
|
+
for a short inline string). Inline **images** and **links** are just Markdown.
|
|
83
|
+
- `--cover` sets the header/cover image (also used as the OG image).
|
|
84
|
+
- `--tags` are comma-separated slugs (`ai,apis,web-development`); they're resolved
|
|
85
|
+
to real Hashnode tag ids automatically (up to 5).
|
|
86
|
+
- `--publication <id>` is only needed if the account has more than one blog.
|
|
124
87
|
|
|
125
|
-
|
|
126
|
-
VARS=$(jq -n --arg id "POST_ID" --arg b "$(cat article.md)" '{
|
|
127
|
-
input: { id: $id, contentMarkdown: $b }
|
|
128
|
-
}')
|
|
88
|
+
## Publish — GATED (dry-run unless trailing `--confirm`)
|
|
129
89
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
90
|
+
```sh
|
|
91
|
+
python3 "$H" publish --title "My Title" --content-file article.md --cover "https://…" --tags ai,apis # dry-run
|
|
92
|
+
python3 "$H" publish --title "My Title" --content-file article.md --cover "https://…" --tags ai,apis --confirm # LIVE
|
|
93
|
+
# → {ok, url} (or {ok:false, warning:"…automoderation removed…"} — rewrite editorially)
|
|
133
94
|
```
|
|
134
95
|
|
|
135
|
-
|
|
96
|
+
A confirmed `publish` is **immediately public** on the user's real blog. Always
|
|
97
|
+
show the final title + body, get an explicit "yes", then run with `--confirm` as
|
|
98
|
+
the **last** argument.
|
|
136
99
|
|
|
137
|
-
|
|
138
|
-
gql 'query { me { posts(pageSize: 20, page: 1) { nodes { id title slug url views reactionCount responseCount publishedAt } } } }' \
|
|
139
|
-
| jq '[.data.me.posts.nodes[] | {id, title, url, views, reactions: .reactionCount, responses: .responseCount}]'
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Single post with full content + stats:
|
|
100
|
+
## Delete a draft or post — GATED
|
|
143
101
|
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
|
|
102
|
+
```sh
|
|
103
|
+
python3 "$H" delete --id <draftOrPostId> # dry-run
|
|
104
|
+
python3 "$H" delete --id <draftOrPostId> --confirm # delete
|
|
147
105
|
```
|
|
148
106
|
|
|
149
107
|
## Gotchas
|
|
150
108
|
|
|
151
|
-
- **
|
|
152
|
-
|
|
153
|
-
-
|
|
154
|
-
|
|
155
|
-
- **
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
blog, not something the skill or a reconnect can fix — surface it and stop.
|
|
160
|
-
- **`publicationId` is mandatory** for publishing/drafting — never guess it, read
|
|
161
|
-
it from the `me` query.
|
|
162
|
-
- **`tags` is required on `publishPost`** — supply at least one `{name, slug}`;
|
|
163
|
-
slug must be lowercase with hyphens (e.g. `machine-learning`).
|
|
164
|
-
- **`errors` on HTTP 200** — GraphQL reports failures in the `errors` array, not
|
|
165
|
-
via status codes; always surface them.
|
|
166
|
-
- **Idempotency** — re-running `publishPost` creates a *new* post each time; to
|
|
167
|
-
change an existing one use `updatePost` with its `id`.
|
|
168
|
-
|
|
109
|
+
- **No Pro, no PAT.** This uses the cookie-authed `hashnode.com/api/*` editor API,
|
|
110
|
+
not `gql-beta.hashnode.com` (which requires a paid Pro plan).
|
|
111
|
+
- **Automoderation** (above) is the main failure mode on free blogs — keep it
|
|
112
|
+
editorial.
|
|
113
|
+
- **This is the user's real blog.** Confirm before any publish/delete.
|
|
114
|
+
- **Never print `HASHNODE_COOKIES`** — it is full account access.
|
|
115
|
+
- **First-party internal API** — it can change when Hashnode updates the editor;
|
|
116
|
+
if a call starts returning HTML/404 unexpectedly, report it as upstream drift.
|
|
169
117
|
|
|
170
118
|
## Record the output
|
|
171
119
|
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
hashnode — read & publish on Hashnode with the user's own login cookies (BYOC).
|
|
4
|
+
Standard-library only (urllib), no third-party deps.
|
|
5
|
+
|
|
6
|
+
Hashnode moved its **public GraphQL API behind a paid Pro plan** (2026-05-13), so
|
|
7
|
+
this skill does NOT use ``gql-beta.hashnode.com`` / a Personal Access Token.
|
|
8
|
+
Instead it drives the **same first-party REST endpoints the web editor uses**
|
|
9
|
+
(``https://hashnode.com/api/...``), authenticated by the login session cookie —
|
|
10
|
+
which is free and needs no Pro plan.
|
|
11
|
+
|
|
12
|
+
The connector injects the cookie jar as a JSON env var ``HASHNODE_COOKIES`` (a
|
|
13
|
+
JSON list of cookie dicts; the session cookie is ``hashnode-session``). It is
|
|
14
|
+
full account access — NEVER echo or print it.
|
|
15
|
+
|
|
16
|
+
⚠️ Non-Pro publications are subject to Hashnode **automoderation**: overtly
|
|
17
|
+
promotional posts (hypey language, repeated identical CTA links, "free credits!!")
|
|
18
|
+
are auto-removed within seconds of publishing, so the post 404s even though the
|
|
19
|
+
publish call returned success. Write **editorial / how-to** content (a helpful
|
|
20
|
+
guide that happens to feature the product, with at most a couple of tasteful
|
|
21
|
+
links) and it stays live. ``publish`` re-fetches the live URL and warns if the
|
|
22
|
+
post was moderated away.
|
|
23
|
+
|
|
24
|
+
Read commands run directly. ``publish`` / ``delete`` are GATED by a trailing
|
|
25
|
+
``--confirm`` (honored only as the LAST arg). ``draft`` saves a private draft.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
python3 hashnode.py publications
|
|
29
|
+
python3 hashnode.py draft --title T --content-file a.md --cover https://x/y.jpg --tags ai,apis
|
|
30
|
+
python3 hashnode.py publish --title T --content-file a.md --cover https://x/y.jpg --tags ai,apis --confirm
|
|
31
|
+
python3 hashnode.py delete --id <draftOrPostId> --confirm
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import gzip
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
import time
|
|
42
|
+
import urllib.error
|
|
43
|
+
import urllib.parse
|
|
44
|
+
import urllib.request
|
|
45
|
+
|
|
46
|
+
UA = (
|
|
47
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
|
48
|
+
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
49
|
+
)
|
|
50
|
+
BASE = "https://hashnode.com"
|
|
51
|
+
|
|
52
|
+
_RAW = sys.argv[1:]
|
|
53
|
+
# --confirm is honored ONLY as the last token, so body text containing
|
|
54
|
+
# "--confirm" can never silently trigger a write.
|
|
55
|
+
CONFIRM = bool(_RAW) and _RAW[-1] == "--confirm"
|
|
56
|
+
ARGV = _RAW[:-1] if CONFIRM else list(_RAW)
|
|
57
|
+
|
|
58
|
+
# State-changing commands — dry-run unless the invocation ends with --confirm.
|
|
59
|
+
GATED = {"publish", "delete"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def out(obj) -> None:
|
|
63
|
+
print(json.dumps(obj, ensure_ascii=False, indent=2, default=str))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def die(msg: str, code: int = 1) -> None:
|
|
67
|
+
out({"error": msg})
|
|
68
|
+
sys.exit(code)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Cookie jar ──────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def load_cookies() -> list:
|
|
74
|
+
raw = os.environ.get("HASHNODE_COOKIES")
|
|
75
|
+
if not raw:
|
|
76
|
+
die("HASHNODE_COOKIES is not set — connect Hashnode at "
|
|
77
|
+
"https://auth.acedata.cloud/user/connections, then retry.")
|
|
78
|
+
try:
|
|
79
|
+
jar = json.loads(raw)
|
|
80
|
+
except json.JSONDecodeError as e:
|
|
81
|
+
die(f"HASHNODE_COOKIES is not valid JSON: {e}")
|
|
82
|
+
if not isinstance(jar, list):
|
|
83
|
+
die(f"HASHNODE_COOKIES must be a JSON list of cookies, got {type(jar).__name__}")
|
|
84
|
+
if not any(c.get("name") == "hashnode-session" for c in jar):
|
|
85
|
+
die("HASHNODE_COOKIES is missing the 'hashnode-session' cookie — re-capture "
|
|
86
|
+
"on hashnode.com with the ACE extension, then reconnect at "
|
|
87
|
+
"https://auth.acedata.cloud/user/connections.")
|
|
88
|
+
return jar
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _domain_matches(host: str, domain: str) -> bool:
|
|
92
|
+
d = domain.lstrip(".").lower()
|
|
93
|
+
h = host.lower()
|
|
94
|
+
return not d or h == d or h.endswith("." + d)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cookie_header(jar: list, url: str) -> str:
|
|
98
|
+
host = urllib.parse.urlsplit(url).hostname or ""
|
|
99
|
+
host_in_scope = any(
|
|
100
|
+
c.get("domain") and _domain_matches(host, str(c["domain"])) for c in jar
|
|
101
|
+
)
|
|
102
|
+
parts = []
|
|
103
|
+
for c in jar:
|
|
104
|
+
name, value = c.get("name"), c.get("value")
|
|
105
|
+
if not name or value is None:
|
|
106
|
+
continue
|
|
107
|
+
domain = c.get("domain")
|
|
108
|
+
if domain:
|
|
109
|
+
if not _domain_matches(host, str(domain)):
|
|
110
|
+
continue
|
|
111
|
+
elif not host_in_scope:
|
|
112
|
+
# A domain-less cookie only rides along when at least one scoped
|
|
113
|
+
# cookie already matched this host (never leak it to a stray host).
|
|
114
|
+
continue
|
|
115
|
+
parts.append(f"{name}={value}")
|
|
116
|
+
return "; ".join(parts)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── HTTP ────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
def request(method, path, jar, *, body=None):
|
|
122
|
+
url = BASE + path
|
|
123
|
+
hdrs = {
|
|
124
|
+
"User-Agent": UA,
|
|
125
|
+
"Accept": "application/json",
|
|
126
|
+
"Origin": BASE,
|
|
127
|
+
"Referer": BASE + "/",
|
|
128
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
129
|
+
}
|
|
130
|
+
data = None
|
|
131
|
+
if body is not None:
|
|
132
|
+
data = json.dumps(body).encode("utf-8")
|
|
133
|
+
hdrs["Content-Type"] = "application/json"
|
|
134
|
+
req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
|
|
135
|
+
# Unredirected → the cookie is not re-sent if the API 30x-redirects to a
|
|
136
|
+
# different host, so the jar never leaks off-site.
|
|
137
|
+
req.add_unredirected_header("Cookie", cookie_header(jar, url))
|
|
138
|
+
try:
|
|
139
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
140
|
+
raw = resp.read()
|
|
141
|
+
if resp.headers.get("Content-Encoding") == "gzip":
|
|
142
|
+
raw = gzip.decompress(raw)
|
|
143
|
+
return resp.status, raw.decode("utf-8", "replace")
|
|
144
|
+
except urllib.error.HTTPError as e:
|
|
145
|
+
raw = e.read()
|
|
146
|
+
try:
|
|
147
|
+
if e.headers.get("Content-Encoding") == "gzip":
|
|
148
|
+
raw = gzip.decompress(raw)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
return e.code, raw.decode("utf-8", "replace")
|
|
152
|
+
except urllib.error.URLError as e:
|
|
153
|
+
die(f"network error reaching {url}: {e.reason}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def api(method, path, jar, *, body=None, _retried=False):
|
|
157
|
+
status, text = request(method, path, jar, body=body)
|
|
158
|
+
# Hashnode sits behind Cloudflare, which intermittently 403/429s an
|
|
159
|
+
# otherwise valid session; retry idempotent GETs once. Never replay a
|
|
160
|
+
# write (POST/PUT/DELETE) — it could duplicate a publish.
|
|
161
|
+
if status in (403, 429) and method == "GET" and not _retried:
|
|
162
|
+
time.sleep(1.5)
|
|
163
|
+
return api(method, path, jar, body=body, _retried=True)
|
|
164
|
+
if status in (401, 403):
|
|
165
|
+
die(f"auth failed ({status}) on {path} — cookie likely expired. "
|
|
166
|
+
f"Reconnect at https://auth.acedata.cloud/user/connections.")
|
|
167
|
+
# A hashnode.com/api path that 404s with an HTML body means the route/id is
|
|
168
|
+
# wrong; a JSON {success:false} is a real API error.
|
|
169
|
+
stripped = text.lstrip()
|
|
170
|
+
if stripped.startswith("<"):
|
|
171
|
+
die(f"unexpected non-JSON ({status}) from {path} — the route or id is "
|
|
172
|
+
f"likely wrong.")
|
|
173
|
+
try:
|
|
174
|
+
d = json.loads(text)
|
|
175
|
+
except json.JSONDecodeError:
|
|
176
|
+
die(f"non-JSON response ({status}) from {path}: {text[:300]}")
|
|
177
|
+
if isinstance(d, dict) and d.get("success") is False:
|
|
178
|
+
die(f"Hashnode API error ({status}) on {path}: {d.get('error')}")
|
|
179
|
+
return d
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ── helpers ─────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
def resolve_publication(jar, wanted: str | None) -> dict:
|
|
185
|
+
d = api("GET", "/api/publications", jar)
|
|
186
|
+
pubs = d.get("publications") or []
|
|
187
|
+
if not pubs:
|
|
188
|
+
die("no publications on this account — create a blog on hashnode.com first.")
|
|
189
|
+
if wanted:
|
|
190
|
+
for p in pubs:
|
|
191
|
+
if wanted in (p.get("_id"), p.get("title"), p.get("displayTitle")):
|
|
192
|
+
return p
|
|
193
|
+
die(f"publication {wanted!r} not found; have: "
|
|
194
|
+
+ ", ".join(f'{p.get("title")}({p.get("_id")})' for p in pubs))
|
|
195
|
+
if len(pubs) > 1:
|
|
196
|
+
die("multiple publications — pass --publication <id>. Have: "
|
|
197
|
+
+ ", ".join(f'{p.get("title")}({p.get("_id")})' for p in pubs))
|
|
198
|
+
return pubs[0]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def resolve_tags(jar, slugs_csv: str | None) -> list:
|
|
202
|
+
if not slugs_csv:
|
|
203
|
+
return []
|
|
204
|
+
tags = []
|
|
205
|
+
for slug in [s.strip() for s in slugs_csv.split(",") if s.strip()]:
|
|
206
|
+
d = api("GET", f"/api/tags/search?q={urllib.parse.quote(slug)}", jar)
|
|
207
|
+
cands = d.get("tags") or []
|
|
208
|
+
pick = next((t for t in cands if t.get("slug") == slug), None) \
|
|
209
|
+
or (max(cands, key=lambda t: t.get("numPosts", 0)) if cands else None)
|
|
210
|
+
if pick:
|
|
211
|
+
tags.append({"_id": pick["_id"], "name": pick["name"], "slug": pick["slug"]})
|
|
212
|
+
return tags[:5]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def read_content(args) -> str:
|
|
216
|
+
if args.content_file:
|
|
217
|
+
if not os.path.isfile(args.content_file):
|
|
218
|
+
die(f"content file not found: {args.content_file}")
|
|
219
|
+
with open(args.content_file, encoding="utf-8") as fh:
|
|
220
|
+
return fh.read()
|
|
221
|
+
if args.content:
|
|
222
|
+
return args.content
|
|
223
|
+
die("provide --content-file or --content")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def build_save(args, pub_id, jar) -> dict:
|
|
227
|
+
body = {
|
|
228
|
+
"title": (args.title or "").strip(),
|
|
229
|
+
"contentMarkdown": read_content(args),
|
|
230
|
+
"publicationId": pub_id,
|
|
231
|
+
}
|
|
232
|
+
if not body["title"]:
|
|
233
|
+
die("provide --title")
|
|
234
|
+
if args.subtitle:
|
|
235
|
+
body["subtitle"] = args.subtitle
|
|
236
|
+
if args.cover:
|
|
237
|
+
body["coverImage"] = args.cover
|
|
238
|
+
body["ogImage"] = args.cover
|
|
239
|
+
if args.slug:
|
|
240
|
+
body["slug"] = args.slug
|
|
241
|
+
tags = resolve_tags(jar, args.tags)
|
|
242
|
+
if tags:
|
|
243
|
+
body["tags"] = tags
|
|
244
|
+
return body
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def create_and_save(args, jar) -> tuple[str, dict]:
|
|
248
|
+
pub = resolve_publication(jar, args.publication)
|
|
249
|
+
d = api("POST", "/api/drafts", jar, body={"publicationId": pub["_id"]})
|
|
250
|
+
did = d.get("draftId")
|
|
251
|
+
if not did:
|
|
252
|
+
die(f"draft creation returned no draftId: {d}")
|
|
253
|
+
save = build_save(args, pub["_id"], jar)
|
|
254
|
+
api("PUT", f"/api/drafts/{did}", jar, body=save)
|
|
255
|
+
return did, pub
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── commands ────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
def cmd_publications(jar, _args):
|
|
261
|
+
d = api("GET", "/api/publications", jar)
|
|
262
|
+
out({"publications": [
|
|
263
|
+
{"id": p.get("_id"), "title": p.get("title"),
|
|
264
|
+
"url": p.get("url") or p.get("domain")} for p in d.get("publications") or []]})
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def cmd_draft(jar, args):
|
|
268
|
+
did, pub = create_and_save(args, jar)
|
|
269
|
+
out({"ok": True, "draft_id": did, "publication": pub.get("title"),
|
|
270
|
+
"editor_url": f"https://hashnode.com/draft/{did}",
|
|
271
|
+
"note": "Private draft saved. Review it, then run `publish ... --confirm` to go live."})
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def cmd_publish(jar, args):
|
|
275
|
+
did, pub = create_and_save(args, jar)
|
|
276
|
+
d = api("POST", f"/api/drafts/{did}/publish", jar, body={})
|
|
277
|
+
post = d.get("post") or {}
|
|
278
|
+
url = post.get("url")
|
|
279
|
+
result = {"ok": True, "posted": True, "id": post.get("id"),
|
|
280
|
+
"title": post.get("title"), "url": url, "publication": pub.get("title")}
|
|
281
|
+
# Verify the post is actually live — non-Pro blogs auto-moderate promo posts.
|
|
282
|
+
if url:
|
|
283
|
+
time.sleep(4)
|
|
284
|
+
try:
|
|
285
|
+
code = _url_status(url)
|
|
286
|
+
if code == 404:
|
|
287
|
+
result["ok"] = False
|
|
288
|
+
result["warning"] = (
|
|
289
|
+
"The publish call succeeded but the live URL returns 404 — "
|
|
290
|
+
"Hashnode automoderation removed the post (common for overtly "
|
|
291
|
+
"promotional content on a non-Pro blog). Rewrite it in an "
|
|
292
|
+
"editorial / how-to style with fewer repeated CTA links and "
|
|
293
|
+
"republish.")
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
out(result)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _url_status(url: str) -> int:
|
|
300
|
+
req = urllib.request.Request(url, headers={"User-Agent": UA}, method="GET")
|
|
301
|
+
try:
|
|
302
|
+
with urllib.request.urlopen(req, timeout=20) as r:
|
|
303
|
+
return r.status
|
|
304
|
+
except urllib.error.HTTPError as e:
|
|
305
|
+
return e.code
|
|
306
|
+
except Exception:
|
|
307
|
+
return 0
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def cmd_delete(jar, args):
|
|
311
|
+
d = api("DELETE", f"/api/drafts/{args.id}", jar)
|
|
312
|
+
out({"ok": True, "deleted": True, "id": d.get("deletedDraftId") or args.id})
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
COMMANDS = {
|
|
316
|
+
"publications": cmd_publications,
|
|
317
|
+
"draft": cmd_draft,
|
|
318
|
+
"publish": cmd_publish,
|
|
319
|
+
"delete": cmd_delete,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def gated_dry_run(args) -> None:
|
|
324
|
+
out({"dry_run": True, "command": args.command,
|
|
325
|
+
"title": getattr(args, "title", None), "id": getattr(args, "id", None),
|
|
326
|
+
"note": "Re-run with --confirm as the LAST argument to actually run this "
|
|
327
|
+
"on the user's REAL Hashnode blog."})
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
331
|
+
p = argparse.ArgumentParser(description="Hashnode via login cookies (no Pro).")
|
|
332
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
333
|
+
|
|
334
|
+
sub.add_parser("publications", help="list my blogs (publications)")
|
|
335
|
+
|
|
336
|
+
def add_write_args(sp):
|
|
337
|
+
sp.add_argument("--title", required=True)
|
|
338
|
+
sp.add_argument("--subtitle")
|
|
339
|
+
sp.add_argument("--content-file", dest="content_file", help="path to a Markdown file")
|
|
340
|
+
sp.add_argument("--content", help="inline Markdown (use --content-file for长文)")
|
|
341
|
+
sp.add_argument("--cover", help="cover image URL (also used as OG image)")
|
|
342
|
+
sp.add_argument("--tags", help="comma-separated tag slugs, e.g. ai,apis")
|
|
343
|
+
sp.add_argument("--slug", help="custom URL slug (optional)")
|
|
344
|
+
sp.add_argument("--publication", help="publication id (needed only if >1 blog)")
|
|
345
|
+
|
|
346
|
+
add_write_args(sub.add_parser("draft", help="save a PRIVATE draft (safe, non-public)"))
|
|
347
|
+
add_write_args(sub.add_parser("publish", help="publish a post (GATED by trailing --confirm)"))
|
|
348
|
+
|
|
349
|
+
sp = sub.add_parser("delete", help="delete a draft or post (GATED by trailing --confirm)")
|
|
350
|
+
sp.add_argument("--id", required=True)
|
|
351
|
+
return p
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def main() -> None:
|
|
355
|
+
args = build_parser().parse_args(ARGV)
|
|
356
|
+
if args.command in GATED and not CONFIRM:
|
|
357
|
+
gated_dry_run(args)
|
|
358
|
+
return
|
|
359
|
+
jar = load_cookies()
|
|
360
|
+
COMMANDS[args.command](jar, args)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
if __name__ == "__main__":
|
|
364
|
+
main()
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: maestro-video
|
|
3
|
+
description: "Produce complete AI videos with Maestro via AceDataCloud API. Use when: Maestro, article-to-video, prompt-to-video, turn a brief or reference media into a finished captioned video, generate scripts/visuals/voiceover/music/editing in one workflow, create multilingual video variants, or remix/edit/extend a previous Maestro video. Covers task creation, progress polling, history, and final output retrieval."
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: acedatacloud
|
|
7
|
+
version: "1.0"
|
|
8
|
+
compatibility: Requires ACEDATACLOUD_API_TOKEN in .env file (see _shared/authentication.md). Optionally pair with mcp-maestro for tool-use.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Maestro End-to-End Video Production
|
|
12
|
+
|
|
13
|
+
Use Maestro when the user wants a **finished video**, not only a generated clip. A headless AI director turns one natural-language brief into a script, visual assets, voiceover, music, edit, captions, quality checks, and rendered video variants.
|
|
14
|
+
|
|
15
|
+
> **Setup:** See [authentication](../_shared/authentication.md) for token setup.
|
|
16
|
+
|
|
17
|
+
## Choose Maestro When
|
|
18
|
+
|
|
19
|
+
- The user wants an article, idea, product brief, or campaign turned into a complete video.
|
|
20
|
+
- The workflow needs scripting, visuals, narration, captions, and editing handled together.
|
|
21
|
+
- The user supplies product images, a logo, portrait, source footage, or reference audio.
|
|
22
|
+
- One visual production must be rendered in multiple languages.
|
|
23
|
+
- A completed Maestro video needs to be remixed, edited, or extended.
|
|
24
|
+
|
|
25
|
+
Use a model-specific video API such as Seedance, Kling, or Veo when the user only needs a short generated shot and wants direct model controls. Maestro may use multiple media services internally and is optimized for the finished production.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
curl -X POST https://api.acedata.cloud/maestro/videos \
|
|
31
|
+
-H "Authorization: Bearer $ACEDATACLOUD_API_TOKEN" \
|
|
32
|
+
-H "Content-Type: application/json" \
|
|
33
|
+
-d '{
|
|
34
|
+
"prompt": "Create a 30-second beginner-friendly video explaining vector databases. End with one memorable takeaway.",
|
|
35
|
+
"aspect": "16:9",
|
|
36
|
+
"duration": 30,
|
|
37
|
+
"quality": "standard",
|
|
38
|
+
"scenario": "narrated",
|
|
39
|
+
"langs": ["en"]
|
|
40
|
+
}'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The response contains a `task_id`. Maestro is asynchronous, so query the task until it reaches `succeeded` or `failed`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
curl -X POST https://api.acedata.cloud/maestro/tasks \
|
|
47
|
+
-H "Authorization: Bearer $ACEDATACLOUD_API_TOKEN" \
|
|
48
|
+
-H "Content-Type: application/json" \
|
|
49
|
+
-d '{"id": "<task_id>", "action": "retrieve"}'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Do not invent output URLs or report completion while the task is still running.
|
|
53
|
+
|
|
54
|
+
## Core Workflow
|
|
55
|
+
|
|
56
|
+
1. Translate the user's goal into a concrete production brief.
|
|
57
|
+
2. Call `POST /maestro/videos` and preserve the returned `task_id` and `trace_id`.
|
|
58
|
+
3. Poll `POST /maestro/tasks` at a reasonable interval.
|
|
59
|
+
4. Continue through nonterminal states; stop only at `succeeded` or `failed`.
|
|
60
|
+
5. On success, return every item in `response.data.variants`, including its language and `output_url`.
|
|
61
|
+
6. If requested, iterate by creating a new task with `action`, `ref_task_id`, and a change-focused prompt.
|
|
62
|
+
|
|
63
|
+
Polling and history queries are free; the video task is settled after production based on delivered output. Avoid submitting duplicate creation requests while an existing task is still running.
|
|
64
|
+
|
|
65
|
+
## Creation Parameters
|
|
66
|
+
|
|
67
|
+
| Parameter | Type | Default | Description |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| `prompt` | string | required | Natural-language production brief: topic, audience, content, tone, and desired result |
|
|
70
|
+
| `action` | string | `generate` | `generate`, `remix`, `edit`, or `extend` |
|
|
71
|
+
| `ref_task_id` | string | - | Required for `remix`, `edit`, and `extend` |
|
|
72
|
+
| `file_urls` | string[] | - | Public image, video, or audio references, up to the limits enforced by the API |
|
|
73
|
+
| `langs` | string[] | `["zh-cn"]` | Output language codes; each creates a localized rendered variant |
|
|
74
|
+
| `aspect` | string | `9:16` | `9:16`, `16:9`, or `1:1` |
|
|
75
|
+
| `duration` | integer | `30` | Target duration from 1 to 600 seconds |
|
|
76
|
+
| `quality` | string | `standard` | `draft`, `standard`, or `premium` |
|
|
77
|
+
| `scenario` | string | `auto` | `auto`, `narrated`, `drama`, `avatar`, `motion`, or `slideshow` |
|
|
78
|
+
| `style` | string | `auto` | Preset or freeform visual direction |
|
|
79
|
+
| `voice` | string | `auto` | Cross-lingual voice preset or a 32-hex-character Fish reference ID |
|
|
80
|
+
| `callback_url` | string | - | Optional webhook called on success or failure |
|
|
81
|
+
|
|
82
|
+
### Quality
|
|
83
|
+
|
|
84
|
+
- `draft`: fast rough cut for validating direction.
|
|
85
|
+
- `standard`: balanced default for normal production.
|
|
86
|
+
- `premium`: richer, more polished production with a higher cost and longer turnaround.
|
|
87
|
+
|
|
88
|
+
### Scenarios
|
|
89
|
+
|
|
90
|
+
- `auto`: let the director choose from the brief.
|
|
91
|
+
- `narrated`: multi-scene explainer, documentary, brand, history, or product video with voiceover.
|
|
92
|
+
- `drama`: acted short drama with characters and dialogue.
|
|
93
|
+
- `avatar`: talking-head or digital-human video; normally provide a portrait in `file_urls`.
|
|
94
|
+
- `motion`: kinetic typography, data, logo, or abstract motion graphics.
|
|
95
|
+
- `slideshow`: presentation deck, pitch, or slide-led video.
|
|
96
|
+
|
|
97
|
+
### Styles
|
|
98
|
+
|
|
99
|
+
Named presets include `cinematic`, `glass`, `luxury`, `swiss`, `modern`, `editorial`, `warm`, `vibrant`, `neon`, `mono`, `pastel`, `bold`, `industrial`, `futuristic`, and `retro`. The API also accepts a freeform style hint.
|
|
100
|
+
|
|
101
|
+
### Voices
|
|
102
|
+
|
|
103
|
+
Available presets include:
|
|
104
|
+
|
|
105
|
+
- Female: `warm-female`, `bright-female`, `anchor-female`, `clean-female`
|
|
106
|
+
- Male: `calm-male`, `deep-male`, `documentary-male`, `energetic-male`, `storyteller-male`
|
|
107
|
+
- Automatic: `auto`
|
|
108
|
+
|
|
109
|
+
Voice controls timbre rather than language. The same preset speaks the language selected in `langs`.
|
|
110
|
+
|
|
111
|
+
## Reference Media
|
|
112
|
+
|
|
113
|
+
Pass public URLs in `file_urls`:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"prompt": "Create a product launch video that clearly shows the camera body and logo. Use the supplied audio as the tone reference.",
|
|
118
|
+
"file_urls": [
|
|
119
|
+
"https://example.com/product.jpg",
|
|
120
|
+
"https://example.com/logo.png",
|
|
121
|
+
"https://example.com/reference.mp3"
|
|
122
|
+
],
|
|
123
|
+
"scenario": "narrated",
|
|
124
|
+
"style": "editorial",
|
|
125
|
+
"aspect": "16:9"
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Reference URLs must be reachable by the service. Do not pass local file paths. Upload local assets first, then use their public URLs.
|
|
130
|
+
|
|
131
|
+
## Multilingual Variants
|
|
132
|
+
|
|
133
|
+
Use one request to reuse the production across languages:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"prompt": "A concise product walkthrough for first-time customers",
|
|
138
|
+
"langs": ["en", "de", "ja"],
|
|
139
|
+
"voice": "warm-female",
|
|
140
|
+
"duration": 45
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The first language is primary. A successful task normally returns one item per delivered language in `response.data.variants`. Do not assume every requested language succeeded; inspect the actual variants.
|
|
145
|
+
|
|
146
|
+
## Iterate on a Previous Video
|
|
147
|
+
|
|
148
|
+
All iteration actions require `ref_task_id`.
|
|
149
|
+
|
|
150
|
+
### Remix
|
|
151
|
+
|
|
152
|
+
Use `remix` for a new creative interpretation that keeps the previous task as context:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"action": "remix",
|
|
157
|
+
"ref_task_id": "previous-task-id",
|
|
158
|
+
"prompt": "Rework this as a faster social cut with a stronger opening hook and neon styling.",
|
|
159
|
+
"aspect": "9:16"
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Edit
|
|
164
|
+
|
|
165
|
+
Use `edit` for targeted revisions:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"action": "edit",
|
|
170
|
+
"ref_task_id": "previous-task-id",
|
|
171
|
+
"prompt": "Keep the structure and visuals. Replace the final call to action and use a calmer narrator."
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Extend
|
|
176
|
+
|
|
177
|
+
Use `extend` to continue or lengthen the production:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"action": "extend",
|
|
182
|
+
"ref_task_id": "previous-task-id",
|
|
183
|
+
"prompt": "Add a 15-second customer example before the conclusion.",
|
|
184
|
+
"duration": 60
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Iteration creates a new task. Continue polling the new `task_id`; do not overwrite or confuse it with the source task.
|
|
189
|
+
|
|
190
|
+
## Task Response
|
|
191
|
+
|
|
192
|
+
A task response exposes top-level progress for user feedback:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"id": "task-id",
|
|
197
|
+
"status": "producing",
|
|
198
|
+
"progress": {
|
|
199
|
+
"percent": 52,
|
|
200
|
+
"stage": "visuals",
|
|
201
|
+
"message": "Generating scene assets",
|
|
202
|
+
"activity": "Creating scene 4"
|
|
203
|
+
},
|
|
204
|
+
"response": null
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
On success, inspect the delivered variants:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"status": "succeeded",
|
|
213
|
+
"response": {
|
|
214
|
+
"success": true,
|
|
215
|
+
"data": {
|
|
216
|
+
"variants": [
|
|
217
|
+
{
|
|
218
|
+
"lang": "en",
|
|
219
|
+
"output_url": "https://cdn.example/video.mp4",
|
|
220
|
+
"captions_url": "https://cdn.example/captions.vtt",
|
|
221
|
+
"cover_url": "https://cdn.example/cover.jpg",
|
|
222
|
+
"duration": 31.2,
|
|
223
|
+
"qc_score": 0.96
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Treat the fields above as a shape guide. Return fields that are actually present; never fabricate missing captions, covers, durations, or QC scores.
|
|
232
|
+
|
|
233
|
+
## List Recent Tasks
|
|
234
|
+
|
|
235
|
+
The live task endpoint also supports account history:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
curl -X POST https://api.acedata.cloud/maestro/tasks \
|
|
239
|
+
-H "Authorization: Bearer $ACEDATACLOUD_API_TOKEN" \
|
|
240
|
+
-H "Content-Type: application/json" \
|
|
241
|
+
-d '{"action": "retrieve_batch", "limit": 20}'
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Optional Unix timestamp filters are `created_at_min` and `created_at_max`. The authenticated identity determines which tasks are returned; do not supply or trust a body-level user ID.
|
|
245
|
+
|
|
246
|
+
## Prompting Guidance
|
|
247
|
+
|
|
248
|
+
A useful brief answers:
|
|
249
|
+
|
|
250
|
+
- What is the subject and desired outcome?
|
|
251
|
+
- Who is the audience?
|
|
252
|
+
- What facts, products, people, or scenes must appear?
|
|
253
|
+
- What should the viewer feel or do afterward?
|
|
254
|
+
- Which platform determines aspect ratio and pacing?
|
|
255
|
+
- Are there exact brand, compliance, or wording constraints?
|
|
256
|
+
|
|
257
|
+
Prefer a concrete production brief over a list of low-level model instructions. Maestro is responsible for choosing and orchestrating media tools.
|
|
258
|
+
|
|
259
|
+
## Gotchas
|
|
260
|
+
|
|
261
|
+
- This API is asynchronous. Always preserve and poll the returned `task_id`.
|
|
262
|
+
- Terminal states are `succeeded` and `failed`; other status names may evolve as the production pipeline changes.
|
|
263
|
+
- `remix`, `edit`, and `extend` fail without `ref_task_id`.
|
|
264
|
+
- `file_urls` must be public URLs, not local paths.
|
|
265
|
+
- `avatar` usually needs a usable portrait reference.
|
|
266
|
+
- Requested duration is a target; report the actual delivered duration from the result.
|
|
267
|
+
- Multilingual requests may return fewer variants than requested if one output fails.
|
|
268
|
+
- Do not resubmit the same brief merely because a long-running poll has not finished.
|
|
269
|
+
- Do not expose the API token in logs, output, source files, or examples.
|
|
270
|
+
|
|
271
|
+
> **MCP:** `pip install mcp-maestro` | Hosted: `https://maestro.mcp.acedata.cloud/mcp` | See [all MCP servers](../_shared/mcp-servers.md)
|