tomo2chin2 commited on
Commit
23914ff
·
verified ·
1 Parent(s): fca3129

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -125
app.py CHANGED
@@ -1,6 +1,9 @@
1
  # --- START OF FILE app.py ---
2
 
3
  import gradio as gr
 
 
 
4
  from selenium import webdriver
5
  from selenium.webdriver.chrome.options import Options
6
  from selenium.webdriver.common.by import By
@@ -11,197 +14,233 @@ from io import BytesIO
11
  import tempfile
12
  import time
13
  import os
14
- from fastapi import FastAPI, HTTPException, Request
15
- from fastapi.responses import StreamingResponse
16
- from pydantic import BaseModel, Field
17
- import logging
18
 
19
- # ロギング設定 (デバッグに役立つ)
20
  logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
- # --- コアロジック: スクリーンショット生成関数 ---
24
- # (元のコードから変更なし、エラーハンドリングを少し明確化)
25
  def render_fullpage_screenshot(html_code: str, extension_percentage: float) -> Image.Image:
26
  """
27
- html_code: HTMLのソースコード文字列
28
- extension_percentage: 上下に追加する余裕の% (例: 4なら合計4%の余裕)
29
- 戻り値: PIL Imageオブジェクト。エラー時は1x1の黒画像。
 
 
 
 
 
30
  """
31
- tmp_file = None
32
- driver = None
33
- tmp_path = ""
34
 
 
35
  try:
36
- # 1) HTMLコードを一時ファイルに保存
37
- # delete=False と NamedTemporaryFile の close/remove の組み合わせはLinux/Windowsで挙動が安定しないことがあるため、
38
- # with構文と手動削除を確実に組み合わせる
39
  with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
40
  tmp_path = tmp_file.name
41
  tmp_file.write(html_code)
