tomo2chin2 commited on
Commit
4f99fb7
·
verified ·
1 Parent(s): e2eb5a0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +405 -179
app.py CHANGED
@@ -15,7 +15,11 @@ import tempfile
15
  import time
16
  import os
17
  import logging
18
- from huggingface_hub import hf_hub_download # 追加: HuggingFace Hubからファイルを直接ダウンロード
 
 
 
 
19
 
20
  # 正しいGemini関連のインポート
21
  import google.generativeai as genai
@@ -24,6 +28,88 @@ import google.generativeai as genai
24
  logging.basicConfig(level=logging.INFO)
25
  logger = logging.getLogger(__name__)
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  # --- Gemini統合 ---
28
  class GeminiRequest(BaseModel):
29
  """Geminiへのリクエストデータモデル"""
@@ -40,9 +126,16 @@ class ScreenshotRequest(BaseModel):
40
  trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
41
  style: str = "standard" # デフォルトはstandard
42
 
43
- # HTMLのFont Awesomeレイアウトを改善する関数
44
  def enhance_font_awesome_layout(html_code):
45
- """Font Awesomeレイアウトを改善するCSSを追加"""
 
 
 
 
 
 
 
46
  # CSSを追加
47
  fa_fix_css = """
48
  <style>
@@ -96,19 +189,19 @@ def enhance_font_awesome_layout(html_code):
96
 
97
  # headタグがある場合はその中に追加
98
  if '<head>' in html_code:
99
- return html_code.replace('</head>', f'{fa_fix_css}</head>')
100
  # HTMLタグがある場合はその後に追加
101
  elif '<html' in html_code:
102
  head_end = html_code.find('</head>')
103
  if head_end > 0:
104
- return html_code[:head_end] + fa_fix_css + html_code[head_end:]
105
  else:
106
  body_start = html_code.find('<body')
107
  if body_start > 0:
108
- return html_code[:body_start] + f'<head>{fa_fix_css}</head>' + html_code[body_start:]
109
 
110
  # どちらもない場合は先頭に追加
111
- return f'<html><head>{fa_fix_css}</head>' + html_code + '</html>'
112
 
113
  def load_system_instruction(style="standard"):
114
  """
@@ -267,11 +360,11 @@ def generate_html_from_text(text, temperature=0.3, style="standard"):
267
  logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
268
  raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")
269
 
270
- # 画像から余分な空白領域をトリミングする関数
271
  def trim_image_whitespace(image, threshold=250, padding=10):
272
  """
273
- 画像から余分な白い空白をトリミングする
274
-
275
  Args:
276
  image: PIL.Image - 入力画像
277
  threshold: int - どの明るさ以上を空白と判断するか (0-255)
@@ -280,64 +373,72 @@ def trim_image_whitespace(image, threshold=250, padding=10):
280
  Returns:
281
  トリミングされたPIL.Image
282
  """
283
- # グレースケールに変換
284
- gray = image.convert('L')
285
-
286
- # ピクセルデータを配列として取得
287
- data = gray.getdata()
288
- width, height = gray.size
289
-
290
- # 有効範囲を見つける
291
- min_x, min_y = width, height
292
- max_x = max_y = 0
293
-
294
- # ピクセルデータを2次元配列に変換して処理
295
- pixels = list(data)
296
- pixels = [pixels[i * width:(i + 1) * width] for i in range(height)]
297
-
298
- # 各行をスキャンして非空白ピクセルを見つける
299
- for y in range(height):
300
- for x in range(width):
301
- if pixels[y][x] < threshold: # 非空白ピクセル
302
- min_x = min(min_x, x)
303
- min_y = min(min_y, y)
304
- max_x = max(max_x, x)
305
- max_y = max(max_y, y)
306
-
307
- # 境界外のトリミングの場合はエラー
308
- if min_x > max_x or min_y > max_y:
 
 
 
 
 
 
 
 
 
309
  logger.warning("トリミング領域が見つかりません。元の画像を返します。")
310
  return image
 
 
 
 
311
 
312
- # パディングを追加
313
- min_x = max(0, min_x - padding)
314
- min_y = max(0, min_y - padding)
315
- max_x = min(width - 1, max_x + padding)
316
- max_y = min(height - 1, max_y + padding)
317
-
318
- # 画像をトリミング
319
- trimmed = image.crop((min_x, min_y, max_x + 1, max_y + 1))
320
-
321
- logger.info(f"画像をトリミングしました: 元サイズ {width}x{height} → トリミング後 {trimmed.width}x{trimmed.height}")
322
- return trimmed
323
-
324
- # 非同期スクリプトを使わず、同期的なスクリプトのみ使用する改善版
325
-
326
  def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0,
327
- trim_whitespace: bool = True) -> Image.Image:
328
  """
329
  Renders HTML code to a full-page screenshot using Selenium.
 
330
 
331
  Args:
332
  html_code: The HTML source code string.
333
  extension_percentage: Percentage of extra space to add vertically.
334
  trim_whitespace: Whether to trim excess whitespace from the image.
 
335
 
336
  Returns:
337
  A PIL Image object of the screenshot.
338
  """
339
  tmp_path = None
340
- driver = None
 
 
 
 
 
 
341
 
342
  # 1) Save HTML code to a temporary file
343
  try:
@@ -347,34 +448,12 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
347
  logger.info(f"HTML saved to temporary file: {tmp_path}")
