ChatGPTでバイブコーディング: 音楽生成AIの「ガチャ」用GUIを構築

AIテキスト

前回まで、ACE-StepとComfyUI APIを利用して、AIによる音楽生成の自動化を試してきました。

これで夜間のバッチ処理で大量に生成することができますが、今度はその中から良質なものを選び出す必要があります。

今回はGUIで作業を楽にする方法を試します。

バイブコーディングでGUIを構築

前回まで、ComfyUIとACE-Stepを利用して音楽生成の自動化を試みました。 APIをPythonから呼び出すことで、生成処理の自動化も実現しています。

次に、生成した音楽ファイルの中から良質なものを選び出す「ガチャ」処理の自動化に着手しています。 ある程度の機械的な評価で選別は可能ですが、最終的には残ったものから人間が選別する作業が残ります。

大量のファイルを1つずつ、手作業で再生して評価はの繰り返しは煩雑なため、GUIを構築して作業を楽にししましょう。

以前はGUIの構築に時間がかかりましたが、AIによるバイブコーディングなら簡単です。

コード生成のプロンプト例

今回、下記のようなプロンプトを与えてGUIを構築するコードを生成させました。

指定したディレクトリにある、ACE-Stepで生成した音楽ファイル群から人間がより良い物を選択し、残った不要なファイルをアーカイブディレクトリに移動するGUIを構築してください。
評価は、GUI上で音楽を波形で確認しながら、実際に再生して行います。
良いと思ったものを「残す」ボタンで選択し、不要なものを「アーカイブ」ボタンで移動する形にしてください。
処理は1つずつ行い、進捗状況や経過時間、残り時間の目安も表示してください。

シンプルに、出力済みのファイルに対して手作業を行う処理として構築を指示しました。
下記は動作イメージです、実際にこれが数分でできます。

GUIはGradio、波形についてはmatplotlibでプロットされています。右にも簡易版の波形が出て機能が重複していますね。またフォルダパスが手入力のため、OSのファイルダイアログで選択できるようにするなど、細かい部分は手直しすれば使い勝手は上がりそうです。

ChatGPTによるコード例

ChatGPTが出力したコードのサンプルです。ChatGPTが出力したものそのままです。

必要ライブラリをインストールし、Python 3.10系での動作を確認しています。

pip install gradio matplotlib numpy pillow
import os
import time
import shutil
from pathlib import Path
from typing import List, Tuple

import gradio as gr
import numpy as np
from PIL import Image
import io
import matplotlib.pyplot as plt

try:
    import soundfile as sf
    _HAS_SOUNDFILE = True
except Exception:
    _HAS_SOUNDFILE = False

try:
    from scipy.io import wavfile
    _HAS_SCIPY = True
except Exception:
    _HAS_SCIPY = False


SUPPORTED_EXTS = [".wav", ".mp3", ".flac", ".ogg", ".m4a"]


def list_audio_files(directory: str) -> List[Path]:
    p = Path(directory)
    if not p.exists() or not p.is_dir():
        return []
    files = [f for f in sorted(p.iterdir()) if f.suffix.lower() in SUPPORTED_EXTS and f.is_file()]
    return files


def read_audio(path: Path) -> Tuple[int, np.ndarray]:
    """Return (samplerate, numpy array mono). Attempts several readers."""
    if _HAS_SOUNDFILE:
        data, sr = sf.read(str(path))
        arr = np.asarray(data)
        # convert to mono
        if arr.ndim > 1:
            arr = arr.mean(axis=1)
        return int(sr), arr

    if _HAS_SCIPY and path.suffix.lower() == ".wav":
        sr, data = wavfile.read(str(path))
        arr = np.asarray(data).astype(float)
        if arr.ndim > 1:
            arr = arr.mean(axis=1)
        return int(sr), arr

    # fallback: support only wav via wave module
    import wave
    import struct

    with wave.open(str(path), "rb") as wf:
        sr = wf.getframerate()
        nframes = wf.getnframes()
        nch = wf.getnchannels()
        raw = wf.readframes(nframes)
        fmt = "<%dh" % (nframes * nch)
        data = struct.unpack(fmt, raw)
        arr = np.array(data, dtype=float)
        if nch > 1:
            arr = arr.reshape(-1, nch).mean(axis=1)
        return int(sr), arr