42
  logger.info(f"HTML saved to temporary file: {tmp_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- # 2) ヘッドレスChrome(Chromium)起動オプション
45
- options = Options()
46
- options.add_argument("--headless")
47
- options.add_argument("--no-sandbox")
48
- options.add_argument("--disable-dev-shm-usage") # メモリ不足エラー対策
49
- options.add_argument("--force-device-scale-factor=1") # DPIスケーリング無効化
50
- options.add_argument("--window-size=1200,800") # 初期ウィンドウサイズ
51
- # デバッグ用にログレベルを上げる (オプション)
52
- # options.add_argument("--enable-logging")
53
- # options.add_argument("--v=1")
54
-
55
  logger.info("Initializing WebDriver...")
56
- # サービスオブジェクトを作成してログ出力を制御 (オプション)
57
- # service = webdriver.chrome.service.Service(log_output=subprocess.STDOUT)
58
- # driver = webdriver.Chrome(options=options, service=service)
59
  driver = webdriver.Chrome(options=options)
60
  logger.info("WebDriver initialized.")
61
 
62
- # 3) 初期ウィンドウサイズでページを読み込む (オプションで設定済みなので不要かも)
63
- # driver.set_window_size(1200, 800)
64
  file_url = "file://" + tmp_path
65
  logger.info(f"Navigating to {file_url}")
66
  driver.get(file_url)
67
 
68
- # 4) ページのロードを待機 (body要素の存在確認)
69
  logger.info("Waiting for body element...")
70
- WebDriverWait(driver, 20).until( # タイムアウトを少し延長
71
  EC.presence_of_element_located((By.TAG_NAME, "body"))
72
  )
73
- logger.info("Body element found. Waiting for potential dynamic content loading...")
74
- time.sleep(3) # 外部リソースやJSによる動的変更の待機時間を少し増やす
75
-
76
- # 5) スクロールバーがキャプチャに写らないようCSSで非表示
77
- logger.info("Hiding scrollbars via JS...")
78
- driver.execute_script(
79
- "document.documentElement.style.overflow = 'hidden';"
80
- "document.body.style.overflow = 'hidden';"
81
- )
82
-
83
- # 6) ページ全体の幅・高さを正確に取得
84
- logger.info("Calculating page dimensions...")
85
- scroll_width = driver.execute_script(
86
- "return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth)"
87
- )
88
- scroll_height = driver.execute_script(
89
- "return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight)"
90
- )
91
- logger.info(f"Calculated dimensions: width={scroll_width}, height={scroll_height}")
92
-
93
- # 幅が0や極端に小さい場合はデフォルト値を使うなど対策が必要になる可能性あり
94
- if scroll_width < 100:
95
- logger.warning(f"Detected very small scroll_width ({scroll_width}), defaulting to 1200")
96
- scroll_width = 1200
97
-
98
- # 7) 上下方向にだけユーザー指定の余裕(%)を加えた高さを計算
99
- adjusted_height = int(scroll_height * (1 + extension_percentage / 100))
100
- logger.info(f"Adjusted height: {adjusted_height} (extension: {extension_percentage}%)")
101
-
102
- # 8) ウィンドウサイズを、幅はそのまま、縦はadjusted_heightに変更
103
- logger.info(f"Resizing window to {scroll_width} x {adjusted_height}")
104
- # set_window_sizeが期待通りに動作しない場合があるため、最大化してからサイズ変更を試すことも検討
105
- # driver.maximize_window()
 
 
 
 
 
 
 
 
 
 
 
106
  driver.set_window_size(scroll_width, adjusted_height)
107
  logger.info("Waiting for layout stabilization after resize...")
108
- time.sleep(3) # レイアウトの安定化待機時間を少し増やす
109
 
110
- # 念のため最上部にスクロール
111
- logger.info("Scrolling to top...")
112
- driver.execute_script("window.scrollTo(0, 0)")
113
- time.sleep(1)
 
 
 
114
 
115
- # 9) スクリーンショット取得
116
  logger.info("Taking screenshot...")
117
  png = driver.get_screenshot_as_png()
118
  logger.info("Screenshot taken successfully.")
119
 
120
- return Image.open(BytesIO(png))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  except Exception as e:
123
- logger.error(f"Error during screenshot generation: {e}", exc_info=True)
124
- # エラー時は1x1の黒画像を返す
125
- return Image.new('RGB', (1, 1), color=(0, 0, 0))
 
 
 
 
126
  finally:
127
- # 必ず WebDriver を終了する
128
  if driver:
129
- logger.info("Quitting WebDriver...")
130
- driver.quit()
131
- logger.info("WebDriver quit.")
132
- # 一時ファイルを確実に削除する
 
133
  if tmp_path and os.path.exists(tmp_path):
134
  try:
135
  os.remove(tmp_path)
136
- logger.info(f"Temporary file removed: {tmp_path}")
137
- except OSError as e:
138
  logger.error(f"Error removing temporary file {tmp_path}: {e}")
139
 
140
- # --- FastAPI アプリケーション設定 ---
141
- app = FastAPI(
142
- title="Full Page Screenshot API",
143
- description="Renders HTML using a headless browser and returns a full-page screenshot.",
144
- version="1.0.0"
145
- )
146
 
147
- # --- APIリクエストモデル ---
148
  class ScreenshotRequest(BaseModel):
149
- html_code: str = Field(..., description="HTML source code to render.")
150
- extension_percentage: float = Field(8.0, ge=0, le=100, description="Percentage of extra height to add (top and bottom combined). Default: 8%")
151
 
152
- # --- APIエンドポイント ---
153
- @app.post("/screenshot",
154
  response_class=StreamingResponse,
155
  tags=["Screenshot"],
156
- summary="Generate Full Page Screenshot",
157
- description="Takes HTML code and returns a PNG image of the rendered full page.")
158
- async def create_screenshot(request: ScreenshotRequest):
159
  """
160
- Generates a full-page screenshot from the provided HTML code.
161
  """
162
- logger.info(f"Received API request. HTML length: {len(request.html_code)}, Extension: {request.extension_percentage}%")
163
  try:
164
- pil_image = render_fullpage_screenshot(request.html_code, request.extension_percentage)
 
 
 
 
 
165
 
166
- # エラー画像 (1x1) が返ってきたかチェック
167
  if pil_image.size == (1, 1):
168
- logger.error("Screenshot generation failed, returning 500 error.")
169
- # エラーを示すために 500 Internal Server Error を返すことも検討できる
170
- # raise HTTPException(status_code=500, detail="Failed to generate screenshot. Check HTML or server logs.")
171
- # ここでは仕様通り 1x1 画像を返すことにするが、APIとしてはエラーの方が親切かもしれない
172
 
173
- # PIL画像をPNGバイトデータに変換
 
174
  img_byte_arr = BytesIO()
175
  pil_image.save(img_byte_arr, format='PNG')
176
- img_byte_arr.seek(0) # ポインタを先頭に戻す
177
 
178
  logger.info("Returning screenshot as PNG stream.")
179
  return StreamingResponse(img_byte_arr, media_type="image/png")
180
 
181
  except Exception as e:
182
- logger.error(f"Unhandled exception in API endpoint: {e}", exc_info=True)
183
- raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
184
 
185
- # --- Gradioインターフェース定義 ---
186
- gradio_interface = gr.Interface(
187
- fn=render_fullpage_screenshot, # コアロジック関数を再利用
 
188
  inputs=[
189
  gr.Textbox(lines=15, label="HTMLコード入力"),
190
- gr.Slider(minimum=0, maximum=30, step=1.0, value=8, label="上下高さ拡張率(%)") # 最大値を少し増やす
191
  ],
192
  outputs=gr.Image(type="pil", label="ページ全体のスクリーンショット"),
193
- title="Full Page Screenshot (高さ拡張調整可能)",
194
- description="HTMLをヘッドレスブラウザでレンダリングし、ページ全体を1枚の画像として取得します。上下のみユーザー指定の余裕(%)を追加します。API /screenshot (POST) で利用可能です。",
195
- allow_flagging="never" # Hugging Face Spacesでフラグ機能を無効化
196
  )
197
 
198
- # --- GradioアプリをFastAPIにマウント ---
199
- # ルートパス ("/") でGradioインターフェースを提供
200
- app = gr.mount_gradio_app(app, gradio_interface, path="/")
201
-
202
- # --- uvicorn で実行するための設定 (Hugging Face Spacesでは通常不要) ---
203
- # if __name__ == "__main__":
204
- # import uvicorn
205
- # uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
206
 
207
  # --- END OF FILE app.py ---
 
1
  # --- START OF FILE app.py ---
2
 
3
  import gradio as gr
4
+ from fastapi import FastAPI, HTTPException, Body
5
+ from fastapi.responses import StreamingResponse
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
 
14
  import tempfile
15
  import time
16
  import os
17
+ import logging # loggingを追加
 
 
 
18
 
19
+ # ロギング設定
20
  logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
+ # --- Core Screenshot Logic ---
 
24
  def render_fullpage_screenshot(html_code: str, extension_percentage: float) -> Image.Image:
25
  """
26
+ Renders HTML code to a full-page screenshot using Selenium.
27
+
28
+ Args:
29
+ html_code: The HTML source code string.
30
+ extension_percentage: Percentage of extra space to add vertically (e.g., 4 means 4% total).
31
+
32
+ Returns:
33
+ A PIL Image object of the screenshot. Returns a 1x1 black image on error.
34
  """
35
+ tmp_path = None # 初期化
36
+ driver = None # 初期化
 
37
 
38
+ # 1) Save HTML code to a temporary file
39
  try:
 
 
 
40
  with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
41
  tmp_path = tmp_file.name
42
  tmp_file.write(html_code)
43
  logger.info(f"HTML saved to temporary file: {tmp_path}")
44
+ except Exception as e:
45
+ logger.error(f"Error writing temporary HTML file: {e}")
46
+ return Image.new('RGB', (1, 1), color=(0, 0, 0)) # エラー時は黒画像
47
+
48
+ # 2) Headless Chrome(Chromium) options
49
+ options = Options()
50
+ options.add_argument("--headless")
51
+ options.add_argument("--no-sandbox")
52
+ options.add_argument("--disable-dev-shm-usage")
53
+ options.add_argument("--force-device-scale-factor=1")
54
+ # Increase logging verbosity for debugging if needed
55
+ # options.add_argument("--enable-logging")
56
+ # options.add_argument("--v=1")
57
 
58
+ try:
 
 
 
 
 
 
 
 
 
 
59
  logger.info("Initializing WebDriver...")
 
 
 
60
  driver = webdriver.Chrome(options=options)
61
  logger.info("WebDriver initialized.")
62
 
63
+ # 3) Load page with initial window size
64
+ driver.set_window_size(1200, 800)
65
  file_url = "file://" + tmp_path
66
  logger.info(f"Navigating to {file_url}")
67
  driver.get(file_url)
68
 
69
+ # 4) Wait for page load
70
  logger.info("Waiting for body element...")
71
+ WebDriverWait(driver, 15).until( # タイムアウトを少し延長
72
  EC.presence_of_element_located((By.TAG_NAME, "body"))
73
  )
74
+ logger.info("Body element found. Waiting for potential resource loading...")
75
+ time.sleep(3) # Wait a bit longer for external resources/scripts
76
+
77
+ # 5) Hide scrollbars via CSS
78
+ try:
79
+ driver.execute_script(
80
+ "document.documentElement.style.overflow = 'hidden';"
81
+ "document.body.style.overflow = 'hidden';"
82
+ )
83
+ logger.info("Scrollbars hidden via JS.")
84
+ except Exception as e:
85
+ logger.warning(f"Could not hide scrollbars via JS: {e}")
86
+
87
+
88
+ # 6) Get full page dimensions accurately
89
+ try:
90
+ scroll_width = driver.execute_script(
91
+ "return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth)"
92
+ )
93
+ scroll_height = driver.execute_script(
94
+ "return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight)"
95
+ )
96
+ logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}")
97
+ # Ensure minimum dimensions to avoid errors
98
+ scroll_width = max(scroll_width, 100) # 最小幅を設定
99
+ scroll_height = max(scroll_height, 100) # 最小高さを設定
100
+
101
+ except Exception as e:
102
+ logger.error(f"Error getting page dimensions: {e}")
103
+ # フォールバックとしてデフォルト値を設定
104
+ scroll_width = 1200
105
+ scroll_height = 800
106
+ logger.warning(f"Falling back to dimensions: width={scroll_width}, height={scroll_height}")
107
+
108
+
109
+ # 7) Calculate adjusted height with user-specified margin
110
+ adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
111
+ # Ensure adjusted height is not excessively large or small
112
+ adjusted_height = max(adjusted_height, scroll_height, 100) # 最小高さを確保
113
+ logger.info(f"Adjusted height calculated: {adjusted_height} (extension: {extension_percentage}%)")
114
+
115
+
116
+ # 8) Set window size to full page dimensions (width) and adjusted height
117
+ logger.info(f"Resizing window to: width={scroll_width}, height={adjusted_height}")
118
  driver.set_window_size(scroll_width, adjusted_height)
