
Generate Video Scripts and Shot Lists With an LLM API (Python Tutorial)
Build a small Python pipeline on the MiMo API: turn a topic into a timed script and a JSON shot list, check it against your video model's clip lengths, and render the narration with MiMo-V2.5-TTS.
If you publish short videos regularly, the slowest step is often not the video generation. It is the planning: writing a script that fits 30 seconds, splitting it into shots, and writing a prompt for each shot that a video model can actually render.
That planning is a good job for a language model, as long as you ask for structured output and check it before using it. In this tutorial you will build a small Python script that:
- Turns a topic into a narrated script and a shot list in JSON.
- Validates the timing against the clip lengths your video model supports.
- Writes the narration to audio with a text-to-speech model.
- Saves one ready-to-paste prompt per shot.
Everything runs through the MiMo API's OpenAI-compatible endpoint, so you can use the official openai Python SDK.
What you need
- Python 3.9 or newer and
pip install openai. - A MiMo API key in an environment variable called
MIMO_API_KEY. The first API call guide shows where to create one. - A text-to-video tool to render the shots. Any model that takes a text prompt works. For example, Dolai's Seedance 2.5 video generator turns a prompt or a photo into a 5 to 30-second clip with its own sound in the browser, so you can paste the prompts this script produces straight in.
The base URL for the OpenAI-compatible endpoint is https://api.mimo-v2.com/v1.
Step 1: Pick a model
For planning, use a text model with strong reasoning. On the models page, MiMo-V2.5-Pro (mimo-v2.5-pro) is listed as the flagship for agent and tool-heavy work, with a 1M-token context window and a 128K output window. MiMo-V2-Pro (mimo-v2-pro) is also available if you already use it.
For the voiceover, use mimo-v2.5-tts, which takes the text to speak as an assistant message and returns audio.
Step 2: Ask for a script and shot list in JSON
The trick is to describe the exact JSON shape you want, give hard limits, and tell the model what each field is for. Shot prompts need to describe what is visible, not what the narrator says.
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["MIMO_API_KEY"],
base_url="https://api.mimo-v2.com/v1",
)
SYSTEM_PROMPT = """You plan short vertical videos.
Return only valid JSON, no markdown, in this shape:
{
"title": string,
"hook": string,
"shots": [
{
"id": integer,
"seconds": integer,
"narration": string,
"visual_prompt": string
}
]
}
Rules:
- Total seconds must equal the target length.
- Each shot's seconds must be one of the allowed clip lengths.
- Narration: at most 2.5 words per second of the shot.
- visual_prompt: one shot only. Describe subject, action, camera
movement, lighting and style. No on-screen text. No narration."""
def plan_video(topic, target_seconds=30, clip_lengths=(5, 10)):
user_prompt = (
f"Topic: {topic}\n"
f"Target length: {target_seconds} seconds\n"
f"Allowed clip lengths: {list(clip_lengths)}\n"
"Style: 3D animated, bright, family friendly."
)
response = client.chat.completions.create(
model="mimo-v2.5-pro",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
temperature=0.8,
max_completion_tokens=8000,
)
text = response.choices[0].message.content.strip()
# Tolerate a stray code fence around the JSON.
text = text.removeprefix("```json").removesuffix("```").strip()
return json.loads(text)Two details matter here. First, max_completion_tokens is set well above the length of the JSON you expect. According to the model parameters guide, this limit includes the model's internal reasoning tokens when thinking mode is on, so a tight limit can cut the answer off. Second, the temperature is slightly below the recommended default of 1.0. You still want creative scripts, but you also want the model to follow the format.
Step 3: Validate before you spend credits on video
Never send model output straight to a paid video render. Check it in code first:
def check_plan(plan, target_seconds, clip_lengths, words_per_second=2.5):
problems = []
total = sum(shot["seconds"] for shot in plan["shots"])
if total != target_seconds:
problems.append(f"Total is {total}s, expected {target_seconds}s")
for shot in plan["shots"]:
if shot["seconds"] not in clip_lengths:
problems.append(f"Shot {shot['id']}: {shot['seconds']}s is not allowed")
words = len(shot["narration"].split())
if words > shot["seconds"] * words_per_second:
problems.append(f"Shot {shot['id']}: {words} words is too long")
return problemsIf check_plan returns problems, send them back to the model in a follow-up user message ("Fix these problems and return the full JSON again") and validate once more. In practice one retry fixes most issues. If the second attempt still fails, stop and look at the prompt instead of looping.
The 2.5 words per second limit is a rough speaking pace for narration. Slow it to about 2 for children's content or calm explainers.
Step 4: Match the clip lengths to your video model
The clip_lengths argument is where most generated shot lists go wrong. Video models only accept certain durations, and these differ a lot between models: some offer 4, 6 or 8 seconds, others anything from 5 to 30. Before you run the planner, look up the exact options for the model you will use. A side-by-side table of video models with their clip lengths, resolutions and credit cost per second makes this quick, and you can copy the allowed durations straight into clip_lengths.
Choosing the target length also sets your video budget. Many video models charge per second, so the planner's total length is effectively the cost of the render. At per-second pricing the split does not change the total, so fewer, longer shots give each one more time to settle without costing more.
Step 5: Render the narration
With a valid plan, turn the narration into audio. Following the MiMo text-to-speech guide, the text goes in an assistant message and the audio comes back base64-encoded:
import base64
def render_narration(plan, out_dir="out"):
os.makedirs(out_dir, exist_ok=True)
for shot in plan["shots"]:
completion = client.chat.completions.create(
model="mimo-v2.5-tts",
messages=[{"role": "assistant", "content": shot["narration"]}],
audio={"format": "wav", "voice": "mimo_default"},
)
audio = base64.b64decode(completion.choices[0].message.audio.data)
path = os.path.join(out_dir, f"shot_{shot['id']:02d}.wav")
with open(path, "wb") as f:
f.write(audio)One file per shot keeps editing simple: each clip and its narration line up one to one on the timeline. If a line runs longer than its clip, you will see it immediately.
Step 6: Put it together
if __name__ == "__main__":
TARGET, CLIPS = 30, (5, 10)
plan = plan_video("Why do cats knock things off tables?", TARGET, CLIPS)
problems = check_plan(plan, TARGET, CLIPS)
if problems:
raise SystemExit("\n".join(problems))
render_narration(plan)
with open("out/shots.json", "w", encoding="utf-8") as f:
json.dump(plan, f, indent=2, ensure_ascii=False)
for shot in plan["shots"]:
print(f"[{shot['seconds']}s] {shot['visual_prompt']}\n")Run it, paste each printed prompt into your video tool with the matching duration, and drop the clips and WAV files into your editor.
Ideas for extending it
- Keep characters consistent. Add a
charactersarray to the JSON with one fixed description each, and tell the model to copy that description word for word into everyvisual_promptthat includes the character. - Batch topics. Loop over a list of topics overnight and review the shot lists in the morning.
- Use tools for stricter output. The chat completions endpoint accepts
toolsandtool_choice. Defining asave_planfunction with a JSON schema for its parameters is another way to get structured arguments back.
When you do not need a planner
Not every video needs a generated script. Trend formats with a fixed story beat are usually better served by single-purpose tools that already encode the structure. Zombie AI, for instance, makes the viral zombie love story from two uploaded photos without any prompt: one person has turned, the other cannot shoot, then a happy memory, with music included. A planner like the one above pays off when every video is different and the structure has to be invented each time.
Cost and next steps
A shot list is a short text answer, so planning calls are small next to the cost of rendering every second of video. API usage is paid with credit packages rather than a subscription; see the pricing page. Start with one topic and refine the system prompt until the shot lists need little editing, then scale up.
Author

Categories
Newsletter
Join the community
Subscribe to our newsletter for the latest news and updates