348
  except Exception as e:
349
  logger.error(f"Error writing temporary HTML file: {e}")
 
 
350
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
351
 
352
- # 2) Headless Chrome(Chromium) options
353
- options = Options()
354
- options.add_argument("--headless")
355
- options.add_argument("--no-sandbox")
356
- options.add_argument("--disable-dev-shm-usage")
357
- options.add_argument("--force-device-scale-factor=1")
358
- options.add_argument("--disable-features=NetworkService")
359
- options.add_argument("--dns-prefetch-disable")
360
- # 環境変数からWebDriverパスを取得(任意)
361
- webdriver_path = os.environ.get("CHROMEDRIVER_PATH")
362
- if webdriver_path and os.path.exists(webdriver_path):
363
- logger.info(f"Using CHROMEDRIVER_PATH: {webdriver_path}")
364
- service = webdriver.ChromeService(executable_path=webdriver_path)
365
- else:
366
- logger.info("CHROMEDRIVER_PATH not set or invalid, using default PATH lookup.")
367
- service = None # Use default behavior
368
-
369
  try:
370
- logger.info("Initializing WebDriver...")
371
- if service:
372
- driver = webdriver.Chrome(service=service, options=options)
373
- else:
374
- driver = webdriver.Chrome(options=options)
375
- logger.info("WebDriver initialized.")
376
-
377
- # 3) 初期ウィンドウサイズを設定
378
  initial_width = 1200
379
  initial_height = 1000
380
  driver.set_window_size(initial_width, initial_height)
@@ -382,57 +461,68 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
382
  logger.info(f"Navigating to {file_url}")
383
  driver.get(file_url)
384
 
385
- # 4) ページ読み込み待機
386
  logger.info("Waiting for body element...")
387
- WebDriverWait(driver, 15).until(
388
  EC.presence_of_element_located((By.TAG_NAME, "body"))
389
  )
390
  logger.info("Body element found. Waiting for resource loading...")
391
 
392
- # 5) 基本的なリソース読み込み待機 - タイムアウト回避
393
- time.sleep(3)
394
-
395
- # Font Awesome読み込み確認 - 非同期を使わない
396
- logger.info("Checking for Font Awesome resources...")
397
- fa_count = driver.execute_script("""
398
- var icons = document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]');
399
- return icons.length;
400
- """)
401
- logger.info(f"Found {fa_count} Font Awesome elements")
402
-
403
- # リソース読み込み状態を確認
404
- doc_ready = driver.execute_script("return document.readyState;")
405
- logger.info(f"Document ready state: {doc_ready}")
406
-
407
- # Font Awesomeが多い場合は追加待機
408
- if fa_count > 50:
409
- logger.info("Many Font Awesome icons detected, waiting additional time")
410
- time.sleep(2)
411
-
412
- # 6) コンテンツレンダリングのためのスクロール処理 - 同期的に実行
 
 
 
 
 
 
 
 
 
 
 
413
  logger.info("Performing content rendering scroll...")
414
  total_height = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);")
415
  viewport_height = driver.execute_script("return window.innerHeight;")
