tomo2chin2 commited on
Commit
f39c339
·
verified ·
1 Parent(s): e318d35

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +547 -272
app.py CHANGED
@@ -1,33 +1,28 @@
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,6 +56,7 @@ class WebDriverPool:
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,6 +66,7 @@ class WebDriverPool:
70
  logger.info("デフォルトのChromeDriverを使用")
71
  return webdriver.Chrome(options=options)
72
 
 
73
  logger.info("WebDriverプールがいっぱいです。利用可能なドライバーを待機中...")
74
  return self.driver_queue.get()
75
 
@@ -77,6 +74,7 @@ class WebDriverPool:
77
  """ドライバーをプールに戻す"""
78
  if driver:
79
  try:
 
80
  driver.get("about:blank")
81
  driver.execute_script("""
82
  document.documentElement.style.overflow = '';
@@ -109,70 +107,90 @@ class WebDriverPool:
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,175 +199,282 @@ def enhance_font_awesome_layout(html_code):
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,34 +482,48 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
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,23 +543,39 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
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,11 +584,15 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
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,128 +600,188 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
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,58 +791,88 @@ app.add_middleware(
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,18 +880,34 @@ async def api_text_to_screenshot(request: GeminiRequest):
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,6 +920,7 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
655
  value="HTML入力"
656
  )
657
 
 
658
  input_text = gr.Textbox(
659
  lines=15,
660
  label="入力",
@@ -663,30 +929,35 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
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,8 +967,10 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
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,12 +982,14 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
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,7 +1005,7 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
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,4 +1013,4 @@ if __name__ == "__main__":
738
 
739
  # アプリケーション終了時にWebDriverプールをクリーンアップ
740
  import atexit
741
- atexit.register(driver_pool.close_all)
 
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
  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
  logger.info("デフォルトのChromeDriverを使用")
67
  return webdriver.Chrome(options=options)
68
 
69
+ # 最大数に達した場合は待機
70
  logger.info("WebDriverプールがいっぱいです。利用可能なドライバーを待機中...")
71
  return self.driver_queue.get()
72
 
 
74
  """ドライバーをプールに戻す"""
75
  if driver:
76
  try:
77
+ # ブラウザをリセット
78
  driver.get("about:blank")
79
  driver.execute_script("""
80
  document.documentElement.style.overflow = '';
 
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
  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
  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
  )
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
 
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
  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
  )
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
  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
  value="HTML入力"
921
  )
922
 
923
+ # 共用のテキストボックス
924
  input_text = gr.Textbox(
925
  lines=15,
926
  label="入力",
 
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
  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
  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
  # --- 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
 
1014
  # アプリケーション終了時にWebDriverプールをクリーンアップ
1015
  import atexit
1016
+ atexit.register(driver_pool.close_all)