品質の良い音声クローンWebアプリをローカル環境(Win/Mac)に構築するガイド 2026.2版

ad

image

目次

Windows(CUDA / CPU)& Mac Apple Silicon 対応


⛔ はじめに必ずお読みください — 利用上の重要な注意事項

音声クローニングは、使い方を誤ると他者の権利を侵害する非常に強力な技術です。
本記事の手順を実行する前に、以下を必ず理解・遵守してください。

✅ 許可される利用

  • 自分自身の声を使ったクローニング・音声生成
  • 声の提供者から明示的な書面による同意を得た上での利用
  • 研究・学習目的での自己検証(公開・配布しない場合)

❌ 絶対に行ってはいけないこと

  • 本人の許可なく、他者の声を複製・生成すること
  • 著名人・芸能人・声優・アナウンサーなど、公人の声を無断で使用すること
  • 生成した音声を本人になりすまして使用すること(詐欺・なりすましに該当)
  • 生成した音声を商業目的で無断使用すること
  • ディープフェイク音声の作成・拡散

⚖️ 法的リスクについて

他者の声を無断でクローニングする行為は、国や状況によって以下の法律に抵触する可能性があります。

  • 著作権法 — 声優・俳優・ナレーターの声には著作隣接権が認められる場合があります
  • 不正競争防止法・パブリシティ権 — 著名人の声・氏名・肖像を無断で商業利用することは違法となり得ます
  • 刑法(詐欺・名誉毀損など) — なりすましや偽情報の拡散目的での利用は刑事罰の対象になります
  • 個人情報保護法・プライバシー権 — 本人の同意なく声のデータを収集・処理することは個人情報の不正利用にあたる場合があります

「ローカルで動かすから外部に漏れない」という考え方は、利用行為そのものの違法性を免除しません。生成した音声をどこかで使用・共有した時点で問題が生じます。

📌 本記事の前提

本記事は、自分自身の声、または適切な許諾を得た音声素材のみを使用することを前提として書かれています。 上記の注意事項を理解・同意できない方は、本記事の手順を実行しないでください。技術の利用に伴う一切の責任は利用者個人に帰属します。


Alibaba Cloudが開発したオープンソースのTTSモデル「Qwen3-TTS」を、自分のPCやMacで完全オフラインで動かす方法を解説します。3〜5秒のサンプル音声さえあれば誰でも声を複製できます。

この記事の対象読者:

  • Windows で CUDA 対応 GPU を持っている方
  • Windows で GPU なし(CPU のみ)の方
  • Mac Apple Silicon(M1/M2/M3/M4)をお持ちの方

⚠️ Intel Mac は本記事の対象外です。 MLX が Apple Silicon 専用のためセットアップが大きく異なります。

完成イメージ

image

Qwen3-TTS とは?

Alibaba Cloud の Qwen チームが 2026年2月に公開したオープンソースの音声合成モデルです。

主な特徴:

  • 音声クローニング: 3〜5秒のサンプルから声を複製
  • 10言語対応: 日本語・英語・中国語・韓国語など
  • 完全ローカル動作: ネット接続不要(モデルダウンロード後)
  • モデルサイズ: 1.7B(高品質)/ 0.6B(軽量・高速)
  • Streamlit GUI: ブラウザで操作できるUIを構築可能

共通の前提条件

Python バージョン

Python 3.11 を推奨します。
今回の手順では分かりやすく、poetryなどを使わず、venv で構築を行います。

バージョン対応状況
3.10○ 動作可
3.11◎ 推奨
3.12○ 動作可
3.13△ 一部パッケージに問題あり

ffmpeg のインストール

音声形式の変換に使います。

Windows:

# Chocolatey を使う場合
choco install ffmpeg

# winget を使う場合
winget install ffmpeg

Mac(arm64 Homebrew):

/opt/homebrew/bin/brew install ffmpeg

セットアップ:環境別ガイド


🖥️ Windows(CUDA 対応 GPU)

必要なもの

  • NVIDIA GPU(VRAM 8GB 以上推奨)
  • CUDA Toolkit 12.x インストール済み
  • Python 3.11

手順