416
- scrolls_needed = max(1, total_height // viewport_height)
417
-
418
- for i in range(scrolls_needed + 1):
419
- scroll_pos = i * (viewport_height - 200) # オーバーラップさせる
 
420
  driver.execute_script(f"window.scrollTo(0, {scroll_pos});")
421
- time.sleep(0.2) # 短い待機
422
-
423
  # トップに戻る
424
  driver.execute_script("window.scrollTo(0, 0);")
425
- time.sleep(0.5)
426
  logger.info("Scroll rendering completed")
427
 
428
- # 7) スクロールバーを非表示に
429
  driver.execute_script("""
430
  document.documentElement.style.overflow = 'hidden';
431
  document.body.style.overflow = 'hidden';
432
  """)
433
- logger.info("Scrollbars hidden")
434
-
435
- # 8) ページの寸法を取得
436
  dimensions = driver.execute_script("""
437
  return {
438
  width: Math.max(
@@ -457,57 +547,26 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
457
  scroll_height = dimensions['height']
458
  logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}")
459
 
460
- # 再検証 - 短いスクロールで再確認
461
- driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
462
- time.sleep(0.5)
463
- driver.execute_script("window.scrollTo(0, 0);")
464
- time.sleep(0.5)
465
-
466
- dimensions_after = driver.execute_script("return {height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)};")
467
- scroll_height = max(scroll_height, dimensions_after['height'])
468
- logger.info(f"After scroll check, height={scroll_height}")
469
-
470
  # 最小/最大値の設定
471
  scroll_width = max(scroll_width, 100)
472
  scroll_height = max(scroll_height, 100)
473
  scroll_width = min(scroll_width, 2000)
474
  scroll_height = min(scroll_height, 4000)
 
 
 
475
 
476
- # 9) レイアウト安定化のための単純な待機 - タイムアウト回避
477
- logger.info("Waiting for layout stabilization...")
478
- time.sleep(2)
479
-
480
- # 10) 高さに余白を追加
481
  adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
482
  adjusted_height = max(adjusted_height, scroll_height, 100)
483
- logger.info(f"Adjusted height calculated: {adjusted_height} (extension: {extension_percentage}%)")
484
-
485
- # 11) ウィンドウサイズを調整
486
  adjusted_width = scroll_width
487
  logger.info(f"Resizing window to: width={adjusted_width}, height={adjusted_height}")
488
  driver.set_window_size(adjusted_width, adjusted_height)
489
- time.sleep(1)
490
-
491
- # リソース状態を確認 - 同期的スクリプト
492
- resource_state = driver.execute_script("""
493
- return {
494
- readyState: document.readyState,
495
- resourcesComplete: !document.querySelector('img:not([complete])') &&
496
- !document.querySelector('link[rel="stylesheet"]:not([loaded])')
497
- };
498
- """)
499
- logger.info(f"Resource state: {resource_state}")
500
-
501
- if resource_state['readyState'] != 'complete':
502
- logger.info("Document still loading, waiting additional time...")
503
- time.sleep(1)
504
 
505
- # トップにスクロール
506
- driver.execute_script("window.scrollTo(0, 0);")
507
- time.sleep(0.5)
508
- logger.info("Scrolled to top.")
509
-
510
- # 12) スクリーンショット取得
511
  logger.info("Taking screenshot...")
512
  png = driver.get_screenshot_as_png()
513
  logger.info("Screenshot taken successfully.")
@@ -516,7 +575,7 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
516
  img = Image.open(BytesIO(png))
517
  logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
518
 
519
- # 余白トリミング
520
  if trim_whitespace:
521
  img = trim_image_whitespace(img, threshold=248, padding=20)
522
  logger.info(f"Trimmed dimensions: {img.width}x{img.height}")
@@ -525,37 +584,200 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
525
 
526
  except Exception as e:
527
  logger.error(f"Error during screenshot generation: {e}", exc_info=True)
528
- # Return a small black image on error
529
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
530
  finally:
531
  logger.info("Cleaning up...")
532
- if driver:
533
- try:
534
- driver.quit()
535
- logger.info("WebDriver quit successfully.")
536
- except Exception as e:
537
- logger.error(f"Error quitting WebDriver: {e}", exc_info=True)
538
  if tmp_path and os.path.exists(tmp_path):
539
  try:
540
  os.remove(tmp_path)
541
  logger.info(f"Temporary file {tmp_path} removed.")
542
  except Exception as e:
543
- logger.error(f"Error removing temporary file {tmp_path}: {e}", exc_info=True)
544
-
545
- # --- Geminiを使った新しい関数 ---
546
- def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 0.3,
547
- trim_whitespace: bool = True, style: str = "standard") -> Image.Image:
548
- """テキストをGemini APIでHTMLに変換し、スクリーンショットを生成する統合関数"""
 
 
 
549
  try:
550
- # 1. テキストからHTMLを生成(温度パラメータとスタイルも渡す)
551
- html_code = generate_html_from_text(text, temperature, style)
552
-
553
- # 2. HTMLからスクリーンショットを生成
554
- return render_fullpage_screenshot(html_code, extension_percentage, trim_whitespace)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  except Exception as e:
556
- logger.error(f"テキストからスクリーンショット生成中にエラーが発生: {e}", exc_info=True)
557
  return Image.new('RGB', (1, 1), color=(0, 0, 0)) # エラー時は黒画像
558
 
 
 
 
 
 
 
 
559
  # --- FastAPI Setup ---
560
  app = FastAPI()
561
 
@@ -611,7 +833,7 @@ async def api_render_screenshot(request: ScreenshotRequest):
611
  """
612
  try:
613
  logger.info(f"API request received. Extension: {request.extension_percentage}%")
614
- # Run the blocking Selenium code in a separate thread (FastAPI handles this)
615
  pil_image = render_fullpage_screenshot(
616
  request.html_code,
617
  request.extension_percentage,
@@ -635,7 +857,7 @@ async def api_render_screenshot(request: ScreenshotRequest):
635
  logger.error(f"API Error: {e}", exc_info=True)
636
  raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
637
 
638
- # --- 新しいGemini API連携エンドポイント ---
639
  @app.post("/api/text-to-screenshot",
640
  response_class=StreamingResponse,
641
  tags=["Screenshot", "Gemini"],
@@ -650,8 +872,8 @@ async def api_text_to_screenshot(request: GeminiRequest):
650
  f"拡張率: {request.extension_percentage}%, 温度: {request.temperature}, "
651
  f"スタイル: {request.style}")
652
 
653
- # テキストからHTMLを生成してスクリーンショットを作成(温度パラメータとスタイルも渡す)
654
- pil_image = text_to_screenshot(
655
  request.text,
656
  request.extension_percentage,
657
  request.temperature,
@@ -661,8 +883,6 @@ async def api_text_to_screenshot(request: GeminiRequest):
661
 
662
  if pil_image.size == (1, 1):
663
  logger.error("スクリーンショット生成に失敗しました。1x1エラー画像を返します。")
664
- # raise HTTPException(status_code=500, detail="スクリーンショット生成に失敗しました")
665
-
666
 
667
  # PIL画像をPNGバイトに変換
668
  img_byte_arr = BytesIO()
@@ -684,13 +904,14 @@ def process_input(input_mode, input_text, extension_percentage, temperature, tri
684
  # HTMLモ��ドの場合は既存の処理(スタイルは使わない)
685
  return render_fullpage_screenshot(input_text, extension_percentage, trim_whitespace)
686
  else:
687
- # テキスト入力モードの場合はGemini APIを使用
688
- return text_to_screenshot(input_text, extension_percentage, temperature, trim_whitespace, style)
689
 
690
  # Gradio UIの定義
691
  with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
692
  gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
693
  gr.Markdown("HTMLコードをレンダリングするか、テキストをGemini APIでインフォグラフィックに変換して画像として取得します。")
 
694
 
695
  with gr.Row():
696
  input_mode = gr.Radio(
@@ -778,6 +999,7 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
778
  ## 設定情報
779
  - 使用モデル: {gemini_model} (環境変数 GEMINI_MODEL で変更可能)
780
  - 対応スタイル: standard, cute, resort, cool, dental
 
781
  """)
782
 
783
  # --- Mount Gradio App onto FastAPI ---
@@ -787,4 +1009,8 @@ app = gr.mount_gradio_app(app, iface, path="/")
787
  if __name__ == "__main__":
788
  import uvicorn
789
  logger.info("Starting Uvicorn server for local development...")
790
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
15
  import time
16
  import os
17
  import logging
18
+ import numpy as np # 追加: 画像処理の最適化用
19
+ import threading # 追加: 並列処理のため
20
+ import queue # 追加: WebDriverプール用
21
+ from concurrent.futures import ThreadPoolExecutor # 追加: 並列処理用
22
+ from huggingface_hub import hf_hub_download
23
 
24
  # 正しいGemini関連のインポート
25
  import google.generativeai as genai
 
28
  logging.basicConfig(level=logging.INFO)
29
  logger = logging.getLogger(__name__)
30
 
31
+ # --- WebDriverプールの実装 ---
32
+ class WebDriverPool:
33
+ """WebDriverインスタンスを再利用するためのプール"""
34
+ def __init__(self, max_drivers=3):
35
+ self.driver_queue = queue.Queue()
36
+ self.max_drivers = max_drivers
37
+ self.lock = threading.Lock()
38
+ self.count = 0
39
+ logger.info(f"WebDriverプールを初期化: 最大 {max_drivers} ドライバー")
40
+
41
+ def get_driver(self):
42
+ """プールからWebDriverを取得、なければ新規作成"""
43
+ if not self.driver_queue.empty():
44
+ logger.info("既存のWebDriverをプールから取得")
45
+ return self.driver_queue.get()
46
+
47
+ with self.lock:
48
+ if self.count < self.max_drivers:
49
+ self.count += 1
50
+ logger.info(f"新しいWebDriverを作成 (合計: {self.count}/{self.max_drivers})")
51
+ options = Options()
52
+ options.add_argument("--headless")
53
+ options.add_argument("--no-sandbox")
54
+ options.add_argument("--disable-dev-shm-usage")
55
+ options.add_argument("--force-device-scale-factor=1")
56
+ options.add_argument("--disable-features=NetworkService")
57
+ options.add_argument("--dns-prefetch-disable")
58
+
59
+ # 環境変数からWebDriverパスを取得(任意)
60
+ webdriver_path = os.environ.get("CHROMEDRIVER_PATH")
61
+ if webdriver_path and os.path.exists(webdriver_path):
62
+ logger.info(f"CHROMEDRIVER_PATH使用: {webdriver_path}")
63
+ service = webdriver.ChromeService(executable_path=webdriver_path)
64
+ return webdriver.Chrome(service=service, options=options)
65
+ else:
66
+ logger.info("デフォルトのChromeDriverを使用")
67
+ return webdriver.Chrome(options=options)
68
+
69
+ # 最大数に達した場合は待機
70
+ logger.info("WebDriverプールがいっぱいです。利用可能なドライバーを待機中...")
71
+ return self.driver_queue.get()
72
+
73
+ def release_driver(self, driver):
74
+ """ドライバーをプールに戻す"""
75
+ if driver:
76
+ try:
77
+ # ブラウザをリセット
78
+ driver.get("about:blank")
79
+ driver.execute_script("""
80
+ document.documentElement.style.overflow = '';
81
+ document.body.style.overflow = '';
82
+ """)
83
+ self.driver_queue.put(driver)
84
+ logger.info("WebDriverをプールに戻しました")
85
+ except Exception as e:
86
+ logger.error(f"ドライバーをプールに戻す際にエラー: {e}")
87
+ driver.quit()
88
+ with self.lock:
89
+ self.count -= 1
90
+
91
+ def close_all(self):
92
+ """全てのドライバーを終了"""
93
+ logger.info("WebDriverプールを終了します")
94
+ closed = 0
95
+ while not self.driver_queue.empty():
96
+ try:
97
+ driver = self.driver_queue.get(block=False)
98
+ driver.quit()
99
+ closed += 1
100
+ except queue.Empty:
101
+ break
102
+ except Exception as e:
103
+ logger.error(f"ドライバー終了中にエラー: {e}")
104
+
105
+ logger.info(f"{closed}個のWebDriverを終了しました")
106
+ with self.lock:
107
+ self.count = 0
108
+
109
+ # グローバルなWebDriverプールを作成
110
+ # サーバー環境のリソースに合わせて調整
111
+ driver_pool = WebDriverPool(max_drivers=int(os.environ.get("MAX_WEBDRIVERS", "3")))
112
+
113
  # --- Gemini統合 ---
114
  class GeminiRequest(BaseModel):
115
  """Geminiへのリクエストデータモデル"""
 
126
  trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
127
  style: str = "standard" # デフォルトはstandard
128
 
129
+ # HTMLのFont Awesomeレイアウトを改善する関数 - プリロード機能を追加
130
  def enhance_font_awesome_layout(html_code):
131
+ """Font Awesomeレイアウトを改善し、プリロードタグを追加"""
132
+ # Font Awesomeリソースのプリロード - パフォーマンス向上
133
+ fa_preload = """
134
+ <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
135
+ <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/webfonts/fa-regular-400.woff2" as="font" type="font/woff2" crossorigin>
136
+ <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/webfonts/fa-brands-400.woff2" as="font" type="font/woff2" crossorigin>
137
+ """
138
+
139
  # CSSを追加
140
  fa_fix_css = """
141
  <style>
 
189
 
190
  # headタグがある場合はその中に追加
191
  if '<head>' in html_code:
192
+ return html_code.replace('</head>', f'{fa_preload}{fa_fix_css}</head>')
193
  # HTMLタグがある場合はその後に追加
194
  elif '<html' in html_code:
195
  head_end = html_code.find('</head>')
196
  if head_end > 0:
197
+ return html_code[:head_end] + fa_preload + fa_fix_css + html_code[head_end:]
198
  else:
199
  body_start = html_code.find('<body')
200
  if body_start > 0:
201
+ return html_code[:body_start] + f'<head>{fa_preload}{fa_fix_css}</head>' + html_code[body_start:]
202
 
203
  # どちらもない場合は先頭に追加
204
+ return f'<html><head>{fa_preload}{fa_fix_css}</head>' + html_code + '</html>'
205
 
206
  def load_system_instruction(style="standard"):
207
  """
 
360
  logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
361
  raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")
362
 
363
+ # 画像から余分な空白領域をトリミングする関数 - NumPyを使って最適化
364
  def trim_image_whitespace(image, threshold=250, padding=10):
365
  """
366
+ NumPyを使用して最適化された画像トリミング関数
367
+
368
  Args:
369
  image: PIL.Image - 入力画像
370
  threshold: int - どの明るさ以上を空白と判断するか (0-255)
 
373
  Returns:
374
  トリミングされたPIL.Image
375
  """
376
+ try:
377
+ # グレースケールに変換
378
+ gray = image.convert('L')
379
+
380
+ # NumPy配列として取得(高速処理のため)
381
+ np_image = np.array(gray)
382
+
383
+ # マスク作成(非白ピクセル)
384
+ mask = np_image < threshold
385
+
386
+ # マスクから行と列のインデックスを取得
387
+ rows = np.any(mask, axis=1)
388
+ cols = np.any(mask, axis=0)
389
+
390
+ # 非空のインデックス範囲を取得
391
+ if np.any(rows) and np.any(cols):
392
+ row_indices = np.where(rows)[0]
393
+ col_indices = np.where(cols)[0]
394
+
395
+ # 範囲取得
396
+ min_y, max_y = row_indices[0], row_indices[-1]
397
+ min_x, max_x = col_indices[0], col_indices[-1]
398
+
399
+ # パディング追加
400
+ min_x = max(0, min_x - padding)
401
+ min_y = max(0, min_y - padding)
402
+ max_x = min(image.width - 1, max_x + padding)
403
+ max_y = min(image.height - 1, max_y + padding)
404
+
405
+ # 画像をトリミング
406
+ trimmed = image.crop((min_x, min_y, max_x + 1, max_y + 1))
407
+
408
+ logger.info(f"画像をトリミングしました: 元サイズ {image.width}x{image.height} → トリミング後 {trimmed.width}x{trimmed.height}")
409
+ return trimmed
410
+
411
  logger.warning("トリミング領域が見つかりません。元の画像を返します。")
412
  return image
413
+
414
+ except Exception as e:
415
+ logger.error(f"画像トリミング中にエラー: {e}", exc_info=True)
416
+ return image # エラー時は元の画像を返す
417
 
418
+ # 最適化されたスクリーンショット生成関数 - 外部から初期化済みドライバーを受け取れるように
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0,
420
+ trim_whitespace: bool = True, driver=None) -> Image.Image:
421
  """
422
  Renders HTML code to a full-page screenshot using Selenium.
423
+ Optimized to accept an external driver or get one from the pool.
424
 
425
  Args:
426
  html_code: The HTML source code string.
427
  extension_percentage: Percentage of extra space to add vertically.
428
  trim_whitespace: Whether to trim excess whitespace from the image.
429
+ driver: An optional pre-initialized WebDriver instance.
430
 
431
  Returns:
432
  A PIL Image object of the screenshot.
433
  """
434
  tmp_path = None
435
+ driver_from_pool = False
436
+
437
+ # ドライバーがない場合はプールから取得
438
+ if driver is None:
439
+ driver = driver_pool.get_driver()
440
+ driver_from_pool = True
441
+ logger.info("WebDriverプールからドライバーを取得しました")
442
 
443
  # 1) Save HTML code to a temporary file
444
  try:
 
448
  logger.info(f"HTML saved to temporary file: {tmp_path}")
449
  except Exception as e:
450
  logger.error(f"Error writing temporary HTML file: {e}")
451
+ if driver_from_pool:
452
+ driver_pool.release_driver(driver)
453
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
454
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  try:
456
+ # ウィンドウサイズ初期設定
 
 
 
 
 
 
 
457
  initial_width = 1200
458
  initial_height = 1000
459
  driver.set_window_size(initial_width, initial_height)
 
461
  logger.info(f"Navigating to {file_url}")
462
  driver.get(file_url)
463
 
464
+ # ページ読み込み待機 - 動的な待機時間を実装
465
  logger.info("Waiting for body element...")
466
+ WebDriverWait(driver, 10).until(
467
  EC.presence_of_element_located((By.TAG_NAME, "body"))
468
  )
469
  logger.info("Body element found. Waiting for resource loading...")
470
 
471
+ # リソース読み込みの動的待機 - 最適化
472
+ max_wait = 3 # 最大待機時間(秒)
473
+ wait_increment = 0.2 # 確認間隔
474
+ wait_time = 0
475
+
476
+ while wait_time < max_wait:
477
+ resource_state = driver.execute_script("""
478
+ return {
479
+ complete: document.readyState === 'complete',
480
+ imgCount: document.images.length,
481
+ imgLoaded: Array.from(document.images).filter(img => img.complete).length,
482
+ faElements: document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]').length
483
+ };
484
+ """)
485
+
486
+ # ドキュメント完了かつ画像が読み込まれている場合、待機終了
487
+ if resource_state['complete'] and (resource_state['imgCount'] == 0 or
488
+ resource_state['imgLoaded'] == resource_state['imgCount']):
489
+ logger.info(f"リソース読み込み完了: {resource_state}")
490
+ break
491
+
492
+ time.sleep(wait_increment)
493
+ wait_time += wait_increment
494
+ logger.info(f"リソース待機中... {wait_time:.1f}秒経過, 状態: {resource_state}")
495
+
496
+ # Font Awesome要素が多い場合は追加待機
497
+ fa_count = resource_state.get('faElements', 0)
498
+ if fa_count > 30:
499
+ logger.info(f"{fa_count}個のFont Awesome要素があるため、追加待機...")
500
+ time.sleep(min(1.0, fa_count / 100)) # 要素数に応じて待機(最大1秒)
501
+
502
+ # コンテンツレンダリングのためのスクロール処理 - パフォーマンス改善
503
  logger.info("Performing content rendering scroll...")
504
  total_height = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);")
505
  viewport_height = driver.execute_script("return window.innerHeight;")
506
+ scrolls_needed = max(1, min(5, total_height // viewport_height)) # 最大5回までに制限
507
+
508
+ # スクロール処理の高速化
509
+ for i in range(scrolls_needed):
510
+ scroll_pos = i * (viewport_height - 100) # 少しだけオーバーラップ
511
  driver.execute_script(f"window.scrollTo(0, {scroll_pos});")
512
+ time.sleep(0.1) # 高速化のため待機時間短縮
513
+
514
  # トップに戻る
515
  driver.execute_script("window.scrollTo(0, 0);")
516
+ time.sleep(0.2) # 短い待機に変更
517
  logger.info("Scroll rendering completed")
518
 
519
+ # スクロールバーを非表示に
520
  driver.execute_script("""
521
  document.documentElement.style.overflow = 'hidden';
522
  document.body.style.overflow = 'hidden';
523
  """)
524
+
525
+ # ページの寸法を取得
 
526
  dimensions = driver.execute_script("""
527
  return {
528
  width: Math.max(
 
547
  scroll_height = dimensions['height']
548
  logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}")
549
 
 
 
 
 
 
 
 
 
 
 
550
  # 最小/最大値の設定
551
  scroll_width = max(scroll_width, 100)
552
  scroll_height = max(scroll_height, 100)
553
  scroll_width = min(scroll_width, 2000)
554
  scroll_height = min(scroll_height, 4000)
555
+
556
+ # レイアウト安定化のための待機 - 短縮
557
+ time.sleep(0.5) # 2秒から0.5秒に短縮
558
 
559
+ # 高���に余白を追加
 
 
 
 
560
  adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
561
  adjusted_height = max(adjusted_height, scroll_height, 100)
562
+
563
+ # ウィンドウサイズを調整
 
564
  adjusted_width = scroll_width
565
  logger.info(f"Resizing window to: width={adjusted_width}, height={adjusted_height}")
566
  driver.set_window_size(adjusted_width, adjusted_height)
567
+ time.sleep(0.5) # 短縮した待機時間
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
 
569
+ # スクリーンショット取得
 
 
 
 
 
570
  logger.info("Taking screenshot...")
571
  png = driver.get_screenshot_as_png()
572
  logger.info("Screenshot taken successfully.")
 
575
  img = Image.open(BytesIO(png))
576
  logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
577
 
578
+ # 余白トリミング - 最適化版を使用
579
  if trim_whitespace:
580
  img = trim_image_whitespace(img, threshold=248, padding=20)
581
  logger.info(f"Trimmed dimensions: {img.width}x{img.height}")
 
584
 
585
  except Exception as e:
586
  logger.error(f"Error during screenshot generation: {e}", exc_info=True)
587
+ # エラー時は小さい黒画像を返す
588
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
589
  finally:
590
  logger.info("Cleaning up...")
591
+ # WebDriverプールに戻す
592
+ if driver_from_pool:
593
+ driver_pool.release_driver(driver)
594
+ logger.info("Returned driver to pool")
595
+ # 一時ファイル削除
 
596
  if tmp_path and os.path.exists(tmp_path):
597
  try:
598
  os.remove(tmp_path)
599
  logger.info(f"Temporary file {tmp_path} removed.")
600
  except Exception as e:
601
+ logger.error(f"Error removing temporary file {tmp_path}: {e}")
602
+
603
+ # --- 並列処理を活用した新しい関数 ---
604
+ def text_to_screenshot_parallel(text: str, extension_percentage: float, temperature: float = 0.3,
605
+ trim_whitespace: bool = True, style: str = "standard") -> Image.Image:
606
+ """テキストをGemini APIでHTMLに変換し、並列処理でスクリーンショットを生成する関数"""
607
+ start_time = time.time()
608
+ logger.info("並列処理によるテキスト→スクリーンショット生成を開始")
609
+
610
  try:
611
+ # WebDriverと HTML生成を並列で実行
612
+ with ThreadPoolExecutor(max_workers=2) as executor:
613
+ # Gemini APIリクエストタスク
614
+ html_future = executor.submit(
615
+ generate_html_from_text,
616
+ text=text,
617
+ temperature=temperature,
618
+ style=style
619
+ )
620
+
621
+ # WebDriver初期化タスク - プール使用
622
+ driver_future = executor.submit(driver_pool.get_driver)
623
+
624
+ # 結果を取得
625
+ html_code = html_future.result()
626
+ driver = driver_future.result()
627
+
628
+ # ドライバーはプールから取得しているためフラグ設定
629
+ driver_from_pool = True
630
+
631
+ # HTMLコードとドライバーが準備できたらスクリーンショット生成
632
+ logger.info(f"HTML生成完了:{len(html_code)}文字。スクリーンショット生成開始。")
633
+
634
+ # レンダリング前にドライバーの初期設定
635
+ tmp_path = None
636
+ try:
637
+ # 一時ファイルにHTMLを保存
638
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
639
+ tmp_path = tmp_file.name
640
+ tmp_file.write(html_code)
641
+ logger.info(f"HTMLを一時ファイルに保存: {tmp_path}")
642
+
643
+ # ウィンドウサイズ初期設定
644
+ initial_width = 1200
645
+ initial_height = 1000
646
+ driver.set_window_size(initial_width, initial_height)
647
+ file_url = "file://" + tmp_path
648
+ logger.info(f"ページに移動: {file_url}")
649
+ driver.get(file_url)
650
+
651
+ # ここからスクリーンショット生成ロジック(前の実装と同様)
652
+ # ページ読み込み待機 - 動的な待機時間を実装
653
+ logger.info("body要素を待機...")
654
+ WebDriverWait(driver, 10).until(
655
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
656
+ )
657
+ logger.info("body要素を検出。リソース読み込みを待機...")
658
+
659
+ # リソース読み込みの動的待機 - 最適化
660
+ max_wait = 3 # 最大待機時間(秒)
661
+ wait_increment = 0.2 # 確認間隔
662
+ wait_time = 0
663
+
664
+ while wait_time < max_wait:
665
+ resource_state = driver.execute_script("""
666
+ return {
667
+ complete: document.readyState === 'complete',
668
+ imgCount: document.images.length,
669
+ imgLoaded: Array.from(document.images).filter(img => img.complete).length,
670
+ faElements: document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]').length
671
+ };
672
+ """)
673
+
674
+ # ドキュメント完了かつ画像が読み込まれている場合、待機終了
675
+ if resource_state['complete'] and (resource_state['imgCount'] == 0 or
676
+ resource_state['imgLoaded'] == resource_state['imgCount']):
677
+ logger.info(f"リソース読み込み完了: {resource_state}")
678
+ break
679
+
680
+ time.sleep(wait_increment)
681
+ wait_time += wait_increment
682
+
683
+ # Font Awesome要素が多い場合は追加待機
684
+ fa_count = resource_state.get('faElements', 0)
685
+ if fa_count > 30:
686
+ logger.info(f"{fa_count}個のFont Awesome要素があるため、追加待機...")
687
+ time.sleep(min(0.5, fa_count / 200)) # 要素数に応じて待機(最大0.5秒)
688
+
689
+ # コンテンツレンダリングのための簡易スクロール
690
+ driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
691
+ time.sleep(0.2)
692
+ driver.execute_script("window.scrollTo(0, 0);")
693
+ time.sleep(0.2)
694
+
695
+ # スクロールバーを非表示に
696
+ driver.execute_script("""
697
+ document.documentElement.style.overflow = 'hidden';
698
+ document.body.style.overflow = 'hidden';
699
+ """)
700
+
701
+ # ページの寸法を取得
702
+ dimensions = driver.execute_script("""
703
+ return {
704
+ width: Math.max(
705
+ document.documentElement.scrollWidth,
706
+ document.documentElement.offsetWidth,
707
+ document.documentElement.clientWidth,
708
+ document.body ? document.body.scrollWidth : 0,
709
+ document.body ? document.body.offsetWidth : 0,
710
+ document.body ? document.body.clientWidth : 0
711
+ ),
712
+ height: Math.max(
713
+ document.documentElement.scrollHeight,
714
+ document.documentElement.offsetHeight,
715
+ document.documentElement.clientHeight,
716
+ document.body ? document.body.scrollHeight : 0,
717
+ document.body ? document.body.offsetHeight : 0,
718
+ document.body ? document.body.clientHeight : 0
719
+ )
720
+ };
721
+ """)
722
+ scroll_width = dimensions['width']
723
+ scroll_height = dimensions['height']
724
+
725
+ # 最小/最大値の設定
726
+ scroll_width = max(scroll_width, 100)
727
+ scroll_height = max(scroll_height, 100)
728
+ scroll_width = min(scroll_width, 2000)
729
+ scroll_height = min(scroll_height, 4000)
730
+
731
+ # 高さに余白を追加
732
+ adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
733
+ adjusted_height = max(adjusted_height, scroll_height, 100)
734
+
735
+ # ウィンドウサイズを調整
736
+ driver.set_window_size(scroll_width, adjusted_height)
737
+ time.sleep(0.2)
738
+
739
+ # スクリーンショット取得
740
+ logger.info("スクリーンショットを撮影...")
741
+ png = driver.get_screenshot_as_png()
742
+
743
+ # PIL画像に変換
744
+ img = Image.open(BytesIO(png))
745
+ logger.info(f"スクリーンショットサイズ: {img.width}x{img.height}")
746
+
747
+ # 余白トリミング
748
+ if trim_whitespace:
749
+ img = trim_image_whitespace(img, threshold=248, padding=20)
750
+ logger.info(f"トリミング後のサイズ: {img.width}x{img.height}")
751
+
752
+ elapsed = time.time() - start_time
753
+ logger.info(f"並列処理による生成完了。所要時間: {elapsed:.2f}秒")
754
+ return img
755
+
756
+ except Exception as e:
757
+ logger.error(f"スクリーンショット生成中にエラー: {e}", exc_info=True)
758
+ return Image.new('RGB', (1, 1), color=(0, 0, 0))
759
+ finally:
760
+ # WebDriverプールに戻す
761
+ if driver_from_pool:
762
+ driver_pool.release_driver(driver)
763
+ # 一時ファイル削除
764
+ if tmp_path and os.path.exists(tmp_path):
765
+ try:
766
+ os.remove(tmp_path)
767
+ except Exception as e:
768
+ logger.error(f"一時ファイル削除エラー: {e}")
769
+
770
  except Exception as e:
771
+ logger.error(f"並列処理中のエラー: {e}", exc_info=True)
772
  return Image.new('RGB', (1, 1), color=(0, 0, 0)) # エラー時は黒画像
773
 
774
+ # 従来の非並列版も残す(互換性のため)
775
+ def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 0.3,
776
+ trim_whitespace: bool = True, style: str = "standard") -> Image.Image:
777
+ """テキストをGemini APIでHTMLに変換し、スクリーンショットを生成する統合関数(レガシー版)"""
778
+ # 並列処理版を呼び出す
779
+ return text_to_screenshot_parallel(text, extension_percentage, temperature, trim_whitespace, style)
780
+
781
  # --- FastAPI Setup ---
782
  app = FastAPI()
783
 
 
833
  """
834
  try:
835
  logger.info(f"API request received. Extension: {request.extension_percentage}%")
836
+ # Run the blocking Selenium code (now using the pooled version)
837
  pil_image = render_fullpage_screenshot(
838
  request.html_code,
839
  request.extension_percentage,
 
857
  logger.error(f"API Error: {e}", exc_info=True)
858
  raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
859
 
860
+ # --- 新しいGemini API連携エンドポイント(並列処理版) ---
861
  @app.post("/api/text-to-screenshot",
862
  response_class=StreamingResponse,
863
  tags=["Screenshot", "Gemini"],
 
872
  f"拡張率: {request.extension_percentage}%, 温度: {request.temperature}, "
873
  f"スタイル: {request.style}")
874
 
875
+ # 並列処理版を使用
876
+ pil_image = text_to_screenshot_parallel(
877
  request.text,
878
  request.extension_percentage,
879
  request.temperature,
 
883
 
884
  if pil_image.size == (1, 1):
885
  logger.error("スクリーンショット生成に失敗しました。1x1エラー画像を返します。")
 
 
886
 
887
  # PIL画像をPNGバイトに変換
888
  img_byte_arr = BytesIO()
 
904
  # HTMLモ��ドの場合は既存の処理(スタイルは使わない)
905
  return render_fullpage_screenshot(input_text, extension_percentage, trim_whitespace)
906
  else:
907
+ # テキスト入力モードの場合はGemini APIを使用(並列処理版)
908
+ return text_to_screenshot_parallel(input_text, extension_percentage, temperature, trim_whitespace, style)
909
 
910
  # Gradio UIの定義
911
  with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
912
  gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
913
  gr.Markdown("HTMLコードをレンダリングするか、テキストをGemini APIでインフォグラフィックに変換して画像として取得します。")
914
+ gr.Markdown("**パフォーマンス向上版**: 並列処理と最適化により処理時間を短縮しています")
915
 
916
  with gr.Row():
917
  input_mode = gr.Radio(
 
999
  ## 設定情報
1000
  - 使用モデル: {gemini_model} (環境変数 GEMINI_MODEL で変更可能)
1001
  - 対応スタイル: standard, cute, resort, cool, dental
1002
+ - WebDriverプール最大数: {driver_pool.max_drivers} (環境変数 MAX_WEBDRIVERS で変更可能)
1003
  """)
1004
 
1005
  # --- Mount Gradio App onto FastAPI ---
 
1009
  if __name__ == "__main__":
1010
  import uvicorn
1011
  logger.info("Starting Uvicorn server for local development...")
1012
+ uvicorn.run(app, host="0.0.0.0", port=7860)
1013
+
1014
+ # アプリケーション終了時にWebDriverプールをクリーンアップ
1015
+ import atexit
1016
+ atexit.register(driver_pool.close_all)