Spaces:
Sleeping
Sleeping
File size: 27,126 Bytes
8cbf758 8efcee2 8cbf758 8efcee2 8cbf758 8efcee2 b5bccfc 5f69f7b 5ab17b2 8efcee2 442c67f b5bccfc 442c67f 2687b7f 442c67f 8efcee2 8cbf758 0e76840 8efcee2 8cbf758 8efcee2 8cbf758 0e76840 b5bccfc 8efcee2 0e76840 5ab17b2 0e76840 8efcee2 442c67f 5f69f7b 442c67f 0e76840 8efcee2 442c67f 0e76840 442c67f 0e76840 5f69f7b 442c67f 8efcee2 442c67f 53750f5 442c67f 5f69f7b 442c67f 0e76840 442c67f 5f69f7b 0e76840 5f69f7b 1e8ba33 0e76840 b5bccfc 0e76840 b5bccfc 0e76840 b5bccfc 0e76840 b5bccfc 0e76840 8efcee2 0e76840 b5bccfc 0e76840 442c67f 0e76840 8efcee2 69e56e6 0e76840 8efcee2 8cbf758 8efcee2 8cbf758 8efcee2 ff982c3 8cbf758 ff982c3 8cbf758 ff982c3 8d6618b ff982c3 8cbf758 b506c92 8cbf758 b506c92 8cbf758 ff982c3 b506c92 ff982c3 8d6618b ff982c3 8d6618b 2d07c53 5ab17b2 2d07c53 8d6618b a5c5dc4 b506c92 8d6618b a5c5dc4 2d07c53 b506c92 8d6618b b506c92 8d6618b b506c92 8cbf758 b506c92 ff982c3 8cbf758 b506c92 8cbf758 8efcee2 8cbf758 8efcee2 81a129d 8cbf758 81a129d 8cbf758 81a129d 2687b7f 81a129d 8cbf758 81a129d 8cbf758 5ab17b2 2d07c53 69e56e6 2d07c53 69e56e6 2d07c53 69e56e6 8efcee2 8cbf758 2d07c53 8cbf758 2d07c53 8efcee2 2d07c53 8efcee2 2687b7f 8cbf758 8efcee2 a5c5dc4 81a129d a5c5dc4 81a129d a5c5dc4 81a129d a5c5dc4 8efcee2 1e8ba33 8efcee2 a5c5dc4 8efcee2 8cbf758 8efcee2 1e8ba33 69e56e6 5ab17b2 8cbf758 8efcee2 2687b7f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 | # app.py (VOICEVOX版)
import random
import gradio as gr
import requests
import tempfile
import json
# =========================
# セリフ素材
# =========================
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][: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 = [
"大丈夫、ちゃんとするから",
"安心して",
"怖くないよ",
"信じて",
]
# =========================
# ユーティリティ
# =========================
def interrupt_rate(max_count):
if max_count == 100:
return 0.25
if max_count == 30:
return 0.18
return 0.12
# =========================
# 台本生成
# =========================
def generate_script(max_count: int) -> str:
script = []
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
return "".join(script)
# =========================
# VOICEVOX API (Web版)
# =========================
def tts_voicevox(text: str, speaker_id: int = 3) -> str:
"""
VOICEVOX Web APIを使用して音声合成
speaker_id: 3 = ずんだもん(ノーマル)
"""
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,
"もち子さん(泣き)": 67,
"もち子さん(怒り)": 68,
"もち子さん(喜び)": 69,
"もち子さん(のんびり)": 70,
# WhiteCUL
"WhiteCUL(かなしい)": 25,
# 後鬼
"後鬼(人間ver.)": 27,
# No.7
"No.7(読み聞かせ)": 31,
# 小夜/SAYO
"小夜/SAYO": 46,
# ナースロボ_タイプT
"ナースロボ_タイプT(ノーマル)": 47,
"ナースロボ_タイプT(楽々)": 48,
"ナースロボ_タイプT(恐怖)": 49,
"ナースロボ_タイプT(内緒話)": 50,
# 猫使ビィ
"猫使ビィ(おちつき)": 58,
"猫使ビィ(人見知り)": 59,
# 満別花丸
"満別花丸(ささやき)": 61,
# 中部つるぎ
"中部つるぎ(ノーマル)": 67,
"中部つるぎ(怒り)": 68,
"中部つるぎ(ひそひそ)": 69,
# ユーレイちゃん
"ユーレイちゃん(甘々)": 63,
"ユーレイちゃん(哀しみ)": 64,
"ユーレイちゃん(ささやき)": 65,
# 東北ずん子
"東北ずん子": 71,
# 東北きりたん
"東北きりたん": 72,
# あんこもん
"あんこもん(ささやき)": 73,
}
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:
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("🎤 使用: VOICEVOX Web API (tts.quest)")
demo.launch(theme=gr.themes.Soft()) |