① 仮想環境の作成
ここでは Python 3.11を指定して進めます

mkdir qwen3-tts
cd qwen3-tts
py -3.11 -m venv .venv
.venv\Scripts\activate
pip install --upgrade pip

② PyTorch(CUDA版)のインストール

# CUDA 12.1 の場合
pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu121

# CUDA 12.4 の場合
pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu124

ご自身の CUDA バージョンは nvidia-smi コマンドで確認できます。

③ Qwen3-TTS と GUI ライブラリのインストール

pip install qwen-tts streamlit soundfile huggingface_hub

④ モデルのダウンロード(音声クローニング用)

python -c "
from huggingface_hub import snapshot_download
snapshot_download(
    repo_id='Qwen/Qwen3-TTS-12Hz-1.7B-Base',
    local_dir='./models/Qwen3-TTS-12Hz-1.7B-Base'
)
print('完了')
"

🖥️ Windows(CPU のみ)

GPU がない場合でも動作しますが、生成に数分かかります。短めのテキストから試すことをおすすめします。

① 仮想環境の作成

ここでは Python 3.11を指定して進めます

mkdir qwen3-tts
cd qwen3-tts
py -3.11 -m venv .venv
.venv\Scripts\activate
pip install --upgrade pip

② PyTorch(CPU版)のインストール

pip install torch torchaudio

③ Qwen3-TTS と GUI ライブラリのインストール

pip install qwen-tts streamlit soundfile huggingface_hub

④ モデルのダウンロード

python -c "
from huggingface_hub import snapshot_download
snapshot_download(
    repo_id='Qwen/Qwen3-TTS-12Hz-1.7B-Base',
    local_dir='./models/Qwen3-TTS-12Hz-1.7B-Base'
)
print('完了')
"

💡 CPU の場合は 0.6B(軽量版)モデル が現実的です:
repo_id='Qwen/Qwen3-TTS-12Hz-0.6B-Base'


🍎 Mac Apple Silicon(M1/M2/M3/M4)

Mac 版は PyTorch の代わりに Apple 製の MLX フレームワークを使います。動作が速く、最適化されています。

必要なもの

  • Apple Silicon Mac(M1/M2/M3/M4)
  • macOS Ventura 13.0 以上
  • RAM 16GB 推奨(1.7B モデル)

手順

① arm64 版 Homebrew のインストール

⚠️ Homebrew が /usr/local にインストールされている場合は Intel 版 です。以下のコマンドで arm64 版を別途インストールしてください。

arch -arm64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

② arm64 Python 3.11 のインストール

/opt/homebrew/bin/brew install python@3.11 ffmpeg

インストール後に arm64 かを確認:

/opt/homebrew/bin/python3.11 -c "import platform; print(platform.machine())"
# → arm64 と表示されればOK

③ MLX 版リポジトリのクローンと仮想環境の構築

git clone https://github.com/kapi2800/qwen3-tts-apple-silicon.git
cd qwen3-tts-apple-silicon

# arm64 Python で仮想環境を作成
/opt/homebrew/bin/python3.11 -m venv .venv
source .venv/bin/activate

# Python が arm64 であることを確認
python3 -c "import platform; print(platform.machine())"
# → arm64 と表示されればOK

④ 依存関係のインストール

pip install --upgrade pip

# Python 3.11 では audioop-lts は不要なので除外してインストール
grep -v 'audioop-lts' requirements.txt > requirements_fixed.txt
pip install -r requirements_fixed.txt && rm requirements_fixed.txt

# MLX 音声ライブラリと GUI をインストール
pip install mlx-audio streamlit soundfile

⑤ モデルのダウンロード(音声クローニング用)

python -c "
from huggingface_hub import snapshot_download
snapshot_download(
    repo_id='Qwen/Qwen3-TTS-12Hz-1.7B-Base',
    local_dir='./models/Qwen3-TTS-12Hz-1.7B-Base-8bit'
)
print('完了')
"

💡 Mac MLX 版はフォルダ名の末尾に -8bit をつけます。

ad

Pythonファイル作成

フォルダ配下にapp.pyを作成し次の内容をコピペします。

