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())