0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザだけで AI 動画生成→OpenCV 加工する

0
Posted at

ブラウザだけで AI 動画生成→OpenCV 加工する

AI で動画を作れるようにはなりましたが、生成した動画をちょっといじる となると、途端に面倒になります。Wan や Sora で作った mp4 を一度ローカルに保存して、Premiere や CapCut、あるいは ffmpeg で編集する……というのが今の一般的な流れでしょうか。

ただ、実は生成から OpenCV 加工、動画書き出しまで ブラウザのタブ 1 枚で完結 させる方法があります。OpenRouter の動画生成 API と、HTML 1 枚で動く Python 実行環境 fudebako (私が開発しています) を組み合わせて、その一連の流れを通してみます。

元動画と Shorts 風加工後の比較

完成イメージはこんな感じです。左が AI で生成した素の 5 秒動画、右が fudebako の中で OpenCV 加工とテロップを乗せた Shorts 風の動画。インストール不要、外部ツールもなし。ブラウザだけで完結しています。

全体像

流れは 3 ステップです。

  1. OpenRouter Wan 2.6 で動画を生成(API を叩く)
  2. fudebako で frame 抽出 + OpenCV 加工(cv2.bilateralFilter など)
  3. cv2.VideoWriter で WebM 書き出し(標準 OpenCV API、ffmpeg 不要)

順番に解説します。

ステップ 1: OpenRouter Wan 2.6 で動画生成

OpenRouter は LLM のイメージが強いですが、2026 年 4 月から動画生成 API も追加されています。alibaba/wan-2.6google/veo-3.1-fast といったモデルが、共通のスキーマで叩けるようになっています。

fudebako の Drive panel に動画ファイル一覧

fudebako のセルから普通に POST します。OpenRouter の動画 API は非同期ジョブ方式なので、投げたあとに polling して、完了したら mp4 の URL を取って保存する、という流れになります。

import os, json, asyncio
from pyodide.http import pyfetch

API_KEY = os.environ["OPENROUTER_API_KEY"]
API_BASE = "https://openrouter.ai/api/v1/videos"

PROMPT = (
    "A black Japanese pencil case (筆箱) sitting on a wooden desk softly opens. "
    "Pastel-colored Python code snippets float out like holographic ribbons. "
    "Warm soft daylight, shallow depth of field, slow cinematic dolly-in."
)

# Submit
r = await pyfetch(API_BASE, method="POST",
    headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
    body=json.dumps({
        "model": "alibaba/wan-2.6",
        "prompt": PROMPT,
        "aspect_ratio": "9:16",
        "resolution": "1080p",
        "duration": 5,
        "generate_audio": False,
    }))
job = await r.json()
poll_url = job["polling_url"]

# Poll until completed
for _ in range(60):
    await asyncio.sleep(10)
    pr = await pyfetch(poll_url, headers={"Authorization": f"Bearer {API_KEY}"})
    status = await pr.json()
    if status["status"] == "completed":
        urls = status["unsigned_urls"]
        break

# Download (Bearer 認証必須 — unsigned URL でも実は要)
dl = await pyfetch(urls[0], headers={"Authorization": f"Bearer {API_KEY}"})
data = await dl.bytes()
with open("/drive/wan26.mp4", "wb") as f:
    f.write(data)

5 秒・1080p・9:16・音声なしで、実測 $0.60 / 約 70 秒 でした。1080p や縦長指定で倍率がかかるらしく、公開されている「\$0.04 per second」よりは少し高めに出る印象です(5s で \$0.20 想定が $0.60 だった)。

ハマりどころ:

  • API キーは fudebako の設定(歯車アイコン)→ 環境変数に OPENROUTER_API_KEY を入れておけば、Python 側から os.environ で読めます。
  • unsigned_urls と書いてありますが、ダウンロード時にも Authorization ヘッダが必要です(最初これに気づかず、エラーメッセージの JSON を動画だと思って保存してしまいました)。
  • /drive/ に保存すると Drive タブからダウンロードできます。v0.4.16 からはプレビュー再生も可能です。

ステップ 2: 動画 → frame 抽出

cv2.VideoCapture は ブラウザ環境では動きません。代わりに、HTML5 の <video> 要素と Canvas を使って frame を抜き出し、/drive/frames_in/ に PNG で保存します。fudebakoにはjsを実行する run_js があるのでこれを使います。

from pyodide.code import run_js

# JS で /drive/wan26.mp4 を <video> に流し込み、各 currentTime で
# canvas に描画 → PNG として /drive/frames_in/ に書き戻し
js = """
(async () => {
  const bytes = await fudebako.drive.readBytes("/drive/wan26.mp4");
  const blob = new Blob([bytes], {type: "video/mp4"});
  const url = URL.createObjectURL(blob);
  const video = document.createElement("video");
  video.src = url; video.muted = true;
  await new Promise(r => video.addEventListener("loadedmetadata", r, {once: true}));

  const FPS = 30, W = 540, H = 960;
  const N = Math.round(video.duration * FPS);
  const canvas = new OffscreenCanvas(W, H);
  const ctx = canvas.getContext("2d");

  for (let i = 0; i < N; i++) {
    video.currentTime = i / FPS;
    await new Promise(r => video.addEventListener("seeked", r, {once: true}));
    ctx.drawImage(video, 0, 0, W, H);
    const pngBlob = await canvas.convertToBlob({type: "image/png"});
    const ab = await pngBlob.arrayBuffer();
    const name = `/drive/frames_in/frame_${String(i).padStart(3, "0")}.png`;
    await fudebako.drive.write(name, new Uint8Array(ab));
  }
  URL.revokeObjectURL(url);
  return N;
})()
"""
n = await run_js(js)
print(f"extracted {n} frames")

