tomo2chin2 commited on
Commit
35a8897
·
verified ·
1 Parent(s): 64edcc1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -170
app.py CHANGED
@@ -6,7 +6,7 @@
6
  # それ以外は 5.x 対応フルロジックを一切カットせず
7
  # ===============================================================
8
 
9
- import os, time, tempfile, logging, threading, queue, zipfile
10
  from io import BytesIO
11
  from concurrent.futures import ThreadPoolExecutor
12
 
@@ -126,22 +126,34 @@ class ScreenshotRequest(BaseModel):
126
  trim_whitespace: bool = True
127
  style: str = "standard"
128
 
129
- # バッチ処理用の新しいモデル
130
- class BatchGeminiRequest(BaseModel):
131
- texts: list[str]
132
- extension_percentage: float = 10.0
133
- temperature: float = 0.5
134
- trim_whitespace: bool = True
135
- style: str = "standard"
136
-
137
  # ---------------------------------------------------------------
138
- # システム指示のキャッシュ実装
139
  # ---------------------------------------------------------------
140
- # プロンプトキャッシュ - 頻繁に使用されるプロンプトを保存
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  _prompt_cache = {}
142
 
143
  def load_system_instruction(style="standard") -> str:
144
- """システム指示をロード (キャッシュ機能付き)"""
145
  # キャッシュに存在すればそれを返す
146
  if style in _prompt_cache:
147
  return _prompt_cache[style]
@@ -149,10 +161,7 @@ def load_system_instruction(style="standard") -> str:
149
  valid_styles = ["standard","cute","resort","cool","dental","school","KOKUGO"]
150
  if style not in valid_styles:
151
  style = "standard"
152
-
153
  local = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
154
- prompt_text = ""
155
-
156
  if os.path.exists(local):
157
  prompt_text = open(local, encoding="utf-8").read()
158
  else:
@@ -167,50 +176,6 @@ def load_system_instruction(style="standard") -> str:
167
  _prompt_cache[style] = prompt_text
168
  return prompt_text
169
 
170
- # ---------------------------------------------------------------
171
- # 初期化時に全スタイルをキャッシュに先読み
172
- # ---------------------------------------------------------------
173
- def preload_all_prompts():
174
- """アプリ起動時に全スタイルの指示を事前ロード"""
175
- styles = ["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"]
176
- logger.info("システム指示の先読み開始...")
177
-
178
- with ThreadPoolExecutor(max_workers=len(styles)) as executor:
179
- futures = {executor.submit(load_system_instruction, style): style for style in styles}
180
- for future in futures:
181
- style = futures[future]
182
- try:
183
- future.result() # 結果を取得
184
- logger.info(f"スタイル '{style}' の指示を先読み完了")
185
- except Exception as e:
186
- logger.error(f"スタイル '{style}' の指示先読みに失敗: {e}")
187
-
188
- logger.info(f"全 {len(_prompt_cache)} スタイルの指示先読み完了")
189
-
190
- # ---------------------------------------------------------------
191
- # 補助関数(FontAwesome レイアウト / Gemini 生成)
192
- # ---------------------------------------------------------------
193
- def enhance_font_awesome_layout(html_code: str) -> str:
194
- fa_preload = """
195
- <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>
196
- <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>
197
- <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>
198
- """
199
- fa_css = """
200
- <style>
201
- [class*="fa-"]{display:inline-block!important;margin-right:8px!important;vertical-align:middle!important;}
202
- h1 [class*="fa-"],h2 [class*="fa-"],h3 [class*="fa-"],h4 [class*="fa-"],h5 [class*="fa-"],h6 [class*="fa-"]{vertical-align:middle!important;margin-right:10px!important;}
203
- .fa+span,.fas+span,.far+span,.fab+span,span+.fa,span+.fas,span+.far+span{display:inline-block!important;margin-left:5px!important;}
204
- .card [class*="fa-"],.card-body [class*="fa-"]{float:none!important;clear:none!important;position:relative!important;}
205
- li [class*="fa-"],p [class*="fa-"]{margin-right:10px!important;}
206
- .inline-icon{display:inline-flex!important;align-items:center!important;justify-content:flex-start!important;}
207
- [class*="fa-"]+span{display:inline-block!important;vertical-align:middle!important;}
208
- </style>
209
- """
210
- if '<head>' in html_code:
211
- return html_code.replace('</head>', f'{fa_preload}{fa_css}</head>')
212
- return f'<html><head>{fa_preload}{fa_css}</head>{html_code}</html>'
213
-
214
  def generate_html_from_text(text: str, temperature=0.5, style="standard") -> str:
