tomo2chin2 commited on
Commit
1281d69
·
verified ·
1 Parent(s): fc646af

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +118 -38
app.py CHANGED
@@ -27,8 +27,15 @@ logger = logging.getLogger(__name__)
27
  class GeminiRequest(BaseModel):
28
  """Geminiへのリクエストデータモデル"""
29
  text: str
30
- extension_percentage: float = 6.0 # ①デフォルト値を6%に変更
31
- temperature: float = 1.0 # ④デフォルト値1.0の温度パラメータを追加
 
 
 
 
 
 
 
32
 
33
  def generate_html_from_text(text, temperature=1.0):
34
  """テキストからHTMLを生成する"""
@@ -158,7 +165,7 @@ def generate_html_from_text(text, temperature=1.0):
158
 
159
  # 生成設定
160
  generation_config = {
161
- "temperature": temperature, # ④パラメータとして受け取った温度を設定
162
  "top_p": 0.95,
163
  "top_k": 64,
164
  "max_output_tokens": 8192,
@@ -194,8 +201,63 @@ def generate_html_from_text(text, temperature=1.0):
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
 
@@ -203,6 +265,7 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
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.
@@ -212,8 +275,8 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
212
 
213
  # Gemini生成コンテンツの場合、拡張率を調整
214
  if is_gemini_content:
215
- # ②最低でも5%の拡張を確保(20%から5%に変更)
216
- extension_percentage = max(extension_percentage, 1.0)
217
  logger.info(f"Gemini生成コンテンツ用に拡張率を調整: {extension_percentage}%")
218
 
219
  # 1) Save HTML code to a temporary file
@@ -235,18 +298,15 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
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")
241
 
242
  try:
243
  logger.info("Initializing WebDriver...")
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}")
@@ -254,7 +314,7 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
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...")
@@ -293,14 +353,19 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
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);")
@@ -314,25 +379,29 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
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) # 最小高さを設定
 
 
 
 
320
 
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
329
  adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
330
  # Ensure adjusted height is not excessively large or small
331
- adjusted_height = max(adjusted_height, scroll_height, 100) # 最小高さを確保
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...")
@@ -378,6 +447,12 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
378
  # 画像サイズの確認とログ
379
  logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
380
 
 
 
 
 
 
 
381
  return img
382
 
383
  except Exception as e:
@@ -399,14 +474,15 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float, is_g
399
  logger.error(f"Error removing temporary file {tmp_path}: {e}")
400
 
401
  # --- Geminiを使った新しい関数 ---
402
- def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 1.0) -> Image.Image:
403
  """テキストをGemini APIでHTMLに変換し、スクリーンショットを生成する統合関数"""
404
  try:
405
  # 1. テキストからHTMLを生成(温度パラメータも渡す)
406
  html_code = generate_html_from_text(text, temperature)
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)) # エラー時は黒画像
@@ -453,11 +529,6 @@ 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 = 6.0 # ①デフォルト値を6%に変更
460
-
461
  # API Endpoint for screenshot generation
462
  @app.post("/api/screenshot",
463
  response_class=StreamingResponse,
@@ -473,7 +544,8 @@ async def api_render_screenshot(request: ScreenshotRequest):
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):
@@ -510,7 +582,8 @@ async def api_text_to_screenshot(request: GeminiRequest):
510
  pil_image = text_to_screenshot(
511
  request.text,
512
  request.extension_percentage,
513
- request.temperature
 
514
  )
515
 
516
  if pil_image.size == (1, 1):
@@ -531,14 +604,14 @@ async def api_text_to_screenshot(request: GeminiRequest):
531
 
532
  # --- Gradio Interface Definition ---
533
  # 入力モードの選択用Radioコンポーネント
534
- def process_input(input_mode, input_text, extension_percentage, temperature):
535
  """入力モードに応じて適切な処理を行う"""
536
  if input_mode == "HTML入力":
537
  # HTMLモードの場合は既存の処理
538
- return render_fullpage_screenshot(input_text, extension_percentage)
539
  else:
540
  # テキスト入力モードの場合はGemini APIを使用
541
- return text_to_screenshot(input_text, extension_percentage, temperature)
542
 
543
  # Gradio UIの定義
544
  with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
@@ -552,7 +625,7 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
552
  value="HTML入力"
553
  )
554
 
555
- # ③共用のテキストボックス(タブ無し)
556
  input_text = gr.Textbox(
557
  lines=15,
558
  label="入力",
@@ -564,11 +637,11 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
564
  minimum=0,
565
  maximum=30,
566
  step=1.0,
567
- value=6, # ①デフォルト値を6%に変更
568
  label="上下高さ拡張率(%)"
569
  )
