tomo2chin2 commited on
Commit
0fc0c6e
·
verified ·
1 Parent(s): c917ee7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +477 -96
app.py CHANGED
@@ -1,12 +1,208 @@
1
- # ページのスクリーンショット取得機能を改善した関数
2
- def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_gemini_generated: bool = False) -> Image.Image:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
  Renders HTML code to a full-page screenshot using Selenium.
5
 
6
  Args:
7
  html_code: The HTML source code string.
8
  extension_percentage: Percentage of extra space to add vertically (e.g., 4 means 4% total).
9
- is_gemini_generated: Geminiによって生成されたHTMLかどうか(特別な処理を適用)
10
 
11
  Returns:
12
  A PIL Image object of the screenshot. Returns a 1x1 black image on error.
@@ -14,9 +210,11 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
14
  tmp_path = None # 初期化
15
  driver = None # 初期化
16
 
17
- # Gemini生成HTMLの場合は余白を多めに取る
18
- if is_gemini_generated:
19
- extension_percentage = max(extension_percentage, 15.0) # 少なくとも15%の余白を確保
 
 
20
 
21
  # 1) Save HTML code to a temporary file
22
  try:
@@ -34,10 +232,9 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
34
  options.add_argument("--no-sandbox")
35
  options.add_argument("--disable-dev-shm-usage")
36
  options.add_argument("--force-device-scale-factor=1")
37
- # Geminiが生成したHTMLの場合、ウィンドウサイズを大きく設定
38
- if is_gemini_generated:
39
- options.add_argument("--window-size=1600,2400") # 初期サイズを大きく
40
-
41
  # Increase logging verbosity for debugging if needed
42
  # options.add_argument("--enable-logging")
43
  # options.add_argument("--v=1")
@@ -47,32 +244,25 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
47
  driver = webdriver.Chrome(options=options)
48
  logger.info("WebDriver initialized.")
49
 
50
- # 3) Load page with initial window size
51
- if is_gemini_generated:
52
- driver.set_window_size(1600, 2400) # Gemini生成HTMLには大きなサイズを使用
53
- else:
54
- driver.set_window_size(1200, 800) # 通常のHTML用の標準サイズ
55
-
56
  file_url = "file://" + tmp_path
57
  logger.info(f"Navigating to {file_url}")
58
  driver.get(file_url)
59
 
60
- # 4) Wait for page load - Gemini生成HTMLは読み込みに時間がかかるので長めに待機
61
  logger.info("Waiting for body element...")
62
- wait_timeout = 30 if is_gemini_generated else 15
63
- WebDriverWait(driver, wait_timeout).until(
64
  EC.presence_of_element_located((By.TAG_NAME, "body"))
65
  )
 
 
 
 
 
66
 
67
- # Font Awesomeとカスタムフォントの読み込み待機(Gemini生成HTMLの場合)
68
- if is_gemini_generated:
69
- logger.info("Waiting for potential external resources (Font Awesome, custom fonts)...")
70
- # Font Awesomeの読み込みを待機(アイコンが表示される)
71
- time.sleep(5) # Gemini生成HTMLには長めの待機時間
72
- else:
73
- logger.info("Waiting for potential resource loading...")
74
- time.sleep(3) # 通常のHTMLには標準の待機時間
75
-
76
  # 5) Hide scrollbars via CSS
77
  try:
78
  driver.execute_script(
@@ -83,58 +273,47 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
83
  except Exception as e:
84
  logger.warning(f"Could not hide scrollbars via JS: {e}")
85
 
86
- # 6) Get full page dimensions accurately - Gemini HTMLには追加のJS実行
87
  try:
88
- if is_gemini_generated:
89
- # Gemini生成HTMLの場合、より慎重にコンテンツサイズを計算
90
- # すべての子要素の合計高さを確認するJSを実行
91
- scroll_width = driver.execute_script("""
92
- return Math.max(
93
- document.body.scrollWidth,
94
- document.documentElement.scrollWidth,
95
- document.body.offsetWidth,
96
- document.documentElement.offsetWidth,
97
- document.body.clientWidth,
98
- document.documentElement.clientWidth
99
- );
100
- """)
101
-
102
- scroll_height = driver.execute_script("""
103
- // ドキュメント全体の高さを取得
104
- var docHeight = Math.max(
105
- document.body.scrollHeight,
106
- document.documentElement.scrollHeight,
107
- document.body.offsetHeight,
108
- document.documentElement.offsetHeight,
109
- document.body.clientHeight,
110
- document.documentElement.clientHeight
111
- );
112
-
113
- // すべての要素をチェックして最大のY位置を見つける
114
- var allElements = document.getElementsByTagName('*');
115
- var maxBottom = 0;
116
-
117
- for (var i = 0; i < allElements.length; i++) {
118
- var rect = allElements[i].getBoundingClientRect();
119
- var bottom = rect.bottom + window.scrollY;
120
- if (bottom > maxBottom) {
121
- maxBottom = bottom;
122
- }
123
- }
124
-
125
- // 最大のY位置とドキュメント高さの大きい方を返す
126
- return Math.max(docHeight, maxBottom);
127
- """)
128
- else:
129
- # 通常のHTML用の標準の計算
130
- scroll_width = driver.execute_script(
131
- "return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth)"
132
  )
133
- scroll_height = driver.execute_script(
134
- "return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight)"
135
- )
136
-
 
 
137
  logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}")
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  # Ensure minimum dimensions to avoid errors
139
  scroll_width = max(scroll_width, 100) # 最小幅を設定
