Bring your own media service

The media processor calls an external media service and places the result on your timeline. This guide walks through wiring one up, whether the service you want already has a preset or not. Background on how the pieces fit together is in Media services.

1. Pick a preset, or plan to write a spec

Check whether the service you want to use already has a shipped preset (see the media processor reference for what's available). If it does, start there: a preset fills in the request shape for you, and you only need to add your key and a few variables.

If there is no preset for your service, you can still use it. media does not require a preset, it works from any inline spec you write, as long as the service is reachable over HTTP. The worked example below does exactly this, for a plain text-to-speech service that has no shipped preset.

When you write your own spec and the request body is JSON, interpolate any free text (like the prompt or script) through the json filter, not bare, so a value containing a quote, newline, or backslash still produces valid JSON: use "prompt": "{{ .Text | json }}", not "prompt": "{{ .Text }}". Shipped presets already do this for you; a hand-written spec does not unless you write it yourself.

2. Set the service's API key

Whatever service you use, the request needs to authenticate with it somehow, usually an API key. Add that key in the workspace's Variables & Service API Keys, under the name your spec (or preset) expects.

If a run needs a key that has not been set, it is refused before it starts, not partway through, so this step is worth doing before your first run.

3. Choose a source strategy

source controls how the request is built from your inputs. There are three strategies:

  • single builds one request from the text you wire into the step's source input. Point source at a step or file that contains the text you want turned into media, for example a script or voiceover line.
  • gaps scans a timeline level (set with gap_level) for empty spans and generates one request per gap, fitted to fill that span. See Filling gaps and generating per segment for a worked example.
  • plan reads a segment plan from a segment_plan input (a list of segments, each with its own text and duration) and generates one request per matching segment.

4. Choose a place strategy

place controls where the generated asset lands on your output timeline:

  • at_position places the asset at position 0 on the output timeline.
  • chain places the asset immediately after whatever is already on that timeline, end to end.
  • gaps fits each generated clip exactly to the span it was created to fill, trimming it if it runs long.

Use chain when you are generating several pieces in sequence and want them to play back to back; use at_position when you are generating a single asset for a fixed spot; use gaps together with source: gaps (or source: plan when the plan targets specific spans) so each clip lands in the exact hole it was generated for.

5. Run it

Add a review gate if you want to approve the result before it is finalized, the same as any other step. Steps cache their output by default, including this one: the same request against the same inputs produces the same asset, so a re-run without changes will not call the service again.

Worked example: a service with no shipped preset

Say you want to use a fictional text-to-speech service, voxcast, that has no shipped preset. It authenticates with a bearer token, accepts a simple JSON body, and returns the audio bytes directly in the response. You describe it with an inline spec:

- id: narration
  processor: media
  input:
    - id: source
      step: script            # step or file input providing the text to speak
  params:
    spec: |
      transport: http
      auth:
        header: Authorization
        value: 'Bearer {{ secret "VOXCAST_API_KEY" }}'
      request:
        method: POST
        url: 'https://api.voxcast.example/v1/speak'
        body: '{"text":"{{ .Text | json }}","voice":"{{ .Voice }}"}'
      response:
        kind: bytes
    vars:
      Voice: "narrator-warm-1"
    source: single
    place: chain
  output:
    timeline: main
  review: true
  cache: true

What each part is doing:

  • spec describes voxcast completely: how to authenticate (a bearer token read from a workspace secret named VOXCAST_API_KEY), what request to send (a POST with the spoken text and a voice name), and how to read the response (the audio comes back as raw bytes).
  • {{ .Text }} is filled in from the source input automatically; you never type your script into the spec itself.
  • vars supplies Voice, a value the request body needs that is not part of your source text. Add whatever extra values your service's request needs the same way.
  • place: chain lands the generated narration right after anything already on the main timeline, useful when you are stitching together several generated lines in order.

Before running this, add VOXCAST_API_KEY under the workspace's Variables & Service API Keys. Nothing else about this step differs from using a shipped preset: the same source, place, vars, review, and cache options apply either way.

Worked example: generating images with a preset

You can also use a preset that already knows how to talk to a service. The imagen preset generates a still image from your text with Google's Imagen and animates it into a vertical motion clip:

- id: cover_image
  processor: media
  input:
    - id: source
      step: script
  params:
    preset: imagen
    vars:
      Model: imagen-4.0-generate-001
    source: single
    place: at_position
  output:
    timeline: broll
  cache: true

Set GOOGLE_API_KEY in the workspace's Variables & Service API Keys before running this. The preset requests a still image from the service, then automatically animates it into a moving vertical clip before it lands on your timeline, so you never have to write your own post-processing step.

Generate video

The veo preset generates a short video clip from your text with Google's Veo:

- id: b_roll
  processor: media
  input:
    - id: source
      step: script
  params:
    preset: veo
    source: single
    place: chain
  output:
    timeline: broll
  cache: true

Set GOOGLE_API_KEY in the workspace's Variables & Service API Keys before running this. Video generation does not come back right away: the request kicks off a job on the service, and the step waits, checking in periodically, until the finished clip is ready before placing it on your timeline. You do not need to do anything differently for this, the step behaves like any other from the outside, it just takes longer to finish.

Generate avatar presentations

The heygen preset generates a video of a digital avatar presenting your script with HeyGen:

- id: avatar_video
  processor: media
  input:
    - id: source
      step: script
  params:
    preset: heygen
    vars:
      AvatarID: "avatar_name_1"
      VoiceID: "en_us_001"
    source: single
    place: at_position
  output:
    timeline: main
  cache: true

Set HEYGEN_API_KEY in the workspace's Variables & Service API Keys before running this. Like video generation, avatar presentation does not come back right away: the step starts a background job with the service and waits, checking in periodically, until the finished video is ready before placing it on your timeline.

Next