tomo2chin2 commited on
Commit
84f5d0d
·
verified ·
1 Parent(s): 26eec55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +239 -120
app.py CHANGED
@@ -1,7 +1,9 @@
1
  # ===============================================================
2
- # app.py Gradio 5.x + FastAPI + Gemini + Selenium
3
- # FastAPI(redirect_slashes=False) で 307 ループを根絶
4
- # サブアプリを /gradio にマウントし、自前でリダイレクト制御
 
 
5
  # ===============================================================
6
 
7
  import os, time, tempfile, logging, threading, queue
@@ -26,12 +28,17 @@ from selenium.webdriver.support import expected_conditions as EC
26
  import google.generativeai as genai
27
  from huggingface_hub import hf_hub_download
28
 
29
- # ---------- ログ ----------
 
 
30
  logging.basicConfig(level=logging.INFO)
31
  logger = logging.getLogger(__name__)
32
 
33
- # ---------- WebDriver プール(前回と同じ) ----------
 
 
34
  class WebDriverPool:
 
35
  def __init__(self, max_drivers: int = 3):
36
  self.driver_queue = queue.Queue()
37
  self.max_drivers = max_drivers
@@ -40,40 +47,70 @@ class WebDriverPool:
40
  logger.info(f"WebDriver プール初期化: 最大 {max_drivers}")
41
 
42
  def _create_driver(self):
43
- opts = Options()
44
- opts.add_argument("--headless"); opts.add_argument("--no-sandbox")
45
- opts.add_argument("--disable-dev-shm-usage")
46
- drv_path = os.getenv("CHROMEDRIVER_PATH")
47
- if drv_path and os.path.exists(drv_path):
48
- return webdriver.Chrome(service=webdriver.ChromeService(executable_path=drv_path), options=opts)
49
- return webdriver.Chrome(options=opts)
 
 
 
 
 
 
 
50
 
51
  def get_driver(self):
52
  if not self.driver_queue.empty():
 
53
  return self.driver_queue.get()
 
54
  with self.lock:
55
  if self.count < self.max_drivers:
56
  self.count += 1
 
57
  return self._create_driver()
 
 
58
  return self.driver_queue.get()
59
 
60
  def release_driver(self, driver):
61
- try:
62
- driver.get("about:blank")
63
- self.driver_queue.put(driver)
64
- except Exception:
65
- driver.quit()
66
- with self.lock: self.count -= 1
 
 
 
 
 
 
 
 
67
 
68
  def close_all(self):
 
 
69
  while not self.driver_queue.empty():
70
- try: self.driver_queue.get(block=False).quit()
71
- except queue.Empty: break
 
 
 
 
 
72
  with self.lock: self.count = 0
 
73
 
74
  driver_pool = WebDriverPool(max_drivers=int(os.getenv("MAX_WEBDRIVERS", "3")))
75
 
76
- # ---------- Pydantic モデル ----------
 
 
77
  class GeminiRequest(BaseModel):
78
  text: str
79
  extension_percentage: float = 10.0
@@ -87,147 +124,229 @@ class ScreenshotRequest(BaseModel):
87
  trim_whitespace: bool = True
88
  style: str = "standard"
89
 
90
- # ---------- 補助関数(前回と同じ:FontAwesome, prompt 取得, Gemini, トリミング) ----------
 
 
91
  def enhance_font_awesome_layout(html_code: str) -> str:
92
- preload = """
93
  <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>
94
  <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>
95
  <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>
96
  """
97
- css = '<style>[class*="fa-"]{display:inline-block;margin-right:8px;vertical-align:middle}</style>'
 
 
 
 
 
 
 
 
 
 
98
  if '<head>' in html_code:
99
- return html_code.replace('</head>', f'{preload}{css}</head>')
100
- return f'<html><head>{preload}{css}</head>{html_code}</html>'
101
 
102
- def load_system_instruction(style="standard"):
103
- if style not in ["standard","cute","resort","cool","dental","school","KOKUGO"]:
 
104
  style = "standard"
105
  local = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