"""
Qwen3-TTS Voice Cloning - ローカルGUIアプ
Streamlit版 | マルチプラットフォーム対応

対応環境:
  - Mac Apple Silicon (M1/M2/M3/M4) : mlx_audio バックエンド
  - Windows CUDA                     : qwen_tts + PyTorch CUDA バックエンド
  - Windows CPU                      : qwen_tts + PyTorch CPU バックエンド

起動方法:
    source .venv/bin/activate   # Mac
    .venv\\Scripts\\activate     # Windows
    streamlit run app.py
"""

import streamlit as st
import os, sys, shutil, tempfile, time, wave, re, subprocess, platform
from pathlib import Path
from datetime import datetime

# ========== ページ設定 ==========
st.set_page_config(
    page_title="Qwen3-TTS Voice Cloning",
    page_icon="🎙️",
    layout="wide",
    initial_sidebar_state="expanded",
)

st.markdown("""
<style>
.main-header{background:linear-gradient(135deg,#1a1a2e,#16213e,#0f3460);
 padding:2rem;border-radius:12px;margin-bottom:1.5rem;text-align:center}
.main-header h1{color:#e94560;font-size:2.2rem;margin:0;font-weight:700}
.main-header p{color:#a8b2d8;margin:.5rem 0 0;font-size:1rem}
.stButton>button{background:linear-gradient(135deg,#e94560,#c23152);color:white;
 border:none;border-radius:8px;padding:.7rem 2rem;font-size:1rem;font-weight:600;width:100%}
.stButton>button:hover{background:linear-gradient(135deg,#c23152,#a01f3f);
 transform:translateY(-1px);box-shadow:0 4px 12px rgba(233,69,96,.3)}
.status-bar{background:#1a1a2e;color:#a8b2d8;padding:.8rem 1rem;border-radius:8px;
 font-family:monospace;font-size:.85rem;min-height:3rem}
.hint-box{background:#fff3cd;border:1px solid #ffc107;border-radius:8px;
 padding:.8rem;font-size:.85rem;color:#856404}
.env-badge{display:inline-block;padding:2px 10px;border-radius:12px;
 font-size:.8rem;font-weight:600;color:white}
</style>
""", unsafe_allow_html=True)

st.markdown("""
<div class="main-header">
    <h1>🎙️ Qwen3-TTS Voice Cloning</h1>
    <p>ローカル完全オフライン - 3〜5秒のサンプル音声から声を複製</p>
</div>
""", unsafe_allow_html=True)

# ========== バックエンド検出 ==========
@st.cache_resource
def detect_backend():
    """利用可能なバックエンドを自動検出"""
    # 1. Mac MLX (Apple Silicon)
    try:
        from mlx_audio.tts.utils import load_model      # noqa
        from mlx_audio.tts.generate import generate_audio  # noqa
        return "mlx"
    except ImportError:
        pass
    # 2. PyTorch (Windows CUDA / CPU)
    try:
        import torch
        from qwen_tts import Qwen3TTSModel  # noqa
        if torch.cuda.is_available():
            return "cuda"
        return "cpu"
    except ImportError:
        pass
    return None

BACKEND = detect_backend()

# ========== パス設定 ==========
IS_MAC  = platform.system() == "Darwin"
IS_WIN  = platform.system() == "Windows"

if IS_MAC:
    INSTALL_DIR = Path.home() / "qwen3-tts-apple-silicon"
    MODELS_DIR  = INSTALL_DIR / "models"
    VOICES_DIR  = INSTALL_DIR / "voices"
    OUTPUT_DIR  = INSTALL_DIR / "outputs"
    SAMPLE_RATE = 24000
    # MLX版はフォルダ名に -8bit がつく
    MODEL_SUFFIX = "-8bit"
else:
    # Windows: app.py と同じ場所に models/ を置く想定
    INSTALL_DIR = Path(__file__).parent
    MODELS_DIR  = INSTALL_DIR / "models"
    VOICES_DIR  = INSTALL_DIR / "voices"
    OUTPUT_DIR  = INSTALL_DIR / "outputs"
    SAMPLE_RATE = 24000
    MODEL_SUFFIX = ""   # 公式モデルはそのままのフォルダ名

