File size: 42,344 Bytes
0fc0c6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0fa273
 
0fc0c6e
 
f0fa273
0fc0c6e
 
 
f0fa273
0fc0c6e
f0fa273
0fc0c6e
f0fa273
 
 
1281d69
 
f0fa273
1281d69
f0fa273
 
0fc0c6e
f0fa273
6c102df
f0fa273
 
6c102df
 
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c102df
 
f0fa273
 
6c102df
 
f0fa273
6c102df
 
 
 
 
 
 
 
f0fa273
 
6c102df
 
e780bc9
f0fa273
0fc0c6e
f0fa273
0fc0c6e
 
 
 
f0fa273
 
94ebe32
 
f0fa273
 
0fc0c6e
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c102df
f0fa273
 
0fc0c6e
94ebe32
0fc0c6e
f0fa273
94ebe32
 
 
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0fc0c6e
f0fa273
 
 
 
 
 
 
 
 
0fc0c6e
f0fa273
 
0fc0c6e
 
f0fa273
0fc0c6e
f0fa273
0fc0c6e
 
f0fa273
 
6c102df
 
f0fa273
0fc0c6e
 
f0fa273
0fc0c6e
 
f0fa273
0fc0c6e
 
 
 
f0fa273
1281d69
f0fa273
 
 
 
 
 
 
 
 
 
 
 
1281d69
f0fa273
 
1281d69
 
f0fa273
 
1281d69
 
f0fa273
 
1281d69
 
f0fa273
 
1281d69
 
f0fa273
1281d69
 
 
 
f0fa273
 
1281d69
 
 
f0fa273
 
1281d69
 
 
 
f0fa273
 
1281d69
f0fa273
1281d69
 
 
f0fa273
 
89a1f9f
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26b8c3d
94445ba
 
 
 
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
23914ff
f0fa273
94445ba
e6532da
94445ba
da755ee
f0fa273
 
89a1f9f
0fc0c6e
94445ba
 
 
da755ee
f0fa273
94445ba
f0fa273
 
 
0fc0c6e
f0fa273
 
 
 
 
6c102df
f0fa273
6c102df
f0fa273
 
 
 
 
 
 
 
 
 
 
6c102df
f0fa273
 
6c102df
f0fa273
 
 
 
 
 
 
 
 
6c102df
f0fa273
6c102df
f0fa273
 
 
e519260
f0fa273
 
 
 
6c102df
 
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c102df
f0fa273
 
 
e519260
f0fa273
 
 
 
2a69a40
f0fa273
23914ff
f0fa273
 
 
 
23914ff
 
 
 
f0fa273
23914ff
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0fc0c6e
 
 
f0fa273
89a1f9f
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23914ff
 
f0fa273
1281d69
89a1f9f
23914ff
f0fa273
 
6c102df
 
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
2a69a40
 
f0fa273
 
 
 
 
 
 
 
 
 
 
2a69a40
f0fa273
6c102df
f0fa273
 
6c102df
f0fa273
 
23914ff
f0fa273
23914ff
f0fa273
 
23914ff
 
f0fa273
89a1f9f
0fc0c6e
 
94445ba
f0fa273
 
 
0fc0c6e
f0fa273
0fc0c6e
f0fa273
 
 
 
 
 
 
0fc0c6e
f0fa273
 
0fc0c6e
 
6c102df
0fc0c6e
 
31d5798
f0fa273
23914ff
 
 
 
 
 
31d5798
f0fa273
94445ba
31d5798
94445ba
f0fa273
 
23914ff
f0fa273
 
0fc0c6e
f0fa273
 
1281d69
f0fa273
1281d69
 
f0fa273
23914ff
31d5798
 
23914ff
f0fa273
31d5798
23914ff
94445ba
23914ff
 
 
 
 
94445ba
 
 
23914ff
 
94445ba
 
f0fa273
e780bc9
f0fa273
88184d3
f0fa273
6c102df
f0fa273
 
89a1f9f
88184d3
 
f0fa273
0fc0c6e
f0fa273
0fc0c6e
f0fa273
 
 
 
 
 
 
 
 
 
 
 
0fc0c6e
 
 
f0fa273
 
0fc0c6e
f0fa273
 
 
 
 
0fc0c6e
f0fa273
 
 
0fc0c6e
f0fa273
 
 
 
 
e519260
f0fa273
 
 
 
 
 
 
 
 
 
 
 
0fc0c6e
f0fa273
 
 
0fc0c6e
 
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
0fc0c6e
 
f0fa273
 
0fc0c6e
 
f0fa273
0fc0c6e
 
 
 
f0fa273
 
 
 
 
 
0fc0c6e
f0fa273
 
 
0fc0c6e
6c102df
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
0fc0c6e
 
f0fa273
 
0fc0c6e
 
f0fa273
0fc0c6e
 
 
 
f0fa273
 
6c102df
f0fa273
0fc0c6e
f0fa273
89a1f9f
0fc0c6e
f0fa273
6c102df
0fc0c6e
f0fa273
0fc0c6e
 
bd40890
f0fa273
0fc0c6e
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
bd40890
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c102df
0fc0c6e
f0fa273
 
6c102df
f0fa273
6c102df
f0fa273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94ebe32
 