def waveform_image(sr: int, samples: np.ndarray, width: int = 1200, height: int = 240) -> Image.Image:
    # downsample for plotting
    max_points = 2000
    n = samples.shape[0]
    if n > max_points:
        idx = np.linspace(0, n - 1, max_points).astype(int)
        s = samples[idx]
    else:
        s = samples

    t = np.linspace(0, n / sr, s.shape[0])

    fig, ax = plt.subplots(figsize=(width / 100, height / 100), dpi=100)
    ax.plot(t, s, color="#2b8cff", linewidth=0.6)
    ax.fill_between(t, s, color="#2b8cff", alpha=0.2)
    ax.set_xticks([])
    ax.set_yticks([])
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)
    ax.spines["left"].set_visible(False)
    ax.spines["bottom"].set_visible(False)
    plt.tight_layout()

    buf = io.BytesIO()
    fig.savefig(buf, format="png", bbox_inches="tight", pad_inches=0)
    plt.close(fig)
    buf.seek(0)
    img = Image.open(buf).convert("RGB")
    return img


def create_archive_ui():
    with gr.Blocks(title="🎧 ACE-Step: Audio Archive Tool") as ui:
        gr.Markdown("# 🎧 ACE-Step: 音楽アーカイブツール")

        with gr.Row():
            input_dir = gr.Textbox(label="音声フォルダパス", value="outputs/audio")
            archive_dir = gr.Textbox(label="アーカイブ先フォルダ", value="outputs/audio/archive")
            load_btn = gr.Button("📂 読み込み")

        status = gr.Markdown("準備完了")

        with gr.Row():
            waveform = gr.Image(type="pil", label="波形")
            audio_player = gr.Audio(label="再生", type="filepath")

        with gr.Row():
            prev_btn = gr.Button("◀ 前へ")
            keep_btn = gr.Button("✔ 残す", variant="primary")
            archive_btn = gr.Button("🗄️ アーカイブ")
            next_btn = gr.Button("次へ ▶")

        progress_text = gr.Textbox(label="進捗", interactive=False)
        timing_text = gr.Textbox(label="時間情報", interactive=False)

        # State: dict with keys files(list Path), index(int), start_time(float), processed(int), statuses(dict)
        state = gr.State({})


        def do_load(inp_dir, arch_dir, st):
            files = list_audio_files(inp_dir)
            Path(arch_dir).mkdir(parents=True, exist_ok=True)
            st = {
                "files": [str(p) for p in files],
                "index": 0,
                "start_time": None,
                "processed": 0,
                "statuses": {},
                "input_dir": inp_dir,
                "archive_dir": arch_dir,
            }

            if not files:
                return st, "エラー: 音声ファイルが見つかりません", None, None, "0/0", "経過: 0s | 残り: -"

            # load first
            return get_item_view(st)


        def get_item_view(st):
            files = st.get("files", [])
            idx = st.get("index", 0)
            total = len(files)

            if total == 0 or idx < 0 or idx >= total:
                return st, "完了: 対象ファイルなし", None, None, f"0/{total}", "経過: 0s | 残り: -"

            path = Path(files[idx])

            try:
                sr, samples = read_audio(path)
                img = waveform_image(sr, samples)
            except Exception as e:
                img = None

            # progress and timing
            processed = st.get("processed", 0)
            start = st.get("start_time")
            now = time.time()
            if start is None:
                elapsed = 0.0
                est = "-"
            else:
                elapsed = now - start
                if processed > 0:
                    avg = elapsed / processed
                    est_remain = avg * (total - processed)
                    est = f"約 {int(est_remain)}s"
                else:
                    est = "-"

            progress = f"{idx+1}/{total}"
            timing = f"経過: {int(elapsed)}s | 推定残り: {est}"

            status = f"表示中: {path.name} ({path.suffix})"

            return st, status, img, str(path), progress, timing


        def step_next(st):
            st["index"] = min(st["index"] + 1, max(len(st.get("files", [])) - 1, 0))
            return get_item_view(st)


        def step_prev(st):
            st["index"] = max(st["index"] - 1, 0)
            return get_item_view(st)


        def mark_keep(st):
            # mark as kept and advance
            idx = st.get("index", 0)
            files = st.get("files", [])
            total = len(files)
            if idx >= total:
                return get_item_view(st)

            st["statuses"][files[idx]] = "keep"
            # start timer at first action
            if st.get("start_time") is None:
                st["start_time"] = time.time()

            st["processed"] = st.get("processed", 0) + 1
            if idx + 1 < total:
                st["index"] = idx + 1
            return get_item_view(st)


        def mark_archive(st):
            idx = st.get("index", 0)
            files = st.get("files", [])
            total = len(files)
            if idx >= total:
                return get_item_view(st)

            src = Path(files[idx])
            dst = Path(st.get("archive_dir")) / src.name
            try:
                shutil.move(str(src), str(dst))
                st["statuses"][files[idx]] = f"archived:{dst}"
            except Exception as e:
                st["statuses"][files[idx]] = f"archive_failed:{e}"

            if st.get("start_time") is None:
                st["start_time"] = time.time()

            st["processed"] = st.get("processed", 0) + 1
            # remove moved file from list so index remains pointing to next
            st_files = st.get("files", [])
            st_files.pop(idx)
            st["files"] = st_files
            if idx >= len(st_files):
                st["index"] = max(0, len(st_files) - 1)

            return get_item_view(st)


        load_btn.click(do_load, inputs=[input_dir, archive_dir, state], outputs=[state, status, waveform, audio_player, progress_text, timing_text])
        next_btn.click(lambda st: step_next(st), inputs=[state], outputs=[state, status, waveform, audio_player, progress_text, timing_text])
        prev_btn.click(lambda st: step_prev(st), inputs=[state], outputs=[state, status, waveform, audio_player, progress_text, timing_text])
        keep_btn.click(lambda st: mark_keep(st), inputs=[state], outputs=[state, status, waveform, audio_player, progress_text, timing_text])
        archive_btn.click(lambda st: mark_archive(st), inputs=[state], outputs=[state, status, waveform, audio_player, progress_text, timing_text])

        gr.Markdown("---")
        gr.Markdown("使い方: フォルダを指定して「読み込み」を押し、波形を確認後に「残す」か「アーカイブ」を選択します。次へ/前へで移動できます。")

    return ui


