tomo2chin2 commited on
Commit
42e5ffa
·
verified ·
1 Parent(s): 9ac8661

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +272 -547
app.py CHANGED
@@ -1,28 +1,33 @@
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
- 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
 
 
 
 
26
 
27
  # ロギング設定
28
  logging.basicConfig(level=logging.INFO)
@@ -56,7 +61,6 @@ class WebDriverPool:
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}")
@@ -66,7 +70,6 @@ class WebDriverPool:
66
  logger.info("デフォルトのChromeDriverを使用")
67
  return webdriver.Chrome(options=options)
68
 
69
- # 最大数に達した場合は待機
70
  logger.info("WebDriverプールがいっぱいです。利用可能なドライバーを待機中...")
71
  return self.driver_queue.get()
72
 
@@ -74,7 +77,6 @@ class WebDriverPool:
74
  """ドライバーをプールに戻す"""
75
  if driver:
76
  try:
77
- # ブラウザをリセット
78
  driver.get("about:blank")
79
  driver.execute_script("""
80
  document.documentElement.style.overflow = '';
@@ -107,90 +109,70 @@ class WebDriverPool:
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へのリクエストデータモデル"""
116
  text: str
117
  extension_percentage: float = 10.0 # デフォルト値10%
118
- temperature: float = 0.5 # デフォルト値を0.5に設定
119
- trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
120
- style: str = "standard" # デフォルトはstandard
121
 
122
  class ScreenshotRequest(BaseModel):
123
  """スクリーンショットリクエストモデル"""
124
  html_code: str
125
- extension_percentage: float = 10.0 # デフォルト値10%
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>
142
- /* Font Awesomeアイコンのレイアウト���正 */
143
  [class*="fa-"] {
144
  display: inline-block !important;
145
  margin-right: 8px !important;
146
  vertical-align: middle !important;
147
  }
148
-
149
- /* テキスト内のアイコン位置調整 */
150
- h1 [class*="fa-"], h2 [class*="fa-"], h3 [class*="fa-"],
151
  h4 [class*="fa-"], h5 [class*="fa-"], h6 [class*="fa-"] {
152
  vertical-align: middle !important;
153
  margin-right: 10px !important;
154
  }
155
-
156
- /* 特定パターンの修正 */
157
  .fa + span, .fas + span, .far + span, .fab + span,
158
  span + .fa, span + .fas, span + .far + span {
159
  display: inline-block !important;
160
  margin-left: 5px !important;
161
  }
162
-
163
- /* カード内アイコン修正 */
164
  .card [class*="fa-"], .card-body [class*="fa-"] {
165
  float: none !important;
166
  clear: none !important;
167
  position: relative !important;
168
  }
169
-
170
- /* アイコンと文字が重なる場合の調整 */
171
  li [class*="fa-"], p [class*="fa-"] {
172
  margin-right: 10px !important;
173
  }
174
-
175
- /* インラインアイコンのスペーシング */
176
  .inline-icon {
177
  display: inline-flex !important;
178
  align-items: center !important;
179
  justify-content: flex-start !important;
180
  }
181
-
182
- /* アイコン後のテキスト */
183
  [class*="fa-"] + span {
184
  display: inline-block !important;
185
  vertical-align: middle !important;
186
  }
187
  </style>
188
  """
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:
@@ -199,282 +181,175 @@ def enhance_font_awesome_layout(html_code):
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
  """
208
  指定されたスタイルのシステムインストラクションを読み込む
209
-
210
- Args:
211
- style: 使用するスタイル名 (standard, cute, resort, cool, dental, school)
212
-
213
- Returns:
214
- 読み込まれたシステムインストラクション
215
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  try:
217
- # 有効なスタイル一覧
218
- valid_styles = ["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"]
219
-
220
- # スタイルの検証
221
- if style not in valid_styles:
222
- logger.warning(f"無効なスタイル '{style}' が指定されました。デフォルトの 'standard' を使用します。")
223
- style = "standard"
224
-
225
- logger.info(f"スタイル '{style}' のシステムインストラクションを読み込みます")
226
-
227
- # まず、ローカルのスタイルィレクリ内のprompt.txtを確認
228
- local_path = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
229
-
230
- # ローカルファイルが存在する場合はそれを使用
231
- if os.path.exists(local_path):
232
- logger.info(f"ローカルファイルを使用: {local_path}")
233
- with open(local_path, 'r', encoding='utf-8') as file:
234
- instruction = file.read()
235
- return instruction
236
-
237
- # HuggingFaceリポジトリからのファイル読み込みを試行
238
- try:
239
- # スタイル固有のファイルパスを指定
240
- file_path = hf_hub_download(
241
- repo_id="tomo2chin2/GURAREKOstlyle",
242
- filename=f"{style}/prompt.txt",
243
- repo_type="dataset"
244
- )
245
-
246
- logger.info(f"スタイル '{style}' のプロンプトをHuggingFaceから読み込みました: {file_path}")
247
- with open(file_path, 'r', encoding='utf-8') as file:
248
- instruction = file.read()
249
- return instruction
250
-
251
- except Exception as style_error:
252
- # スタイル固有ファイルの読み込みに失敗した場合、デフォルトのprompt.txtを使用
253
- logger.warning(f"スタイル '{style}' のプロンプト読み込みに失敗: {str(style_error)}")
254
- logger.info("デフォルトのprompt.txtを読み込みます")
255
-
256
- file_path = hf_hub_download(
257
- repo_id="tomo2chin2/GURAREKOstlyle",
258
- filename="prompt.txt",
259
- repo_type="dataset"
260
- )
261
-
262
- with open(file_path, 'r', encoding='utf-8') as file:
263
- instruction = file.read()
264
-
265
- logger.info("デフォルトのシステムインストラクションを読み込みました")
266
- return instruction
267
-
268
- except Exception as e:
269
- error_msg = f"システムインストラクションの読み込みに失敗: {str(e)}"
270
- logger.error(error_msg)
271
- raise ValueError(error_msg)
272
 
 
273
  def generate_html_from_text(text, temperature=0.5, style="standard"):
274
- """テキストからHTMLを生成する"""
 
 
 
275
  try:
276
- # APIキーの取得と設定
277
  api_key = os.environ.get("GEMINI_API_KEY")
278
  if not api_key:
279
  logger.error("GEMINI_API_KEY 環境変数が設定されていません")
280
- raise ValueError("GEMINI_API_KEY 環境変数が設定されていません")
281
 
282
- # モデル名の取得(環境変数から、なければデフォルト値)
283
  model_name = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
284
- logger.info(f"使用するGeminiモデル: {model_name}")
285
-
286
- # Gemini APIの設定
287
- genai.configure(api_key=api_key)
288
-
289
- # 指定されたスタルのシステムインストクション読み込む
290
- system_instruction = load_system_instruction(style)
291
-
292
- # モデル初期化
293
- logger.info(f"Gemini APIにリクエストを送信: テキスト長さ = {len(text)}, 温度 = {temperature}, スタイル = {style}")
294
-
295
- # モデル初期化
296
- model = genai.GenerativeModel(model_name)
297
-
298
- # 生成設定 - ばらつきを減らすために設定を調整
299
- generation_config = {
300
- "temperature": temperature, # より低い温度を設定
301
- "top_p": 0.7, # 0.95から0.7に下げて出力の多様性を制限
302
- "top_k": 20, # 64から20に下げて候補を絞る
303
- "max_output_tokens": 8192,
304
- "candidate_count": 1 # 候補は1つだけ生成
305
- }
306
-
307
- # 安全設定 - デフォルトの安全設定を使用
308
- safety_settings = [
309
- {
310
- "category": "HARM_CATEGORY_HARASSMENT",
311
- "threshold": "BLOCK_MEDIUM_AND_ABOVE"
312
- },
313
- {
314
- "category": "HARM_CATEGORY_HATE_SPEECH",
315
- "threshold": "BLOCK_MEDIUM_AND_ABOVE"
316
- },
317
- {
318
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
319
- "threshold": "BLOCK_MEDIUM_AND_ABOVE"
320
- },
321
- {
322
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
323
- "threshold": "BLOCK_MEDIUM_AND_ABOVE"
324
- }
325
- ]
326
-
327
- # プロンプト構築
328
- prompt = f"{system_instruction}\n\n{text}"
329
-
330
- # コンテンツ生成
331
- response = model.generate_content(
332
- prompt,
333
- generation_config=generation_config,
334
- safety_settings=safety_settings
335
- )
336
-
337
- # レスポンスからHTMLを抽出
338
- raw_response = response.text
339
-
340
- # HTMLタグ部分だけを抽出(```html と ``` の間)
341
- html_start = raw_response.find("```html")
342
- html_end = raw_response.rfind("```")
343
 
 
 
 
344
  if html_start != -1 and html_end != -1 and html_start < html_end:
345
- html_start += 7 # "```html" の長さ分進める
346
- html_code = raw_response[html_start:html_end].strip()
347
- logger.info(f"HTMLの生成に成功: 長さ = {len(html_code)}")
348
-
349
- # Font Awesomeのレイアウト改善
350
- html_code = enhance_font_awesome_layout(html_code)
351
- logger.info("Font Awesomeレイアウトの最適化を適用しました")
352
-
353
- return html_code
354
  else:
355
- # HTMLタグが見つからない場合、レスポンス全体を返す
356
- logger.warning("レスポンスから ```html ``` タグが見つかりませんでした。全テキストを返します。")
357
- return raw_response
 
 
 
358
 
359
  except Exception as e:
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)
371
- padding: int - トリミング後に残す余白のピクセル数
372
-
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:
 