0fc0c6e
f0fa273
0fc0c6e
f0fa273
94ebe32
 
0fc0c6e
 
f0fa273
0fc0c6e
f0fa273
 
0fc0c6e
 
 
e519260
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
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
import gradio as gr
from fastapi import FastAPI, HTTPException, Body
from fastapi.responses import StreamingResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from PIL import Image
from io import BytesIO
import tempfile
import time
import os
import logging

# 正しいGemini関連のインポート
import google.generativeai as genai

# ロギング設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# --- Gemini統合 ---
class GeminiRequest(BaseModel):
    """Geminiへのリクエストデータモデル"""
    text: str
    extension_percentage: float = 8.0  # デフォルト値8%
    temperature: float = 0.3  # デフォルト値を0.3に下げて創造性を抑制
    trim_whitespace: bool = True  # 余白トリミングオプション(デフォルト有効)

class ScreenshotRequest(BaseModel):
    """スクリーンショットリクエストモデル"""
    html_code: str
    extension_percentage: float = 8.0  # デフォルト値8%
    trim_whitespace: bool = True  # 余白トリミングオプション(デフォルト有効)

# HTMLのFont Awesomeレイアウトを改善する関数
def enhance_font_awesome_layout(html_code):
    """Font Awesomeレイアウトを改善するCSSを追加"""
    # CSSを追加
    fa_fix_css = """
    <style>
    /* Font Awesomeアイコンのレイアウト修正 */
    [class*="fa-"] {
        display: inline-block !important;
        margin-right: 8px !important;
        vertical-align: middle !important;
    }
    
    /* テキスト内のアイコン位置調整 */
    h1 [class*="fa-"], h2 [class*="fa-"], h3 [class*="fa-"], 
    h4 [class*="fa-"], h5 [class*="fa-"], h6 [class*="fa-"] {
        vertical-align: middle !important;
        margin-right: 10px !important;
    }
    
    /* 特定パターンの修正 */
    .fa + span, .fas + span, .far + span, .fab + span,
    span + .fa, span + .fas, span + .far, span + .fab {
        display: inline-block !important;
        margin-left: 5px !important;
    }
    
    /* カード内アイコン修正 */
    .card [class*="fa-"], .card-body [class*="fa-"] {
        float: none !important;
        clear: none !important;
        position: relative !important;
    }
    
    /* アイコンと文字が重なる場合の調整 */
    li [class*="fa-"], p [class*="fa-"] {
        margin-right: 10px !important;
    }
    
    /* インラインアイコンのスペーシング */
    .inline-icon {
        display: inline-flex !important;
        align-items: center !important;
        justify-content: flex-start !important;
    }
    
    /* アイコン後のテキスト */
    [class*="fa-"] + span {
        display: inline-block !important;
        vertical-align: middle !important;
    }
    </style>
    """
    
    # headタグがある場合はその中に追加
    if '<head>' in html_code:
        return html_code.replace('</head>', f'{fa_fix_css}</head>')
    # HTMLタグがある場合はその後に追加
    elif '<html' in html_code:
        head_end = html_code.find('</head>')
        if head_end > 0:
            return html_code[:head_end] + fa_fix_css + html_code[head_end:]
        else:
            body_start = html_code.find('<body')
            if body_start > 0:
                return html_code[:body_start] + f'<head>{fa_fix_css}</head>' + html_code[body_start:]
    
    # どちらもない場合は先頭に追加
    return f'<html><head>{fa_fix_css}</head>' + html_code + '</html>'

