写真1枚+参考動画でダンス動画を作るAIの裏側:ジョブ設計・クレジット管理・入力検証 #バックエンド - Qiita
@weijiejason119

Creating a Dance Video with AI Using One Photo and One Reference Video: Job Design, Credit Management, and Input Validation

  • AI
  • Video Generation
  • Design
  • Backend
0
Posted at 2026-11-12

This article explains a general architecture. As an example of product behavior, we will take up a service that generates a dance video from a photo and a reference video Danceo, but since the internal implementation is not public, the following content is about "how I would design it," and does not explain the actual implementation of the service.

Introduction

"A service that uploads one portrait and one reference dance video, and then generates a video of the person in the photo dancing." From the outside, such a service looks simple.

However, behind the scenes, it involves an asynchronous workflow that takes a long time, uses GPU, and involves money (credits). I feel that the difficulty lies not in the model itself, but in the surrounding design.

In this article, we will cover the following three points.

  1. Concept of Motion Transfer
  2. Designing the generation job as a state machine
  3. Credit management that prevents double charging or refunds errors in case of failure

1. What is Motion Transfer

The text-to-video approach creates video from text, where the model "creates" the motion from the text. On the other hand, Motion Transfer splits the input into two parts.

  • Motion Input: Reference video (responsible for choreography, timing, and camera movement)
  • Appearance Input: Portrait (responsible for who is dancing)

Conceptually, the process involves extracting motion information from the reference video and appearance information from the photo, then passing both as conditions to the video generation model. (The specific methods vary depending on the model, and are not clear from the product page.)

As an engineer, what is important is that the output quality strongly depends on the input quality.

  • If the reference video has many scene cuts or obstructions → motion information becomes incomplete
  • If the portrait is blurry or dark → appearance information becomes unstable

Therefore, input validation and preview are important not only for UX but also for system design.

2. Generation jobs are handled by a state machine

Generation can take several minutes and may fail due to reasons beyond control such as GPU interruption or timeout. Design it as an asynchronous job with an explicit state, not using request/response.

CREATED → VALIDATING → QUEUED → RENDERING → POSTPROCESSING → COMPLETED │ │ │ │ └──────────┴─────────┴─────────────┴──→ FAILED(Refund)

This is a minimal schema example.

schema.sql
CREATE TABLE render_jobs ( id UUID PRIMARY KEY, user_id BIGINT NOT NULL, status TEXT NOT NULL, portrait_key TEXT NOT NULL, -- object storage key reference_key TEXT NOT NULL, output_seconds INT NOT NULL, -- integer seconds credit_cost INT NOT NULL, -- save the estimated cost at the time of estimation idempotency_key TEXT NOT NULL, attempt INT NOT NULL DEFAULT 0, error_code TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (user_id, idempotency_key) );

The points are two.

  • credit_cost is saved in the job during estimation. Even if the pricing system changes later, the amount the user sees is protected.
  • UNIQUE (user_id, idempotency_key) ensures that "double-clicking a button" or "client resending after timeout" becomes harmless.

Enforce state transition in one place

Aggregate transitions to prevent workers from writing any arbitrary status.

"CREATED": {"VALIDATING", "FAILED"}, "VALIDATING": {"QUEUED", "FAILED"}, "QUEUED": {"RENDERING", "FAILED"}, "RENDERING": {"POSTPROCESSING", "FAILED", "QUEUED"}, # QUEUED is retry "POSTPROCESSING": { "COMPLETED", "FAILED"}, } def transition(db, job_id, expected, new): if new not in ALLOWED: raise ValueError(f"illegal transition {expected} -> {new}") updated = db.execute( """ UPDATE render_jobs SET status = %s, updated_at = now() WHERE id = %s AND status = %s """, (new, job_id, expected), ) if updated.rowcount == 0: raise ConcurrentUpdate(job_id) # Other workers have transitioned first WHERE status = expected is using optimistic concurrency control, and even if two workers compete, only one can win. Credit is managed through 'Reserve → Confirm or Release' pattern. Common mistakes are two. Withdrawing at completion: multiple jobs can run with the same balance. Withdrawing first but forgetting to refund: inquiries increase. Stability is achieved by reserving using an append-only ledger (ledger) and confirming or releasing. ledger.sql CREATE TABLE credit_ledger ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, job_id UUID, kind TEXT NOT NULL, -- GRANT, RESERVE, RELEASE, SETTLE amount INT NOT NULL, -- signed. RESERVE is negative created_at TIMESTAMPTZ NOT NULL DEFAULT now()(), UNIQUE (job_id, kind) -- RESERVE and RELEASE/SETTLE are processed once per job );

Balance is SUM(amount), and the reserved amount is immediately treated as deducted from the balance.

create_job.py
def create_job(db, user_id, req): quote = price_quote(req.output_seconds, req.reference_seconds) with db.transaction(): balance = ledger_balance(db, user_id, for_update=True) # Row lock if balance < quote: raise InsufficientCredits(balance, quote) job = insert_job(db, user_id, req, credit_cost=quote) db.execute( "INSERT INTO credit_ledger (user_id, job_id, kind, amount) " "VALUES (%s, %s, 'RESERVE', %s)", (user_id, job.id, -quote), ) enqueue(job.id) return job

