light_countdown / app.py
loveasmrmeeeee's picture
Update app.py
78b4c7e verified
Raw
History Blame Contribute Delete
30.6 kB
# app.py (VOICEVOX版)
import random
import gradio as gr
import requests
import tempfile
import json
import numpy as np
from scipy.io import wavfile
# =========================
# セリフ素材
# =========================
COUNT_VARIANTS = {
100: ["ひゃく、、、", "ひゃーく、、、、", "ひゃーーーく、、、、、"],
99: ["きゅうじゅうきゅう、、、", "きゅーじゅーきゅー、、、、"],
98: ["きゅうじゅうはち、、、", "きゅーじゅーはち、、、、"],
97: ["きゅうじゅうなな、、、", "きゅーじゅーなな、、、、"],
96: ["きゅうじゅうろく、、、", "きゅーじゅーろく、、、、"],
95: ["きゅうじゅうご、、、", "きゅーじゅーご、、、、"],
94: ["きゅうじゅうよん、、、", "きゅーじゅーよん、、、、"],
93: ["きゅうじゅうさん、、、", "きゅーじゅーさん、、、、"],
92: ["きゅうじゅうに、、、", "きゅーじゅーに、、、、"],
91: ["きゅうじゅういち、、、", "きゅーじゅーいち、、、、"],
90: ["きゅうじゅう、、、", "きゅーじゅー、、、、", "きゅーーーじゅー、、、、、"],
# 80番台
89: ["はちじゅうきゅう、、、", "はーちじゅーきゅー、、、、"],
88: ["はちじゅうはち、、、", "はーちじゅーはち、、、、"],
87: ["はちじゅうなな、、、", "はーちじゅーなな、、、、"],
86: ["はちじゅうろく、、、", "はーちじゅーろく、、、、"],
85: ["はちじゅうご、、、", "はーちじゅーご、、、、"],
84: ["はちじゅうよん、、、", "はーちじゅーよん、、、、"],
83: ["はちじゅうさん、、、", "はーちじゅーさん、、、、"],
82: ["はちじゅうに、、、", "はーちじゅーに、、、、"],
81: ["はちじゅういち、、、", "はーちじゅーいち、、、、"],
80: ["はちじゅう、、、", "はーちじゅー、、、、", "はーーーちじゅー、、、、、"],
# 70番台
79: ["ななじゅうきゅう、、、", "なーなじゅーきゅー、、、、"],
78: ["ななじゅうはち、、、", "なーなじゅーはち、、、、"],
77: ["ななじゅうなな、、、", "なーなじゅーなな、、、、"],
76: ["ななじゅうろく、、、", "なーなじゅーろく、、、、"],
75: ["ななじゅうご、、、", "なーなじゅーご、、、、"],
74: ["ななじゅうよん、、、", "なーなじゅーよん、、、、"],
73: ["ななじゅうさん、、、", "なーなじゅーさん、、、、"],
72: ["ななじゅうに、、、", "なーなじゅーに、、、、"],
71: ["ななじゅういち、、、", "なーなじゅーいち、、、、"],
70: ["ななじゅう、、、", "なーなじゅー、、、、", "なーーーなじゅー、、、、、"],
# 60番台
69: ["ろくじゅうきゅう、、、", "ろーくじゅーきゅー、、、、"],
68: ["ろくじゅうはち、、、", "ろーくじゅーはち、、、、"],
67: ["ろくじゅうなな、、、", "ろーくじゅーなな、、、、"],
66: ["ろくじゅうろく、、、", "ろーくじゅーろく、、、、"],
65: ["ろくじゅうご、、、", "ろーくじゅーご、、、、"],
64: ["ろくじゅうよん、、、", "ろーくじゅーよん、、、、"],
63: ["ろくじゅうさん、、、", "ろーくじゅーさん、、、、"],
62: ["ろくじゅうに、、、", "ろーくじゅーに、、、、"],
61: ["ろくじゅういち、、、", "ろーくじゅーいち、、、、"],
60: ["ろくじゅう、、、", "ろーくじゅー、、、、", "ろーーーくじゅー、、、、、"],
# 50番台
59: ["ごじゅうきゅう、、、", "ごーじゅーきゅー、、、、"],
58: ["ごじゅうはち、、、", "ごーじゅーはち、、、、"],
57: ["ごじゅうなな、、、", "ごーじゅーなな、、、、"],
56: ["ごじゅうろく、、、", "ごーじゅーろく、、、、"],
55: ["ごじゅうご、、、", "ごーじゅーご、、、、"],
54: ["ごじゅうよん、、、", "ごーじゅーよん、、、、"],
53: ["ごじゅうさん、、、", "ごーじゅーさん、、、、"],
52: ["ごじゅうに、、、", "ごーじゅーに、、、、"],
51: ["ごじゅういち、、、", "ごーじゅーいち、、、、"],
50: ["ごじゅう、、、", "ごーじゅー、、、、", "ごーーーじゅー、、、、、"],
# 40番台
49: ["よんじゅうきゅう、、、", "よーんじゅーきゅー、、、、"],
48: ["よんじゅうはち、、、", "よーんじゅーはち、、、、"],
47: ["よんじゅうなな、、、", "よーんじゅーなな、、、、"],
46: ["よんじゅうろく、、、", "よーんじゅーろく、、、、"],
45: ["よんじゅうご、、、", "よーんじゅーご、、、、"],
44: ["よんじゅうよん、、、", "よーんじゅーよん、、、、"],
43: ["よんじゅうさん、、、", "よーんじゅーさん、、、、"],
42: ["よんじゅうに、、、", "よーんじゅーに、、、、"],
41: ["よんじゅういち、、、", "よーんじゅーいち、、、、"],
40: ["よんじゅう、、、", "よーんじゅー、、、、", "よーーーんじゅー、、、、、"],
# 30番台
39: ["さんじゅうきゅう、、、", "さーんじゅーきゅー、、、、"],
38: ["さんじゅうはち、、、", "さーんじゅーはち、、、、"],
37: ["さんじゅうなな、、、", "さーんじゅーなな、、、、"],
36: ["さんじゅうろく、、、", "さーんじゅーろく、、、、"],
35: ["さんじゅうご、、、", "さーんじゅーご、、、、"],
34: ["さんじゅうよん、、、", "さーんじゅーよん、、、、"],
33: ["さんじゅうさん、、、", "さーんじゅーさん、、、、"],
32: ["さんじゅうに、、、", "さーんじゅーに、、、、"],
31: ["さんじゅういち、、、", "さーんじゅーいち、、、、"],
30: ["さんじゅう、、、", "さーんじゅー、、、、", "さーーーんじゅー、、、、、"],
# 20番台
29: ["にじゅうきゅう、、、", "にーじゅーきゅー、、、、"],
28: ["にじゅうはち、、、", "にーじゅーはち、、、、"],
27: ["にじゅうなな、、、", "にーじゅーなな、、、、"],
26: ["にじゅうろく、、、", "にーじゅーろく、、、、"],
25: ["にじゅうご、、、", "にーじゅーご、、、、"],
24: ["にじゅうよん、、、", "にーじゅーよん、、、、"],
23: ["にじゅうさん、、、", "にーじゅーさん、、、、"],
22: ["にじゅうに、、、", "にーじゅーに、、、、"],
21: ["にじゅういち、、、", "にーじゅーいち、、、、"],
20: ["にじゅう、、、", "にーじゅー、、、、", "にーーーじゅー、、、、、"],
# 10番台
19: ["じゅうきゅう、、、", "じゅーきゅー、、、、"],
18: ["じゅうはち、、、", "じゅーはち、、、、"],
17: ["じゅうなな、、、", "じゅーなな、、、、"],
16: ["じゅうろく、、、", "じゅーろく、、、、"],
15: ["じゅうご、、、", "じゅーご、、、、"],
14: ["じゅうよん、、、", "じゅーよん、、、、"],
13: ["じゅうさん、、、", "じゅーさん、、、、"],
12: ["じゅうに、、、", "じゅーに、、、、"],
11: ["じゅういち、、、", "じゅーいち、、、、"],
10: ["じゅう、、、", "じゅーう、、、、", "じゅーーーう、、、、、"],
9: ["きゅう、、、", "きゅーう、、、、", "きゅーーーう、、、、、"],
8: ["はち、、、", "はーち、、、、", "はーーーち、、、、、"],
7: ["なな、、、", "なーな、、、、", "なーーーな、、、、、"],
6: ["ろく、、、", "ろーく、、、、", "ろーーーく、、、、、"],
5: ["ご、、、", "ごー、、、、", "ごーーー、、、、、"],
4: ["よん、、、", "よーん、、、、", "よーーーん、、、、、"],
3: ["さん、、、", "さーん、、、、", "さーーーん、、、、、"],
2: ["に、、、", "にー、、、、", "にーーー、、、、、", "にい、、、、"],
1: ["いち、、、", "いーち、、、、", "いーーーち、、、、、"],
0: ["ぜろ", "ぜーろ", "ぜろっ", "ぜろぜろぜろ", "、、、、、、、ぜーーーーーーーろ、、、"],
}
# 長めの読み方の重み付け(長い読み方ほど確率高く)
def get_count_variant(num):
variants = COUNT_VARIANTS.get(num, [str(num) + "、、、"])
if len(variants) == 1:
return variants[0]
elif len(variants) == 2:
weights = [1, 2]
elif len(variants) == 3:
weights = [1, 2, 3]
else: # 4以上
weights = [1, 2, 3, 4, 5][:len(variants)]
return random.choices(variants, weights=weights)[0]
INTERRUPTS = [
"はい。。ストップ",
"だめ。。まだ",
"おあずけ",
"ふふ。。止めちゃおうかな",
"あーあ。。残念",
"今のなし",
"最初から。。ね",
"。。。お。。あ。。ず。。け。。。",
]
REACTIONS = [
"ふふ",
"ん",
"あーあ",
"焦ってる?",
"そんな顔しないの",
"かわいいね",
"がーまーん",
"ほーら",
]
# 新レイヤー①: 状態を匂わせる独り言
OBSERVATIONS = [
"呼吸。。早くなってる",
"。。。。。今。。目そらしたでしょ",
"そんなに待てないの?",
"手。。動いてるよ",
"あーあ。。正直だね",
"我慢できてる?",
"顔。。赤いよ",
"震えてる",
]
# 新レイヤー②: 支配/主導権を示す一言
CONTROL_LINES = [
"まだ。。私のペース",
"勝手に進まないの",
"ちゃんと聞いて",
"逃げ道。。ないよ",
"ほら。。続けるよ",
"私が決めるの",
"焦らないで",
]
# 後半専用セリフ(進行度70%以降)
LATE_GAME_LINES = [
"ここまで来たのに",
"もう。。戻れないよ",
"最後まで付き合って",
"逃げると思った?",
"もうすぐだよ",
"頑張って",
]
# 0直前専用(1〜3の時)
ALMOST_ZERO = [
"。。。。。言わないよ?",
"まだ。。だめ",
"期待してる顔",
"ほら。。止まった",
"もう少しだけ",
"焦らないで",
]
# 優しいふり(ギャップ用)
FAKE_KINDNESS = [
"大丈夫。。ちゃんとするから",
"安心して",
"怖くないよ",
"信じて",
]
# =========================
# スタート演出
# =========================
START_LINES_NORMAL = [
"……じゃあ、いくよ",
"準備、いい?",
"ちゃんと聞いて",
"逃げないでね",
"ほら、足、開いて。",
]
START_LINES_WHISPER = [
"……ふふ",
"……静かだね",
"……聞こえてる?",
"……大丈夫",
]
# =========================
# ユーティリティ
# =========================
def interrupt_rate(max_count):
if max_count == 100:
return 0.25
if max_count == 30:
return 0.18
return 0.12
# add by GPT
# バイノーラル風にする
def mono_to_pseudo_binaural(
wav_path,
side="random", # "left", "right", "center", "random"
delay_ms=6,
other_gain=0.4
):
sr, data = wavfile.read(wav_path)
# モノラル前提(VOICEVOXは int16 モノラル)
if data.ndim > 1:
return wav_path
mono = data.astype(np.float32)
if side == "random":
side = random.choice(["left", "right"])
if side == "center":
stereo = np.stack([mono, mono], axis=1)
else:
delay_samples = int(sr * delay_ms / 1000)
def delay(sig):
if delay_samples <= 0:
return sig
return np.concatenate([
np.zeros(delay_samples),
sig[:-delay_samples]
])
if side == "left":
left = mono
right = delay(mono) * other_gain
else:
right = mono
left = delay(mono) * other_gain
stereo = np.stack([left, right], axis=1)
# クリップ防止
stereo = np.clip(stereo, -32768, 32767).astype(np.int16)
out = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
wavfile.write(out.name, sr, stereo)
out.close()
return out.name
# =========================
# 台本生成
# =========================
def generate_script(max_count: int) -> str:
script = []
# add by gpt:スタート演出
script.extend(get_start_sequence())
###
current = max_count
interrupt_count = 0
MAX_INTERRUPTS = 10
# 最初の何カウント分は中断やリアクションを入れないか
initial_countdown = random.randint(3, 5)
counts_done = 0
# 最近使ったセリフを記憶(重複回避用)
recent_interrupts = []
recent_reactions = []
recent_observations = []
recent_control = []
recent_late_game = []
recent_almost_zero = []
recent_fake = []
def get_unique_item(pool, recent_list, history_size=3):
"""最近使ったものを避けて選択"""
available = [item for item in pool if item not in recent_list]
if not available:
available = pool
selected = random.choice(available)
recent_list.append(selected)
if len(recent_list) > history_size:
recent_list.pop(0)
return selected
while current >= 0:
# 進行度を計算
progress = (max_count - current) / max_count if max_count > 0 else 1.0
is_late_game = progress > 0.7
is_almost_zero = current <= 3 and current > 0
# カウント(既に間を含む)
variant = get_count_variant(current)
script.append(variant)
# 0の場合は複数回繰り返す
if current == 0:
repeat_count = random.randint(4, 10)
for _ in range(repeat_count - 1):
script.append("、、、、")
script.append(get_count_variant(0))
break
counts_done += 1
# 最初の数カウントは他のセリフを挟まない
if counts_done <= initial_countdown:
current -= 1
continue
# === 0直前専用の溜め地獄 ===
if is_almost_zero and random.random() < 0.5:
almost = get_unique_item(ALMOST_ZERO, recent_almost_zero)
script.append(almost)
script.append("、、、、、、、")
current -= 1
continue
# 追加の間(長めに)
if random.random() < 0.4:
script.append("、、、、")
# === 優しいふり(ギャップ) ===
if random.random() < 0.15:
fake = get_unique_item(FAKE_KINDNESS, recent_fake)
script.append(fake)
script.append("、、、、、、、")
# ギャップ演出:50%で直後に中断
if progress >= 0.5 and interrupt_count < MAX_INTERRUPTS and random.random() < 0.5:
interrupt = get_unique_item(INTERRUPTS, recent_interrupts)
script.append(interrupt)
script.append("、、、、、、、、")
current = max_count
interrupt_count += 1
counts_done = 0
continue
# === リアクション(1〜3個連続) ===
if random.random() < 0.4:
reaction_count = random.choices([1, 2, 3], weights=[5, 3, 1])[0]
for i in range(reaction_count):
reaction = get_unique_item(REACTIONS, recent_reactions, history_size=5)
script.append(reaction)
if i < reaction_count - 1:
script.append("、、、")
else:
script.append("、、、、")
# === リアクション直後に独り言 ===
if random.random() < 0.4:
observation = get_unique_item(OBSERVATIONS, recent_observations)
script.append(observation)
script.append("、、、、、")
# === 後半専用セリフ ===
if is_late_game and random.random() < 0.25:
late_line = get_unique_item(LATE_GAME_LINES, recent_late_game)
script.append(late_line)
script.append("、、、、")
# === 支配/主導権セリフ ===
if random.random() < 0.2:
control = get_unique_item(CONTROL_LINES, recent_control)
script.append(control)
script.append("、、、、")
# === 中断判定(半分以降) ===
can_interrupt = progress >= 0.5
# 中断の直前に独り言を入れる(効果大)
if can_interrupt and interrupt_count < MAX_INTERRUPTS and random.random() < interrupt_rate(max_count):
# 50%で中断前に独り言
if random.random() < 0.5:
observation = get_unique_item(OBSERVATIONS, recent_observations)
script.append(observation)
script.append("、、、、")
interrupt = get_unique_item(INTERRUPTS, recent_interrupts)
script.append(interrupt)
script.append("、、、、、、、、")
current = max_count
interrupt_count += 1
counts_done = 0
continue
current -= 1
# SSMLタグでラップ
final_script = "" + "".join(script) + ""
return final_script
# added by gpt
def get_start_sequence():
"""
開始前の無音+導入セリフを生成
戻り値: list[str]
"""
seq = []
# 開始前無音(0.5〜2.5秒相当)
silent_count = random.randint(3, 10) # 1個 ≒ 0.25秒想定
seq.append("、" * silent_count)
r = random.random()
# ③ セリフなし(即カウント) 約25%
if r < 0.25:
return seq
# 囁き・独り言 約25%
elif r < 0.5:
seq.append(random.choice(START_LINES_WHISPER))
seq.append("、、、、")
return seq
# フェイント開始 約15%
elif r < 0.65:
seq.append(random.choice(START_LINES_NORMAL))
seq.append("、、、、")
seq.append("……まだ")
seq.append("、、、、")
return seq
# 通常導入 約35%
else:
seq.append(random.choice(START_LINES_NORMAL))
seq.append("、、、、")
return seq
# =========================
# VOICEVOX API (Web版)
# =========================
def tts_voicevox(text: str, speaker_id: int = 3) -> str:
"""
VOICEVOX Web APIを使用して音声合成
"""
try:
import urllib.parse
import time
# URLエンコード
encoded_text = urllib.parse.quote(text)
# GETリクエスト用URL
url = f"https://api.tts.quest/v3/voicevox/synthesis?speaker={speaker_id}&text={encoded_text}"
print(f"[TTS] リクエスト送信中... (テキスト長: {len(text)}文字)")
# API呼び出し
response = requests.get(url, timeout=60)
if response.status_code == 200:
result = response.json()
print(f"[TTS] APIレスポンス: success={result.get('success')}")
if result.get("success"):
status_url = result.get("audioStatusUrl")
wav_url = result.get("wavDownloadUrl")
if not wav_url:
print(f"[TTS] wavDownloadUrlが見つかりません")
return None
# 音声生成完了を待つ(台本の長さに応じて動的に設定)
# 間やセリフが増えたので係数を大幅に上げる
# 目安: 1文字あたり1秒 + 基本60秒の余裕
estimated_time = len(text) * 1.0 + 60
max_wait_time = max(90, int(estimated_time)) # 最低90秒
print(f"[TTS] 音声生成待機中... (推定待機時間: 最大{max_wait_time}秒)")
for attempt in range(max_wait_time):
# ステータス確認
if status_url:
try:
status_response = requests.get(status_url, timeout=10)
if status_response.status_code == 200:
status_data = status_response.json()
if status_data.get("isAudioReady"):
print(f"[TTS] 音声生成完了!(待機時間: {attempt}秒)")
break
except:
pass
# 直接WAVファイルを試す
try:
test_response = requests.head(wav_url, timeout=10)
if test_response.status_code == 200:
print(f"[TTS] 音声ファイル確認成功 ({attempt + 1}秒待機)")
break
except:
pass
if attempt % 10 == 0: # 10秒ごとにログ出力
print(f"[TTS] 待機中... ({attempt + 1}/{max_wait_time}秒)")
time.sleep(1)
# WAVファイルをダウンロード
print(f"[TTS] 音声ダウンロード中... URL: {wav_url}")
audio_response = requests.get(wav_url, timeout=60)
if audio_response.status_code == 200:
print(f"[TTS] ダウンロード成功 (サイズ: {len(audio_response.content)} bytes)")
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
tmp.write(audio_response.content)
tmp.close()
print(f"[TTS] ファイル保存完了: {tmp.name}")
return tmp.name
else:
print(f"[TTS] 音声ダウンロード失敗: {audio_response.status_code}")
return None
else:
print(f"[TTS] API success=False")
return None
else:
print(f"[TTS] APIエラー: {response.status_code}")
print(f"レスポンス: {response.text}")
return None
except Exception as e:
print(f"[TTS] 例外発生: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
return None
# =========================
# Gradio用関数
# =========================
def generate_and_speak(count_choice, speaker_choice):
max_count = int(count_choice)
script = generate_script(max_count)
# 話者IDマップ(アップロードファイルから修正済み)
speaker_map = {
# 四国めたん
"四国めたん(あまあま)": 0,
"四国めたん(ノーマル)": 2,
"四国めたん(セクシー)": 4,
"四国めたん(ツンツン)": 6,
"四国めたん(ささやき)": 36,
"四国めたん(ひそひそ)": 37,
# 春日部つむぎ
"春日部つむぎ": 8,
# 雨晴はう
"雨晴はう": 10,
# 冥鳴ひまり
"冥鳴ひまり": 14,
# 九州そら
"九州そら(あまあま)": 15,
"九州そら(ノーマル)": 16,
"九州そら(セクシー)": 17,
"九州そら(ツンツン)": 18,
"九州そら(ささやき)": 19,
# もち子さん
"もち子さん(ノーマル)": 20,
"もち子さん(セクシー/あん子)": 66,
"もち子さん(泣き)": 77,
"もち子さん(怒り)": 78,
"もち子さん(喜び)": 79,
"もち子さん(のんびり)": 80,
# WhiteCUL
"WhiteCUL(かなしい)": 25,
# 後鬼
"後鬼(鬼ver.)": 88,
# No.7
"No.7(読み聞かせ)": 31,
# 小夜/SAYO
"小夜/SAYO": 46,
# ナースロボ_タイプT
"ナースロボ_タイプT(ノーマル)": 47,
"ナースロボ_タイプT(楽々)": 48,
"ナースロボ_タイプT(恐怖)": 49,
"ナースロボ_タイプT(内緒話)": 50,
# 猫使ビィ
"猫使ビィ(おちつき)": 59,
"猫使ビィ(人見知り)": 60,
# 満別花丸
"満別花丸(ささやき)": 71,
# 中部つるぎ
"中部つるぎ(ノーマル)": 94,
"中部つるぎ(怒り)": 95,
"中部つるぎ(ひそひそ)": 96,
# ユーレイちゃん
"ユーレイちゃん(甘々)": 103,
"ユーレイちゃん(哀しみ)": 104,
"ユーレイちゃん(ささやき)": 105,
# 東北ずん子
"東北ずん子": 107,
# 東北きりたん
"東北きりたん": 108,
# あんこもん
"あんこもん(ささやき)": 117,
}
speaker_id = speaker_map.get(speaker_choice, 2)
# 推定待機時間を計算(間とセリフ増加に対応)
estimated_time = len(script) * 1.0 + 60
max_wait = max(90, int(estimated_time))
# 台本の長さに応じた情報
if len(script) > 200:
info = f"⚠️ 台本が長いため音声生成に時間がかかります\n({len(script)}文字 / 最大{max_wait}秒待機)\n\n"
else:
info = f"📊 台本: {len(script)}文字 / 推定待機時間: 最大{max_wait}秒\n\n"
try:
audio_path = tts_voicevox(script, speaker_id)
if audio_path:
# add by gpt
audio_path = mono_to_pseudo_binaural(
audio_path,
side="random" # ← とりあえずランダム
)
##
return audio_path, info + script
else:
return None, f"{info}⚠️ 音声生成に失敗しました(タイムアウトの可能性があります)\n\n台本:\n{script}"
except Exception as e:
return None, f"{info}エラー: {str(e)}\n\n台本:\n{script}"
# =========================
# UI
# =========================
with gr.Blocks() as demo:
gr.Markdown("# 🎙️ ゆっくりカウントダウン (VOICEVOX版)")
gr.Markdown("ゆっくりボイスで焦らされるカウントダウン")
count_choice = gr.Dropdown(
["5", "10", "30", "100"],
value="10",
label="カウント数"
)
speaker_choice = gr.Dropdown(
[
"四国めたん(あまあま)",
"四国めたん(ノーマル)",
"四国めたん(セクシー)",
"四国めたん(ツンツン)",
"四国めたん(ささやき)",
"四国めたん(ひそひそ)",
"春日部つむぎ",
"雨晴はう",
"冥鳴ひまり",
"九州そら(あまあま)",
"九州そら(ノーマル)",
"九州そら(セクシー)",
"九州そら(ツンツン)",
"九州そら(ささやき)",
"もち子さん(ノーマル)",
"もち子さん(セクシー/あん子)",
"もち子さん(泣き)",
"もち子さん(怒り)",
"もち子さん(喜び)",
"もち子さん(のんびり)",
"WhiteCUL(かなしい)",
"後鬼(鬼ver.)",
"No.7(読み聞かせ)",
"小夜/SAYO",
"ナースロボ_タイプT(ノーマル)",
"ナースロボ_タイプT(楽々)",
"ナースロボ_タイプT(恐怖)",
"ナースロボ_タイプT(内緒話)",
"猫使ビィ(おちつき)",
"猫使ビィ(人見知り)",
"満別花丸(ささやき)",
"中部つるぎ(ノーマル)",
"中部つるぎ(怒り)",
"中部つるぎ(ひそひそ)",
"ユーレイちゃん(甘々)",
"ユーレイちゃん(哀しみ)",
"ユーレイちゃん(ささやき)",
"東北ずん子",
"東北きりたん",
"あんこもん(ささやき)",
],
value="四国めたん(ノーマル)",
label="声の種類"
)
btn = gr.Button("▶️ スタート", variant="primary")
audio_out = gr.Audio(label="🔊 音声再生")
script_out = gr.Textbox(
label="📝 生成された台本",
lines=8
)
btn.click(
fn=generate_and_speak,
inputs=[count_choice, speaker_choice],
outputs=[audio_out, script_out]
)
gr.Markdown("---")
gr.Markdown("💡 **仕様**")
gr.Markdown("- カウントダウン中にランダムで中断されます(最大10回まで、進行度50%以降)")
gr.Markdown("- 待機時間は台本の長さに応じて自動調整(1文字=1秒 + 余裕60秒、最低90秒)")
gr.Markdown("- 例: 100文字の台本 → 最大160秒待機、200文字 → 最大260秒待機")
gr.Markdown("- 句読点の数で間を制御(多いほど長い間)")
gr.Markdown("🎤 使用: VOICEVOX Web API (tts.quest)")
demo.launch(theme=gr.themes.Soft())