自分の声の棒読みちゃんを作る?!Windowsローカル構築で行うQwen3-TTS Base入門編

Qwen3-TTS Base とは?何者?

Qwen3-TTS-12Hz-1.7B-Base / Qwen3-TTS-12Hz-0.6B-Base は、Alibaba が公開している Qwen3-TTS ファミリーの中でも「高品質ボイスクローン用のベースモデル」です。
特徴をざっくり言うと:

  • 参照音声(自分の声)+その文字起こしを渡すと、かなりそれっぽい声質で喋ってくれる
  • マルチリンガル対応で、日本語も自然なイントネーション

という特徴があります。実は声優さんの声でもいけるらしい?です。
好きなセリフを、好きな声で、好きなタイミングで作成できるのが特徴です。

今回はこの Qwen3 TTS Base を Windows 11 上でローカル実行し、PowerShell や既存ツールから「棒読みちゃん互換」で叩ける TTS サーバに仕立てます。
最終的には:

  • http://localhost:50081/Talk に POST でテキストを投げる
    → 棒読みちゃんは 50080 ですが、あえて同時起動できるように +1 しています
  • キューに溜めて、1件ずつ音声生成 → ファイル保存 → 再生
  • 自分の声クローンで、テンポ速め・ピッチ維持・末尾ブツ切れ対策済の音声

という「なんちゃって棒読みちゃん(中身はQwen3)」が動くところまで行きます。
今回はCPUベースで行いますが、GPUでの動作でもできるように随時コメントを追加しています。


1. 環境準備:Windows 11 上に Python + ライブラリを整える

1-1. Python 3.10〜3.12 を用意する

Qwen3-TTS と onnxruntime の組み合わせは、最新すぎる Python(3.13〜)だとホイールがなくてハマります。
安全圏は 3.10〜3.12 なので、以下のどれかで揃えておきます:

  1. 公式サイトから Python 3.12 x64 をインストール
  2. 既に 3.12 が入っているなら、それを使って仮想環境を作ることもできます。

1-2. 作業フォルダと仮想環境の作成

PowerShell を開いて、下記のようなパスを実行します。太字のコマンドを実行してください。

powershellPS C:\> mkdir C:\Qwen3TTS
PS C:\> cd C:\Qwen3TTS

# Python 3.12 のパスが通ていない場合は環境に合わせて変更
PS C:\Qwen3TTS> python.exe -m venv .venv
PS C:\Qwen3TTS> .\.venv\Scripts\Activate.ps1
(.venv) PS C:\Qwen3TTS> python -V

# → 3.12.x を確認

以降はこの仮想環境上で作業します。

1-3. 必要ライブラリのインストール

TTS 本体+音声入出力+Web サーバをまとめて入れます。太字のコマンドを実行してください。

(.venv) PS C:\Qwen3TTS> python -m pip install -U pip

# PyTorch(CPU版)。GPUがあればCUDA版でもOK pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128

(.venv) PS C:\Qwen3TTS> pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu

# onnxruntime(qwen-tts が要求)
(.venv) PS C:\Qwen3TTS> pip install onnxruntime

# Qwen3-TTS ラッパー
(.venv) PS C:\Qwen3TTS> pip install qwen-tts

# 音声入出力
(.venv) PS C:\Qwen3TTS> pip install soundfile sounddevice

# Webサーバ(棒読みちゃん互換API用)
(.venv) PS C:\Qwen3TTS> pip install fastapi uvicorn

# ピッチを変えずに話速を変えるためのタイムストレッチ
(.venv) PS C:\Qwen3TTS> pip install --upgrade audiostretchy

ここまで通れば、ライブラリの地獄はほぼ脱出です。


2. 自分の声を録音して「参照音声+スクリプト」を用意する

2-1. 参照音声(my_voice_sample.wav)を作成

  1. Windows の「ボイスレコーダー」などで、10秒程度の音声を録音します
  2. 内容はハッキリ・一定のトーンで、環境ノイズ少なめが理想
    • 例:
      「春の朝、私は7時30分に起き、白いカップで熱いコーヒーを飲みました。今日は2026年1月27日、天気は晴れ、気温は6度です。静かな部屋で深呼吸し、落ち着いた声で一文ずつ、正確に読み上げます。」
  3. 録音を WAV(16bit/24bit, 44.1kHz〜48kHz)として保存し、
    C:\Qwen3TTS\my_voice_sample.wav という名前にします。