445
  with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
446
  tmp_path = tmp_file.name
447
  tmp_file.write(html_code)
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)
460
- file_url = "file://" + tmp_path
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 = 5 # 最大待機時間(秒)
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,
@@ -482,48 +357,34 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
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(
529
  document.documentElement.scrollWidth,
@@ -543,39 +404,23 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
543
  )
544
  };
545
  """)
546
- scroll_width = dimensions['width']
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(2.0)
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.")
573
-
574
- # PIL画像に変換
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,15 +429,11 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
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)
@@ -600,188 +441,128 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
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.5,
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(1.0, fa_count / 100)) # 要素数に応じて待機(最大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
-
784
- # CORS設定を追加
785
  app.add_middleware(
786
  CORSMiddleware,
787
  allow_origins=["*"],
@@ -791,88 +572,58 @@ app.add_middleware(
791
  )
792
 
793
  # 静的ファイルのサービング設定
794
- # Gradioのディレクトリを探索してアセットを見つける
795
  gradio_dir = os.path.dirname(gr.__file__)
796
- logger.info(f"Gradio version: {gr.__version__}")
797
- logger.info(f"Gradio directory: {gradio_dir}")
798
-
799
- # 基本的な静的ファイルディレクトリをマウント
800
  static_dir = os.path.join(gradio_dir, "templates", "frontend", "static")
801
  if os.path.exists(static_dir):
802
- logger.info(f"Mounting static directory: {static_dir}")
803
  app.mount("/static", StaticFiles(directory=static_dir), name="static")
804
-
805
- # _appディレクトリを探す(新しいSvelteKitベースのフロントエンド用)
806
  app_dir = os.path.join(gradio_dir, "templates", "frontend", "_app")
807
  if os.path.exists(app_dir):
808
- logger.info(f"Mounting _app directory: {app_dir}")
809
  app.mount("/_app", StaticFiles(directory=app_dir), name="_app")
810
-
811
- # assetsディレクトリを探す
812
  assets_dir = os.path.join(gradio_dir, "templates", "frontend", "assets")
813
  if os.path.exists(assets_dir):
814
- logger.info(f"Mounting assets directory: {assets_dir}")
815
  app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
816
-
817
- # cdnディレクトリがあれば追加
818
  cdn_dir = os.path.join(gradio_dir, "templates", "cdn")
819
  if os.path.exists(cdn_dir):
820
- logger.info(f"Mounting cdn directory: {cdn_dir}")
821
  app.mount("/cdn", StaticFiles(directory=cdn_dir), name="cdn")
822
 
823
-
824
- # API Endpoint for screenshot generation
825
  @app.post("/api/screenshot",
826
  response_class=StreamingResponse,
827
  tags=["Screenshot"],
828
  summary="Render HTML to Full Page Screenshot",
829
  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.")
830
  async def api_render_screenshot(request: ScreenshotRequest):
831
- """
832
- API endpoint to render HTML and return a screenshot.
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,
840
  request.trim_whitespace
