tomo2chin2 commited on
Commit
1d7ecdb
·
verified ·
1 Parent(s): aeb0d3b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +48 -575
app.py CHANGED
@@ -15,11 +15,10 @@ import tempfile
15
  import time
16
  import os
17
  import logging
18
- from huggingface_hub import hf_hub_download
19
 
20
- # Correct Gemini imports, including types
21
  import google.generativeai as genai
22
- from google.generativeai import types # Import types module needed for thinking_config
23
 
24
  # ロギング設定
25
  logging.basicConfig(level=logging.INFO)
@@ -39,7 +38,7 @@ class ScreenshotRequest(BaseModel):
39
  html_code: str
40
  extension_percentage: float = 10.0 # デフォルト値10%
41
  trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
42
- style: str = "standard" # デフォルトはstandard # Note: Style is not used for direct HTML rendering
43
 
44
  # HTMLのFont Awesomeレイアウトを改善する関数
45
  def enhance_font_awesome_layout(html_code):
@@ -53,40 +52,40 @@ def enhance_font_awesome_layout(html_code):
53
  margin-right: 8px !important;
54
  vertical-align: middle !important;
55
  }
56
-
57
  /* テキスト内のアイコン位置調整 */
58
- h1 [class*="fa-"], h2 [class*="fa-"], h3 [class*="fa-"],
59
  h4 [class*="fa-"], h5 [class*="fa-"], h6 [class*="fa-"] {
60
  vertical-align: middle !important;
61
  margin-right: 10px !important;
62
  }
63
-
64
  /* 特定パターンの修正 */
65
  .fa + span, .fas + span, .far + span, .fab + span,
66
- span + .fa, span + .fas, span + .far, span + .fab {
67
  display: inline-block !important;
68
  margin-left: 5px !important;
69
  }
70
-
71
  /* カード内アイコン修正 */
72
  .card [class*="fa-"], .card-body [class*="fa-"] {
73
  float: none !important;
74
  clear: none !important;
75
  position: relative !important;
76
  }
77
-
78
  /* アイコンと文字が重なる場合の調整 */
79
  li [class*="fa-"], p [class*="fa-"] {
80
  margin-right: 10px !important;
81
  }
82
-
83
  /* インラインアイコンのスペーシング */
84
  .inline-icon {
85
  display: inline-flex !important;
86
  align-items: center !important;
87
  justify-content: flex-start !important;
88
  }
89
-
90
  /* アイコン後のテキスト */