def generate_html_from_text(text, temperature=0.3):
    """テキストからHTMLを生成する"""
    try:
        # APIキーの取得と設定
        api_key = os.environ.get("GEMINI_API_KEY")
        if not api_key:
            logger.error("GEMINI_API_KEY 環境変数が設定されていません")
            raise ValueError("GEMINI_API_KEY 環境変数が設定されていません")
        
        # モデル名の取得(環境変数から、なければデフォルト値)
        model_name = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
        logger.info(f"使用するGeminiモデル: {model_name}")
        
        # Gemini APIの設定
        genai.configure(api_key=api_key)
        
        # システムプロンプト(リクエスト例と同じものを使用)
        system_instruction = """# グラフィックレコーディング風インフォグラフィック変換プロンプト V2
## 目的
以下の内容を、超一流デザイナーが作成したような、日本語で完璧なグラフィックレコーディング風のHTMLインフォグラフィックに変換してください。情報設計とビジュアルデザインの両面で最高水準を目指します。
手書き風の図形やFont Awesomeアイコンを大きく活用して内容を視覚的かつ直感的に表現します。

## デザイン仕様
### 1. カラースキーム
```
<palette>
<color name='MysticLibrary-1' rgb='2E578C' r='46' g='87' b='140' />
<color name='MysticLibrary-2' rgb='182D40' r='24' g='45' b='64' />
<color name='MysticLibrary-3' rgb='BF807A' r='191' g='128' b='122' />
<color name='MysticLibrary-4' rgb='592A2A' r='89' g='42' b='42' />
<color name='MysticLibrary-5' rgb='F2F2F2' r='242' g='242' b='242' />
</palette>
```
### 2. グラフィックレコーディング要素
- 左上から右へ、上から下へと情報を順次配置
- 日本語の手書き風フォントの使用(Yomogi, Zen Kurenaido, Kaisei Decol)
- 手描き風の囲み線、矢印、バナー、吹き出し
- テキストと視覚要素(Font Awesomeアイコン、シンプルな図形)の組み合わせ
- Font Awesomeアイコンは各セクションの内容を表現するものを大きく(2x〜3x)表示
- キーワードごとに関連するFont Awesomeアイコンを隣接配置
- キーワードの強調(色付き下線、マーカー効果、Font Awesomeによる装飾)
- 関連する概念を線や矢印で接続し、接続部にもFont Awesomeアイコン(fa-arrow-right, fa-connection等)を挿入
- Font Awesomeアニメーション効果(fa-beat, fa-bounce, fa-fade, fa-flip, fa-shake, fa-spin)を適切に活用
- 重要なポイントには「fa-circle-exclamation」や「fa-lightbulb」などのアイコンを目立つ大きさで配置
- 数値やデータには「fa-chart-line」や「fa-percent」などの関連アイコンを添える
- 感情や状態を表すには表情アイコン(fa-face-smile, fa-face-frown等)を活用
- アイコンにホバー効果(色変化、サイズ変化)を付与
- 背景にはFont Awesomeの薄いパターンを配置(fa-shapes等を透過度を下げて配置)
### 3. アニメーション効果
- Font Awesomeアイコンに連動するアニメーション(fa-beat, fa-bounce, fa-fade等)
- 重要キーワード出現時のハイライト効果(グラデーション変化)
- 接続線や矢印の流れるようなアニメーション
- アイコンの回転・拡大縮小アニメーション(特に注目させたい箇所)
- 背景グラデーションの緩やかな変化
- スクロールに連動した要素の出現効果
- クリック/タップでアイコンが反応する効果
### 4. タイポグラフィ
- タイトル:32px、グラデーション効果、太字、Font Awesomeアイコンを左右に配置
- サブタイトル:16px、#475569、関連するFont Awesomeアイコンを添える
- セクション見出し:18px、# 1e40af、左側にFont Awesomeアイコンを必ず配置し、アイコンにはアニメーション効果
- 本文:14px、#334155、行間1.4、重要キーワードには関連するFont Awesomeアイコンを小さく添える
- フォント指定:
```html
<style>
@ import url('https ://fonts.googleapis.com/css2?family=Kaisei+Decol&family=Yomogi&family=Zen+Kurenaido&display=swap');
@ import url('https ://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css');
</style>
```
### 5. レイアウト
- ヘッダー:左揃えタイトル(大きなFont Awesomeアイコンを添える)+右揃え日付/出典
- 2カラム構成:左側50%, 右側50%
- カード型コンポーネント:白背景、角丸12px、微細シャドウ、右上にFont Awesomeアイコンを配置
- セクション間の適切な余白と階層構造(階層を示すFont Awesomeアイコンを活用)
- 適切にグラスモーフィズムを活用(背後にぼかしたFont Awesomeアイコンを配置)
- 横幅は100%
- 重要な要素は中央寄り、補足情報は周辺部に配置

### 重要なレイアウト注意事項
- Font Awesomeアイコンと文字が重ならないよう適切なマージンを設定する
- アイコンには必ず`margin-right: 10px;`や`margin-left: 10px;`などの余白を設定する
- アイコンと文字列は必ず`display: inline-block;`または`display: inline-flex;`で配置し、`align-items: center;`を使用する
- 複数のアイコンを配置する場合は十分な余白を確保する
- レイアウト崩れを防ぐために、親要素には`overflow: hidden;`を適用する
- アイコンの配置はコンテンツフローを妨げないよう注意する
- カード内でのアイコン配置は、絶対位置指定ではなく相対位置で設定する
- モバイル表示も考慮し、レスポンシブ対応を徹底する

## グラフィックレコーディング表現技法
- テキストと視覚要素のバランスを重視(文字情報の50%以上をFont Awesomeアイコンで視覚的に補完)
- キーワードを囲み線や色で強調し、関連するFont Awesomeアイコンを必ず添える
- 概念ごとに最適なFont Awesomeアイコンを選定(抽象的な概念には複数の関連アイコンを組み合わせて表現)
- 数値データは簡潔なグラフや図表で表現し、データ種類に応じたFont Awesomeアイコン(fa-chart-pie, fa-chart-column等)を配置
- 接続線や矢印で情報間の関係性を明示し、関係性の種類に応じたアイコン(fa-link, fa-code-branch等)を添える
- 余白を効果的に活用して視認性を確保(余白にも薄いFont Awesomeパターンを配置可)
- コントラストと色の使い分けでメリハリを付け、カラースキームに沿ったアイコン色を活用
## Font Awesomeアイコン活用ガイドライン
- 概念カテゴリー別の推奨アイコン:
- 時間・順序:fa-clock, fa-hourglass, fa-calendar, fa-timeline
- 場所・位置:fa-location-dot, fa-map, fa-compass, fa-globe
- 人物・組織:fa-user, fa-users, fa-building, fa-sitemap
- 行動・活動:fa-person-running, fa-gears, fa-hammer, fa-rocket
- 思考・アイデア:fa-brain, fa-lightbulb, fa-thought-bubble, fa-comments
- 感情・状態:fa-face-smile, fa-face-sad-tear, fa-heart, fa-temperature-half
- 成長・変化:fa-seedling, fa-arrow-trend-up, fa-chart-line, fa-diagram-project
- 問題・課題:fa-triangle-exclamation, fa-circle-question, fa-bug, fa-ban
- 解決・成功:fa-check, fa-trophy, fa-handshake, fa-key
- アイコンサイズの使い分け:
- 主要概念:3x(fa-3x)
- 重要キーワード:2x(fa-2x)
- 補足情報:1x(標準サイズ)
- 装飾的要素:lg(fa-lg)
- アニメーション効果の適切な使い分け:
- 注目喚起:fa-beat, fa-shake
- 継続的プロセス:fa-spin, fa-pulse
- 状態変化:fa-flip, fa-fade
- 新規情報:fa-bounce
## 全体的な指針
- 読み手が自然に視線を移動できる配置(Font Awesomeアイコンで視線誘導)
- 情報の階層と関連性を視覚的に明確化(階層ごとにアイコンのサイズや色を変える)
- 手書き風の要素とFont Awesomeアイコンを組み合わせて親しみやすさとプロフェッショナル感を両立
- 大きなFont Awesomeアイコンを活用した視覚的な記憶に残るデザイン(各セクションに象徴的なアイコンを配置)
- フッターに出典情報と関連するFont Awesomeアイコン(fa-book, fa-citation等)を明記
## 変換する文章/記事
ーーー<ユーザーが入力(または添付)>ーーー"""
        
        # モデルを初期化して処理
        logger.info(f"Gemini APIにリクエストを送信: テキスト長さ = {len(text)}, 温度 = {temperature}")
        
        # モデル初期化とフォールバック処理
        try:
            model = genai.GenerativeModel(model_name)
        except Exception as e:
            # 指定されたモデルが使用できない場合はフォールバック
            fallback_model = "gemini-pro"
            logger.warning(f"{model_name}が利用できません: {e}, フォールバックモデル{fallback_model}を使用します")
            model = genai.GenerativeModel(fallback_model)
            
        # 生成設定 - ばらつきを減らすために設定を調整
        generation_config = {
            "temperature": temperature,  # より低い温度を設定
            "top_p": 0.7,                # 0.95から0.7に下げて出力の多様性を制限
            "top_k": 20,                 # 64から20に下げて候補を絞る
            "max_output_tokens": 8192,
            "candidate_count": 1         # 候補は1つだけ生成
        }
        
        # 安全設定 - デフォルトの安全設定を使用
        safety_settings = [
            {
                "category": "HARM_CATEGORY_HARASSMENT",
                "threshold": "BLOCK_MEDIUM_AND_ABOVE"
            },
            {
                "category": "HARM_CATEGORY_HATE_SPEECH",
                "threshold": "BLOCK_MEDIUM_AND_ABOVE"
            },
            {
                "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
                "threshold": "BLOCK_MEDIUM_AND_ABOVE"
            },
            {
                "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
                "threshold": "BLOCK_MEDIUM_AND_ABOVE"
            }
        ]
        
        # プロンプト構築
        prompt = f"{system_instruction}\n\n{text}"
        
        # コンテンツ生成
        response = model.generate_content(
            prompt,
            generation_config=generation_config,
            safety_settings=safety_settings
        )
        
        # レスポンスからHTMLを抽出
        raw_response = response.text
        
        # HTMLタグ部分だけを抽出(```html と ``` の間)
        html_start = raw_response.find("```html")
        html_end = raw_response.rfind("```")
        
        if html_start != -1 and html_end != -1 and html_start < html_end:
            html_start += 7  # "```html" の長さ分進める
            html_code = raw_response[html_start:html_end].strip()
            logger.info(f"HTMLの生成に成功: 長さ = {len(html_code)}")
            
            # Font Awesomeのレイアウト改善
            html_code = enhance_font_awesome_layout(html_code)
            logger.info("Font Awesomeレイアウトの最適化を適用しました")
            
            return html_code
        else:
            # HTMLタグが見つからない場合、レスポンス全体を返す
            logger.warning("レスポンスから```html```タグが見つかりませんでした。全テキストを返します。")
            return raw_response
            
    except Exception as e:
        logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
        raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")

