ottoport 1.13.1 → 1.14.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ottoport",
3
3
  "description": "One API for every model. Call chat, image, video, speech and music models through the OttoPort gateway — as MCP tools, slash commands, or the bundled CLI.",
4
- "version": "1.13.1",
4
+ "version": "1.14.0",
5
5
  "author": {
6
6
  "name": "LITBOX LLC",
7
7
  "email": "support@ottoport.ai"
package/README.md CHANGED
@@ -30,6 +30,7 @@ ottoport video "<prompt>" [--model kling-3.0] [--duration 5] [--resolution 1080p
30
30
  ottoport webhook status|retry <job-id> # delivery attempts / redeliver now
31
31
  ottoport speech "<text>" [--model gpt-4o-mini-tts] [--voice alloy] [--out speech.mp3]
32
32
  ottoport music "<prompt>" [--model suno-v5] [--duration 15] [--out song.mp3]
33
+ ottoport music --model mirelo-sfx-2.0 --video <url> [--duration 8] # soundtrack for a video
33
34
 
34
35
  ottoport install <host> # wire the MCP server into an agent
35
36
  ottoport mcp # run the MCP server on stdio
package/cli/ottoport.mjs CHANGED
@@ -10,6 +10,7 @@
10
10
  // ottoport video "prompt" [--model kling-3.0] [--duration 5] [--out clip.mp4]
11
11
  // ottoport speech "text" [--model gpt-4o-mini-tts] [--voice alloy] [--out speech.mp3]
12
12
  // ottoport music "prompt" [--model suno-v5] [--duration 15] [--out song.mp3]
13
+ // ottoport music --model mirelo-sfx-2.0 --video <url> [--duration 8]
13
14
  // ottoport collections [list | create "name"]
14
15
  // ottoport mcp
15
16
  // ottoport install <codex|claude|hermes|cursor|windsurf|claude-desktop>
@@ -24,6 +25,9 @@ import { readFile, stat, writeFile } from "node:fs/promises";
24
25
  const BASE = "https://ottoport.ai";
25
26
 
26
27
  // ── arg parsing ──────────────────────────────────────────────────────
28
+ // Switches that never take a value, so `--draft "a boat"` keeps its prompt.
29
+ const BOOLEAN_FLAGS = new Set(["draft"]);
30
+
27
31
  function parseArgs(argv) {
28
32
  const positional = [];
29
33
  const flags = {};
@@ -33,7 +37,7 @@ function parseArgs(argv) {
33
37
  const key = a.slice(2);
34
38
  if (key.startsWith("no-")) {
35
39
  flags[key.slice(3)] = false;
36
- } else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
40
+ } else if (!BOOLEAN_FLAGS.has(key) && i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
37
41
  const value = argv[++i];
38
42
  // Repeats accumulate rather than overwrite, so `--ref a --ref b` keeps
39
43
  // both. Everything downstream reads the last value of a scalar flag,
@@ -297,10 +301,14 @@ async function cmdImage(prompt, flags) {
297
301
 
298
302
  async function cmdVideo(prompt, flags) {
299
303
  // A depth model needs only --video; the gateway refuses a prompt there.
300
- if (!prompt && !last(flags.video)) die("video requires a prompt: ottoport video \"a drone shot\" (or --video <url> on a depth model)");
304
+ // A draft's 1080p render takes everything from the draft, the prompt included.
305
+ const draftId = last(flags["draft-id"]);
306
+ if (!prompt && !last(flags.video) && !draftId) die("video requires a prompt: ottoport video \"a drone shot\" (or --video <url> on a depth model)");
301
307
  const { baseUrl, apiKey } = config(flags);
308
+ // Drafts are Seedance 2.5's, so a draft with no model named is one of its.
309
+ const model = last(flags.model) || (flags.draft === true || draftId ? "seedance-2.5" : "kling-3.0");
302
310
  const body = {
303
- model: last(flags.model) || "kling-3.0",
311
+ model,
304
312
  ...(prompt ? { prompt } : {}),
305
313
  ...(last(flags.video) ? { video_url: last(flags.video) } : {}),
306
314
  ...(last(flags.duration) ? { duration: Number(last(flags.duration)) } : {}),
@@ -312,6 +320,8 @@ async function cmdVideo(prompt, flags) {
312
320
  ...(references(flags) ? { reference_image_urls: references(flags) } : {}),
313
321
  ...(references(flags, "ref-video") ? { reference_video_urls: references(flags, "ref-video") } : {}),
314
322
  ...(references(flags, "ref-audio") ? { reference_audio_urls: references(flags, "ref-audio") } : {}),
323
+ ...(flags.draft === true ? { draft: true } : {}),
324
+ ...(draftId ? { draft_id: draftId } : {}),
315
325
  ...webhook(flags),
316
326
  ...collection(flags),
317
327
  };
@@ -338,6 +348,10 @@ async function cmdVideo(prompt, flags) {
338
348
  const url = job.data?.[0]?.url;
339
349
  if (url) {
340
350
  console.log(outputLine(job.data[0]));
351
+ // The draft's job id is what renders it in 1080p, so say it.
352
+ if (!draftId && (body.draft || model.endsWith("-draft"))) {
353
+ process.stderr.write(`draft ${job.id} — render it in 1080p within seven days: ottoport video --model ${model} --draft-id ${job.id}\n`);
354
+ }
341
355
  if (flags.out) await download(url, last(flags.out));
342
356
  } else {
343
357
  console.log(JSON.stringify(job, null, 2));
@@ -381,11 +395,14 @@ async function cmdWebhook(action, jobId, flags) {
381
395
  }
382
396
 
383
397
  async function cmdAudio(kind, prompt, flags) {
384
- if (!prompt) die(`${kind} requires a prompt: ottoport ${kind} "hello"`);
398
+ // A sound-effects model scores a video, so the prompt is optional with one.
399
+ const video = kind === "music" ? last(flags.video) : undefined;
400
+ if (!prompt && !video) die(`${kind} requires a prompt: ottoport ${kind} "hello"`);
385
401
  const { baseUrl, apiKey } = config(flags);
386
402
  const body = {
387
403
  model: last(flags.model) || (kind === "speech" ? "gpt-4o-mini-tts" : "suno-v5"),
388
- prompt,
404
+ ...(prompt ? { prompt } : {}),
405
+ ...(video ? { video_url: video } : {}),
389
406
  ...(last(flags.voice) ? { voice: last(flags.voice) } : {}),
390
407
  ...(last(flags.duration) ? { duration: Number(last(flags.duration)) } : {}),
391
408
  ...(last(flags.format) ? { format: last(flags.format) } : {}),
@@ -475,14 +492,20 @@ Usage:
475
492
  [--aspect-ratio 16:9] [--seed 7] [--image <url>]
476
493
  [--last-frame <url>] [--ref <url>[,<url>…]]
477
494
  [--ref-video <url>[,<url>…]] [--ref-audio <url>[,<url>…]]
478
- [--webhook-url https://…] [--webhook-secret <s>]
495
+ [--draft] [--webhook-url https://…] [--webhook-secret <s>]
479
496
  [--no-wait] [--out clip.mp4]
497
+ ottoport video --draft-id <job-id> [--model seedance-2.5] [--out clip.mp4]
498
+ render a Seedance 2.5 draft (--draft, or the
499
+ seedance-2.5-draft model) in 1080p, within 7 days
480
500
  ottoport video --model video-depth-anything --video <url> [--out depth.mp4]
481
501
  turn a video (≤60s) into a grayscale depth video
482
502
  ottoport webhook status <job-id> [--json] delivery attempts and responses
483
503
  ottoport webhook retry <job-id> [--json] redeliver a finished job's webhook now
484
504
  ottoport speech "<text>" [--model gpt-4o-mini-tts] [--voice alloy] [--format mp3] [--out speech.mp3]
485
505
  ottoport music "<prompt>" [--model suno-v5] [--duration 15] [--format mp3] [--out song.mp3]
506
+ ottoport music ["<prompt>"] --model mirelo-sfx-2.0 --video <url> [--duration 8] [--out sfx.wav]
507
+ a soundtrack synced to the video, dialogue kept;
508
+ set --duration to the clip's length (it bills)
486
509
  ottoport collections [list] [--json] collections in the key's project
487
510
  ottoport collections create "<name>" [--description "..."] [--external-id <id>]
488
511
  ottoport mcp
@@ -512,6 +535,8 @@ Examples:
512
535
  ottoport image --model seedream-5.0-layers --image https://…/poster.png
513
536
  ottoport speech "Welcome to OttoPort" --voice alloy --out welcome.mp3
514
537
  ottoport music "lo-fi focus beat" --duration 20 --out focus.mp3
538
+ ottoport music "rain on a tin roof" --model mirelo-sfx-2.0 --duration 10 --format mp3 --out rain.mp3
539
+ ottoport music --model mirelo-sfx-2.0 --video https://example.com/chase.mp4 --duration 8 --out chase.wav
515
540
  ottoport collections create "Spring campaign"
516
541
  ottoport image "spring hero banner" --collection <collection-id>
517
542
  ottoport mcp
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Turn text into speech, or a prompt into music, through the OttoPort gateway
2
+ description: Turn text into speech, or a prompt into music or sound effects, through the OttoPort gateway
3
3
  argument-hint: "<text> [--voice alloy] [--music]"
4
4
  allowed-tools: mcp__ottoport__ottoport_generate_speech, mcp__ottoport__ottoport_generate_music, Bash(curl:*)
5
5
  ---
@@ -7,7 +7,9 @@ allowed-tools: mcp__ottoport__ottoport_generate_speech, mcp__ottoport__ottoport_
7
7
  Request: $ARGUMENTS
8
8
 
9
9
  Use `ottoport_generate_music` when the request is for music, a song, or a
10
- backing track; otherwise use `ottoport_generate_speech` to read the text aloud.
10
+ backing track — or for sound effects, ambience or a soundtrack for a video, with
11
+ `model` `mirelo-sfx-2.0` (pass the video as `video_url`, and `duration` as its
12
+ length); otherwise use `ottoport_generate_speech` to read the text aloud.
11
13
  Pass `voice` and `format` only when named — speech defaults to `gpt-4o-mini-tts`
12
14
  with the `alloy` voice, music to `suno-v5`.
13
15
 
package/commands/video.md CHANGED
@@ -22,6 +22,11 @@ Request: $ARGUMENTS
22
22
  discovering it from a failed call.
23
23
  - `resolution` is billed: a 1080p or 4k clip costs more per second than the
24
24
  model's base tier. Send one only when the request asks for it.
25
+ - When the user wants to preview before paying for the full clip, draft it:
26
+ `model: "seedance-2.5-draft"` (or `seedance-2.5` with `draft: true`) renders
27
+ a cheap 480p take. To keep it, call again with the same model and
28
+ `draft_id` set to the draft's job id — nothing else; it renders that shot in
29
+ 1080p, within seven days.
25
30
  - Video generation takes a minute or more and the call blocks until the job is
26
31
  terminal. Say so before you start, and do not retry a call that is merely slow
27
32
  — a retry is a second billable generation.
package/mcp/server.mjs CHANGED
@@ -358,7 +358,8 @@ async function generateVideo({ prompt, model, webhook_url, webhook_secret, colle
358
358
  const output = job.data?.[0];
359
359
  const url = output?.url ? outputLine(output) : undefined;
360
360
  // The draft's job id is what completes it, so it is worth saying.
361
- if (url && rest.draft) return `${url}\nDraft ${job.id} — complete it at 1080p with draft_id "${job.id}" within seven days.`;
361
+ // `seedance-2.5-draft` drafts without being told to.
362
+ if (url && !rest.draft_id && (rest.draft || /-draft$/.test(model ?? ""))) return `${url}\nDraft ${job.id} — complete it at 1080p with draft_id "${job.id}" within seven days.`;
362
363
  return url || JSON.stringify(job);
363
364
  }
364
365
 
@@ -374,15 +375,16 @@ async function videoWebhook({ job_id, action = "status" }) {
374
375
  return JSON.stringify({ id: body.id, status: body.status, webhook: body.webhook ?? null }, null, 2);
375
376
  }
376
377
 
377
- async function generateAudio(kind, { prompt, model, voice, duration, format, collection }) {
378
- if (!prompt) throw new Error("`prompt` is required");
378
+ async function generateAudio(kind, { prompt, model, voice, duration, format, video_url, collection }) {
379
+ if (!prompt && !video_url) throw new Error("`prompt` is required");
379
380
  const res = await fetch(`${BASE}/api/v1/audio/${kind === "tts" ? "speech" : "music"}`, {
380
381
  method: "POST",
381
382
  headers: headers(),
382
383
  body: JSON.stringify({
383
384
  model: model || (kind === "tts" ? "gpt-4o-mini-tts" : "suno-v5"),
384
- prompt,
385
+ ...(prompt ? { prompt } : {}),
385
386
  ...(voice ? { voice } : {}),
387
+ ...(video_url ? { video_url } : {}),
386
388
  ...(duration ? { duration } : {}),
387
389
  ...(format ? { format } : {}),
388
390
  ...withCollection(await collectionId(collection)),
@@ -458,7 +460,7 @@ const TOOLS = [
458
460
  },
459
461
  {
460
462
  name: "ottoport_generate_video",
461
- description: "Generate a video through an OttoPort video model (Veo, Kling, Seedance, …). Modes: text-to-video, image-to-video (`image_url`), first-and-last-frame (plus `last_frame_url`), reference-to-video (`reference_image_urls`, plus `reference_video_urls` / `reference_audio_urls` on models whose capabilities list `referenceMedia`), and on some models video edit/extend (`video_url`). Seedance 2.5 also drafts: `draft: true` renders a cheap 480p preview, and `draft_id` set to that job's id renders the same shot at 1080p. video-depth-anything instead turns `video_url` into a grayscale depth video and takes no prompt. Kling motion control (`kling-2.6-motion-control`, `kling-3.0-motion-control` and their `-pro` variants) makes the character in `image_url` perform the movement in `video_url`; the prompt is optional and the output runs as long as the clip. Which a model accepts is in its capabilities — call ottoport_list_models. Blocks on the queue and returns a video URL, unless `webhook_url` is given.",
463
+ description: "Generate a video through an OttoPort video model (Veo, Kling, Seedance, …). Modes: text-to-video, image-to-video (`image_url`), first-and-last-frame (plus `last_frame_url`), reference-to-video (`reference_image_urls`, plus `reference_video_urls` / `reference_audio_urls` on models whose capabilities list `referenceMedia`), and on some models video edit/extend (`video_url`). Seedance 2.5 also drafts: `draft: true` (or the model `seedance-2.5-draft`, where every request is a draft) renders a cheap 480p preview, and `draft_id` set to that job's id renders the same shot at 1080p. video-depth-anything instead turns `video_url` into a grayscale depth video and takes no prompt. Kling motion control (`kling-2.6-motion-control`, `kling-3.0-motion-control` and their `-pro` variants) makes the character in `image_url` perform the movement in `video_url`; the prompt is optional and the output runs as long as the clip. Which a model accepts is in its capabilities — call ottoport_list_models. Blocks on the queue and returns a video URL, unless `webhook_url` is given.",
462
464
  inputSchema: {
463
465
  type: "object",
464
466
  properties: {
@@ -480,8 +482,8 @@ const TOOLS = [
480
482
  character_orientation: { type: "string", enum: ["video", "image"], description: "Motion-control models only: whether the character faces the way the person in `video_url` does (\"video\", the default) or keeps the pose of `image_url` (\"image\")." },
481
483
  keep_original_sound: { type: "boolean", description: "Motion-control models only: keep the audio of `video_url` in the result." },
482
484
  video_task: { type: "string", enum: ["edit", "extend"], description: "edit (default) restyles video_url; extend continues it for `duration` more seconds." },
483
- draft: { type: "boolean", description: "Seedance 2.5 only: render a 480p preview at about a quarter of the 1080p price, to check the shot before paying for it. Text, image and reference-image requests; no `resolution`." },
484
- draft_id: { type: "string", description: "Seedance 2.5 only: the job id of a finished draft, to render that same shot at 1080p. Send `model` and this alone — prompt, inputs and duration come from the draft. Within seven days of the draft." },
485
+ draft: { type: "boolean", description: "seedance-2.5 only (implied on seedance-2.5-draft): render a 480p preview at about a quarter of the 1080p price, to check the shot before paying for it. Text, image and reference-image requests; no `resolution`." },
486
+ draft_id: { type: "string", description: "seedance-2.5 or seedance-2.5-draft: the job id of a finished draft, to render that same shot at 1080p — either id completes a draft made with the other. Send `model` and this alone — prompt, inputs and duration come from the draft. Within seven days of the draft." },
485
487
  webhook_url: { type: "string", description: "Public HTTPS endpoint to POST the finished job to (events video.completed / video.failed, signed with HMAC-SHA256). With it the tool returns the job id at once instead of waiting." },
486
488
  webhook_secret: { type: "string", description: "Signing secret for webhook_url. Omit to use the server's OTTOPORT_WEBHOOK_SECRET." },
487
489
  collection: { type: "string", description: "Save the result for good into this collection — its id or exact name (see ottoport_list_collections). Without it the file expires after a few days. Needs collections unlocked on the account. Defaults to OTTOPORT_COLLECTION when set." },
@@ -521,17 +523,17 @@ const TOOLS = [
521
523
  },
522
524
  {
523
525
  name: "ottoport_generate_music",
524
- description: "Generate music from a prompt through an OttoPort music model. Returns an audio URL or data URL.",
526
+ description: "Generate music or sound effects from a prompt through an OttoPort music model — or, with mirelo-sfx-2.0 and a video_url, a soundtrack synced to that video. Returns an audio URL or data URL.",
525
527
  inputSchema: {
526
528
  type: "object",
527
529
  properties: {
528
- prompt: { type: "string", description: "Music-generation prompt." },
529
- model: { type: "string", description: "Music model id. Default suno-v5." },
530
+ prompt: { type: "string", description: "Music-generation prompt, or the sounds to make. Optional with video_url." },
531
+ model: { type: "string", description: "Music model id. Default suno-v5; mirelo-sfx-2.0 for sound effects." },
530
532
  duration: { type: "number", description: "Requested duration in seconds." },
533
+ video_url: { type: "string", description: "mirelo-sfx-2.0 only: an http(s) video to score. Set duration to its length — duration is what bills." },
531
534
  format: { type: "string", description: 'e.g. "mp3" or "wav".' },
532
535
  collection: { type: "string", description: "Save the result for good into this collection — its id or exact name (see ottoport_list_collections). Without it the file expires after a few days. Needs collections unlocked on the account. Defaults to OTTOPORT_COLLECTION when set." },
533
536
  },
534
- required: ["prompt"],
535
537
  },
536
538
  handler: (args) => generateAudio("music", args),
537
539
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ottoport",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "description": "Claude Code plugin, CLI and MCP server for OttoPort — one OpenAI-compatible API for every LLM, image, video, and speech model.",
5
5
  "homepage": "https://ottoport.ai",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ottoport
3
- description: Generate text, images, video, speech, and music through OttoPort — one gateway for every model (GPT, Claude, Gemini, GPT Image, Nano Banana, Veo, Kling, Seedance, Suno). Use when the user wants to call an LLM or generate an image, video, or audio and has OttoPort configured, or asks to "use OttoPort".
3
+ description: Generate text, images, video, speech, and music through OttoPort — one gateway for every model (GPT, Claude, Gemini, GPT Image, Nano Banana, Veo, Kling, Seedance, Suno, Mirelo). Use when the user wants to call an LLM or generate an image, video, audio or sound effects and has OttoPort configured, or asks to "use OttoPort".
4
4
  ---
5
5
 
6
6
  # OttoPort
@@ -17,10 +17,10 @@ and do not call the HTTP API, when a tool covers the job:
17
17
  | `ottoport_list_models` | The catalog: what each model is for, and its rate in credits and USD. Filter with `modality`, or pass `model` for ONE model in full — its modes, its resolution tiers with prices, and every parameter it reads. |
18
18
  | `ottoport_chat` | `prompt`, plus optional `model`, `system`, `temperature`, `max_tokens`. |
19
19
  | `ottoport_generate_image` | `prompt`, plus optional `model`, `size` or `resolution`, `quality` (GPT Image and Ideogram), `aspect_ratio`, `n`, `seed`, `image_url` (one reference), `reference_image_urls` (several). Layer decomposition too: `model` `qwen-image-layered` or `seedream-5.0-layers` with `image_url` alone (prompt optional, `num_layers` on Qwen) returns one URL per RGBA layer. |
20
- | `ottoport_generate_video` | `prompt`, plus optional `model`, `duration`, `resolution`, `aspect_ratio`, `seed`, `image_url` (first frame), `last_frame_url`, `reference_image_urls`, `reference_video_urls` / `reference_audio_urls` (models listing `referenceMedia`), `video_url` (edit / extend, or the movement for motion control), `character_orientation` / `keep_original_sound` (motion control), `draft` / `draft_id` (Seedance 2.5: a 480p preview, then the same shot at 1080p from the preview's job id), `webhook_url` (returns the job id at once and POSTs the result there; the secret comes from `webhook_secret` or the server's `OTTOPORT_WEBHOOK_SECRET`). |
20
+ | `ottoport_generate_video` | `prompt`, plus optional `model`, `duration`, `resolution`, `aspect_ratio`, `seed`, `image_url` (first frame), `last_frame_url`, `reference_image_urls`, `reference_video_urls` / `reference_audio_urls` (models listing `referenceMedia`), `video_url` (edit / extend, or the movement for motion control), `character_orientation` / `keep_original_sound` (motion control), `draft` / `draft_id` (Seedance 2.5: a 480p preview, then the same shot at 1080p from the preview's job id; the model `seedance-2.5-draft` drafts on every request without `draft`), `webhook_url` (returns the job id at once and POSTs the result there; the secret comes from `webhook_secret` or the server's `OTTOPORT_WEBHOOK_SECRET`). |
21
21
  | `ottoport_video_webhook` | `job_id`, plus `action` `status` (every delivery attempt and the endpoint's response) or `retry` (redeliver a finished job's webhook now). |
22
22
  | `ottoport_generate_speech` | `prompt` (the text), plus optional `model`, `voice`, `format`. |
23
- | `ottoport_generate_music` | `prompt`, plus optional `model`, `duration`, `format`. |
23
+ | `ottoport_generate_music` | `prompt`, plus optional `model`, `duration`, `format`. Sound effects too: `model` `mirelo-sfx-2.0` makes them from the prompt, or with `video_url` a soundtrack synced to that video, dialogue kept (prompt then optional). It bills per requested second, so set `duration` to the clip's length. |
24
24
  | `ottoport_list_collections` | The project's collections (ids, asset counts) and whether this account may save. Read it to find the collection the user means. |
25
25
  | `ottoport_create_collection` | `name`, plus optional `description`, `external_id`. |
26
26
  | `ottoport_list_assets` | Saved files, optionally by `collection` and `kind`. |
@@ -90,7 +90,7 @@ sensible default.
90
90
  | image | `gpt-image-2` | `nano-banana-2.1` (fast generation and edits), `nano-banana-pro` (high fidelity) |
91
91
  | video | `kling-3.0` | `seedance-2.0-fast` (cheap), `veo-3.1` (with audio) |
92
92
  | speech | `gpt-4o-mini-tts` | `eleven-v3` |
93
- | music | `suno-v5` | `lyria-2` |
93
+ | music | `suno-v5` | `lyria-2`, `mirelo-sfx-2.0` (sound effects, or a soundtrack for a video) |
94
94
 
95
95
  Never invent a model id. Call `ottoport_list_models` when unsure — the catalog
96
96
  changes, and a wrong id is a failed billable call. Rates come back in credits (the unit the ledger