2-2. 文字起こし(REF_TEXT)を用意する

録音するときに使用した文を、誤字なしで1行の文字列にします。この分は後でPythonスクリプトに組み込みます。これは Qwen3-TTS が「この音声はこの文章を読んでいる」と認識するためのラベルで、クローン精度を上げるために必要です。


3. まずは単発テスト:0.6B-Base で自分の声クローン

本格的なサーバ構築の前に、スタンドアロンで 1ファイルだけ喋らせてみます。
C:\Qwen3TTS\qwen3_voice_clone_06b.py を作成して、以下の内容を保存します。

# file: qwen3_voice_clone_06b.py
import torch
import soundfile as sf
from qwen_tts import Qwen3TTSModel

MODEL_ID = "Qwen/Qwen3-TTS-12Hz-0.6B-Base" # GPUがあれば "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
DEVICE_MAP = "cpu" # GPUがあれば "cuda:0"
DTYPE = torch.float32 # GPUなら torch.bfloat16 でもOK

REF_AUDIO = "my_voice_sample.wav"

# 録音するときに使用した文ここで組み込み
REF_TEXT = (
"春の朝、私は7時30分に起き、白いカップで熱いコーヒーを飲みました。"
"今日は2026年1月27日、天気は晴れ、気温は6度です。"
"静かな部屋で深呼吸し、落ち着いた声で一文ずつ、正確に読み上げます。"
)


# 出力する文字を指定
TARGET_TEXT = "こんにちは!私はAIです。すごい時代になりましたね。漢字を読むのは苦手です。"
OUTPUT_WAV = "my_clone_voice_06b.wav"

def main():
print("モデル読み込み中...")
model = Qwen3TTSModel.from_pretrained(
MODEL_ID,
device_map=DEVICE_MAP,
dtype=DTYPE,
)

print("ボイスクローン用プロンプト作成中...")
prompt = model.create_voice_clone_prompt(
ref_audio=REF_AUDIO,
ref_text=REF_TEXT,
x_vector_only_mode=False,
)

print("音声生成中...")
wavs, sr = model.generate_voice_clone(
text=TARGET_TEXT,
voice_clone_prompt=prompt,
language="Japanese",
)

print("ファイル保存中:", OUTPUT_WAV)
sf.write(OUTPUT_WAV, wavs[0], sr)

print("完了:", OUTPUT_WAV, "sr:", sr)

if __name__ == "__main__":
main()

実行:

(.venv) PS C:\Qwen3TTS> python qwen3_voice_clone_06b.py

同じフォルダに my_clone_voice_06b.wav ができるので、再生して「自分っぽく聞こえるか」をチェック。
ここまでOKなら、次は「高速化&サーバ化」に進みます。


4. 高速化:プロンプトキャッシュ+ピッチ維持の速度変更

Qwen3 TTS Base はそれなりに重いので、毎回 create_voice_clone_prompt を呼ぶと結構待たされます。
そこで:

  • 参照音声から作った voice_clone_prompt をファイルに保存
  • 起動時・次回以降はキャッシュを読み込むだけ
  • 生成された WAV に対して、ピッチ保持タイムストレッチでテンポだけ速く

というパターンにします。

これは棒読みちゃんサーバ側に最初から組み込むので、ここではイメージだけ押さえておけばOKです。


5. 棒読みちゃん互換サーバを組み立てる

いよいよ本番。C:\Qwen3TTS\bouyomi_qwen3_server.py を作成し、以下を丸ごと入れます。

# file: bouyomi_qwen3_server.py
import os
import pickle
import threading
import queue# file: bouyomi_qwen3_server.py
import os
import pickle
import threading
import queue
import time
from typing import Optional

import numpy as np
import torch
import soundfile as sf
import sounddevice as sd
from fastapi import FastAPI, Form
from fastapi.responses import PlainTextResponse
from qwen_tts import Qwen3TTSModel
from audiostretchy.stretch import stretch_audio # ピッチ保持タイムストレッチ