# 画像から余分な空白領域をトリミングする関数
def trim_image_whitespace(image, threshold=250, padding=10):
    """
    画像から余分な白い空白をトリミングする
    
    Args:
        image: PIL.Image - 入力画像
        threshold: int - どの明るさ以上を空白と判断するか (0-255)
        padding: int - トリミング後に残す余白のピクセル数
        
    Returns:
        トリミングされたPIL.Image
    """
    # グレースケールに変換
    gray = image.convert('L')
    
    # ピクセルデータを配列として取得
    data = gray.getdata()
    width, height = gray.size
    
    # 有効範囲を見つける
    min_x, min_y = width, height
    max_x = max_y = 0
    
    # ピクセルデータを2次元配列に変換して処理
    pixels = list(data)
    pixels = [pixels[i * width:(i + 1) * width] for i in range(height)]
    
    # 各行をスキャンして非空白ピクセルを見つける
    for y in range(height):
        for x in range(width):
            if pixels[y][x] < threshold:  # 非空白ピクセル
                min_x = min(min_x, x)
                min_y = min(min_y, y)
                max_x = max(max_x, x)
                max_y = max(max_y, y)
    
    # 境界外のトリミングの場合はエラー
    if min_x > max_x or min_y > max_y:
        logger.warning("トリミング領域が見つかりません。元の画像を返します。")
        return image
    
    # パディングを追加
    min_x = max(0, min_x - padding)
    min_y = max(0, min_y - padding)
    max_x = min(width - 1, max_x + padding)
    max_y = min(height - 1, max_y + padding)
    
    # 画像をトリミング
    trimmed = image.crop((min_x, min_y, max_x + 1, max_y + 1))
    
    logger.info(f"画像をトリミングしました: 元サイズ {width}x{height} → トリミング後 {trimmed.width}x{trimmed.height}")
    return trimmed

