tomo2chin2/HTML2PDF_API
0
1import os2import uuid3import tempfile4import json5import re6from datetime import datetime7from pathlib import Path8from typing import Optional9 10import httpx11from fastapi import FastAPI, HTTPException, File, UploadFile12from fastapi.responses import JSONResponse, RedirectResponse13from fastapi.middleware.cors import CORSMiddleware14from pydantic import BaseModel15from playwright.async_api import async_playwright16from huggingface_hub import HfApi, upload_file17 18 19class HTMLRequest(BaseModel):20 html_content: str21 22 23class YAMLRequest(BaseModel):24 yaml_content: str25 26 27class PDFResponse(BaseModel):28 pdf_url: str29 filename: str30 message: str31 repository_url: str32 33 34async def call_gemini_api(yaml_content: str) -> str:35 """36 Gemini APIを呼び出してYAMLコンテンツからHTMLを生成37 """38 try:39 # 環境変数から設定を取得40 gemini_api_key = os.getenv("GEMINI_API_KEY")41 model_id = os.getenv("MODEL", "gemini-2.5-flash")42 system_instruction = os.getenv("SYSTEM", "YAMLデータを基にHTMLを生成してください。")43 44 if not gemini_api_key:45 raise HTTPException(status_code=500, detail="GEMINI_API_KEY環境変数が設定されていません")46 47 # Gemini APIリクエストボディを構築(公式ドキュメント準拠のシンプルな形式)48 # システムインストラクションとYAMLデータを結合49 combined_prompt = f"{system_instruction}\n\n以下のYAMLデータを基にして、美しいHTMLドキュメントを生成してください。\n\n{yaml_content}"50 51 request_body = {52 "contents": [53 {54 "parts": [55 {56 "text": combined_prompt57 }58 ]59 }60 ],61 "generationConfig": {62 "temperature": 0.75,63 "responseMimeType": "text/plain"64 }65 }66 67 # Gemini APIを呼び出し68 url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_id}:generateContent?key={gemini_api_key}"69 70 async with httpx.AsyncClient(timeout=60.0) as client:71 response = await client.post(url, json=request_body)72 response.raise_for_status()73 74 # レスポンスからHTMLを抽出75 result = response.json()76 77 # レスポンス構造を解析してHTMLを抽出78 if "candidates" in result and len(result["candidates"]) > 0:79 candidate = result["candidates"][0]80 if "content" in candidate and "parts" in candidate["content"]:81 parts = candidate["content"]["parts"]82 for part in parts:83 if "text" in part:84 text = part["text"]85 # HTMLタグを含む部分を抽出86 html_match = re.search(r'<html.*?>.*?</html>', text, re.DOTALL | re.IGNORECASE)87 if html_match:88 return html_match.group(0)89 # HTML全体がない場合はbodyタグを探す90 body_match = re.search(r'<body.*?>.*?</body>', text, re.DOTALL | re.IGNORECASE)91 if body_match:92 return f"<html><head><meta charset='utf-8'></head>{body_match.group(0)}</html>"93 # それでもない場合は全体をHTMLとして扱う94 if "<" in text and ">" in text:95 return text96 97 raise HTTPException(status_code=500, detail="Gemini APIレスポンスからHTMLを抽出できませんでした")98 99 except httpx.HTTPError as e:100 raise HTTPException(status_code=500, detail=f"Gemini API呼び出しエラー: {str(e)}")101 except Exception as e:102 raise HTTPException(status_code=500, detail=f"Gemini API処理中にエラーが発生しました: {str(e)}")103 104 105async def html_to_pdf_api(html_content: str) -> tuple[str, str]:106 """107 HTMLコンテンツをPDFに変換してHugging Faceデータセットリポジトリにアップロード108 knowledge.txtのPlaywright手法を使用(Async版)109 """110 try:111 # 環境変数からリポジトリ情報を取得112 hf_repo_id = os.getenv("HF_DATASET_REPO_ID")113 hf_token = os.getenv("HF_TOKEN")114 115 if not hf_repo_id:116 raise HTTPException(status_code=500, detail="HF_DATASET_REPO_ID環境変数が設定されていません")117 if not hf_token:118 raise HTTPException(status_code=500, detail="HF_TOKEN環境変数が設定されていません")119 120 # 一意のファイル名を生成121 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")122 unique_id = str(uuid.uuid4())[:8]123 filename = f"document_{timestamp}_{unique_id}.pdf"124 125 # 一時ファイルでPDFを生成126 with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file:127 temp_path = temp_file.name128 129 # Playwrightでヘッドレスブラウザを起動(Async版)130 async with async_playwright() as pw:131 browser = await pw.chromium.launch(headless=True)132 page = await browser.new_page()133 134 # HTMLコンテンツを設定(外部リソース読み込み待機)135 await page.set_content(html_content, wait_until="networkidle")136 137 # 印刷メディアを有効にする138 await page.emulate_media(media="print")139 140 # PDFを生成(test.htmlの設定に準拠)141 await page.pdf(142 path=temp_path,143 format="A4",144 print_background=True,145 margin={"top":"15mm","bottom":"15mm","left":"15mm","right":"15mm"},146 scale=0.88 # 90%に縮小してA4 2ページに収める147 )148 149 await browser.close()150 151 # Hugging Face リポジトリにアップロード152 api = HfApi(token=hf_token)153 154 upload_file(155 path_or_fileobj=temp_path,156 path_in_repo=f"pdfs/{filename}",157 repo_id=hf_repo_id,158 repo_type="dataset",159 token=hf_token,160 commit_message=f"Add PDF: {filename}"161 )162 163 # 一時ファイルを削除164 os.unlink(temp_path)165 166 # ダウンロードURLを生成167 pdf_url = f"https://huggingface.co/datasets/{hf_repo_id}/resolve/main/pdfs/{filename}"168 169 return filename, pdf_url170 171 except HTTPException:172 raise173 except Exception as e:174 raise HTTPException(status_code=500, detail=f"PDF生成中にエラーが発生しました: {str(e)}")175 176 177# FastAPIアプリケーションの初期化178app = FastAPI(179 title="HTML to PDF Converter API",180 description="日本語対応のHTML→PDF変換API。複雑なレイアウトやWebフォントを含むHTMLコンテンツを、正確にA4サイズのPDFに変換します。",181 version="1.0.0",182 docs_url="/docs",183 redoc_url="/redoc"184)185 186# CORS設定187app.add_middleware(188 CORSMiddleware,189 allow_origins=["*"],190 allow_credentials=True,191 allow_methods=["*"],192 allow_headers=["*"],193)194 195 196@app.get("/")197async def root():198 """199 API情報を返すルートエンドポイント200 """201 hf_repo_id = os.getenv("HF_DATASET_REPO_ID", "未設定")202 return {203 "message": "HTML to PDF Converter API",204 "description": "HTMLコンテンツをA4サイズのPDFに変換し、Hugging Faceデータセットリポジトリに保存するAPI",205 "version": "1.0.0",206 "storage": f"Hugging Face Dataset Repository: {hf_repo_id}",207 "endpoints": {208 "convert": "/convert - HTMLをPDFに変換してHFリポジトリに保存",209 "convert-yaml": "/convert-yaml - YAMLをGemini APIでHTMLに変換してPDF化",210 "files": "/files - HFリポジトリ内のPDFファイル一覧",211 "docs": "/docs - API仕様書",212 "health": "/health - ヘルスチェック"213 }214 }215 216 217@app.post("/convert", response_model=PDFResponse)218async def convert_html_to_pdf(request: HTMLRequest):219 """220 HTMLコンテンツをPDFに変換してHugging Faceデータセットリポジトリに保存するエンドポイント221 222 - **html_content**: 変換するHTMLコンテンツ223 224 Returns:225 - **pdf_url**: 生成されたPDFのダウンロードURL(Hugging Face上)226 - **filename**: PDFファイル名227 - **message**: 処理結果メッセージ228 - **repository_url**: リポジトリURL229 """230 if not request.html_content or not request.html_content.strip():231 raise HTTPException(status_code=400, detail="HTMLコンテンツが空です")232 233 try:234 filename, pdf_url = await html_to_pdf_api(request.html_content)235 hf_repo_id = os.getenv("HF_DATASET_REPO_ID")236 repository_url = f"https://huggingface.co/datasets/{hf_repo_id}"237 238 return PDFResponse(239 pdf_url=pdf_url,240 filename=filename,241 message=f"✅ PDFが正常に生成され、Hugging Faceリポジトリに保存されました: {filename}",242 repository_url=repository_url243 )244 except HTTPException:245 raise246 except Exception as e:247 raise HTTPException(status_code=500, detail=f"処理中にエラーが発生しました: {str(e)}")248 249 250@app.get("/download/{filename}")251async def download_pdf(filename: str):252 """253 Hugging FaceリポジトリのPDFファイルへリダイレクトするエンドポイント254 255 - **filename**: ダウンロードするPDFファイル名256 """257 hf_repo_id = os.getenv("HF_DATASET_REPO_ID")258 if not hf_repo_id:259 raise HTTPException(status_code=500, detail="HF_DATASET_REPO_ID環境変数が設定されていません")260 261 # Hugging Face上のファイルURLにリダイレクト262 pdf_url = f"https://huggingface.co/datasets/{hf_repo_id}/resolve/main/pdfs/{filename}"263 return RedirectResponse(url=pdf_url)264 265 266@app.get("/health")267async def health_check():268 """269 ヘルスチェックエンドポイント270 """271 return {"status": "healthy", "timestamp": datetime.now().isoformat()}272 273 274@app.post("/convert-yaml", response_model=PDFResponse)275async def convert_yaml_to_pdf(request: YAMLRequest):276 """277 YAMLコンテンツをGemini APIでHTMLに変換し、PDFを生成してHugging Faceデータセットリポジトリに保存するエンドポイント278 279 - **yaml_content**: 変換するYAMLコンテンツ280 281 Returns:282 - **pdf_url**: 生成されたPDFのダウンロードURL(Hugging Face上)283 - **filename**: PDFファイル名284 - **message**: 処理結果メッセージ285 - **repository_url**: リポジトリURL286 """287 if not request.yaml_content or not request.yaml_content.strip():288 raise HTTPException(status_code=400, detail="YAMLコンテンツが空です")289 290 try:291 # YAMLからGemini APIでHTMLを生成292 html_content = await call_gemini_api(request.yaml_content)293 294 # 生成されたHTMLをPDFに変換295 filename, pdf_url = await html_to_pdf_api(html_content)296 hf_repo_id = os.getenv("HF_DATASET_REPO_ID")297 repository_url = f"https://huggingface.co/datasets/{hf_repo_id}"298 299 return PDFResponse(300 pdf_url=pdf_url,301 filename=filename,302 message=f"✅ YAMLからPDFが正常に生成され、Hugging Faceリポジトリに保存されました: {filename}",303 repository_url=repository_url304 )305 except HTTPException:306 raise307 except Exception as e:308 raise HTTPException(status_code=500, detail=f"処理中にエラーが発生しました: {str(e)}")309 310 311@app.get("/files")312async def list_files():313 """314 Hugging Faceリポジトリ内のPDFファイル一覧を取得するエンドポイント315 """316 try:317 hf_repo_id = os.getenv("HF_DATASET_REPO_ID")318 hf_token = os.getenv("HF_TOKEN")319 320 if not hf_repo_id:321 raise HTTPException(status_code=500, detail="HF_DATASET_REPO_ID環境変数が設定されていません")322 if not hf_token:323 raise HTTPException(status_code=500, detail="HF_TOKEN環境変数が設定されていません")324 325 api = HfApi(token=hf_token)326 327 # リポジトリ内のファイル一覧を取得328 try:329 repo_files = api.list_repo_files(repo_id=hf_repo_id, repo_type="dataset")330 pdf_files = [f for f in repo_files if f.startswith("pdfs/") and f.endswith(".pdf")]331 332 files = []333 for file_path in pdf_files:334 filename = Path(file_path).name335 file_info = api.get_paths_info(repo_id=hf_repo_id, paths=[file_path], repo_type="dataset")[0]336 337 files.append({338 "filename": filename,339 "path": file_path,340 "size": file_info.size if hasattr(file_info, 'size') else 0,341 "last_modified": file_info.last_commit.date.isoformat() if hasattr(file_info, 'last_commit') and file_info.last_commit else None,342 "download_url": f"https://huggingface.co/datasets/{hf_repo_id}/resolve/main/{file_path}",343 "api_download_url": f"/download/{filename}"344 })345 346 return {347 "repository": hf_repo_id,348 "total_files": len(files),349 "files": files350 }351 352 except Exception as e:353 # リポジトリが空の場合やpdfsフォルダが存在しない場合354 return {355 "repository": hf_repo_id,356 "total_files": 0,357 "files": [],358 "note": "No PDF files found or repository is empty"359 }360 361 except HTTPException:362 raise363 except Exception as e:364 raise HTTPException(status_code=500, detail=f"ファイル一覧取得中にエラーが発生しました: {str(e)}")365 366 367if __name__ == "__main__":368 import uvicorn369 uvicorn.run(app, host="0.0.0.0", port=7860)