570
 
571
- # ④温度調整スライダー(テキストモード時のみ表示)
572
  temperature = gr.Slider(
573
  minimum=0.0,
574
  maximum=1.4,
@@ -578,12 +651,19 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
578
  visible=False # 最初は非表示
579
  )
580
 
 
 
 
 
 
 
 
581
  submit_btn = gr.Button("生成")
582
  output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
583
 
584
  # 入力モード変更時のイベント処理(テキストモード時のみ温度スライダーを表示)
585
  def update_temperature_visibility(mode):
586
- # Gradio 4.x用のアップデート方法に修正
587
  return {"visible": mode == "テキスト入力", "__type__": "update"}
588
 
589
  input_mode.change(
@@ -595,7 +675,7 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
595
  # 生成ボタンクリック時のイベント処理
596
  submit_btn.click(
597
  fn=process_input,
598
- inputs=[input_mode, input_text, extension_percentage, temperature],
599
  outputs=output_image
600
  )
601
 
 
27
  class GeminiRequest(BaseModel):
28
  """Geminiへのリクエストデータモデル"""
29
  text: str
30
+ extension_percentage: float = 6.0 # デフォルト値6%
31
+ temperature: float = 1.0 # デフォルト値1.0の温度パラメータ
32
+ trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
33
+
34
+ class ScreenshotRequest(BaseModel):
35
+ """スクリーンショットリクエストモデル"""
36
+ html_code: str
37
+ extension_percentage: float = 6.0 # デフォルト値6%
38
+ trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
39
 
40
  def generate_html_from_text(text, temperature=1.0):
41
  """テキストからHTMLを生成する"""
 
165
 
166
  # 生成設定
167
  generation_config = {
168
+ "temperature": temperature, # パラメータとして受け取った温度を設定
169
  "top_p": 0.95,
170
  "top_k": 64,
171
  "max_output_tokens": 8192,
 
201
  logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
202
  raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")
203
 
204
+ # 画像から余分な空白領域をトリミングする関数
205
+ def trim_image_whitespace(image, threshold=250, padding=10):
206
+ """
207
+ 画像から余分な白い空白をトリミングする
208
+
209
+ Args:
210
+ image: PIL.Image - 入力画像
211
+ threshold: int - どの明るさ以上を空白と判断するか (0-255)
212
+ padding: int - トリミング後に残す余白のピクセル数
213
+
214
+ Returns:
215
+ トリミングされたPIL.Image
216
+ """
217
+ # グレースケールに変換
218
+ gray = image.convert('L')
219
+
220
+ # ピクセルデータを配列として取得
221
+ data = gray.getdata()
222
+ width, height = gray.size
223
+
224
+ # 有効範囲を見つける
225
+ min_x, min_y = width, height
226
+ max_x = max_y = 0
227
+
228
+ # ピクセルデータを2次元配列に変換して処理
229
+ pixels = list(data)
230
+ pixels = [pixels[i * width:(i + 1) * width] for i in range(height)]
231
+
232
+ # 各行をスキャンして非空白ピクセルを見つける
233
+ for y in range(height):
234
+ for x in range(width):
235
+ if pixels[y][x] < threshold: # 非空白ピクセル
236
+ min_x = min(min_x, x)
237
+ min_y = min(min_y, y)
238
+ max_x = max(max_x, x)
239
+ max_y = max(max_y, y)
240
+
241
+ # 境界外のトリミングの場合はエラー
242
+ if min_x > max_x or min_y > max_y:
243
+ logger.warning("トリミング領域が見つかりません。元の画像を返します。")
244
+ return image
245
+
246
+ # パディングを追加
247
+ min_x = max(0, min_x - padding)
248
+ min_y = max(0, min_y - padding)
249
+ max_x = min(width - 1, max_x + padding)
250
+ max_y = min(height - 1, max_y + padding)
251
+
252
+ # 画像をトリミング
253
+ trimmed = image.crop((min_x, min_y, max_x + 1, max_y + 1))
254
+
255
+ logger.info(f"画像をトリミングしました: 元サイズ {width}x{height} → トリミング後 {trimmed.width}x{trimmed.height}")
256
+ return trimmed
257
+
258
  # --- Core Screenshot Logic ---
259
+ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0,
260
+ is_gemini_content: bool = False, trim_whitespace: bool = True) -> Image.Image:
261
  """
262
  Renders HTML code to a full-page screenshot using Selenium.
263
 
 
265
  html_code: The HTML source code string.
266
  extension_percentage: Percentage of extra space to add vertically (e.g., 4 means 4% total).
267
  is_gemini_content: True if the HTML was generated by Gemini API (requires special handling).
268
+ trim_whitespace: Whether to trim excess whitespace from the image.
269
 
270
  Returns:
271
  A PIL Image object of the screenshot. Returns a 1x1 black image on error.
 
275
 
276
  # Gemini生成コンテンツの場合、拡張率を調整
277
  if is_gemini_content:
278
+ # 最低でも5%の拡張を確保
279
+ extension_percentage = max(extension_percentage, 5.0)
280
  logger.info(f"Gemini生成コンテンツ用に拡張率を調整: {extension_percentage}%")
281
 
282
  # 1) Save HTML code to a temporary file
 
298
  # Font Awesomeが読み込まれない場合があるため、読み込み待機時間を長く設定
299
  options.add_argument("--disable-features=NetworkService")
300
  options.add_argument("--dns-prefetch-disable")
 
 
 
301
 
302
  try:
303
  logger.info("Initializing WebDriver...")
304
  driver = webdriver.Chrome(options=options)
305
  logger.info("WebDriver initialized.")
306
 
307
+ # 3) 初期ウィンドウサイズを調整(小さなコンテンツのために減らす)
308
+ initial_width = 1200 # 1800 -> 1200 に変更
309
+ initial_height = 1000 if is_gemini_content else 800 # 2000/1200 -> 1000/800 に変更
310
  driver.set_window_size(initial_width, initial_height)
311
  file_url = "file://" + tmp_path
312
  logger.info(f"Navigating to {file_url}")
 
314
 
315
  # 4) Wait for page load with extended timeout
316
  logger.info("Waiting for body element...")
317
+ WebDriverWait(driver, 15).until(
318
  EC.presence_of_element_located((By.TAG_NAME, "body"))
319
  )
320
  logger.info("Body element found. Waiting for potential resource loading...")
 
353
  document.body ? document.body.scrollHeight : 0,
354
  document.body ? document.body.offsetHeight : 0,
355
  document.body ? document.body.clientHeight : 0
356
+ ),
357
+ visibleHeight: Math.max(
358
+ document.documentElement.clientHeight,
359
+ document.body ? document.body.clientHeight : 0
360
  )
361
  };
362
  """
363
  dimensions = driver.execute_script(dimensions_script)
364
  scroll_width = dimensions['width']
365
  scroll_height = dimensions['height']
366
+ visible_height = dimensions.get('visibleHeight', 0)
367
 
368
+ logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}, visibleHeight={visible_height}")
369
 
