Sunoの曲を、歌詞が飛ぶMVにしてみた。Pythonとffmpegで作る手順と、1行ずれた失敗

[最終更新]

Googleで優先するソースとして追加するボタン

歌詞が飛ぶMVを作ったのに、最初の字幕は、歌より1行ずつ遅れていました。記録の数字を見直すと、各行の時刻が、次の行の歌い出しになっていたのです。Sunoの曲を、Pythonとffmpegで動画にした手順と、この失敗の見つけ方を、数行のコードつきで書きます。

先に、完成した動画

「りんごの色」のMVは、歌詞の文字が歌に合わせて画面に飛び込む、2分52秒の動画です。YouTube向けの横長(1920×1080)と、Instagramのリール向けの縦長(1080×1920、24秒)の2つを作りました。

「りんごの色」のMV(YouTube、2分52秒)。曲はSuno、歌詞はAIとの共作、画像はAI生成
白いクリーム色の紙の背景に、赤い細い文字で歌詞の一部が出ている。右下に、赤いギターを持つ小さなキャラクター
MVの静かな場面。紙の背景に、歌詞の一部が細い文字で出る(画像:筆者が制作。素材の画像はAI生成)
墨の飛沫の上に、太い黒い文字で歌詞の一部が出ている。左に赤いギターのキャラクター、右に黒いベースのキャラクター
サビの場面。太い文字が飛び込み、背景に墨の飛沫が広がる(画像:筆者が制作。素材の画像はAI生成)
夕暮れの学校の廊下の背景に、歌詞の一部が薄い文字で出ている。手前の床に、赤いりんごがある
ブリッジの場面。夕暮れの廊下に、赤いりんごが出る(画像:筆者が制作。素材の画像はAI生成)
赤い滲みの背景に、太い黒い文字で歌詞の一部が出ている。左右に、2人のキャラクター
最後のサビの場面。背景は、赤い滲みになる(画像:筆者が制作。素材の画像はAI生成)
赤い滲みの背景。画面の上半分に、太い黒い文字で歌詞の一部、下半分に、2人のキャラクター
縦動画の1コマ。文字を上半分に、キャラクターを下半分に置いた(画像:筆者が制作。素材の画像はAI生成)

曲はSunoで生成し、歌詞はAIとの共作、キャラクターなどの画像もAIで生成しています。制作には、プログラムの作成と手順の整理にClaude、キャラクターの画像にChatGPTを使いました。何を作るかの判断と、仕上がりの確認は、私が行いました。

MVは、動画を自動で作るツールでも作れます。今回は、タイミングを人が取り、描画は自作のプログラムにしました。その分、手順が増えます。この記事は、その記録です。

材料をそろえる

必要なのは、曲、歌詞、画像の3つです。曲は、Sunoで生成したmp3(171.6秒)です。

歌詞は、2つの版を用意しました。Sunoに入れる、漢字を開いてひらがなにした版と、画面に出す、漢字まじりの版です。読みを間違えられないよう、Sunoへの入力は、ひらがなにしました。

画像は、ChatGPTで、キャラクターの設定画を作り、そこから全身像を作りました。背景が灰色のまま返ってくる画像は、背景の色を取り除いて、透明にします。

ここで、2つ失敗しました。1つ目は、ギターを持つ手が左右反転して、左利きに描かれたことです。2つ目は、楽器のペグ(弦を巻く部品)の数が、設定と違ったことです。どちらも、見本の画像を添付し、数と向きを文章でも指定して直しました。

画像を置く背景の色も、決める必要があります。白い髪のキャラクターは、白い背景では髪が消えます。黒、赤、濃い色の上に置くか、後ろに黒い墨を置きました。

歌詞のタイミングを取る

歌詞が何秒に歌われるかは、人が曲を聴きながら押して取りました。専用のHTMLのツールを作り、歌詞を1行ずつ表示して、各行の歌い出しの瞬間に、ボタンかスペースキーを押します。

押した時刻には、反応の遅れがあります。ツールは、押した時刻から0.12秒を引いて記録します。再生の速度を0.75倍にすると、押しやすくなります。記録は、JSONで書き出せます。