91
  [class*="fa-"] + span {
92
  display: inline-block !important;
@@ -178,7 +177,6 @@ def load_system_instruction(style="standard"):
178
  logger.error(error_msg)
179
  raise ValueError(error_msg)
180
 
181
-
182
  def generate_html_from_text(text, temperature=0.3, style="standard"):
183
  """テキストからHTMLを生成する"""
184
  try:
@@ -189,7 +187,6 @@ def generate_html_from_text(text, temperature=0.3, style="standard"):
189
  raise ValueError("GEMINI_API_KEY 環境変数が設定されていません")
190
 
191
  # モデル名の取得(環境変数から、なければデフォルト値)
192
- # ユーザーの要望に従い、環境変数が設定されていない場合のデフォルト値を gemini-1.5-pro のままにする
193
  model_name = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
194
  logger.info(f"使用するGeminiモデル: {model_name}")
195
 
@@ -199,34 +196,48 @@ def generate_html_from_text(text, temperature=0.3, style="standard"):
199
  # 指定されたスタイルのシステムインストラクションを読み込む
200
  system_instruction = load_system_instruction(style)
201
 
 
 
 
202
  # モデル初期化
203
  model = genai.GenerativeModel(model_name)
204
 
205
  # 生成設定 - ばらつきを減らすために設定を調整
206
- # generation_config は辞書として定義し、thinking_config を後で追加する可能性がある
207
- generation_config_dict = {
208
- "temperature": temperature, # APIリクエストで指定された温度を使用
209
- "top_p": 0.7,
210
- "top_k": 20,
211
  "max_output_tokens": 8192,
212
- "candidate_count": 1,
213
  }
214
 
215
- # モデルが gemini-2.5-flash-preview-04-17 の場合のみ thinking_budget=0 を設定
216
- if model_name == "gemini-2.5-flash-preview-04-17":
217
- logger.info(f"Model {model_name} detected, setting thinking_budget=0")
218
- # thinking_config を generation_config_dict に追加
219
- generation_config_dict["thinking_config"] = types.ThinkingConfig(thinking_budget=0)
220
- # 他のモデルの場合は thinking_config を追加しない(デフォルト動作)
221
-
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
- logger.info(f"Gemini APIにリクエストを送信: テキスト長さ = {len(text)}, 温度 = {temperature}, スタイル = {style}, モデル = {model_name}, Generation Config = {generation_config_dict}")
 
224
 
225
  # コンテンツ生成
226
- # generation_config パラメータに設定済みの辞書を渡す
227
  response = model.generate_content(
228
- prompt=f"{system_instruction}\n\n{text}",
229
- generation_config=generation_config_dict,
230
  safety_settings=safety_settings
231
  )
232
 
@@ -256,7 +267,6 @@ def generate_html_from_text(text, temperature=0.3, style="standard"):
256
  logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
257
  raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")
258
 
259
-
260
  # 画像から余分な空白領域をトリミングする関数
261
  def trim_image_whitespace(image, threshold=250, padding=10):
262
  """
@@ -293,6 +303,7 @@ def trim_image_whitespace(image, threshold=250, padding=10):
293
  min_y = min(min_y, y)
294
  max_x = max(max_x, x)
295
  max_y = max(max_y, y)
 
296
  # 境界外のトリミングの場合はエラー
297
  if min_x > max_x or min_y > max_y:
298
  logger.warning("トリミング領域が見つかりません。元の画像を返します。")
@@ -335,7 +346,7 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
335
  tmp_file.write(html_code)
336
  logger.info(f"HTML saved to temporary file: {tmp_path}")
337
  except Exception as e:
338
- logger.error(f"Error writing temporary HTML file: {e}", exc_info=True)
339
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
340
 
341
  # 2) Headless Chrome(Chromium) options
@@ -348,16 +359,12 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
348
  options.add_argument("--dns-prefetch-disable")
349
  # 環境変数からWebDriverパスを取得(任意)
350
  webdriver_path = os.environ.get("CHROMEDRIVER_PATH")
351
- service = None
352
  if webdriver_path and os.path.exists(webdriver_path):
353
  logger.info(f"Using CHROMEDRIVER_PATH: {webdriver_path}")
354
- try:
355
- service = webdriver.ChromeService(executable_path=webdriver_path)
356
- except Exception as e:
357
- logger.error(f"Error creating ChromeService with {webdriver_path}: {e}", exc_info=True)
358
- service = None # Fallback to default
359
- if service is None:
360
- logger.info("CHROMEDRIVER_PATH not set or invalid, using default PATH lookup for WebDriver.")
361
 
362
  try:
363
  logger.info("Initializing WebDriver...")
@@ -535,540 +542,6 @@ def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0
535
  except Exception as e:
536
  logger.error(f"Error removing temporary file {tmp_path}: {e}", exc_info=True)
537
 
538
-
539
- # --- Geminiを使った新しい関数 ---
540
- def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 0.3,
541
- trim_whitespace: bool = True, style: str = "standard") -> Image.Image:
542
- """テキストをGemini APIでHTMLに変換し、スクリーンショットを生成する統合関数"""
543
- try:
544
- # 1. テキストからHTMLを生成(温度パラメータとスタイルも渡す)
545
- html_code = generate_html_from_text(text, temperature, style)
546
-
547
- # 2. HTMLからスクリーンショットを生成
548
- return render_fullpage_screenshot(html_code, extension_percentage, trim_whitespace)
549
- except Exception as e:
550
- logger.error(f"テキストからスクリーンショット生成中にエラーが発生: {e}", exc_info=True)
551
- return Image.new('RGB', (1, 1), color=(0, 0, 0)) # エラー時は黒画像
552
-
553
- # --- FastAPI Setup ---
554
- app = FastAPI()
555
-
556
- # CORS設定を追加
557
- app.add_middleware(
558
- CORSMiddleware,
559
- allow_origins=["*"],
560
- allow_credentials=True,
561
- allow_methods=["*"],
562
- allow_headers=["*"],
563
- )
564
-
565
- # 静的ファイルのサービング設定
566
- # Gradioのディレクトリを探索してアセットを見つける
567
- gradio_dir = os.path.dirname(gr.__file__)
568
- logger.info(f"Gradio version: {gr.__version__}")
569
- logger.info(f"Gradio directory: {gradio_dir}")
570
-
571
- # 基本的な静的ファイルディレクトリをマウント
572
- static_dir = os.path.join(gradio_dir, "templates", "frontend", "static")
573
- if os.path.exists(static_dir):
574
- logger.info(f"Mounting static directory: {static_dir}")
575
- app.mount("/static", StaticFiles(directory=static_dir), name="static")
576
-
577
- # _appディレクトリを探す(新しいSvelteKitベースのフロントエンド用)
578
- app_dir = os.path.join(gradio_dir, "templates", "frontend", "_app")
579
- if os.path.exists(app_dir):
580
- logger.info(f"Mounting _app directory: {app_dir}")
581
- app.mount("/_app", StaticFiles(directory=app_dir), name="_app")
582
-
583
- # assetsディレクトリを探す
584
- assets_dir = os.path.join(gradio_dir, "templates", "frontend", "assets")
585
- if os.path.exists(assets_dir):
586
- logger.info(f"Mounting assets directory: {assets_dir}")
587
- app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
588
-
589
- # cdnディレクトリがあれば追加
590
- cdn_dir = os.path.join(gradio_dir, "templates", "cdn")
591
- if os.path.exists(cdn_dir):
592
- logger.info(f"Mounting cdn directory: {cdn_dir}")
593
- app.mount("/cdn", StaticFiles(directory=cdn_dir), name="cdn")
594
-
595
-
596
- # API Endpoint for screenshot generation
597
- @app.post("/api/screenshot",
598
- response_class=StreamingResponse,
599
- tags=["Screenshot"],
600
- summary="Render HTML to Full Page Screenshot",
601
- 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.")
602
- async def api_render_screenshot(request: ScreenshotRequest):
603
- """
604
- API endpoint to render HTML and return a screenshot.
605
- """
606
- try:
607
- logger.info(f"API request received. Extension: {request.extension_percentage}%")
608
- # Run the blocking Selenium code in a separate thread (FastAPI handles this)
609
- pil_image = render_fullpage_screenshot(
610
- request.html_code,
611
- request.extension_percentage,
612
- request.trim_whitespace
613
- )
614
-
615
- if pil_image.size == (1, 1):
616
- logger.error("Screenshot generation failed, returning 1x1 error image.")
617
- # Optionally return a proper error response instead of 1x1 image
618
- # raise HTTPException(status_code=500, detail="Failed to generate screenshot")
619
-
620
- # Convert PIL Image to PNG bytes
621
- img_byte_arr = BytesIO()
622
- pil_image.save(img_byte_arr, format='PNG')
623
- img_byte_arr.seek(0) # Go to the start of the BytesIO buffer
624
-
625
- logger.info("Returning screenshot as PNG stream.")
626
- return StreamingResponse(img_byte_arr, media_type="image/png")
627
-
628
- except Exception as e:
629
- logger.error(f"API Error: {e}", exc_info=True)
630
- raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
631
-
632
- # --- 新しいGemini API連携エンドポイント ---
633
- @app.post("/api/text-to-screenshot",
634
- response_class=StreamingResponse,
635
- tags=["Screenshot", "Gemini"],
636
- summary="テキストからインフォグラフィックを生成",
637
- description="テキストをGemini APIを使ってHTMLインフォグラフィックに変換し、スクリーンショットとして返します。")
638
- async def api_text_to_screenshot(request: GeminiRequest):
639
- """
640
- テキストからHTMLインフォグラフィックを生成してスクリーンショットを返すAPIエンドポイント
641
- """
642
- try:
643
- logger.info(f"テキスト→スクリーンショットAPIリクエスト受信。テキスト長さ: {len(request.text)}, "
644
- f"拡張率: {request.extension_percentage}%, 温度: {request.temperature}, "
645
- f"スタイル: {request.style}")
646
-
647
- # テキストからHTMLを生成してスクリーンショットを作成(温度パラメータとスタイルも渡す)
648
- pil_image = text_to_screenshot(
649
- request.text,
650
- request.extension_percentage,
651
- request.temperature,
652
- request.trim_whitespace,
653
- request.style
654
- )
655
-
656
- if pil_image.size == (1, 1):
657
- logger.error("スクリーンショット生成に失敗しました。1x1エラー画像を返します。")
658
- # raise HTTPException(status_code=500, detail="スクリーンショット生成に失敗しました")
659
-
660
-
661
- # PIL画像をPNGバイトに変換
662
- img_byte_arr = BytesIO()
663
- pil_image.save(img_byte_arr, format='PNG')
664
- img_byte_arr.seek(0) # BytesIOバッファの先頭に戻る
665
-
666
- logger.info("スクリーンショットをPNGストリームとして返��ます。")
667
- return StreamingResponse(img_byte_arr, media_type="image/png")
668
-
669
- except Exception as e:
670
- logger.error(f"API Error: {e}", exc_info=True)
671
- raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}")
672
-
673
- # --- Gradio Interface Definition ---
674
- # 入力モードの選択用Radioコンポーネント
675
- def process_input(input_mode, input_text, extension_percentage, temperature, trim_whitespace, style):
676
- """入力モードに応じて適切な処理を行う"""
677
- if input_mode == "HTML入力":
678
- # HTMLモードの場合は既存の処理(スタイルは使わない)
679
- return render_fullpage_screenshot(input_text, extension_percentage, trim_whitespace)
680
- else:
681
- # テキスト入力モードの場合はGemini APIを使用
682
- return text_to_screenshot(input_text, extension_percentage, temperature, trim_whitespace, style)
683
-
684
- # Gradio UIの定義
685
- with gr.Blocks(title="Full Page Screenshot (テキスト変換対応)", theme=gr.themes.Base()) as iface:
686
- gr.Markdown("# HTMLビューア & テキスト→インフォグラフィック変換")
687
- gr.Markdown("HTMLコードをレンダリングするか、テキストをGemini APIでインフォグラフィックに変換して画像として取得します。")
688
-
689
- with gr.Row():
690
- input_mode = gr.Radio(
691
- ["HTML入力", "テキスト入力"],
692
- label="入力モード",
693
- value="HTML入力"
694
- )
695
-
696
- # 共用のテキストボックス
697
- input_text = gr.Textbox(
698
- lines=15,
699
- label="入力",
700
- placeholder="HTMLコードまたはテキストを入力してください。入力モードに応じて処理されます。"
701
- )
702
-
703
- with gr.Row():
704
- with gr.Column(scale=1):
705
- # スタイル選択ドロップダウン
706
- style_dropdown = gr.Dropdown(
707
- choices=["standard", "cute", "resort", "cool", "dental"],
708
- value="standard",
709
- label="デザインスタイル",
710
- info="テキスト→HTML変換時のデザインテーマを選択します",
711
- visible=False # テキスト入力モードの時だけ表示
712
- )
713
-
714
- with gr.Column(scale=2):
715
- extension_percentage = gr.Slider(
716
- minimum=0,
717
- maximum=30,
718
- step=1.0,
719
- value=10, # デフォルト値10%
720
- label="上下高さ拡張率(%)"
721
- )
722
-
723
- # 温度調整スライダー(テキストモード時のみ表示)
724
- temperature = gr.Slider(
725
- minimum=0.0,
726
- maximum=1.0,
727
- step=0.1,
728
- value=0.5, # デフォルト値を0.5に設定
729
- label="生成時の温度(低い=一貫性高、高い=創造性高)",
730
- visible=False # 最初は非表示
731
- )
732
-
733
- # 余白トリミングオプション
734
- trim_whitespace = gr.Checkbox(
735
- label="余白を自動トリミング",
736
- value=True,
737
- info="生成される画像から余分な空白領域を自動的に削除します"
738
- )
739
-
740
- submit_btn = gr.Button("生成")
741
- output_image = gr.Image(type="pil", label="ページ全体のスクリーンショット")
742
-
743
- # 入力モード変更時のイベント処理(テキストモード時のみ温度スライダーとスタイルドロップダウンを表示)
744
- def update_controls_visibility(mode):
745
- # Gradio 4.x用のアップデート方法
746
- is_text_mode = mode == "テキスト入力"
747
- return [
748
- gr.update(visible=is_text_mode), # temperature
749
- gr.update(visible=is_text_mode), # style_dropdown
750
- ]
751
-
752
- input_mode.change(
753
- fn=update_controls_visibility,
754
- inputs=input_mode,
755
- outputs=[temperature, style_dropdown]
756
- )
757
-
758
- # 生成ボタンクリック時のイベント処理
759
- submit_btn.click(
760
- fn=process_input,
761
- inputs=[input_mode, input_text, extension_percentage, temperature, trim_whitespace, style_dropdown],
762
- outputs=output_image
763
- )
764
-
765
- # 環境変数情報を表示
766
- gemini_model = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
767
- gr.Markdown(f"""
768
- ## APIエンドポイント
769
- - `/api/screenshot` - HTMLコードからスクリーンショットを生成
770
- - `/api/text-to-screenshot` - テキストからインフォグラフィックスクリーンショットを生成
771
-
772
- ## 設定情報
773
- - 使用モデル: {gemini_model} (環境変数 GEMINI_MODEL で変更可能)
774
- - 対応スタイル: standard, cute, resort, cool, dental
775
- """)
776
-
777
- # --- Mount Gradio App onto FastAPI ---
778
- app = gr.mount_gradio_app(app, iface, path="/")
779
-
780
- # --- Run with Uvicorn (for local testing) ---
781
- if __name__ == "__main__":
782
- import uvicorn
783
- logger.info("Starting Uvicorn server for local development...")
784
- uvicorn.run(app, host="0.0.0.0", port=7860)
785
- `````` タグが見つかりませんでした。全テキストを返します。")
786
- return raw_response
787
-
788
- except Exception as e:
789
- logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
790
- raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")
791
-
792
-
793
- # 画像から余分な空白領域をトリミングする関数
794
- def trim_image_whitespace(image, threshold=250, padding=10):
795
- """
796
- 画像から余分な白い空白をトリミングする
797
-
798
- Args:
799
- image: PIL.Image - 入力画像
800
- threshold: int - どの明るさ以上を空白と判断するか (0-255)
801
- padding: int - トリミング後に残す余白のピクセル数
802
-
803
- Returns:
804
- トリミングされたPIL.Image
805
- """
806
- # グレースケールに変換
807
- gray = image.convert('L')
808
-
809
- # ピクセルデータを配列として取得
810
- data = gray.getdata()
811
- width, height = gray.size
812
-
813
- # 有効範囲を見つける
814
- min_x, min_y = width, height
815
- max_x = max_y = 0
816
-
817
- # ピクセルデータを2次元配列に変換して処理
818
- pixels = list(data)
819
- pixels = [pixels[i * width:(i + 1) * width] for i in range(height)]
820
-
821
- # 各行をスキャンして非空白ピクセルを見つける
822
- for y in range(height):
823
- for x in range(width):
824
- if pixels[y][x] < threshold: # 非空白ピクセル
825
- min_x = min(min_x, x)
826
- min_y = min(min_y, y)
827
- max_x = max(max_x, x)
828
- max_y = max(max_y, y)
829
- # 境界外のトリミングの場合はエラー
830
- if min_x > max_x or min_y > max_y:
831
- logger.warning("トリミング領域が見つかりません。元の画像を返します。")
832
- return image
833
-
834
- # パディングを追加
835
- min_x = max(0, min_x - padding)
836
- min_y = max(0, min_y - padding)
837
- max_x = min(width - 1, max_x + padding)
838
- max_y = min(height - 1, max_y + padding)
839
-
840
- # 画像をトリミング
841
- trimmed = image.crop((min_x, min_y, max_x + 1, max_y + 1))
842
-
843
- logger.info(f"画像をトリミングしました: 元サイズ {width}x{height} → トリミング後 {trimmed.width}x{trimmed.height}")
844
- return trimmed
845
-
846
- # 非同期スクリプトを使わず、同期的なスクリプトのみ使用する改善版
847
-
848
- def render_fullpage_screenshot(html_code: str, extension_percentage: float = 6.0,
849
- trim_whitespace: bool = True) -> Image.Image:
850
- """
851
- Renders HTML code to a full-page screenshot using Selenium.
852
-
853
- Args:
854
- html_code: The HTML source code string.
855
- extension_percentage: Percentage of extra space to add vertically.
856
- trim_whitespace: Whether to trim excess whitespace from the image.
857
-
858
- Returns:
859
- A PIL Image object of the screenshot.
860
- """
861
- tmp_path = None
862
- driver = None
863
-
864
- # 1) Save HTML code to a temporary file
865
- try:
866
- with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
867
- tmp_path = tmp_file.name
868
- tmp_file.write(html_code)
869
- logger.info(f"HTML saved to temporary file: {tmp_path}")
870
- except Exception as e:
871
- logger.error(f"Error writing temporary HTML file: {e}", exc_info=True)
872
- return Image.new('RGB', (1, 1), color=(0, 0, 0))
873
-
874
- # 2) Headless Chrome(Chromium) options
875
- options = Options()
876
- options.add_argument("--headless")
877
- options.add_argument("--no-sandbox")
878
- options.add_argument("--disable-dev-shm-usage")
879
- options.add_argument("--force-device-scale-factor=1")
880
- options.add_argument("--disable-features=NetworkService")
881
- options.add_argument("--dns-prefetch-disable")
882
- # 環境変数からWebDriverパスを取得(任意)
883
- webdriver_path = os.environ.get("CHROMEDRIVER_PATH")
884
- service = None
885
- if webdriver_path and os.path.exists(webdriver_path):
886
- logger.info(f"Using CHROMEDRIVER_PATH: {webdriver_path}")
887
- try:
888
- service = webdriver.ChromeService(executable_path=webdriver_path)
889
- except Exception as e:
890
- logger.error(f"Error creating ChromeService with {webdriver_path}: {e}", exc_info=True)
891
- service = None # Fallback to default
892
- if service is None:
893
- logger.info("CHROMEDRIVER_PATH not set or invalid, using default PATH lookup for WebDriver.")
894
-
895
- try:
896
- logger.info("Initializing WebDriver...")
897
- if service:
898
- driver = webdriver.Chrome(service=service, options=options)
899
- else:
900
- driver = webdriver.Chrome(options=options)
901
- logger.info("WebDriver initialized.")
902
-
903
- # 3) 初期ウィンドウサイズを設定
904
- initial_width = 1200
905
- initial_height = 1000
906
- driver.set_window_size(initial_width, initial_height)
907
- file_url = "file://" + tmp_path
908
- logger.info(f"Navigating to {file_url}")
909
- driver.get(file_url)
910
-
911
- # 4) ページ読み込み待機
912
- logger.info("Waiting for body element...")
913
- WebDriverWait(driver, 15).until(
914
- EC.presence_of_element_located((By.TAG_NAME, "body"))
915
- )
916
- logger.info("Body element found. Waiting for resource loading...")
917
-
918
- # 5) 基本的なリソース読み込み待機 - タイムアウト回避
919
- time.sleep(3)
920
-
921
- # Font Awesome読み込み確認 - 非同期を使わない
922
- logger.info("Checking for Font Awesome resources...")
923
- fa_count = driver.execute_script("""
924
- var icons = document.querySelectorAll('.fa, .fas, .far, .fab, [class*="fa-"]');
925
- return icons.length;
926
- """)
927
- logger.info(f"Found {fa_count} Font Awesome elements")
928
-
929
- # リソース読み込み状態を確認
930
- doc_ready = driver.execute_script("return document.readyState;")
931
- logger.info(f"Document ready state: {doc_ready}")
932
-
933
- # Font Awesomeが多い場合は追加待機
934
- if fa_count > 50:
935
- logger.info("Many Font Awesome icons detected, waiting additional time")
936
- time.sleep(2)
937
-
938
- # 6) コンテンツレンダリングのためのスクロール処理 - 同期的に実行
939
- logger.info("Performing content rendering scroll...")
940
- total_height = driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);")
941
- viewport_height = driver.execute_script("return window.innerHeight;")
942
- scrolls_needed = max(1, total_height // viewport_height)
943
-
944
- for i in range(scrolls_needed + 1):
945
- scroll_pos = i * (viewport_height - 200) # オーバーラップさせる
946
- driver.execute_script(f"window.scrollTo(0, {scroll_pos});")
947
- time.sleep(0.2) # 短い待機
948
-
949
- # トップに戻る
950
- driver.execute_script("window.scrollTo(0, 0);")
951
- time.sleep(0.5)
952
- logger.info("Scroll rendering completed")
953
-
954
- # 7) スクロールバーを非表示に
955
- driver.execute_script("""
956
- document.documentElement.style.overflow = 'hidden';
957
- document.body.style.overflow = 'hidden';
958
- """)
959
- logger.info("Scrollbars hidden")
960
-
961
- # 8) ページの寸法を取得
962
- dimensions = driver.execute_script("""
963
- return {
964
- width: Math.max(
965
- document.documentElement.scrollWidth,
966
- document.documentElement.offsetWidth,
967
- document.documentElement.clientWidth,
968
- document.body ? document.body.scrollWidth : 0,
969
- document.body ? document.body.offsetWidth : 0,
970
- document.body ? document.body.clientWidth : 0
971
- ),
972
- height: Math.max(
973
- document.documentElement.scrollHeight,
974
- document.documentElement.offsetHeight,
975
- document.documentElement.clientHeight,
976
- document.body ? document.body.scrollHeight : 0,
977
- document.body ? document.body.offsetHeight : 0,
978
- document.body ? document.body.clientHeight : 0
979
- )
980
- };
981
- """)
982
- scroll_width = dimensions['width']
983
- scroll_height = dimensions['height']
984
- logger.info(f"Detected dimensions: width={scroll_width}, height={scroll_height}")
985
-
986
- # 再検証 - 短いスクロールで再確認
987
- driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
988
- time.sleep(0.5)
989
- driver.execute_script("window.scrollTo(0, 0);")
990
- time.sleep(0.5)
991
-
992
- dimensions_after = driver.execute_script("return {height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)};")
993
- scroll_height = max(scroll_height, dimensions_after['height'])
994
- logger.info(f"After scroll check, height={scroll_height}")
995
-
996
- # 最小/最大値の設定
997
- scroll_width = max(scroll_width, 100)
998
- scroll_height = max(scroll_height, 100)
999
- scroll_width = min(scroll_width, 2000)
1000
- scroll_height = min(scroll_height, 4000)
1001
-
1002
- # 9) レイアウト安定化のための単純な待機 - タイムアウト回避
1003
- logger.info("Waiting for layout stabilization...")
1004
- time.sleep(2)
1005
-
1006
- # 10) 高さに余白を追加
1007
- adjusted_height = int(scroll_height * (1 + extension_percentage / 100.0))
1008
- adjusted_height = max(adjusted_height, scroll_height, 100)
1009
- logger.info(f"Adjusted height calculated: {adjusted_height} (extension: {extension_percentage}%)")
1010
-
1011
- # 11) ウィンドウサイズを調整
1012
- adjusted_width = scroll_width
1013
- logger.info(f"Resizing window to: width={adjusted_width}, height={adjusted_height}")
1014
- driver.set_window_size(adjusted_width, adjusted_height)
1015
- time.sleep(1)
1016
-
1017
- # リソース状態を確認 - 同期的スクリプト
1018
- resource_state = driver.execute_script("""
1019
- return {
1020
- readyState: document.readyState,
1021
- resourcesComplete: !document.querySelector('img:not([complete])') &&
1022
- !document.querySelector('link[rel="stylesheet"]:not([loaded])')
1023
- };
1024
- """)
1025
- logger.info(f"Resource state: {resource_state}")
1026
-
1027
- if resource_state['readyState'] != 'complete':
1028
- logger.info("Document still loading, waiting additional time...")
1029
- time.sleep(1)
1030
-
1031
- # トップにスクロール
1032
- driver.execute_script("window.scrollTo(0, 0);")
1033
- time.sleep(0.5)
1034
- logger.info("Scrolled to top.")
1035
-
1036
- # 12) スクリーンショット取得
1037
- logger.info("Taking screenshot...")
1038
- png = driver.get_screenshot_as_png()
1039
- logger.info("Screenshot taken successfully.")
1040
-
1041
- # PIL画像に変換
1042
- img = Image.open(BytesIO(png))
1043
- logger.info(f"Screenshot dimensions: {img.width}x{img.height}")
1044
-
1045
- # 余白トリミング
1046
- if trim_whitespace:
1047
- img = trim_image_whitespace(img, threshold=248, padding=20)
1048
- logger.info(f"Trimmed dimensions: {img.width}x{img.height}")
1049
-
1050
- return img
1051
-
1052
- except Exception as e:
1053
- logger.error(f"Error during screenshot generation: {e}", exc_info=True)
1054
- # Return a small black image on error
1055
- return Image.new('RGB', (1, 1), color=(0, 0, 0))
1056
- finally:
1057
- logger.info("Cleaning up...")
1058
- if driver:
1059
- try:
1060
- driver.quit()
1061
- logger.info("WebDriver quit successfully.")
1062
- except Exception as e:
1063
- logger.error(f"Error quitting WebDriver: {e}", exc_info=True)
1064
- if tmp_path and os.path.exists(tmp_path):
1065
- try:
1066
- os.remove(tmp_path)
1067
- logger.info(f"Temporary file {tmp_path} removed.")
1068
- except Exception as e:
1069
- logger.error(f"Error removing temporary file {tmp_path}: {e}", exc_info=True)
1070
-
1071
-
1072
  # --- Geminiを使った新しい関数 ---
1073
  def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 0.3,
1074
  trim_whitespace: bool = True, style: str = "standard") -> Image.Image:
 
15
  import time
16
  import os
17
  import logging
18
+ from huggingface_hub import hf_hub_download # 追加: HuggingFace Hubからファイルを直接ダウンロード
19
 
20
+ # 正しいGemini関連のインポート
21
  import google.generativeai as genai
 
22
 
23
  # ロギング設定
24
  logging.basicConfig(level=logging.INFO)
 
38
  html_code: str
39
  extension_percentage: float = 10.0 # デフォルト値10%
40
  trim_whitespace: bool = True # 余白トリミングオプション(デフォルト有効)
41
+ style: str = "standard" # デフォルトはstandard
42
 
43
  # HTMLのFont Awesomeレイアウトを改善する関数
44
  def enhance_font_awesome_layout(html_code):
 
52
  margin-right: 8px !important;
53
  vertical-align: middle !important;
54
  }
55
+
56
  /* テキスト内のアイコン位置調整 */
57
+ h1 [class*="fa-"], h2 [class*="fa-"], h3 [class*="fa-"],
58
  h4 [class*="fa-"], h5 [class*="fa-"], h6 [class*="fa-"] {
59
  vertical-align: middle !important;
60
  margin-right: 10px !important;
61
  }
62
+
63
  /* 特定パターンの修正 */
64
  .fa + span, .fas + span, .far + span, .fab + span,
65
+ span + .fa, span + .fas, span + .far + span {
66
  display: inline-block !important;
67
  margin-left: 5px !important;
68
  }
69
+
70
  /* カード内アイコン修正 */
71
  .card [class*="fa-"], .card-body [class*="fa-"] {
72
  float: none !important;
73
  clear: none !important;
74
  position: relative !important;
75
  }
76
+
77
  /* アイコンと文字が重なる場合の調整 */
78
  li [class*="fa-"], p [class*="fa-"] {
79
  margin-right: 10px !important;
80
  }
81
+
82
  /* インラインアイコンのスペーシング */
83
  .inline-icon {
84
  display: inline-flex !important;
85
  align-items: center !important;
86
  justify-content: flex-start !important;
87
  }
88
+
89
  /* アイコン後のテキスト */
90
  [class*="fa-"] + span {
91
  display: inline-block !important;
 
177
  logger.error(error_msg)
178
  raise ValueError(error_msg)
179
 
 
180
  def generate_html_from_text(text, temperature=0.3, style="standard"):
181
  """テキストからHTMLを生成する"""
182
  try:
 
187
  raise ValueError("GEMINI_API_KEY 環境変数が設定されていません")
188
 
189
  # モデル名の取得(環境変数から、なければデフォルト値)
 
190
  model_name = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
191
  logger.info(f"使用するGeminiモデル: {model_name}")
192
 
 
196
  # 指定されたスタイルのシステムインストラクションを読み込む
197
  system_instruction = load_system_instruction(style)
198
 
199
+ # モデル初期化
200
+ logger.info(f"Gemini APIにリクエストを送信: テキスト長さ = {len(text)}, 温度 = {temperature}, スタイル = {style}")
201
+
202
  # モデル初期化
203
  model = genai.GenerativeModel(model_name)
204
 
205
  # 生成設定 - ばらつきを減らすために設定を調整
206
+ generation_config = {
207
+ "temperature": temperature, # より低い温度を設定
208
+ "top_p": 0.7, # 0.95から0.7に下げて出力の多様性を制限
209
+ "top_k": 20, # 64から20に下げて候補を絞る
 
210
  "max_output_tokens": 8192,
211
+ "candidate_count": 1 # 候補は1つだけ生成
212
  }
213
 
214
+ # 安全設定 - デフォルトの安全設定を使用
215
+ safety_settings = [
216
+ {
217
+ "category": "HARM_CATEGORY_HARASSMENT",
218
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
219
+ },
220
+ {
221
+ "category": "HARM_CATEGORY_HATE_SPEECH",
222
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
223
+ },
224
+ {
225
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
226
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
227
+ },
228
+ {
229
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
230
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
231
+ }
232
+ ]
233
 
234
+ # プロンプト構築
235
+ prompt = f"{system_instruction}\n\n{text}"
236
 
237
  # コンテンツ生成
 
238
  response = model.generate_content(
239
+ prompt,
240
+ generation_config=generation_config,
241
  safety_settings=safety_settings
242
  )
243
 
 
267
  logger.error(f"HTML生成中にエラーが発生: {e}", exc_info=True)
268
  raise Exception(f"Gemini APIでのHTML生成に失敗しました: {e}")
269
 
 
270
  # 画像から余分な空白領域をトリミングする関数
271
  def trim_image_whitespace(image, threshold=250, padding=10):
272
  """
 
303
  min_y = min(min_y, y)
304
  max_x = max(max_x, x)
305
  max_y = max(max_y, y)
306
+
307
  # 境界外のトリミングの場合はエラー
308
  if min_x > max_x or min_y > max_y:
309
  logger.warning("トリミング領域が見つかりません。元の画像を返します。")
 
346
  tmp_file.write(html_code)
347
  logger.info(f"HTML saved to temporary file: {tmp_path}")
348
  except Exception as e:
349
+ logger.error(f"Error writing temporary HTML file: {e}")
350
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
351
 
352
  # 2) Headless Chrome(Chromium) options
 
359
  options.add_argument("--dns-prefetch-disable")
360
  # 環境変数からWebDriverパスを取得(任意)
361
  webdriver_path = os.environ.get("CHROMEDRIVER_PATH")
 
362
  if webdriver_path and os.path.exists(webdriver_path):
363
  logger.info(f"Using CHROMEDRIVER_PATH: {webdriver_path}")
364
+ service = webdriver.ChromeService(executable_path=webdriver_path)
365
+ else:
366
+ logger.info("CHROMEDRIVER_PATH not set or invalid, using default PATH lookup.")
367
+ service = None # Use default behavior
 
 
 
368
 
369
  try:
370
  logger.info("Initializing WebDriver...")
 
542
  except Exception as e:
543
  logger.error(f"Error removing temporary file {tmp_path}: {e}", exc_info=True)
544
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  # --- Geminiを使った新しい関数 ---
546
  def text_to_screenshot(text: str, extension_percentage: float, temperature: float = 0.3,
547
  trim_whitespace: bool = True, style: str = "standard") -> Image.Image: