npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@martini-film/client

v0.10.0

Published

The official JavaScript and TypeScript client for the Martini API: generation, workflow runs, projects and canvases

Downloads

754

Readme

@martini-film/client

The JavaScript and TypeScript client for the Martini API: generate media, and run saved workflows.

npm install --save @martini-film/client

Requires Node.js 18 or later.

import { createMartiniClient } from '@martini-film/client'

const martini = createMartiniClient({
  apiKey: process.env.MARTINI_API_KEY,
})

const result = await martini.subscribe('nano-banana-2', {
  input: {
    prompt: 'A practical miniature moon base photographed on 35mm film.',
  },
  projectId,
  canvasId,
  placement: 'auto',
  idempotencyKey: 'shot-42',
})

console.log(result.data.images[0].url)
console.log(result.data.olive_cost)
console.log(result.data.martini.preview_url)

The client provides typed model IDs and an asynchronous queue interface for generation jobs. Submit and status responses include the snapshotted olive_cost once pricing is available, and result responses always include it. An overlapping retry of the same idempotent submission can briefly omit the field while the original request is still being priced. The snapshotted cost does not change if model pricing changes later. Non-blocking input advisories are returned in martini.warnings.

Seedance 2.0 Base, Fast, and Mini expose text, image, and reference-to-video endpoints under bytedance/seedance-2.0, bytedance/seedance-2.0/fast, and bytedance/seedance-2.0/mini. Seedance 2.5 exposes the same three modes under bytedance/seedance-2.5, with 4–30 second takes and Auto duration.

Grok Imagine 1.5 exposes typed text, image, and reference-to-video endpoints under xai/grok-imagine-video/v1.5. Text and image modes support up to 1080p; reference mode accepts up to seven images and one optional audio reference at 480p or 720p.

MiniMax H3 and H3 Max expose typed text, image, and reference-to-video endpoints under minimax/h3 and minimax/h3-max. Reference mode accepts up to nine image, three video, and three audio references; H3 allows all 15 files together while H3 Max caps the combined total at 12. H3 Max Turbo (preview) exposes text and image-to-video only under minimax/h3-max-turbo, with H3 Max's contract at half the cost.

Wan 3.0 and Wan 3.0 Prime expose typed text, image, and reference-to-video endpoints under alibaba/wan-3.0 and alibaba/wan-3.0-prime. All three modes support 480p, 720p, and 1080p output with optional native audio. The Prime contract does not expose prompt expansion, enhanced reasoning, or a provider safety-checker toggle because its provider has no corresponding controls.

Queue URL fields returned from queue.submit(), queue.status(), and onQueueUpdate use the configured Martini API domain, such as https://api.martini.film/fal/queue/.... The SDK keeps the Fal-compatible proxy details internal. Direct draft lifecycle calls use Martini-owned URLs under https://api.martini.film/generation-api/requests/.

projectId and canvasId optionally override the API key's default project and canvas. If projectId is supplied without canvasId, Martini uses the project's first canvas. A canvasId override requires projectId. placement currently accepts auto. Use idempotencyKey to safely retry the same logical submission.

Projects and canvases

martini.projects lists where a generation, an upload, or a workflow run can land: the projects of your organization that the key's user can see, and each project's canvases. It is the same set the MCP connector's get_projects shows them, so an id picked here works everywhere a projectId or canvasId is accepted.

const { projects } = await martini.projects.list({ query: 'Pilot' }) // ranked: exact name or id first
const pilot = projects.find(project => project.canEdit) // canEdit: false means read-only for this key

const { canvases } = await martini.projects.canvases(pilot.id)
const canvasId = canvases.find(canvas => canvas.isDefault)!.id // where the project opens

await martini.generations.submit('nano-banana-2', { input: { prompt: '…' }, projectId: pilot.id, canvasId })

list() returns the most recently joined projects first (limit 1–50, default 20; truncated says whether more matched) and ranks them when query is given: an exact id or name first, then a name that starts with, contains, or holds every word of the query, then an id fragment. exactName: true keeps only exact name (or id) matches. Every project carries openInMartini, a link to it in the app, and visibility (private, link_view, or link_edit). A project the key cannot read, or one from another organization, answers PROJECT_NOT_FOUND (404). canvases() answers PROJECT_DOCUMENT_TOO_LARGE (409, durable — retrying will not help) when the project's document is too large to open, and PROJECT_DOCUMENT_UNAVAILABLE (503, retry) when it could not be read.