5 秒 / 30fps の動画を 540×960 に落としながら 150 枚抜き出すのに 約 8 秒。1080×1920 のままだと PNG が重すぎるので、SNS 用ならこのくらいのサイズで十分でしょう。

ステップ 3: OpenCV で per-frame 加工 + cv2.VideoWriter で書き出し

ここからは普通の OpenCV のコードです。fudebako の v0.4.16 から cv2.VideoWriter がそのまま動く ようになったので、out.write(bgr) で 1 フレームずつ流し込めば、/drive/shorts.webm が出来上がります。中身は WebCodecs API + WebM コンテナですが、書く側のコードはローカル Python で書く OpenCV と同じです。

Python セルで cv2.VideoWriter を使った加工+書き出し

日本語テロップには fudebako.text.render を使います。v0.4.16 で追加した API で、ブラウザの Canvas 経由で文字を焼いてくれます。Pyodide 同梱のフォントは日本語が化けますが、fudebakoはしっかり日本語対応しています。

import cv2, glob
import numpy as np
import fudebako

JP_FONT = "Hiragino Sans, Yu Gothic, Noto Sans CJK JP, sans-serif"
H, W, FPS = 960, 540, 30

# テロップ PNG を 30 ms で生成(マルチライン対応、stroke 付き)
ov = fudebako.text.render(
    "fudebako で\nOpenCV 加工",
    width=540, height=200, font_size=50, font_family=JP_FONT,
    fill="#FFFFFF", stroke="#000000", stroke_width=7,
    align="center", baseline="top",
)
ov_bgra = cv2.imdecode(np.frombuffer(ov, dtype=np.uint8), cv2.IMREAD_UNCHANGED)

# 標準 OpenCV API で WebM 書き出し
out = cv2.VideoWriter("/drive/shorts.webm", cv2.VideoWriter_fourcc(*"VP80"), FPS, (W, H))

paths = sorted(glob.glob("/drive/frames_in/frame_*.png"))
for p in paths:
    bgr = cv2.imread(p)
    # HSV boost で彩度アップ
    hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
    hsv[:, :, 1] = np.clip(hsv[:, :, 1] * 1.25, 0, 255)
    bgr = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
    # 下部に日本語テロップ合成
    a = ov_bgra[:, :, 3:4].astype(np.float32) / 255.0
    sub = bgr[H-240:H-40, :].astype(np.float32)
    bgr[H-240:H-40, :] = (sub * (1 - a) + ov_bgra[:, :, :3].astype(np.float32) * a).astype(np.uint8)
    out.write(bgr)

out.release()

150 フレーム分の処理+エンコードで 約 6 秒。出力は 1.3 MB ほど。ホスト側の ffmpegなど 一切使うことなく編集できました。

Shorts 風縦動画プレビュー(5 秒ループ)

ステップ 4: Drive タブでそのままプレビュー

書き出した shorts.webm は Drive タブからクリックするだけでプレビュー再生できます。ホストにダウンロードする前に、ブラウザの中で内容を確認できるのは、何回も加工パラメタを変えながら回すときに地味に効きます。

fudebako の Drive タブで shorts.webm をプレビュー再生中

満足するまでブラウザの中だけでイテレーションを回して、最終版だけを ボタンでホストへダウンロードする、という使い方ができます。

OpenCVを使った応用:漫画化(cartoonize)

加工部分を書き換えるだけで、色々な表現が試せます。たとえば cv2.bilateralFiltercv2.adaptiveThreshold を組み合わせれば漫画風になります。

def cartoonize(img):
    color = img.copy()
    for _ in range(2):
        color = cv2.bilateralFilter(color, d=9, sigmaColor=200, sigmaSpace=80)
    g = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    g = cv2.medianBlur(g, 7)
    edges = cv2.adaptiveThreshold(
        g, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 2)
    em = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
    return cv2.bitwise_and(color, em)

cartoonize で漫画化したフレーム

bilateral filter が重いので 150 フレームで 26 秒ほどかかりましたが、ブラウザを閉じずにそのまま webm として取り出せるのはやはり便利です。

制約・注意点

正直なところ、まだ不便な点もあります。

  • cv2.VideoCapture が未実装:動画の読み込みに JS を介する必要があるのは今後の課題です。
  • コーデックは WebM が基本:H.264 mp4 の書き出しはブラウザ側のサポート状況(特に Safari)に左右されます。SNS に上げるなら、Chrome で WebM を作るのが一番安定します。
  • Wan 2.6 のコスト:前述の通り、高解像度だと意外と料金がかかります。OpenRouter のダッシュボードで予算制限をかけておくのが無難です。
  • API キーの扱い:localStorage に保存されるので、共有 PC などでは注意してください。

まとめ

OpenRouter で動画を生成して、fudebako の中で OpenCV 加工、そして動画保存までをブラウザ 1 枚で通してみました。

  • 生成: OpenRouter の API を pyfetch で叩く
  • 抽出: <video> + Canvas でフレームを抜き出す
  • 加工 + 出力: 標準 OpenCV API(cv2.VideoWriter)で WebM 保存
  • 確認: Drive タブのプレビューでそのまま再生

「AI で生成して、自分でちょっと加工して、そのまま SNS へ」というサイクルを 1 タブの中で完結させたい場合には、なかなか面白い組み合わせではないでしょうか。

関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?