tomo2chin2 commited on
Commit
b3a1e22
·
verified ·
1 Parent(s): b0403d0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +129 -285
app.py CHANGED
@@ -11,22 +11,18 @@ 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
  from huggingface_hub import hf_hub_download
19
 
20
- # --- Gemini SDK (v1.x) ---------------------------------
21
- from google import genai
22
- from google.genai import types
23
- # -------------------------------------------------------
24
 
25
- # ロギング設定
26
  logging.basicConfig(level=logging.INFO)
27
  logger = logging.getLogger(__name__)
28
 
29
- # ---------- データモデル ----------
30
  class GeminiRequest(BaseModel):
31
  text: str
32
  extension_percentage: float = 10.0
@@ -38,321 +34,169 @@ class ScreenshotRequest(BaseModel):
38
  html_code: str
39
  extension_percentage: float = 10.0
40
  trim_whitespace: bool = True
41
- style: str = "standard"
42
 
43
- # ---------- ユーティリティ ----------
44
- def enhance_font_awesome_layout(html_code):
45
- fa_fix_css = """
 
46
  <style>
47
- [class*="fa-"]{display:inline-block!important;margin-right:8px!important;vertical-align:middle!important;}
48
- h1 [class*="fa-"],h2 [class*="fa-"],h3 [class*="fa-"],h4 [class*="fa-"],h5 [class*="fa-"],h6 [class*="fa-"]{
49
- vertical-align:middle!important;margin-right:10px!important;}
50
- .fa+span,.fas+span,.far+span,.fab+span,span+.fa,span+.fas,span+.far,span+.fab{
51
- display:inline-block!important;margin-left:5px!important;}
52
- .card [class*="fa-"],.card-body [class*="fa-"]{float:none!important;clear:none!important;position:relative!important;}
53
- li [class*="fa-"],p [class*="fa-"]{margin-right:10px!important;}
54
- .inline-icon{display:inline-flex!important;align-items:center!important;justify-content:flex-start!important;}
55
- [class*="fa-"]+span{display:inline-block!important;vertical-align:middle!important;}
56
  </style>
57
  """
58
- if '<head>' in html_code:
59
- return html_code.replace('</head>', f'{fa_fix_css}</head>')
60
- elif '<html' in html_code:
61
- head_end = html_code.find('</head>')
62
- if head_end > 0:
63
- return html_code[:head_end] + fa_fix_css + html_code[head_end:]
64
- body_start = html_code.find('<body')
65
- if body_start > 0:
66
- return html_code[:body_start] + f'<head>{fa_fix_css}</head>' + html_code[body_start:]
67
- return f'<html><head>{fa_fix_css}</head>' + html_code + '</html>'
68
-
69
- def load_system_instruction(style="standard"):
70
- valid_styles = ["standard", "cute", "resort", "cool", "dental"]
71
- if style not in valid_styles:
72
- logger.warning(f"無効なスタイル '{style}'。'standard' を使用")
73
- style = "standard"
74
- local_path = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
75
- if os.path.exists(local_path):
76
- with open(local_path, encoding="utf-8") as f:
77
- return f.read()
78
- try:
79
- file_path = hf_hub_download(
80
- repo_id="tomo2chin2/GURAREKOstlyle",
81
- filename=f"{style}/prompt.txt",
82
- repo_type="dataset"
83
- )
84
- with open(file_path, encoding="utf-8") as f:
85
- return f.read()
86
- except Exception:
87
- file_path = hf_hub_download(
88
- repo_id="tomo2chin2/GURAREKOstlyle",
89
- filename="prompt.txt",
90
- repo_type="dataset"
91
- )
92
- with open(file_path, encoding="utf-8") as f:
93
- return f.read()
94
-
95
- # ---------- Gemini HTML 生成 ----------
96
  def generate_html_from_text(text, temperature=0.3, style="standard"):
97
- """Gemini HTML を生成。2.5 Flash Preview の場合は thinking_off"""
98
- api_key = os.environ.get("GEMINI_API_KEY")
99
- if not api_key:
100
- raise ValueError("GEMINI_API_KEY が未設定")
101
  model_name = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
102
-
103
  client = genai.Client(api_key=api_key)