# ===== Qwen3-TTS 設定 =====

MODEL_ID = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
DEVICE_MAP = "cpu" # GPUがあれば "cuda:0"
DTYPE = torch.float32 # GPUなら torch.bfloat16

REF_AUDIO = "my_voice_sample.wav"
REF_TEXT = (
"春の朝、私は7時30分に起き、白いカップで熱いコーヒーを飲みました。"
"今日は2026年1月27日、天気は晴れ、気温は6度です。"
"静かな部屋で深呼吸し、落ち着いた声で一文ずつ、正確に読み上げます。"
)

OUTPUT_DIR = "outputs"

MODEL_SAFE_NAME = MODEL_ID.replace("/", "_").replace(":", "_")
PROMPT_CACHE_PATH = f"voice_prompt_{MODEL_SAFE_NAME}.pt"

SPEED_FACTOR = 1.2 # 1.2倍速(テンポだけ速く)
FADE_OUT_SEC = 0.15
TAIL_SILENCE_SEC = 0.25

SERVER_PORT = 50081 # 必要に応じて変更

# ===== グローバル状態 =====

app = FastAPI()

qwen_model: Optional[Qwen3TTSModel] = None
voice_clone_prompt = None

play_queue: "queue.Queue[dict]" = queue.Queue()
player_thread_started = False
player_thread_lock = threading.Lock()


# ===== Qwen3-TTS 関連 =====

def load_model() -> Qwen3TTSModel:
global qwen_model
if qwen_model is None:
print(f"[Qwen3] モデル読み込み中... ({MODEL_ID})")
qwen_model = Qwen3TTSModel.from_pretrained(
MODEL_ID,
device_map=DEVICE_MAP,
dtype=DTYPE,
)
return qwen_model


def build_or_load_voice_prompt(model: Qwen3TTSModel):
global voice_clone_prompt
if voice_clone_prompt is not None:
return voice_clone_prompt

if os.path.exists(PROMPT_CACHE_PATH):
print(f"[Qwen3] 既存プロンプトを読み込み中... ({PROMPT_CACHE_PATH})")
with open(PROMPT_CACHE_PATH, "rb") as f:
voice_clone_prompt = pickle.load(f)
return voice_clone_prompt

print("[Qwen3] ボイスクローン用プロンプト作成中...")
voice_clone_prompt = model.create_voice_clone_prompt(
ref_audio=REF_AUDIO,
ref_text=REF_TEXT,
x_vector_only_mode=False,
)

print(f"[Qwen3] プロンプトをキャッシュに保存中... ({PROMPT_CACHE_PATH})")
with open(PROMPT_CACHE_PATH, "wb") as f:
pickle.dump(voice_clone_prompt, f)

return voice_clone_prompt


def apply_tail_fade_and_silence(wav: np.ndarray, sr: int) -> np.ndarray:
"""末尾フェード+無音で「ブツッ」と切れるのを防ぐ。"""
wav = wav.astype(np.float32)

fade_samples = int(FADE_OUT_SEC * sr)
if fade_samples > 0 and fade_samples < wav.shape[0]:
fade = np.linspace(1.0, 0.0, fade_samples, endpoint=True, dtype=np.float32)
start = wav.shape[0] - fade_samples
wav[start:] *= fade

silence_samples = int(TAIL_SILENCE_SEC * sr)
if silence_samples > 0:
silence = np.zeros(silence_samples, dtype=np.float32)
wav = np.concatenate([wav, silence], axis=0)

return wav


def tts_generate_to_file(text: str, index: int) -> str:
"""テキストからWAVを生成してファイル保存し、そのパスを返す。"""
os.makedirs(OUTPUT_DIR, exist_ok=True)

model = load_model()
prompt = build_or_load_voice_prompt(model)

print(f"[Qwen3] 音声生成中... (#{index}) text={text[:30]}...")
wavs, sr = model.generate_voice_clone(
text=text,
voice_clone_prompt=prompt,
language="Japanese",
)
wav = wavs[0].astype(np.float32)

# 末尾処理
wav = apply_tail_fade_and_silence(wav, sr)