119
  logger.info("Waiting for layout stabilization after resize...")
120
+ time.sleep(3) # Wait longer for layout stabilization
121
 
122
+ # Scroll to top just in case
123
+ try:
124
+ driver.execute_script("window.scrollTo(0, 0)")
125
+ time.sleep(1)
126
+ logger.info("Scrolled to top.")
127
+ except Exception as e:
128
+ logger.warning(f"Could not scroll to top: {e}")
129
 
130
+ # 9) Take screenshot
131
  logger.info("Taking screenshot...")
132
  png = driver.get_screenshot_as_png()
133
  logger.info("Screenshot taken successfully.")
134
 
135
+ # Convert to PIL Image
136
+ img = Image.open(BytesIO(png))
137
+ # Crop the image back to the original scroll_height plus margin
138
+ # This removes extra blank space at the bottom if window resize was much larger
139
+ # The screenshot captures the viewport, which we set to adjusted_height
140
+ # We only need scroll_height + (scroll_height * extension_percentage / 200) top/bottom margin
141
+ # However, simple approach: use adjusted_height directly or crop if needed.
142
+ # Let's return the full adjusted_height capture for now.
143
+ # If cropping is needed:
144
+ # final_height = int(scroll_height * (1 + extension_percentage / 100.0))
145
+ # if img.height > final_height:
146
+ # img = img.crop((0, 0, img.width, final_height))
147
+ # logger.info(f"Cropped image to final height: {final_height}")
148
+
149
+ return img
150
 
