解説一覧

Ürünereklam

完成

商品のリンクを貼ると、そのまま使えるトルコ語と英語の動画広告と画像広告ができます。お金をかける前に、予測モデルがそれぞれにスコアをつけます。2026年のBTK Akademi Hackathonで生まれました。

担当
デザインと開発をひとりで
制作年
2026
使用技術
Next.js · Inngest · Supabase · Gemini · Veo · ElevenLabs · Remotion · PyTorch on Modal · Polar
SCRAPER: PYTHON, HARDENEDFETCHSTRATEGY + FRAMES: GEMINIVIDEO: VEO / SEEDANCEVOICE + MUSIC: ELEVENLABSEDIT: REMOTION, CAPTIONS, CTASCORE: TRIBE V2 ON MODAL GPUSUPABASE: STATE, CREDITLEDGERFACTSFRAMESWORD TIMINGADSRANKEDONE PRODUCT LINK INSPAWN + POLL: GPU JOB OUTLIVES EDGE TIMEOUTSTEP DIES → CREDITS REFUNDED ONCE

横にスクロールすると、図の全体を見られます。

アーキテクチャSCRAPE → GENERATE → RENDER → SCORE, REFUND ON FAILURE

課題

ネットショップに必要な量の広告を2つの言語で用意するとなると、たいていは代理店と継続契約を結ぶことになります。それでも、予算を使う前にどのバージョンが効くかは誰にもわかりません。

どう作られているか

Next.jsアプリが、Inngestのdurableなジョブをオーケストレーションします。Pythonのスクレイパーが商品ページを読み取り、取得が難しいストアフロントには堅牢化した取得経路を使います。Geminiが戦略、コピー、フレームを書き、VeoかSeedanceが動画をレンダリングし、ElevenLabsが音声を付け、Remotionが単語単位でタイミングを合わせた字幕、ウォーターマーク、CTAを加えます。静的な画像広告と広告コピーは、別のスタジオで作ります。そのあと、すべてのバリエーションを、人がどう反応するかを予測するセルフホストのモデル(TRIBE v2)にかけます。モデルはModalのGPUで動かし、ジョブを開始してからポーリングします。レンダリングはエッジ関数のタイムアウトより長くかかるからです。状態はSupabaseに保持して行レベルセキュリティで保護し、メディアには有効期限の短い署名付きURLを使います。クレジットは追記専用の台帳で管理し、課金と返金はアトミックに行います。決済はPolarで、価格はトルコリラとドルです。

重要だった判断

  • 失敗したステップは自動で返金する

    パイプラインはdurableなジョブで構成され、ステップごとに失敗時のハンドラーを持っています。途中で落ちたステップは、そのクレジットをちょうど1回だけ返金し、実行を詰まらせたままにせず解放します。

  • メディアはデフォルトで非公開

    レンダリングしたままの素材は非公開のバケットに置き、カラム単位のSELECT権限は取り消し、メディアのリンクは有効期限の短い署名付きURLにしています。無料プランも含めて、推測できるメディアのアドレスは1つもありません。

  • どの広告も、お金をかける前にスコアをつける

    スコアのない広告がスタジオから出ることはありません。誰かが予算をつける前に、モデルがバリエーションに順位をつけます。

リポジトリのコード

server/inngest/functions.tsクレジットをちょうど1回だけ返金する失敗時ハンドラー
async function safeRefund(
  opts: Parameters<typeof refundCredits>[0],
): Promise<void> {
  try {
    await refundCredits(opts);
  } catch (err) {
    console.error(
      `[billing][RECONCILE-NEEDED] refund failed user=${opts.userId} ` +1
        `amount=${opts.amount} ref=${opts.refType}:${opts.refId} ` +
        `key=${opts.idempotencyKey ?? "-"}`,
      err,
    );
  }
}
 
// …
await safeRefund({
  supa,
  userId: campaign.user_id,
  amount: campaignCreditCost({
    videoCount: brief.videoCount ?? 1,
    videoModel: brief.videoModel ?? "seedance",
    durationSec: brief.durationSec,
  }),
  refType: "campaign",
  refId: campaignId,
  idempotencyKey: `refund:campaign-all:${campaignId}`,2
  description: "Kampanya başlatılamadı — otomatik kredi iadesi",3
});
  1. 8行目. 返金に失敗しても例外は投げず、RECONCILE-NEEDEDのマーカーをログに残します。そのため、クレジットが黙って失われることはなく、パイプラインはそれでも実行を解放します。

  2. 27行目. イベントごとに冪等キーを1つ使います。失敗時ハンドラーが再実行されることはあっても、台帳に記録される返金は1回だけです。

  3. 28行目. 返金理由がトルコ語なのは、プロダクト全体が2言語対応だからです。

今の状況

ハッカソンのデモから、GPUでスコアリングする2言語対応のプロダクトに育ちました。現在はクローズドソースで、有料での公開に向けて準備中です。

次の解説Pistochia