# [-1, 1] を 16bit PCM スケールにクリップ・変換
peak = np.max(np.abs(wav)) if wav.size > 0 else 0.0
if peak > 1.0:
wav = wav / peak
wav_int16 = np.int16(np.clip(wav, -1.0, 1.0) * 32767)

# 等速版を一時ファイルに16bitで保存
base_path = os.path.join(OUTPUT_DIR, f"bouyomi_base_{index:05d}.wav")
sf.write(base_path, wav_int16, sr, subtype="PCM_16")

# ピッチを維持したまま速度変更
if SPEED_FACTOR != 1.0:
ratio = 1.0 / SPEED_FACTOR # 速く → ratio < 1.0
print(f"[Qwen3] ピッチ保持タイムストレッチ中... ratio={ratio:.3f}")
out_path = os.path.join(OUTPUT_DIR, f"bouyomi_{index:05d}.wav")
stretch_audio(
base_path,
out_path,
ratio=ratio,
)
else:
out_path = os.path.join(OUTPUT_DIR, f"bouyomi_{index:05d}.wav")
os.replace(base_path, out_path)

print(f"[Qwen3] ファイル保存完了: {out_path}")
return out_path


def play_wav_file(path: str):
"""WAVファイルを再生して、再生完了までブロック。"""
print(f"[Player] 再生開始: {path}")
# ここも 16bit PCM 前提の読み出しでOK
data, sr = sf.read(path, dtype="int16")
data = data.astype(np.float32) / 32767.0 # sounddevice に float32 で渡す
sd.stop()
sd.play(data, sr)
sd.wait()
sd.stop()
print(f"[Player] 再生終了: {path}")


# ===== 再生スレッド =====

def player_loop():
index = 1
while True:
item = play_queue.get()
try:
text = item.get("text", "")
req_id = item.get("id", index)

if not text:
print("[Player] 空テキストをスキップ")
continue

print(f"[Player] キュー処理開始: id={req_id}, text={text[:30]}...")
wav_path = tts_generate_to_file(text, index=req_id)
play_wav_file(wav_path)
print(f"[Player] キュー処理完了: id={req_id}")
index += 1

except Exception as e:
print("[Player] エラー:", e)
finally:
play_queue.task_done()


def ensure_player_thread_running():
global player_thread_started
with player_thread_lock:
if not player_thread_started:
print("[Player] 再生スレッド起動")
t = threading.Thread(target=player_loop, daemon=True)
t.start()
player_thread_started = True


# ===== FastAPI エンドポイント =====

@app.post("/Talk", response_class=PlainTextResponse)
async def talk(
text: str = Form(...),
speed: int = Form(0),
tone: int = Form(0),
volume: int = Form(0),
voice: int = Form(0),
command: int = Form(0),
):
"""棒読みちゃんの /Talk っぽいインタフェース。"""
if not text:
return "NG"

ensure_player_thread_running()

req_id = int(time.time() * 1000)
item = {"text": text, "id": req_id, "created_at": time.time()}
play_queue.put(item)

print(f"[API] キュー追加: id={req_id}, text={text[:30]}... (queue size={play_queue.qsize()})")
return "OK"


@app.get("/health", response_class=PlainTextResponse)
async def health():
return "OK"


if __name__ == "__main__":
# 起動時にモデルとプロンプトをロード
m = load_model()
build_or_load_voice_prompt(m)

import uvicorn
uvicorn.run(app, host="0.0.0.0", port=SERVER_PORT)
import time
from typing import Optional

import numpy as np
import torch
import soundfile as sf
import sounddevice as sd
from fastapi import FastAPI, Form
from fastapi.responses import PlainTextResponse
from qwen_tts import Qwen3TTSModel
from audiostretchy.stretch import stretch_audio

# ===== Qwen3-TTS 設定 =====

MODEL_ID = "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
DEVICE_MAP = "cpu" # GPUがあれば "cuda:0"
DTYPE = torch.float32 # GPUなら torch.bfloat16

REF_AUDIO = "my_voice_sample.wav"
REF_TEXT = (
"春の朝、私は7時30分に起き、白いカップで熱いコーヒーを飲みました。"
"今日は2026年1月27日、天気は晴れ、気温は6度です。"
"静かな部屋で深呼吸し、落ち着いた声で一文ずつ、正確に読み上げます。"
)