subscribe() and queue.submit() generate immediately by default. To create an editable draft without spending olives or contacting a provider:

const draft = await martini.queue.submit('nano-banana-2', {
  input: {
    prompt: 'A practical miniature moon base photographed on 35mm film.',
  },
  mode: 'draft',
  projectId,
})

await martini.queue.generate({ requestId: draft.request_id })

Draft generation uses the latest compatible settings saved on the Martini canvas. Raw @fal-ai/client callers can create the same draft by adding the X-Martini-Mode: draft submit header.

Workflows

A workflow is a chain of Actions saved from a Martini canvas; a run is one execution of it. Runs of a saved workflow start from empty bins, so pass asset ids for every input bin; the outputs land in your workspace's "Workflow runs" project (or in a projectId you name). The same API key works for generation and workflows.

const { workflows } = await martini.workflows.list() // saved workflows: steps, inputs, outputs, fingerprint
const reference = await martini.assets.upload(file, { wait: true }) // lands in your API Generations project

const { run, results } = await martini.workflows.subscribe(workflowId, {
  variables: { Script: 'INT. LAB - NIGHT. Sophie reads the results.' },
  bins: { References: [reference.assetId] },
  onUpdate: run => console.log(run.status, run.olives.generation),
})

for (const output of results.outputs) {
  if (output.status === 'ready') console.log(output.filename, output.url)
}

subscribe() starts the run, polls its status with jittered backoff until it settles (completed, failed, or cancelled), then fetches the results. The pieces are also available separately:

const run = await martini.workflows.run(workflowId, {
  variables: { Script: '…' },
  bins: { References: [reference.assetId] },
  fingerprint: workflow.fingerprint, // optional: refuse to run if the workflow changed since you read it
  idempotencyKey: 'job-42',
})
const status = await martini.runs.status(run.id) // status.phase: what Martini is doing right now
const detailed = await martini.runs.status(run.id, { activity: true }) // adds each step's activity trail as events
const results = await martini.runs.results(run.id) // the takes settled so far; results.status says whether more may come
const settled = await martini.runs.wait(run.id) // poll an existing run, then fetch results
const resumed = await martini.runs.resume(run.id) // retry a failed run's failed step with the same inputs (409 unless failed)
const me = await martini.me() // connection check: your organization and key label

workflows.list({ all: true }) appends the placed workflows you can run in place (copies on canvases you edit), each with its current canvas inputs; workflows.run(placedId, …) then reads the canvas bins you do not pin. workflows.create({ from, projectId }) places a copy of a saved workflow on a canvas of your choosing.

Every workflow and run carries a fingerprint: the content identity of its machinery. Pass the one you read to run() and Martini answers WORKFLOW_CHANGED (with the current fingerprint) instead of running something else.

Input keys are names (case-insensitive) or ids. A key the workflow does not have rejects with WORKFLOW_VARIABLE_NOT_FOUND or WORKFLOW_BIN_NOT_FOUND; a saved workflow's input bin you left out with WORKFLOW_BIN_REQUIRED; an asset you cannot read with WORKFLOW_ASSET_NOT_FOUND. Each body lists the accepted inputs. The run echoes what it pinned under run.variables and run.bins.

Output urls are presigned downloads valid for expiresIn seconds; fetch them promptly rather than storing them. oliveBudget is optional: when omitted, Martini arms the spend rail per action from the plan estimate; when set, a plan estimated over it fails the run with the estimate in run.error. Runs started over the API auto-approve, so nobody has to click anything in the app.

engine: 'fable' (Workflow Beta) runs the whole workflow in one autonomous session instead of the per-Action harness. It needs the workflow_beta grant for your organization and an oliveBudget; resume is refused on such runs, start a new one instead. Omit it for the default engine.

Martini-native generation routes

martini.generations is the Martini-native way to generate: the same models, the same key, the same billing and canvas placement as subscribe()/queue.*, over /v1/generations instead of the fal-compatible queue. One resource describes a generation from draft to terminal state and carries output once status is completed, so a caller polls one URL. Statuses are draft, pending, running, completed, failed, and cancelled.

const generation = await martini.generations.subscribe('bytedance/seedance-2.5/text-to-video', {
  input: { prompt: 'A practical miniature moon base at blue hour.', duration: 'auto', resolution: '720p' },
  projectId,
  idempotencyKey: 'shot-42',
  onUpdate: update => console.log(update.status),
})

if (generation.output) console.log(generation.output.video.url, generation.oliveCost)
else console.log(generation.status, generation.error) // failed or cancelled: returned, not thrown