最初の版は、字幕が歌より、ちょうど1行ずつ遅れました。ツールの画面には、各行の歌い出しで押す、と案内していましたが、記録は、1行遅れていました。

記録は、JSONの lines に各行を並べ、t に曲の先頭からの秒数を入れたものです。未記録の行は、t が null になります。この曲では、各行の歌い出しの間隔は、約3.5秒でした。次のコードで、記録時刻の間隔が中央値の2倍を超える所を探します。記録済みの時刻が2つ以上あることが前提です。未記録の行を挟む区間は、複数行ぶんの間隔になるので、表示された行番号と合わせて確認します。

コード1:タップの記録(taps.json)から、長い間隔を探す(欠けた行があっても、元の行番号を保つ)

import json, statistics
lines = json.load(open("taps.json", encoding="utf-8"))["lines"]
taps = [(i + 1, l["t"]) for i, l in enumerate(lines) if l.get("t") is not None]   # (行番号, 時刻)
gaps = [(a, b, tb - ta) for (a, ta), (b, tb) in zip(taps, taps[1:])]
med = statistics.median(g for _, _, g in gaps)
print([f"{a}→{b}行目: {g:.1f}秒" for a, b, g in gaps if g > med * 2])

この曲の記録では、10行目と11行目の間(10.5秒)と、32行目と33行目の間(12.1秒)が出ました。後者は、最後の行を、余韻を残して歌う所です。前者は、サビの途中です。サビは続けて歌うので、ここに10秒を超える空白があるのは不自然です。1行ずらして読むと、この空白は、サビの最後の行と、2番の最初の行の間、つまり間奏になります。

直し方は、全行を1行ぶん前にずらすことです(行kの歌い出しを、記録のk−1番目にします)。この曲の記録は、各行ではなく、次の行の歌い出しの時刻になっていたので、これで合いました。最初の行だけは、記録がないので、2小節ぶん前(約3.5秒前)と推定し、人が聴いて確かめました。

歌い終わりで押した記録だった場合は、間奏のある所で、ずらした記録が、次の行の歌い出しと合いません。サビの最後と、2番の最初のように、間奏をはさむ所は、聴いて確かめてください。

拍を調べる

文字を拍に合わせて動かすため、曲のテンポを調べました。曲の低音の立ち上がりから、拍の間隔と最初の拍の位置を推定するプログラムを書き、138BPM、最初の拍が0.07秒の位置、という結果が出ました。1小節は約1.74秒(4拍子として)、2小節は約3.48秒です。

この曲では、歌詞1行がほぼ2小節でした。上の検査の目安にもなります。

この推定は、ドラムが止まる場面では当てになりません。曲の途中でテンポが変わる曲にも使えません。

文字を動かす

描画は、Pythonの画像ライブラリ(Pillow)で、1コマずつ画像を重ねて作ります。30フレーム/秒、横1920×1080で、171.6秒なら5,148コマです。

文字は、拍に合わせて画面の端から飛び込み、少し行き過ぎて戻ります。サビは半拍ずつ、最後のサビは4分の1拍ずつ、行の入りをずらしました。

文字の縁を荒らす加工は、強さを数字で決めました。強すぎると、画数の多い漢字がつぶれます。この曲では、上限を1.0にしました(1.4では読めませんでした)。

フォントは、無料で商用にも使えるものを選びました。極太の「Dela Gothic One」(SIL Open Font License)と、細めの「Noto Sans CJK JP」です。使う前に、歌詞のすべての文字が入っているかを、プログラムで調べます。別のフォントには、「眩」の字が欠けていて使えないものがありました。

場面は、静かな紙の背景から、黒、赤い滲みへと、曲の盛り上がりに合わせて変えました。色は、白と黒に、赤を1色だけ足します。

書き出す

書き出す前に、場面ごとに数枚の静止画を作り、一覧にして見ました。文字が読めるか、キャラクターの色が背景に溶けていないか、画面からはみ出していないかを、ここで直します。書き出し直しは、数分から十数分かかります。