MODEL_FOLDERS = {
    ("1.7B", "Voice Cloning"):  f"Qwen3-TTS-12Hz-1.7B-Base{MODEL_SUFFIX}",
    ("1.7B", "Custom Voice"):   f"Qwen3-TTS-12Hz-1.7B-CustomVoice{MODEL_SUFFIX}",
    ("1.7B", "Voice Design"):   f"Qwen3-TTS-12Hz-1.7B-VoiceDesign{MODEL_SUFFIX}",
    ("0.6B", "Voice Cloning"):  f"Qwen3-TTS-12Hz-0.6B-Base{MODEL_SUFFIX}",
    ("0.6B", "Custom Voice"):   f"Qwen3-TTS-12Hz-0.6B-CustomVoice{MODEL_SUFFIX}",
    ("0.6B", "Voice Design"):   f"Qwen3-TTS-12Hz-0.6B-VoiceDesign{MODEL_SUFFIX}",
}

# ========== ユーティリティ ==========
def get_model_path(folder_name: str):
    """HuggingFace snapshots 構造にも対応"""
    p = MODELS_DIR / folder_name
    if not p.exists():
        return None
    snap = p / "snapshots"
    if snap.exists():
        subs = [f for f in snap.iterdir() if not f.name.startswith(".")]
        if subs:
            return str(subs[0])
    return str(p)

def convert_to_wav(src: str) -> str | None:
    if not os.path.exists(src):
        return None
    _, ext = os.path.splitext(src)
    if ext.lower() == ".wav":
        try:
            with wave.open(src, "rb") as f:
                if f.getnchannels() > 0:
                    return src
        except wave.Error:
            pass
    out = str(INSTALL_DIR / f"tmp_conv_{int(time.time())}.wav")
    cmd = ["ffmpeg", "-y", "-v", "error", "-i", src,
           "-ar", str(SAMPLE_RATE), "-ac", "1", "-c:a", "pcm_s16le", out]
    try:
        subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
        return out
    except (subprocess.CalledProcessError, FileNotFoundError):
        return None

def get_saved_voices():
    if not VOICES_DIR.exists():
        return []
    return sorted([f.stem for f in VOICES_DIR.glob("*.wav")])

# ========== サイドバー ==========
with st.sidebar:
    st.markdown("### ⚙️ 設定")

    model_size  = st.selectbox("🤖 モデルサイズ", ["1.7B(高品質)", "0.6B(軽量)"])
    size_key    = "1.7B" if "1.7B" in model_size else "0.6B"
    mode_label  = st.selectbox("🎛️ モード", [
        "Voice Cloning(音声クローン)",
        "Custom Voice(プリセット話者)",
        "Voice Design(声質デザイン)",
    ])
    mode_short  = mode_label.split("(")[0]

    st.markdown("---")
    st.markdown("### 📊 システム情報")
    st.caption(f"OS: {platform.system()} {platform.machine()}")

    backend_labels = {
        "mlx":  ("🍎 MLX (Apple Silicon)", "#27ae60"),
        "cuda": ("⚡ PyTorch CUDA",         "#2980b9"),
        "cpu":  ("🖥️ PyTorch CPU",          "#e67e22"),
        None:   ("❌ バックエンドなし",       "#e74c3c"),
    }
    label, color = backend_labels[BACKEND]
    st.markdown(f'<span class="env-badge" style="background:{color}">{label}</span>',
                unsafe_allow_html=True)
    if BACKEND is None:
        st.error("バックエンドが見つかりません")
        if IS_MAC:
            st.code("pip install mlx-audio", language="bash")
        else:
            st.code("pip install qwen-tts\npip install torch", language="bash")

    current_folder = MODEL_FOLDERS.get((size_key, mode_short), "")
    current_model  = get_model_path(current_folder)
    if current_model:
        st.success(f"✓ モデル検出\n`{current_folder}`")
    else:
        st.warning(f"⚠ モデル未検出\n`{current_folder}`")
        with st.expander("ダウンロード方法"):
            hf_name = current_folder.replace("-8bit", "").replace(
                "Qwen3-TTS-12Hz-", "Qwen/Qwen3-TTS-12Hz-")
            st.code(
                f"huggingface-cli download {hf_name} \\\n"
                f"  --local-dir ./models/{current_folder}",
                language="bash",
            )

    st.markdown("---")
    if IS_WIN and BACKEND == "cpu":
        st.warning("⚠ CPU モード: 処理に数分かかります")
    elif IS_WIN and BACKEND == "cuda":
        try:
            import torch
            st.caption(f"CUDA: {torch.version.cuda}")
            st.caption(f"GPU: {torch.cuda.get_device_name(0)}")
        except Exception:
            pass

