leave-everything commited on
Commit
736bda3
·
verified ·
1 Parent(s): 9b488df

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +7 -7
  2. app.py +1141 -0
  3. gitattributes.txt +35 -0
  4. packages.txt +2 -0
  5. requirements.txt +13 -0
README.md CHANGED
@@ -1,12 +1,12 @@
1
  ---
2
- title: HTMLviewer3 API DATASET URL
3
- emoji: 📚
4
- colorFrom: green
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.49.1
8
  app_file: app.py
9
- pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: HTMLviewer3_API_DATASET_URL
3
+ emoji: 🏢
4
+ colorFrom: indigo
5
+ colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 4.19.2
8
  app_file: app.py
9
+ pinned: true
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,1141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from fastapi import FastAPI, HTTPException, Body
3
+ from fastapi.responses import StreamingResponse, JSONResponse
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
21
+ import uuid
22
+ from datetime import datetime
23
+ from concurrent.futures import ThreadPoolExecutor
24
+ from huggingface_hub import hf_hub_download, upload_file, login
25
+
26
+ # 正しいGemini関連のインポート
27
+ import google.generativeai as genai
28
+
29
+ # ロギング設定
30
+ logging.basicConfig(level=logging.INFO)
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # --- HuggingFace Hub アップロード機能 ---
34
+ class HuggingFaceUploader:
35
+ """HuggingFace Hubへ画像をアップロードする機能を提供するクラス"""
36
+ def __init__(self):
37
+ self.repo_id = os.environ.get("HF_REPO_ID", "tomo2chin2/SUPER_TENSAI_JIN")
38
+ self.token = os.environ.get("HF_TOKEN", None)
39
+ if self.token:
40
+ try:
41
+ login(token=self.token)
42
+ logger.info(f"HuggingFace Hubにログインしました。リポジトリ: {self.repo_id}")
43
+ except Exception as e:
44
+ logger.error(f"HuggingFace Hubへのログインに失敗: {e}")
45
+ else:
46
+ logger.warning("HF_TOKEN環境変数が設定されていません。アップロード機能は制限されます。")
47
+
48
+ def upload_image(self, image, prefix="generated"):
49
+ """
50
+ PIL Imageをアップロードし、アクセス可能なURLを返す
51
+
52
+ Args:
53
+ image: PIL.Image - アップロードする画像
54
+ prefix: str - ファイル名のプレフィックス
55
+
56
+ Returns:
57
+ str - アップロードされた画像のURL
58
+ """
59
+ try:
60
+ if not self.token:
61
+ logger.error("HF_TOKENが設定されていないため、アップロードできません")
62
+ return None
63
+
64
+ # ユニークなファイル名を生成 - JPEGフォーマットを使用
65
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
66
+ unique_id = str(uuid.uuid4())[:8]
67
+ filename = f"{prefix}_{timestamp}_{unique_id}.jpg"
68
+ path_in_repo = f"images/{filename}"
69
+
70
+ # 一時ファイルに保存 - JPEGフォーマットで保存
71
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_file:
72
+ tmp_path = tmp_file.name
73
+ image.save(tmp_path, format="JPEG", quality=95) # 高品質JPEGとして保存
74
+
75
+ logger.info(f"画像を一時ファイルに保存: {tmp_path}")
76
+
77
+ # HuggingFaceにアップロード
78
+ logger.info(f"HuggingFace Hubにアップロード中: {path_in_repo}")
79
+ upload_info = upload_file(
80
+ path_or_fileobj=tmp_path,
81
+ path_in_repo=path_in_repo,
82
+ repo_id=self.repo_id,
83
+ repo_type="dataset"
84
+ )
85
+
86
+ # 一時ファイルを削除
87
+ try:
88
+ os.remove(tmp_path)
89
+ except Exception as e:
90
+ logger.warning(f"一時ファイル削除エラー: {e}")
91
+
92
+ # URLを構築して返す - データセットリポジトリの場合は datasets/ を含める
93
+ url = f"https://huggingface.co/datasets/{self.repo_id}/resolve/main/{path_in_repo}"
94
+ logger.info(f"アップロード成功: {url}")
95
+ return url
96
+
97
+ except Exception as e:
98
+ logger.error(f"HuggingFace Hubへのアップロード中にエラー: {e}", exc_info=True)
99
+ return None
100
+
101
+ # グローバルなアップローダーインスタンスを作成
102
+ hf_uploader = HuggingFaceUploader()
103
+
104
+ # --- WebDriverプールの実装 ---
105
+ class WebDriverPool:
106
+ """WebDriverインスタンスを再利用するためのプール"""
107
+ def __init__(self, max_drivers=3):
108
+ self.driver_queue = queue.Queue()
109
+ self.max_drivers = max_drivers
110
+ self.lock = threading.Lock()
111
+ self.count = 0
112
+ logger.info(f"WebDriverプールを初期化: 最大 {max_drivers} ドライバー")
113
+
114
+ def get_driver(self):
115
+ """プールからWebDriverを取得、なければ新規作成"""
116
+ if not self.driver_queue.empty():
117
+ logger.info("既存のWebDriverをプールから取得")
118
+ return self.driver_queue.get()
119
+
120
+ with self.lock:
121
+ if self.count < self.max_drivers:
122
+ self.count += 1
123
+ logger.info(f"新しいWebDriverを作成 (合計: {self.count}/{self.max_drivers})")
124
+ options = Options()
125
+ options.add_argument("--headless")
126
+ options.add_argument("--no-sandbox")
127
+ options.add_argument("--disable-dev-shm-usage")
128
+ options.add_argument("--force-device-scale-factor=1")
129
+ options.add_argument("--disable-features=NetworkService")
130
+ options.add_argument("--dns-prefetch-disable")
131
+
132
+ # 環境変数からWebDriverパスを取得(任意)
133
+ webdriver_path = os.environ.get("CHROMEDRIVER_PATH")
134
+ if webdriver_path and os.path.exists(webdriver_path):
135
+ logger.info(f"CHROMEDRIVER_PATH使用: {webdriver_path}")
136
+ service = webdriver.ChromeService(executable_path=webdriver_path)
137
+ return webdriver.Chrome(service=service, options=options)
138
+ else:
139
+ logger.info("デフォルトのChromeDriverを使用")
140
+ return webdriver.Chrome(options=options)
141
+
142
+ # 最大数に達した場合は待機
143
+ logger.info("WebDriverプールがいっぱいです。利用可能なドライバーを待機中...")
144
+ return self.driver_queue.get()
145
+
146
+ def release_driver(self, driver):
147
+ """ドライバーをプールに戻す"""
148
+ if driver:
149
+ try:
150
+ # ブラウザをリセット
151
+ driver.get("about:blank")
152
+ driver.execute_script("""
153
+ document.documentElement.style.overflow = '';
154
+ document.body.style.overflow = '';
155
+ """)
156
+ self.driver_queue.put(driver)
157
+ logger.info("WebDriverをプールに戻しました")
158
+ except Exception as e:
159
+ logger.error(f"ドライバーをプールに戻す際にエラー: {e}")
160
+ driver.quit()
161
+ with self.lock:
162
+ self.count -= 1
163
+
164
+ def close_all(self):
165
+ """全てのドライバーを終了"""
166
+ logger.info("WebDriverプールを終了します")
167
+ closed = 0
168
+ while not self.driver_queue.empty():
169
+ try:
170
+ driver = self.driver_queue.get(block=False)
171
+ driver.quit()
172
+ closed += 1
173
+ except queue.Empty:
174
+ break
175
+ except Exception as e:
176
+ logger.error(f"ドライバー終了中にエラー: {e}")
177
+
178
+ logger.info(f"{closed}個のWebDriverを終了しました")
179
+ with self.lock:
180
+ self.count = 0
181
+
182
+ # グローバルなWebDriverプールを作成
183
+ # サーバー環境のリソースに合わせて調整
184
+ driver_pool = WebDriverPool(max_drivers=int(os.environ.get("MAX_WEBDRIVERS", "3")))
185
+
186
+ # --- Gemini統合 ---
187
+ class GeminiRequest(BaseModel):
188
+ """Geminiへのリクエストデータモデル"""
189
+ text: str
190
+ extension_percentage: float = 15.0 # デフォルト値15%
191
+ temperature: float = 1.0 # デフォルト値を1.0に設定
192
+ trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
193
+ style: str = "standard" # デフォルトはstandard
194
+
195
+ class ScreenshotRequest(BaseModel):
196
+ """スクリーンショットリクエストモデル"""
197
+ html_code: str
198
+ extension_percentage: float = 15.0 # デフォルト値15%
199
+ trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
200
+ style: str = "standard" # デフォルトはstandard
201
+
202
+ # --- レスポンスモデル ---
203
+ class ImageUrlResponse(BaseModel):
204
+ """画像URLのレスポンスモデル"""
205
+ url: str
206
+ status: str = "success"
207
+ message: str = "画像が正常に生成されました"
208
+
209
+ # HTMLのFont Awesomeレイアウトを改善する関数 - プリロード機能を追加
210
+ def enhance_font_awesome_layout(html_code):
211
+ """Font Awesomeレイアウトを改善し、プリロードタグを追加"""
212
+ # Font Awesomeリソースのプリロード - パフォーマンス向上
213
+ fa_preload = """
214
+ <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>
215
+ <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>
216
+ <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>
217
+ """
218
+
219
+ # CSSを追加
220
+ fa_fix_css = """
221
+ <style>
222
+ /* Font Awesomeアイコンのレイアウト修正 */
223
+ [class*="fa-"] {
224
+ display: inline-block !important;
225
+ margin-right: 8px !important;
226
+ vertical-align: middle !important;
227
+ }
228
+
229
+ /* テキ���ト内のアイコン位置調整 */
230
+ h1 [class*="fa-"], h2 [class*="fa-"], h3 [class*="fa-"],
231
+ h4 [class*="fa-"], h5 [class*="fa-"], h6 [class*="fa-"] {
232
+ vertical-align: middle !important;
233
+ margin-right: 10px !important;
234
+ }
235
+
236
+ /* 特定パターンの修正 */
237
+ .fa + span, .fas + span, .far + span, .fab + span,
238
+ span + .fa, span + .fas, span + .far + span {
239
+ display: inline-block !important;
240
+ margin-left: 5px !important;
241
+ }
242
+
243
+ /* カード内アイコン修正 */
244
+ .card [class*="fa-"], .card-body [class*="fa-"] {
245
+ float: none !important;
246
+ clear: none !important;
247
+ position: relative !important;
248
+ }
249
+
250
+ /* アイコンと文字が重なる場合の調整 */
251
+ li [class*="fa-"], p [class*="fa-"] {
252
+ margin-right: 10px !important;
253
+ }
254
+
255
+ /* インラインアイコンのスペーシング */
256
+ .inline-icon {
257
+ display: inline-flex !important;
258
+ align-items: center !important;
259
+ justify-content: flex-start !important;
260
+ }
261
+
262
+ /* アイコン後のテキスト */
263
+ [class*="fa-"] + span {
264
+ display: inline-block !important;
265
+ vertical-align: middle !important;
266
+ }
267
+ </style>
268
+ """
269
+
270
+ # headタグがある場合はその中に追加
271
+ if '<head>' in html_code:
272
+ return html_code.replace('</head>', f'{fa_preload}{fa_fix_css}</head>')
273
+ # HTMLタグがある場合はその後に追加
274
+ elif '<html' in html_code:
275
+ head_end = html_code.find('</head>')
276
+ if head_end > 0:
277
+ return html_code[:head_end] + fa_preload + fa_fix_css + html_code[head_end:]
278
+ else:
279
+ body_start = html_code.find('<body')
280
+ if body_start > 0:
281
+ return html_code[:body_start] + f'<head>{fa_preload}{fa_fix_css}</head>' + html_code[body_start:]
282
+
283
+ # どちらもない場合は先頭に追加
284
+ return f'<html><head>{fa_preload}{fa_fix_css}</head>' + html_code + '</html>'
285
+
286
+ def load_system_instruction(style="standard"):
287
+ """
288
+ 指定されたスタイルのシステムインストラクションを読み込む
289
+
290
+ Args:
291
+ style: 使用するスタイル名 (standard, cute, resort, cool, dental, school)
292
+
293
+ Returns:
294
+ 読み込まれたシステムインストラクション
295
+ """
296
+ try:
297
+ # 有効なスタイル一覧
298
+ valid_styles = ["standard", "cute", "resort", "cool", "dental", "school","KOKUGO"]
299
+
300
+ # スタイルの検証
301
+ if style not in valid_styles:
302
+ logger.warning(f"無効なスタイル '{style}' が指定されました。デフォルトの 'standard' を使用します。")
303
+ style = "standard"
304
+
305
+ logger.info(f"スタイル '{style}' のシステムインストラクションを読み込みます")
306
+
307
+ # まず、ローカルのスタイルディレクトリ内のprompt.txtを確認
308
+ local_path = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
309
+
310
+ # ローカルファイルが存在する場合はそれを使用
311
+ if os.path.exists(local_path):
312
+ logger.info(f"ローカルファイルを使用: {local_path}")
313
+ with open(local_path, 'r', encoding='utf-8') as file:
314
+ instruction = file.read()
315
+ return instruction
316
+
317
+ # HuggingFaceリポジトリからのファイル読み込みを試行
318
+ try:
319
+ # スタイル固有のファイルパスを指定
320
+ file_path = hf_hub_download(
321
+ repo_id="tomo2chin2/GURAREKOstlyle",
322
+ filename=f"{style}/prompt.txt",
323
+ repo_type="dataset"
324
+ )
325
+
326
+ logger.info(f"スタイル '{style}' のプロンプトをHuggingFaceから読み込みました: {file_path}")
327
+ with open(file_path, 'r', encoding='utf-8') as file:
328
+ instruction = file.read()
329
+ return instruction
330
+
331
+ except Exception as style_error:
332
+ # スタイル固有ファイルの読み込みに失敗した場合、デフォルトのprompt.txtを使用
333
+ logger.warning(f"スタイル '{style}' のプロンプト読み込みに失敗: {str(style_error)}")
334
+ logger.info("デフォルトのprompt.txtを読み込みます")
335
+
336
+ file_path = hf_hub_download(
337
+ repo_id="tomo2chin2/GURAREKOstlyle",
338
+ filename="prompt.txt",
339
+ repo_type="dataset"
340
+ )
341
+
342
+ with open(file_path, 'r', encoding='utf-8') as file:
343
+ instruction = file.read()
344
+
345
+ logger.info("デフォルトのシステムインストラクションを読み込みました")
346
+ return instruction
347
+
348
+ except Exception as e:
349
+ error_msg = f"システムインストラクションの読み込みに失敗: {str(e)}"
350
+ logger.error(error_msg)
351
+ raise ValueError(error_msg)
352
+
353
+ def generate_html_from_text(text, temperature=0.3, style="standard"):
354
+ """���キストからHTMLを生成する"""
355
+ try:
356
+ # APIキーの取得と設定
357
+ api_key = os.environ.get("GEMINI_API_KEY")
358
+ if not api_key:
359
+ logger.error("GEMINI_API_KEY 環境変数が設定されていません")
360
+ raise ValueError("GEMINI_API_KEY 環境変数が設定されていません")
361
+
362
+ # モデル名の取得(環境変数から、なければデフォルト値)
363
+ model_name = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
364
+ logger.info(f"使用するGeminiモデル: {model_name}")
365
+
366
+ # Gemini APIの設定
367
+ genai.configure(api_key=api_key)
368
+
369
+ # 指定されたスタイルのシステムインストラクションを読み込む
370
+ system_instruction = load_system_instruction(style)
371
+
372
+ # モデル初期化
373
+ logger.info(f"Gemini APIにリクエストを送信: テキスト長さ = {len(text)}, 温度 = {temperature}, スタイル = {style}")
374
+
375
+ # モデル初期化
376
+ model = genai.GenerativeModel(model_name)
377
+
378
+ # 生成設定 - ばらつきを減らすために設定を調整
379
+ generation_config = {
380
+ "temperature": temperature, # より低い温度を設定
381
+ "top_p": 0.7, # 0.95から0.7に下げて出力の多様性を制限
382
+ "top_k": 20, # 64から20に下げて候補を絞る
383
+ "max_output_tokens": 8192,
384
+ "candidate_count": 1 # 候補は1つだけ生成
385
+ }
386
+
387
+ # 安全設定 - デフォルトの安全設定を使用
388
+ safety_settings = [
389
+ {
390
+ "category": "HARM_CATEGORY_HARASSMENT",
391
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
392
+ },
393
+ {
394
+ "category": "HARM_CATEGORY_HATE_SPEECH",
395
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
396
+ },
397
+ {
398
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
399
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
400
+ },
401
+ {
402
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
403
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
404
+ }
405
+ ]
406
+
407
+ # プロンプト構築
408
+ prompt = f"{system_instruction}\n\n{text}"
409
+
410
+ # コンテンツ生成
411
+ response = model.generate_content(
412
+ prompt,
413
+ generation_config=generation_config,
414
+ safety_settings=safety_settings
415
+ )
416
+
417
+ # レスポンスからHTMLを抽出
418
+ raw_response = response.text
419
+
420
+ # HTMLタグ部分だけを抽出(```html と ``` の間)
421
+ html_start = raw_response.find("```html")
422
+ html_end = raw_response.rfind("```")
423
+
424
+ if html_start != -1 and html_end != -1 and html_start < html_end:
425
+ html_start += 7 # "```html" の長さ分進める
426
+ html_code = raw_response[html_start:html_end].strip()
427
+ logger.info(f"HTMLの生成に成功: 長さ = {len(html_code)}")
428
+
429
+ # Font Awesomeのレイアウト改善
430
+ html_code = enhance_font_awesome_layout(html_code)
431
+ logger.info("Font Awesomeレイアウトの最適化を適用しました")
432
+
433
+ return html_code
434
+ else:
435
+ # HTMLタグが見つからない場合、レスポンス全体を返す
436
+ logger.warning("レスポンスから ```html ``` タグが見つかりませんでした。全テキストを返します。")
437
+ return raw_response
438
+
439
+ except Exception as e:
440
+ logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
441
+ raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")
442
+
443
+ # 画像から余分な空白領域をトリミングする関数 - NumPyを使って最適化
444
+ def trim_image_whitespace(image, threshold=250, padding=10):
445
+ """
446
+ NumPyを使用して最適化された画像トリミング関数
447
+
448
+ Args:
449
+ image: PIL.Image - 入力画像
450
+ threshold: int - どの明るさ以上を空白と判断するか (0-255)
451
+ padding: int - トリミング後に残す余白のピクセル数
452
+
453
+ Returns:
454
+ トリミングされたPIL.Image
455
+ """
456
+ try:
457
+ # グレースケールに変換
458
+ gray = image.convert('L')
459
+
460
+ # NumPy配列として取得(高速処理のため)
461
+ np_image = np.array(gray)
462
+
463
+ # マスク作成(非白ピクセル)
464
+ mask = np_image < threshold
465
+
466
+ # マスクから行と列のインデックスを取得
467
+ rows = np.any(mask, axis=1)
468
+ cols = np.any(mask, axis=0)
469
+
470
+ # 非空のインデックス範囲を取得
471
+ if np.any(rows) and np.any(cols):
472
+ row_indices = np.where(rows)[0]
473
+ col_indices = np.where(cols)[0]
474
+
475
+ # 範囲取得
476
+ min_y, max_y = row_indices[0], row_indices[-1]
477
+ min_x, max_x = col_indices[0], col_indices[-1]
478
+
479
+ # パディング追加
480
+ min_x = max(0, min_x - padding)
481
+ min_y = max(0, min_y - padding)
482
+ max_x = min(image.width - 1, max_x + padding)
483
+ max_y = min(image.height - 1, max_y + padding)
484
+
485
+ # 画像をトリミング
486
+ trimmed = image.crop((min_x, min_y, max_x + 1, max_y + 1))
487
+
488
+ logger.info(f"画像をトリミングしました: 元サイズ {image.width}x{image.height} → トリミング後 {trimmed.width}x{trimmed.height}")
489
+ return trimmed
490
+
491
+ logger.warning("トリミング領域が見つかりません。元の画像を返します。")
492
+ return image
493
+
494
+ except Exception as e:
495
+ logger.error(f"画像トリミング中にエラー: {e}", exc_info=True)
496
+ return image # エラー時は元の画像を返す
497
+
498
+ # 最適化されたスクリーンショット生成関数 - 外部から初期化済みドライバーを受け取れるように
499
+ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0,
500
+ trim_whitespace: bool = True, driver=None) -> Image.Image:
501
+ """
502
+ Renders HTML code to a full-page screenshot using Selenium.
503
+ Optimized to accept an external driver or get one from the pool.
504
+
505
+ Args:
506
+ html_code: The HTML source code string.
507
+ extension_percentage: Percentage of extra space to add vertically.
508
+ trim_whitespace: Whether to trim excess whitespace from the image.
509
+ driver: An optional pre-initialized WebDriver instance.
510
+
511
+ Returns:
512
+ A PIL Image object of the screenshot.
513
+ """
514
+ tmp_path = None
515
+ driver_from_pool = False
516
+
517
+ # ドライバーがない場合はプールから取得
518
+ if driver is None:
519
+ driver = driver_pool.get_driver()
520
+ driver_from_pool = True
521
+ logger.info("WebDriverプールからドライバーを取得しました")
522
+
523
+ # 1) Save HTML code to a temporary file
524
+ try:
525
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
526
+ tmp_path = tmp_file.name
527
+ tmp_file.write(html_code)
528
+ logger.info(f"HTML saved to temporary file: {tmp_path}")
529
+ except Exception as e:
530
+ logger.error(f"Error writing temporary HTML file: {e}")
531
+ if driver_from_pool:
532
+ driver_pool.release_driver(driver)
533
+ return Image.new('RGB', (1, 1), color=(0, 0, 0))
534
+
535
+ try:
536
+ # ウィンドウサイズ初期設定
537
+ initial_width = 1200
538
+ initial_height = 1000
539
+ driver.set_window_size(initial_width, initial_height)
540
+ file_url = "file://" + tmp_path
541
+ logger.info(f"Navigating to {file_url}")
542
+ driver.get(file_url)
543
+
544
+ # ページ読み込み待機 - 動的な待機時間を実装
545
+ logger.info("Waiting for body element...")
546
+ WebDriverWait(driver, 10).until(
547
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
548
+ )
549
+ logger.info("Body element found. Waiting for resource loading...")
550
+
551
+ # リソース読み込みの動的待機 - 最適化
552
+ max_wait = 5 # 最大待機時間(秒)
553
+ wait_increment = 0.2 # 確認間隔
554
+ wait_time = 0
555
+
556
+ while wait_time < max_wait:
557
+ resource_state = driver.execute_script("""
558
+ return {
559
+ complete: document.readyState === 'complete',
560
+ imgCount: document.images.length,
561
+ imgLoaded: Array.from(document.images).filter(img => img.complete).length,
562
+ faElements: document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]').length
563
+ };
564
+ """)
565
+
566
+ # ドキュメント完了かつ画像が読み込まれている場合、待機終了
567
+ if resource_state['complete'] and (resource_state['imgCount'] == 0 or
568
+ resource_state['imgLoaded'] == resource_state['imgCount']):
569
+ logger.info(f"リソース読み込み完了: {resource_state}")
570
+ break
571
+
572
+ time.sleep(wait_increment)
573
+ wait_time += wait_increment
574
+ logger.info(f"リソース待機中... {wait_time:.1f}秒経過, 状態: {resource_state}")
575
+
576
+ # Font Awesome要素が多い場合は追加待機
577
+ fa_count = resource_state.get('faElements', 0)
578
+ if fa_count > 30:
579
+ logger.info(f"{fa_count}個のFont Awesome要素があるため、追加待機...")
580
+ time.sleep(min(1.0, fa_count / 100)) # 要素数に応じて待機(最大1秒)
581
+
582
+ # コンテンツレンダリングのためのスクロール処理 - パフォーマンス改善
583
+ logger.info("Performing content rendering scroll...")
584
+ total_height = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);")
585
+ viewport_height = driver.execute_script("return window.innerHeight;")
586
+ scrolls_needed = max(1, min(5, total_height // viewport_height)) # 最大5回までに制限
587
+
588
+ # スクロール処理の高速化
589
+ for i in range(scrolls_needed):
590
+ scroll_pos = i * (viewport_height - 100) # 少しだけオーバーラップ
591
+ driver.execute_script(f"window.scrollTo(0, {scroll_pos});")
592
+ time.sleep(0.1) # 高速化のため待機時間短縮
593
+
594
+ # トップに戻る
595
+ driver.execute_script("window.scrollTo(0, 0);")
596
+ time.sleep(0.2) # 短い待機に変更
597
+ logger.info("Scroll rendering completed")
598
+
599
+ # スクロールバーを非表示に
600
+ driver.execute_script("""
601
+ document.documentElement.style.overflow = 'hidden';
602
+ document.body.style.overflow = 'hidden';
603
+ """)
604
+
605
+ # ページの寸法を取得
606
+ dimensions = driver.execute_script("""
607
+ return {
608
+ width: Math.max(
609
+ document.documentElement.scrollWidth,
610
+ document.documentElement.offsetWidth,
611
+ document.documentElement.clientWidth,
612
+ document.body ? document.body.scrollWidth : 0,
613
+ document.body ? document.body.offsetWidth : 0,
614
+ document.body ? document.body.clientWidth : 0
615
+ ),
616
+ height: Math.max(
617
+ document.documentElement.scrollHeight,
618
+ document.documentElement.offsetHeight,
619
+ document.documentElement.clientHeight,
620
+ document.body ? document.body.scrollHeight : 0,
621
+ document.body ? document.body.offsetHeight : 0,
622
+ document.body ? document.body.clientHeight : 0
623
+ )
624
+ };
625
+ """)
626
+ scroll_width = dimensions['width']
627
+ scroll_height = dimensions['height']
628
+ logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}")
629
+
630
+ # 最小/最大値の設定
631
+ scroll_width = max(scroll_width, 100)
632
+ scroll_height = max(scroll_height, 100)
633
+ scroll_width = min(scroll_width, 2000)
634
+ scroll_height = min(scroll_height, 4000)
635
+
636
+ # レイアウト安定化のための待機
637
+ time.sleep(2.0)
638
+
639
+ # 高さに余白を追加
640
+ adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
641
+ adjusted_height = max(adjusted_height, scroll_height, 100)
642
+
643
+ # ウィンドウサイズを調整
644
+ adjusted_width = scroll_width
645
+ logger.info(f"Resizing window to: width={adjusted_width}, height={adjusted_height}")
646
+ driver.set_window_size(adjusted_width, adjusted_height)
647
+ time.sleep(0.5) # 短縮した待機時間
648
+
649
+ # スクリーンショット取得
650
+ logger.info("Taking screenshot...")
651
+ png = driver.get_screenshot_as_png()
652
+ logger.info("Screenshot taken successfully.")
653
+
654
+ # PIL画像に変換
655
+ img = Image.open(BytesIO(png))
656
+ logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
657
+
658
+ # 余白トリミング - 最適化版を使用
659
+ if trim_whitespace:
660
+ img = trim_image_whitespace(img, threshold=248, padding=20)
661
+ logger.info(f"Trimmed dimensions: {img.width}x{img.height}")
662
+
663
+ return img
664
+
665
+ except Exception as e:
666
+ logger.error(f"Error during screenshot generation: {e}", exc_info=True)
667
+ # エラー時は小さい黒画像を返す
668
+ return Image.new('RGB', (1, 1), color=(0, 0, 0))
669
+ finally:
670
+ logger.info("Cleaning up...")
671
+ # WebDriverプールに戻す
672
+ if driver_from_pool:
673
+ driver_pool.release_driver(driver)
674
+ logger.info("Returned driver to pool")
675
+ # 一時ファイル削除
676
+ if tmp_path and os.path.exists(tmp_path):
677
+ try:
678
+ os.remove(tmp_path)
679
+ logger.info(f"Temporary file {tmp_path} removed.")
680
+ except Exception as e:
681
+ logger.error(f"Error removing temporary file {tmp_path}: {e}")
682
+
683
+ # --- 並列処理を活用した新しい関数 ---
684
+ def text_to_screenshot_parallel(text: str, extension_percentage: float, temperature: float = 0.3,
685
+ trim_whitespace: bool = True, style: str = "standard") -> tuple:
686
+ """
687
+ テキストをGemini APIでHTMLに変換し、並列処理でスクリーンショットを生成する関数
688
+
689
+ Returns:
690
+ tuple - (PIL.Image, URL) - 生成された画像とHuggingFaceのURL
691
+ """
692
+ start_time = time.time()
693
+ logger.info("並列処理によるテキスト→スクリーンショット生成を開始")
694
+
695
+ try:
696
+ # WebDriverと HTML生成を並列で実行
697
+ with ThreadPoolExecutor(max_workers=2) as executor:
698
+ # Gemini APIリクエストタスク
699
+ html_future = executor.submit(
700
+ generate_html_from_text,
701
+ text=text,
702
+ temperature=temperature,
703
+ style=style
704
+ )
705
+
706
+ # WebDriver初期化タスク - プール使用
707
+ driver_future = executor.submit(driver_pool.get_driver)
708
+
709
+ # 結果を取得
710
+ html_code = html_future.result()
711
+ driver = driver_future.result()
712
+
713
+ # ドライバーはプールから取得しているためフラグ設定
714
+ driver_from_pool = True
715
+
716
+ # HTMLコードとドライバーが準備できたらスクリーンショット生成
717
+ logger.info(f"HTML生成完了:{len(html_code)}文字。スクリーンショット生成開始。")
718
+
719
+ # レンダリング前にドライバーの初期設定
720
+ tmp_path = None
721
+ try:
722
+ # 一時ファイルにHTMLを保存
723
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
724
+ tmp_path = tmp_file.name
725
+ tmp_file.write(html_code)
726
+ logger.info(f"HTMLを一時ファイルに保存: {tmp_path}")
727
+
728
+ # ウィンドウサイズ初期設定
729
+ initial_width = 1200
730
+ initial_height = 1000
731
+ driver.set_window_size(initial_width, initial_height)
732
+ file_url = "file://" + tmp_path
733
+ logger.info(f"ページに移動: {file_url}")
734
+ driver.get(file_url)
735
+
736
+ # ここからスクリーンショット生成ロジック(前の実装と同様)
737
+ # ページ読み込み待機 - 動的な待機時間を実装
738
+ logger.info("body要素を待機...")
739
+ WebDriverWait(driver, 10).until(
740
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
741
+ )
742
+ logger.info("body要素を検出。リソース読み込みを待機...")
743
+
744
+ # リソース読み込みの動的待機 - 最適化
745
+ max_wait = 3 # 最大待機時間(秒)
746
+ wait_increment = 0.2 # 確認間隔
747
+ wait_time = 0
748
+
749
+ while wait_time < max_wait:
750
+ resource_state = driver.execute_script("""
751
+ return {
752
+ complete: document.readyState === 'complete',
753
+ imgCount: document.images.length,
754
+ imgLoaded: Array.from(document.images).filter(img => img.complete).length,
755
+ faElements: document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]').length
756
+ };
757
+ """)
758
+
759
+ # ドキュメント完了かつ画像が読み込まれている場合、待機終了
760
+ if resource_state['complete'] and (resource_state['imgCount'] == 0 or
761
+ resource_state['imgLoaded'] == resource_state['imgCount']):
762
+ logger.info(f"リソース読み込み完了: {resource_state}")
763
+ break
764
+
765
+ time.sleep(wait_increment)
766
+ wait_time += wait_increment
767
+
768
+ # Font Awesome要素が多い場合は追加待機
769
+ fa_count = resource_state.get('faElements', 0)
770
+ if fa_count > 30:
771
+ logger.info(f"{fa_count}個のFont Awesome要素があるため、追加待機...")
772
+ time.sleep(min(1.0, fa_count / 100)) # 要素数に応じて待機(最大0.5秒)
773
+
774
+ # コンテンツレンダリングのための簡易スクロール
775
+ driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
776
+ time.sleep(0.2)
777
+ driver.execute_script("window.scrollTo(0, 0);")
778
+ time.sleep(0.2)
779
+
780
+ # スクロールバーを非表示に
781
+ driver.execute_script("""
782
+ document.documentElement.style.overflow = 'hidden';
783
+ document.body.style.overflow = 'hidden';
784
+ """)
785
+
786
+ # ページの寸法を取得
787
+ dimensions = driver.execute_script("""
788
+ return {
789
+ width: Math.max(
790
+ document.documentElement.scrollWidth,
791
+ document.documentElement.offsetWidth,
792
+ document.documentElement.clientWidth,
793
+ document.body ? document.body.scrollWidth : 0,
794
+ document.body ? document.body.offsetWidth : 0,
795
+ document.body ? document.body.clientWidth : 0
796
+ ),
797
+ height: Math.max(
798
+ document.documentElement.scrollHeight,
799
+ document.documentElement.offsetHeight,
800
+ document.documentElement.clientHeight,
801
+ document.body ? document.body.scrollHeight : 0,
802
+ document.body ? document.body.offsetHeight : 0,
803
+ document.body ? document.body.clientHeight : 0
804
+ )
805
+ };
806
+ """)
807
+ scroll_width = dimensions['width']
808
+ scroll_height = dimensions['height']
809
+
810
+ # 最小/最大値の設定
811
+ scroll_width = max(scroll_width, 100)
812
+ scroll_height = max(scroll_height, 100)
813
+ scroll_width = min(scroll_width, 2000)
814
+ scroll_height = min(scroll_height, 4000)
815
+
816
+ # 高さに余白を追加
817
+ adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
818
+ adjusted_height = max(adjusted_height, scroll_height, 100)
819
+
820
+ # ウィンドウサイズを調整
821
+ driver.set_window_size(scroll_width, adjusted_height)
822
+ time.sleep(0.2)
823
+
824
+ # スクリーンショット取得
825
+ logger.info("スクリーンショットを撮影...")
826
+ png = driver.get_screenshot_as_png()
827
+
828
+ # PIL画像に変換
829
+ img = Image.open(BytesIO(png))
830
+ logger.info(f"スクリーンショットサイズ: {img.width}x{img.height}")
831
+
832
+ # 余白トリミング
833
+ if trim_whitespace:
834
+ img = trim_image_whitespace(img, threshold=248, padding=20)
835
+ logger.info(f"トリミング後のサイズ: {img.width}x{img.height}")
836
+
837
+ # 画像をHuggingFaceにアップロード
838
+ prefix = f"infographic_{style}"
839
+ image_url = hf_uploader.upload_image(img, prefix=prefix)
840
+
841
+ elapsed = time.time() - start_time
842
+ logger.info(f"並列処理による生成完了。所要時間: {elapsed:.2f}秒、URL: {image_url}")
843
+ return img, image_url
844
+
845
+ except Exception as e:
846
+ logger.error(f"スクリーンショット生成中にエラー: {e}", exc_info=True)
847
+ return Image.new('RGB', (1, 1), color=(0, 0, 0)), None
848
+ finally:
849
+ # WebDriverプールに戻す
850
+ if driver_from_pool:
851
+ driver_pool.release_driver(driver)
852
+ # 一時ファイル削除
853
+ if tmp_path and os.path.exists(tmp_path):
854
+ try:
855
+ os.remove(tmp_path)
856
+ except Exception as e:
857
+ logger.error(f"一時ファイル削除エラー: {e}")
858
+
859
+ except Exception as e:
860
+ logger.error(f"並列処理中のエラー: {e}", exc_info=True)
861
+ return Image.new('RGB', (1, 1), color=(0, 0, 0)), None # エラー時は黒画像とNone URL
862
+
863
+ # 従来の非並列版も残す(互換性のため)
864
+ def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 1.0,
865
+ trim_whitespace: bool = True, style: str = "standard") -> tuple:
866
+ """テキストをGemini APIでHTMLに変換し、スクリーンショットを生成する統合関数(レガシー版)"""
867
+ # 並列処理版を呼び出す
868
+ return text_to_screenshot_parallel(text, extension_percentage, temperature, trim_whitespace, style)
869
+
870
+ # 新しい関数: HTMLからスクリーンショットを生成し、HuggingFaceにアップロード
871
+ def render_and_upload_screenshot(html_code: str, extension_percentage: float = 10.0,
872
+ trim_whitespace: bool = True, prefix: str = "screenshot") -> tuple:
873
+ """
874
+ HTMLコードからスクリーンショットを生成し、HuggingFaceにアップロードする
875
+
876
+ Returns:
877
+ tuple - (PIL.Image, URL) - 生成された画像とHuggingFaceのURL
878
+ """
879
+ try:
880
+ # スクリーンショット生成
881
+ img = render_fullpage_screenshot(html_code, extension_percentage, trim_whitespace)
882
+
883
+ # 画像をHuggingFaceにアップロード
884
+ image_url = hf_uploader.upload_image(img, prefix=prefix)
885
+
886
+ return img, image_url
887
+ except Exception as e:
888
+ logger.error(f"スクリーンショット生成とアップロード中にエラー: {e}", exc_info=True)
889
+ return Image.new('RGB', (1, 1), color=(0, 0, 0)), None
890
+
891
+ # --- FastAPI Setup ---
892
+ app = FastAPI()
893
+
894
+ # CORS設定を追加
895
+ app.add_middleware(
896
+ CORSMiddleware,
897
+ allow_origins=["*"],
898
+ allow_credentials=True,
899
+ allow_methods=["*"],
900
+ allow_headers=["*"],
901
+ )
902
+
903
+ # 静的ファイルのサービング設定
904
+ # Gradioのディレクトリを探索してアセットを見つける
905
+ gradio_dir = os.path.dirname(gr.__file__)
906
+ logger.info(f"Gradio version: {gr.__version__}")
907
+ logger.info(f"Gradio directory: {gradio_dir}")
908
+
909
+ # 基本的な静的ファイルディレクトリをマウント
910
+ static_dir = os.path.join(gradio_dir, "templates", "frontend", "static")
911
+ if os.path.exists(static_dir):
912
+ logger.info(f"Mounting static directory: {static_dir}")
913
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
914
+
915
+ # _appディレクトリを探す(新しいSvelteKitベースのフロントエンド用)
916
+ app_dir = os.path.join(gradio_dir, "templates", "frontend", "_app")
917
+ if os.path.exists(app_dir):
918
+ logger.info(f"Mounting _app directory: {app_dir}")
919
+ app.mount("/_app", StaticFiles(directory=app_dir), name="_app")
920
+
921
+ # assetsディレクトリを探す
922
+ assets_dir = os.path.join(gradio_dir, "templates", "frontend", "assets")
923
+ if os.path.exists(assets_dir):
924
+ logger.info(f"Mounting assets directory: {assets_dir}")
925
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
926
+
927
+ # cdnディレクトリがあれば追加
928
+ cdn_dir = os.path.join(gradio_dir, "templates", "cdn")
929
+ if os.path.exists(cdn_dir):
930
+ logger.info(f"Mounting cdn directory: {cdn_dir}")
931
+ app.mount("/cdn", StaticFiles(directory=cdn_dir), name="cdn")
932
+
933
+
934
+ # API Endpoint for screenshot generation - 更新版(URLを返すように変更)
935
+ @app.post("/api/screenshot",
936
+ response_model=ImageUrlResponse,
937
+ tags=["Screenshot"],
938
+ summary="Render HTML to Full Page Screenshot and Upload to HuggingFace",
939
+ description="Takes HTML code and an optional vertical extension percentage, renders it using a headless browser, uploads to HuggingFace, and returns the URL.")
940
+ async def api_render_screenshot(request: ScreenshotRequest):
941
+ """
942
+ API endpoint to render HTML, upload to HuggingFace, and return the URL.
943
+ """
944
+ try:
945
+ logger.info(f"API request received. Extension: {request.extension_percentage}%")
946
+
947
+ # スクリーンショット生成とアップロード
948
+ pil_image, image_url = render_and_upload_screenshot(
949
+ request.html_code,
950
+ request.extension_percentage,
951
+ request.trim_whitespace,
952
+ prefix="screenshot"
953
+ )
954
+
955
+ if pil_image.size == (1, 1) or not image_url:
956
+ logger.error("Screenshot generation failed, or upload failed.")
957
+ raise HTTPException(status_code=500, detail="Failed to generate or upload screenshot")
958
+
959
+ # URLを返す
960
+ logger.info(f"返却URL: {image_url}")
961
+ return ImageUrlResponse(url=image_url)
962
+
963
+ except Exception as e:
964
+ logger.error(f"API Error: {e}", exc_info=True)
965
+ raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
966
+
967
+ # --- 新しいGemini API連携エンドポイント(並列処理版)- URLを返すよう更新 ---
968
+ @app.post("/api/text-to-screenshot",
969
+ response_model=ImageUrlResponse,
970
+ tags=["Screenshot", "Gemini"],
971
+ summary="テキストからインフォグラフィックを生成しHuggingFaceにアップロード",
972
+ description="テキストをGemini APIを使ってHTMLインフォグラフィックに変換し、HuggingFaceにアップロードしたURLを返します。")
973
+ async def api_text_to_screenshot(request: GeminiRequest):
974
+ """
975
+ テキストからHTMLインフォグラフィックを生成してアップロードし、URLを返すAPIエンドポイント
976
+ """
977
+ try:
978
+ logger.info(f"テキスト→スクリーンショットAPIリクエスト受信。テキスト長さ: {len(request.text)}, "
979
+ f"拡張率: {request.extension_percentage}%, 温度: {request.temperature}, "
980
+ f"スタイル: {request.style}")
981
+
982
+ # 並列処理版を使用 - 画像とURLを取得
983
+ pil_image, image_url = text_to_screenshot_parallel(
984
+ request.text,
985
+ request.extension_percentage,
986
+ request.temperature,
987
+ request.trim_whitespace,
988
+ request.style
989
+ )
990
+
991
+ if pil_image.size == (1, 1) or not image_url:
992
+ logger.error("スクリーンショット生成に失敗したか、アップロードに失敗しました。")
993
+ raise HTTPException(status_code=500, detail="Failed to generate or upload screenshot")
994
+
995
+ # URLを返す
996
+ logger.info(f"返却URL: {image_url}")
997
+ return ImageUrlResponse(url=image_url)
998
+
999
+ except Exception as e:
1000
+ logger.error(f"API Error: {e}", exc_info=True)
1001
+ raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
1002
+
1003
+ # --- Gradio Interface Definition ---
1004
+ # 入力モードの選択用Radioコンポーネント
1005
+ def process_input(input_mode, input_text, extension_percentage, temperature, trim_whitespace, style):
1006
+ """入力モードに応じて適切な処理を行う"""
1007
+ if input_mode == "HTML入力":
1008
+ # HTMLモードの場合はレンダリングとアップロード
1009
+ img, url = render_and_upload_screenshot(
1010
+ input_text,
1011
+ extension_percentage,
1012
+ trim_whitespace,
1013
+ prefix="html_screenshot"
1014
+ )
1015
+ return img, url if url else "アップロード失敗またはURL取得できませんでした"
1016
+ else:
1017
+ # テキスト入力モードの場合はGemini API使用(並列処理版)
1018
+ img, url = text_to_screenshot_parallel(
1019
+ input_text,
1020
+ extension_percentage,
1021
+ temperature,
1022
+ trim_whitespace,
1023
+ style
1024
+ )
1025
+ return img, url if url else "アップロード失敗またはURL取得できませんでした"
1026
+
1027
+ # Gradio UIの定義
1028
+ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
1029
+ gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
1030
+ gr.Markdown("HTMLコードをレンダリングするか、テキストをGemini APIでインフォグラフィックに変換して画像として取得します。")
1031
+ gr.Markdown("**パフォーマンス向上版**: 並列処理と最適化により処理時間を短縮しています")
1032
+
1033
+ with gr.Row():
1034
+ input_mode = gr.Radio(
1035
+ ["HTML入力", "テキスト入力"],
1036
+ label="入力モード",
1037
+ value="HTML入力"
1038
+ )
1039
+
1040
+ # 共用のテキストボックス
1041
+ input_text = gr.Textbox(
1042
+ lines=15,
1043
+ label="入力",
1044
+ placeholder="HTMLコードまたはテキストを入力してください。入力モードに応じて処理されます。"
1045
+ )
1046
+
1047
+ with gr.Row():
1048
+ with gr.Column(scale=1):
1049
+ # スタイル選択ドロップダウン
1050
+ style_dropdown = gr.Dropdown(
1051
+ choices=["standard", "cute", "resort", "cool", "dental", "school","KOKUGO"],
1052
+ value="standard",
1053
+ label="デザインスタイル",
1054
+ info="テキスト→HTML変換時のデザインテーマを選択します",
1055
+ visible=False # テキスト入力モードの時だけ表示
1056
+ )
1057
+
1058
+ with gr.Column(scale=2):
1059
+ extension_percentage = gr.Slider(
1060
+ minimum=0,
1061
+ maximum=30,
1062
+ step=1.0,
1063
+ value=15, # デフォルト値15%
1064
+ label="上下高さ拡張率(%)"
1065
+ )
1066
+
1067
+ # 温度調整スライダー(テキストモード時のみ表示)
1068
+ temperature = gr.Slider(
1069
+ minimum=0.0,
1070
+ maximum=1.0,
1071
+ step=0.1,
1072
+ value=1.0, # デフォルト値を1.0に設定
1073
+ label="生成時の温度(低い=一貫性高、高い=創造性高)",
1074
+ visible=False # 最初は非表示
1075
+ )
1076
+
1077
+ # 余白トリミングオプション
1078
+ trim_whitespace = gr.Checkbox(
1079
+ label="余白を自動トリミング",
1080
+ value=True,
1081
+ info="生成される画像から余分な空白領域を自動的に削除します"
1082
+ )
1083
+
1084
+ submit_btn = gr.Button("生成")
1085
+
1086
+ # 出力部分をRowで分ける
1087
+ with gr.Row():
1088
+ with gr.Column(scale=1):
1089
+ output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
1090
+ with gr.Column(scale=1):
1091
+ output_url = gr.Textbox(label="画像URL(HuggingFace)", info="生成された画像のURLです。このURLを使用して画像にアクセスできます。")
1092
+
1093
+ # 入力モード変更時のイベント処理(テキストモード時のみ温度スライダーとスタイルドロップダウンを表示)
1094
+ def update_controls_visibility(mode):
1095
+ # Gradio 4.x用のアップデート方法
1096
+ is_text_mode = mode == "テキスト入力"
1097
+ return [
1098
+ gr.update(visible=is_text_mode), # temperature
1099
+ gr.update(visible=is_text_mode), # style_dropdown
1100
+ ]
1101
+
1102
+ input_mode.change(
1103
+ fn=update_controls_visibility,
1104
+ inputs=input_mode,
1105
+ outputs=[temperature, style_dropdown]
1106
+ )
1107
+
1108
+ # 生成ボタンクリック時のイベント処理
1109
+ submit_btn.click(
1110
+ fn=process_input,
1111
+ inputs=[input_mode, input_text, extension_percentage, temperature, trim_whitespace, style_dropdown],
1112
+ outputs=[output_image, output_url]
1113
+ )
1114
+
1115
+ # 環境変数情報を表示
1116
+ gemini_model = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
1117
+ hf_repo = os.environ.get("HF_REPO_ID", "tomo2chin2/SUPER_TENSAI_JIN")
1118
+ gr.Markdown(f"""
1119
+ ## APIエンドポイント
1120
+ - `/api/screenshot` - HTMLコードからスクリーンショットを生成し、URLを返します
1121
+ - `/api/text-to-screenshot` - テキストからインフォグラフィックスクリーンショットを生成し、URLを返します
1122
+
1123
+ ## 設定情報
1124
+ - 使用モデル: {gemini_model} (環境変数 GEMINI_MODEL で変更可能)
1125
+ - HuggingFaceリポジトリ: {hf_repo} (環境変数 HF_REPO_ID で変更可能)
1126
+ - WebDriverプール最大数: {driver_pool.max_drivers} (環境変数 MAX_WEBDRIVERS で変更可能)
1127
+ - 対応スタイル: standard, cute, resort, cool, dental, KOKUGO
1128
+ """)
1129
+
1130
+ # --- Mount Gradio App onto FastAPI ---
1131
+ app = gr.mount_gradio_app(app, iface, path="/")
1132
+
1133
+ # --- Run with Uvicorn (for local testing) ---
1134
+ if __name__ == "__main__":
1135
+ import uvicorn
1136
+ logger.info("Starting Uvicorn server for local development...")
1137
+ uvicorn.run(app, host="0.0.0.0", port=7860)
1138
+
1139
+ # アプリケーション終了時にWebDriverプールをクリーンアップ
1140
+ import atexit
1141
+ atexit.register(driver_pool.close_all)
gitattributes.txt ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ chromium-driver
2
+ fonts-noto-cjk
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- START OF FILE requirements.txt ---
2
+
3
+ selenium
4
+ gradio==4.19.2
5
+ Pillow
6
+ fastapi
7
+ uvicorn[standard]
8
+ google-generativeai
9
+ huggingface_hub==0.19.4
10
+ python-dotenv
11
+ uuid
12
+
13
+ # --- END OF FILE requirements.txt ---