diff --git a/go/provider_maestro.go b/go/provider_maestro.go index a4fc0b5..828ed8b 100644 --- a/go/provider_maestro.go +++ b/go/provider_maestro.go @@ -5,7 +5,6 @@ package acedatacloud import "context" - // Maestro is the maestro provider client. type Maestro struct { t *transport @@ -15,26 +14,28 @@ type Maestro struct { type MaestroGenerateRequest struct { // Natural-language brief describing the video to produce (the topic, what to show, tone, audience). The agent de Prompt string - // Output languages, e.g. ["zh-cn", "en"]. The first is the primary language; each additional one reuses the visu + // Production action. Lite supports generate/edit; Standard adds remix; Pro adds extend. remix/edit/extend requir + Action string + // Required when `action` is remix / edit / extend: the task_id of the previous video to start from. + RefTaskID string + // Optional reference media (image / video / audio URLs) the agent can use — e.g. a product shot or logo to featu + FileURLs []string + // Output languages. Lite supports 1, Standard up to 2, and Pro up to 4. The first is primary; each additional de Langs []string + // Required output aspect ratio. Lite renders 720p/24fps; Standard and Pro render 1080p/30fps. + Aspect string + // Target video length in seconds. Lite: 5–30; Standard: 5–120; Pro: 5–300. Successful jobs are billed by actual + Duration int + // Production SKU. `lite` = fast 720p short video at 0.20 credits/s (up to 30s); `standard` = balanced 1080p at 0 + Quality string + // Production route. Lite supports auto/narrated/captions; Standard adds avatar; Pro adds drama. `captions` requi + Scenario string // Optional visual-style preset — expressed through typography, palette, motion, image treatment and pacing. Orth Style string // Optional narration voice — the **timbre** of the voiceover, independent of language. `auto` (default) lets the Voice string - // generate = a new video. remix / edit / extend = iterate on a previous video (require `ref_task_id`). - Action string - // Output aspect ratio (hint — the agent may follow the prompt). - Aspect string - // Production tier, a multiplier on the duration-based price. `draft` = a fast rough cut for previewing the idea - Quality string - // Target video length in seconds (1–600, i.e. up to 10 minutes). Billed by duration: credits ≈ 0.85 × duration × - Duration int - // How to route the video — a hint; the AI director still decides the final structure. `auto` (default) = the dir - Scenario string - // Optional reference media (image / video / audio URLs) the agent can use — e.g. a product shot or logo to featu - FileURLs []string - // Required when `action` is remix / edit / extend: the task_id of the previous video to start from. - RefTaskID string + // Async submits without blocking; poll the returned handle. Defaults true. + Async *bool // CallbackURL optionally receives the completion webhook. CallbackURL string // Extra fields merged into the request body. @@ -44,47 +45,51 @@ type MaestroGenerateRequest struct { func (r MaestroGenerateRequest) toBody() map[string]any { body := map[string]any{} body["prompt"] = r.Prompt - body["langs"] = r.Langs - if r.Style != "" { - body["style"] = r.Style - } else { - body["style"] = "auto" - } - if r.Voice != "" { - body["voice"] = r.Voice - } else { - body["voice"] = "auto" - } if r.Action != "" { body["action"] = r.Action } else { body["action"] = "generate" } + if r.RefTaskID != "" { + body["ref_task_id"] = r.RefTaskID + } + if r.FileURLs != nil { + body["file_urls"] = r.FileURLs + } + body["langs"] = r.Langs if r.Aspect != "" { body["aspect"] = r.Aspect } else { body["aspect"] = "9:16" } - if r.Quality != "" { - body["quality"] = r.Quality - } else { - body["quality"] = "standard" - } if r.Duration != 0 { body["duration"] = r.Duration } else { body["duration"] = 30 } + if r.Quality != "" { + body["quality"] = r.Quality + } else { + body["quality"] = "standard" + } if r.Scenario != "" { body["scenario"] = r.Scenario } else { body["scenario"] = "auto" } - if r.FileURLs != nil { - body["file_urls"] = r.FileURLs + if r.Style != "" { + body["style"] = r.Style + } else { + body["style"] = "auto" } - if r.RefTaskID != "" { - body["ref_task_id"] = r.RefTaskID + if r.Voice != "" { + body["voice"] = r.Voice + } else { + body["voice"] = "auto" + } + body["async"] = true + if r.Async != nil { + body["async"] = *r.Async } if r.CallbackURL != "" { body["callback_url"] = r.CallbackURL @@ -97,41 +102,15 @@ func (r MaestroGenerateRequest) toBody() map[string]any { return body } -// Generate Maestro Video Generation API -func (c *Maestro) Generate(ctx context.Context, req MaestroGenerateRequest) (map[string]any, error) { - return c.t.do(ctx, requestOpts{ +// Generate Call /maestro/videos. +func (c *Maestro) Generate(ctx context.Context, req MaestroGenerateRequest) (*TaskHandle, error) { + result, err := c.t.do(ctx, requestOpts{ Method: "POST", Path: "/maestro/videos", Body: req.toBody(), }) -} - -// MaestroEstimatesRequest is the input to maestro.Estimates. -type MaestroEstimatesRequest struct { - // CallbackURL optionally receives the completion webhook. - CallbackURL string - // Extra fields merged into the request body. - Extra map[string]any -} - -func (r MaestroEstimatesRequest) toBody() map[string]any { - body := map[string]any{} - if r.CallbackURL != "" { - body["callback_url"] = r.CallbackURL - } - for k, v := range r.Extra { - if _, exists := body[k]; !exists { - body[k] = v - } + if err != nil { + return nil, err } - return body -} - -// Estimates Call /maestro/estimates. -func (c *Maestro) Estimates(ctx context.Context, req MaestroEstimatesRequest) (map[string]any, error) { - return c.t.do(ctx, requestOpts{ - Method: "POST", - Path: "/maestro/estimates", - Body: req.toBody(), - }) + return newTaskHandle(taskIDFrom(result), "/maestro/tasks", c.t, result), nil } diff --git a/python/src/acedatacloud/resources/providers/maestro.py b/python/src/acedatacloud/resources/providers/maestro.py index e35c78f..30efe1e 100644 --- a/python/src/acedatacloud/resources/providers/maestro.py +++ b/python/src/acedatacloud/resources/providers/maestro.py @@ -9,6 +9,21 @@ from typing import Any, Literal # noqa: F401 +from ..._runtime.tasks import AsyncTaskHandle, TaskHandle + +MaestroAction = Literal[ + "generate", + "remix", + "edit", + "extend", +] +MaestroScenario = Literal[ + "auto", + "narrated", + "captions", + "avatar", + "drama", +] MaestroStyle = Literal[ "auto", "cinematic", @@ -39,20 +54,6 @@ "energetic-male", "storyteller-male", ] -MaestroAction = Literal[ - "generate", - "remix", - "edit", - "extend", -] -MaestroScenario = Literal[ - "auto", - "narrated", - "drama", - "avatar", - "motion", - "slideshow", -] def _task_id(result: Any) -> str: @@ -77,51 +78,47 @@ def generate( self, *, prompt: str, - langs: list[str] | None = None, - style: MaestroStyle | None = None, - voice: MaestroVoice | None = None, action: MaestroAction | None = None, + ref_task_id: str | None = None, + file_urls: list[str] | None = None, + langs: list[str] | None = None, aspect: Literal["9:16", "16:9", "1:1"] | None = None, - quality: Literal["draft", "standard", "premium"] | None = None, duration: int | None = None, + quality: Literal["lite", "standard", "pro"] | None = None, scenario: MaestroScenario | None = None, - file_urls: list[str] | None = None, - ref_task_id: str | None = None, + style: MaestroStyle | None = None, + voice: MaestroVoice | None = None, + async_: bool | None = None, + wait: bool = False, + poll_interval: float = 3.0, + max_wait: float = 600.0, callback_url: str | None = None, **extra: Any, - ) -> dict[str, Any]: - """Maestro Video Generation API""" + ) -> TaskHandle: + """Call /maestro/videos.""" body: dict[str, Any] = {} body["prompt"] = prompt - body["langs"] = langs if langs is not None else ["zh-cn"] - body["style"] = style if style is not None else "auto" - body["voice"] = voice if voice is not None else "auto" body["action"] = action if action is not None else "generate" + if ref_task_id is not None: + body["ref_task_id"] = ref_task_id + if file_urls is not None: + body["file_urls"] = file_urls + body["langs"] = langs if langs is not None else ["zh-cn"] body["aspect"] = aspect if aspect is not None else "9:16" - body["quality"] = quality if quality is not None else "standard" body["duration"] = duration if duration is not None else 30 + body["quality"] = quality if quality is not None else "standard" body["scenario"] = scenario if scenario is not None else "auto" - if file_urls is not None: - body["file_urls"] = file_urls - if ref_task_id is not None: - body["ref_task_id"] = ref_task_id - body.update(extra) - if callback_url is not None: - body["callback_url"] = callback_url - return self._transport.request("POST", "/maestro/videos", json=body) - - def estimates( - self, - *, - callback_url: str | None = None, - **extra: Any, - ) -> dict[str, Any]: - """Call /maestro/estimates.""" - body: dict[str, Any] = {} + body["style"] = style if style is not None else "auto" + body["voice"] = voice if voice is not None else "auto" body.update(extra) if callback_url is not None: body["callback_url"] = callback_url - return self._transport.request("POST", "/maestro/estimates", json=body) + body["async"] = True if async_ is None else async_ + result = self._transport.request("POST", "/maestro/videos", json=body) + handle = TaskHandle(_task_id(result), "/maestro/tasks", self._transport, submitted=result) + if wait: + handle.wait(poll_interval=poll_interval, max_wait=max_wait) + return handle class AsyncMaestro: @@ -134,48 +131,44 @@ async def generate( self, *, prompt: str, - langs: list[str] | None = None, - style: MaestroStyle | None = None, - voice: MaestroVoice | None = None, action: MaestroAction | None = None, + ref_task_id: str | None = None, + file_urls: list[str] | None = None, + langs: list[str] | None = None, aspect: Literal["9:16", "16:9", "1:1"] | None = None, - quality: Literal["draft", "standard", "premium"] | None = None, duration: int | None = None, + quality: Literal["lite", "standard", "pro"] | None = None, scenario: MaestroScenario | None = None, - file_urls: list[str] | None = None, - ref_task_id: str | None = None, + style: MaestroStyle | None = None, + voice: MaestroVoice | None = None, + async_: bool | None = None, + wait: bool = False, + poll_interval: float = 3.0, + max_wait: float = 600.0, callback_url: str | None = None, **extra: Any, - ) -> dict[str, Any]: - """Maestro Video Generation API""" + ) -> AsyncTaskHandle: + """Call /maestro/videos.""" body: dict[str, Any] = {} body["prompt"] = prompt - body["langs"] = langs if langs is not None else ["zh-cn"] - body["style"] = style if style is not None else "auto" - body["voice"] = voice if voice is not None else "auto" body["action"] = action if action is not None else "generate" + if ref_task_id is not None: + body["ref_task_id"] = ref_task_id + if file_urls is not None: + body["file_urls"] = file_urls + body["langs"] = langs if langs is not None else ["zh-cn"] body["aspect"] = aspect if aspect is not None else "9:16" - body["quality"] = quality if quality is not None else "standard" body["duration"] = duration if duration is not None else 30 + body["quality"] = quality if quality is not None else "standard" body["scenario"] = scenario if scenario is not None else "auto" - if file_urls is not None: - body["file_urls"] = file_urls - if ref_task_id is not None: - body["ref_task_id"] = ref_task_id - body.update(extra) - if callback_url is not None: - body["callback_url"] = callback_url - return await self._transport.request("POST", "/maestro/videos", json=body) - - async def estimates( - self, - *, - callback_url: str | None = None, - **extra: Any, - ) -> dict[str, Any]: - """Call /maestro/estimates.""" - body: dict[str, Any] = {} + body["style"] = style if style is not None else "auto" + body["voice"] = voice if voice is not None else "auto" body.update(extra) if callback_url is not None: body["callback_url"] = callback_url - return await self._transport.request("POST", "/maestro/estimates", json=body) + body["async"] = True if async_ is None else async_ + result = await self._transport.request("POST", "/maestro/videos", json=body) + handle = AsyncTaskHandle(_task_id(result), "/maestro/tasks", self._transport, submitted=result) + if wait: + await handle.wait(poll_interval=poll_interval, max_wait=max_wait) + return handle diff --git a/python/tests/test_maestro.py b/python/tests/test_maestro.py new file mode 100644 index 0000000..9eeaf61 --- /dev/null +++ b/python/tests/test_maestro.py @@ -0,0 +1,51 @@ +"""Maestro generated provider contract tests.""" + +from typing import Any + +from acedatacloud.resources.providers.maestro import Maestro + + +class Transport: + def __init__(self) -> None: + self.calls: list[tuple[str, str, dict[str, Any]]] = [] + + def request(self, method: str, path: str, *, json: dict[str, Any]) -> dict[str, Any]: + self.calls.append((method, path, json)) + return {"success": True, "task_id": "task-maestro", "trace_id": "trace-maestro"} + + +def test_generate_serializes_new_sku_contract() -> None: + transport = Transport() + client = Maestro(transport) + + result = client.generate( + prompt="Launch video", + quality="pro", + duration=300, + scenario="drama", + action="extend", + ref_task_id="task-before", + langs=["en", "de"], + ) + + assert result.id == "task-maestro" + assert transport.calls == [ + ( + "POST", + "/maestro/videos", + { + "prompt": "Launch video", + "action": "extend", + "ref_task_id": "task-before", + "langs": ["en", "de"], + "aspect": "9:16", + "duration": 300, + "quality": "pro", + "scenario": "drama", + "style": "auto", + "voice": "auto", + "async": True, + }, + ) + ] + assert not hasattr(client, "estimates") diff --git a/scripts/genlib/go_gen.py b/scripts/genlib/go_gen.py index c50e443..08e9939 100644 --- a/scripts/genlib/go_gen.py +++ b/scripts/genlib/go_gen.py @@ -40,7 +40,7 @@ def _request_struct(svc: Service, ep) -> str: lines.append(f"type {name} struct {{") for p in ep.callable_params: comment = p.description or ("required" if p.required else "optional") - lines.append(f"\t// {comment[:110]}") + lines.append(f"\t// {comment[:110].rstrip()}") lines.append(f"\t{_field_name(p.name)} {p.go_type()}") if ep.pollable: lines.append("\t// Async submits without blocking; poll the returned handle. Defaults true.") diff --git a/scripts/services.json b/scripts/services.json index b0de92f..060315a 100644 --- a/scripts/services.json +++ b/scripts/services.json @@ -22,11 +22,6 @@ "id": "ab512c42-324c-42e6-8d79-49a5042f3548", "path": "/maestro/videos", "name": "Maestro Videos API" - }, - { - "id": "2a5bb4f0-9f83-4ecf-b357-f29ddcc0e9c5", - "path": "/maestro/estimates", - "name": "Maestro Estimates API" } ], "tasks": "/maestro/tasks" diff --git a/scripts/specs/ab512c42-324c-42e6-8d79-49a5042f3548.json b/scripts/specs/ab512c42-324c-42e6-8d79-49a5042f3548.json index 88691fc..be1975b 100644 --- a/scripts/specs/ab512c42-324c-42e6-8d79-49a5042f3548.json +++ b/scripts/specs/ab512c42-324c-42e6-8d79-49a5042f3548.json @@ -1 +1,338 @@ -{"info": {"title": "Maestro Videos API", "version": "2.0.0"}, "paths": {"/maestro/videos": {"post": {"summary": "Maestro Video Generation API", "security": [{"bearerAuth": []}], "responses": {"200": {"content": {"application/json": {"schema": {"type": "object", "properties": {"success": {"type": "boolean"}, "task_id": {"type": "string", "description": "Use this with POST /maestro/tasks."}, "trace_id": {"type": "string"}}}}}, "description": "Job accepted; poll /maestro/tasks with the task_id."}, "400": {"description": "Bad request (missing prompt or invalid fields)."}, "401": {"description": "Unauthorized (missing/invalid token)."}, "403": {"description": "Forbidden (insufficient balance or access)."}, "429": {"description": "Too many requests."}, "500": {"description": "Internal error."}}, "description": "Maestro is an agent-native video producer: describe the video you want in `prompt` (optionally attaching reference images/videos/audio via `file_urls`) and a headless creative director plans, generates the assets (images, voiceover, music, clips), composes and renders a finished, captioned video. This is an async job \u2014 it returns a `task_id` immediately; poll `POST /maestro/tasks` for the result, or supply `callback_url`. Use `action: remix` / `edit` / `extend` with `ref_task_id` to iterate on a previous video.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "required": ["prompt"], "properties": {"langs": {"type": "array", "items": {"type": "string"}, "default": ["zh-cn"], "description": "Output languages, e.g. [\"zh-cn\", \"en\"]. The first is the primary language; each additional one reuses the visuals with a localized voiceover + render and is billed +6 credits."}, "style": {"enum": ["auto", "cinematic", "glass", "luxury", "swiss", "modern", "editorial", "warm", "vibrant", "neon", "mono", "pastel", "bold", "industrial", "futuristic", "retro"], "type": "string", "default": "auto", "description": "Optional visual-style preset \u2014 expressed through typography, palette, motion, image treatment and pacing. Orthogonal to `scenario` (it does NOT change routing). `auto` (default) lets the director pick; every other value adopts a real named look: `cinematic` = dark film-noir (black + blood-red, Oswald); `glass` = Apple / iOS-26 frosted liquid glass; `luxury` = timeless near-black + indigo, huge whitespace; `swiss` = precise grid + electric blue + oversized numerals; `modern` = clean light SaaS; `editorial` = cream magazine + serif; `warm` = intimate cream + amber; `vibrant` = festive folk colour; `neon` = electric neon glow; `mono` = grayscale, type-led; `pastel` = soft candy pastels; `bold` = huge poster type; `industrial` = raw glitch + rust; `futuristic` = particle glow. A freeform string is also accepted as a soft hint."}, "voice": {"enum": ["auto", "warm-female", "bright-female", "anchor-female", "clean-female", "calm-male", "deep-male", "documentary-male", "energetic-male", "storyteller-male"], "type": "string", "default": "auto", "description": "Optional narration voice \u2014 the **timbre** of the voiceover, independent of language. `auto` (default) lets the director pick a fitting voice. Every preset is cross-lingual: the same voice speaks whatever language(s) you set in `langs`, so choose purely by character \u2014 `warm-female`, `bright-female`, `anchor-female`, `clean-female`, `calm-male`, `deep-male`, `documentary-male`, `energetic-male`, `storyteller-male`. Advanced: a raw 32-character Fish `reference_id` is also accepted. For `drama` / `avatar` this sets the primary / narrator timbre; distinct characters may still get their own."}, "action": {"enum": ["generate", "remix", "edit", "extend"], "type": "string", "default": "generate", "description": "generate = a new video. remix / edit / extend = iterate on a previous video (require `ref_task_id`)."}, "aspect": {"enum": ["9:16", "16:9", "1:1"], "type": "string", "default": "9:16", "description": "Output aspect ratio (hint \u2014 the agent may follow the prompt)."}, "prompt": {"type": "string", "example": "\u7528 20 \u79d2\u8bb2\u6e05\u695a\u4ec0\u4e48\u662f\u5411\u91cf\u6570\u636e\u5e93,\u9002\u5408\u96f6\u57fa\u7840\u89c2\u4f17,\u7ed3\u5c3e\u7ed9\u4e00\u53e5\u8bb0\u5fc6\u70b9", "description": "Natural-language brief describing the video to produce (the topic, what to show, tone, audience). The agent decides the script, visuals, voiceover and edit."}, "quality": {"enum": ["draft", "standard", "premium"], "type": "string", "default": "standard", "description": "Production tier, a multiplier on the duration-based price. `draft` = a fast rough cut for previewing the idea (~0.5\u00d7 the standard credits); `standard` = balanced (default, 1\u00d7); `premium` = richer, more detailed and polished (~2\u00d7 the standard credits). Affects turnaround, detail and price."}, "duration": {"type": "integer", "default": 30, "maximum": 600, "minimum": 1, "description": "Target video length in seconds (1\u2013600, i.e. up to 10 minutes). Billed by duration: credits \u2248 0.85 \u00d7 duration \u00d7 quality multiplier \u00d7 scenario multiplier, so a longer or video-native workflow costs proportionally more."}, "scenario": {"enum": ["auto", "narrated", "drama", "avatar", "motion", "slideshow"], "type": "string", "default": "auto", "description": "How to route the video \u2014 a hint; the AI director still decides the final structure. `auto` (default) = the director chooses from your brief. `narrated` = multi-scene narrated video with real photos + voiceover + data cards (people / brands / explainers / history / products). `drama` = acted short drama with characters + dialogue (\u77ed\u5267) and bills at 1.35\u00d7. `avatar` = talking-head / digital human (needs a portrait image via `file_urls`, or a chosen digital human) and bills at 1.15\u00d7. `motion` = abstract kinetic-typography / data / logo motion graphic. `slideshow` = presentation deck / pitch. Legacy values `general` / `explainer` / `product` / `website` / `changelog` / `captions` are still accepted (mapped to `auto`), and `slides` maps to `slideshow`."}, "file_urls": {"type": "array", "items": {"type": "string"}, "description": "Optional reference media (image / video / audio URLs) the agent can use \u2014 e.g. a product shot or logo to feature, footage to caption."}, "ref_task_id": {"type": "string", "description": "Required when `action` is remix / edit / extend: the task_id of the previous video to start from."}, "callback_url": {"type": "string", "description": "Optional. Fired with the result when the task reaches a terminal state (succeeded / failed)."}}}}}, "required": true}}}}, "openapi": "3.0.0", "servers": [{"url": "https://api.acedata.cloud", "description": "Ace Data Cloud API server"}], "components": {"securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}}}} \ No newline at end of file +{ + "openapi": "3.0.0", + "info": { + "title": "Maestro Videos API", + "version": "2.0.0" + }, + "servers": [ + { + "url": "https://api.acedata.cloud", + "description": "Ace Data Cloud API server" + } + ], + "paths": { + "/maestro/videos": { + "post": { + "summary": "$t(api_description_maestro_videos)", + "description": "Maestro is an agent-native video producer: describe the video you want in `prompt` (optionally attaching reference images/videos/audio via `file_urls`) and a headless creative director plans, generates the assets (images, voiceover, music, clips), composes and renders a finished, captioned video. This is an async job — it returns a `task_id` immediately; poll `POST /maestro/tasks` for the result, or supply `callback_url`. Use `action: remix` / `edit` / `extend` with `ref_task_id` to iterate on a previous video.", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "prompt" + ], + "properties": { + "prompt": { + "type": "string", + "description": "Natural-language brief describing the video to produce (the topic, what to show, tone, audience). The agent decides the script, visuals, voiceover and edit.", + "example": "用 20 秒讲清楚什么是向量数据库,适合零基础观众,结尾给一句记忆点" + }, + "action": { + "type": "string", + "enum": [ + "generate", + "remix", + "edit", + "extend" + ], + "default": "generate", + "description": "Production action. Lite supports generate/edit; Standard adds remix; Pro adds extend. remix/edit/extend require `ref_task_id`." + }, + "ref_task_id": { + "type": "string", + "description": "Required when `action` is remix / edit / extend: the task_id of the previous video to start from." + }, + "file_urls": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional reference media (image / video / audio URLs) the agent can use — e.g. a product shot or logo to feature, footage to caption." + }, + "langs": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "zh-cn" + ], + "description": "Output languages. Lite supports 1, Standard up to 2, and Pro up to 4. The first is primary; each additional delivered language is billed +6 credits.", + "maxItems": 4 + }, + "aspect": { + "type": "string", + "enum": [ + "9:16", + "16:9", + "1:1" + ], + "default": "9:16", + "description": "Required output aspect ratio. Lite renders 720p/24fps; Standard and Pro render 1080p/30fps." + }, + "duration": { + "type": "integer", + "default": 30, + "minimum": 5, + "maximum": 300, + "description": "Target video length in seconds. Lite: 5–30; Standard: 5–120; Pro: 5–300. Successful jobs are billed by actual delivered duration, never above the requested duration." + }, + "quality": { + "type": "string", + "enum": [ + "lite", + "standard", + "pro" + ], + "default": "standard", + "description": "Production SKU. `lite` = fast 720p short video at 0.20 credits/s (up to 30s); `standard` = balanced 1080p at 0.60 credits/s (up to 120s); `pro` = advanced production at 1.20 credits/s (up to 300s). Default: standard." + }, + "scenario": { + "type": "string", + "enum": [ + "auto", + "narrated", + "captions", + "avatar", + "drama" + ], + "default": "auto", + "description": "Production route. Lite supports auto/narrated/captions; Standard adds avatar; Pro adds drama. `captions` requires source video in `file_urls`; `avatar` requires a portrait. avatar bills at 1.15× and drama at 1.35×." + }, + "style": { + "type": "string", + "enum": [ + "auto", + "cinematic", + "glass", + "luxury", + "swiss", + "modern", + "editorial", + "warm", + "vibrant", + "neon", + "mono", + "pastel", + "bold", + "industrial", + "futuristic", + "retro" + ], + "default": "auto", + "description": "Optional visual-style preset — expressed through typography, palette, motion, image treatment and pacing. Orthogonal to `scenario` (it does NOT change routing). `auto` (default) lets the director pick; every other value adopts a real named look: `cinematic` = dark film-noir (black + blood-red, Oswald); `glass` = Apple / iOS-26 frosted liquid glass; `luxury` = timeless near-black + indigo, huge whitespace; `swiss` = precise grid + electric blue + oversized numerals; `modern` = clean light SaaS; `editorial` = cream magazine + serif; `warm` = intimate cream + amber; `vibrant` = festive folk colour; `neon` = electric neon glow; `mono` = grayscale, type-led; `pastel` = soft candy pastels; `bold` = huge poster type; `industrial` = raw glitch + rust; `futuristic` = particle glow. A freeform string is also accepted as a soft hint." + }, + "voice": { + "type": "string", + "enum": [ + "auto", + "warm-female", + "bright-female", + "anchor-female", + "clean-female", + "calm-male", + "deep-male", + "documentary-male", + "energetic-male", + "storyteller-male" + ], + "default": "auto", + "description": "Optional narration voice — the **timbre** of the voiceover, independent of language. `auto` (default) lets the director pick a fitting voice. Every preset is cross-lingual: the same voice speaks whatever language(s) you set in `langs`, so choose purely by character — `warm-female`, `bright-female`, `anchor-female`, `clean-female`, `calm-male`, `deep-male`, `documentary-male`, `energetic-male`, `storyteller-male`. Advanced: a raw 32-character Fish `reference_id` is also accepted. For `drama` / `avatar` this sets the primary / narrator timbre; distinct characters may still get their own." + }, + "callback_url": { + "type": "string", + "description": "Optional. Fired with the result when the task reaches a terminal state (succeeded / failed)." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Job created; poll POST /maestro/tasks with the task_id.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "task_id": { + "type": "string", + "description": "Use this with POST /maestro/tasks." + }, + "trace_id": { + "type": "string" + } + }, + "required": [ + "success", + "task_id", + "trace_id" + ], + "example": { + "success": true, + "task_id": "35c6159f-f94e-4b39-82b8-a3d77009bc1d", + "trace_id": "trace_7f8c2b1a" + } + } + } + } + }, + "400": { + "description": "Bad request (missing prompt or invalid fields).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DetailError" + }, + "example": { + "detail": "missing field: prompt" + } + } + } + }, + "401": { + "description": "Unauthorized (missing or invalid token).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GatewayError" + }, + "example": { + "error": { + "code": "invalid_token", + "message": "The token is invalid." + }, + "trace_id": "trace_7f8c2b1a" + } + } + } + }, + "403": { + "description": "Forbidden (insufficient balance, restricted access, or private option).", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/GatewayError" + }, + { + "$ref": "#/components/schemas/DetailError" + } + ] + }, + "example": { + "error": { + "code": "used_up", + "message": "The available balance is insufficient for this request." + }, + "trace_id": "trace_7f8c2b1a" + } + } + } + }, + "429": { + "description": "Too many requests.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GatewayError" + }, + "example": { + "error": { + "code": "too_many_requests", + "message": "Too many requests. Try again later." + }, + "trace_id": "trace_7f8c2b1a" + } + } + } + }, + "500": { + "description": "Internal error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GatewayError" + }, + "example": { + "error": { + "code": "api_error", + "message": "The service is temporarily unavailable." + }, + "trace_id": "trace_7f8c2b1a" + } + } + } + } + }, + "operationId": "createMaestroVideos" + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "GatewayError": { + "type": "object", + "required": [ + "error", + "trace_id" + ], + "properties": { + "error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Stable machine-readable error code." + }, + "message": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "trace_id": { + "type": "string", + "description": "Request trace ID for support and diagnostics." + } + } + }, + "DetailError": { + "type": "object", + "required": [ + "detail" + ], + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error detail." + } + } + } + } + } +} diff --git a/typescript/src/resources/providers/maestro.ts b/typescript/src/resources/providers/maestro.ts index b224cf4..4f1adf1 100644 --- a/typescript/src/resources/providers/maestro.ts +++ b/typescript/src/resources/providers/maestro.ts @@ -6,37 +6,45 @@ */ import { Transport } from '../../runtime/transport'; +import { TaskHandle } from '../../runtime/tasks'; +function taskId(result: Record): string { + if (typeof result?.task_id === 'string') return result.task_id; + const data = result?.data as Record | undefined; + if (data && typeof data.task_id === 'string') return data.task_id; + return typeof result?.id === 'string' ? result.id : ''; +} + export interface MaestroGenerateOptions { /** Natural-language brief describing the video to produce (the topic, what to show, tone, audience). The agent decides the script, visuals, voiceover and edit. */ prompt: string; - /** Output languages, e.g. ["zh-cn", "en"]. The first is the primary language; each additional one reuses the visuals with a localized voiceover + render and is billed +6 credits. */ + /** Production action. Lite supports generate/edit; Standard adds remix; Pro adds extend. remix/edit/extend require `ref_task_id`. */ + action?: "generate" | "remix" | "edit" | "extend"; + /** Required when `action` is remix / edit / extend: the task_id of the previous video to start from. */ + refTaskId?: string; + /** Optional reference media (image / video / audio URLs) the agent can use — e.g. a product shot or logo to feature, footage to caption. */ + fileUrls?: string[]; + /** Output languages. Lite supports 1, Standard up to 2, and Pro up to 4. The first is primary; each additional delivered language is billed +6 credits. */ langs?: string[]; + /** Required output aspect ratio. Lite renders 720p/24fps; Standard and Pro render 1080p/30fps. */ + aspect?: "9:16" | "16:9" | "1:1"; + /** Target video length in seconds. Lite: 5–30; Standard: 5–120; Pro: 5–300. Successful jobs are billed by actual delivered duration, never above the requested duration. */ + duration?: number; + /** Production SKU. `lite` = fast 720p short video at 0.20 credits/s (up to 30s); `standard` = balanced 1080p at 0.60 credits/s (up to 120s); `pro` = advanced production at 1.20 credits/s (up to 300s). Default: standard. */ + quality?: "lite" | "standard" | "pro"; + /** Production route. Lite supports auto/narrated/captions; Standard adds avatar; Pro adds drama. `captions` requires source video in `file_urls`; `avatar` requires a portrait. avatar bills at 1.15× and drama at 1.35×. */ + scenario?: "auto" | "narrated" | "captions" | "avatar" | "drama"; /** Optional visual-style preset — expressed through typography, palette, motion, image treatment and pacing. Orthogonal to `scenario` (it does NOT change routing). `auto` (default) lets the director pick; every other value adopts a real named look: `cinematic` = dark film-noir (black + blood-red, Oswald); `glass` = Apple / iOS-26 frosted liquid glass; `luxury` = timeless near-black + indigo, huge whitespace; `swiss` = precise grid + electric blue + oversized numerals; `modern` = clean light SaaS; `editorial` = cream magazine + serif; `warm` = intimate cream + amber; `vibrant` = festive folk colour; `neon` = electric neon glow; `mono` = grayscale, type-led; `pastel` = soft candy pastels; `bold` = huge poster type; `industrial` = raw glitch + rust; `futuristic` = particle glow. A freeform string is also accepted as a soft hint. */ style?: "auto" | "cinematic" | "glass" | "luxury" | "swiss" | "modern" | "editorial" | "warm" | "vibrant" | "neon" | "mono" | "pastel" | "bold" | "industrial" | "futuristic" | "retro"; /** Optional narration voice — the **timbre** of the voiceover, independent of language. `auto` (default) lets the director pick a fitting voice. Every preset is cross-lingual: the same voice speaks whatever language(s) you set in `langs`, so choose purely by character — `warm-female`, `bright-female`, `anchor-female`, `clean-female`, `calm-male`, `deep-male`, `documentary-male`, `energetic-male`, `storyteller-male`. Advanced: a raw 32-character Fish `reference_id` is also accepted. For `drama` / `avatar` this sets the primary / narrator timbre; distinct characters may still get their own. */ voice?: "auto" | "warm-female" | "bright-female" | "anchor-female" | "clean-female" | "calm-male" | "deep-male" | "documentary-male" | "energetic-male" | "storyteller-male"; - /** generate = a new video. remix / edit / extend = iterate on a previous video (require `ref_task_id`). */ - action?: "generate" | "remix" | "edit" | "extend"; - /** Output aspect ratio (hint — the agent may follow the prompt). */ - aspect?: "9:16" | "16:9" | "1:1"; - /** Production tier, a multiplier on the duration-based price. `draft` = a fast rough cut for previewing the idea (~0.5× the standard credits); `standard` = balanced (default, 1×); `premium` = richer, more detailed and polished (~2× the standard credits). Affects turnaround, detail and price. */ - quality?: "draft" | "standard" | "premium"; - /** Target video length in seconds (1–600, i.e. up to 10 minutes). Billed by duration: credits ≈ 0.85 × duration × quality multiplier × scenario multiplier, so a longer or video-native workflow costs proportionally more. */ - duration?: number; - /** How to route the video — a hint; the AI director still decides the final structure. `auto` (default) = the director chooses from your brief. `narrated` = multi-scene narrated video with real photos + voiceover + data cards (people / brands / explainers / history / products). `drama` = acted short drama with characters + dialogue (短剧) and bills at 1.35×. `avatar` = talking-head / digital human (needs a portrait image via `file_urls`, or a chosen digital human) and bills at 1.15×. `motion` = abstract kinetic-typography / data / logo motion graphic. `slideshow` = presentation deck / pitch. Legacy values `general` / `explainer` / `product` / `website` / `changelog` / `captions` are still accepted (mapped to `auto`), and `slides` maps to `slideshow`. */ - scenario?: "auto" | "narrated" | "drama" | "avatar" | "motion" | "slideshow"; - /** Optional reference media (image / video / audio URLs) the agent can use — e.g. a product shot or logo to feature, footage to caption. */ - fileUrls?: string[]; - /** Required when `action` is remix / edit / extend: the task_id of the previous video to start from. */ - refTaskId?: string; - callbackUrl?: string; - /** Any parameter added upstream before the SDK is regenerated. */ - [key: string]: unknown; -} - -export interface MaestroEstimatesOptions { + /** Submit asynchronously and poll. Defaults to true. */ + async?: boolean; + /** Wait for completion before returning the handle. */ + wait?: boolean; + pollInterval?: number; + maxWait?: number; callbackUrl?: string; /** Any parameter added upstream before the SDK is regenerated. */ [key: string]: unknown; @@ -46,39 +54,33 @@ export interface MaestroEstimatesOptions { export class Maestro { constructor(private transport: Transport) {} - /** Maestro Video Generation API */ - async generate(options: MaestroGenerateOptions): Promise> { + /** Call /maestro/videos. */ + async generate(options: MaestroGenerateOptions): Promise { const body: Record = {}; body["prompt"] = options.prompt; - body["langs"] = options.langs ?? ["zh-cn"]; - body["style"] = options.style ?? "auto"; - body["voice"] = options.voice ?? "auto"; body["action"] = options.action ?? "generate"; + if (options.refTaskId !== undefined) body["ref_task_id"] = options.refTaskId; + if (options.fileUrls !== undefined) body["file_urls"] = options.fileUrls; + body["langs"] = options.langs ?? ["zh-cn"]; body["aspect"] = options.aspect ?? "9:16"; - body["quality"] = options.quality ?? "standard"; body["duration"] = options.duration ?? 30; + body["quality"] = options.quality ?? "standard"; body["scenario"] = options.scenario ?? "auto"; - if (options.fileUrls !== undefined) body["file_urls"] = options.fileUrls; - if (options.refTaskId !== undefined) body["ref_task_id"] = options.refTaskId; + body["style"] = options.style ?? "auto"; + body["voice"] = options.voice ?? "auto"; for (const [key, value] of Object.entries(options)) { if (!["action", "aspect", "async", "callbackUrl", "duration", "fileUrls", "langs", "maxWait", "pollInterval", "prompt", "quality", "refTaskId", "scenario", "style", "voice", "wait"].includes(key) && value !== undefined) { body[key] = value; } } if (options.callbackUrl !== undefined) body.callback_url = options.callbackUrl; - return (await this.transport.request('POST', "/maestro/videos", { json: body })) as Record; - } - - /** Call /maestro/estimates. */ - async estimates(options: MaestroEstimatesOptions = {}): Promise> { - const body: Record = {}; - for (const [key, value] of Object.entries(options)) { - if (!["async", "callbackUrl", "maxWait", "pollInterval", "wait"].includes(key) && value !== undefined) { - body[key] = value; - } + body.async = options.async ?? true; + const result = (await this.transport.request('POST', "/maestro/videos", { json: body })) as Record; + const handle = new TaskHandle(taskId(result), "/maestro/tasks", this.transport, result); + if (options.wait) { + await handle.wait({ pollInterval: options.pollInterval, maxWait: options.maxWait }); } - if (options.callbackUrl !== undefined) body.callback_url = options.callbackUrl; - return (await this.transport.request('POST', "/maestro/estimates", { json: body })) as Record; + return handle; } }