自宅の RTX 4070 Ti に Qwen3-TTS をデプロイした記録。クラス名・メソッド名・戻り値の形式が全部変わっていて3回詰まった。同じ罠に落ちる人が減るよう全部書く。
自宅のサーバーラックに、RTX 4070 Ti が刺さっています。12GB VRAM のカードが、毎日 ComfyUI と ACE-Step のために待機している。
音声合成もそこにある GPU でやればいい、という話です。Qwen3-TTS は Alibaba が2026年にリリースした TTS モデルで、日本語を含む10言語に対応し、VRAM は 4GB で足ります。クラウド TTS API は文字数課金なので、利用頻度が上がると費用が積み上がります。一度ローカルにデプロイしてしまえば、その後は GPU の電力代だけです。
実際に入れてみたら、ドキュメントと実際の API が噛み合わない箇所で3回つまずきました。同じところで止まる人が減るよう、全部記録します。
Alibaba(阿里巴巴)が開発した多言語 TTS モデルです。
| 項目 | 詳細 |
|---|---|
| モデル | Qwen3-TTS-12Hz-1.7B-CustomVoice |
| パラメータ数 | 1.7B |
| 対応言語 | 10言語(日本語・英語・中国語・ロシア語・仏語・独語・西語・韓国語・ポルトガル語・伊語) |
| 日本語話者 | Ono_Anna / Seina_Yuki / Koharu ほか(9名) |
| VRAM要件 | 約4GB(bfloat16) |
| ライセンス | Apache 2.0 |
1.7B で VRAM 4GB というのは、RTX 4070 Ti(12GB)にとって余裕のある要件です。日本語話者が複数用意されていて、Ono_Anna は聴いてすぐ「使える」と判断できる品質でした。
META-MARK の AI ワークロードページに Qwen3-TTS の詳細スペック をまとめていますので、技術仕様だけ確認したい方はそちらへ。
| 項目 | 値 |
|---|---|
| CPU | AMD Ryzen 9 5900(24コア) |
| RAM | 32GB |
| GPU | NVIDIA GeForce RTX 4070 Ti(12GB VRAM) |
| OS | Ubuntu 24.04.4 LTS |
| CUDA | 13.0 |
| Python | 3.12 |
| PyTorch | 2.6.0+cu124 |
| qwen-tts | 0.1.1 |
mkdir -p ~/qwen3-tts/output
chmod 700 ~/qwen3-tts
cd ~/qwen3-tts
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install torch torchaudio \
--index-url https://download.pytorch.org/whl/cu124
pip install torch だけではCPU版が入る可能性があります。--index-url の指定は省略しないこと。
pip install qwen-tts
sudo apt install sox
sox はPythonライブラリとシステムバイナリが別物です。pip install qwen-tts でPython側の sox ライブラリは入りますが、システム側のバイナリは別途必要です。sudo apt install sox を忘れると起動時に警告が出続けます。
次の「詰まったところ」セクションを先に読むことをすすめます。
API の変更点を知らずに書くと、3箇所で止まります。
# api_server.py
from contextlib import asynccontextmanager
import asyncio, io, os
import soundfile as sf
import torch
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel, Field
from typing import Optional
from dotenv import load_dotenv
load_dotenv()
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice")
_model = None
_gpu_lock = asyncio.Lock()
@asynccontextmanager
async def lifespan(app: FastAPI):
global _model
from qwen_tts import Qwen3TTSModel # ★ QwenTTS ではない
_model = Qwen3TTSModel.from_pretrained(
MODEL_NAME,
device_map="cuda:0",
dtype=torch.bfloat16, # ★ torch_dtype= ではなく dtype=
attn_implementation="sdpa", # FlashAttention2 未インストール時はこれ
)
yield
app = FastAPI(lifespan=lifespan)
class SynthesizeRequest(BaseModel):
text: str = Field(..., min_length=1, max_length=2000)
speaker: str = Field(default="Ono_Anna")
language: str = Field(default="Japanese")
instruct: Optional[str] = None
@app.get("/health")
async def health():
return {"status": "ok", "model_loaded": _model is not None}
@app.post("/v1/tts/synthesize")
async def synthesize(body: SynthesizeRequest):
if _model is None:
raise HTTPException(503, "モデルロード中")
async with _gpu_lock:
loop = asyncio.get_event_loop()
wavs, sr = await loop.run_in_executor(
None,
lambda: _model.generate_custom_voice( # ★ synthesize() ではない
text=body.text,
speaker=body.speaker, # ★ speaker_name= ではなく speaker=
language=body.language,
instruct=body.instruct,
)
)
buf = io.BytesIO()
sf.write(buf, wavs[0], sr, format="WAV") # ★ 戻り値は (List[ndarray], int) → wavs[0]
return Response(content=buf.getvalue(), media_type="audio/wav")
# /etc/systemd/system/qwen3tts.service
[Unit]
Description=Qwen3-TTS Speech Synthesis Server
After=network-online.target
Conflicts=comfyui.service acestep.service
[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/home/YOUR_USER/qwen3-tts
ExecStart=/home/YOUR_USER/qwen3-tts/.venv/bin/uvicorn \
api_server:app --host 0.0.0.0 --port 9001 --workers 1
Restart=on-failure
EnvironmentFile=/home/YOUR_USER/qwen3-tts/.env
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/home/YOUR_USER/qwen3-tts /home/YOUR_USER/.cache/huggingface
MemoryMax=16G
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start qwen3tts
journalctl -u qwen3tts -f
sudo ufw allow from 192.168.x.x to any port 9001 proto tcp
curl -X POST http://localhost:9001/v1/tts/synthesize \
-H "Content-Type: application/json" \
-d '{"text":"こんにちは、テスト音声です。"}' \
-o test.wav && file test.wav
ImportError: cannot import name 'QwenTTS'最初に必ず引っかかる罠です。
ImportError: cannot import name 'QwenTTS' from 'qwen_tts'
検索で出てくるサンプルコードのほぼ全部が古い API 名を使っています。qwen-tts 0.1.1 の実際のクラス名は Qwen3TTSModel です。クラス名だけでなく、メソッド名・引数名・戻り値の形式も全部変わっています。
| 旧API(誤り) | 新API(qwen-tts 0.1.1) |
|---|---|
QwenTTS | Qwen3TTSModel |
.synthesize() | .generate_custom_voice() |
speaker_name= | speaker= |
| 戻り値: ndarray | 戻り値: (List[ndarray], int) |
戻り値がタプルになっていることに気づかず、最初は wavs を直接 sf.write に渡してエラーになりました。正しくは wavs[0] です。
現在の公開インターフェースを確認するには:
import qwen_tts; print(dir(qwen_tts))
# → ['Qwen3TTSModel', 'Qwen3TTSTokenizer', 'VoiceClonePromptItem', ...]
バージョンアップのたびに変わる可能性があるので、インストール後は一度 dir() で確認する習慣をつけておくと時間を節約できます。
torch_dtype is deprecated!# ❌
Qwen3TTSModel.from_pretrained(MODEL, torch_dtype=torch.bfloat16)
# ✅
Qwen3TTSModel.from_pretrained(MODEL, dtype=torch.bfloat16)
動きはしますが、毎回 deprecated 警告が出ます。dtype= に変えれば消えます。些細なことですが、ログが汚いと本当のエラーを見落とします。
FlashAttention2 を入れようとするとコンパイルが走ります。数十分かけた結果、失敗しました。
PyTorch 組み込みの sdpa(Scaled Dot-Product Attention)でフォールバックすれば動きます。RTX 4070 Ti なら速度も実用的です。FlashAttention2 に拘る必要はありません。
try:
model = Qwen3TTSModel.from_pretrained(MODEL, attn_implementation="flash_attention_2", ...)
except Exception:
model = Qwen3TTSModel.from_pretrained(MODEL, attn_implementation="sdpa", ...)
sudo grep が動かないインストールスクリプトで Conflicts= ディレクティブを自動追記しようとしたとき、NOPASSWD リストに grep が入っていないために条件分岐が壊れました。
sudo cat で読み込んでから通常の grep に渡す組み合わせで解決できます。
if ! sudo cat /etc/systemd/system/foo.service | grep -q "bar"; then
if sudo cat /etc/systemd/system/foo.service | grep -q "^Conflicts="; then
sudo sed -i '/^Conflicts=/ s/$/ bar.service/' /etc/systemd/system/foo.service
else
sudo sed -i '/^\[Unit\]/a Conflicts=bar.service' /etc/systemd/system/foo.service
fi
fi
| 項目 | 値 |
|---|---|
| VRAM使用量 | 約4GB(3,985 MiB) |
| モデルロード時間(キャッシュ後) | 約30秒 |
| 日本語音声生成(約20文字) | 約7秒 |
| 生成音声の長さ | 5.68秒 |
| 出力フォーマット | WAV 24000Hz 16bit mono |
20文字で7秒は「速い」とは言えません。リアルタイム会話には使えないです。ただ、目的がバッチ処理——テキストを渡して音声ファイルをまとめて生成しておく——なら十分実用的です。クラウドAPIと違って並列数に料金が発生しないので、大量に流すような使い方には向いています。
自宅サーバーで ComfyUI や ACE-Step を既に動かしている場合、VRAM の競合が起きます。systemd の Conflicts= ディレクティブを使うと、Qwen3-TTS 起動時に他のサービスが自動停止します。
[Unit]
Conflicts=comfyui.service acestep.service
停止したサービスは Qwen3-TTS を止めたあとに手動で起動し直す必要があります。自動復帰の仕組みは入れていませんが、用途的にはこれで十分です。
ローカル AI を複数同居させる構成については、
BlogローカルAI vs クラウドAI、2026年の今どちらが本命なのかを正直に語る2026年のAI環境の中で、ローカルAIとクラウドAIのメリット・デメリットを徹底比較。コスト、プライバシー、性能、手軽さを管理人の視点で解説します。→にも整理しています。
Qwen3TTSModel(QwenTTS ではない)dtype=(torch_dtype= は非推奨).generate_custom_voice() を使う(.synthesize() ではない)speaker=(speaker_name= ではない)(wavs, sr) のタプル → wavs[0] が音声データsudo apt install sox でシステム側 sox もインストールAPI はバージョンアップで変わる可能性があります。import qwen_tts; print(dir(qwen_tts)) で現在の公開クラスを都度確認するのが確実です。
ローカルでまともに動く TTS が、VRAM 4GB で動く。3年前はこんなことになるとは思っていませんでした。モデルが小さくなって、ハードルが下がって、自宅のラックに立てるのが「普通」になりつつある。
ローカルで音楽生成を試したいなら、
Blogローカルで音楽を作る。ACEStep 1.5を本気で試したら詰まって、直して、驚いたACEStep 1.5をRTX 4070 Ti 12GBで実際に動かした記録。CUDA graphバグの修正からJ-Pop・Lo-fi・Rock・Ambient 4ジャンルの生成実験まで、数字ありで正直に書く。→も参考になると思います。あちらも同じくローカル環境で動かせます。
何が来ても驚かなくなってきたのが、正直なところです。それがいいことなのかはまだわかりません。
HW系エンジニアとして20年以上、10,000件を超える顧客訪問と2,000件を超える単独ソリューション実績。AIツールを使った個人開発やIoT農園など、Raspberry Piを使ったオートメーション化なども実践中です!エンジニア専門結婚相談所も運営中、ClaudeCodeで解決できない心の課題も解決いたします!
META-MARK × AI
ローカルAIを動かすGPU、ちゃんと選べていますか?
VRAM・性能・コスパをMetaScoreで数値化。AIアプリ別の推奨ハードウェア要件も確認できます。