370
  # スクロールして確認する追加の検証
371
  driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
 
379
 
380
  logger.info(f"After scroll check, height={scroll_height}")
381
 
382
+ # 最小値の調整 - 小さなコンテンツの場合でも適切に表示できるよう調整
383
+ scroll_width = max(scroll_width, 100) # 最小幅を設定
384
+ scroll_height = max(scroll_height, 100) # 最小高さを設定
385
+
386
+ # 最大値も設定(過度に大きな画像を防ぐため)
387
+ scroll_width = min(scroll_width, 2000) # 最大幅
388
+ scroll_height = min(scroll_height, 4000) # 最大高さ
389
 
390
  except Exception as e:
391
  logger.error(f"Error getting page dimensions: {e}")
392
  # フォールバックとしてデフォルト値を設定
393
+ scroll_width = 1200
394
+ scroll_height = 800
395
  logger.warning(f"Falling back to dimensions: width={scroll_width}, height={scroll_height}")
396
 
397
  # 7) Calculate adjusted height with user-specified margin
398
  adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
399
  # Ensure adjusted height is not excessively large or small
400
+ adjusted_height = max(adjusted_height, scroll_height, 100) # 最小高さを確保
401
  logger.info(f"Adjusted height calculated: {adjusted_height} (extension: {extension_percentage}%)")
402
 
403
  # 8) Set window size to full page dimensions (width) and adjusted height
404
+ adjusted_width = scroll_width # 初期幅との比較を削除
405
  logger.info(f"Resizing window to: width={adjusted_width}, height={adjusted_height}")
406
  driver.set_window_size(adjusted_width, adjusted_height)
407
  logger.info("Waiting for layout stabilization after resize...")
 
447
  # 画像サイズの確認とログ
