DEV Community

Cover image for How I Built an AI Product Ad Pipeline with Claude Code and AIHubMix
AIHubMix
AIHubMix

Posted on Edited on

How I Built an AI Product Ad Pipeline with Claude Code and AIHubMix

The goal was straightforward: start with one product image, generate three references of the same character, and create a 20-second, 720p vertical ad.

The engineering work happens between the model calls: passing character identity from one image to the next, registering a photorealistic virtual person as a usable asset, recovering asynchronous tasks, and ensuring that multiple asset IDs reach the video endpoint as separate arguments.

This article breaks down a working Python implementation built on AIHubMix. Claude Code helped organize prompts and execute the workflow; the script sent every API request through AIHubMix.

AIHubMix is useful here for more than sharing one Base URL. It covers most mainstream models through one API service, so the image stage can use Seedream, the video stage can switch to Seedance, and later versions can select another model without rebuilding authentication and request entry points for every supplier.

AIHubMix connects to multiple suppliers and automatically selects an available low-latency route, reducing the effect of a single supplier's instability. For asynchronous image and video jobs, one entry point is easier to maintain and fits a multi-stage workflow orchestrated with Claude Code. Actual model availability, routes, and latency still depend on the platform state at request time.

The code, model IDs, and schema details come from a project snapshot dated September 16, 2026. Check the current API before reusing them. No online generation API was called while preparing this publication package.

Download the Code and Run the Shortest Path

Download the Complete Starter

You need Python 3, your own AIHubMix API key, access and quota for the required models, and public URLs for the three reference images. The script uses only the Python standard library; no SDK installation is required.

After extracting the package, enter seedance-starter and run:

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

Edit .env and add your key. Upload assets/frame-01.jpg, assets/frame-02.jpg, and assets/frame-03.jpg to your own public image hosting, then replace these three URLs:

REF_A='https://your-host.example/frame-01.jpg'
REF_B='https://your-host.example/frame-02.jpg'
REF_C='https://your-host.example/frame-03.jpg'
python3 flow.py check "$REF_A" "$REF_B" "$REF_C"
python3 flow.py assets "$REF_A" "$REF_B" "$REF_C"
Enter fullscreen mode Exit fullscreen mode

After the assets become active, read their saved IDs and generate the video:

python3 - <<'PYCODE'
import json
import subprocess
import sys
from pathlib import Path

state = json.loads(Path("state.json").read_text())
refs = ["asset://" + asset_id for asset_id in state["asset_ids"]]
subprocess.run([sys.executable, "flow.py", "video", *refs], check=True)
PYCODE
Enter fullscreen mode Exit fullscreen mode

The completed result is saved to out/video.mp4. This shortest path reuses the included images and covers asset registration plus video generation. To regenerate the images for another product, follow the dependency chain below.

1. Model the Workflow as Explicit State

Product image and prompts
  → three character reference images
  → publicly accessible image URLs
  → active virtual-portrait assets in one group
  → asset:// references
  → video task ID
  → completed
  → local MP4
Enter fullscreen mode Exit fullscreen mode

The script exposes one command for each state transition:

Command Responsibility
stills Generate and download the reference images in order
check URL... Verify HTTP 200 and an image Content-Type
assets URL... Create or reuse an asset group, register images, and wait for activation
video asset://... Create a video task, poll it, and download the result
status [ID] Resume polling or download an existing task
tasks List recent tasks when a create response was lost

Public image hosting is a manual step; the script does not push files to GitHub.

2. Image Generation Is a Dependency Chain

prompts.json defines these reference relationships:

A: prompt-a.txt → frame-01.jpg
B: product image + frame-01.jpg + prompt-b.txt → frame-02.jpg
C: product image + frame-02.jpg + prompt-c.txt → frame-03.jpg
Enter fullscreen mode Exit fullscreen mode

A establishes the person, wardrobe, setting, and lighting. B introduces the product. C introduces its use action. B and C both reference the original product image to constrain packaging.

To switch products, replace assets/product.png, update the runtime prompts, back up and move the three old reference images, then run:

python3 flow.py stills
Enter fullscreen mode Exit fullscreen mode

Existing image files are skipped. If you edit a prompt and rerun without moving its output, nothing may happen. After changing A, decide whether B and C also need regeneration.

character-lock.txt is a writing reference; the script never reads it. Editing that file alone does not change an API request.

The script skips existing files. If you change a prompt, back up and move the corresponding output before rerunning. Changing A may also require regenerating B and C.