OUTPUT_DIR = "outputs"

MODEL_SAFE_NAME = MODEL_ID.replace("/", "_").replace(":", "_")
PROMPT_CACHE_PATH = f"voice_prompt_{MODEL_SAFE_NAME}.pt"

SPEED_FACTOR = 1.2 # 1.2倍速(テンポだけ速く)
FADE_OUT_SEC = 0.15
TAIL_SILENCE_SEC = 0.25

SERVER_PORT = 50081 # 使用ポート(必要なら変更)

# ===== グローバル状態 =====

app = FastAPI()

qwen_model: Optional[Qwen3TTSModel] = None
voice_clone_prompt = None

play_queue: "queue.Queue[dict]" = queue.Queue()
player_thread_started = False
player_thread_lock = threading.Lock()


# ===== Qwen3-TTS 関連 =====

def load_model() -> Qwen3TTSModel:
global qwen_model
if qwen_model is None:
print(f"[Qwen3] モデル読み込み中... ({MODEL_ID})")
qwen_model = Qwen3TTSModel.from_pretrained(
MODEL_ID,
device_map=DEVICE_MAP,
dtype=DTYPE,
)
return qwen_model


def build_or_load_voice_prompt(model: Qwen3TTSModel):
global voice_clone_prompt
if voice_clone_prompt is not None:
return voice_clone_prompt

if os.path.exists(PROMPT_CACHE_PATH):
print(f"[Qwen3] 既存プロンプトを読み込み中... ({PROMPT_CACHE_PATH})")
with open(PROMPT_CACHE_PATH, "rb") as f:
voice_clone_prompt = pickle.load(f)
return voice_clone_prompt

print("[Qwen3] ボイスクローン用プロンプト作成中...")
voice_clone_prompt = model.create_voice_clone_prompt(
ref_audio=REF_AUDIO,
ref_text=REF_TEXT,
x_vector_only_mode=False,
)

print(f"[Qwen3] プロンプトをキャッシュに保存中... ({PROMPT_CACHE_PATH})")
with open(PROMPT_CACHE_PATH, "wb") as f:
pickle.dump(voice_clone_prompt, f)

return voice_clone_prompt


def apply_tail_fade_and_silence(wav: np.ndarray, sr: int) -> np.ndarray:
"""末尾フェード+無音で「ブツッ」と切れるのを防ぐ。"""
wav = wav.astype(np.float32)

fade_samples = int(FADE_OUT_SEC * sr)
if fade_samples > 0 and fade_samples < wav.shape[0]:
fade = np.linspace(1.0, 0.0, fade_samples, endpoint=True, dtype=np.float32)
start = wav.shape[0] - fade_samples
wav[start:] *= fade

silence_samples = int(TAIL_SILENCE_SEC * sr)
if silence_samples > 0:
silence = np.zeros(silence_samples, dtype=np.float32)
wav = np.concatenate([wav, silence], axis=0)

return wav


def tts_generate_to_file(text: str, index: int) -> str:
"""テキストからWAVを生成してファイル保存し、そのパスを返す。"""
os.makedirs(OUTPUT_DIR, exist_ok=True)

model = load_model()
prompt = build_or_load_voice_prompt(model)

print(f"[Qwen3] 音声生成中... (#{index}) text={text[:30]}...")
wavs, sr = model.generate_voice_clone(
text=text,
voice_clone_prompt=prompt,
language="Japanese",
)
wav = wavs[0].astype(np.float32)

# 末尾処理
wav = apply_tail_fade_and_silence(wav, sr)

# 等速版を一時ファイルに保存
base_path = os.path.join(OUTPUT_DIR, f"bouyomi_base_{index:05d}.wav")
sf.write(base_path, wav, sr, subtype="FLOAT")

# ピッチを維持したまま速度変更
if SPEED_FACTOR != 1.0:
ratio = 1.0 / SPEED_FACTOR # 速く → ratio < 1.0
print(f"[Qwen3] ピッチ保持タイムストレッチ中... ratio={ratio:.3f}")
out_path = os.path.join(OUTPUT_DIR, f"bouyomi_{index:05d}.wav")
stretch_audio(
base_path,
out_path,
ratio=ratio,
)
else:
out_path = os.path.join(OUTPUT_DIR, f"bouyomi_{index:05d}.wav")
os.replace(base_path, out_path)

