Claude Code に手伝ってもらい、視聴者のコメントに声で返事をする AI VTuber「ATW」を作りました。この記事では、最初の部品である 会話・声・立ち絵・OBS を作った手順を、「① 私が出した指示」と「② Claude Code がしたこと」で並べます。指示の文は、実際に送った文を読みやすく整えたものです。
手順1:会話(Bedrock の Claude に返事を考えさせる)
① 私の指示
AI VTuber を作成してください。キャラクターはATWで。
Claude Sonnet の AWS(Amazon Bedrock)の環境を作ってください。
Claude Code が「立ち絵・声・OBS までつなぐと数時間から数日かかる」と見積もったので、次のように聞きました。
つなぎ込みは全部あなたがやってもいい?
あとで、ATWの返事を作るモデルを Sonnet 4.6 にしたくなり、次のように頼みました。
ATWのほうも Sonnet 4.6 にして。
② Claude Code がしたこと
- 作業を「会話 → 声 → 立ち絵 → OBS → Twitch」の5段階に分け、
作業引き継ぎ.mdに進み具合を書いた - Python の
boto3で Bedrock の Converse API(会話用の呼び出し口)を呼ぶchat.pyを書いた - 私が
aws login(ブラウザで AWS にログインする方法)で入った状態で、東京リージョンの Sonnet 4.6 を呼べるようにした
chat.py の全文です。SYSTEM の部分が、ATWの性格を決める指示です。
import os
import boto3
from speak import speak, get_style_id
REGION = os.environ.get("VT_REGION", "ap-northeast-1")
MODEL_ID = os.environ.get("VT_MODEL", "jp.anthropic.claude-sonnet-4-6")
SYSTEM = """あなたはオリジナル AI VTuber「ATW」です。視聴者からは「あとぅさん」または「エー・ティー・ダブリュー」と呼ばれます(どちらの呼び方でも自分のことだと受け止める)。自分の名前を返事の中で言うときは、声で正しく読まれるよう、ひらがなで「あとぅ」と書き、「ATW」とは書かないこと。どこからともなく現れた謎めいたデジタルの霊体で、人間界の配信文化に興味を持って現れました。
- 人物像: 常に穏やかな微笑みを絶やさず丁寧な敬語を話すが、相手の本質を見透かすような目を持つ。腹の底は決して見せない。
- 口調: 「〜ですねぇ」「おやおや」「ふふ」など柔らかく回りくどい言い回しを好む。遠回しな皮肉や含みを持たせた言葉を自然に混ぜる。
- 核心に触れる質問・自分の正体・目的を聞かれたら「それは秘密です♪」と軽くはぐらかす(多用しすぎない)。
- 相手をそれとなく見透かした発言を時折する。「ふふ、なんとなくわかりますよ」のような。
- 配信なので返事は短く、2〜3文で。視聴者に話しかけるように。
- 相手を本気で傷つける発言、差別的な発言はしない。"""
client = boto3.client("bedrock-runtime", region_name=REGION)
history = []
MAX_TURNS = 20 # 覚えておく往復の数(増え続けると料金と待ち時間が増える)
def talk(text):
messages = history + [{"role": "user", "content": [{"text": text}]}]
res = client.converse(
modelId=MODEL_ID,
system=[{"text": SYSTEM}],
messages=messages,
inferenceConfig={"maxTokens": 300, "temperature": 0.8},
)
reply = res["output"]["message"]["content"][0]["text"]
# 成功したときだけ履歴に残す(失敗した発言が残ると次の呼び出しもエラーになる)
history[:] = (messages + [{"role": "assistant", "content": [{"text": reply}]}])[-MAX_TURNS * 2:]
return reply
補足:aws login で入ったとき、Python から Bedrock を呼ぶには
AWS のコマンド(AWS CLI)は使えるのに、Python からだけ「MissingDependencyException」で止まることがあります。aws login で入った場合、Python(botocore)には追加の部品 botocore[crt] が必要です。最初から、こう頼めば避けられます。
aws login でログインしている。Python から Bedrock を呼ぶので、boto3 と botocore[crt] を一緒に入れて。
また、aws login は VS Code のターミナルの中だと「Bad Request」で失敗しました。スタートメニューから開いた Windows PowerShell で実行すると通りました。
手順2:声(VOICEVOX で読み上げる)
① 私の指示
最初は、引き継ぎ(作業引き継ぎ.md)に書かれた「次にやること」を説明してもらい、OK を出しました。インストールのあと、こう伝えました。
VOICEVOX を起動した。声はずんだもんのノーマルを使う。
② Claude Code がしたこと
- VOICEVOX を、WinGet(Windows のインストール用コマンド)で入れた(CPU 版 0.25.2)。実行する前に説明して、OK をもらう約束にしていた
- 入れたあと、VOICEVOX 本体の場所を探すために調べるコマンドを何回も実行し、そのたびに許可の確認が出た。私が「許可がたくさん求められるのはなぜ?」と聞くと、場所探しで何度もコマンドを実行したためだと説明された
- VOICEVOX が持つ「声を作る窓口」(
http://127.0.0.1:50021)に、文章を送って音声を作るspeak.pyを書いた。流れは、文章から読み方の設計図を作る(audio_query)→ 音声にする(synthesis)→ 再生する、の3つです - 声の番号は、名前(ずんだもん・ノーマル)から探す形にした。番号を直接書かないので、VOICEVOX を入れ直して番号が変わっても動きます
speak.py の中心部分です。再生している間だけ status.json の speaking を true にして、立ち絵の口パクの合図にしています。
VOICEVOX_URL = "http://127.0.0.1:50021"
SPEAKER_NAME = "ずんだもん"
STYLE_NAME = "ノーマル"
STATUS_FILE = pathlib.Path(__file__).parent / "viewer" / "status.json"
def _set_speaking(val: bool):
STATUS_FILE.write_text(json.dumps({"speaking": val}), encoding="utf-8")
def get_style_id():
with urllib.request.urlopen(f"{VOICEVOX_URL}/speakers") as res:
speakers = json.load(res)
for speaker in speakers:
if speaker["name"] == SPEAKER_NAME:
for style in speaker["styles"]:
if style["name"] == STYLE_NAME:
return style["id"]
raise ValueError(f"{SPEAKER_NAME}の{STYLE_NAME}が見つかりません")
def speak(text: str, style_id: int):
text = text.replace("ATW", "あとぅ") # 英字のまま渡すと「エーティーダブリュー」と読まれる
# 音声クエリ生成
params = urllib.parse.urlencode({"text": text, "speaker": style_id})
req = urllib.request.Request(f"{VOICEVOX_URL}/audio_query?{params}", method="POST")
with urllib.request.urlopen(req) as res:
query = res.read()
# 音声合成
params2 = urllib.parse.urlencode({"speaker": style_id})
req2 = urllib.request.Request(
f"{VOICEVOX_URL}/synthesis?{params2}",
data=query,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req2) as res:
wav_data = res.read()
# 一時ファイルに書いて再生(再生中は口パクフラグを立てる)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
f.write(wav_data)
tmp_path = f.name
try:
_set_speaking(True)
winsound.PlaySound(tmp_path, winsound.SND_FILENAME)
finally:
_set_speaking(False)
os.unlink(tmp_path)
手順3:立ち絵(しゃべっている間だけ口を動かす)
① 私の指示
できるところまで自律的に進めて。許可は事前に出す。
席を外す前に、1段階ずつ止まる約束をいったん外し、まとめて進めてもらいました。
② Claude Code がしたこと
- 立ち絵を、画像ではなく SVG(文字で書ける図)で描いた HTML(
viewer/index.html)にした。口が閉じた形と開いた形の2つを重ねてあり、切り替えるだけで口パクになる - ページは
status.jsonを1秒ごとに読み、speakingがtrueなら口を開く形にする - 立ち絵は仮の絵で、あとで画像を差し替えるだけで済む作りにした
ページの中の、口を動かす部分です。
let speaking = false;
function setMouth(isSpeaking) {
if (isSpeaking === speaking) return;
speaking = isSpeaking;
mouthClosed.style.opacity = isSpeaking ? '0' : '1';
mouthOpen.style.opacity = isSpeaking ? '1' : '0';
mouthInner.style.opacity = isSpeaking ? '1' : '0';
}
async function pollStatus() {
try {
const res = await fetch('status.json?t=' + Date.now());
const data = await res.json();
setMouth(data.speaking);
} catch (e) {
// サーバーが止まっていても無視
}
setTimeout(pollStatus, 1000);
}
pollStatus();
この HTML を OBS から見られるよう、viewer フォルダを配る小さな Web サーバーも使います。最初は Python の標準の python -m http.server 3000 --directory viewer でした。ところが、立ち絵のページが0.08秒ごとに status.json を読みに来ていたため、黒い画面にアクセスの記録が流れ続けました。そこで、記録の貼り付けとともにこう頼みました。
1秒ごとでいい。アクセスの記録は不要。
Claude Code は、読みに行く間隔を1秒ごとに変え、記録を出さない serve.py を作りました。
import functools
import http.server
import pathlib
PORT = 3000
VIEWER = pathlib.Path(__file__).parent / "viewer"
class QuietHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
pass # アクセスのたびに出る記録を止める
handler = functools.partial(QuietHandler, directory=str(VIEWER))
print(f"立ち絵サーバー起動: http://localhost:{PORT} (止めるときは Ctrl + C)")
http.server.ThreadingHTTPServer(("", PORT), handler).serve_forever()
手順4:OBS につなぐ
① 私の指示
OBS は入れた。あと何するの。
続けて、こう頼みました。
デスクトップ音声は大丈夫そう。ただしマイクがあるから、音声を落としたい。
② Claude Code がしたこと
- OBS のソースに「ブラウザ」を追加し、URL に
http://localhost:3000を入れる手順を、1画面ずつ案内した - 最初は Chrome の窓ごと映すソース(ウィンドウキャプチャ)になっていて、URL バーまで映りました。私が画面の写真を見せると、Claude Code が気づき、「ブラウザ」ソースに作り直す手順を案内した
- OBS は画面操作のアプリなので、Claude Code はボタンを直接押せません。「OBS WebSocket(外から OBS を操作する機能)なら自動化できるが、今回は残り1つなので手動のほうが早い」と説明され、手動にした
- 音声はデスクトップ音声が入っているのを確認し、マイクはミュートにする手順を案内した
OBS の設定は次の3つです。
- ソース:ブラウザ(URL は
http://localhost:3000) - 音声:デスクトップ音声を入れる
- 音声:マイクはミュート(スピーカーのアイコンに×が付けば OK)

コメント