CoolFace
Apppublic

tomo2chin2/HTML2PDF_API

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
app.py369 linesDownload Raw Back to root
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)