print(f"[Qwen3] ファイル保存完了: {out_path}")
return out_path


def play_wav_file(path: str):
"""WAVファイルを再生して、再生完了までブロック。"""
print(f"[Player] 再生開始: {path}")
data, sr = sf.read(path, dtype="float32")
sd.stop()
sd.play(data, sr)
sd.wait()
sd.stop()
print(f"[Player] 再生終了: {path}")


# ===== 再生スレッド =====

def player_loop():
index = 1
while True:
item = play_queue.get()
try:
text = item.get("text", "")
req_id = item.get("id", index)

if not text:
print("[Player] 空テキストをスキップ")
continue

print(f"[Player] キュー処理開始: id={req_id}, text={text[:30]}...")
wav_path = tts_generate_to_file(text, index=req_id)
play_wav_file(wav_path)
print(f"[Player] キュー処理完了: id={req_id}")
index += 1

except Exception as e:
print("[Player] エラー:", e)
finally:
play_queue.task_done()


def ensure_player_thread_running():
global player_thread_started
with player_thread_lock:
if not player_thread_started:
print("[Player] 再生スレッド起動")
t = threading.Thread(target=player_loop, daemon=True)
t.start()
player_thread_started = True


# ===== FastAPI エンドポイント =====

@app.post("/Talk", response_class=PlainTextResponse)
async def talk(
text: str = Form(...),
speed: int = Form(0),
tone: int = Form(0),
volume: int = Form(0),
voice: int = Form(0),
command: int = Form(0),
):
"""棒読みちゃんの /Talk っぽいインタフェース。"""
if not text:
return "NG"

ensure_player_thread_running()

req_id = int(time.time() * 1000)
item = {"text": text, "id": req_id, "created_at": time.time()}
play_queue.put(item)

print(f"[API] キュー追加: id={req_id}, text={text[:30]}... (queue size={play_queue.qsize()})")
return "OK"


@app.get("/health", response_class=PlainTextResponse)
async def health():
return "OK"


if __name__ == "__main__":
# 起動時にモデルとプロンプトをロード
m = load_model()
build_or_load_voice_prompt(m)

import uvicorn
uvicorn.run(app, host="0.0.0.0", port=SERVER_PORT)

6. サーバの起動とテスト

6-1. サーバ起動

(.venv) PS C:\Qwen3TTS> python bouyomi_qwen3_server.py
  • 初回はモデルのダウンロード&プロンプト作成が入るので少し待ちます。
  • Application startup complete. 的なメッセージが出たら待ち受け開始です。
  • Windowsファイヤウォールのメッセージが出たら許可してください。

6-2. テストで /Talk を叩く

別の PowerShell から:

C:\Qwen3TTS> Invoke-WebRequest -Uri "http://localhost:50081/Talk" -Method POST -Body @{ text = "テスト1です。Qwen3-TTSで喋っています。" }

PS C:\Qwen3TTS> Invoke-WebRequest -Uri "http://localhost:50081/Talk" -Method POST -Body @{ text = "テスト2です。キューに溜めた2件目です。" }
  • サーバ側コンソールには [API] キュー追加 → [Player] キュー処理開始 → [Qwen3] 音声生成中... → [Player] 再生開始 というログが順番に流れます。
  • 音声は 1件ずつ、前の再生が終わってから次が流れます。
  • WAV は outputs フォルダに bouyomi_*.wav として全部残るので、あとから確認も可能です。

7. 面白いですね、これ

AI(LLM)が出てきて、一気にいろいろなものが進んできており、すごい時代になったものです。

今回の構築では「ローカルの Windows マシンが、自分専用の“喋るエージェント”になった」という点がすごいことだと思います。

Qwen3-TTS-12Hz-1.7B-Base は、クラウドAPIを叩かなくても、自分の声そっくりの音声をかなり高いクオリティで生成してくれます。そこに FastAPI で HTTP サーバをかぶせ、棒読みちゃん互換の /Talk エンドポイントを用意したことで、既存のスクリプトやツールから「棒読みちゃんに送るノリのまま」自分クローンボイスを呼び出せるようになりました。