subscribe() submits, polls with jittered backoff until the generation settles, and returns it — a failed or cancelled generation is returned with its error, not thrown. The pieces are also available separately:

const submitted = await martini.generations.submit('nano-banana-2', { input: { prompt: '…' }, mode: 'draft' })
const started = await martini.generations.generate(submitted.id) // a draft never settles until it is started
const current = await martini.generations.status(submitted.id) // output is on this object once completed
const settled = await martini.generations.wait(submitted.id) // poll an existing generation until it settles
const cancelled = await martini.generations.cancel(submitted.id)
const catalog = await martini.models() // the aliases this key can invoke, with your negotiated pricing

projectId, canvasId, placement, idempotencyKey, and mode are body fields on this surface; the fal-compatible queue carries the same options as X-Martini-* headers. Errors are MartiniApiErrors with the same stable codes as the queue (for example INSUFFICIENT_OLIVES, RATE_LIMITED, REQUEST_ALREADY_TERMINAL).

Martini-native routes (generations.*, models(), projects.*, workflows.*, runs.*, me(), queue.generate(), assets.*) reject with MartiniApiError, which carries status, the stable code, and the parsed body:

import { MartiniApiError } from '@martini-film/client'

try {
  await martini.runs.status(runId)
} catch (error) {
  if (error instanceof MartiniApiError && error.code === 'WORKFLOW_RUN_NOT_FOUND') {
    // unknown run, or one from another organization
  }
}

The Fal-compatible queue calls (subscribe(), queue.submit(), queue.status(), queue.result(), queue.cancel()) keep throwing ApiError.

Reference-to-video binding

Reference arrays are positional, and array order determines each 1-based token. Image bindings are recommended; multiple-video and all audio bindings are required:

| Input field | Prompt token | Meaning | | --- | --- | --- | | image_urls | @ImageN | @Image1 is the first image URL. | | video_urls | @VideoN | @Video1 is the first video URL. | | audio_urls | @AudioN | @Audio1 is the first audio URL. |

Each media type has its own numbering. Bare names and labels do not bind media: write @Image1 is Norman, not NORMAN. One URL is one reference, so a contact sheet remains one image rather than one reference per panel. If one sheet repeats a person across views, state that every panel depicts the same single character.

const result = await martini.subscribe('bytedance/seedance-2.5/reference-to-video', {
  input: {
    prompt:
      '@Image1 is the lead character. @Image2 is the laboratory. Keep exactly one instance of @Image1. Inside @Image2, @Image1 dodges the attack. Follow the camera movement from @Video1 and time the impact to @Audio1.',
    image_urls: [characterUrl, locationUrl],
    video_urls: [cameraGuideUrl],
    audio_urls: [impactAudioUrl],
    duration: '8',
    resolution: '720p',
    aspect_ratio: '16:9',
    generate_audio: true,
  },
})

Only tagged audio references are used; generate_audio independently controls generated audio. Image tokens are recommended but not required: unbound images are still submitted, and the response reports the advisory in martini.warnings, because Seedance may not use them. Multiple video references and every audio reference must have matching tokens. Out-of-range tokens and duplicate URLs fail validation. Seedance 2.0 requests accept up to 9 images, 3 videos, and 3 audio files, with 12 files total. Seedance 2.5 requests accept up to 30 images, 10 videos, and 10 audio files, with 50 files total. Use image-to-video for start/end frames and reference-to-video for reference arrays.

Keep the action achievable within the selected duration. Split prompts with many cuts, dialogue lines, entrances, and impacts into separate generations when their order must remain clear.

Base64 media inputs

Media URL fields accept public HTTPS URLs, Martini-hosted URLs, or base64 data URLs. Put the data URL directly in the normal field; there is no separate base64 property or upload call.

const result = await martini.subscribe('bytedance/seedance-2.0/reference-to-video', {
  input: {
    prompt: 'Follow the camera movement from @Video1.',
    video_urls: [`data:video/mp4;base64,${videoBase64}`],
  },
})

Limits apply to decoded or fetched media: 10 MB per image, 30 MB per MP4 video reference, and 15 MB per MP3 or WAV reference. The complete request body is limited to 72 MiB. Base64 adds roughly 33% overhead, so use HTTPS URLs for large or multiple references. Binary File and Blob values are not uploaded automatically.