書き出しは、区間に分けます。私の作業環境では、1回の実行に時間の制限があったため、約12秒(360コマ)ずつ書き出しました。1コマずつ作った画像は、ffmpegに渡して、動画にします。

コード2:1コマずつ作った画像を、ffmpegに渡して、区間の動画にする

import subprocess
# frames:Pillowの画像(RGB・1920×1080)を、1コマずつ返すもの
cmd = ["ffmpeg", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", "1920x1080", "-r", "30", "-i", "-",
       "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-pix_fmt", "yuv420p", "part0.mp4"]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
for frame in frames:
    p.stdin.write(frame.tobytes())
p.stdin.close(); p.wait()

画質は、CRFで決めます。FFmpegの公式ガイドによると、libx264では、範囲は0〜51で、23が既定です。17〜18は、見た目の劣化がほぼない目安とされています。私は23にして、2分52秒で約51MBでした。

区間の動画は、再エンコードせずにつなぎます。リストに絶対パスで書くときは、-safe 0 が必要です。

コード3:区間の動画を、再エンコードせずにつなぐ

# list.txt(1行に1区間):  file '/work/part0.mp4'
ffmpeg -f concat -safe 0 -i list.txt -c copy video.mp4

最後に、コマ数(5,148)、長さ(171.6秒)、サイズ、音声の有無を、コマンドで確認します。ここで確認できるのは、ファイルとして成立していることまでです。

字幕と縦動画

字幕は、タップの記録から、SRTファイルを作りました。歌詞33行ぶんです。YouTubeの字幕の画面から、時刻付きのSRTファイルをアップロードします。字幕をオンにした視聴者には、画面の歌詞と二重に見えますが、字幕は視聴者がオン・オフを切り替えられるので、そのままにしました。

リールの縦動画は、曲の113.5〜137.7秒、24.2秒の見せ場だけを使いました。曲の途中から始まるので、音声の前後にフェードを付けます。音声は、AACで書き出します。今回使ったmp3には、ジャケット画像が、映像として埋め込まれていました。そのまま.m4aに書き出すとエラーになりました。-vn で画像を除けば、.m4aにも書き出せます。私は、拡張子を.aacにしました。

コード4:曲の一部を切り出して、前後にフェードを付ける(-vn で、埋め込み画像を除く)

ffmpeg -ss 113.5 -t 24.2 -i song.mp3 -vn \
  -af "afade=t=in:st=0:d=0.15,afade=t=out:st=23.5:d=0.7" -c:a aac -b:a 192k audio.aac

縦長は、上と下が、ボタンや文字で隠れることがあります。私は、文字を画面の上半分に、キャラクターを下半分に置きました。

人が確認するところ

この作業で使ったClaudeは、書き出した動画を再生して、見聞きすることができません。確認できるのは、静止画の見た目と、コマ数、長さ、サイズまでです。そのため、次の4つは、人が確認します。

  • 映像と音のタイミング(サビで、文字が歌とずれていないか)
  • 最初の行の時刻(推定した値)
  • 文字が読めるか(荒らし加工で、誤読されないか)
  • 歌詞が、Sunoが実際に歌った言葉と合っているか(Sunoは、歌い違えることがあります)

最終版は、通しで見て、問題ないと判断しました。

公開の前に

公開の前後に、3つ確認しました。1つ目は、AIの利用の表記です。説明欄に、音楽はSuno、歌詞はAIとの共作、画像はAI生成、登場人物は架空、と書きました。YouTubeには、説明欄とは別に、YouTube Studioの[AIの使用]の設定があります。YouTubeは、AI生成の音楽を、開示が必要な例に挙げています。開示しても、視聴の制限や、収益化の資格には、影響しない、と案内されています。公開したあとに、この動画の詳細を開くと、[はい]と[いいえ]のどちらも、選ばれていませんでした。私は、[はい]を選びました。

2つ目は、Sunoの曲の権利です。Sunoは、有料プランでダウンロードした曲に、商用利用の権利が付く、としています。ダウンロードの数え方は、同じ曲を別の形式で落としても、1回です。3つ目は、フォントの利用条件です。