448
  logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
449
 
450
+ # 余白トリミングが有効な場合
451
+ if trim_whitespace:
452
+ # 余分な空白をトリミング
453
+ img = trim_image_whitespace(img, threshold=248, padding=20)
454
+ logger.info(f"Trimmed dimensions: {img.width}x{img.height}")
455
+
456
  return img
457
 
458
  except Exception as e:
 
474
  logger.error(f"Error removing temporary file {tmp_path}: {e}")
475
 
476
  # --- Geminiを使った新しい関数 ---
477
+ def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 1.0, trim_whitespace: bool = True) -> Image.Image:
478
  """テキストをGemini APIでHTMLに変換し、スクリーンショットを生成する統合関数"""
479
  try:
480
  # 1. テキストからHTMLを生成(温度パラメータも渡す)
481
  html_code = generate_html_from_text(text, temperature)
482
 
483
  # 2. HTMLからスクリーンショットを生成(Gemini生成コンテンツとしてフラグをオン)
484
+ return render_fullpage_screenshot(html_code, extension_percentage,
485
+ is_gemini_content=True, trim_whitespace=trim_whitespace)
486
  except Exception as e:
487
  logger.error(f"テキストからスクリーンショット生成中にエラーが発生: {e}", exc_info=True)
488
  return Image.new('RGB', (1, 1), color=(0, 0, 0)) # エラー時は黒画像
 
529
  logger.info(f"Mounting cdn directory: {cdn_dir}")
530
  app.mount("/cdn", StaticFiles(directory=cdn_dir), name="cdn")
531
 
 
 
 
 
 
532
  # API Endpoint for screenshot generation
533
  @app.post("/api/screenshot",
534
  response_class=StreamingResponse,
 
544
  # Run the blocking Selenium code in a separate thread (FastAPI handles this)
545
  pil_image = render_fullpage_screenshot(
546
  request.html_code,
547
+ request.extension_percentage,
548
+ trim_whitespace=request.trim_whitespace
549
  )
550
 
551
  if pil_image.size == (1, 1):
 
582
  pil_image = text_to_screenshot(
583
  request.text,
584
  request.extension_percentage,
585
+ request.temperature,
586
+ request.trim_whitespace
587
  )
588
 
589
  if pil_image.size == (1, 1):
 
604
 
605
  # --- Gradio Interface Definition ---
606
  # 入力モードの選択用Radioコンポーネント
607
+ def process_input(input_mode, input_text, extension_percentage, temperature, trim_whitespace):
608
  """入力モードに応じて適切な処理を行う"""
609
  if input_mode == "HTML入力":
610
  # HTMLモードの場合は既存の処理
611
+ return render_fullpage_screenshot(input_text, extension_percentage, trim_whitespace=trim_whitespace)
612
  else:
613
  # テキスト入力モードの場合はGemini APIを使用
614
+ return text_to_screenshot(input_text, extension_percentage, temperature, trim_whitespace)
615
 
616
  # Gradio UIの定義
617
  with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
 
625
  value="HTML入力"
626
  )
627
 
628
+ # 共用のテキストボックス
629
  input_text = gr.Textbox(
630
  lines=15,
631
  label="入力",
 
637
  minimum=0,
638
  maximum=30,
639
  step=1.0,
640
+ value=6, # デフォルト値6%
641
  label="上下高さ拡張率(%)"
642
  )
643
 
644
+ # 温度調整スライダー(テキストモード時のみ表示)
645
  temperature = gr.Slider(
646
  minimum=0.0,
647
  maximum=1.4,
 
651
  visible=False # 最初は非表示
652
  )
653
 
654
+ # 余白トリミングオプション
655
+ trim_whitespace = gr.Checkbox(
656
+ label="余白を自動トリミング",
657
+ value=True,
658
+ info="生成される画像から余分な空白領域を自動的に削除します"
659
+ )
660
+
661
  submit_btn = gr.Button("生成")
662
  output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
663
 
664
  # 入力モード変更時のイベント処理(テキストモード時のみ温度スライダーを表示)
665
  def update_temperature_visibility(mode):
666
+ # Gradio 4.x用のアップデート方法
667
  return {"visible": mode == "テキスト入力", "__type__": "update"}
668
 
669
  input_mode.change(
 
675
  # 生成ボタンクリック時のイベント処理
676
  submit_btn.click(
677
  fn=process_input,
678
+ inputs=[input_mode, input_text, extension_percentage, temperature, trim_whitespace],
679
  outputs=output_image
680
  )
681