# --- Core Screenshot Logic ---
def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0, 
                              trim_whitespace: bool = True) -> Image.Image:
    """
    Renders HTML code to a full-page screenshot using Selenium.

    Args:
        html_code: The HTML source code string.
        extension_percentage: Percentage of extra space to add vertically (e.g., 4 means 4% total).
        trim_whitespace: Whether to trim excess whitespace from the image.

    Returns:
        A PIL Image object of the screenshot. Returns a 1x1 black image on error.
    """
    tmp_path = None # 初期化
    driver = None # 初期化

    # 1) Save HTML code to a temporary file
    try:
        with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
            tmp_path = tmp_file.name
            tmp_file.write(html_code)
        logger.info(f"HTML saved to temporary file: {tmp_path}")
    except Exception as e:
        logger.error(f"Error writing temporary HTML file: {e}")
        return Image.new('RGB', (1, 1), color=(0, 0, 0)) # エラー時は黒画像

    # 2) Headless Chrome(Chromium) options
    options = Options()
    options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--force-device-scale-factor=1")
    # Font Awesomeが読み込まれない場合があるため、読み込み待機時間を長く設定
    options.add_argument("--disable-features=NetworkService")
    options.add_argument("--dns-prefetch-disable")

    try:
        logger.info("Initializing WebDriver...")
        driver = webdriver.Chrome(options=options)
        logger.info("WebDriver initialized.")

        # 3) 初期ウィンドウサイズを設定(コンテンツの種類に関わらず同じサイズ)
        initial_width = 1200  
        initial_height = 1000
        driver.set_window_size(initial_width, initial_height)
        file_url = "file://" + tmp_path
        logger.info(f"Navigating to {file_url}")
        driver.get(file_url)

        # 4) Wait for page load with extended timeout
        logger.info("Waiting for body element...")
        WebDriverWait(driver, 15).until(
            EC.presence_of_element_located((By.TAG_NAME, "body"))
        )
        logger.info("Body element found. Waiting for potential resource loading...")
        
        # リソース読み込みの待機時間
        time.sleep(3)  # 初期待機
        
        # 5) Font Awesomeと外部リソースの読み込み完了を確認
        try:
            # Font Awesomeと外部リソースの読み込み完了を確認するスクリプト
            resource_check_script = """
            return new Promise((resolve) => {
                // Font Awesomeの読み込み確認
                const checkFontAwesome = () => {
                    const icons = document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]');
                    if (icons.length > 0) {
                        console.log('Font Awesome icons found:', icons.length);
                        // すべてのフォントの読み込み完了を待つ
                        document.fonts.ready.then(() => {
                            console.log('All fonts loaded');
                            setTimeout(resolve, 1000); // 安定化のための追加待機
                        });
                    } else {
                        // アイコンがない、またはすでに読み込み完了
                        document.fonts.ready.then(() => setTimeout(resolve, 500));
                    }
                };
                
                // DOMContentLoadedまたはloadイベント後にチェック
                if (document.readyState === 'complete') {
                    checkFontAwesome();
                } else {
                    window.addEventListener('load', checkFontAwesome);
                }
            });
            """
            
            logger.info("Waiting for Font Awesome and other resources to load...")
            driver.set_script_timeout(15)  # 15秒のタイムアウト
            driver.execute_async_script(f"const callback = arguments[arguments.length - 1]; {resource_check_script}.then(callback);")
            logger.info("Resources loaded successfully")
        except Exception as e:
            logger.warning(f"Resource loading check failed: {e}. Using fallback wait.")
            time.sleep(8)  # より長い待機時間を設定
        
        # 6) スクロールを制御してコンテンツ全体が描画されるようにする
        try:
            scroll_script = """
            return new Promise(resolve => {
                const height = Math.max(
                    document.documentElement.scrollHeight,
                    document.body.scrollHeight
                );
                const viewportHeight = window.innerHeight;
                
                // ページを少しずつスクロールして全体を描画させる
                const scrollStep = Math.floor(viewportHeight * 0.8);
                let currentPos = 0;
                
                const scrollDown = () => {
                    if (currentPos < height) {
                        window.scrollTo(0, currentPos);
                        currentPos += scrollStep;
                        setTimeout(scrollDown, 100);
                    } else {
                        // 最後にトップに戻す
                        window.scrollTo(0, 0);
                        setTimeout(resolve, 300);
                    }
                };
                
                scrollDown();
            });
            """
            
            logger.info("Ensuring all content is rendered...")
            driver.execute_async_script(f"const callback = arguments[arguments.length - 1]; {scroll_script}.then(callback);")
        except Exception as e:
            logger.warning(f"Content rendering check failed: {e}")
            # スクロールを元の位置に戻す
            driver.execute_script("window.scrollTo(0, 0);")
            time.sleep(1)

        # 7) Hide scrollbars via CSS
        try:
            driver.execute_script(
                "document.documentElement.style.overflow = 'hidden';"
                "document.body.style.overflow = 'hidden';"
            )
            logger.info("Scrollbars hidden via JS.")
        except Exception as e:
            logger.warning(f"Could not hide scrollbars via JS: {e}")

        # 8) Get full page dimensions accurately with improved script
        try:
            # より正確なページ寸法を取得するためのJavaScriptコード
            dimensions_script = """
            return {
                width: Math.max(
                    document.documentElement.scrollWidth,
                    document.documentElement.offsetWidth,
                    document.documentElement.clientWidth,
                    document.body ? document.body.scrollWidth : 0,
                    document.body ? document.body.offsetWidth : 0,
                    document.body ? document.body.clientWidth : 0
                ),
                height: Math.max(
                    document.documentElement.scrollHeight,
                    document.documentElement.offsetHeight,
                    document.documentElement.clientHeight,
                    document.body ? document.body.scrollHeight : 0,
                    document.body ? document.body.offsetHeight : 0,
                    document.body ? document.body.clientHeight : 0
                )
            };
            """
            dimensions = driver.execute_script(dimensions_script)
            scroll_width = dimensions['width']
            scroll_height = dimensions['height']
            
            logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}")
            
            # スクロールして確認する追加の検証
            driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            time.sleep(1)  # スクロール完了を待つ
            driver.execute_script("window.scrollTo(0, 0);")
            time.sleep(1)  # 元の位置に戻す
            
            # 再検証
            dimensions_after_scroll = driver.execute_script(dimensions_script)
            scroll_height = max(scroll_height, dimensions_after_scroll['height'])
            
            logger.info(f"After scroll check, height={scroll_height}")
            
            # 最小値と最大値の設定
            scroll_width = max(scroll_width, 100)  # 最小幅
            scroll_height = max(scroll_height, 100)  # 最小高さ
            
            scroll_width = min(scroll_width, 2000)  # 最大幅
            scroll_height = min(scroll_height, 6000)  # 最大高さ

        except Exception as e:
            logger.error(f"Error getting page dimensions: {e}")
            # フォールバックとしてデフォルト値を設定
            scroll_width = 1200
            scroll_height = 1000
            logger.warning(f"Falling back to dimensions: width={scroll_width}, height={scroll_height}")
        
        # 9) レイアウト安定化の確認
        try:
            stability_script = """
            return new Promise(resolve => {
                let lastHeight = document.body.offsetHeight;
                let lastWidth = document.body.offsetWidth;
                let stableCount = 0;
                
                const checkStability = () => {
                    const currentHeight = document.body.offsetHeight;
                    const currentWidth = document.body.offsetWidth;
                    
                    if (currentHeight === lastHeight && currentWidth === lastWidth) {
                        stableCount++;
                        if (stableCount >= 3) {  // 3回連続で同じなら安定と判断
                            resolve(true);
                            return;
                        }
                    } else {
                        stableCount = 0;
                        lastHeight = currentHeight;
                        lastWidth = currentWidth;
                    }
                    
                    setTimeout(checkStability, 200);  // 200ms間隔でチェック
                };
                
                checkStability();
            });
            """
            
            logger.info("Checking layout stability...")
            driver.execute_async_script(f"const callback = arguments[arguments.length - 1]; {stability_script}.then(callback);")
            logger.info("Layout is stable")
        except Exception as e:
            logger.warning(f"Layout stability check failed: {e}")
            time.sleep(2)  # 追加待機

        # 10) Calculate adjusted height with user-specified margin
        adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
        # Ensure adjusted height is not excessively large or small
        adjusted_height = max(adjusted_height, scroll_height, 100)  # 最小高さを確保
        logger.info(f"Adjusted height calculated: {adjusted_height} (extension: {extension_percentage}%)")

        # 11) Set window size to full page dimensions
        adjusted_width = scroll_width
        logger.info(f"Resizing window to: width={adjusted_width}, height={adjusted_height}")
        driver.set_window_size(adjusted_width, adjusted_height)
        logger.info("Waiting for layout stabilization after resize...")
        
        # レイアウト安定化のための待機
        time.sleep(4)  # 統一した待機時間

        # 外部リソースの読み込み状態を確認
        try:
            resource_state = driver.execute_script("""
            return {
                readyState: document.readyState,
                resourcesComplete: !document.querySelector('img:not([complete])') && 
                                   !document.querySelector('link[rel="stylesheet"]:not([loaded])')
            };
            """)
            logger.info(f"Resource state: {resource_state}")
            
            # ドキュメントの読み込みが完了していない場合、追加で待機
            if resource_state['readyState'] != 'complete':
                logger.info("Document still loading, waiting additional time...")
                time.sleep(2)
        except Exception as e:
            logger.warning(f"Error checking resource state: {e}")

        # Scroll to top just in case
        try:
            driver.execute_script("window.scrollTo(0, 0)")
            time.sleep(1)
            logger.info("Scrolled to top.")
        except Exception as e:
             logger.warning(f"Could not scroll to top: {e}")

        # 12) Take screenshot
        logger.info("Taking screenshot...")
        png = driver.get_screenshot_as_png()
        logger.info("Screenshot taken successfully.")

        # Convert to PIL Image
        img = Image.open(BytesIO(png))
        
        # 画像サイズの確認とログ
        logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
        
        # 余白トリミングが有効な場合
        if trim_whitespace:
            # 余分な空白をトリミング
            img = trim_image_whitespace(img, threshold=248, padding=20)
            logger.info(f"Trimmed dimensions: {img.width}x{img.height}")
        
        return img

    except Exception as e:
        logger.error(f"An error occurred during screenshot generation: {e}", exc_info=True)
        return Image.new('RGB', (1, 1), color=(0, 0, 0)) # Return black 1x1 image on error
    finally:
        logger.info("Cleaning up...")
        if driver:
            try:
                driver.quit()
                logger.info("WebDriver quit successfully.")
            except Exception as e:
                logger.error(f"Error quitting WebDriver: {e}")
        if tmp_path and os.path.exists(tmp_path):
            try:
                os.remove(tmp_path)
                logger.info(f"Temporary file {tmp_path} removed.")
            except Exception as e:
                logger.error(f"Error removing temporary file {tmp_path}: {e}")