841
  )
842
-
843
  if pil_image.size == (1, 1):
844
- logger.error("Screenshot generation failed, returning 1x1 error image.")
845
- # Optionally return a proper error response instead of 1x1 image
846
- # raise HTTPException(status_code=500, detail="Failed to generate screenshot")
847
-
848
- # Convert PIL Image to PNG bytes
849
  img_byte_arr = BytesIO()
850
  pil_image.save(img_byte_arr, format='PNG')
851
- img_byte_arr.seek(0) # Go to the start of the BytesIO buffer
852
-
853
  logger.info("Returning screenshot as PNG stream.")
854
  return StreamingResponse(img_byte_arr, media_type="image/png")
855
-
856
  except Exception as e:
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"],
864
  summary="テキストからインフォグラフィックを生成",
865
  description="テキストをGemini APIを使ってHTMLインフォグラフィックに変換し、スクリーンショットとして返します。")
866
  async def api_text_to_screenshot(request: GeminiRequest):
867
- """
868
- テキストからHTMLインフォグラフィックを生成してスクリーンショットを返すAPIエンドポイント
869
- """
870
  try:
871
- logger.info(f"テキスト→スクリーンショットAPIリクエスト受信。テキスト長さ: {len(request.text)}, "
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,
@@ -880,34 +631,18 @@ async def api_text_to_screenshot(request: GeminiRequest):
880
  request.trim_whitespace,
881
  request.style
882
  )
883
-
884
  if pil_image.size == (1, 1):
885
  logger.error("スクリーンショット生成に失敗しました。1x1エラー画像を返します。")
886
-
887
- # PIL画像をPNGバイトに変換
888
  img_byte_arr = BytesIO()
889
  pil_image.save(img_byte_arr, format='PNG')
890
- img_byte_arr.seek(0) # BytesIOバッファの先頭に戻る
891
-
892
  logger.info("スクリーンショットをPNGストリームとして返します。")
893
  return StreamingResponse(img_byte_arr, media_type="image/png")
894
-
895
  except Exception as e:
896
  logger.error(f"API Error: {e}", exc_info=True)
897
  raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
898
 
899
  # --- Gradio Interface Definition ---
900
- # 入力モードの選択用Radioコンポーネント
901
- def process_input(input_mode, input_text, extension_percentage, temperature, trim_whitespace, style):
902
- """入力モードに応じて適切な処理を行う"""
903
- if input_mode == "HTML入力":
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でインフォグラフィックに変換して画像として取得します。")
@@ -920,7 +655,6 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
920
  value="HTML入力"
921
  )
922
 