This is the processing for completion and failure.

finish_job.py
def finish_job(db, job, ok: bool): with db.transaction(): if ok: transition(db, @"job.id", "POSTPROCESSING", "COMPLETED") db.execute( "INSERT INTO credit_ledger (user_id, job_id, kind, amount) " "VALUES (%s, %s, 'SETTLE', 0) ON CONFLICT DO NOTHING", (job.user_id, job.id), ) else: transition(db, job.id, job.status, "FAILED") # Refund is definitely done once. db.execute( "INSERT INTO credit_ledger (user_id, job_id, kind, amount) " "VALUES (%s, %s, 'RELEASE', %s) ON CONFLICT DO NOTHING", (job.user_id, job.id, job.credit_cost), )

The combination of UNIQUE (job_id, kind) and ON CONFLICT DO NOTHING ensures idempotency of refunds. Even if a worker crashes and is restarted, or if failure events are duplicated, double refunds will not occur.

The promise that "credit is refunded if rendering fails" can only be upheld if this path is subtle and robust.

4. Validate input before using GPU

Since output quality depends on input and GPU time is costly, we perform cheap validation early on.

validate.py
import json, os, subprocess def probe(path): out = subprocess.check_output([ffprobe, "-v", "error", "-print_format", "json", "-show_format", "-show_streams", path]) return json.loads(out) def validate_reference(path, max_bytes=100 * 1024 * 1024, max_seconds=30): if os.path.getsize(path) > max_bytes: raise Invalid("file_too_large") info = probe(path) video = next((s for s in info["streams"] if s["codec_type"] == "video"), None) if video is None: raise Invalid("no_video_stream") duration = float(info[format], info[duration]) if duration > max_seconds: raise Invalid(too_long) w, h = int(video[width]), int(video[height]) return { "duration": duration, orientation : portrait if

h >=

w

else
landscape
, } キューに入れる前に確認したい項目です。 コンテナ/コーデック:パイプラインがデコードできる形式(MP4/MOVなど)だけ受け付け、それ以外は分かりやすいメッセージで拒否する 長さ・サイズの上限:GPUに触れる前に失敗させる 縦横の不一致:ポートレートが縦で参考動画が横なら警告する。向きを揃えると構図を確認しやすい シーンカット:参考動画のハードカットは動きの一貫性を損ねる。フレーム間差分で警告を出せる アップロードは署名付きURLで 100 MB級の動画をAPIサーバー経由で中継せず、クライアントからオブジェクトストレージへ直接アップロードさせ、APIにはキーだけを渡します。 presign.py def presign_upload ( user_id , filename , content_type ): key = f"uploads/{user_id}/{uuid4()}"/sanitize(filename)" url = s3.generate_presigned_url( "put_object", Params={"Bucket": BUCKET, "Key": key, "ContentType": content_type}, ExpiresIn=900, ) return { "": "url", "": "url", "key": "key", } When creating a job, the server side will re-validate the object (size, type, probe result). It does not trust the client's declaration.
5. Failure and Retry

Failures in GPU processing are mostly transient (OOM, preemption, timeout).

Only retry for transient failures (with a limit on the number of retries). Do not retry for definitive failures such as input errors.

Set a

visibility timeout
    on the queue, so that jobs from dead workers can be seen again.
  • While rendering, send
  • heartbeat
  • messages, so that the reaper (cleanup process) can return old jobs back to QUEUED or FAILED status.
  • Save
  • error_code instead of just "failure". This allows support and UI to display proper messages.
  • 6. Privacy and Consent are Part of the Design

The service that creates videos from photos includes the faces of real people in the generated footage. Therefore, some rules should be built into the system.

Default is private

: associate inputs and outputs with an account, and make them not enumerable to the general public.
  • Retention period: decide how long to keep uploaded images and videos, and enforce it via lifecycle rules.
  • Consent confirmation at upload time: confirm with the user that they have obtained permission for all photos, portraits, performances, and audio sources used.
  • Content policy checks: for example, rules like "do not depict real people as minors" are more effective if written in the terms of service and also checked within the pipeline.
  • It's recommended to include these from the first version. Adding a consent flow later is very difficult.

7. Points to Consider for the Product

Looking at the public flow of Danceo, I felt that the following design elements are worth emulating.

Preview input before generation: Notice problematic reference videos before unnecessary credit consumption
  • Display cost before rendering starts: Eliminate the surprise of billing
  • Refund credits upon failure: Only when the above ledger design is robust can it be trusted
  • Save the completed video to a dedicated page: Even if you close the browser tab, you won't lose the video
  • None of them are flashy, but these aspects are what transform an AI product from 'experimental' to 'trustworthy'

    End While models are often the focus, the success or failure of the photo-to-dance video product depends on subtle aspects such as explicit job status, idempotent credit ledger, low-cost early validation, and consent confirmation integrated into the pipeline .

    If you want to verify these designs from the user's perspective, please try a short test rendering with Danceo. Also, if you're making a similar product, please let us know how you're handling retries and refunds in comments. I feel that the most bugs in actual use come from there

    .
    0
    0
    0You get articles that match your needs
  • You can efficiently read back useful information
  • You can use dark theme
  • What you can do with signing up
    Sign upLogin