151
  except Exception as e:
152
+ logger.error(f"An error occurred during screenshot generation: {e}", exc_info=True)
153
+ # Optionally capture a screenshot even on error for debugging
154
+ # try:
155
+ # if driver: driver.save_screenshot("error_screenshot.png")
156
+ # except Exception as screen_err:
157
+ # logger.error(f"Could not save error screenshot: {screen_err}")
158
+ return Image.new('RGB', (1, 1), color=(0, 0, 0)) # Return black 1x1 image on error
159
  finally:
160
+ logger.info("Cleaning up...")
161
  if driver:
162
+ try:
163
+ driver.quit()
164
+ logger.info("WebDriver quit successfully.")
165
+ except Exception as e:
166
+ logger.error(f"Error quitting WebDriver: {e}")
167
  if tmp_path and os.path.exists(tmp_path):
168
  try:
169
  os.remove(tmp_path)
170
+ logger.info(f"Temporary file {tmp_path} removed.")
171
+ except Exception as e:
172
  logger.error(f"Error removing temporary file {tmp_path}: {e}")
173
 
174
+ # --- FastAPI Setup ---
175
+ app = FastAPI()
 
 
 
 
176
 
177
+ # Pydantic model for API request body validation
178
  class ScreenshotRequest(BaseModel):