# ========== メインレイアウト ==========
col_left, col_right = st.columns([1, 1], gap="large")

# ============================
# 左カラム入力
# ============================
with col_left:

    if "Voice Cloning" in mode_label:
        st.markdown("#### 🎤 参照音声(Reference Audio)")
        st.caption("クローンしたい声のサンプルをアップロード(3〜5秒推奨)")

        clone_src = st.radio("音声の取得方法",
                             ["新しくアップロード", "保存済みの声を使う"], horizontal=True)

        ref_audio_path = None
        ref_text_val   = ""

        if clone_src == "新しくアップロード":
            uploaded = st.file_uploader("参照音声", type=["wav","mp3","m4a","flac"],
                                        label_visibility="collapsed")
            if uploaded:
                st.audio(uploaded)
                suffix = Path(uploaded.name).suffix
                tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix,
                                                  dir=str(INSTALL_DIR))
                tmp.write(uploaded.read())
                tmp.close()
                ref_audio_path = tmp.name
                st.success(f"✓ {uploaded.name}")
            else:
                st.markdown("""
                <div class="hint-box">💡 WAV推奨・3〜5秒・雑音なし・16kHz以上</div>
                """, unsafe_allow_html=True)
        else:
            saved = get_saved_voices()
            if saved:
                sel = st.selectbox("保存済みの声", saved)
                ref_audio_path = str(VOICES_DIR / f"{sel}.wav")
                txt = VOICES_DIR / f"{sel}.txt"
                if txt.exists():
                    ref_text_val = txt.read_text(encoding="utf-8").strip()
                st.audio(ref_audio_path)
            else:
                st.info("保存済みの声がありません。")

        st.markdown("")
        st.markdown("#### 📝 参照テキスト(Reference Text)")
        ref_text_val = st.text_area("参照テキスト", value=ref_text_val,
            placeholder="例: こんにちは。これはクローニングのテストです。",
            height=80, label_visibility="collapsed")
        use_xvector = st.checkbox("🔬 x-vector のみ(参照テキスト不要・品質低下あり)")
        if use_xvector:
            ref_text_val = "."

        save_voice = st.checkbox("✅ この声を名前をつけて保存する")
        voice_name = st.text_input("保存名", placeholder="例: Boss, Mom") if save_voice else ""

    elif "Custom Voice" in mode_label:
        st.markdown("#### 👤 話者の選択")
        spk_map = {
            "Japanese": ["Ono_Anna"],
            "English":  ["Ryan","Aiden","Ethan","Chelsie","Serena","Vivian"],
            "Chinese":  ["Vivian","Serena","Uncle_Fu","Dylan","Eric"],
            "Korean":   ["Sohee"],
        }
        lang     = st.selectbox("言語", list(spk_map.keys()))
        speaker  = st.selectbox("話者", spk_map[lang])
        st.markdown("#### 🎭 感情・口調")
        emotions = ["Normal tone","Sad and crying, speaking slowly",
                    "Excited and happy, speaking very fast",
                    "Angry and shouting","Whispering quietly"]
        preset   = st.selectbox("プリセット", emotions)
        custom_e = st.text_input("カスタム指定(英語)", placeholder="例: Calm and professional")
        instruct_val = custom_e.strip() or preset
        st.markdown("#### ⚡ 速度")
        speed_opt = st.select_slider("速度", [0.8,1.0,1.3], value=1.0,
            format_func=lambda x:{0.8:"(0.8x)",1.0:"普通(1.0x)",1.3:"(1.3x)"}[x])
    else:
        st.markdown("#### 🎨 声質のデザイン")
        instruct_val = st.text_area("声質の説明(英語)",
            placeholder="例: A calm middle-aged Japanese male voice, speaking slowly.",
            height=100, label_visibility="collapsed")

    st.markdown("---")
    st.markdown("#### 💬 生成テキスト(Target Text)")
    target_text = st.text_area("生成テキスト",
        placeholder="例: 今日も一日、お疲れ様でした。",
        height=110, label_visibility="collapsed")

    # トーン調整Voice Cloning モードのみ
    tone_instruct = ""
    if "Voice Cloning" in mode_label:
        st.markdown("#### 🎭 トーン調整")
        tone_presets = {
            "通常指定なし)":         "",
            "ゆっくり丁寧に":           "Speak slowly and politely.",
            "明るく元気に":             "Speak in a bright and energetic tone.",
            "落ち着いてナレーション風": "Speak calmly like a narrator.",
            "ニュースキャスター風":     "Speak clearly and professionally like a news anchor.",
            "悲しそうにしんみりと":   "Speak sadly and solemnly.",
        }
        tone_label  = st.selectbox("話し方のスタイル", list(tone_presets.keys()),
                                   label_visibility="collapsed")
        tone_custom = st.text_input("カスタム指定(英語)",
                                    placeholder="例: Speak with excitement and enthusiasm.")
        tone_instruct = tone_custom.strip() or tone_presets[tone_label]

    generate_btn = st.button("🎵  Clone & Generate", use_container_width=True)