215
  # Updated: Use the new Google Genai client API
216
  api_key = os.environ["GEMINI_API_KEY"]
@@ -232,9 +197,9 @@ def generate_html_from_text(text: str, temperature=0.5, style="standard") -> str
232
  if model_name == "gemini-2.5-flash-preview-04-17":
233
  logger.info("gemini-2.5-flash-preview-04-17 モデル検出: 思考モードをオフに設定")
234
  config.thinking_config = types.ThinkingConfig(thinking_budget=0)
235
- # max_output_tokens を 50000 に拡張
236
  logger.info("gemini-2.5-flash-preview-04-17 モデル検出: max_output_tokens を 50000 に設定")
237
- config.max_output_tokens = 50000
238
 
239
  # Generate content
240
  response = client.models.generate_content(
@@ -261,7 +226,7 @@ def trim_image_whitespace(img: Image.Image, threshold=248, padding=20) -> Image.
261
  return img
262
 
263
  # ---------------------------------------------------------------
264
- # HTML → スクショ 最適化版 (並列処理強化)
265
  # ---------------------------------------------------------------
266
  def render_fullpage_screenshot(html_code: str, extension_percentage=6.0,
267
  trim_whitespace=True, driver=None) -> Image.Image:
@@ -272,62 +237,46 @@ def render_fullpage_screenshot(html_code: str, extension_percentage=6.0,
272
  driver = driver_pool.get_driver()
273
  from_pool = True
274
 
275
- # HTML 保存と読み込みを並列化
276
  with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as tmp:
277
  tmp_path = tmp.name
278
  tmp.write(html_code)
279
-
280
  driver.set_window_size(1200, 1000)
281
  driver.get("file://" + tmp_path)
282
-
283
- # 非同期でリソースロード待機とスクリプト実行を行う
284
- def wait_for_resources():
285
- WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
286
- max_wait, inc, waited = 5, 0.2, 0.0
287
- while waited < max_wait:
288
- state = driver.execute_script("""
289
- return {complete: document.readyState==='complete',
290
- imgs: document.images.length,
291
- loaded: Array.from(document.images).filter(i=>i.complete).length};
292
- """)
293
- if state['complete'] and (state['imgs']==0 or state['imgs']==state['loaded']):
294
- break
295
- time.sleep(inc); waited += inc
296
- return True
297
-
298
- # リソー待機をスレッドプールで実行
299
- with ThreadPoolExecutor(max_workers=1) as executor:
300
- resource_wait = executor.submit(wait_for_resources)
301
- resource_wait.result() # 待機完了を確認
302
-
303
- # スクロールレンダリングを最適化
304
  total_h = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)")
305
  vh = driver.execute_script("return window.innerHeight")
306
-
307
- # 並列スクロール処理は安定性の問題があるため、直列実行のままに
308
  for i in range(max(1, min(5, total_h // vh))):
309
  driver.execute_script(f"window.scrollTo(0, {(vh-100)*i})")
310
  time.sleep(0.1)
311
  driver.execute_script("window.scrollTo(0,0)"); time.sleep(0.2)
312
 
313
- # サイズ計算と画像取得を並列化
314
- def get_dimensions_and_resize():
315
- dims = driver.execute_script("""
316
- return {w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth),
317
- h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)}
318
- """)
319
- w = min(max(dims['w'], 100), 2000)
320
- h = min(max(dims['h'], 100), 4000)
321
- h = int(h * (1 + extension_percentage / 100.0))
322
- driver.set_window_size(w, h)
323
- time.sleep(0.5)
324
- return w, h
325
-
326
- with ThreadPoolExecutor(max_workers=1) as executor:
327
- dims_future = executor.submit(get_dimensions_and_resize)
328
- dims_future.result() # サイズ調整完了を確認
329
-
330
- # スクリーンショット取得と画像処理
331
  img = Image.open(BytesIO(driver.get_screenshot_as_png()))
332
  return trim_image_whitespace(img, padding=20) if trim_whitespace else img
333
 
@@ -342,44 +291,18 @@ def render_fullpage_screenshot(html_code: str, extension_percentage=6.0,
342
  except Exception: pass
343
 
344
  # ---------------------------------------------------------------
345
- # テキスト → スクショ (並列処理強化版)
346
  # ---------------------------------------------------------------
347
- def text_to_screenshot(text, ext_perc, temp=0.5, trim_ws=True, style="standard") -> Image.Image:
348
- # 3つの並列タスク: HTML生成、ドライバ取得、必要なスタイルのシステム指示ロード
349
- with ThreadPoolExecutor(max_workers=3) as exe:
350
- # システム指示が未キャッシュの場合に備えて並列ロード
351
- prompt_future = exe.submit(load_system_instruction, style)
352
  html_future = exe.submit(generate_html_from_text, text, temp, style)
353
  driver_future = exe.submit(driver_pool.get_driver)
354
-
355
- # 結果取得
356
- prompt_future.result() # プロンプトをキャッシュ確保
357
  html_code = html_future.result()
358
  driver = driver_future.result()
359
-
360
- # 最適化されたスクリーンショット関数を使用
361
  return render_fullpage_screenshot(html_code, ext_perc, trim_ws, driver)
362
 
363
- # ---------------------------------------------------------------
364
- # テキスト → スクショ (複数同時処理版)
365
- # ---------------------------------------------------------------
366
- def batch_text_to_screenshot(texts, ext_perc, temp=0.5, trim_ws=True, style="standard") -> list:
367
- """複数テキストを同時に処理"""
368
- with ThreadPoolExecutor(max_workers=min(len(texts), 3)) as exe:
369
- futures = [exe.submit(text_to_screenshot, text, ext_perc, temp, trim_ws, style)
370
- for text in texts]
371
- return [f.result() for f in futures]
372
-
373
- # ---------------------------------------------------------------
374
- # アプリ初期化時に実行する処理
375
- # ---------------------------------------------------------------
376
- def initialize_app():
377
- """アプリケーション初期化処理"""
378
- # システム指示を事前にキャッシュにロード
379
- preload_all_prompts()
380
-
381
- # その他の初期化処理があればここに追加
382
- logger.info("アプリケーション初期化完了")
383
 
384
  # ===============================================================
385
  # FastAPI (★ redirect_slashes=False で自動 307 を殺す)
@@ -394,7 +317,7 @@ app.add_middleware(
394
  allow_headers=["*"],
395
  )
396
 
397
- # -------- API エンドポイント --------
398
  @app.post("/api/screenshot", response_class=StreamingResponse, tags=["Screenshot"])
399
  async def api_render_screenshot(req: ScreenshotRequest):
400
  img = render_fullpage_screenshot(req.html_code, req.extension_percentage, req.trim_whitespace)
@@ -403,40 +326,17 @@ async def api_render_screenshot(req: ScreenshotRequest):
403
 
404
  @app.post("/api/text-to-screenshot", response_class=StreamingResponse, tags=["Gemini","Screenshot"])
405
  async def api_text_to_screenshot(req: GeminiRequest):
406
- img = text_to_screenshot(
407
  req.text, req.extension_percentage, req.temperature, req.trim_whitespace, req.style)
408
  buf = BytesIO(); img.save(buf, format="PNG"); buf.seek(0)
409
  return StreamingResponse(buf, media_type="image/png")
410
 
411
- # バッチ処理用の新しいエンドポイント
412
- @app.post("/api/batch-text-to-screenshot", tags=["Gemini","Screenshot"])
413
- async def api_batch_text_to_screenshot(req: BatchGeminiRequest):
414
- # 複数テキストを並列処理
415
- images = batch_text_to_screenshot(
416
- req.texts, req.extension_percentage, req.temperature, req.trim_whitespace, req.style)
417
-
418
- # 結果をZIP形式で返す
419
- buf = BytesIO()
420
- with zipfile.ZipFile(buf, 'w') as zf:
421
- for i, img in enumerate(images):
422
- img_buf = BytesIO()
423
- img.save(img_buf, format="PNG")
424
- img_buf.seek(0)
425
- zf.writestr(f"screenshot_{i+1}.png", img_buf.getvalue())
426
-
427
- buf.seek(0)
428
- return StreamingResponse(
429
- buf,
430
- media_type="application/zip",
431
- headers={"Content-Disposition": "attachment; filename=screenshots.zip"}
432
- )
433
-
434
  # ===============================================================
435
  # Gradio UI (完全版 UI 定義)
436
  # ===============================================================
437
  def process_input(mode, text, ext, temp, trim, style):
438
  return render_fullpage_screenshot(text, ext, trim) if mode == "HTML入力" else \
439
- text_to_screenshot(text, ext, temp, trim, style)
440
 
441
  with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Origin()) as iface:
442
  gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
@@ -448,8 +348,8 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
448
  ["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"],
449
  value="standard", label="デザインスタイル", visible=False)
450
  with gr.Column(scale=2):
451
- ext = gr.Slider(0, 30, value=10, step=1, label="上下高さ拡張率(%)")
452
- temp = gr.Slider(0.0, 1.0, value=0.5, step=0.1,
453
  label="生成時の温度", visible=False)
454
  trim = gr.Checkbox(value=True, label="余白を自動トリミング")
455
  btn = gr.Button("生成")
@@ -465,7 +365,7 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
465
  model_name = os.getenv('GEMINI_MODEL', 'gemini-1.5-pro')
466
  thinking_status = ""
467
  if model_name == "gemini-2.5-flash-preview-04-17":
468
- thinking_status = "(思考モード: オフ、最大トークン: 50000)"
469
 
470
  gr.Markdown(f"**API** `/api/screenshot`, `/api/text-to-screenshot` &nbsp;&nbsp; "
471
  f"使用モデル: `{model_name}` {thinking_status}")
@@ -484,12 +384,14 @@ def _root(): return RedirectResponse(GRADIO_PATH + "/")
484
  @app.get(GRADIO_PATH)
485
  def _no_slash(): return RedirectResponse(GRADIO_PATH + "/")
486
 
487
- # アプリケーション起動時の初期化
488
  @app.on_event("startup")
489
  async def startup_event():
490
- # バックグラウンドで初期化処理を実行
491
- threading.Thread(target=initialize_app).start()
492
- logger.info("アプリケーション起動: 並列処理による最適化を適用")
 
 
493
 
494
  # ===============================================================
495
  # ローカルデバッグ
 
6
  # それ以外は 5.x 対応フルロジックを一切カットせず
7
  # ===============================================================
8
 
9
+ import os, time, tempfile, logging, threading, queue
10
  from io import BytesIO
11
  from concurrent.futures import ThreadPoolExecutor
12
 
 
126
  trim_whitespace: bool = True
127
  style: str = "standard"
128
 
 
 
 
 
 
 
 
 
129
  # ---------------------------------------------------------------
130
+ # 補助関数(FontAwesome レイアウト / prompt 読み込み / Gemini 生成)
131
  # ---------------------------------------------------------------
132
+ def enhance_font_awesome_layout(html_code: str) -> str:
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_css = """
139
+ <style>
140
+ [class*="fa-"]{display:inline-block!important;margin-right:8px!important;vertical-align:middle!important;}
141
+ h1 [class*="fa-"],h2 [class*="fa-"],h3 [class*="fa-"],h4 [class*="fa-"],h5 [class*="fa-"],h6 [class*="fa-"]{vertical-align:middle!important;margin-right:10px!important;}
142
+ .fa+span,.fas+span,.far+span,.fab+span,span+.fa,span+.fas,span+.far+span{display:inline-block!important;margin-left:5px!important;}
143
+ .card [class*="fa-"],.card-body [class*="fa-"]{float:none!important;clear:none!important;position:relative!important;}
144
+ li [class*="fa-"],p [class*="fa-"]{margin-right:10px!important;}
145
+ .inline-icon{display:inline-flex!important;align-items:center!important;justify-content:flex-start!important;}
146
+ [class*="fa-"]+span{display:inline-block!important;vertical-align:middle!important;}
147
+ </style>
148
+ """
149
+ if '<head>' in html_code:
150
+ return html_code.replace('</head>', f'{fa_preload}{fa_css}</head>')
151
+ return f'<html><head>{fa_preload}{fa_css}</head>{html_code}</html>'
152
+
153
+ # シンプルなプロンプトキャッシュを実装
154
  _prompt_cache = {}
155
 
156
  def load_system_instruction(style="standard") -> str:
 
157
  # キャッシュに存在すればそれを返す
158
  if style in _prompt_cache:
159
  return _prompt_cache[style]
 
161
  valid_styles = ["standard","cute","resort","cool","dental","school","KOKUGO"]
162
  if style not in valid_styles:
163
  style = "standard"
 
164
  local = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
 
 
165
  if os.path.exists(local):
166
  prompt_text = open(local, encoding="utf-8").read()
167
  else:
 
176
  _prompt_cache[style] = prompt_text
177
  return prompt_text
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  def generate_html_from_text(text: str, temperature=0.5, style="standard") -> str:
180
  # Updated: Use the new Google Genai client API
181
  api_key = os.environ["GEMINI_API_KEY"]
 
197
  if model_name == "gemini-2.5-flash-preview-04-17":
198
  logger.info("gemini-2.5-flash-preview-04-17 モデル検出: 思考モードをオフに設定")
199
  config.thinking_config = types.ThinkingConfig(thinking_budget=0)
200
+ # max_output_tokens を 50000 に拡張 (唯一追加した最適化)
201
  logger.info("gemini-2.5-flash-preview-04-17 モデル検出: max_output_tokens を 50000 に設定")
202
+ config.max_output_tokens = 10000
203
 
204
  # Generate content
205
  response = client.models.generate_content(
 
226
  return img
227
 
228
  # ---------------------------------------------------------------
229
+ # HTML → スクショ (完全版ロジック)
230
  # ---------------------------------------------------------------
231
  def render_fullpage_screenshot(html_code: str, extension_percentage=6.0,
232
  trim_whitespace=True, driver=None) -> Image.Image:
 
237
  driver = driver_pool.get_driver()
238
  from_pool = True
239
 
240
+ # HTML 保存
241
  with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as tmp:
242
  tmp_path = tmp.name
243
  tmp.write(html_code)
244
+
245
  driver.set_window_size(1200, 1000)
246
  driver.get("file://" + tmp_path)
247
+
248
+ # body 出現を待機
249
+ WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
250
+
251
+ # リソースロード確認ループ(詳細ロジックは元コード準拠)
252
+ max_wait, inc, waited = 5, 0.2, 0.0
253
+ while waited < max_wait:
254
+ state = driver.execute_script("""
255
+ return {complete: document.readyState==='complete',
256
+ imgs: document.images.length,
257
+ loaded: Array.from(document.images).filter(i=>i.complete).length};
258
+ """)
259
+ if state['complete'] and (state['imgs']==0 or state['imgs']==state['loaded']):
260
+ break
261
+ time.sleep(inc); waited += inc
262
+
263
+ # スクロールレンダリング
 
 
 
 
 
264
  total_h = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)")
265
  vh = driver.execute_script("return window.innerHeight")
 
 
266
  for i in range(max(1, min(5, total_h // vh))):
267
  driver.execute_script(f"window.scrollTo(0, {(vh-100)*i})")
268
  time.sleep(0.1)
269
  driver.execute_script("window.scrollTo(0,0)"); time.sleep(0.2)
270
 
271
+ dims = driver.execute_script("""
272
+ return {w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth),
273
+ h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)}
274
+ """)
275
+ w = min(max(dims['w'], 100), 2000)
276
+ h = min(max(dims['h'], 100), 4000)
277
+ h = int(h * (1 + extension_percentage / 100.0))
278
+ driver.set_window_size(w, h); time.sleep(0.5)
279
+
 
 
 
 
 
 
 
 
 
280
  img = Image.open(BytesIO(driver.get_screenshot_as_png()))
281
  return trim_image_whitespace(img, padding=20) if trim_whitespace else img
282
 
 
291
  except Exception: pass
292
 
293
  # ---------------------------------------------------------------
294
+ # テキスト → スクショ (並列 API 呼び出し + ドライバ確保)
295
  # ---------------------------------------------------------------
296
+ def text_to_screenshot_parallel(text, ext_perc, temp=0.5, trim_ws=True, style="standard") -> Image.Image:
297
+ with ThreadPoolExecutor(max_workers=2) as exe:
 
 
 
298
  html_future = exe.submit(generate_html_from_text, text, temp, style)
299
  driver_future = exe.submit(driver_pool.get_driver)
 
 
 
300
  html_code = html_future.result()
301
  driver = driver_future.result()
 
 
302
  return render_fullpage_screenshot(html_code, ext_perc, trim_ws, driver)
303
 
304
+ def text_to_screenshot(*args, **kwargs):
305
+ return text_to_screenshot_parallel(*args, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  # ===============================================================
308
  # FastAPI (★ redirect_slashes=False で自動 307 を殺す)
 
317
  allow_headers=["*"],
318
  )
319
 
320
+ # -------- API エンドポイントはそのまま --------
321
  @app.post("/api/screenshot", response_class=StreamingResponse, tags=["Screenshot"])
322
  async def api_render_screenshot(req: ScreenshotRequest):
323
  img = render_fullpage_screenshot(req.html_code, req.extension_percentage, req.trim_whitespace)
 
326
 
327
  @app.post("/api/text-to-screenshot", response_class=StreamingResponse, tags=["Gemini","Screenshot"])
328
  async def api_text_to_screenshot(req: GeminiRequest):
329
+ img = text_to_screenshot_parallel(
330
  req.text, req.extension_percentage, req.temperature, req.trim_whitespace, req.style)
331
  buf = BytesIO(); img.save(buf, format="PNG"); buf.seek(0)
332
  return StreamingResponse(buf, media_type="image/png")
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  # ===============================================================
335
  # Gradio UI (完全版 UI 定義)
336
  # ===============================================================
337
  def process_input(mode, text, ext, temp, trim, style):
338
  return render_fullpage_screenshot(text, ext, trim) if mode == "HTML入力" else \
339
+ text_to_screenshot_parallel(text, ext, temp, trim, style)
340
 
341
  with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Origin()) as iface:
342
  gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
 
348
  ["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"],
349
  value="standard", label="デザインスタイル", visible=False)
350
  with gr.Column(scale=2):
351
+ ext = gr.Slider(0, 30, value=15, step=1, label="上下高さ拡張率(%)")
352
+ temp = gr.Slider(0.0, 1.0, value=1.0, step=0.1,
353
  label="生成時の温度", visible=False)
354
  trim = gr.Checkbox(value=True, label="余白を自動トリミング")
355
  btn = gr.Button("生成")
 
365
  model_name = os.getenv('GEMINI_MODEL', 'gemini-1.5-pro')
366
  thinking_status = ""
367
  if model_name == "gemini-2.5-flash-preview-04-17":
368
+ thinking_status = "(思考モード: オフ、最大トークン: 10000)"
369
 
370
  gr.Markdown(f"**API** `/api/screenshot`, `/api/text-to-screenshot` &nbsp;&nbsp; "
371
  f"使用モデル: `{model_name}` {thinking_status}")
 
384
  @app.get(GRADIO_PATH)
385
  def _no_slash(): return RedirectResponse(GRADIO_PATH + "/")
386
 
387
+ # 起動時に頻繁に使用するプロンプトを先読み
388
  @app.on_event("startup")
389
  async def startup_event():
390
+ # 初期化は最小限に
391
+ styles = ["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"]
392
+ for style in styles:
393
+ load_system_instruction(style)
394
+ logger.info("システムプロンプトのキャッシュを準備完了")
395
 
396
  # ===============================================================
397
  # ローカルデバッグ