104
- system_instruction = load_system_instruction(style)
105
- prompt = f"{system_instruction}\n\n{text}"
106
-
107
- safety_settings = [
108
- {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
109
- {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
110
- {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
111
- {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
112
- ]
113
 
114
- # ---- モデル分岐 ----
115
- if model_name == "gemini-2.5-flash-preview-04-17":
116
- generation_cfg = types.GenerateContentConfig(
117
- temperature=temperature,
118
- top_p=0.7,
119
- top_k=20,
120
- max_output_tokens=8192,
121
- candidate_count=1,
122
- thinking_config=types.ThinkingConfig(thinking_budget=0) # thinking OFF
123
- )
124
- else:
125
- generation_cfg = types.GenerateContentConfig(
126
- temperature=temperature,
127
- top_p=0.7,
128
- top_k=20,
129
- max_output_tokens=8192,
130
- candidate_count=1,
131
- )
132
 
133
- response = client.models.generate_content(
134
  model=model_name,
135
- contents=prompt,
136
- config=generation_cfg,
137
- safety_settings=safety_settings
 
 
 
 
 
138
  )
139
- raw = response.text
140
-
141
- # ```html ... ``` 抽出
142
- start = raw.find("```html")
143
- end = raw.rfind("```")
144
- if start != -1 and end != -1 and start < end:
145
- html_code = raw[start + 7:end].strip()
146
- return enhance_font_awesome_layout(html_code)
147
- logger.warning("```html``` ブロックが見つからず全文返却")
148
- return raw
149
-
150
- # ---------- 画像トリミング ----------
151
- def trim_image_whitespace(image, threshold=250, padding=10):
152
- gray = image.convert("L")
153
- data = list(gray.getdata())
154
- w, h = gray.size
155
- pixels = [data[i * w:(i + 1) * w] for i in range(h)]
156
- min_x, min_y, max_x, max_y = w, h, 0, 0
157
- for y in range(h):
158
- for x in range(w):
159
- if pixels[y][x] < threshold:
160
- min_x, min_y = min(min_x, x), min(min_y, y)
161
- max_x, max_y = max(max_x, x), max(max_y, y)
162
- if min_x > max_x or min_y > max_y:
163
- return image
164
- min_x = max(0, min_x - padding)
165
- min_y = max(0, min_y - padding)
166
- max_x = min(w - 1, max_x + padding)
167
- max_y = min(h - 1, max_y + padding)
168
- return image.crop((min_x, min_y, max_x + 1, max_y + 1))
169
-
170
- # ---------- HTML → スクリーンショット(Selenium) ----------
171
- def render_fullpage_screenshot(html_code, extension_percentage=6.0, trim_whitespace=True):
172
- tmp_path, driver = None, None
173
  try:
174
- with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as f:
175
- tmp_path = f.name
176
- f.write(html_code)
177
- options = Options()
178
- options.add_argument("--headless")
179
- options.add_argument("--no-sandbox")
180
- options.add_argument("--disable-dev-shm-usage")
181
- driver = webdriver.Chrome(options=options)
182
- driver.set_window_size(1200, 1000)
183
- driver.get("file://" + tmp_path)
184
- WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
185
  time.sleep(3)
186
-
187
- total_height = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)")
188
- viewport = driver.execute_script("return window.innerHeight")
189
- for i in range(max(1, total_height // viewport) + 1):
190
- driver.execute_script(f"window.scrollTo(0, {i * (viewport - 200)})")
191
- time.sleep(0.2)
192
- driver.execute_script("window.scrollTo(0, 0)")
193
- driver.execute_script("document.documentElement.style.overflow='hidden';document.body.style.overflow='hidden'")
194
- dims = driver.execute_script("return {w:document.documentElement.scrollWidth,h:document.documentElement.scrollHeight}")
195
- driver.set_window_size(dims["w"], int(dims["h"] * (1 + extension_percentage / 100)))
196
- time.sleep(1)
197
- png = driver.get_screenshot_as_png()
198
- img = Image.open(BytesIO(png))
199
- return trim_image_whitespace(img, 248, 20) if trim_whitespace else img
200
  except Exception as e:
201
- logger.error(f"Selenium error: {e}", exc_info=True)
202
- return Image.new("RGB", (1, 1))
203
  finally:
204
- if driver:
205
- driver.quit()
206
- if tmp_path and os.path.exists(tmp_path):
207
- os.remove(tmp_path)
208
 
209
- # ---------- 統合 ----------
210
- def text_to_screenshot(text, ext, temp=0.3, trim=True, style="standard"):
211
- html = generate_html_from_text(text, temp, style)
212
- return render_fullpage_screenshot(html, ext, trim)
213
 
214
- # ---------- FastAPI ----------
215
  app = FastAPI()
216
- app.add_middleware(
217
- CORSMiddleware,
218
- allow_origins=["*"], allow_credentials=True,
219
- allow_methods=["*"], allow_headers=["*"],
220
- )
221
-
222
- # Gradio 静的ファイル
223
  gradio_dir = os.path.dirname(gr.__file__)
224
- app.mount("/static", StaticFiles(directory=os.path.join(gradio_dir, "templates/frontend/static")), name="static")
225
 
226
- # ---------- API ----------
227
  @app.post("/api/screenshot", response_class=StreamingResponse)
228
- async def api_render(req: ScreenshotRequest):
229
  img = render_fullpage_screenshot(req.html_code, req.extension_percentage, req.trim_whitespace)
230
- buf = BytesIO()
231
- img.save(buf, format="PNG")
232
- buf.seek(0)
233
- return StreamingResponse(buf, media_type="image/png")
234
 
235
  @app.post("/api/text-to-screenshot", response_class=StreamingResponse)
236
- async def api_text_screenshot(req: GeminiRequest):
237
- img = text_to_screenshot(req.text, req.extension_percentage, req.temperature, req.trim_whitespace, req.style)
238
- buf = BytesIO()
239
- img.save(buf, format="PNG")
240
- buf.seek(0)
241
- return StreamingResponse(buf, media_type="image/png")
242
 
243
- # ---------- Gradio ----------
244
- def process_input(mode, txt, ext, temp, trim, style):
245
- if mode == "HTML入力":
246
- return render_fullpage_screenshot(txt, ext, trim)
247
- return text_to_screenshot(txt, ext, temp, trim, style)
248
 
249
- # ---------- Gradio UI ----------
250
- with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
251
- # 見出し
252
- gr.Markdown(
253
- "<h1 style='text-align:center;margin:0.2em 0'>HTMLビューア & テキスト→インフォグラフィック変換</h1>",
254
- elem_id="title",
255
- inline=True,
256
- )
257
  gr.Markdown(
258
- "HTML を直接レンダリングするか、テキストを Gemini API でインフォグラフィックに変換して画像取得できます。",
259
- elem_id="subtitle",
260
  )
261
 
262
- # === 入力モード選択 ===
263
  with gr.Row():
264
- input_mode = gr.Radio(
265
- ["HTML入力", "テキスト入力"],
266
- value="HTML入力",
267
- label="入力モード",
268
- )
269
 
270
- # === 共通入力テキスト ===
271
- input_text = gr.Textbox(
272
- lines=15,
273
- label="入力",
274
- placeholder="HTMLコードまたはテキストを入力してください(モードに応じて処理)。",
275
- )
276
-
277
- # === スタイル + スライダー類を 2 カラムで ===
278
  with gr.Row():
279
- # 左:スタイル
280
- with gr.Column(scale=1, min_width=180):
281
- style_dropdown = gr.Dropdown(
282
- choices=["standard", "cute", "resort", "cool", "dental"],
283
- value="standard",
284
- label="デザインスタイル",
285
- info="テキスト→HTML 変換時のテーマ",
286
- visible=False, # 初期は非表示
287
- )
288
- # 右:拡張率 & 温度
289
- with gr.Column(scale=3):
290
- extension_percentage = gr.Slider(
291
- 0, 30, value=10, step=1,
292
- label="上下高さ拡張率(%)"
293
- )
294
- temperature = gr.Slider(
295
- 0.0, 1.0, value=0.5, step=0.1,
296
- label="生成時の温度(低い=一貫性高、高い=創造性高)",
297
- visible=False, # 初期は非表示
298
- )
299
 
300
- # === トリミング & ボタン ===
301
- trim_whitespace = gr.Checkbox(
302
- value=True,
303
- label="余白を自動トリミング",
304
- )
305
- submit_btn = gr.Button("生成", variant="primary", size="lg")
306
 
307
- # === 出力 ===
308
- output_image = gr.Image(
309
- type="pil",
310
- label="ページ全体のスクリーンショット",
311
- show_label=True,
312
- show_download_button=True,
313
- )
314
 
315
- # ---------- 変化イベント ----------
316
- def toggle_controls(mode):
317
- """テキストモードのときだけ温度とスタイルを表示"""
318
- is_text = mode == "テキスト入力"
319
- return (
320
- gr.update(visible=is_text), # temperature
321
- gr.update(visible=is_text), # style_dropdown
322
- )
323
 
324
- input_mode.change(
325
- fn=toggle_controls,
326
- inputs=input_mode,
327
- outputs=[temperature, style_dropdown],
328
- )
329
 
330
- submit_btn.click(
331
- fn=process_input,
332
- inputs=[
333
- input_mode,
334
- input_text,
335
- extension_percentage,
336
- temperature,
337
- trim_whitespace,
338
- style_dropdown,
339
- ],
340
- outputs=output_image,
341
- )
342
-
343
- # 下部にモデル表示
344
  gr.Markdown(
345
- f"""
346
- **使用モデル** : `{os.getenv("GEMINI_MODEL", "gemini-1.5-pro")}`
347
- **API** : `/api/screenshot` / `/api/text-to-screenshot`
348
- """,
349
- elem_id="footnote",
350
  )
351
 
352
- # --- Gradio を FastAPI にマウント ---
353
  app = gr.mount_gradio_app(app, iface, path="/")
354
 
355
-
356
  if __name__ == "__main__":
357
- import uvicorn
358
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
11
  from selenium.webdriver.support import expected_conditions as EC
12
  from PIL import Image
13
  from io import BytesIO
14
+ import tempfile, time, os, logging
 
 
 
15
  from huggingface_hub import hf_hub_download
16
 
17
+ # Google Gen AI SDK 1.x -----------------------------------------------
18
+ from google import genai # 新 SDK
19
+ from google.genai import types # ThinkingConfig 用
20
+ # ---------------------------------------------------------------------
21
 
 
22
  logging.basicConfig(level=logging.INFO)
23
  logger = logging.getLogger(__name__)
24
 
25
+ # ------------------------- 入出力スキーマ ---------------------------
26
  class GeminiRequest(BaseModel):
27
  text: str
28
  extension_percentage: float = 10.0
 
34
  html_code: str
35
  extension_percentage: float = 10.0
36
  trim_whitespace: bool = True
 
37
 
38
+ # ------------------------- 補助関数 -------------------------------
39
+ def enhance_font_awesome_layout(html_code: str) -> str:
40
+ """FontAwesome のアイコン詰まりを CSS で解消"""
41
+ css = """
42
  <style>
43
+ [class*="fa-"]{display:inline-block!important;margin-right:8px!important;vertical-align:middle!important;}
44
+ h1 [class*="fa-"],h2 [class*="fa-"],h3 [class*="fa-"],
45
+ h4 [class*="fa-"],h5 [class*="fa-"],h6 [class*="fa-"]{margin-right:10px!important;}
46
+ li [class*="fa-"],p [class*="fa-"]{margin-right:10px!important;}
 
 
 
 
 
47
  </style>
48
  """
49
+ if "<head>" in html_code:
50
+ return html_code.replace("</head>", f"{css}</head>")
51
+ return f"<html><head>{css}</head>{html_code}</html>"
52
+
53
+ def load_system_instruction(style="standard") -> str:
54
+ valid = ["standard", "cute", "resort", "cool", "dental"]
55
+ style = style if style in valid else "standard"
56
+ local = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
57
+ if os.path.exists(local):
58
+ return open(local, encoding="utf-8").read()
59
+ path = hf_hub_download("tomo2chin2/GURAREKOstlyle",
60
+ filename=f"{style}/prompt.txt", repo_type="dataset")
61
+ return open(path, encoding="utf-8").read()
62
+
63
+ # ----------------- Gemini → HTML(thinking_off 分岐) ----------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  def generate_html_from_text(text, temperature=0.3, style="standard"):
65
+ api_key = os.environ["GEMINI_API_KEY"]
 
 
 
66
  model_name = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
 
67
  client = genai.Client(api_key=api_key)
 
 
 
 
 
 
 
 
 
68
 
69
+ cfg_kwargs = dict(
70
+ temperature=temperature, top_p=0.7, top_k=20,
71
+ max_output_tokens=8192, candidate_count=1
72
+ )
73
+ if model_name == "gemini-2.5-flash-preview-04-17": # thinking OFF :contentReference[oaicite:3]{index=3}
74
+ cfg_kwargs["thinking_config"] = types.ThinkingConfig(thinking_budget=0)
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ resp = client.models.generate_content(
77
  model=model_name,
78
+ contents=f"{load_system_instruction(style)}\n\n{text}",
79
+ config=types.GenerateContentConfig(**cfg_kwargs),
80
+ safety_settings=[
81
+ {"category":"HARM_CATEGORY_HARASSMENT","threshold":"BLOCK_MEDIUM_AND_ABOVE"},
82
+ {"category":"HARM_CATEGORY_HATE_SPEECH","threshold":"BLOCK_MEDIUM_AND_ABOVE"},
83
+ {"category":"HARM_CATEGORY_SEXUALLY_EXPLICIT","threshold":"BLOCK_MEDIUM_AND_ABOVE"},
84
+ {"category":"HARM_CATEGORY_DANGEROUS_CONTENT","threshold":"BLOCK_MEDIUM_AND_ABOVE"},
85
+ ]
86
  )
87
+ raw = resp.text
88
+ s, e = raw.find("```html"), raw.rfind("```")
89
+ return enhance_font_awesome_layout(raw[s+7:e].strip()) if s!=-1<e else raw
90
+
91
+ # ------------------------- 画像トリミング ---------------------------
92
+ def trim_image_whitespace(img: Image.Image, th=248, pad=20):
93
+ g = img.convert("L"); w,h = g.size; d=list(g.getdata())
94
+ mat=[d[i*w:(i+1)*w] for i in range(h)]
95
+ xs=[x for y in range(h) for x in range(w) if mat[y][x]<th]
96
+ ys=[y for y in range(h) for x in range(w) if mat[y][x]<th]
97
+ if not xs: return img
98
+ minx,maxx,miny,maxy = max(min(xs)-pad,0),min(max(xs)+pad,w-1),max(min(ys)-pad,0),min(max(ys)+pad,h-1)
99
+ return img.crop((minx,miny,maxx+1,maxy+1))
100
+
101
+ # ------------- HTML → フルページ PNG (Selenium) ----------------------
102
+ def render_fullpage_screenshot(html, ext=6.0, trim=True):
103
+ tmp, drv = None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  try:
105
+ with tempfile.NamedTemporaryFile("w", delete=False, suffix=".html", encoding="utf-8") as f:
106
+ f.write(html); tmp=f.name
107
+ opt=Options(); opt.add_argument("--headless"); opt.add_argument("--no-sandbox")
108
+ opt.add_argument("--disable-dev-shm-usage")
109
+ drv=webdriver.Chrome(options=opt)
110
+ drv.set_window_size(1200,1000); drv.get("file://"+tmp)
111
+ WebDriverWait(drv,15).until(EC.presence_of_element_located((By.TAG_NAME,"body")))
 
 
 
 
112
  time.sleep(3)
113
+ h=drv.execute_script("return Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)")
114
+ vh=drv.execute_script("return window.innerHeight")
115
+ for i in range(max(1,h//vh)+1):
116
+ drv.execute_script(f"window.scrollTo(0,{i*(vh-200)})"); time.sleep(0.2)
117
+ drv.execute_script("window.scrollTo(0,0)")
118
+ drv.execute_script("document.documentElement.style.overflow='hidden';document.body.style.overflow='hidden'")
119
+ w=drv.execute_script("return Math.max(document.body.scrollWidth,document.documentElement.scrollWidth)")
120
+ drv.set_window_size(w,int(h*(1+ext/100))); time.sleep(1)
121
+ img=Image.open(BytesIO(drv.get_screenshot_as_png()))
122
+ return trim_image_whitespace(img) if trim else img
 
 
 
 
123
  except Exception as e:
124
+ logger.error(e, exc_info=True)
125
+ return Image.new("RGB",(1,1))
126
  finally:
127
+ drv.quit() if drv else None
128
+ os.remove(tmp) if tmp and os.path.exists(tmp) else None
 
 
129
 
130
+ def text_to_screenshot(txt, ext, temp=0.3, trim=True, style="standard"):
131
+ return render_fullpage_screenshot(generate_html_from_text(txt,temp,style), ext, trim)
 
 
132
 
133
+ # ---------------------------- FastAPI -------------------------------
134
  app = FastAPI()
135
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
136
+ allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
137
  gradio_dir = os.path.dirname(gr.__file__)
138
+ app.mount("/static", StaticFiles(directory=os.path.join(gradio_dir,"templates/frontend/static")),name="static")
139
 
 
140
  @app.post("/api/screenshot", response_class=StreamingResponse)
141
+ async def api_ss(req: ScreenshotRequest):
142
  img = render_fullpage_screenshot(req.html_code, req.extension_percentage, req.trim_whitespace)
143
+ buf=BytesIO(); img.save(buf,"PNG"); buf.seek(0); return StreamingResponse(buf,media_type="image/png")
 
 
 
144
 
145
  @app.post("/api/text-to-screenshot", response_class=StreamingResponse)
146
+ async def api_t2s(req: GeminiRequest):
147
+ img = text_to_screenshot(req.text, req.extension_percentage, req.temperature,
148
+ req.trim_whitespace, req.style)
149
+ buf=BytesIO(); img.save(buf,"PNG"); buf.seek(0); return StreamingResponse(buf,media_type="image/png")
 
 
150
 
151
+ # ---------------------------- Gradio UI -----------------------------
152
+ def process(mode, txt, ext, temp, trim, style):
153
+ return render_fullpage_screenshot(txt, ext, trim) if mode=="HTML入力" else text_to_screenshot(txt, ext, temp, trim, style)
 
 
154
 
155
+ with gr.Blocks(title="Full Page Screenshot (Gemini 2.5 Flash)", theme=gr.themes.Base()) as iface:
156
+ # ヘッダー
 
 
 
 
 
 
157
  gr.Markdown(
158
+ "<h1 style='text-align:center;'>HTMLビューア & テキスト→インフォグラフィック変換</h1>"
159
+ "<p style='text-align:center;'>HTML をレンダリングするか、テキストを Gemini API で変換して画像化します。</p>"
160
  )
161
 
162
+ # ------- 入力モード + テキストボックス -------
163
  with gr.Row():
164
+ input_mode = gr.Radio(["HTML入力", "テキスト入力"], value="HTML入力", label="入力モード")
165
+ input_text = gr.Textbox(lines=15, label="入力",
166
+ placeholder="HTMLコードまたは説明テキストを入力")
 
 
167
 
168
+ # ------- オプションエリア -------
 
 
 
 
 
 
 
169
  with gr.Row():
170
+ with gr.Column(scale=1):
171
+ style_dd = gr.Dropdown(["standard","cute","resort","cool","dental"], value="standard",
172
+ label="デザインスタイル", visible=False)
173
+ with gr.Column(scale=2):
174
+ ext_sl = gr.Slider(0,30,value=10,step=1,label="上下高さ拡張率(%)")
175
+ temp_sl = gr.Slider(0.0,1.0,value=0.5,step=0.1,label="生成時の温度",visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ trim_cb = gr.Checkbox(value=True, label="余白を自動トリミング")
178
+ gen_btn = gr.Button("生成")
 
 
 
 
179
 
180
+ # ------- 出力 -------
181
+ out_img = gr.Image(type="pil", label="ページ全体のスクリーンショット")
 
 
 
 
 
182
 
183
+ # ------- ��視性トグル -------
184
+ def toggle_vis(mode):
185
+ show = mode=="テキスト入力"
186
+ return [{"visible":show,"__type__":"update"},{"visible":show,"__type__":"update"}]
187
+ input_mode.change(toggle_vis, input_mode, [temp_sl, style_dd])
 
 
 
188
 
189
+ # ------- 実行 -------
190
+ gen_btn.click(process, [input_mode,input_text,ext_sl,temp_sl,trim_cb,style_dd], out_img)
 
 
 
191
 
192
+ # ------- フッター -------
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  gr.Markdown(
194
+ f"<small>使用モデル: <code>{os.environ.get('GEMINI_MODEL','gemini-1.5-pro')}</code> / "
195
+ "API: <code>/api/screenshot</code> ・ <code>/api/text-to-screenshot</code></small>"
 
 
 
196
  )
197
 
198
+ # FastAPI へマウント
199
  app = gr.mount_gradio_app(app, iface, path="/")
200
 
 
201
  if __name__ == "__main__":
202
+ import uvicorn; uvicorn.run(app, host="0.0.0.0", port=7860)