# ============================
# 右カラム出力
# ============================
with col_right:
    st.markdown("#### 🔊 生成音声(Generated Audio)")
    audio_ph  = st.empty()
    status_ph = st.empty()
    log_ph    = st.empty()

    with audio_ph.container():
        st.markdown("""
        <div style="background:#f0f2f6;border:2px dashed #ccc;border-radius:10px;
                    padding:3rem;text-align:center;color:#999;">
            <div style="font-size:3rem;">🎧</div>
            <div>生成後にここに音声が表示されます</div>
        </div>""", unsafe_allow_html=True)

    with status_ph.container():
        st.markdown('<div class="status-bar">> 待機中...</div>',
                    unsafe_allow_html=True)

    st.markdown("---")
    with st.expander("📖 使い方", expanded=True):
        st.markdown("""
**Voice Cloning:** 参照音声をアップロード → 参照テキスト入力 → 生成テキスト入力 → Generate

**Custom Voice:** 話者感情を選んで生成テキスト入力

**Voice Design:** 声の特徴を英語で記述して生成
        """)

# ========== 生成処理 ==========
def upd(msg):
    status_ph.markdown(f'<div class="status-bar">> {msg}</div>',
                       unsafe_allow_html=True)

if generate_btn:
    errs = []
    if not target_text.strip():
        errs.append("生成テキストを入力してください。")
    if "Voice Cloning" in mode_label and not ref_audio_path:
        errs.append("参照音声をアップロードしてください。")
    if BACKEND is None:
        errs.append("バックエンドがインストールされていません。")
    if not current_model:
        errs.append(f"モデルが見つかりません: `{current_folder}`")
    for e in errs:
        st.error(f"❌ {e}")

    if not errs:
        OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
        ts        = datetime.now().strftime("%H-%M-%S")
        clean_t   = re.sub(r'[^\w\s-]', '', target_text)[:20].strip().replace(' ','_') or "audio"
        out_name  = f"{ts}_{clean_t}.wav"
        temp_dir  = str(INSTALL_DIR / f"tmp_{int(time.time())}")
        os.makedirs(temp_dir, exist_ok=True)

        try:
            # ===== MLX バックエンドMac Apple Silicon)=====
            if BACKEND == "mlx":
                from mlx_audio.tts.utils import load_model
                from mlx_audio.tts.generate import generate_audio

                cache_key = f"mlx_model_{current_folder}"
                if cache_key not in st.session_state:
                    upd(f"モデルを読み込んでいます(MLX): {current_folder} ...")
                    st.session_state[cache_key] = load_model(current_model)
                model = st.session_state[cache_key]
                upd("音声を生成しています(MLX)...")

                if "Voice Cloning" in mode_label:
                    wav_path = convert_to_wav(ref_audio_path)
                    if not wav_path:
                        st.error("❌ 参照音声の変換失敗。ffmpegを確認してください。")
                        st.stop()
                    gen_kwargs = dict(
                        model=model, text=target_text,
                        ref_audio=wav_path, ref_text=ref_text_val,
                        output_path=temp_dir,
                    )
                    if tone_instruct:
                        gen_kwargs["instruct"] = tone_instruct
                    generate_audio(**gen_kwargs)
                    if save_voice and voice_name.strip():
                        VOICES_DIR.mkdir(exist_ok=True)
                        safe = re.sub(r'[^\w-]','',voice_name).strip()
                        shutil.copy(wav_path, str(VOICES_DIR/f"{safe}.wav"))
                        if ref_text_val:
                            (VOICES_DIR/f"{safe}.txt").write_text(ref_text_val, encoding="utf-8")
                    if wav_path != ref_audio_path and os.path.exists(wav_path):
                        os.remove(wav_path)
                elif "Custom Voice" in mode_label:
                    generate_audio(model=model, text=target_text,
                                   voice=speaker, instruct=instruct_val,
                                   speed=speed_opt, output_path=temp_dir)
                else:
                    generate_audio(model=model, text=target_text,
                                   instruct=instruct_val, output_path=temp_dir)

                src_wav = os.path.join(temp_dir, "audio_000.wav")

            # ===== PyTorch バックエンドWindows CUDA / CPU)=====
            else:
                import torch
                import soundfile as sf
                from qwen_tts import Qwen3TTSModel

                device = "cuda" if BACKEND == "cuda" else "cpu"
                dtype  = torch.float16 if BACKEND == "cuda" else torch.float32

                cache_key = f"pt_model_{current_folder}"
                if cache_key not in st.session_state:
                    upd(f"モデルを読み込んでいます({device.upper()}): {current_folder} ...")
                    st.session_state[cache_key] = Qwen3TTSModel.from_pretrained(
                        current_model,
                        device_map=device,
                        dtype=dtype,
                        attn_implementation="sdpa",
                    )
                model = st.session_state[cache_key]
                upd(f"音声を生成しています({device.upper()})...")

                src_wav = os.path.join(temp_dir, "output.wav")

                if "Voice Cloning" in mode_label:
                    clone_kwargs = dict(
                        text=target_text,
                        ref_audio=ref_audio_path,
                        ref_text=ref_text_val if not use_xvector else None,
                    )
                    if tone_instruct:
                        clone_kwargs["instruct"] = tone_instruct
                    wavs, sr = model.generate_voice_clone(**clone_kwargs)
                    sf.write(src_wav, wavs[0], sr)
                    if save_voice and voice_name.strip():
                        VOICES_DIR.mkdir(exist_ok=True)
                        safe = re.sub(r'[^\w-]','',voice_name).strip()
                        shutil.copy(ref_audio_path, str(VOICES_DIR/f"{safe}.wav"))
                        if ref_text_val:
                            (VOICES_DIR/f"{safe}.txt").write_text(ref_text_val, encoding="utf-8")
                else:
                    # Custom Voice / Voice Design
                    wavs, sr = model.generate(text=target_text)
                    sf.write(src_wav, wavs[0], sr)

            # 出力先に移動
            final_dir = OUTPUT_DIR / mode_short.replace(" ","")
            final_dir.mkdir(parents=True, exist_ok=True)
            final_path = str(final_dir / out_name)
            if os.path.exists(src_wav):
                shutil.move(src_wav, final_path)
            shutil.rmtree(temp_dir, ignore_errors=True)

            upd(f"✓ 完了 → {final_path}")

            with audio_ph.container():
                st.success("✅ 生成完了!")
                with open(final_path, "rb") as f:
                    ab = f.read()
                st.audio(ab, format="audio/wav")
                st.download_button("⬇️ WAVをダウンロード", ab,
                                   file_name=out_name, mime="audio/wav",
                                   use_container_width=True)

            with log_ph.container():
                with st.expander("📋 生成パラメータ"):
                    p = {"モード": mode_label, "バックエンド": BACKEND,
                         "モデル": current_folder, "生成テキスト": target_text}
                    if "Voice Cloning" in mode_label:
                        p["参照テキスト"] = ref_text_val
                    elif "Custom Voice" in mode_label:
                        p["話者"] = speaker; p["感情"] = instruct_val; p["速度"] = speed_opt
                    else:
                        p["声質指定"] = instruct_val
                    st.json(p)

        except Exception as e:
            upd(f"❌ エラー: {e}")
            st.error(f"```\n{e}\n```")