140
  scroll_height = max(scroll_height, 100) # 最小高さを設定
@@ -142,12 +321,8 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
142
  except Exception as e:
143
  logger.error(f"Error getting page dimensions: {e}")
144
  # フォールバックとしてデフォルト値を設定
145
- if is_gemini_generated:
146
- scroll_width = 1600
147
- scroll_height = 2400
148
- else:
149
- scroll_width = 1200
150
- scroll_height = 800
151
  logger.warning(f"Falling back to dimensions: width={scroll_width}, height={scroll_height}")
152
 
153
  # 7) Calculate adjusted height with user-specified margin
@@ -157,11 +332,32 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
157
  logger.info(f"Adjusted height calculated: {adjusted_height} (extension: {extension_percentage}%)")
158
 
159
  # 8) Set window size to full page dimensions (width) and adjusted height
160
- logger.info(f"Resizing window to: width={scroll_width}, height={adjusted_height}")
161
- driver.set_window_size(scroll_width, adjusted_height)
 
162
  logger.info("Waiting for layout stabilization after resize...")
163
- wait_time = 5 if is_gemini_generated else 3 # Gemini生成HTMLには長めの待機時間
164
- time.sleep(wait_time)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  # Scroll to top just in case
167
  try:
@@ -173,17 +369,15 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
173
 
174
  # 9) Take screenshot
175
  logger.info("Taking screenshot...")
176
- # Gemini生成HTMLの場合、完全なスクリーンショットを取得するための特別な処理
177
- if is_gemini_generated:
178
- # スクロールしながら複数の部分スクリーンショットを撮り、それらを結合する方法もあります
179
- # ここではシンプルに全体のスクリーンショットを取得します
180
- time.sleep(1) # 最終的な安定化のための短い待機
181
-
182
  png = driver.get_screenshot_as_png()
183
  logger.info("Screenshot taken successfully.")
184
 
185
  # Convert to PIL Image
186
  img = Image.open(BytesIO(png))
 
 
 
 
187
  return img
188
 
189
  except Exception as e:
@@ -211,8 +405,195 @@ def text_to_screenshot(text: str, extension_percentage: float) -> Image.Image:
211
  # 1. テキストからHTMLを生成
212
  html_code = generate_html_from_text(text)
213
 
214
- # 2. HTMLからスクリーンショットを生成(Gemini生成フラグをTrue)
215
- return render_fullpage_screenshot(html_code, extension_percentage, is_gemini_generated=True)
216
  except Exception as e:
217
  logger.error(f"テキストからスクリーンショット生成中にエラーが発生: {e}", exc_info=True)