179
+ html_code: str
180
+ extension_percentage: float = 8.0 # Default value same as Gradio slider
181
 
182
+ # API Endpoint for screenshot generation
183
+ @app.post("/api/screenshot",
184
  response_class=StreamingResponse,
185
  tags=["Screenshot"],
186
+ summary="Render HTML to Full Page Screenshot",
187
+ 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.")
188
+ async def api_render_screenshot(request: ScreenshotRequest):
189
  """
190
+ API endpoint to render HTML and return a screenshot.
191
  """
 
192
  try:
193
+ logger.info(f"API request received. Extension: {request.extension_percentage}%")
194
+ # Run the blocking Selenium code in a separate thread (FastAPI handles this)
195
+ pil_image = render_fullpage_screenshot(
196
+ request.html_code,
197
+ request.extension_percentage
198
+ )
199
 
 
200
  if pil_image.size == (1, 1):
201
+ logger.error("Screenshot generation failed, returning 1x1 image.")
202
+ # Optionally return a proper error response instead of 1x1 image
203
+ # raise HTTPException(status_code=500, detail="Failed to generate screenshot")
 
204
 
205
+
206
+ # Convert PIL Image to PNG bytes
207
  img_byte_arr = BytesIO()
208
  pil_image.save(img_byte_arr, format='PNG')
209
+ img_byte_arr.seek(0) # Go to the start of the BytesIO buffer
210
 
211
  logger.info("Returning screenshot as PNG stream.")
212
  return StreamingResponse(img_byte_arr, media_type="image/png")
213
 
214
  except Exception as e:
215
+ logger.error(f"API Error: {e}", exc_info=True)
216
+ raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
217
 
218
+ # --- Gradio Interface Definition ---
219
+ # Note: We reuse the same core function 'render_fullpage_screenshot'
220
+ iface = gr.Interface(
221
+ fn=render_fullpage_screenshot,
222
  inputs=[
223
  gr.Textbox(lines=15, label="HTMLコード入力"),
224
+ gr.Slider(minimum=0, maximum=20, step=1.0, value=8, label="上下高さ拡張率(%)")
225
  ],
226
  outputs=gr.Image(type="pil", label="ページ全体のスクリーンショット"),
227
+ title="Full Page Screenshot (高さ拡張調整可能)", # APIを削除
228
+ description="HTMLをヘッドレスブラウザでレンダリングし、ページ全体を1枚の画像として取得します。上下のみユーザー指定の余裕(%)を追加します。APIエンドポイントは /api/screenshot で利用可能です。",
229
+ allow_flagging="never" # Hugging Face Spacesでのフラグ付けを無効化 (任意)
230
  )
231
 
232
+ # --- Mount Gradio App onto FastAPI ---
233
+ # Mount the Gradio interface at the root path "/"
234
+ app = gr.mount_gradio_app(app, iface, path="/")
235
+
236
+ # --- Run with Uvicorn (for local testing) ---
237
+ # This part is mainly for local development.
238
+ # When deploying on Hugging Face Spaces, the Spaces runtime handles launching the app.
239
+ if __name__ == "__main__":
240
+ import uvicorn
241
+ logger.info("Starting Uvicorn server for local development...")
242
+ # Host '0.0.0.0' makes it accessible on the network
243
+ # Port 7860 is a common default for Gradio, but 8000 is common for FastAPI
244
+ uvicorn.run(app, host="0.0.0.0", port=7860)
245
 
246
  # --- END OF FILE app.py ---