st.markdown("---")
st.markdown("""
<div style="text-align:center;color:#888;font-size:.8rem">
    Qwen3-TTS ローカルGUI | Powered by Qwen Team (Alibaba Cloud) |
    <a href="https://github.com/QwenLM/Qwen3-TTS">GitHub</a>
</div>""", unsafe_allow_html=True)

Streamlit GUI アプリの起動

モデルのダウンロードが完了したら、ブラウザで操作できる GUI アプリを起動できます。

app.py をダウンロード(後述のリンクから)して、プロジェクトフォルダに配置します。

Windows:

.venv\Scripts\activate
streamlit run app.py

Mac:

source .venv/bin/activate
streamlit run app.py

ブラウザが自動で開き http://localhost:8501 にアクセスできます。

サイドバーの確認

起動後、サイドバーに以下が表示されれば準備完了です:

表示意味
🍎 MLX (Apple Silicon)Mac 正常動作中
⚡ PyTorch CUDAWindows GPU 正常動作中
🖥️ PyTorch CPUWindows CPU 動作中
✓ モデル検出モデルが正しく配置されている

音声クローニングの手順

STEP 1: 参照音声をアップロード
クローンしたい声の WAV ファイルなど(3〜5秒)をアップロード

STEP 2: 参照テキストを入力
参照音声で話されている内容を正確に入力(省略可だが品質低下)