内部では、参照音声+文字起こしから生成したボイスクローンプロンプトをキャッシュしておき、毎回のリクエストではそれを再利用して低オーバーヘッドで生成する構成にしています。生成後の WAV には末尾のフェードアウトと無音を足し、さらにタイムストレッチでピッチを維持したままテンポだけ速くしているので、「早口だけどブツ切れない、ちゃんと滑らかな読み上げ」になっているのもポイントです。

ニュース読み上げ・チャットログ読み上げ・通知読み上げといった用途に、そのまま流用しやすい小さな音声基盤になりました。ここに「テキスト整形」や「感情プロンプト」を足していけば、もっと人間らしい喋り方や状況に応じたトーンの切り替えも狙えます。今回の構成をベースにして、

  • 別の声(別の参照音声)を増やして複数ボイスに対応する
  • クライアント側から voice や speed パラメータを受けて切り替える
  • OBS や配信ソフトと組み合わせて「自分ボイスの読み上げ配信」をする

といった方向にも簡単に伸ばしていけるはずです。

「棒読みちゃんを自分で作り直した」みたいな感覚で遊べるので、興味のある人はぜひ自分の Windows 環境でも試して、少しずつ好みの“自分ボイスエージェント”に育ててみてください。

PowerShell ISEで実行環境をWindows PowerShell 5からPowerShell 7に変更する方法

Windows標準で導入されていて、ちょっと検証するだけなら便利なPowerShell ISEですが、実行するバージョンが標準ではWindows PowerShell 5までとなっています。これをPowerShell 7でする方法が紹介されていたので試してみました。

Using PowerShell 7 in the Windows PowerShell ISE
https://blog.ironmansoftware.com/using-powershell-7-in-the-windows-powershell-ise/

まず、端末にPowerShell 7をインストールしていない場合、MSIパッケージなどで新規導入します。ダウンロードサイトは下記となり、今回は「PowerShell-7.5.0-win-x64.msi」をインストールしました。

Windows への PowerShell のインストール

ダウンロードしたファイルを実行して、そのまま進めていけば導入は完了します。途中のOptional Actionsの下二つに、必要があればチェックを入れてください。
「Add ‘Open here’ context menus to Explorer」にチェックw入れると右クリックのコンテキストメニューに「PowerShell 7」を追加され、「Add ‘Run with PowerShell 7’ context menu for PowerShell files」にチェックを入れると右クリックのメニューに「PowerShellで実行」が追加されます。

導入が完了したら、スタートメニューを押して「PowerShell ISE」と入力、PowerShell ISEを実行します。初期状態ではプロファイルがないため、下記で紹介されているコマンドを実行します。
Windows PowerShell ISE でプロファイルを使用する方法

if (!(Test-Path -Path $PROFILE )) { New-Item -Type File -Path $PROFILE -Force }

次に作成したプロファイルを編集するため、「psEdit $PROFILE」と入力して実行します。するとプロファイルが自動的に開かれて編集可能となります。

psEdit $PROFILE

開かれたプロファイルに、サイトで紹介されていた下記を貼り付けて保存すれば完成です。

$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Clear()
$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Switch to PowerShell 7", { 
        function New-OutOfProcRunspace {
            param($ProcessId)

            $ci = New-Object -TypeName System.Management.Automation.Runspaces.NamedPipeConnectionInfo -ArgumentList @($ProcessId)
            $tt = [System.Management.Automation.Runspaces.TypeTable]::LoadDefaultTypeFiles()

            $Runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($ci, $Host, $tt)

            $Runspace.Open()
            $Runspace
        }

        $PowerShell = Start-Process PWSH -ArgumentList @("-NoExit") -PassThru -WindowStyle Hidden
        $Runspace = New-OutOfProcRunspace -ProcessId $PowerShell.Id
        $Host.PushRunspace($Runspace)
}, "ALT+F5") | Out-Null