923
- # 共用のテキストボックス
924
  input_text = gr.Textbox(
925
  lines=15,
926
  label="入力",
@@ -929,35 +663,30 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
929
 
930
  with gr.Row():
931
  with gr.Column(scale=1):
932
- # スタイル選択ドロップダウン
933
  style_dropdown = gr.Dropdown(
934
  choices=["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"],
935
  value="standard",
936
  label="デザインスタイル",
937
  info="テキスト→HTML変換時のデザインテーマを選択します",
938
- visible=False # テキスト入力モードの時だけ表示
939
  )
940
-
941
  with gr.Column(scale=2):
942
  extension_percentage = gr.Slider(
943
  minimum=0,
944
  maximum=30,
945
  step=1.0,
946
- value=10, # デフォルト値10%
947
  label="上下高さ拡張率(%)"
948
  )
949
-
950
- # 温度調整スライダー(テキストモード時のみ表示)
951
  temperature = gr.Slider(
952
  minimum=0.0,
953
  maximum=1.0,
954
  step=0.1,
955
- value=0.5, # デフォルト値を0.5に設定
956
  label="生成時の温度(低い=一貫性高、高い=創造性高)",
957
- visible=False # 最初は非表示
958
  )
959
 
960
- # 余白トリミングオプション
961
  trim_whitespace = gr.Checkbox(
962
  label="余白を自動トリミング",
963
  value=True,
@@ -967,10 +696,8 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
967
  submit_btn = gr.Button("生成")
968
  output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
969
 
970
- # 入力モード変更時のイベント処理(テキストモード時のみ温度スライダーとスタイルドロップダウンを表示)
971
  def update_controls_visibility(mode):
972
- # Gradio 4.x用のアップデー方法
973
- is_text_mode = mode == "テキスト入力"
974
  return [
975
  gr.update(visible=is_text_mode), # temperature
976
  gr.update(visible=is_text_mode), # style_dropdown
@@ -982,14 +709,12 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
982
  outputs=[temperature, style_dropdown]
983
  )
984
 
985
- # 生成ボタンクリック時のイベント処理
986
  submit_btn.click(
987
  fn=process_input,
988
  inputs=[input_mode, input_text, extension_percentage, temperature, trim_whitespace, style_dropdown],
989
  outputs=output_image
990
  )
991
 
992
- # 環境変数情報を表示
993
  gemini_model = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
994
  gr.Markdown(f"""
995
  ## APIエンドポイント
@@ -1005,7 +730,7 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
1005
  # --- Mount Gradio App onto FastAPI ---
1006
  app = gr.mount_gradio_app(app, iface, path="/")
1007
 
1008
- # --- Run with Uvicorn (for local testing) ---
1009
  if __name__ == "__main__":
1010
  import uvicorn
1011
  logger.info("Starting Uvicorn server for local development...")
@@ -1013,4 +738,4 @@ if __name__ == "__main__":
1013
 
1014
  # アプリケーション終了時にWebDriverプールをクリーンアップ
1015
  import atexit
1016
- atexit.register(driver_pool.close_all)
 
1
+ # app.py
2
+ import os
3
+ import time
4
+ import tempfile
5
+ import threading
6
+ import queue
7
+ import logging
8
+ import numpy as np # 追加: 画像処理の最適化用
9
+ from io import BytesIO
10
+ from PIL import Image
11
+ from pydantic import BaseModel
12
  from fastapi import FastAPI, HTTPException, Body
13
  from fastapi.responses import StreamingResponse
14
  from fastapi.staticfiles import StaticFiles
15
  from fastapi.middleware.cors import CORSMiddleware
16
+ import gradio as gr
17
  from selenium import webdriver
18
  from selenium.webdriver.chrome.options import Options
19
  from selenium.webdriver.common.by import By
20
  from selenium.webdriver.support.ui import WebDriverWait
21
  from selenium.webdriver.support import expected_conditions as EC
 
 
 
 
 
 
 
 
 
22
  from concurrent.futures import ThreadPoolExecutor # 追加: 並列処理用
23
  from huggingface_hub import hf_hub_download
24
 
25
+ # 既存 Gemini ラブラリ
26
+ import google.generativeai as genai_old
27
+
28
+ # 新しい Gemini ライブラリ(2.5系モデル用)
29
+ from google import genai as genai_new
30
+ from google.genai import types
31
 
32
  # ロギング設定
33
  logging.basicConfig(level=logging.INFO)
 
61
  options.add_argument("--disable-features=NetworkService")
62
  options.add_argument("--dns-prefetch-disable")
63
 
 
64
  webdriver_path = os.environ.get("CHROMEDRIVER_PATH")
65
  if webdriver_path and os.path.exists(webdriver_path):
66
  logger.info(f"CHROMEDRIVER_PATH使用: {webdriver_path}")
 
70
  logger.info("デフォルトのChromeDriverを使用")
71
  return webdriver.Chrome(options=options)
72
 
 
73
  logger.info("WebDriverプールがいっぱいです。利用可能なドライバーを待機中...")
74
  return self.driver_queue.get()
75
 
 
77
  """ドライバーをプールに戻す"""
78
  if driver:
79
  try:
 
80
  driver.get("about:blank")
81
  driver.execute_script("""
82
  document.documentElement.style.overflow = '';
 
109
  self.count = 0
110
 
111
  # グローバルなWebDriverプールを作成
 
112
  driver_pool = WebDriverPool(max_drivers=int(os.environ.get("MAX_WEBDRIVERS", "3")))
113
 
114
+ # --- リクエストモデル ---
115
  class GeminiRequest(BaseModel):
116
  """Geminiへのリクエストデータモデル"""
117
  text: str
118
  extension_percentage: float = 10.0 # デフォルト値10%
119
+ temperature: float = 0.5 # デフォルト値を0.5
120
+ trim_whitespace: bool = True # 余白トリミングオプション(既定で有効)
121
+ style: str = "standard" # デフォルトはstandard
122
 
123
  class ScreenshotRequest(BaseModel):
124
  """スクリーンショットリクエストモデル"""
125
  html_code: str
126
+ extension_percentage: float = 10.0
127
+ trim_whitespace: bool = True
128
+ style: str = "standard"
129
 
130
+ # --- Font Awesomeレイアウト改善 ---
131
  def enhance_font_awesome_layout(html_code):
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
  fa_fix_css = """
139
  <style>
 
140
  [class*="fa-"] {
141
  display: inline-block !important;
142
  margin-right: 8px !important;
143
  vertical-align: middle !important;
144
  }
145
+ h1 [class*="fa-"], h2 [class*="fa-"], h3 [class*="fa-"],
 
 
146
  h4 [class*="fa-"], h5 [class*="fa-"], h6 [class*="fa-"] {
147
  vertical-align: middle !important;
148
  margin-right: 10px !important;
149
  }
 
 
150
  .fa + span, .fas + span, .far + span, .fab + span,
151
  span + .fa, span + .fas, span + .far + span {
152
  display: inline-block !important;
153
  margin-left: 5px !important;
154
  }
 
 
155
  .card [class*="fa-"], .card-body [class*="fa-"] {
156
  float: none !important;
157
  clear: none !important;
158
  position: relative !important;
159
  }
 
 
160
  li [class*="fa-"], p [class*="fa-"] {
161
  margin-right: 10px !important;
162
  }
 
 
163
  .inline-icon {
164
  display: inline-flex !important;
165
  align-items: center !important;
166
  justify-content: flex-start !important;
167
  }
 
 
168
  [class*="fa-"] + span {
169
  display: inline-block !important;
170
  vertical-align: middle !important;
171
  }
172
  </style>
173
  """
 
 
174
  if '<head>' in html_code:
175
  return html_code.replace('</head>', f'{fa_preload}{fa_fix_css}</head>')
 
176
  elif '<html' in html_code:
177
  head_end = html_code.find('</head>')
178
  if head_end > 0:
 
181
  body_start = html_code.find('<body')
182
  if body_start > 0:
183
  return html_code[:body_start] + f'<head>{fa_preload}{fa_fix_css}</head>' + html_code[body_start:]
 
 
184
  return f'<html><head>{fa_preload}{fa_fix_css}</head>' + html_code + '</html>'
185
 
186
+ # --- システムインストラクション読み込み ---
187
  def load_system_instruction(style="standard"):
188
  """
189
  指定されたスタイルのシステムインストラクションを読み込む
 
 
 
 
 
 
190
  """
191
+ valid_styles = ["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"]
192
+ if style not in valid_styles:
193
+ logger.warning(f"無効なスタイル '{style}' が指定されました。デフォルトの 'standard' を使用します。")
194
+ style = "standard"
195
+ logger.info(f"スタイル '{style}' のシステムインストラクションを読み込みます")
196
+
197
+ # ローカルファイル優先
198
+ local_path = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
199
+ if os.path.exists(local_path):
200
+ logger.info(f"ローカルファイルを使用: {local_path}")
201
+ with open(local_path, 'r', encoding='utf-8') as file:
202
+ return file.read()
203
+
204
+ # HuggingFace から取得
205
  try:
206
+ file_path = hf_hub_download(
207
+ repo_id="tomo2chin2/GURAREKOstlyle",
208
+ filename=f"{style}/prompt.txt",
209
+ repo_type="dataset"
210
+ )
211
+ logger.info(f"HuggingFace から読み込み: {file_path}")
212
+ with open(file_path, 'r', encoding='utf-8') as file:
213
+ return file.read()
214
+ except Exception as style_error:
215
+ logger.warning(f"スタイル '{style}' の読み込み失敗: {style_error}")
216
+ logger.info("フォルトの prompt.txt 読み込みます")
217
+ file_path = hf_hub_download(
218
+ repo_id="tomo2chin2/GURAREKOstlyle",
219
+ filename="prompt.txt",
220
+ repo_type="dataset"
221
+ )
222
+ with open(file_path, 'r', encoding='utf-8') as file:
223
+ return file.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
+ # --- テキストからHTML生成 ---
226
  def generate_html_from_text(text, temperature=0.5, style="standard"):
227
+ """
228
+ テキストからHTMLを生成する
229
+ gemini-2.5-flash-preview-04-17 のときのみ新ライブラリ+thinkingBudget=0
230
+ """
231
  try:
 
232
  api_key = os.environ.get("GEMINI_API_KEY")
233
  if not api_key:
234
  logger.error("GEMINI_API_KEY 環境変数が設定されていません")
235
+ raise ValueError("GEMINI_API_KEY が設定されていません")
236
 
 
237
  model_name = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
238
+ logger.info(f"使用する Gemini モデル: {model_name}")
239
+
240
+ if model_name == "gemini-2.5-flash-preview-04-17":
241
+ # 新ライブラリ(genai_new)を使用し thinkingBudget=0 を設定
242
+ client = genai_new.Client(api_key=api_key)
243
+ logger.info("新ラリ genai_new 使用 (thinkingBudget=0)")
244
+ cfg = types.GenerateContentConfig(
245
+ thinking_config=types.ThinkingConfig(thinking_budget=0)
246
+ )
247
+ response = client.models.generate_content(
248
+ model=model_name,
249
+ contents=text,
250
+ config=cfg
251
+ )
252
+ raw = response.text
253
+ else:
254
+ # 既存ライブラリ(genai_old)のまま
255
+ genai_old.configure(api_key=api_key)
256
+ system_instruction = load_system_instruction(style)
257
+ prompt = f"{system_instruction}\n\n{text}"
258
+ response = genai_old.GenerativeModel(model_name).generate_content(
259
+ prompt,
260
+ generation_config={
261
+ "temperature": temperature,
262
+ "top_p": 0.7,
263
+ "top_k": 20,
264
+ "max_output_tokens": 8192,
265
+ "candidate_count": 1
266
+ },
267
+ safety_settings=[
268
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
269
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
270
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","threshold": "BLOCK_MEDIUM_AND_ABOVE"},
271
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT","threshold": "BLOCK_MEDIUM_AND_ABOVE"},
272
+ ]
273
+ )
274
+ raw = response.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
+ # Markdown ```html``` 部分を取り出す
277
+ html_start = raw.find("```html")
278
+ html_end = raw.rfind("```")
279
  if html_start != -1 and html_end != -1 and html_start < html_end:
280
+ html_code = raw[html_start + 7:html_end].strip()
 
 
 
 
 
 
 
 
281
  else:
282
+ html_code = raw
283
+
284
+ # Font Awesome レイアウト最適化
285
+ html_code = enhance_font_awesome_layout(html_code)
286
+ logger.info("Font Awesome レイアウトの最適化を適用しました")
287
+ return html_code
288
 
289
  except Exception as e:
290
+ logger.error(f"HTML生成中にエラー: {e}", exc_info=True)
291
+ raise Exception(f"Gemini API での HTML 生成に失敗しました: {e}")
292
 
293
+ # --- 画像トリミング ---
294
  def trim_image_whitespace(image, threshold=250, padding=10):
 
 
 
 
 
 
 
 
 
 
 
295
  try:
 
296
  gray = image.convert('L')
 
 
297
  np_image = np.array(gray)
 
 
298
  mask = np_image < threshold
 
 
299
  rows = np.any(mask, axis=1)
300
  cols = np.any(mask, axis=0)
 
 
301
  if np.any(rows) and np.any(cols):
302
  row_indices = np.where(rows)[0]
303
  col_indices = np.where(cols)[0]
 
 
304
  min_y, max_y = row_indices[0], row_indices[-1]
305
  min_x, max_x = col_indices[0], col_indices[-1]
 
 
306
  min_x = max(0, min_x - padding)
307
  min_y = max(0, min_y - padding)
308
  max_x = min(image.width - 1, max_x + padding)
309
  max_y = min(image.height - 1, max_y + padding)
 
 
310
  trimmed = image.crop((min_x, min_y, max_x + 1, max_y + 1))
311
+ logger.info(f"画像をトリミングしました: {image.width}x{image.height} → {trimmed.width}x{trimmed.height}")
 
312
  return trimmed
 
313
  logger.warning("トリミング領域が見つかりません。元の画像を返します。")
314
  return image
 
315
  except Exception as e:
316
  logger.error(f"画像トリミング中にエラー: {e}", exc_info=True)
317
+ return image
318
 
319
+ # --- スクリーンショット生成 ---
320
  def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0,
321
  trim_whitespace: bool = True, driver=None) -> Image.Image:
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  tmp_path = None
323
  driver_from_pool = False
 
 
324
  if driver is None:
325
  driver = driver_pool.get_driver()
326
  driver_from_pool = True
327
  logger.info("WebDriverプールからドライバーを取得しました")
328
 
 
329
  try:
330
+ # HTML を一時ファイルに保存
331
  with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
332
  tmp_path = tmp_file.name
333
  tmp_file.write(html_code)
334
  logger.info(f"HTML saved to temporary file: {tmp_path}")
 
 
 
 
 
335
 
336
+ # ウィンドウ初期サイズ設定
 
337
  initial_width = 1200
338
  initial_height = 1000
339
  driver.set_window_size(initial_width, initial_height)
340
+ driver.get("file://" + tmp_path)
 
 
341
 
342
+ # body 要素の読み込み待機
 
343
  WebDriverWait(driver, 10).until(
344
  EC.presence_of_element_located((By.TAG_NAME, "body"))
345
  )
 
346
 
347
+ # リソース読み込み待機
348
+ max_wait = 5
349
+ wait_increment = 0.2
350
  wait_time = 0
 
351
  while wait_time < max_wait:
352
+ state = driver.execute_script("""
353
  return {
354
  complete: document.readyState === 'complete',
355
  imgCount: document.images.length,
 
357
  faElements: document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]').length
358
  };
359
  """)
360
+ if state['complete'] and (state['imgCount'] == 0 or state['imgLoaded'] == state['imgCount']):
 
 
 
 
361
  break
 
362
  time.sleep(wait_increment)
363
  wait_time += wait_increment
364
+
365
+ # Font Awesome 要素��多い場合は少し待機
366
+ if state.get('faElements', 0) > 30:
367
+ time.sleep(min(1.0, state['faElements'] / 100))
368
+
369
+ # スクロール処理
 
 
 
 
370
  total_height = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);")
371
  viewport_height = driver.execute_script("return window.innerHeight;")
372
+ scrolls_needed = max(1, min(5, total_height // viewport_height))
 
 
373
  for i in range(scrolls_needed):
374
+ scroll_pos = i * (viewport_height - 100)
375
  driver.execute_script(f"window.scrollTo(0, {scroll_pos});")
376
+ time.sleep(0.1)
 
 
377
  driver.execute_script("window.scrollTo(0, 0);")
378
+ time.sleep(0.2)
 
379
 
380
+ # スクロールバー非表示
381
  driver.execute_script("""
382
  document.documentElement.style.overflow = 'hidden';
383
  document.body.style.overflow = 'hidden';
384
  """)
385
+
386
+ # ページ寸法取得
387
+ dims = driver.execute_script("""
388
  return {
389
  width: Math.max(
390
  document.documentElement.scrollWidth,
 
404
  )
405
  };
406
  """)
407
+ scroll_width = max(dims['width'], 100)
408
+ scroll_height = max(dims['height'], 100)
 
 
 
 
 
409
  scroll_width = min(scroll_width, 2000)
410
  scroll_height = min(scroll_height, 4000)
 
 
 
411
 
412
+ # 余白追加
413
  adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
414
  adjusted_height = max(adjusted_height, scroll_height, 100)
415
+ driver.set_window_size(scroll_width, adjusted_height)
416
+ time.sleep(0.5)
 
 
 
 
417
 
418
  # スクリーンショット取得
 
419
  png = driver.get_screenshot_as_png()
 
 
 
420
  img = Image.open(BytesIO(png))
421
  logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
422
 
423
+ # 余白トリミング
424
  if trim_whitespace:
425
  img = trim_image_whitespace(img, threshold=248, padding=20)
426
  logger.info(f"Trimmed dimensions: {img.width}x{img.height}")
 
429
 
430
  except Exception as e:
431
  logger.error(f"Error during screenshot generation: {e}", exc_info=True)
 
432
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
433
+
434
  finally:
 
 
435
  if driver_from_pool:
436
  driver_pool.release_driver(driver)
 
 
437
  if tmp_path and os.path.exists(tmp_path):
438
  try:
439
  os.remove(tmp_path)
 
441
  except Exception as e:
442
  logger.error(f"Error removing temporary file {tmp_path}: {e}")
443
 
444
+ # --- 並列処理版スクリーンショット生成 ---
445
  def text_to_screenshot_parallel(text: str, extension_percentage: float, temperature: float = 0.5,
446
+ trim_whitespace: bool = True, style: str = "standard") -> Image.Image:
 
447
  start_time = time.time()
448
+ drv = None
449
+ tmp_path = None
450
+ driver_from_pool = False
451
  try:
 
452
  with ThreadPoolExecutor(max_workers=2) as executor:
453
+ html_future = executor.submit(generate_html_from_text, text, temperature, style)
 
 
 
 
 
 
 
 
454
  driver_future = executor.submit(driver_pool.get_driver)
 
 
455
  html_code = html_future.result()
456
+ drv = driver_future.result()
457
+ driver_from_pool = True
458
+
459
+ # HTML→一時ファイル
460
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
461
+ tmp_path = tmp_file.name
462
+ tmp_file.write(html_code)
463
+ logger.info(f"HTMLを一時ファイルに保存: {tmp_path}")
464
+
465
+ # ドライバ初期化
466
+ drv.set_window_size(1200, 1000)
467
+ drv.get("file://" + tmp_path)
468
+ WebDriverWait(drv, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
469
+
470
+ # リソース待機
471
+ max_wait = 3
472
+ wait_increment = 0.2
473
+ wait_time = 0
474
+ while wait_time < max_wait:
475
+ state = drv.execute_script("""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  return {
477
+ complete: document.readyState==='complete',
478
+ imgCount: document.images.length,
479
+ imgLoaded: Array.from(document.images).filter(img=>img.complete).length,
480
+ faElements: document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]').length
 
 
 
 
 
 
 
 
 
 
 
 
481
  };
482
  """)
483
+ if state['complete'] and (state['imgCount']==0 or state['imgLoaded']==state['imgCount']):
484
+ break
485
+ time.sleep(wait_increment)
486
+ wait_time += wait_increment
487
+
488
+ if state.get('faElements', 0) > 30:
489
+ time.sleep(min(1.0, state['faElements'] / 100))
490
+
491
+ # 簡易スクロール
492
+ drv.execute_script("window.scrollTo(0, document.body.scrollHeight);")
493
+ time.sleep(0.2)
494
+ drv.execute_script("window.scrollTo(0, 0);")
495
+ time.sleep(0.2)
496
+ drv.execute_script("document.documentElement.style.overflow='hidden';document.body.style.overflow='hidden';")
497
+
498
+ # 寸法取得
499
+ dims = drv.execute_script("""
500
+ return {
501
+ width: Math.max(
502
+ document.documentElement.scrollWidth,
503
+ document.documentElement.offsetWidth,
504
+ document.documentElement.clientWidth,
505
+ document.body ? document.body.scrollWidth : 0,
506
+ document.body ? document.body.offsetWidth : 0,
507
+ document.body ? document.body.clientWidth : 0
508
+ ),
509
+ height: Math.max(
510
+ document.documentElement.scrollHeight,
511
+ document.documentElement.offsetHeight,
512
+ document.documentElement.clientHeight,
513
+ document.body ? document.body.scrollHeight : 0,
514
+ document.body ? document.body.offsetHeight : 0,
515
+ document.body ? document.body.clientHeight : 0
516
+ )
517
+ };
518
+ """)
519
+ w = max(dims['width'], 100)
520
+ h = max(dims['height'], 100)
521
+ w = min(w, 2000)
522
+ h = min(h, 4000)
523
+ adjusted_h = int(h * (1 + extension_percentage / 100.0))
524
+ adjusted_h = max(adjusted_h, h, 100)
525
+
526
+ drv.set_window_size(w, adjusted_h)
527
+ time.sleep(0.2)
528
+
529
+ # スクリーンショット取得
530
+ png = drv.get_screenshot_as_png()
531
+ img = Image.open(BytesIO(png))
532
+ if trim_whitespace:
533
+ img = trim_image_whitespace(img, threshold=248, padding=20)
534
+
535
+ elapsed = time.time() - start_time
536
+ logger.info(f"Parallel generation 完了 (所要時間: {elapsed:.2f}秒)")
537
+ return img
538
+
539
  except Exception as e:
540
+ logger.error(f"Parallel generation error: {e}", exc_info=True)
541
+ return Image.new('RGB', (1, 1), color=(0, 0, 0))
542
+
543
+ finally:
544
+ if driver_from_pool and drv:
545
+ driver_pool.release_driver(drv)
546
+ if tmp_path and os.path.exists(tmp_path):
547
+ try:
548
+ os.remove(tmp_path)
549
+ except Exception:
550
+ pass
551
 
552
+ # --- レガシースクリーンショット生成 ---
553
  def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 0.3,
554
+ trim_whitespace: bool = True, style: str = "standard") -> Image.Image:
 
 
555
  return text_to_screenshot_parallel(text, extension_percentage, temperature, trim_whitespace, style)
556
 
557
+ # --- 入力モード切り替え用関数 ---
558
+ def process_input(input_mode, input_text, extension_percentage, temperature, trim_whitespace, style):
559
+ if input_mode == "HTML入力":
560
+ return render_fullpage_screenshot(input_text, extension_percentage, trim_whitespace)
561
+ else:
562
+ return text_to_screenshot_parallel(input_text, extension_percentage, temperature, trim_whitespace, style)
563
+
564
  # --- FastAPI Setup ---
565
  app = FastAPI()
 
 
566
  app.add_middleware(
567
  CORSMiddleware,
568
  allow_origins=["*"],
 
572
  )
573
 
574
  # 静的ファイルのサービング設定
 
575
  gradio_dir = os.path.dirname(gr.__file__)
 
 
 
 
576
  static_dir = os.path.join(gradio_dir, "templates", "frontend", "static")
577
  if os.path.exists(static_dir):
 
578
  app.mount("/static", StaticFiles(directory=static_dir), name="static")
 
 
579
  app_dir = os.path.join(gradio_dir, "templates", "frontend", "_app")
580
  if os.path.exists(app_dir):
 
581
  app.mount("/_app", StaticFiles(directory=app_dir), name="_app")
 
 
582
  assets_dir = os.path.join(gradio_dir, "templates", "frontend", "assets")
583
  if os.path.exists(assets_dir):
 
584
  app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
 
 
585
  cdn_dir = os.path.join(gradio_dir, "templates", "cdn")
586
  if os.path.exists(cdn_dir):
 
587
  app.mount("/cdn", StaticFiles(directory=cdn_dir), name="cdn")
588
 
589
+ # --- API Endpoint for HTML→Screenshot ---
 
590
  @app.post("/api/screenshot",
591
  response_class=StreamingResponse,
592
  tags=["Screenshot"],
593
  summary="Render HTML to Full Page Screenshot",
594
  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.")
595
  async def api_render_screenshot(request: ScreenshotRequest):
 
 
 
596
  try:
597
  logger.info(f"API request received. Extension: {request.extension_percentage}%")
 
598
  pil_image = render_fullpage_screenshot(
599
  request.html_code,
600
  request.extension_percentage,
601
  request.trim_whitespace
602
  )
 
603
  if pil_image.size == (1, 1):
604
+ logger.error("Screenshot generation failed, returning 1x1 error image.")
 
 
 
 
605
  img_byte_arr = BytesIO()
606
  pil_image.save(img_byte_arr, format='PNG')
607
+ img_byte_arr.seek(0)
 
608
  logger.info("Returning screenshot as PNG stream.")
609
  return StreamingResponse(img_byte_arr, media_type="image/png")
 
610
  except Exception as e:
611
  logger.error(f"API Error: {e}", exc_info=True)
612
  raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
613
 
614
+ # --- API Endpoint for Text→Infographic Screenshot ---
615
  @app.post("/api/text-to-screenshot",
616
  response_class=StreamingResponse,
617
  tags=["Screenshot", "Gemini"],
618
  summary="テキストからインフォグラフィックを生成",
619
  description="テキストをGemini APIを使ってHTMLインフォグラフィックに変換し、スクリーンショットとして返します。")
620
  async def api_text_to_screenshot(request: GeminiRequest):
 
 
 
621
  try:
622
+ logger.info(
623
+ f"テキスト→スクリーンショットAPIリクエスト受信。"
624
+ f"テキト長さ: {len(request.text)}, 拡張率: {request.extension_percentage}%, "
625
+ f"温度: {request.temperature}, スタイル: {request.style}"
626
+ )
627
  pil_image = text_to_screenshot_parallel(
628
  request.text,
629
  request.extension_percentage,
 
631
  request.trim_whitespace,
632
  request.style
633
  )
 
634
  if pil_image.size == (1, 1):
635
  logger.error("スクリーンショット生成に失敗しました。1x1エラー画像を返します。")
 
 
636
  img_byte_arr = BytesIO()
637
  pil_image.save(img_byte_arr, format='PNG')
638
+ img_byte_arr.seek(0)
 
639
  logger.info("スクリーンショットをPNGストリームとして返します。")
640
  return StreamingResponse(img_byte_arr, media_type="image/png")
 
641
  except Exception as e:
642
  logger.error(f"API Error: {e}", exc_info=True)
643
  raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
644
 
645
  # --- Gradio Interface Definition ---
 
 
 
 
 
 
 
 
 
 
 
646
  with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
647
  gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
648
  gr.Markdown("HTMLコードをレンダリングするか、テキストをGemini APIでインフォグラフィックに変換して画像として取得します。")
 
655
  value="HTML入力"
656
  )
657
 
 
658
  input_text = gr.Textbox(
659
  lines=15,
660
  label="入力",
 
663
 
664
  with gr.Row():
665
  with gr.Column(scale=1):
 
666
  style_dropdown = gr.Dropdown(
667
  choices=["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"],
668
  value="standard",
669
  label="デザインスタイル",
670
  info="テキスト→HTML変換時のデザインテーマを選択します",
671
+ visible=False
672
  )
 
673
  with gr.Column(scale=2):
674
  extension_percentage = gr.Slider(
675
  minimum=0,
676
  maximum=30,
677
  step=1.0,
678
+ value=10,
679
  label="上下高さ拡張率(%)"
680
  )
 
 
681
  temperature = gr.Slider(
682
  minimum=0.0,
683
  maximum=1.0,
684
  step=0.1,
685
+ value=0.5,
686
  label="生成時の温度(低い=一貫性高、高い=創造性高)",
687
+ visible=False
688
  )
689
 
 
690
  trim_whitespace = gr.Checkbox(
691
  label="余白を自動トリミング",
692
  value=True,
 
696
  submit_btn = gr.Button("生成")
697
  output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
698
 
 
699
  def update_controls_visibility(mode):
700
+ is_text_mode = (mode == "テキス入力")
 
701
  return [
702
  gr.update(visible=is_text_mode), # temperature
703
  gr.update(visible=is_text_mode), # style_dropdown
 
709
  outputs=[temperature, style_dropdown]
710
  )
711
 
 
712
  submit_btn.click(
713
  fn=process_input,
714
  inputs=[input_mode, input_text, extension_percentage, temperature, trim_whitespace, style_dropdown],
715
  outputs=output_image
716
  )
717
 
 
718
  gemini_model = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
719
  gr.Markdown(f"""
720
  ## APIエンドポイント
 
730
  # --- Mount Gradio App onto FastAPI ---
731
  app = gr.mount_gradio_app(app, iface, path="/")
732
 
733
+ # --- ローカル開発用 Uvicorn 起動 ---
734
  if __name__ == "__main__":
735
  import uvicorn
736
  logger.info("Starting Uvicorn server for local development...")
 
738
 
739
  # アプリケーション終了時にWebDriverプールをクリーンアップ
740
  import atexit
741
+ atexit.register(driver_pool.close_all)