tomo2chin2 commited on
Commit
ae54c37
·
verified ·
1 Parent(s): 36cf1e3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -57
app.py CHANGED
@@ -1,6 +1,6 @@
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
@@ -15,11 +15,13 @@ 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
@@ -28,6 +30,77 @@ import google.generativeai as genai
28
  logging.basicConfig(level=logging.INFO)
29
  logger = logging.getLogger(__name__)
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # --- WebDriverプールの実装 ---
32
  class WebDriverPool:
33
  """WebDriverインスタンスを再利用するためのプール"""
@@ -126,6 +199,13 @@ class ScreenshotRequest(BaseModel):
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レイアウトを改善し、プリロードタグを追加"""
@@ -602,8 +682,13 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
602
 
603
  # --- 並列処理を活用した新しい関数 ---
604
  def text_to_screenshot_parallel(text: str, extension_percentage: float, temperature: float = 0.3,
605
- trim_whitespace: bool = True, style: str = "standard") -> Image.Image:
606
- """テキストをGemini APIでHTMLに変換し、並列処理でスクリーンショットを生成する関数"""
 
 
 
 
 
607
  start_time = time.time()
608
  logger.info("並列処理によるテキスト→スクリーンショット生成を開始")
609
 
@@ -749,13 +834,17 @@ def text_to_screenshot_parallel(text: str, extension_percentage: float, temperat
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:
@@ -769,15 +858,36 @@ def text_to_screenshot_parallel(text: str, extension_percentage: float, temperat
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
 
@@ -821,59 +931,56 @@ if os.path.exists(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,
879
  request.temperature,
@@ -881,16 +988,13 @@ async def api_text_to_screenshot(request: GeminiRequest):
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)
@@ -901,11 +1005,24 @@ async def api_text_to_screenshot(request: GeminiRequest):
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:
@@ -965,7 +1082,13 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
965
  )
966
 
967
  submit_btn = gr.Button("生成")
968
- output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
 
 
 
 
 
 
969
 
970
  # 入力モード変更時のイベント処理(テキストモード時のみ温度スライダーとスタイルドロップダウンを表示)
971
  def update_controls_visibility(mode):
@@ -986,20 +1109,22 @@ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr
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エンドポイント
996
- - `/api/screenshot` - HTMLコードからスクリーンショットを生成
997
- - `/api/text-to-screenshot` - テキストからインフォグラフィックスクリーンショットを生成
998
 
999
  ## 設定情報
1000
  - 使用モデル: {gemini_model} (環境変数 GEMINI_MODEL で変更可能)
1001
- - 対応スタイル: standard, cute, resort, cool, dental
1002
  - WebDriverプール最大数: {driver_pool.max_drivers} (環境変数 MAX_WEBDRIVERS で変更可能)
 
1003
  """)
1004
 
1005
  # --- Mount Gradio App onto FastAPI ---
 
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
 
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
 
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
+ # ユニークなファイル名を生成
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}.png"
68
+ path_in_repo = f"images/{filename}"
69
+
70
+ # 一時ファイルに保存
71
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
72
+ tmp_path = tmp_file.name
73
+ image.save(tmp_path, format="PNG")
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を構築して返す
93
+ url = f"https://huggingface.co/{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インスタンスを再利用するためのプール"""
 
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レイアウトを改善し、プリロードタグを追加"""
 
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
 
 
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:
 
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 = 0.3,
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
 
 
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,
 
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)
 
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:
 
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):
 
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
1128
  """)
1129
 
1130
  # --- Mount Gradio App onto FastAPI ---