"""ElevenLabs の Voice Design で、登場人物の声の候補を作り、気に入った候補を声として保存する。エイイチの Mac で動かす。 準備: ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY 使い方(_制作ベース のフォルダで) 候補を作る: python3 voice_design.py design 大城 → 声の候補/大城/候補1.mp3〜候補3.mp3 を聞き比べる python3 voice_design.py design 大城 --seed 12 (気に入るものがなければ seed を変えてもう一度) 保存する: python3 voice_design.py save 大城 2 → 候補2 を声にして、声の一覧.json に Voice ID を書く 人物の説明と試しの文は、下の CAST に書く(英語の説明の方が安定する) """ import argparse, base64, json, os, sys, urllib.request, urllib.error CAST = { "大城": dict( desc="A Japanese woman in her late twenties, a kind and cheerful hospital nurse. Clear, warm, natural " "conversational Japanese with a medium pitch, friendly and a little brisk. Native Japanese speaker with " "natural standard Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="空き箱、ぶつけないようにね。今日は申し送りが多いから、先にこっちの確認からいこう。ハコブくん、" "三階のナースステーションまでお願いね。ありがとう、助かったよ。次の患者さんの点滴、準備しておくね。それじゃあ、よろしくお願いします。"), "寺島": dict( desc="A Japanese woman in her mid forties, a seasoned, down-to-earth hospital nurse. Warm, slightly low pitch, " "practical and a little blunt but kind, natural everyday Japanese. Native Japanese speaker with " "natural standard Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="この箱、運ぶの手伝って。え、呼んでないよ? ちょっと、そこ通るよ。まったくもう、慌てなくていいからね。" "あとで倉庫の在庫も見ておいてくれる? 足りないものがあったら、私に言ってちょうだい。じゃあ、お願いね。"), "白石": dict( desc="A Japanese woman in her late twenties, a serious young robotics engineer. Polite, earnest and a little " "nervous, clear medium pitch, natural conversational Japanese. Native Japanese speaker with natural " "standard Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="今月から、ハコブくんの評価指標は配送件数です。えっと、ログを見ると、件数は先月の十倍です。" "ただ、ちょっと気になる点もあって。すみません、もう一度データを確認してから、改めてご報告しますね。よろしくお願いします。"), "槙原": dict( desc="A Japanese man in his late twenties, a calm and polite robotics engineer. Soft, clear, slightly low " "medium pitch, careful and earnest, natural conversational Japanese. Native Japanese speaker with natural " "standard Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="センサーの窓だけは、塞がないでください。飾り付けは大歓迎です。ただ、前を見るための窓がここにあるので、" "この丸い部分だけは空けておいてもらえると助かります。目印のシールを貼っておきますね。どうぞ、よろしくお願いします。"), "田中": dict( desc="A Japanese man in his mid thirties, a busy hospital doctor. Brisk, confident, slightly impatient but " "kind, medium-low pitch, natural conversational Japanese. Native Japanese speaker with natural standard " "Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="三〇三号室まで、急ぎで頼む。わかったから、早く。確認はもういいよ。次の患者さんが待ってるんだ。" "ああ、ごめん、さっきの検査結果、あとで病棟にも回しておいてくれるかな。よろしく。それじゃあ、また夕方の回診で。"), "小芝": dict( desc="A Japanese man in his mid twenties who has only just become a doctor after residency and has no confidence yet. " "Timid, hesitant and unsure of himself, soft and slightly thin voice, speaks a little quietly with small pauses " "as if checking whether he is right, light medium-high pitch, gentle and awkward but sincere. Natural conversational " "Japanese. Native Japanese speaker with natural standard Tokyo Japanese intonation, speaking Japanese. " "Not anime-like, not breathy, not a deep voice. Studio quality recording.", text="あの、三〇一号室に、ラシックスを……お願いします。あれ、さっきも言いましたっけ。すみません、えっと、" "ちょっと自信がなくて。大城さん、移動可能な経路の中で、という条件定義が……必要だった、かな。次からは、ちゃんと確認します。"), "高杉": dict( desc="A Japanese woman in her early thirties, a calm and polite hospital nurse. Gentle, clear, medium-high pitch, " "natural conversational Japanese. Native Japanese speaker with natural standard Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="はい、今行きます。こちらのお部屋で大丈夫ですよ。ゆっくりで構いませんからね。何かあったら、" "このボタンで呼んでください。夜中でも、すぐに来ますから。お大事になさってください。明日の朝も、また様子を見に来ますね。"), "中村": dict( # 2026-10-24 追加(入院患者の中村拓也さん・第72話・第84話。名前の出ない男性の患者にも使う) desc="A Japanese man in his early forties, an inpatient at a hospital. Friendly, relaxed and a little cheeky, " "medium-low pitch, casual everyday Japanese, a bit playful. Native Japanese speaker with natural standard " "Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="ちょっと、トイレどこ? いやあ、置いてかれるかと思ったよ。先生、俺より緊張してません? " "声まね、けっこう自信あるんですよ。プリン、まだかなあ。入院してると、楽しみがそれくらいしかなくてね。よろしく頼みますよ。"), "森田茂": dict( # 2026-10-24 追加(年配の男性。第65話の面会の人。名前の出ない年配の男性にも使う) desc="A Japanese man in his late sixties, a gentle and polite retired man visiting a hospital. Soft, slightly hoarse, " "low pitch, calm but a little unsure and hesitant, natural conversational Japanese. Native Japanese speaker with " "natural standard Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="すいません、三〇四号室は、どっちですか。家内の見舞いに来たんですが、どうも道がわからなくて。" "この病院は広いですなあ。ああ、ありがとうございます。助かりました。エレベーターは、あちらでよろしいですか。お世話になります。"), "森田久美子": dict( # 2026-10-24 追加(年配の女性。第71話の面会の久美子さん) desc="A Japanese woman in her mid sixties, a warm, talkative and frank housewife. Medium pitch, lively, a little brisk " "like a cheerful neighborhood aunt, natural everyday Japanese. Native Japanese speaker with natural standard " "Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="あら、ロボットさんは? 速いのよ、あなた。こっちは荷物持ってるんだから。そう、それでいいの。" "ゆっくりでいいのよ、ゆっくりで。うちの人の病室、もうすぐかしら。ありがとうね、ほんとに助かったわ。また来週も来るからね。"), "ハコブ_イケボ": dict( # 2026-10-24 追加(第17話。アップデートでなった「超重低音のイケメンボイス」のハコブくん) desc="A Japanese man with an extremely deep, rich bass voice, very handsome and theatrical, like a loyal samurai retainer " "in a period drama. Slow, dignified, smooth and a little over-the-top, very low pitch. Native Japanese speaker " "with natural standard Tokyo Japanese intonation, speaking Japanese. Not anime-like, not breathy. Studio quality recording.", text="御意。ただいま参上つかまつった、姫君。お申し付けとあらば、この身が朽ちるまで、どこへなりとも運びましょう。" "……御意。姫君の、望むままに。拙者、検体の搬送にかけては、誰にも引けを取りませぬ。いざ、参らん。"), } ap = argparse.ArgumentParser() ap.add_argument("mode", choices=["design", "save"]) ap.add_argument("who") ap.add_argument("pick", nargs="?", type=int) ap.add_argument("--seed", type=int, default=7) ap.add_argument("--model", default="eleven_ttv_v3") # 2026-10-19: 既定(multilingual_ttv_v2)では日本語の抑揚が不自然だったので v3 にした a = ap.parse_args() key = os.environ.get("ELEVENLABS_API_KEY") if not key: sys.exit("ELEVENLABS_API_KEY が入っていません。read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY を先に") if a.who not in CAST: sys.exit(f"CAST にいない名前です: {a.who}(いるのは {'・'.join(CAST)})") c = CAST[a.who] d = os.path.join("声の候補", a.who) os.makedirs(d, exist_ok=True) def post(url, body): req = urllib.request.Request(url, data=json.dumps(body).encode(), headers={"xi-api-key": key, "Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=180) as r: return json.loads(r.read()) except urllib.error.HTTPError as e: sys.exit(f"止まりました: {e.code} {e.read().decode(errors='ignore')[:400]}") if a.mode == "design": r = post("https://api.elevenlabs.io/v1/text-to-voice/design?output_format=mp3_44100_128", {"voice_description": c["desc"], "text": c["text"], "seed": a.seed, "model_id": a.model}) ids = [] for k, p in enumerate(r["previews"], 1): open(os.path.join(d, f"候補{k}.mp3"), "wb").write(base64.b64decode(p["audio_base_64"])) ids.append(p["generated_voice_id"]) json.dump({"seed": a.seed, "ids": ids}, open(os.path.join(d, "候補.json"), "w"), ensure_ascii=False, indent=1) print(f"{a.who} の候補を {len(ids)} つ作りました: {d}/候補1.mp3〜 聞いて、気に入った番号で python3 voice_design.py save {a.who} 番号") else: if not a.pick: sys.exit("保存する候補の番号を書いてください(例: python3 voice_design.py save 大城 2)") ids = json.load(open(os.path.join(d, "候補.json")))["ids"] r = post("https://api.elevenlabs.io/v1/text-to-voice", {"voice_name": f"P2M_{a.who}", "voice_description": c["desc"], "generated_voice_id": ids[a.pick - 1]}) book = json.load(open("声の一覧.json")) if os.path.exists("声の一覧.json") else {} book[a.who] = r["voice_id"] json.dump(book, open("声の一覧.json", "w"), ensure_ascii=False, indent=1) print(f"{a.who} の声を保存しました: {r['voice_id']}(声の一覧.json に書きました)")