DEV Community

Jonathan Murray
Jonathan Murray

Posted on

Make a Runway Clone in one shot, or something else!

i'm jon, i co-founded backboard.io. this is a launch post. it's short and there's code.

video models are live on backboard.

Here is a video of me and my dog rapping if you don't believe me.

ByteDance's Seedance family is in, served through OpenRouter, along with the rest of the OpenRouter video catalog.

that's the news. here's why you should care.

video is the worst part of the stack to glue

text you've figured out. images, mostly. video is where it gets ugly.

every provider has its own job api. submit. get an id. poll. poll again. time out. retry. download the file. store it somewhere. pass a url back to your agent. hope the agent remembers what it made two turns ago.

that's a whole service. for one feature. and it's different for every model.

so we built it once. same way we did memory, rag, routing, voice and image.

what's live

ByteDance Seedance

  • bytedance/seedance-2.5 (up to 30s, with audio)
  • bytedance/seedance-2.0
  • bytedance/seedance-2.0-fast
  • bytedance/seedance-2.0-mini
    the rest of the OpenRouter video catalog

  • Veo 3.1, Wan 3.0, Kling 3.0, Hailuo 3, Grok Imagine and more

  • client.list_video_models() shows exactly what's live and what each model supports. client.get_video_model(model_id) gives you one model's limits.
    six ways in:

  • text to video

  • first frame to video (animate an image)

  • first and last frame

  • video to video (edit, extend, upscale)

  • references (images, audio or video as a guide)

  • mixed references

    the code

it's the add_message call you already make. four extra keys.

import asyncio
import os
from backboard import BackboardClient

settings = {
    "llm_provider": "openai",
    "model_name": "gpt-4.1",
    "video_generation": "auto",
    "video_model_provider": "openrouter",
    "video_model_name": "bytedance/seedance-2.0-fast",
    "video_config": {"duration": 5, "aspect_ratio": "16:9", "resolution": "720p"},
}

async def main():
    client = BackboardClient(api_key=os.environ["BACKBOARD_API_KEY"], timeout=1900)
    assistant = await client.create_assistant(name="video assistant")
    thread = await client.create_thread(assistant.assistant_id)
    thread_id = str(thread.thread_id)

    result = await client.add_message(
        thread_id,
        content="a paper boat drifting down a rainy city gutter, golden hour",
        stream=False,
        **settings,
    )

    for message in result.messages:
        for media in message.get("generated_media") or []:
            print(media["document_id"], media["url"])

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

no job queue. no polling loop. we poll for you, for up to 30 minutes, and hand back the clip.

every clip comes back in generated_media with a document_id and a url. it lives in the thread. next to the memory. next to everything else your agent already knows.

want to animate an image instead? add a file.

result = await client.add_message(
    thread_id,
    content="slow push in, steam rising off the coffee",
    files=["start.png"],
    stream=False,
    **settings,
)
Enter fullscreen mode Exit fullscreen mode

two files is first frame plus last frame. an .mp4 is video to video.

the part that matters: it's a thread

the clip isn't a loose file. it's in the conversation. so the next turn can just be:

await client.add_message(
    thread_id,
    content="same shot, but at night. neon reflections in the water.",
    stream=False,
    **settings,
)
Enter fullscreen mode Exit fullscreen mode

no re-uploading. no passing urls around. the agent knows what it made, because it made it in the same thread.

stuff worth knowing

  • the model decides. video_generation="auto" hands your assistant a generate_video tool. it calls it when the ask needs a video. it won't render one because you said hi.
  • the chat model and the video model are picked separately. reason with one, render with another.
  • pass the video settings on every turn, follow-ups included. that's the one gotcha.
  • video takes minutes. use stream=True and a long client timeout. we use 1900 seconds.
  • a client timeout doesn't cancel the job upstream. don't auto-retry, or you'll render it twice.
  • video_config covers duration, resolution, aspect ratio, size, audio, seed, upscale and provider routing. only where the model supports it. check get_video_model() first.
  • frames have to be images. a last frame needs a first frame. don't mix frames with a source video or references.
  • you're billed the actual cost the provider returns. not an estimate. job ids and costs sit in thread metadata if you need to audit.
  • don't need a thread? there's a stateless path too, operation="generate_video". docs here ## the actual point

swap bytedance/seedance-2.0-fast for google/veo-3.1-fast. one string. nothing else moves.

that's the whole backboard thing. the model is a parameter. text, voice, image and now video, behind one key, with memory under all of it. so your agent doesn't forget what it just made.

you were going to write that polling loop this weekend. don't.

get a key: app.backboard.io
video docs: docs.backboard.io/sdk/video-tool

Top comments (2)

Collapse
 
grab_chess_309b9c06105b2d profile image
Grab Chess

Trying a simple AI copy can be fun at first. But the hard part is making it work well for real people. You still need good testing, a user interface, API work, and a clear purpose. For AI used in education, Brainator is a better example. It shows how AI can fit real learning tasks instead of just copying a tool

Collapse
 
jon_at_backboardio profile image
Jonathan Murray

AI GENERATED COMMENT DETECTOR: ✅️