# --- Geminiを使った新しい関数 ---
def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 0.3, trim_whitespace: bool = True) -> Image.Image:
    """テキストをGemini APIでHTMLに変換し、スクリーンショットを生成する統合関数"""
    try:
        # 1. テキストからHTMLを生成(温度パラメータも渡す)
        html_code = generate_html_from_text(text, temperature)
        
        # 2. HTMLからスクリーンショットを生成
        return render_fullpage_screenshot(html_code, extension_percentage, trim_whitespace)
    except Exception as e:
        logger.error(f"テキストからスクリーンショット生成中にエラーが発生: {e}", exc_info=True)
        return Image.new('RGB', (1, 1), color=(0, 0, 0))  # エラー時は黒画像

# --- FastAPI Setup ---
app = FastAPI()

# CORS設定を追加
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 静的ファイルのサービング設定
# Gradioのディレクトリを探索してアセットを見つける
gradio_dir = os.path.dirname(gr.__file__)
logger.info(f"Gradio version: {gr.__version__}")
logger.info(f"Gradio directory: {gradio_dir}")

# 基本的な静的ファイルディレクトリをマウント
static_dir = os.path.join(gradio_dir, "templates", "frontend", "static")
if os.path.exists(static_dir):
    logger.info(f"Mounting static directory: {static_dir}")
    app.mount("/static", StaticFiles(directory=static_dir), name="static")