STEP 3: 生成テキストを入力
クローンした声で読み上げさせたい文章

STEP 4: Clone & Generate をクリック

参照音声のポイント

項目推奨値
長さ3〜5秒(短すぎると品質低下)
形式WAV 推奨(MP3 も可)
サンプルレート16kHz 以上
品質雑音・エコーなし
内容読み上げや会話(音楽・笑い声不可)

トラブルシューティング

Windows: No matching distribution found for mlx

→ mlx は Apple Silicon 専用です。Windows では qwen-tts(PyTorch版)を使います。

Windows: 処理が遅い

→ CPU モードでは 1.7B モデルで数分かかります。0.6B モデルを試してください。

Mac: arch: /opt/homebrew/bin/brew isn't executable

→ arm64 版 Homebrew が未インストールです。上記手順① を実行してください。

Mac: mlx のインストールが失敗する

→ 仮想環境の Python が x86_64(Rosetta 版)になっています。以下で確認:

python3 -c "import platform; print(platform.machine())"

x86_64 と表示された場合は /opt/homebrew/bin/python3.11 で仮想環境を作り直してください。

huggingface-cli が見つからない

→ 仮想環境が有効になっていません。source .venv/bin/activate(Mac)または .venv\Scripts\activate(Windows)を実行してから再試行してください。コマンドが見つからない場合は以下で代替できます:

python -c "
from huggingface_hub import snapshot_download
snapshot_download(repo_id='Qwen/Qwen3-TTS-12Hz-1.7B-Base',
                  local_dir='./models/Qwen3-TTS-12Hz-1.7B-Base')
"

まとめ

環境バックエンド速度必要 RAM/VRAM
Mac Apple SiliconMLX⚡ 速い16GB RAM 推奨
Windows CUDAPyTorch CUDA⚡ 速い8GB VRAM 推奨
Windows CPUPyTorch CPU🐢 遅い16GB RAM 推奨

Qwen3-TTS は完全オープンソースで商用利用も可能です(ライセンスを確認の上ご利用ください)。ローカルで動かすことでプライバシーを守りながら高品質な音声合成が実現できます。


参考リンク

ad