if __name__ == "__main__":
    ui = create_archive_ui()
    ui.launch(server_name="0.0.0.0", server_port=7864, inbrowser=True)

既に300行近いコードとなっており、これ以上の機能追加はバイブコーディングではあまりおすすめできません

前回までのComfyUIやACE-Stepとの連携、評価指標による処理なども一括で処理することも可能ですが、AIによるコーディングでは指示が難しくなってきます。

生成や評価といった段階ごとにファイルに出力し、ファイルを中心に作業をするというパイプライン設計にして、コードを小さく分割していくことをお勧めします。

バイブコーディングとの「距離感」

以前は難しかったGUIのたたき台を、バイブコーディングなら数分で用意できるのは圧倒的な魅力です。

一方で、生成されるコードの品質にはばらつきがあり、段階的な機能追加やリファクタリングはAIは得意とは言えません。

ここで意識したいのは、AIが生成したコードも「ガチャ」の対象、という前提です。

生成AIのメリットは、そこそこの質の結果を早く大量に得られること。だからこそ、品質の十分でない素材を延々と修正するより、出来の良い案を選別して採用するほうが結果的に速いケースがあります。

おすすめの運用は次の流れです。

  1. まずは動くプロトタイプを最速で作る(使い捨て前提でOK)
  2. 触って問題点を洗い出す(UI/操作導線、例外処理、パス入力など)
  3. 必要な部分だけを手直しして実用品に近づける
  4. それでも厳しければ、別プロンプトで別案を作り、より良い方を採用する

プロトタイプ段階では「完璧さ」より「動くこと」に重きを置きます。

動くものを早く作って選別し、積極的に一から作り直す——この割り切りが、バイブコーディングをうまく使うコツと言えそうです。

まとめ

今回は、生成AIを使って音楽ファイルの選別用GUIを構築しました。

バイブコーディングでたたき台を作り、手直しして使い勝手を向上させる流れを試しています。

生成AIを使うことで、GUI構築のハードルが大きく下がりました。

次回は、動画ファイルの生成について考えてみます。

タイトルとURLをコピーしました