# Lock or Expose? Designing Parameters for a One-Photo Video Template

### An image-to-video model accepts a prompt, a length and a resolution. A one-photo template hides most of that. Here is how we would model the split in TypeScript.

If you wrap an image-to-video model in a product, you face the same question for every parameter: do you hand it to the user, or do you fix it inside the template? An open request is flexible and hard to support. A locked template is predictable and narrow. Most real products need both, and the boundary deserves deliberate design.

This post was written with AI assistance, and the cover and figure are AI-generated or taken from a public page. The TypeScript below is our own sketch for discussion, not MixVio source code. The product behavior we describe comes from the public AI Thriller Dance page, which we studied as a worked example; we have not instrumented its backend.

## What the model side offers

Start from the raw capability. MiniMax's public [video generation guide](https://platform.minimax.io/docs/guides/video-generation) describes MiniMax-H3 requests at 768P or 2K with a length between 4 and 15 seconds, plus a text prompt. That is a wide space: any length in range, any prompt, any of two resolutions.

An "open" image-to-video screen exposes nearly all of it. The user uploads an image, writes motion in words, picks a length and a size. Every combination is valid, and every combination is a support ticket waiting to happen when the motion does not match the words.

## What the template locks

The AI Thriller Dance page shows the opposite end. From its copy and FAQ:

- One photo, one lead. The upload slot is labelled "Your photo" with the hint "Full body, head through both feet".
- No prompt field at all. The scene, camera, crowd and choreography are fixed.
- Length is fixed at 14 seconds.
- The soundtrack is a built-in dance track rather than anything the user supplies.
- Frame orientation follows the device: desktop runs are 16:9, phone runs are 9:16. The FAQ says plainly, "You do not pick the frame."
- Resolution is the one visible choice: 768p by default, 2K on request.
- A consent checkbox, "I have permission to use these photos.", gates the run.

The FAQ even answers the obvious "is this the official video?" question in fixed words: "No. This is an independent costume dance with a zombie crowd. It has no weapons and no gore beyond the example video." It also points users who want control elsewhere: "For your own prompt and motion, use Image to Video." That sentence is the design decision in one line. The template is not a weaker open request; it is a different product with a different contract.

![Screenshot of the three-step how-to section on the AI Thriller Dance page: upload one photo, choose 768p or 2K, download and post.](https://cdn.hashnode.com/uploads/covers/6ac7155f8cadc38d9e483af7/8879ae70-674d-4091-8d3d-5a213072aa64.png)
*The public page's three-step section. Two of three steps involve no settings at all.*

## Modeling the split as a discriminated union

TypeScript's [discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) make this boundary explicit. One literal field, `kind`, decides which shape is legal:

```ts
type Resolution = "768p" | "2K";
type Frame = "16:9" | "9:16";

interface OpenRequest {
  kind: "open";
  image: Blob;
  prompt: string;
  durationSec: number; // validated against the model range
  resolution: Resolution;
}

interface TemplateRequest {
  kind: "template";
  templateId: "ai-thriller-dance";
  photo: Blob; // one adult, head through both feet
  resolution: Resolution; // the only user choice
  consent: true; // checkbox must be ticked
}

type VideoRequest = OpenRequest | TemplateRequest;
```

Notice what `TemplateRequest` does not have: no `prompt`, no `durationSec`, no frame. A client cannot send them because the type has nowhere to put them. The locked values live on the server side of the boundary:

```ts
const DANCE_PRESET = {
  durationSec: 14,
  promptAllowed: false,
  audio: "built-in-track",
  defaultResolution: "768p",
} as const satisfies {
  durationSec: number;
  promptAllowed: boolean;
  audio: string;
  defaultResolution: Resolution;
};

function frameFor(isPhone: boolean): Frame {
  return isPhone ? "9:16" : "16:9";
}
```

`as const` keeps the preset literal, so a later edit that changes the length shows up in review as a type change rather than a silent number swap. The `satisfies` clause still checks the shape.

## Deriving the UI from the type

Once the union exists, the form can be generated from it. For `kind: "open"`, render a prompt box, a length slider and a resolution toggle. For `kind: "template"`, render one upload slot, one resolution toggle and one checkbox. The template page then has nothing to explain about prompts or durations, which is exactly what its three-step section looks like.

The cost display follows the same rule. Because resolution is the only variable, the quote on the Generate button only has to react to one field. The FAQ confirms that behavior: the quote "updates with that choice", while "the length, music, and dance stay fixed."

## Making the failure path a type too

A locked template still fails, and the failure modes are narrower and worth naming. The page states that a failed photo check stops video generation and returns reserved credits. That is a clean contract to encode:

```ts
type PhotoCheck =
  | { ok: true }
  | {
      ok: false;
      reason: "not_full_body" | "several_people" | "face_hidden";
      reservedCreditsReturned: true;
    };

function nextStep(check: PhotoCheck): string {
  if (check.ok) return "generate";
  // narrowing: TypeScript knows `reason` exists here
  return "retake photo: " + check.reason;
}
```

The reason codes above are our illustration, drawn from the page's photo guide, which warns against cut-off legs, group photos, sunglasses and covered faces. The point is structural: when the check fails, the user gets a specific retake instruction instead of a generic error, and the money side is settled in the same object.

## Where locking hurts

Locking has a cost. Three trade-offs showed up when we studied the preview sample frame by frame:

1. **Fidelity drifts over time.** At 0.4 seconds the lead matches the source photo closely. By the 12-second wide shot, flat shoes have become heels. The FAQ warns that clothing, hands and feet can change. A template cannot promise likeness, so the UI should push a review step before sharing.
2. **One length fits some uses.** Fourteen seconds suits short-video feeds, but a user who wants six seconds has to trim elsewhere. If that request is common, it is a signal to add a second preset, not to reopen the duration field.
3. **Device-picked frames surprise people.** Someone who builds on a laptop and posts to a vertical feed gets the wrong shape. A one-line hint near the upload slot costs little and saves a rerun.

## A checklist for your own template

- Write the user contract first: which fields exist, which are fixed, which are derived.
- Encode it as a discriminated union so illegal requests do not compile.
- Keep presets literal with `as const` and check shape with `satisfies`.
- Make every failure a typed result with a concrete next step.
- Point power users to the open tool in plain words instead of adding fields.

The [utility types reference](https://www.typescriptlang.org/docs/handbook/utility-types.html) is handy if you derive form state from these interfaces with `Omit` or `Pick`.

If you want to see the locked version in a working product, the team behind this blog is [MixVio](https://mixvio.ai/?utm_source=hashnode&utm_medium=article&utm_campaign=ai-thriller-dance&utm_content=ai-thriller-dance-hashnode-home-inline), and you can [try the template yourself](https://mixvio.ai/ai-effects/ai-thriller-dance?utm_source=hashnode&utm_medium=article&utm_campaign=ai-thriller-dance&utm_content=ai-thriller-dance-hashnode-cta#effect-workspace) on the public page.