218
- return Image.new('RGB', (1, 1), color=(0, 0, 0)) # エラー時は黒画像
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from fastapi import FastAPI, HTTPException, Body
3
+ from fastapi.responses import StreamingResponse
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from pydantic import BaseModel
7
+ from selenium import webdriver
8
+ from selenium.webdriver.chrome.options import Options
9
+ from selenium.webdriver.common.by import By
10
+ from selenium.webdriver.support.ui import WebDriverWait
11
+ from selenium.webdriver.support import expected_conditions as EC
12
+ from PIL import Image
13
+ from io import BytesIO
14
+ import tempfile
15
+ import time
16
+ import os
17
+ import logging
18
+
19
+ # 正しいGemini関連のインポート
20
+ import google.generativeai as genai
21
+
22
+ # ロギング設定
23
+ logging.basicConfig(level=logging.INFO)
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # --- Gemini統合 ---
27
+ class GeminiRequest(BaseModel):
28
+ """Geminiへのリクエストデータモデル"""
29
+ text: str
30
+ extension_percentage: float = 8.0 # Default value same as Gradio slider
31
+
32
+ def generate_html_from_text(text):
33
+ """テキストからHTMLを生成する"""
34
+ try:
35
+ # APIキーの取得と設定
36
+ api_key = os.environ.get("GEMINI_API_KEY")
37
+ if not api_key:
38
+ logger.error("GEMINI_API_KEY 環境変数が設定されていません")
39
+ raise ValueError("GEMINI_API_KEY 環境変数が設定されていません")
40
+
41
+ # Gemini APIの設定
42
+ genai.configure(api_key=api_key)
43
+
44
+ # システムプロンプト(リクエスト例と同じものを使用)
45
+ system_instruction = """# グラフィックレコーディング風インフォグラフィック変換プロンプト V2
46
+ ## 目的
47
+ 以下の内容を、超一流デザイナーが作成したような、日本語で完璧なグラフィックレコーディング風のHTMLインフォグラフィックに変換してください。情報設計とビジュアルデザインの両面で最高水準を目指します。
48
+ 手書き風の図形やFont Awesomeアイコンを大きく活用して内容を視覚的かつ直感的に表現します。
49
+
50
+ ## デザイン仕様
51
+ ### 1. カラースキーム
52
+ ```
53
+ <palette>
54
+ <color name='MysticLibrary-1' rgb='2E578C' r='46' g='87' b='140' />
55
+ <color name='MysticLibrary-2' rgb='182D40' r='24' g='45' b='64' />
56
+ <color name='MysticLibrary-3' rgb='BF807A' r='191' g='128' b='122' />
57
+ <color name='MysticLibrary-4' rgb='592A2A' r='89' g='42' b='42' />
58
+ <color name='MysticLibrary-5' rgb='F2F2F2' r='242' g='242' b='242' />
59
+ </palette>
60
+ ```
61
+ ### 2. グラフィックレコーディング要素
62
+ - 左上から右へ、上から下へと情報を順次配置
63
+ - 日本語の手書き風フォントの使用(Yomogi, Zen Kurenaido, Kaisei Decol)
64
+ - 手描き風の囲み線、矢印、バナー、吹き出し
65
+ - テキストと視覚要素(Font Awesomeアイコン、シンプルな図形)の組み合わせ
66
+ - Font Awesomeアイコンは各セクションの内容を表現するものを大きく(2x〜3x)表示
67
+ - キーワードごとに関連するFont Awesomeアイコンを隣接配置
68
+ - キーワードの強調(色付き下線、マーカー効果、Font Awesomeによる装飾)
69
+ - 関連する概念を線や矢印で接続し、接続部にもFont Awesomeアイコン(fa-arrow-right, fa-connection等)を挿入
70
+ - Font Awesomeアニメーション効果(fa-beat, fa-bounce, fa-fade, fa-flip, fa-shake, fa-spin)を適切に活用
71
+ - 重要なポイントには「fa-circle-exclamation」や「fa-lightbulb」などのアイコンを目立つ大きさで配置
72
+ - 数値やデータには「fa-chart-line」や「fa-percent」などの関連アイコンを添える
73
+ - 感情や状態を表すには表情アイコン(fa-face-smile, fa-face-frown等)を活用
74
+ - アイコンにホバー効果(色変化、サイズ変化)を付与
75
+ - 背景にはFont Awesomeの薄いパターンを配置(fa-shapes等を透過度を下げて配置)
76
+ ### 3. アニメーション効果
77
+ - Font Awesomeアイコンに連動するアニメーション(fa-beat, fa-bounce, fa-fade等)
78
+ - 重要キーワード出現時のハイライト効果(グラデーション変化)
79
+ - 接続線や矢印の流れるようなアニメーション
80
+ - アイコンの回転・拡大縮小アニメーション(特に注目させたい箇所)
81
+ - 背景グラデーションの緩やかな変化
82
+ - スクロールに連動した要素の出現効果
83
+ - クリック/タップでアイコンが反応する効果
84
+ ### 4. タイポグラフィ
85
+ - タイトル:32px、グラデーション効果、太字、Font Awesomeアイコンを左右に配置
86
+ - サブタイトル:16px、#475569、関連するFont Awesomeアイコンを添える
87
+ - セクション見出し:18px、# 1e40af、左側にFont Awesomeアイコンを必ず配置し、��イコンにはアニメーション効果
88
+ - 本文:14px、#334155、行間1.4、重要キーワードには関連するFont Awesomeアイコンを小さく添える
89
+ - フォント指定:
90
+ ```html
91
+ <style>
92
+ @ import url('https ://fonts.googleapis.com/css2?family=Kaisei+Decol&family=Yomogi&family=Zen+Kurenaido&display=swap');
93
+ @ import url('https ://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css');
94
+ </style>
95
+ ```
96
+ ### 5. レイアウト
97
+ - ヘッダー:左揃えタイトル(大きなFont Awesomeアイコンを添える)+右揃え日付/出典
98
+ - 3カラム構成:左側33%、中央33%、右側33%
99
+ - カード型コンポーネント:白背景、角丸12px、微細シャドウ、右上にFont Awesomeアイコンを配置
100
+ - セクション間の適切な余白と階層構造(階層を示すFont Awesomeアイコンを活用)
101
+ - 適切にグラスモーフィズムを活用(背後にぼかしたFont Awesomeアイコンを配置)
102
+ - 横幅は100%
103
+ - 重要な要素は中央寄り、補足情報は周辺部に配置
104
+ ## グラフィックレコーディング表現技法
105
+ - テキストと視覚要素のバランスを重視(文字情報の50%以上をFont Awesomeアイコンで視覚的に補完)
106
+ - キーワードを囲み線や色で強調し、関連するFont Awesomeアイコンを必ず添える
107
+ - 概念ごとに最適なFont Awesomeアイコンを選定(抽象的な概念には複数の関連アイコンを組み合わせて表現)
108
+ - 数値データは簡潔なグラフや図表で表現し、データ種類に応じたFont Awesomeアイコン(fa-chart-pie, fa-chart-column等)を配置
109
+ - 接続線や矢印で情報間の関係性を明示し、関係性の種類に応じたアイコン(fa-link, fa-code-branch等)を添える
110
+ - 余白を効果的に活用して視認性を確保(余白にも薄いFont Awesomeパターンを配置可)
111
+ - コントラストと色の使い分けでメリハリを付け、カラースキームに沿ったアイコン色を活用
112
+ ## Font Awesomeアイコン活用ガイドライン
113
+ - 概念カテゴリー別の推奨アイコン:
114
+ - 時間・順序:fa-clock, fa-hourglass, fa-calendar, fa-timeline
115
+ - 場所・位置:fa-location-dot, fa-map, fa-compass, fa-globe
116
+ - 人物・組織:fa-user, fa-users, fa-building, fa-sitemap
117
+ - 行動・活動:fa-person-running, fa-gears, fa-hammer, fa-rocket
118
+ - 思考・アイデア:fa-brain, fa-lightbulb, fa-thought-bubble, fa-comments
119
+ - 感情・状態:fa-face-smile, fa-face-sad-tear, fa-heart, fa-temperature-half
120
+ - 成長・変化:fa-seedling, fa-arrow-trend-up, fa-chart-line, fa-diagram-project
121
+ - 問題・課題:fa-triangle-exclamation, fa-circle-question, fa-bug, fa-ban
122
+ - 解決・成功:fa-check, fa-trophy, fa-handshake, fa-key
123
+ - アイコンサイズの使い分け:
124
+ - 主要概念:3x(fa-3x)
125
+ - 重要キーワード:2x(fa-2x)
126
+ - 補足情報:1x(標準サイズ)
127
+ - 装飾的要素:lg(fa-lg)
128
+ - アニメーション効果の適切な使い分け:
129
+ - 注目喚起:fa-beat, fa-shake
130
+ - 継続的プロセス:fa-spin, fa-pulse
131
+ - 状態変化:fa-flip, fa-fade
132
+ - 新規情報:fa-bounce
133
+ ## 全体的な指針
134
+ - 読み手が自然に視線を移動できる配置(Font Awesomeアイコンで視線誘導)
135
+ - 情報の階層と関連性を視覚的に明確化(階層ごとにアイコンのサイズや色を変える)
136
+ - 手書き風の要素とFont Awesomeアイコンを組み合わせて親しみやすさとプロフェッショナル感を両立
137
+ - 大きなFont Awesomeアイコンを活用した視覚的な記憶に残るデザイン(各セクションに象徴的なアイコンを配置)
138
+ - フッターに出典情報と関連するFont Awesomeアイコン(fa-book, fa-citation等)を明記
139
+ ## 変換する文章/記事
140
+ ーーー<ユーザーが入力(または添付)>ーーー"""
141
+
142
+ # モデルを初期化して処理
143
+ logger.info(f"Gemini APIにリクエストを送信: テキスト長さ = {len(text)}")
144
+
145
+ # 利用可能なモデルの確認
146
+ try:
147
+ models = genai.list_models()
148
+ logger.info(f"利用可能なモデル: {[m.name for m in models if 'gemini' in m.name]}")
149
+ except Exception as e:
150
+ logger.warning(f"モデル一覧取得エラー: {e}")
151
+
152
+ # モデル選択
153
+ try:
154
+ model = genai.GenerativeModel('gemini-1.5-pro')
155
+ except Exception as e:
156
+ logger.warning(f"gemini-1.5-proモデル利用不可: {e}, gemini-proを試します")
157
+ model = genai.GenerativeModel('gemini-pro')
158
+
159
+ # 生成設定
160
+ generation_config = {
161
+ "temperature": 0.7,
162
+ "top_p": 0.95,
163
+ "top_k": 64,
164
+ "max_output_tokens": 8192,
165
+ }
166
+
167
+ # プロンプト構築
168
+ prompt = f"{system_instruction}\n\n{text}"
169
+
170
+ # コンテンツ生成
171
+ response = model.generate_content(
172
+ prompt,
173
+ generation_config=generation_config
174
+ )
175
+
176
+ # レスポンスからHTMLを抽出
177
+ raw_response = response.text
178
+
179
+ # HTMLタグ部分だけを抽出(```html と ``` の間)
180
+ html_start = raw_response.find("```html")
181
+ html_end = raw_response.rfind("```")
182
+
183
+ if html_start != -1 and html_end != -1 and html_start < html_end:
184
+ html_start += 7 # "```html" の長さ分進める
185
+ html_code = raw_response[html_start:html_end].strip()
186
+ logger.info(f"HTMLの生成に成功: 長さ = {len(html_code)}")
187
+ return html_code
188
+ else:
189
+ # HTMLタグが見つからない場合、レスポンス全体を返す
190
+ logger.warning("レスポンスから```html```タグが見つかりませんでした。全テキストを返します。")
191
+ return raw_response
192
+
193
+ except Exception as e:
194
+ logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
195
+ raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")
196
+
197
+ # --- Core Screenshot Logic ---
198
+ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_gemini_content: bool = False) -> Image.Image:
199
  """
200
  Renders HTML code to a full-page screenshot using Selenium.
201
 
202
  Args:
203
  html_code: The HTML source code string.
204
  extension_percentage: Percentage of extra space to add vertically (e.g., 4 means 4% total).
205
+ is_gemini_content: True if the HTML was generated by Gemini API (requires special handling).
206
 
207
  Returns:
208
  A PIL Image object of the screenshot. Returns a 1x1 black image on error.
 
210
  tmp_path = None # 初期化
211
  driver = None # 初期化
212
 
213
+ # Gemini生成コンテンツの場合、拡張率を調整
214
+ if is_gemini_content:
215
+ # 最低でも20%の拡張を確保
216
+ extension_percentage = max(extension_percentage, 20.0)
217
+ logger.info(f"Gemini生成コンテンツ用に拡張率を調整: {extension_percentage}%")
218
 
219
  # 1) Save HTML code to a temporary file
220
  try:
 
232
  options.add_argument("--no-sandbox")
233
  options.add_argument("--disable-dev-shm-usage")
234
  options.add_argument("--force-device-scale-factor=1")
235
+ # Font Awesomeが読み込まれない場合があるため、読み込み待機時間を長く設定
236
+ options.add_argument("--disable-features=NetworkService")
237
+ options.add_argument("--dns-prefetch-disable")
 
238
  # Increase logging verbosity for debugging if needed
239
  # options.add_argument("--enable-logging")
240
  # options.add_argument("--v=1")
 
244
  driver = webdriver.Chrome(options=options)
245
  logger.info("WebDriver initialized.")
246
 
247
+ # 3) Load page with initial large window size (Geminiコンテンツ用に高さを増やす)
248
+ initial_width = 1800
249
+ initial_height = 2000 if is_gemini_content else 1200
250
+ driver.set_window_size(initial_width, initial_height)
 
 
251
  file_url = "file://" + tmp_path
252
  logger.info(f"Navigating to {file_url}")
253
  driver.get(file_url)
254
 
255
+ # 4) Wait for page load with extended timeout
256
  logger.info("Waiting for body element...")
257
+ WebDriverWait(driver, 15).until( # タイムアウトを少し延長
 
258
  EC.presence_of_element_located((By.TAG_NAME, "body"))
259
  )
260
+ logger.info("Body element found. Waiting for potential resource loading...")
261
+
262
+ # Gemini生成コンテンツの場合、リソース読み込みに余裕を持たせる
263
+ wait_time = 6 if is_gemini_content else 3
264
+ time.sleep(wait_time) # Wait longer for external resources/scripts
265
 
 
 
 
 
 
 
 
 
 
266
  # 5) Hide scrollbars via CSS
267
  try:
268
  driver.execute_script(
 
273
  except Exception as e:
274
  logger.warning(f"Could not hide scrollbars via JS: {e}")
275
 
276
+ # 6) Get full page dimensions accurately with improved script
277
  try:
278
+ # より正確なページ寸法を取得するためのJavaScriptコード
279
+ dimensions_script = """
280
+ return {
281
+ width: Math.max(
282
+ document.documentElement.scrollWidth,
283
+ document.documentElement.offsetWidth,
284
+ document.documentElement.clientWidth,
285
+ document.body ? document.body.scrollWidth : 0,
286
+ document.body ? document.body.offsetWidth : 0,
287
+ document.body ? document.body.clientWidth : 0
288
+ ),
289
+ height: Math.max(
290
+ document.documentElement.scrollHeight,
291
+ document.documentElement.offsetHeight,
292
+ document.documentElement.clientHeight,
293
+ document.body ? document.body.scrollHeight : 0,
294
+ document.body ? document.body.offsetHeight : 0,
295
+ document.body ? document.body.clientHeight : 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  )
297
+ };
298
+ """
299
+ dimensions = driver.execute_script(dimensions_script)
300
+ scroll_width = dimensions['width']
301
+ scroll_height = dimensions['height']
302
+
303
  logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}")
304
+
305
+ # スクロールして確認する追加の検証
306
+ driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
307
+ time.sleep(1) # スクロール完了を待つ
308
+ driver.execute_script("window.scrollTo(0, 0);")
309
+ time.sleep(1) # 元の位置に戻す
310
+
311
+ # 再検証
312
+ dimensions_after_scroll = driver.execute_script(dimensions_script)
313
+ scroll_height = max(scroll_height, dimensions_after_scroll['height'])
314
+
315
+ logger.info(f"After scroll check, height={scroll_height}")
316
+
317
  # Ensure minimum dimensions to avoid errors
318
  scroll_width = max(scroll_width, 100) # 最小幅を設定
319
  scroll_height = max(scroll_height, 100) # 最小高さを設定
 
321
  except Exception as e:
322
  logger.error(f"Error getting page dimensions: {e}")
323
  # フォールバックとしてデフォルト値を設定
324
+ scroll_width = 1800
325
+ scroll_height = 2000 if is_gemini_content else 1200
 
 
 
 
326
  logger.warning(f"Falling back to dimensions: width={scroll_width}, height={scroll_height}")
327
 
328
  # 7) Calculate adjusted height with user-specified margin
 
332
  logger.info(f"Adjusted height calculated: {adjusted_height} (extension: {extension_percentage}%)")
333
 
334
  # 8) Set window size to full page dimensions (width) and adjusted height
335
+ adjusted_width = max(scroll_width, initial_width)
336
+ logger.info(f"Resizing window to: width={adjusted_width}, height={adjusted_height}")
337
+ driver.set_window_size(adjusted_width, adjusted_height)
338
  logger.info("Waiting for layout stabilization after resize...")
339
+
340
+ # Gemini生成コンテンツの場合はさらに長く待機
341
+ stabilization_time = 5 if is_gemini_content else 3
342
+ time.sleep(stabilization_time) # Wait longer for layout stabilization
343
+
344
+ # 外部リソースの読み込み状態を確認
345
+ try:
346
+ resource_state = driver.execute_script("""
347
+ return {
348
+ readyState: document.readyState,
349
+ resourcesComplete: !document.querySelector('img:not([complete])') &&
350
+ !document.querySelector('link[rel="stylesheet"]:not([loaded])')
351
+ };
352
+ """)
353
+ logger.info(f"Resource state: {resource_state}")
354
+
355
+ # ドキュメントの読み込みが完了していない場合、追加で待機
356
+ if resource_state['readyState'] != 'complete':
357
+ logger.info("Document still loading, waiting additional time...")
358
+ time.sleep(2)
359
+ except Exception as e:
360
+ logger.warning(f"Error checking resource state: {e}")
361
 
362
  # Scroll to top just in case
363
  try:
 
369
 
370
  # 9) Take screenshot
371
  logger.info("Taking screenshot...")
 
 
 
 
 
 
372
  png = driver.get_screenshot_as_png()
373
  logger.info("Screenshot taken successfully.")
374
 
375
  # Convert to PIL Image
376
  img = Image.open(BytesIO(png))
377
+
378
+ # 画像サイズの確認とログ
379
+ logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
380
+
381
  return img
382
 
383
  except Exception as e:
 
405
  # 1. テキストからHTMLを生成
406
  html_code = generate_html_from_text(text)
407
 
408
+ # 2. HTMLからスクリーンショットを生成(Gemini生成コンテンツとしてフラグをオン)
409
+ return render_fullpage_screenshot(html_code, extension_percentage, is_gemini_content=True)
410
  except Exception as e:
411
  logger.error(f"テキストからスクリーンショット生成中にエラーが発生: {e}", exc_info=True)
412
+ return Image.new('RGB', (1, 1), color=(0, 0, 0)) # エラー時は黒画像
413
+
414
+ # --- FastAPI Setup ---
415
+ app = FastAPI()
416
+
417
+ # CORS設定を追加
418
+ app.add_middleware(
419
+ CORSMiddleware,
420
+ allow_origins=["*"],
421
+ allow_credentials=True,
422
+ allow_methods=["*"],
423
+ allow_headers=["*"],
424
+ )
425
+
426
+ # 静的ファイルのサービング設定
427
+ # Gradioのディレクトリを探索してアセットを見つける
428
+ gradio_dir = os.path.dirname(gr.__file__)
429
+ logger.info(f"Gradio version: {gr.__version__}")
430
+ logger.info(f"Gradio directory: {gradio_dir}")
431
+
432
+ # 基本的な静的ファイルディレクトリをマウント
433
+ static_dir = os.path.join(gradio_dir, "templates", "frontend", "static")
434
+ if os.path.exists(static_dir):
435
+ logger.info(f"Mounting static directory: {static_dir}")
436
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
437
+
438
+ # _appディレクトリを探す(新しいSvelteKitベースのフロントエンド用)
439
+ app_dir = os.path.join(gradio_dir, "templates", "frontend", "_app")
440
+ if os.path.exists(app_dir):
441
+ logger.info(f"Mounting _app directory: {app_dir}")
442
+ app.mount("/_app", StaticFiles(directory=app_dir), name="_app")
443
+
444
+ # assetsディレクトリを探す
445
+ assets_dir = os.path.join(gradio_dir, "templates", "frontend", "assets")
446
+ if os.path.exists(assets_dir):
447
+ logger.info(f"Mounting assets directory: {assets_dir}")
448
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
449
+
450
+ # cdnディレクトリがあれば追加
451
+ cdn_dir = os.path.join(gradio_dir, "templates", "cdn")
452
+ if os.path.exists(cdn_dir):
453
+ logger.info(f"Mounting cdn directory: {cdn_dir}")
454
+ app.mount("/cdn", StaticFiles(directory=cdn_dir), name="cdn")
455
+
456
+ # Pydantic model for API request body validation
457
+ class ScreenshotRequest(BaseModel):
458
+ html_code: str
459
+ extension_percentage: float = 8.0 # Default value same as Gradio slider
460
+
461
+ # API Endpoint for screenshot generation
462
+ @app.post("/api/screenshot",
463
+ response_class=StreamingResponse,
464
+ tags=["Screenshot"],
465
+ summary="Render HTML to Full Page Screenshot",
466
+ 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.")
467
+ async def api_render_screenshot(request: ScreenshotRequest):
468
+ """
469
+ API endpoint to render HTML and return a screenshot.
470
+ """
471
+ try:
472
+ logger.info(f"API request received. Extension: {request.extension_percentage}%")
473
+ # Run the blocking Selenium code in a separate thread (FastAPI handles this)
474
+ pil_image = render_fullpage_screenshot(
475
+ request.html_code,
476
+ request.extension_percentage
477
+ )
478
+
479
+ if pil_image.size == (1, 1):
480
+ logger.error("Screenshot generation failed, returning 1x1 image.")
481
+ # Optionally return a proper error response instead of 1x1 image
482
+ # raise HTTPException(status_code=500, detail="Failed to generate screenshot")
483
+
484
+ # Convert PIL Image to PNG bytes
485
+ img_byte_arr = BytesIO()
486
+ pil_image.save(img_byte_arr, format='PNG')
487
+ img_byte_arr.seek(0) # Go to the start of the BytesIO buffer
488
+
489
+ logger.info("Returning screenshot as PNG stream.")
490
+ return StreamingResponse(img_byte_arr, media_type="image/png")
491
+
492
+ except Exception as e:
493
+ logger.error(f"API Error: {e}", exc_info=True)
494
+ raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
495
+
496
+ # --- 新しいGemini API連携エンドポイント ---
497
+ @app.post("/api/text-to-screenshot",
498
+ response_class=StreamingResponse,
499
+ tags=["Screenshot", "Gemini"],
500
+ summary="テキストからインフォグラフィックを生成",
501
+ description="テキストをGemini APIを使ってHTMLインフォグラフィックに変換し、スクリーンショットとして返します。")
502
+ async def api_text_to_screenshot(request: GeminiRequest):
503
+ """
504
+ テキストからHTMLインフォグラフィックを生成してスクリーンショットを返すAPIエンドポイント
505
+ """
506
+ try:
507
+ logger.info(f"テキスト→スクリーンショットAPIリクエスト受信。テキスト長さ: {len(request.text)}, 拡張率: {request.extension_percentage}%")
508
+
509
+ # テキストからHTMLを生成してスクリーンショットを作成
510
+ pil_image = text_to_screenshot(
511
+ request.text,
512
+ request.extension_percentage
513
+ )
514
+
515
+ if pil_image.size == (1, 1):
516
+ logger.error("スクリーンショット生成に失敗しました。1x1画像を返します。")
517
+ # raise HTTPException(status_code=500, detail="スクリーンショット��成に失敗しました")
518
+
519
+ # PIL画像をPNGバイトに変換
520
+ img_byte_arr = BytesIO()
521
+ pil_image.save(img_byte_arr, format='PNG')
522
+ img_byte_arr.seek(0) # BytesIOバッファの先頭に戻る
523
+
524
+ logger.info("スクリーンショットをPNGストリームとして返します。")
525
+ return StreamingResponse(img_byte_arr, media_type="image/png")
526
+
527
+ except Exception as e:
528
+ logger.error(f"API Error: {e}", exc_info=True)
529
+ raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
530
+
531
+ # --- Gradio Interface Definition ---
532
+ # 入力モードの選択用Radioコンポーネント
533
+ def process_input(input_mode, html_input, text_input, extension_percentage):
534
+ """入力モードに応じて適切な処理を行う"""
535
+ if input_mode == "HTML入力":
536
+ # HTMLモードの場合は既存の処理
537
+ return render_fullpage_screenshot(html_input, extension_percentage)
538
+ else:
539
+ # テキスト入力モードの場合はGemini APIを使用
540
+ return text_to_screenshot(text_input, extension_percentage)
541
+
542
+ # Gradio UIの定義
543
+ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
544
+ gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
545
+ gr.Markdown("HTMLをヘッドレスブラウザでレンダリングするか、テキストをGemini APIでインフォグラフィックに変換して画像として取得します。")
546
+
547
+ with gr.Row():
548
+ input_mode = gr.Radio(
549
+ ["HTML入力", "テキスト入力"],
550
+ label="入力モード",
551
+ value="HTML入力"
552
+ )
553
+
554
+ with gr.Tabs():
555
+ with gr.TabItem("HTML入力"):
556
+ html_input = gr.Textbox(
557
+ lines=15,
558
+ label="HTMLコード入力",
559
+ placeholder="<!DOCTYPE html>\n<html>\n<head>\n <title>Example</title>\n</head>\n<body>\n <h1>Hello World</h1>\n</body>\n</html>"
560
+ )
561
+
562
+ with gr.TabItem("テキスト入力"):
563
+ text_input = gr.Textbox(
564
+ lines=15,
565
+ label="テキスト入力 (Geminiで変換)",
566
+ placeholder="ここにテキストを入力すると、Gemini APIによってグラフィカルなHTMLに変換されます。"
567
+ )
568
+
569
+ extension_percentage = gr.Slider(
570
+ minimum=0,
571
+ maximum=30, # より高い値を許可
572
+ step=1.0,
573
+ value=15, # デフォルト値を増加
574
+ label="上下高さ拡張率(%)"
575
+ )
576
+
577
+ submit_btn = gr.Button("生成")
578
+ output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
579
+
580
+ submit_btn.click(
581
+ fn=process_input,
582
+ inputs=[input_mode, html_input, text_input, extension_percentage],
583
+ outputs=output_image
584
+ )
585
+
586
+ gr.Markdown("""
587
+ ## APIエンドポイント
588
+ - `/api/screenshot` - HTMLコードからスクリーンショットを生成
589
+ - `/api/text-to-screenshot` - テキストからインフォグラフィックスクリーンショットを生成
590
+ """)
591
+
592
+ # --- Mount Gradio App onto FastAPI ---
593
+ app = gr.mount_gradio_app(app, iface, path="/")
594
+
595
+ # --- Run with Uvicorn (for local testing) ---
596
+ if __name__ == "__main__":
597
+ import uvicorn
598
+ logger.info("Starting Uvicorn server for local development...")
599
+ uvicorn.run(app, host="0.0.0.0", port=7860)