$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Switch to Windows PowerShell", { 
    $Host.PopRunspace()

    $Child = Get-CimInstance -ClassName win32_process | where {$_.ParentProcessId -eq $Pid}
    $Child | ForEach-Object { Stop-Process -Id $_.ProcessId }

}, "ALT+F6") | Out-Null

一度PowerShell ISEを終了させて、もう一度PowerShell ISEを実行すると、上部メニューのアドオンの所に“Switch to PowerShell 7”が追加されており、そちらをクリックすることでWindows PowerShell 5からPowerShell 7に切り替えることが出来るようになります。

PowerShell 7の方が、対応している.NETバージョンが新しいため、色々なことを試せるかと思います。本格的に作るまでもないようなものは、OS標準のISEで実行するのはアリですね。

ESXi-Customizer-PSを実行したら[WinError 10054]となりISO作成できなかったときの対処

ESXi-Customizer-PSは、VMware PowerCLIのモジュールを使用してカスタマイズされたESXiイメージを自動的に作成してくれる、便利なPowerShellスクリプトです。そしてカスタムESXiを使うとHyper-V上でESXiを動かすことができるなどと聞けば試さずにはいれません。

ESXi-Customizer-PSの使用方法は、 VMware PowerCLI をインストール後、下記サイトからESXi-Customizer-PS-v2.6.0.ps1をダウンロードして実行するだけのはずだったんですがうまくいきませんでした。
VMware Front Experience

行った手順としては、下記PowerShellでコマンドを実行しています。このコマンドでは -v67を指定しているのでESXi 6.7最新版に、Hyper-Vのネットワークアダプタのドライバ net-tulip を組み込んだものができるはずでした。
Install-Module -Name VMware.PowerCLI
Set-ExecutionPolicy RemoteSigned
.\ESXi-Customizer-PS-v2.6.0.ps1 -v67 -vft -load net-tulip

An unexpected error occured:
[WinError 10054] ?????????????????????????????? ????????????????????????????????????????????????

If requesting support please be sure to include the log file
C:\Users\ADMINI~1\AppData\Local\Temp\2\ESXi-Customizer-PS-17360.log

いろいろ調べてみると、最新のVMware.PowerCLIではうまく動かないことがあるみたいです。Install-ModuleコマンドでVersion 11.5.0.14912921をインストールしてしまったのが問題ですね。
まず最新の PowerCLI を下記コマンドでアンインストールします。
Uninstall-Module -Name VMware.PowerCLI
そしてEXE版の古いPowerCLIを用意。今回はVMware-vSphere-CLI-6.0.0-2503617.exeをインストールしてみました。そして同じようにESXiカスタマイズのコマンドを実行。

All done. と表示されうまくいきました。
ESXi-Customizer-PSのサイトには VMware PowerCLI version 5.1 or newer と書かれているんですが、なるべく書いてあるバージョンの最低のものを使用したほうが確実に使用できそうですね。

PowerShellにてCitrix XenDesktop7.6のDCに未登録マシンがないか確認する

最近のソリューションはPowerShellにて管理できるようでして、様々な作業を半自動化することが可能です。
そんなわけで、Citrix XenDesktop/XenAppで少しだけDeliveryController(DC)の状態を取得してみました。

とりあえずテストとして行ったものは、DC上でVDAの情報を取得して結果をイベントログに記録することです。
具体的に何がしたかったかというと、各VDAがどのくらいの頻度で再起動を繰り返しているかを記録することですが、スクリプト書いている途中でめんどくさくなりイベントログに書き込んで終わりにしています。

他にもいろいろ出来そうですが、そのうちいろいろやろうかと思った次第。

#Citrixのスナップインをすべて読み込む
asnp citrix*
#DCにマシンステータスがUnRegistered(未登録)のものがないか問い合わせる
$objUnregMachin = (Get-BrokerMachine -RegistrationState UnRegistered).MachineName
#変数objUnregMachinに何か入っていたらイベントログに書き込む
if ( $objUnregMachin -ne $null ){
[Diagnostics.EventLog]::WriteEntry("XenApp", "$objUnregMachin" , "Warning", 1)
}

備考:Get-BrokerMachine – Citrix eDocs
http://support.citrix.com/proddocs/topic/citrix-broker-admin-v2-xd76/get-brokermachine-xd76.html