106
  if os.path.exists(local):
107
  return open(local, encoding="utf-8").read()
108
  try:
109
- path = hf_hub_download("tomo2chin2/GURAREKOstlyle", f"{style}/prompt.txt", repo_type="dataset")
110
- return open(path, encoding="utf-8").read()
111
  except Exception:
112
- path = hf_hub_download("tomo2chin2/GURAREKOstlyle", "prompt.txt", repo_type="dataset")
113
- return open(path, encoding="utf-8").read()
114
 
115
- def generate_html_from_text(text, temperature=0.5, style="standard"):
116
- genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
117
- model = genai.GenerativeModel(os.getenv("GEMINI_MODEL","gemini-1.5-pro"))
 
118
  prompt = f"{load_system_instruction(style)}\n\n{text}"
119
- cfg=dict(temperature=temperature,top_p=0.7,top_k=20,max_output_tokens=8192,candidate_count=1)
120
- raw=model.generate_content(prompt,generation_config=cfg).text
121
- s,e=raw.find("```html"),raw.rfind("```")
122
- html=raw[s+7:e].strip() if s!=-1 and e!=-1 else raw
 
123
  return enhance_font_awesome_layout(html)
124
 
125
- def trim_image_whitespace(img:Image.Image,th=248,pad=20)->Image.Image:
126
- arr=np.array(img.convert("L")); m=arr<th
127
- if m.any():
128
- y,x=np.where(m.any(1))[0],np.where(m.any(0))[0]
129
- return img.crop((max(0,x[0]-pad),max(0,y[0]-pad),
130
- min(img.width-1,x[-1]+pad),min(img.height-1,y[-1]+pad)))
 
 
131
  return img
132
 
133
- # ---------- HTML → PNG ----------
134
- def render_fullpage_screenshot(html, ext=6.0, trim_ws=True, driver=None):
135
- tmp=None; from_pool=False
 
 
 
 
136
  try:
137
  if driver is None:
138
- driver=driver_pool.get_driver(); from_pool=True
139
- with tempfile.NamedTemporaryFile(suffix=".html",delete=False,mode="w",encoding="utf-8") as f:
140
- tmp=f.name; f.write(html)
141
- driver.set_window_size(1200,1000)
142
- driver.get("file://"+tmp)
143
- WebDriverWait(driver,10).until(EC.presence_of_element_located((By.TAG_NAME,"body")))
144
- h=driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)")
145
- vh=driver.execute_script("return window.innerHeight")
146
- for i in range(max(1,min(5,h//vh))):
147
- driver.execute_script(f"window.scrollTo(0,{i*(vh-100)})"); time.sleep(0.1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  driver.execute_script("window.scrollTo(0,0)"); time.sleep(0.2)
149
- dims=driver.execute_script("return {w:Math.max(document.body.scrollWidth,document.documentElement.scrollWidth),h:Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}")
150
- w=min(max(dims['w'],100),2000); h=min(max(dims['h'],100),4000); h=int(h*(1+ext/100))
151
- driver.set_window_size(w,h); time.sleep(0.4)
152
- img=Image.open(BytesIO(driver.get_screenshot_as_png()))
153
- return trim_image_whitespace(img,248,20) if trim_ws else img
 
 
 
 
 
 
 
 
154
  except Exception as e:
155
- logger.error(f"Screenshot err: {e}",exc_info=True)
156
- return Image.new("RGB",(1,1),(0,0,0))
157
  finally:
158
- if from_pool: driver_pool.release_driver(driver)
159
- if tmp and os.path.exists(tmp): os.remove(tmp)
 
 
 
160
 
161
- # ---------- テキスト → PNG ----------
162
- def text_to_screenshot_parallel(text, ext, temp=0.5, trim_ws=True, style="standard"):
163
- with ThreadPoolExecutor(max_workers=2) as ex:
164
- html=ex.submit(generate_html_from_text,text,temp,style).result()
165
- drv=ex.submit(driver_pool.get_driver).result()
166
- return render_fullpage_screenshot(html,ext,trim_ws,drv)
 
 
 
 
 
 
 
167
 
168
  # ===============================================================
169
- # FastAPI (★ リダイレクト無効化)
170
  # ===============================================================
171
  app = FastAPI(redirect_slashes=False)
172
- app.add_middleware(CORSMiddleware,allow_origins=["*"],allow_credentials=True,
173
- allow_methods=["*"],allow_headers=["*"])
174
-
175
- @app.post("/api/screenshot",response_class=StreamingResponse,tags=["Screenshot"])
176
- async def api_screenshot(r:ScreenshotRequest):
177
- img=render_fullpage_screenshot(r.html_code,r.extension_percentage,r.trim_whitespace)
178
- buf=BytesIO(); img.save(buf,"PNG"); buf.seek(0); return StreamingResponse(buf,media_type="image/png")
179
 
180
- @app.post("/api/text-to-screenshot",response_class=StreamingResponse,tags=["Gemini","Screenshot"])
181
- async def api_text_to_screenshot(r:GeminiRequest):
182
- img=text_to_screenshot_parallel(r.text,r.extension_percentage,r.temperature,r.trim_whitespace,r.style)
183
- buf=BytesIO(); img.save(buf,"PNG"); buf.seek(0); return StreamingResponse(buf,media_type="image/png")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  # ===============================================================
186
- # Gradio UI
187
  # ===============================================================
188
- def process(mode,txt,ext,temp,trim,style):
189
- return render_fullpage_screenshot(txt,ext,trim) if mode=="HTML入力" else \
190
- text_to_screenshot_parallel(txt,ext,temp,trim,style)
191
-
192
- with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)",
193
- theme=gr.themes.Origin()) as gui:
194
- gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック")
195
- mode=gr.Radio(["HTML入力","テキスト入力"],value="HTML入力",label="入力モード")
196
- txt=gr.Textbox(lines=15,label="入力")
197
  with gr.Row():
198
- style=gr.Dropdown(["standard","cute","resort","cool","dental","school","KOKUGO"],
199
- value="standard",label="デザインスタイル",visible=False)
 
200
  with gr.Column(scale=2):
201
- ext=gr.Slider(0,30,10,1,label="上下高さ拡張率(%)")
202
- temp=gr.Slider(0.0,1.0,0.5,0.1,label="生成時の温度",visible=False)
203
- trim=gr.Checkbox(True,label="余白を自動トリミング")
204
- btn=gr.Button("生成")
205
- out=gr.Image(type="pil",label="スクリーンショット")
 
206
 
207
- mode.change(lambda m:[gr.update(visible=m=="テキスト入力")]*2,mode,[temp,style])
208
- btn.click(process,[mode,txt,ext,temp,trim,style],out)
 
209
 
210
- gr.Markdown(f"**API** `/api/screenshot`, `/api/text-to-screenshot` \n使用モデル: `{os.getenv('GEMINI_MODEL','gemini-1.5-pro')}`")
211
 
212
- # ---------------------------------------------------------------
213
- # Gradio を /gradio にマウント
214
- # ---------------------------------------------------------------
215
- GRADIO_BASE="/gradio"
216
- app = gr.mount_gradio_app(app, gui, path=GRADIO_BASE, ssr_mode=False)
217
 
218
- # ルート → /gradio/ へワンショット転送
 
 
 
 
 
 
219
  @app.get("/")
220
- def _root(): return RedirectResponse(GRADIO_BASE+"/")
221
 
222
- # /gradio/gradio/ にも転送して 307 を回避
223
- @app.get(GRADIO_BASE)
224
- def _no_slash(): return RedirectResponse(GRADIO_BASE+"/")
225
 
 
226
  # ローカルデバッグ
227
- if __name__=="__main__":
228
- import uvicorn, httpx
 
229
  logger.info("Uvicorn 起動 (ローカル)")
230
- uvicorn.run(app,host="0.0.0.0",port=7860)
231
 
232
- # 終了クリーンアップ
233
- import atexit; atexit.register(driver_pool.close_all)
 
 
 
 
1
  # ===============================================================
2
+ # app.py Gradio 5.x + FastAPI + Gemini + Selenium + 307対策
3
+ # (1) FastAPI(redirect_slashes=False) ←★追加
4
+ # (2) Gradio /gradio にマウント
5
+ # (3) / と /gradio を /gradio/ へリダイレクト ←★追加
6
+ # それ以外は 5.x 対応フルロジックを一切カットせず
7
  # ===============================================================
8
 
9
  import os, time, tempfile, logging, threading, queue
 
28
  import google.generativeai as genai
29
  from huggingface_hub import hf_hub_download
30
 
31
+ # ---------------------------------------------------------------
32
+ # ロギング
33
+ # ---------------------------------------------------------------
34
  logging.basicConfig(level=logging.INFO)
35
  logger = logging.getLogger(__name__)
36
 
37
+ # ---------------------------------------------------------------
38
+ # WebDriverPool ― 以前提示した完全版と同一
39
+ # ---------------------------------------------------------------
40
  class WebDriverPool:
41
+ """再利用可能な WebDriver をプールして高速化"""
42
  def __init__(self, max_drivers: int = 3):
43
  self.driver_queue = queue.Queue()
44
  self.max_drivers = max_drivers
 
47
  logger.info(f"WebDriver プール初期化: 最大 {max_drivers}")
48
 
49
  def _create_driver(self):
50
+ options = Options()
51
+ options.add_argument("--headless")
52
+ options.add_argument("--no-sandbox")
53
+ options.add_argument("--disable-dev-shm-usage")
54
+ options.add_argument("--force-device-scale-factor=1")
55
+ options.add_argument("--disable-features=NetworkService")
56
+ options.add_argument("--dns-prefetch-disable")
57
+
58
+ chromedriver_path = os.environ.get("CHROMEDRIVER_PATH")
59
+ if chromedriver_path and os.path.exists(chromedriver_path):
60
+ logger.info(f"CHROMEDRIVER_PATH 使用: {chromedriver_path}")
61
+ service = webdriver.ChromeService(executable_path=chromedriver_path)
62
+ return webdriver.Chrome(service=service, options=options)
63
+ return webdriver.Chrome(options=options)
64
 
65
  def get_driver(self):
66
  if not self.driver_queue.empty():
67
+ logger.info("既存 WebDriver 取得")
68
  return self.driver_queue.get()
69
+
70
  with self.lock:
71
  if self.count < self.max_drivers:
72
  self.count += 1
73
+ logger.info(f"新規 WebDriver 作成 ({self.count}/{self.max_drivers})")
74
  return self._create_driver()
75
+
76
+ logger.info("プール満杯、空き待機中…")
77
  return self.driver_queue.get()
78
 
79
  def release_driver(self, driver):
80
+ if driver:
81
+ try:
82
+ driver.get("about:blank")
83
+ driver.execute_script("""
84
+ document.documentElement.style.overflow='';
85
+ document.body.style.overflow='';
86
+ """)
87
+ self.driver_queue.put(driver)
88
+ logger.info("WebDriver をプールに返却")
89
+ except Exception as e:
90
+ logger.error(f"返却エラー: {e}")
91
+ driver.quit()
92
+ with self.lock:
93
+ self.count -= 1
94
 
95
  def close_all(self):
96
+ logger.info("プール全 WebDriver 終了")
97
+ closed = 0
98
  while not self.driver_queue.empty():
99
+ try:
100
+ drv = self.driver_queue.get(block=False)
101
+ drv.quit(); closed += 1
102
+ except queue.Empty:
103
+ break
104
+ except Exception as e:
105
+ logger.error(f"終了エラー: {e}")
106
  with self.lock: self.count = 0
107
+ logger.info(f"{closed} 個終了")
108
 
109
  driver_pool = WebDriverPool(max_drivers=int(os.getenv("MAX_WEBDRIVERS", "3")))
110
 
111
+ # ---------------------------------------------------------------
112
+ # Pydantic モデル
113
+ # ---------------------------------------------------------------
114
  class GeminiRequest(BaseModel):
115
  text: str
116
  extension_percentage: float = 10.0
 
124
  trim_whitespace: bool = True
125
  style: str = "standard"
126
 
127
+ # ---------------------------------------------------------------
128
+ # 補助関数(FontAwesome レイアウト / prompt 読み込み / Gemini 生成)
129
+ # ---------------------------------------------------------------
130
  def enhance_font_awesome_layout(html_code: str) -> str:
131
+ fa_preload = """
132
  <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>
133
  <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>
134
  <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>
135
  """
136
+ fa_css = """
137
+ <style>
138
+ [class*="fa-"]{display:inline-block!important;margin-right:8px!important;vertical-align:middle!important;}
139
+ 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;}
140
+ .fa+span,.fas+span,.far+span,.fab+span,span+.fa,span+.fas,span+.far+span{display:inline-block!important;margin-left:5px!important;}
141
+ .card [class*="fa-"],.card-body [class*="fa-"]{float:none!important;clear:none!important;position:relative!important;}
142
+ li [class*="fa-"],p [class*="fa-"]{margin-right:10px!important;}
143
+ .inline-icon{display:inline-flex!important;align-items:center!important;justify-content:flex-start!important;}
144
+ [class*="fa-"]+span{display:inline-block!important;vertical-align:middle!important;}
145
+ </style>
146
+ """
147
  if '<head>' in html_code:
148
+ return html_code.replace('</head>', f'{fa_preload}{fa_css}</head>')
149
+ return f'<html><head>{fa_preload}{fa_css}</head>{html_code}</html>'
150
 
151
+ def load_system_instruction(style="standard") -> str:
152
+ valid_styles = ["standard","cute","resort","cool","dental","school","KOKUGO"]
153
+ if style not in valid_styles:
154
  style = "standard"
155
  local = os.path.join(os.path.dirname(__file__), style, "prompt.txt")
156
  if os.path.exists(local):
157
  return open(local, encoding="utf-8").read()
158
  try:
159
+ f = hf_hub_download("tomo2chin2/GURAREKOstlyle", f"{style}/prompt.txt", repo_type="dataset")
160
+ return open(f, encoding="utf-8").read()
161
  except Exception:
162
+ f = hf_hub_download("tomo2chin2/GURAREKOstlyle", "prompt.txt", repo_type="dataset")
163
+ return open(f, encoding="utf-8").read()
164
 
165
+ def generate_html_from_text(text: str, temperature=0.5, style="standard") -> str:
166
+ api_key = os.environ["GEMINI_API_KEY"]
167
+ genai.configure(api_key=api_key)
168
+ model = genai.GenerativeModel(os.getenv("GEMINI_MODEL", "gemini-1.5-pro"))
169
  prompt = f"{load_system_instruction(style)}\n\n{text}"
170
+ cfg = dict(temperature=temperature, top_p=0.7, top_k=20, max_output_tokens=8192, candidate_count=1)
171
+ resp = model.generate_content(prompt, generation_config=cfg)
172
+ raw = resp.text
173
+ s, e = raw.find("```html"), raw.rfind("```")
174
+ html = raw[s+7:e].strip() if s != -1 and e != -1 else raw
175
  return enhance_font_awesome_layout(html)
176
 
177
+ def trim_image_whitespace(img: Image.Image, threshold=248, padding=20) -> Image.Image:
178
+ arr = np.array(img.convert("L"))
179
+ mask = arr < threshold
180
+ if np.any(mask):
181
+ ys, xs = np.where(mask.any(1))[0], np.where(mask.any(0))[0]
182
+ return img.crop((max(xs[0]-padding,0), max(ys[0]-padding,0),
183
+ min(xs[-1]+padding, img.width-1),
184
+ min(ys[-1]+padding, img.height-1)))
185
  return img
186
 
187
+ # ---------------------------------------------------------------
188
+ # HTML スクショ (完全版ロジック)
189
+ # ---------------------------------------------------------------
190
+ def render_fullpage_screenshot(html_code: str, extension_percentage=6.0,
191
+ trim_whitespace=True, driver=None) -> Image.Image:
192
+ tmp_path = None
193
+ from_pool = False
194
  try:
195
  if driver is None:
196
+ driver = driver_pool.get_driver()
197
+ from_pool = True
198
+
199
+ # HTML 保存
200
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as tmp:
201
+ tmp_path = tmp.name
202
+ tmp.write(html_code)
203
+
204
+ driver.set_window_size(1200, 1000)
205
+ driver.get("file://" + tmp_path)
206
+
207
+ # body 出現を待機
208
+ WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
209
+
210
+ # リソースロード確認ループ(詳細ロジックは元コード準拠)
211
+ max_wait, inc, waited = 5, 0.2, 0.0
212
+ while waited < max_wait:
213
+ state = driver.execute_script("""
214
+ return {complete: document.readyState==='complete',
215
+ imgs: document.images.length,
216
+ loaded: Array.from(document.images).filter(i=>i.complete).length};
217
+ """)
218
+ if state['complete'] and (state['imgs']==0 or state['imgs']==state['loaded']):
219
+ break
220
+ time.sleep(inc); waited += inc
221
+
222
+ # スクロールレンダリング
223
+ total_h = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)")
224
+ vh = driver.execute_script("return window.innerHeight")
225
+ for i in range(max(1, min(5, total_h // vh))):
226
+ driver.execute_script(f"window.scrollTo(0, {(vh-100)*i})")
227
+ time.sleep(0.1)
228
  driver.execute_script("window.scrollTo(0,0)"); time.sleep(0.2)
229
+
230
+ dims = driver.execute_script("""
231
+ return {w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth),
232
+ h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)}
233
+ """)
234
+ w = min(max(dims['w'], 100), 2000)
235
+ h = min(max(dims['h'], 100), 4000)
236
+ h = int(h * (1 + extension_percentage / 100.0))
237
+ driver.set_window_size(w, h); time.sleep(0.5)
238
+
239
+ img = Image.open(BytesIO(driver.get_screenshot_as_png()))
240
+ return trim_image_whitespace(img, padding=20) if trim_whitespace else img
241
+
242
  except Exception as e:
243
+ logger.error(f"Screenshot error: {e}", exc_info=True)
244
+ return Image.new("RGB", (1, 1), (0, 0, 0))
245
  finally:
246
+ if from_pool:
247
+ driver_pool.release_driver(driver)
248
+ if tmp_path and os.path.exists(tmp_path):
249
+ try: os.remove(tmp_path)
250
+ except Exception: pass
251
 
252
+ # ---------------------------------------------------------------
253
+ # テキスト → スクショ (並列 API 呼び出し + ドライバ確保)
254
+ # ---------------------------------------------------------------
255
+ def text_to_screenshot_parallel(text, ext_perc, temp=0.5, trim_ws=True, style="standard") -> Image.Image:
256
+ with ThreadPoolExecutor(max_workers=2) as exe:
257
+ html_future = exe.submit(generate_html_from_text, text, temp, style)
258
+ driver_future = exe.submit(driver_pool.get_driver)
259
+ html_code = html_future.result()
260
+ driver = driver_future.result()
261
+ return render_fullpage_screenshot(html_code, ext_perc, trim_ws, driver)
262
+
263
+ def text_to_screenshot(*args, **kwargs):
264
+ return text_to_screenshot_parallel(*args, **kwargs)
265
 
266
  # ===============================================================
267
+ # FastAPI (★ redirect_slashes=False で自動 307 を殺す)
268
  # ===============================================================
269
  app = FastAPI(redirect_slashes=False)
 
 
 
 
 
 
 
270
 
271
+ app.add_middleware(
272
+ CORSMiddleware,
273
+ allow_origins=["*"],
274
+ allow_credentials=True,
275
+ allow_methods=["*"],
276
+ allow_headers=["*"],
277
+ )
278
+
279
+ # -------- API エンドポイントはそのまま --------
280
+ @app.post("/api/screenshot", response_class=StreamingResponse, tags=["Screenshot"])
281
+ async def api_render_screenshot(req: ScreenshotRequest):
282
+ img = render_fullpage_screenshot(req.html_code, req.extension_percentage, req.trim_whitespace)
283
+ buf = BytesIO(); img.save(buf, format="PNG"); buf.seek(0)
284
+ return StreamingResponse(buf, media_type="image/png")
285
+
286
+ @app.post("/api/text-to-screenshot", response_class=StreamingResponse, tags=["Gemini","Screenshot"])
287
+ async def api_text_to_screenshot(req: GeminiRequest):
288
+ img = text_to_screenshot_parallel(
289
+ req.text, req.extension_percentage, req.temperature, req.trim_whitespace, req.style)
290
+ buf = BytesIO(); img.save(buf, format="PNG"); buf.seek(0)
291
+ return StreamingResponse(buf, media_type="image/png")
292
 
293
  # ===============================================================
294
+ # Gradio UI (完全版 UI 定義)
295
  # ===============================================================
296
+ def process_input(mode, text, ext, temp, trim, style):
297
+ return render_fullpage_screenshot(text, ext, trim) if mode == "HTML入力" else \
298
+ text_to_screenshot_parallel(text, ext, temp, trim, style)
299
+
300
+ with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Origin()) as iface:
301
+ gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
302
+ with gr.Row():
303
+ mode = gr.Radio(["HTML入力", "テキスト入力"], value="HTML入力", label="入力モード")
304
+ text = gr.Textbox(lines=15, label="入力")
305
  with gr.Row():
306
+ style = gr.Dropdown(
307
+ ["standard", "cute", "resort", "cool", "dental", "school", "KOKUGO"],
308
+ value="standard", label="デザインスタイル", visible=False)
309
  with gr.Column(scale=2):
310
+ ext = gr.Slider(0, 30, value=10, step=1, label="上下高さ拡張率(%)")
311
+ temp = gr.Slider(0.0, 1.0, value=0.5, step=0.1,
312
+ label="生成時の温度", visible=False)
313
+ trim = gr.Checkbox(value=True, label="余白を自動トリミング")
314
+ btn = gr.Button("生成")
315
+ out = gr.Image(type="pil", label="スクリーンショット")
316
 
317
+ # 可視制御
318
+ def toggle(m): vis = m == "テキスト入力"; return [gr.update(visible=vis), gr.update(visible=vis)]
319
+ mode.change(toggle, mode, [temp, style])
320
 
321
+ btn.click(process_input, [mode, text, ext, temp, trim, style], out)
322
 
323
+ gr.Markdown(f"**API** `/api/screenshot`, `/api/text-to-screenshot` &nbsp;&nbsp; "
324
+ f"使用モデル: `{os.getenv('GEMINI_MODEL','gemini-1.5-pro')}`")
 
 
 
325
 
326
+ # ===============================================================
327
+ # Gradio を /gradio にマウントし、明示リダイレクトを追加
328
+ # ===============================================================
329
+ GRADIO_PATH = "/gradio"
330
+ app = gr.mount_gradio_app(app, iface, path=GRADIO_PATH, ssr_mode=False)
331
+
332
+ # ルート → /gradio/ へ転送
333
  @app.get("/")
334
+ def _root(): return RedirectResponse(GRADIO_PATH + "/")
335
 
336
+ # 末尾スラッシュ無し有りへ転送
337
+ @app.get(GRADIO_PATH)
338
+ def _no_slash(): return RedirectResponse(GRADIO_PATH + "/")
339
 
340
+ # ===============================================================
341
  # ローカルデバッグ
342
+ # ===============================================================
343
+ if __name__ == "__main__":
344
+ import uvicorn
345
  logger.info("Uvicorn 起動 (ローカル)")
346
+ uvicorn.run(app, host="0.0.0.0", port=7860)
347
 
348
+ # ===============================================================
349
+ # 終了時 WebDriver クリーンアップ
350
+ # ===============================================================
351
+ import atexit
352
+ atexit.register(driver_pool.close_all)