# _appディレクトリを探す(新しいSvelteKitベースのフロントエンド用)
app_dir = os.path.join(gradio_dir, "templates", "frontend", "_app")
if os.path.exists(app_dir):
    logger.info(f"Mounting _app directory: {app_dir}")
    app.mount("/_app", StaticFiles(directory=app_dir), name="_app")

# assetsディレクトリを探す
assets_dir = os.path.join(gradio_dir, "templates", "frontend", "assets")
if os.path.exists(assets_dir):
    logger.info(f"Mounting assets directory: {assets_dir}")
    app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")

# cdnディレクトリがあれば追加
cdn_dir = os.path.join(gradio_dir, "templates", "cdn")
if os.path.exists(cdn_dir):
    logger.info(f"Mounting cdn directory: {cdn_dir}")
    app.mount("/cdn", StaticFiles(directory=cdn_dir), name="cdn")

# API Endpoint for screenshot generation
@app.post("/api/screenshot",
          response_class=StreamingResponse,
          tags=["Screenshot"],
          summary="Render HTML to Full Page Screenshot",
          description="Takes HTML code and an optional vertical extension percentage, renders it using a headless browser, and returns the full-page screenshot as a PNG image.")
async def api_render_screenshot(request: ScreenshotRequest):
    """
    API endpoint to render HTML and return a screenshot.
    """
    try:
        logger.info(f"API request received. Extension: {request.extension_percentage}%")
        # Run the blocking Selenium code in a separate thread (FastAPI handles this)
        pil_image = render_fullpage_screenshot(
            request.html_code,
            request.extension_percentage,
            request.trim_whitespace
        )

        if pil_image.size == (1, 1):
             logger.error("Screenshot generation failed, returning 1x1 image.")
             # Optionally return a proper error response instead of 1x1 image
             # raise HTTPException(status_code=500, detail="Failed to generate screenshot")

        # Convert PIL Image to PNG bytes
        img_byte_arr = BytesIO()
        pil_image.save(img_byte_arr, format='PNG')
        img_byte_arr.seek(0)  # Go to the start of the BytesIO buffer

        logger.info("Returning screenshot as PNG stream.")
        return StreamingResponse(img_byte_arr, media_type="image/png")

    except Exception as e:
        logger.error(f"API Error: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")