The three images establish character identity, product presentation, and the use action.

3. One API Does Not Mean One Request Schema

The project uses these full model IDs:

IMAGE_MODEL = doubao-seedream-5-0-pro-260628
VIDEO_MODEL = doubao-seedance-2-5-260628
Enter fullscreen mode Exit fullscreen mode

In the bundled schema snapshot, image generation uses size, while video generation uses aspect_ratio. They can coexist in the local configuration, but the script must build two different request bodies.

A unified API solves model access and switching; it does not make model-specific parameters interchangeable. This project keeps shared authentication and task handling in flow.py, then assembles Seedream and Seedance requests separately.

Here is the video request structure. Replace ACTIVE_ASSET_ID with an asset ID registered and activated in your account:

{
  "model": "doubao-seedance-2-5-260628",
  "prompt": "the complete contents of prompts/prompt-video.txt",
  "duration": 20,
  "resolution": "720p",
  "aspect_ratio": "9:16",
  "generate_audio": true,
  "input_references": [
    {"type": "image_url", "url": "asset://ACTIVE_ASSET_ID"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

The request is sent to POST /ai/v1/videos. The real script creates one input_references item per asset.

preflight() checks the cached schema for unknown top-level fields and catches accidental parameter mixing locally. It is not a complete JSON Schema validator and does not validate every type, enum, or range.

4. Register a Photorealistic Virtual Person Before Referencing It

Passing the photorealistic character references directly as image URLs produced this error in the project:

doubao_real_person_required
Enter fullscreen mode Exit fullscreen mode

Because the character in this example was AI-generated, the correct classification was virtual_portrait. The script then performs these steps:

  1. POST /ai/v1/asset-groups creates a virtual-portrait group.
  2. POST /ai/v1/asset-groups/{group_id}/assets registers each public image URL.
  3. GET /ai/v1/assets/{asset_id} polls until the asset becomes active.
  4. The video request uses an asset:// reference.

All references in one video request should belong to the same group. A photograph of a real person must follow the identity-verification path and must not be classified as a virtual person.

The URL must return the actual image. A GitHub /blob/ page can return HTTP 200 while its Content-Type is HTML, so the script checks both status and media type.

The references were generated and all three assets were active; the video task was still running when this screenshot was captured.

5. Do Not Pass Three Asset IDs as One String

The source project encountered asset_invalid in zsh because multiple references stored in a plain string reached the program as one argument.

The quick-start command calls the script with a Python argument list:

subprocess.run([sys.executable, "flow.py", "video", *refs], check=True)
Enter fullscreen mode Exit fullscreen mode

Each reference becomes a separate argument without relying on shell word splitting.

6. Recover an Async Task Instead of Creating Another One

Video generation is asynchronous. An HTTP 200 from the create endpoint does not mean that the video is complete.

The script saves the returned video_id to state.json, polls the task's status, and downloads only after it reaches completed. Resume after a local timeout with:

python3 flow.py status
Enter fullscreen mode Exit fullscreen mode

If the create response was lost, list recent tasks first:

python3 flow.py tasks
python3 flow.py status VIDEO_TASK_ID
Enter fullscreen mode Exit fullscreen mode

video() never automatically retries the create request, because the server may already have created the task. Read-only status requests can be retried; creation should be handled differently.

7. Give the Prompt Testable Constraints

Once the API works, the output still needs to satisfy the ad brief.

The final video prompt divides one continuous take into four actions: present, spray, react, and present again. The camera may push in only to a mid-close shot. The product must stay visible, and the label should remain legible during the first and last three seconds.

The full text is in prompts/prompt-video.txt. Resolution and duration belong in configuration; character action and camera limits belong in the prompt. Both must be verified in the generated result.

Reusing the Structure

The starter preserves the source implementation. Its documentation calls out a stale code comment, existing-output skips, output-file replacement, and asset-group reuse.

Offline checks can verify local configuration, dependencies, and request assembly. They cannot prove account permissions, current endpoint availability, routing behavior, or output quality. Run the shortest path with the included references before replacing the product and character so that failures are easier to isolate.

To switch image or video models later, select the target model in AIHubMix, read its current schema, and update the model ID, parameters, and result handling. A unified entry point lowers the switching cost, but it does not remove differences in model capability or request fields.

See the starter package for the complete workflow. README.md contains the runbook, while docs/PROMPTS.md and docs/TROUBLESHOOTING.md cover prompt adaptation and failure modes.

Top comments (0)