Supported methods:

  • subscribe()
  • queue.submit()
  • queue.status()
  • queue.result()
  • queue.generate()
  • queue.cancel()
  • assets.upload()
  • assets.prepareUpload()
  • assets.completeUpload()
  • assets.get()
  • assets.uploadBase64()
  • projects.list()
  • projects.canvases()
  • workflows.list()
  • workflows.get()
  • workflows.create()
  • workflows.run()
  • workflows.subscribe()
  • runs.status()
  • runs.results()
  • runs.wait()
  • generations.submit()
  • generations.status()
  • generations.cancel()
  • generations.generate()
  • generations.wait()
  • generations.subscribe()
  • models()
  • me()

The API key is a server-side secret. Do not embed it in browser bundles or public environment variables.

Upload reusable project assets

Uploads create visible canvas assets in the API key's default project and canvas. Use the returned stable Martini URL in later generation requests:

const asset = await martini.assets.upload(file, {
  filename: 'reference.mp4',
  contentType: 'video/mp4',
  projectId,
  canvasId,
})

const result = await martini.subscribe('bytedance/seedance-2.0/reference-to-video', {
  input: {
    prompt: 'Follow the camera movement from @Video1 for a nighttime city shot.',
    video_urls: [asset.url!],
  },
})

For large files, assets.upload() performs the same explicit flow you can call manually: prepare a create-only presigned upload URL, PUT bytes directly to Martini storage, complete the upload, then poll asset status with jittered backoff until processing finishes. Completion can return queued while Martini waits for a bounded upload-processing worker. Upload destination overrides must provide projectId and canvasId together.

Changelog

0.10.0

  • Breaking: needs_revision is a terminal MartiniWorkflowRunStatus beside completed, failed, and cancelled: the agent stopped on purpose because the inputs fail a precondition the brief sets. The same value is the status of the stopped action and of each item the agent ended (the former declined item status is gone). Earlier versions keep polling such a run until their timeout — upgrade before Martini ships the status. outcome and revision are unchanged; POST /v1/runs/{runId}/resume accepts a needs_revision run.
  • The status summary's outputs gains needsRevision, the count of units the agent ended that way.

0.9.0

  • martini.projects.list({ query?, exactName?, limit? }) (GET /v1/projects) lists the organization's projects the key's user can see, ranked by query, each with canEdit, visibility, and openInMartini. martini.projects.canvases(projectId) (GET /v1/projects/{projectId}/canvases) lists a project's canvases in the app's order with isDefault on the first. Use them to pick the projectId / canvasId for a generation, an upload, or a run. New types: MartiniProject, MartiniProjectList, MartiniCanvas, MartiniCanvasList, MartiniProjectVisibility, MartiniProjectListOptions, MartiniProjectClient.
  • New error codes from projects.canvases(): PROJECT_DOCUMENT_TOO_LARGE (409) and PROJECT_DOCUMENT_UNAVAILABLE (503).

0.7.1

  • The run object carries outcome on every action and item: null until settled, then generated, failed, or needs_revision. needs_revision means the agent stopped before generating because the inputs fail a precondition the brief sets; the item's revision ({ reason, inputRefs }) says what to change. The row's status stays failed, so existing polling loops need no change. New types: MartiniWorkflowOutcome, MartiniWorkflowRevision.

0.7.0

Breaking: the Workflows API now has two nouns, workflows and runs (no instances).

  • workflows.run(workflowId, options) runs a saved workflow directly; options.instance is gone. Pass bins for every input bin (WORKFLOW_BIN_REQUIRED otherwise) and, optionally, projectId / canvasId to choose where the run lands.
  • Run status and results moved to martini.runs.status(runId), martini.runs.results(runId), and martini.runs.wait(runId) (GET /v1/runs/{runId}[/results]). workflows.subscribe() is unchanged.
  • workflows.list({ all: true }) appends placed workflows; workflows.create({ from, projectId }) places a copy.
  • The run object: workflowId now names the placed copy that ran (only for copies you can see), the saved workflow is savedWorkflow { id, version }, and every workflow and run carries a fingerprint; pass it to run() to get WORKFLOW_CHANGED when the workflow changed since you read it. openInMartini links to the run's canvas.
  • Removed error codes: WORKFLOW_INSTANCE_NOT_FOUND, WORKFLOW_INSTANCE_AMBIGUOUS. Added: WORKFLOW_CHANGED, WORKFLOW_BIN_REQUIRED, WORKFLOW_CREATE_REJECTED, WORKFLOW_CREATE_FAILED, PROJECT_NOT_FOUND.

0.6.0

  • workflows.run() accepts per-run variables and bins.

0.5.0

  • First release with workflows.* and me().