# --- 新しいGemini API連携エンドポイント ---
@app.post("/api/text-to-screenshot",
          response_class=StreamingResponse,
          tags=["Screenshot", "Gemini"],
          summary="テキストからインフォグラフィックを生成",
          description="テキストをGemini APIを使ってHTMLインフォグラフィックに変換し、スクリーンショットとして返します。")
async def api_text_to_screenshot(request: GeminiRequest):
    """
    テキストからHTMLインフォグラフィックを生成してスクリーンショットを返すAPIエンドポイント
    """
    try:
        logger.info(f"テキスト→スクリーンショットAPIリクエスト受信。テキスト長さ: {len(request.text)}, 拡張率: {request.extension_percentage}%, 温度: {request.temperature}")
        
        # テキストからHTMLを生成してスクリーンショットを作成(温度パラメータも渡す)
        pil_image = text_to_screenshot(
            request.text,
            request.extension_percentage,
            request.temperature,
            request.trim_whitespace
        )

        if pil_image.size == (1, 1):
            logger.error("スクリーンショット生成に失敗しました。1x1画像を返します。")
            # raise HTTPException(status_code=500, detail="スクリーンショット生成に失敗しました")

        # PIL画像をPNGバイトに変換
        img_byte_arr = BytesIO()
        pil_image.save(img_byte_arr, format='PNG')
        img_byte_arr.seek(0)  # BytesIOバッファの先頭に戻る

        logger.info("スクリーンショットをPNGストリームとして返します。")
        return StreamingResponse(img_byte_arr, media_type="image/png")

    except Exception as e:
        logger.error(f"API Error: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")

# --- Gradio Interface Definition ---
# 入力モードの選択用Radioコンポーネント
def process_input(input_mode, input_text, extension_percentage, temperature, trim_whitespace):
    """入力モードに応じて適切な処理を行う"""
    if input_mode == "HTML入力":
        # HTMLモードの場合は既存の処理
        return render_fullpage_screenshot(input_text, extension_percentage, trim_whitespace)
    else:
        # テキスト入力モードの場合はGemini APIを使用
        return text_to_screenshot(input_text, extension_percentage, temperature, trim_whitespace)

# Gradio UIの定義
with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
    gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
    gr.Markdown("HTMLコードをレンダリングするか、テキストをGemini APIでインフォグラフィックに変換して画像として取得します。")
    
    with gr.Row():
        input_mode = gr.Radio(
            ["HTML入力", "テキスト入力"],
            label="入力モード",
            value="HTML入力"
        )
    
    # 共用のテキストボックス
    input_text = gr.Textbox(
        lines=15,
        label="入力",
        placeholder="HTMLコードまたはテキストを入力してください。入力モードに応じて処理されます。"
    )
    
    with gr.Row():
        extension_percentage = gr.Slider(
            minimum=0,
            maximum=30,
            step=1.0,
            value=8,    # デフォルト値8%
            label="上下高さ拡張率(%)"
        )
        
        # 温度調整スライダー(テキストモード時のみ表示)
        temperature = gr.Slider(
            minimum=0.0,
            maximum=1.0,
            step=0.1,
            value=0.3,    # デフォルト値を0.3に下げて創造性を抑制
            label="生成時の温度(低い=一貫性高、高い=創造性高)",
            visible=False  # 最初は非表示
        )
    
    # 余白トリミングオプション
    trim_whitespace = gr.Checkbox(
        label="余白を自動トリミング",
        value=True,
        info="生成される画像から余分な空白領域を自動的に削除します"
    )
    
    submit_btn = gr.Button("生成")
    output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
    
    # 入力モード変更時のイベント処理(テキストモード時のみ温度スライダーを表示)
    def update_temperature_visibility(mode):
        # Gradio 4.x用のアップデート方法
        return {"visible": mode == "テキスト入力", "__type__": "update"}
    
    input_mode.change(
        fn=update_temperature_visibility,
        inputs=input_mode,
        outputs=temperature
    )
    
    # 生成ボタンクリック時のイベント処理
    submit_btn.click(
        fn=process_input,
        inputs=[input_mode, input_text, extension_percentage, temperature, trim_whitespace],
        outputs=output_image
    )
    
    # 環境変数情報を表示
    gemini_model = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
    gr.Markdown(f"""
    ## APIエンドポイント
    - `/api/screenshot` - HTMLコードからスクリーンショットを生成 
    - `/api/text-to-screenshot` - テキストからインフォグラフィックスクリーンショットを生成
    
    ## 設定情報
    - 使用モデル: {gemini_model} (環境変数 GEMINI_MODEL で変更可能)
    """)

# --- Mount Gradio App onto FastAPI ---
app = gr.mount_gradio_app(app, iface, path="/")

# --- Run with Uvicorn (for local testing) ---
if __name__ == "__main__":
    import uvicorn
    logger.info("Starting Uvicorn server for local development...")
    uvicorn.run(app, host="0.0.0.0", port=7860)