歌詞が飛ぶMVは、自動のツールでも作れます。自作の手順は、手間が増える代わりに、文字の動きも場面の設計も、曲に合わせて決められます。次は、Day 2以降の曲で、この手順をどこまで使い回せるかを試します。

【関連記事】

Sunoで実際に曲を作ってみた。
2024年4月公開の、筆者によるSunoの体験記事。テーマ曲を作った手順と、作詞にChatGPT-4を使ったことを書いている。

【解説】SpotifyがAIペルソナのバッジ導入を発表、DistroKidの画面で確かめた
このMVの曲「りんごの色」を、SpotifyにDistroKid経由で配信する側の話。AIペルソナの定義と、申込画面の初期値を整理している。

Suno新規約、商用利用の条件はダウンロードの取得に|9月3日施行
9月3日に施行されたSunoの新しい規約を整理した記事。商用利用の条件が、ダウンロードの取得に結びつけられた経緯を伝える。

【編集部後記】

本文に入れなかったことが一つあります。この記事の下書きでは、1行ずれの原因を、各行の歌い終わりで押したためだと書いていました。記録の数字を見直したところ、各行ではなく、次の行の歌い出しの時刻になっていました。

押し方の違いは、自分では、気づきにくいものです。コードを動かさなくても、記録の時刻を並べて、サビの途中に長い空白がないかを見るだけで、手がかりになります。この記事のコード1は、その見比べを、少しだけ楽にするものです。


【用語解説】

BPM
1分あたりの拍の数。曲のテンポを表す数字。

CRF
本稿で使うlibx264の画質の指定。0〜51の数字で、小さいほど高画質になる。既定は23。

SRT
時刻つきの字幕を書く、テキストファイルの形式。YouTubeに、字幕としてアップロードできる。

ffmpeg
動画や音声を、変換、編集、結合する、無料のソフト。コマンドで操作する。

Pillow
Pythonで、画像を作ったり、加工したりするための、画像ライブラリ。

JSON
データを、文字で書き表す形式。本稿では、タップの記録の保存に使う。

ペグ
弦を巻いて、音の高さを合わせる、楽器の部品。

【参考リンク】

FFmpeg Wiki「H.264 Video Encoding Guide」(外部)
libx264のCRFの範囲、既定値、見た目の劣化がほぼない目安を示すFFmpeg Wikiのガイド。画質の考え方もある。

FFmpeg Wiki「Concatenate」(外部)
動画を、再エンコードせずにつなぐconcatの使い方と、絶対パスのとき -safe 0 が必要なことを示すFFmpeg Wiki。

FFmpeg afadeフィルタ(ソースコード)(外部)
音声のフェードイン・フェードアウトを行うafadeフィルタのソース。type、start_time、durationの指定が分かる。

YouTubeヘルプ「生成 AI コンテンツの使用に関する開示」(外部)
AI生成の音楽を、開示が必要な例に挙げ、[AIの使用]の設定と、ラベルが付く場合を案内するYouTubeの公式ヘルプ(日本語版)。

YouTubeヘルプ「字幕を追加する」(外部)
字幕ファイルのアップロードなど、動画に字幕を追加する方法を案内するYouTubeヘルプ。ファイル形式の確認にも触れている。

Google Fonts「Dela Gothic One」(外部)
極太の日本語フォント、Dela Gothic Oneの紹介ページ。デザイナーの情報と、ライセンス(利用条件)の表示がある。

Suno公式ヘルプ「Upcoming Changes」FAQ(外部)
Sunoのプラン別ダウンロード数、請求日のリセット、形式を変えて落とした場合の数え方などを、質問と答えで説明する公式FAQ。

Suno公式ブログ「An update to our downloads policy and Terms of Service」(外部)
2026年9月3日に始まるSunoのダウンロード上限と利用規約の変更を、2026年8月10日に告知した公式ブログ(Team Suno)。

Googleで優先するソースとして追加するボタン
投稿者アバター
山本 達也 代表社員
合同会社デジタルの窓口 代表。ウェブ解析士。生成AI・サイバーセキュリティ・ 宇宙開発領域を中心に執筆。

おすすめ記事