tomo2chin2 commited on
Commit
abe5a08
·
verified ·
1 Parent(s): 13ff327

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +233 -6
  2. app.py +507 -0
  3. packages.txt +2 -0
  4. requirements.txt +9 -0
  5. 実装ステップ.md +709 -0
README.md CHANGED
@@ -1,12 +1,239 @@
1
  ---
2
- title: PDF SlideShow
3
- emoji: 🏆
4
  colorFrom: blue
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 5.49.0
8
  app_file: app.py
9
- pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PDF to Video Converter
3
+ emoji: 🎬
4
  colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.19.2
8
  app_file: app.py
9
+ pinned: true
10
  ---
11
 
12
+ # 📄 PDF to Video Converter 🎬
13
+
14
+ PDFファイルをスライドショー動画に変換するAPIです。指定されたURLからPDFをダウンロードし、各ページを画像化して、1ページあたり指定秒数のスライドショー動画を生成します。
15
+
16
+ ## 🌟 機能
17
+
18
+ ### ✅ 主要機能
19
+ - **PDF URLからのダウンロード**: HTTPリクエストで指定されたURLからPDFを取得
20
+ - **PDF→画像変換**: pdf2imageを使用して各ページを高品質な画像に変換
21
+ - **スライドショー動画生成**: 各画像を指定秒数(デフォルト5秒)表示する動画を作成
22
+ - **Hugging Faceアップロード**: 生成した動画をデータセットリポジトリに自動保存
23
+ - **URL返却**: アップロードされた動画のアクセス可能なURLを返却
24
+
25
+ ### 🎨 ユーザーインターフェース
26
+ - **Gradio UI**: 直感的なWebインターフェース
27
+ - **FastAPI**: RESTful APIエンドポイント
28
+ - **リアルタイム進捗表示**: 処理状況を確認可能
29
+ - **動画プレビュー**: 生成された動画をその場で確認
30
+
31
+ ## 🔌 API仕様
32
+
33
+ ### エンドポイント: `POST /api/pdf-to-video`
34
+
35
+ #### リクエスト
36
+ ```json
37
+ {
38
+ "pdf_url": "https://example.com/sample.pdf",
39
+ "duration_per_page": 5,
40
+ "dpi": 150
41
+ }
42
+ ```
43
+
44
+ **パラメータ:**
45
+ - `pdf_url` (string, 必須): PDFファイルのURL
46
+ - `duration_per_page` (integer, オプション): 1ページあたりの表示秒数(デフォルト: 5)
47
+ - `dpi` (integer, オプション): 画像解像度(デフォルト: 150)
48
+
49
+ #### レスポンス
50
+ ```json
51
+ {
52
+ "status": "success",
53
+ "video_url": "https://huggingface.co/datasets/username/repo/resolve/main/videos/video_20250107_123456_abc123.mp4",
54
+ "message": "動画の生成とアップロードに成功しました",
55
+ "total_pages": 10,
56
+ "video_duration": 50.0
57
+ }
58
+ ```
59
+
60
+ **フィールド:**
61
+ - `status` (string): 処理結果("success" または "error")
62
+ - `video_url` (string): 生成された動画のURL
63
+ - `message` (string): 処理結果メッセージ
64
+ - `total_pages` (integer): PDFの総ページ数
65
+ - `video_duration` (float): 動画の総再生時間(秒)
66
+
67
+ ### エンドポイント: `GET /health`
68
+
69
+ ヘルスチェック用エンドポイント
70
+
71
+ #### レスポンス
72
+ ```json
73
+ {
74
+ "status": "healthy",
75
+ "service": "PDF to Video API",
76
+ "hf_configured": true
77
+ }
78
+ ```
79
+
80
+ ## 🛠️ 技術スタック
81
+
82
+ - **Gradio 4.19.2**: WebUI/SDK
83
+ - **FastAPI**: REST APIフレームワーク
84
+ - **pdf2image**: PDF→画像変換
85
+ - **OpenCV (cv2)**: 動画生成エンジン
86
+ - **huggingface_hub**: データセットアップロード
87
+ - **Pillow**: 画像処理
88
+ - **requests**: HTTP通信
89
+
90
+ ## ⚙️ 環境変数
91
+
92
+ 以下の環境変数を設定する必要があります:
93
+
94
+ | 変数名 | 必須 | デフォルト値 | 説明 |
95
+ |--------|------|--------------|------|
96
+ | `HF_TOKEN` | ✅ | - | Hugging Face認証トークン |
97
+ | `HF_REPO_ID` | ❌ | `tomo2chin2/video-storage` | データセットリポジトリID |
98
+
99
+ ### HF_TOKENの取得方法
100
+
101
+ 1. [Hugging Face](https://huggingface.co/)にログイン
102
+ 2. Settings → Access Tokens に移動
103
+ 3. "New token" をクリック
104
+ 4. Write権限を付与してトークンを生成
105
+ 5. 生成されたトークンをコピー
106
+ 6. Space設定の "Repository secrets" に追加
107
+
108
+ ## 🚀 使用方法
109
+
110
+ ### Web UIから使用
111
+
112
+ 1. Space URLにアクセス
113
+ 2. PDF URLを入力欄に貼り付け
114
+ 3. 表示秒数とDPIを調整(任意)
115
+ 4. 「🎬 動画生成」ボタンをクリック
116
+ 5. 生成された動画URLをコピー
117
+
118
+ ### APIから使用
119
+
120
+ #### curlの例
121
+
122
+ ```bash
123
+ curl -X POST "https://your-space.hf.space/api/pdf-to-video" \
124
+ -H "Content-Type: application/json" \
125
+ -d '{
126
+ "pdf_url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
127
+ "duration_per_page": 5,
128
+ "dpi": 150
129
+ }'
130
+ ```
131
+
132
+ #### Pythonの例
133
+
134
+ ```python
135
+ import requests
136
+
137
+ response = requests.post(
138
+ "https://your-space.hf.space/api/pdf-to-video",
139
+ json={
140
+ "pdf_url": "https://example.com/sample.pdf",
141
+ "duration_per_page": 5,
142
+ "dpi": 150
143
+ }
144
+ )
145
+
146
+ result = response.json()
147
+ print(f"動画URL: {result['video_url']}")
148
+ ```
149
+
150
+ #### JavaScriptの例
151
+
152
+ ```javascript
153
+ fetch('https://your-space.hf.space/api/pdf-to-video', {
154
+ method: 'POST',
155
+ headers: {
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ body: JSON.stringify({
159
+ pdf_url: 'https://example.com/sample.pdf',
160
+ duration_per_page: 5,
161
+ dpi: 150
162
+ })
163
+ })
164
+ .then(response => response.json())
165
+ .then(data => console.log('動画URL:', data.video_url));
166
+ ```
167
+
168
+ ## 📊 処理フロー
169
+
170
+ ```
171
+ 1. PDF URLダウンロード
172
+
173
+ 2. PDF→画像変換(pdf2image)
174
+
175
+ 3. 画像サイズ統一(Pillow)
176
+
177
+ 4. 動画生成(OpenCV)
178
+ - 各ページを指定秒数表示
179
+ - 30fps
180
+
181
+ 5. Hugging Faceアップロード
182
+
183
+ 6. URL返却
184
+ ```
185
+
186
+ ## 🧪 テスト用サンプルPDF
187
+
188
+ 以下のURLでテスト可能です:
189
+
190
+ - **W3C ダミーPDF**: `https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf`
191
+ - **小サイズPDF**: 処理時間が短く、動作確認に最適
192
+
193
+ ## ⚠️ 制限事項
194
+
195
+ 1. **PDFサイズ**: 大きなPDF(20ページ以上)は処理に時間がかかります
196
+ 2. **タイムアウト**: Hugging Face Spacesのタイムアウト制限(〜60秒)に注意
197
+ 3. **同時リクエスト**: 大量の同時リクエストには対応していません
198
+ 4. **ファイル形式**: PDFのみ対応(画像ファイルは非対応)
199
+
200
+ ## 🔧 トラブルシューティング
201
+
202
+ ### エラー: "HuggingFace API が初期化されていません"
203
+ → `HF_TOKEN` 環境変数が設定されているか確認してください
204
+
205
+ ### エラー: "PDFのダウンロードに失敗しました"
206
+ → PDF URLが正しいか、アクセス可能か確認してください
207
+
208
+ ### エラー: "PDFの画像変換に失敗しました"
209
+ → PDFが破損していないか確認してください
210
+
211
+ ### 動画が生成されない
212
+ → ログを確認し、どの段階でエラーが発生しているか特定してください
213
+
214
+ ## 📁 ファイル構成
215
+
216
+ ```
217
+ majin/v4_spinoff/HUG/
218
+ ├── app.py # メインアプリケーション
219
+ ├── requirements.txt # Python依存パッケージ
220
+ ├── packages.txt # システムパッケージ
221
+ ├── README.md # このファイル
222
+ └── 実装ステップ.md # 詳細な実装手順
223
+ ```
224
+
225
+ ## 🤝 貢献
226
+
227
+ プルリクエストやイシューの報告を歓迎します。
228
+
229
+ ## 📜 ライセンス
230
+
231
+ MIT License
232
+
233
+ ## 📞 連絡先
234
+
235
+ 問題や質問がある場合は、Issueを作成してください。
236
+
237
+ ---
238
+
239
+ **最終更新**: 2025-10-07
app.py ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PDF to Video Converter API
3
+ PDFをダウンロードして各ページを画像化し、スライドショー動画を生成してHugging Faceにアップロードする
4
+ """
5
+
6
+ import gradio as gr
7
+ from fastapi import FastAPI, HTTPException
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel, HttpUrl
10
+ from typing import Optional
11
+ import requests
12
+ import tempfile
13
+ import os
14
+ import logging
15
+ import numpy as np
16
+ from datetime import datetime
17
+ import uuid
18
+ from pathlib import Path
19
+
20
+ # 画像・動画処理ライブラリ
21
+ from pdf2image import convert_from_path
22
+ from PIL import Image
23
+ import cv2
24
+
25
+ # Hugging Face Hub
26
+ from huggingface_hub import HfApi, login
27
+
28
+ # ロギング設定
29
+ logging.basicConfig(level=logging.INFO)
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # ==============================
33
+ # リクエスト/レスポンスモデル
34
+ # ==============================
35
+
36
+ class PdfToVideoRequest(BaseModel):
37
+ """PDF→動画変換リクエストモデル"""
38
+ pdf_url: HttpUrl
39
+ duration_per_page: int = 5 # デフォルト5秒
40
+ dpi: int = 150 # デフォルトDPI
41
+
42
+ class VideoResponse(BaseModel):
43
+ """動画生成レスポンスモデル"""
44
+ status: str
45
+ video_url: Optional[str] = None
46
+ message: str
47
+ total_pages: Optional[int] = None
48
+ video_duration: Optional[float] = None # 秒
49
+
50
+ # ==============================
51
+ # コア機能実装
52
+ # ==============================
53
+
54
+ def download_pdf_from_url(pdf_url: str) -> str:
55
+ """
56
+ 指定されたURLからPDFをダウンロードして一時ファイルとして保存
57
+
58
+ Args:
59
+ pdf_url: PDFファイルのURL
60
+
61
+ Returns:
62
+ str: ダウンロードされたPDFファイルのパス
63
+
64
+ Raises:
65
+ Exception: ダウンロード失敗時
66
+ """
67
+ try:
68
+ logger.info(f"PDFダウンロード開始: {pdf_url}")
69
+
70
+ # HTTPリクエスト
71
+ response = requests.get(pdf_url, timeout=30, stream=True)
72
+ response.raise_for_status()
73
+
74
+ # Content-Typeの検証
75
+ content_type = response.headers.get('Content-Type', '')
76
+ if 'pdf' not in content_type.lower():
77
+ logger.warning(f"Content-Type が PDF ではありません: {content_type}")
78
+
79
+ # 一時ファイルに保存
80
+ with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_file:
81
+ tmp_path = tmp_file.name
82
+ for chunk in response.iter_content(chunk_size=8192):
83
+ tmp_file.write(chunk)
84
+
85
+ logger.info(f"PDFダウンロード完了: {tmp_path} ({os.path.getsize(tmp_path)} bytes)")
86
+ return tmp_path
87
+
88
+ except requests.exceptions.RequestException as e:
89
+ logger.error(f"PDFダウンロードエラー: {e}")
90
+ raise Exception(f"PDFのダウンロードに失敗しました: {e}")
91
+
92
+ def convert_pdf_to_images(pdf_path: str, dpi: int = 150) -> list:
93
+ """
94
+ PDFファイルを画像リストに変換
95
+
96
+ Args:
97
+ pdf_path: PDFファイルのパス
98
+ dpi: 解像度(デフォルト150)
99
+
100
+ Returns:
101
+ list: PIL.Imageオブジェクトのリスト
102
+ """
103
+ try:
104
+ logger.info(f"PDF→画像変換開始: {pdf_path}, DPI={dpi}")
105
+
106
+ # PDFを画像に変換
107
+ images = convert_from_path(
108
+ pdf_path,
109
+ dpi=dpi,
110
+ fmt='jpeg', # JPEG形式で出力
111
+ thread_count=2 # 並列処理スレッド数
112
+ )
113
+
114
+ logger.info(f"PDF変換完了: {len(images)}ページ")
115
+ return images
116
+
117
+ except Exception as e:
118
+ logger.error(f"PDF変換エラー: {e}")
119
+ raise Exception(f"PDFの画像変換に失敗しました: {e}")
120
+
121
+ def create_video_from_images(
122
+ images: list,
123
+ duration_per_page: int = 5,
124
+ fps: int = 30
125
+ ) -> str:
126
+ """
127
+ 画像リストからスライドショー動画を生成
128
+
129
+ Args:
130
+ images: PIL.Imageオブジェクトのリスト
131
+ duration_per_page: 1ページあたりの表示秒数(デフォルト5秒)
132
+ fps: フレームレート(デフォルト30fps)
133
+
134
+ Returns:
135
+ str: 生成された動画ファイルのパス
136
+ """
137
+ try:
138
+ if not images:
139
+ raise ValueError("画像リストが空です")
140
+
141
+ logger.info(f"動画生成開始: {len(images)}ページ, {duration_per_page}秒/ページ, {fps}fps")
142
+
143
+ # 全画像を同じサイズにリサイズ(最初の画像のサイズに統一)
144
+ first_img = images[0]
145
+ width, height = first_img.size
146
+ logger.info(f"動画サイズ: {width}x{height}")
147
+
148
+ # 一時ファイルパス
149
+ tmp_video = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
150
+ video_path = tmp_video.name
151
+ tmp_video.close()
152
+
153
+ # 動画ライター初期化
154
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
155
+ video_writer = cv2.VideoWriter(
156
+ video_path,
157
+ fourcc,
158
+ fps,
159
+ (width, height)
160
+ )
161
+
162
+ # 各画像を指定秒数分のフレームとして追加
163
+ frames_per_page = duration_per_page * fps
164
+
165
+ for idx, img in enumerate(images):
166
+ logger.info(f"ページ {idx+1}/{len(images)} を処理中...")
167
+
168
+ # 画像をリサイズ(必要な場合)
169
+ if img.size != (width, height):
170
+ img = img.resize((width, height), Image.Resampling.LANCZOS)
171
+
172
+ # PIL Image → OpenCV形式に変換(RGB→BGR)
173
+ img_array = np.array(img)
174
+ if len(img_array.shape) == 3 and img_array.shape[2] == 3:
175
+ img_bgr = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
176
+ else:
177
+ img_bgr = img_array
178
+
179
+ # 同じフレームを複数回書き込み(静止画として表示)
180
+ for _ in range(frames_per_page):
181
+ video_writer.write(img_bgr)
182
+
183
+ video_writer.release()
184
+ logger.info(f"動画生成完了: {video_path} ({os.path.getsize(video_path)} bytes)")
185
+
186
+ return video_path
187
+
188
+ except Exception as e:
189
+ logger.error(f"動画生成エラー: {e}")
190
+ raise Exception(f"動画の生成に失敗しました: {e}")
191
+
192
+ # ==============================
193
+ # Hugging Face アップロード
194
+ # ==============================
195
+
196
+ class VideoUploader:
197
+ """Hugging Face Datasetへの動画アップロード機能"""
198
+
199
+ def __init__(self):
200
+ self.repo_id = os.environ.get("HF_REPO_ID", "tomo2chin2/video-storage")
201
+ self.token = os.environ.get("HF_TOKEN")
202
+
203
+ if not self.token:
204
+ logger.warning("HF_TOKEN 環境変数が設定されていません")
205
+ self.api = None
206
+ return
207
+
208
+ try:
209
+ # ログイン
210
+ login(token=self.token)
211
+ self.api = HfApi()
212
+ logger.info(f"HuggingFace にログイン完了: {self.repo_id}")
213
+ except Exception as e:
214
+ logger.error(f"HuggingFace ログインエラー: {e}")
215
+ self.api = None
216
+
217
+ def upload_video(self, video_path: str, prefix: str = "video") -> str:
218
+ """
219
+ 動画をHugging Faceデータセットにアップロード
220
+
221
+ Args:
222
+ video_path: アップロードする動画ファイルのパス
223
+ prefix: ファイル名のプレフィックス
224
+
225
+ Returns:
226
+ str: アップロードされた動画のURL
227
+ """
228
+ if not self.api:
229
+ raise Exception("HuggingFace API が初期化されていません。HF_TOKEN を確認してください。")
230
+
231
+ try:
232
+ # ユニークなファイル名を生成
233
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
234
+ unique_id = str(uuid.uuid4())[:8]
235
+ filename = f"{prefix}_{timestamp}_{unique_id}.mp4"
236
+ path_in_repo = f"videos/{filename}"
237
+
238
+ logger.info(f"動画アップロード開始: {path_in_repo}")
239
+
240
+ # アップロード実行
241
+ upload_info = self.api.upload_file(
242
+ path_or_fileobj=video_path,
243
+ path_in_repo=path_in_repo,
244
+ repo_id=self.repo_id,
245
+ repo_type="dataset"
246
+ )
247
+
248
+ # URLを構築
249
+ video_url = f"https://huggingface.co/datasets/{self.repo_id}/resolve/main/{path_in_repo}"
250
+
251
+ logger.info(f"動画アップロード完了: {video_url}")
252
+ return video_url
253
+
254
+ except Exception as e:
255
+ logger.error(f"動画アップロードエラー: {e}")
256
+ raise Exception(f"動画のアップロードに失敗しました: {e}")
257
+
258
+ # グローバルなアップローダーインスタンスを作成
259
+ video_uploader = VideoUploader()
260
+
261
+ # ==============================
262
+ # FastAPI アプリケーション
263
+ # ==============================
264
+
265
+ app = FastAPI(
266
+ title="PDF to Video API",
267
+ description="PDFをスライドショー動画に変換するAPI",
268
+ version="1.0.0"
269
+ )
270
+
271
+ # CORS設定
272
+ app.add_middleware(
273
+ CORSMiddleware,
274
+ allow_origins=["*"],
275
+ allow_credentials=True,
276
+ allow_methods=["*"],
277
+ allow_headers=["*"],
278
+ )
279
+
280
+ @app.post(
281
+ "/api/pdf-to-video",
282
+ response_model=VideoResponse,
283
+ tags=["Video Generation"],
284
+ summary="PDFをスライドショー動画に変換",
285
+ description="指定されたURLからPDFをダウンロードし、各ページを画像化して動画を生成します。"
286
+ )
287
+ async def pdf_to_video(request: PdfToVideoRequest):
288
+ """PDF→動画変換APIエンドポイント"""
289
+ pdf_path = None
290
+ video_path = None
291
+
292
+ try:
293
+ logger.info(f"API リクエスト受信: {request.pdf_url}")
294
+
295
+ # 1. PDFダウンロード
296
+ pdf_path = download_pdf_from_url(str(request.pdf_url))
297
+
298
+ # 2. PDF→画像変換
299
+ images = convert_pdf_to_images(pdf_path, dpi=request.dpi)
300
+ total_pages = len(images)
301
+
302
+ # 3. 動画生成
303
+ video_path = create_video_from_images(
304
+ images,
305
+ duration_per_page=request.duration_per_page
306
+ )
307
+
308
+ # 4. Hugging Faceにアップロード
309
+ video_url = video_uploader.upload_video(video_path, prefix="pdf_video")
310
+
311
+ # 動画の総再生時間を計算
312
+ video_duration = total_pages * request.duration_per_page
313
+
314
+ logger.info(f"処理完了: {video_url}")
315
+
316
+ return VideoResponse(
317
+ status="success",
318
+ video_url=video_url,
319
+ message="動画の生成とアップロードに成功しました",
320
+ total_pages=total_pages,
321
+ video_duration=video_duration
322
+ )
323
+
324
+ except Exception as e:
325
+ logger.error(f"エラー発生: {e}", exc_info=True)
326
+ raise HTTPException(
327
+ status_code=500,
328
+ detail=f"動画生成に失敗しました: {str(e)}"
329
+ )
330
+
331
+ finally:
332
+ # 一時ファイルのクリーンアップ
333
+ if pdf_path and os.path.exists(pdf_path):
334
+ try:
335
+ os.remove(pdf_path)
336
+ logger.info(f"一時PDFファイル削除: {pdf_path}")
337
+ except Exception as e:
338
+ logger.warning(f"PDFファイル削除エラー: {e}")
339
+
340
+ if video_path and os.path.exists(video_path):
341
+ try:
342
+ os.remove(video_path)
343
+ logger.info(f"一時動画ファイル削除: {video_path}")
344
+ except Exception as e:
345
+ logger.warning(f"動画ファイル削除エラー: {e}")
346
+
347
+ @app.get("/health")
348
+ async def health_check():
349
+ """ヘルスチェックエンドポイント"""
350
+ return {
351
+ "status": "healthy",
352
+ "service": "PDF to Video API",
353
+ "hf_configured": video_uploader.api is not None
354
+ }
355
+
356
+ # ==============================
357
+ # Gradio UI
358
+ # ==============================
359
+
360
+ def process_pdf_url(pdf_url, duration_per_page, dpi, progress=gr.Progress()):
361
+ """Gradio UIからの処理関数"""
362
+ try:
363
+ if not pdf_url:
364
+ return None, "❌ PDF URLを入力してください", None
365
+
366
+ progress(0, desc="PDFダウンロード中...")
367
+
368
+ # PDFダウンロード
369
+ pdf_path = download_pdf_from_url(pdf_url)
370
+
371
+ progress(0.3, desc="PDF→画像変換中...")
372
+
373
+ # PDF→画像変換
374
+ images = convert_pdf_to_images(pdf_path, dpi=dpi)
375
+ total_pages = len(images)
376
+
377
+ progress(0.6, desc=f"動画生成中({total_pages}ページ)...")
378
+
379
+ # 動画生成
380
+ video_path = create_video_from_images(
381
+ images,
382
+ duration_per_page=duration_per_page
383
+ )
384
+
385
+ progress(0.9, desc="Hugging Faceにアップロード中...")
386
+
387
+ # アップロード
388
+ video_url = video_uploader.upload_video(video_path, prefix="pdf_video")
389
+
390
+ # クリーンアップ
391
+ if pdf_path and os.path.exists(pdf_path):
392
+ os.remove(pdf_path)
393
+ if video_path and os.path.exists(video_path):
394
+ os.remove(video_path)
395
+
396
+ video_duration = total_pages * duration_per_page
397
+
398
+ progress(1.0, desc="完了!")
399
+
400
+ return (
401
+ video_url, # ビデオURL
402
+ f"✅ 成功: {total_pages}ページ、{video_duration}秒の動画を生成しました",
403
+ video_url # プレビュー用
404
+ )
405
+
406
+ except Exception as e:
407
+ logger.error(f"Gradio処理エラー: {e}", exc_info=True)
408
+ return None, f"❌ エラー: {str(e)}", None
409
+
410
+ # Gradio UI定義
411
+ with gr.Blocks(title="PDF to Video Converter", theme=gr.themes.Soft()) as demo:
412
+ gr.Markdown("# 📄 PDF → 🎬 動画変換")
413
+ gr.Markdown("PDFのURLを指定すると、各ページをスライドショー動画に変換します。")
414
+
415
+ with gr.Row():
416
+ with gr.Column(scale=2):
417
+ pdf_url_input = gr.Textbox(
418
+ label="PDF URL",
419
+ placeholder="https://example.com/sample.pdf",
420
+ info="変換したいPDFファイルのURLを入力してください"
421
+ )
422
+
423
+ with gr.Row():
424
+ duration_slider = gr.Slider(
425
+ minimum=1,
426
+ maximum=10,
427
+ step=1,
428
+ value=5,
429
+ label="1ページあたりの表示秒数"
430
+ )
431
+
432
+ dpi_slider = gr.Slider(
433
+ minimum=72,
434
+ maximum=300,
435
+ step=1,
436
+ value=150,
437
+ label="画像解像度(DPI)",
438
+ info="高いほど高画質ですが処理時間が増加します"
439
+ )
440
+
441
+ convert_btn = gr.Button("🎬 動画生成", variant="primary", size="lg")
442
+
443
+ with gr.Column(scale=1):
444
+ status_output = gr.Textbox(
445
+ label="ステータス",
446
+ interactive=False
447
+ )
448
+ video_url_output = gr.Textbox(
449
+ label="動画URL",
450
+ interactive=False,
451
+ info="生成された動画のURL"
452
+ )
453
+
454
+ with gr.Row():
455
+ video_preview = gr.Video(
456
+ label="プレビュー",
457
+ interactive=False
458
+ )
459
+
460
+ # イベント設定
461
+ convert_btn.click(
462
+ fn=process_pdf_url,
463
+ inputs=[pdf_url_input, duration_slider, dpi_slider],
464
+ outputs=[video_url_output, status_output, video_preview]
465
+ )
466
+
467
+ # 使用例とサンプルURL
468
+ gr.Markdown("""
469
+ ## 📖 使用方法
470
+ 1. PDFのURLを入力
471
+ 2. 1ページあたりの表示秒数を調整(デフォルト5秒)
472
+ 3. 解像度(DPI)を調整(デフォルト150)
473
+ 4. 「動画生成」ボタンをクリック
474
+ 5. 生成された動画URLをコピーして利用
475
+
476
+ ## 🔗 サンプルPDF URL(テスト用)
477
+ - W3C サンプル: `https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf`
478
+
479
+ ## ⚙️ 環境変数
480
+ - `HF_TOKEN`: Hugging Face認証トークン(必須)
481
+ - `HF_REPO_ID`: データセットリポジトリID(デフォルト: tomo2chin2/video-storage)
482
+
483
+ ## 🔗 APIエンドポイント
484
+ - `POST /api/pdf-to-video`: PDF→動画変換API
485
+ - `GET /health`: ヘルスチェック
486
+ """)
487
+
488
+ # 環境変数情報表示
489
+ hf_repo = os.environ.get("HF_REPO_ID", "tomo2chin2/video-storage")
490
+ hf_configured = "✅ 設定済み" if video_uploader.api else "❌ 未設定"
491
+ gr.Markdown(f"""
492
+ ## 📊 現在の設定
493
+ - HuggingFace リポジトリ: `{hf_repo}`
494
+ - HF_TOKEN: {hf_configured}
495
+ """)
496
+
497
+ # FastAPIにGradioをマウント
498
+ app = gr.mount_gradio_app(app, demo, path="/")
499
+
500
+ # ==============================
501
+ # アプリケーション起動
502
+ # ==============================
503
+
504
+ if __name__ == "__main__":
505
+ import uvicorn
506
+ logger.info("Starting PDF to Video Converter API...")
507
+ uvicorn.run(app, host="0.0.0.0", port=7860)
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ poppler-utils
2
+ ffmpeg
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==4.19.2
2
+ fastapi
3
+ uvicorn[standard]
4
+ pdf2image
5
+ opencv-python-headless
6
+ Pillow
7
+ requests
8
+ huggingface_hub
9
+ python-dotenv
実装ステップ.md ADDED
@@ -0,0 +1,709 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PDF→動画変換API 実装ステップ
2
+
3
+ ## 📋 プロジェクト概要
4
+
5
+ ### 目的
6
+ PDFファイルをURLから取得し、各ページを画像に分解して5秒ずつ表示するスライドショー動画を生成し、Hugging Faceデータセットリポジトリに保存してURLを返すAPIを構築する。
7
+
8
+ ### 技術スタック
9
+ - **Gradio 4.19.2** - UI/SDK(Hugging Face Spacesのフレームワーク)
10
+ - **FastAPI** - RESTful APIエンドポイント
11
+ - **pdf2image** - PDF→画像変換(popplerベース)
12
+ - **OpenCV (cv2)** - 動画生成
13
+ - **huggingface_hub** - データセットへのファイルアップロード
14
+ - **requests** - PDF URLからのダウンロード
15
+
16
+ ### 処理フロー
17
+ ```
18
+ PDF URL → ダウンロード → PDF→画像変換 → 動画生成(5秒/ページ) → HF Dataset保存 → URL返却
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 🗂️ ディレクトリ構造
24
+
25
+ ```
26
+ majin/v4_spinoff/HUG/
27
+ ├── 実装ステップ.md # 本ドキュメント
28
+ ├── app.py # メインアプリケーション
29
+ ├── requirements.txt # Python依存パッケージ
30
+ ├── packages.txt # システムパッケージ
31
+ ├── README.md # Hugging Face Space設定
32
+ ├── .env.example # 環境変数のサンプル
33
+ └── FASTAPI_GRADIO_sample/ # 参考資料(既存)
34
+ ```
35
+
36
+ ---
37
+
38
+ ## 📝 実装ステップ
39
+
40
+ ### フェーズ1: 環境セットアップ
41
+
42
+ #### ステップ1.1: packages.txt作成
43
+ システムレベルの依存パッケージを定義します。
44
+
45
+ **ファイル: `packages.txt`**
46
+ ```
47
+ poppler-utils
48
+ ffmpeg
49
+ ```
50
+
51
+ **解説:**
52
+ - `poppler-utils`: pdf2imageがPDFをレンダリングするために必要
53
+ - `ffmpeg`: OpenCVの動画エンコーディングに必要
54
+
55
+ #### ステップ1.2: requirements.txt作成
56
+ Pythonパッケージの依存関係を定義します。
57
+
58
+ **ファイル: `requirements.txt`**
59
+ ```
60
+ gradio==4.19.2
61
+ fastapi
62
+ uvicorn[standard]
63
+ pdf2image
64
+ opencv-python-headless
65
+ Pillow
66
+ requests
67
+ huggingface_hub
68
+ python-dotenv
69
+ ```
70
+
71
+ **解説:**
72
+ - `opencv-python-headless`: GUI不要のOpenCV(Spaceに最適)
73
+ - `huggingface_hub`: Dataset APIアクセス用
74
+ - `python-dotenv`: 環境変数管理(開発時)
75
+
76
+ ---
77
+
78
+ ### フェーズ2: コア機能実装
79
+
80
+ #### ステップ2.1: PDFダウンロード機能
81
+
82
+ ```python
83
+ import requests
84
+ import tempfile
85
+ import os
86
+ from pathlib import Path
87
+
88
+ def download_pdf_from_url(pdf_url: str) -> str:
89
+ """
90
+ 指定されたURLからPDFをダウンロードして一時ファイルとして保存
91
+
92
+ Args:
93
+ pdf_url: PDFファイルのURL
94
+
95
+ Returns:
96
+ str: ダウンロードされたPDFファイルのパス
97
+
98
+ Raises:
99
+ Exception: ダウンロード失敗時
100
+ """
101
+ try:
102
+ logger.info(f"PDFダウンロード開始: {pdf_url}")
103
+
104
+ # HTTPリクエスト
105
+ response = requests.get(pdf_url, timeout=30, stream=True)
106
+ response.raise_for_status()
107
+
108
+ # Content-Typeの検証
109
+ content_type = response.headers.get('Content-Type', '')
110
+ if 'pdf' not in content_type.lower():
111
+ logger.warning(f"Content-Type が PDF ではありません: {content_type}")
112
+
113
+ # 一時ファイルに保存
114
+ with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_file:
115
+ tmp_path = tmp_file.name
116
+ for chunk in response.iter_content(chunk_size=8192):
117
+ tmp_file.write(chunk)
118
+
119
+ logger.info(f"PDFダウンロード完了: {tmp_path}")
120
+ return tmp_path
121
+
122
+ except requests.exceptions.RequestException as e:
123
+ logger.error(f"PDFダウンロードエラー: {e}")
124
+ raise Exception(f"PDFのダウンロードに失敗しました: {e}")
125
+ ```
126
+
127
+ **ポイント:**
128
+ - `stream=True`で大きなファイルにも対応
129
+ - タイムアウト設定で無限待機を防止
130
+ - Content-Type検証でPDF以外のファイルを警告
131
+
132
+ #### ステップ2.2: PDF→画像変換機能
133
+
134
+ ```python
135
+ from pdf2image import convert_from_path
136
+ from PIL import Image
137
+ import logging
138
+
139
+ logger = logging.getLogger(__name__)
140
+
141
+ def convert_pdf_to_images(pdf_path: str, dpi: int = 150) -> list:
142
+ """
143
+ PDFファイルを画像リストに変換
144
+
145
+ Args:
146
+ pdf_path: PDFファイルのパス
147
+ dpi: 解像度(デフォルト150)
148
+
149
+ Returns:
150
+ list: PIL.Imageオブジェクトのリスト
151
+ """
152
+ try:
153
+ logger.info(f"PDF→画像変換開始: {pdf_path}, DPI={dpi}")
154
+
155
+ # PDFを画像に変換
156
+ images = convert_from_path(
157
+ pdf_path,
158
+ dpi=dpi,
159
+ fmt='jpeg', # JPEG形式で出力
160
+ thread_count=2 # 並列処理スレッド数
161
+ )
162
+
163
+ logger.info(f"PDF変換完了: {len(images)}ページ")
164
+ return images
165
+
166
+ except Exception as e:
167
+ logger.error(f"PDF変換エラー: {e}")
168
+ raise Exception(f"PDFの画像変換に失敗しました: {e}")
169
+ ```
170
+
171
+ **ポイント:**
172
+ - DPI調整で画質と処理速度のバランス調整
173
+ - JPEG形式で動画サイズを抑制
174
+ - thread_count でリソース使用を制御
175
+
176
+ #### ステップ2.3: 画像→動画生成機能
177
+
178
+ ```python
179
+ import cv2
180
+ import numpy as np
181
+ from PIL import Image
182
+ import tempfile
183
+
184
+ def create_video_from_images(
185
+ images: list,
186
+ duration_per_page: int = 5,
187
+ fps: int = 30
188
+ ) -> str:
189
+ """
190
+ 画像リストからスライドショー動画を生成
191
+
192
+ Args:
193
+ images: PIL.Imageオブジェクトのリスト
194
+ duration_per_page: 1ページあたりの表示秒数(デフォルト5秒)
195
+ fps: フレームレート(デフォルト30fps)
196
+
197
+ Returns:
198
+ str: 生成された動画ファイルのパス
199
+ """
200
+ try:
201
+ if not images:
202
+ raise ValueError("画像リストが空です")
203
+
204
+ logger.info(f"動画生成開始: {len(images)}ページ, {duration_per_page}秒/ページ, {fps}fps")
205
+
206
+ # 全画像を同じサイズにリサイズ(最初の画像のサイズに統一)
207
+ first_img = images[0]
208
+ width, height = first_img.size
209
+ logger.info(f"動画サイズ: {width}x{height}")
210
+
211
+ # 一時ファイルパス
212
+ tmp_video = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
213
+ video_path = tmp_video.name
214
+ tmp_video.close()
215
+
216
+ # 動画ライター初期化
217
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
218
+ video_writer = cv2.VideoWriter(
219
+ video_path,
220
+ fourcc,
221
+ fps,
222
+ (width, height)
223
+ )
224
+
225
+ # 各画像を指定秒数分のフレームとして追加
226
+ frames_per_page = duration_per_page * fps
227
+
228
+ for idx, img in enumerate(images):
229
+ logger.info(f"ページ {idx+1}/{len(images)} を処理中...")
230
+
231
+ # 画像をリサイズ(必要な場合)
232
+ if img.size != (width, height):
233
+ img = img.resize((width, height), Image.Resampling.LANCZOS)
234
+
235
+ # PIL Image → OpenCV形式に変換(RGB→BGR)
236
+ img_array = np.array(img)
237
+ if len(img_array.shape) == 3 and img_array.shape[2] == 3:
238
+ img_bgr = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
239
+ else:
240
+ img_bgr = img_array
241
+
242
+ # 同じフレームを複数回書き込み(静止画として表示)
243
+ for _ in range(frames_per_page):
244
+ video_writer.write(img_bgr)
245
+
246
+ video_writer.release()
247
+ logger.info(f"動画生成完了: {video_path}")
248
+
249
+ return video_path
250
+
251
+ except Exception as e:
252
+ logger.error(f"動画生成エラー: {e}")
253
+ raise Exception(f"動画の生成に失敗しました: {e}")
254
+ ```
255
+
256
+ **ポイント:**
257
+ - 全画像を統一サイズにリサイズ(動画の要件)
258
+ - RGB→BGR変換(OpenCVの要件)
259
+ - フレーム数計算: `duration_per_page * fps`
260
+
261
+ #### ステップ2.4: Hugging Faceアップロード機能
262
+
263
+ ```python
264
+ from huggingface_hub import HfApi, login
265
+ import os
266
+ from datetime import datetime
267
+ import uuid
268
+
269
+ class VideoUploader:
270
+ """Hugging Face Datasetへの動画アップロード機能"""
271
+
272
+ def __init__(self):
273
+ self.repo_id = os.environ.get("HF_REPO_ID", "tomo2chin2/video-storage")
274
+ self.token = os.environ.get("HF_TOKEN")
275
+
276
+ if not self.token:
277
+ raise ValueError("HF_TOKEN 環境変数が設定されていません")
278
+
279
+ # ログイン
280
+ login(token=self.token)
281
+ self.api = HfApi()
282
+ logger.info(f"HuggingFace にログイン完了: {self.repo_id}")
283
+
284
+ def upload_video(self, video_path: str, prefix: str = "video") -> str:
285
+ """
286
+ 動画をHugging Faceデータセットにアップロード
287
+
288
+ Args:
289
+ video_path: アップロードする動画ファイルのパス
290
+ prefix: ファイル名のプレフィックス
291
+
292
+ Returns:
293
+ str: アップロードされた動画のURL
294
+ """
295
+ try:
296
+ # ユニークなファイル名を生成
297
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
298
+ unique_id = str(uuid.uuid4())[:8]
299
+ filename = f"{prefix}_{timestamp}_{unique_id}.mp4"
300
+ path_in_repo = f"videos/{filename}"
301
+
302
+ logger.info(f"動画アップロード開始: {path_in_repo}")
303
+
304
+ # アップロード実行
305
+ upload_info = self.api.upload_file(
306
+ path_or_fileobj=video_path,
307
+ path_in_repo=path_in_repo,
308
+ repo_id=self.repo_id,
309
+ repo_type="dataset"
310
+ )
311
+
312
+ # URLを構築
313
+ video_url = f"https://huggingface.co/datasets/{self.repo_id}/resolve/main/{path_in_repo}"
314
+
315
+ logger.info(f"動画アップロード完了: {video_url}")
316
+ return video_url
317
+
318
+ except Exception as e:
319
+ logger.error(f"動画アップロードエラー: {e}")
320
+ raise Exception(f"動画のアップロードに失敗しました: {e}")
321
+ ```
322
+
323
+ **ポイント:**
324
+ - タイムスタンプ+UUIDでファイル名の一意性を保証
325
+ - `repo_type="dataset"` でデータセットリポジトリを指定
326
+ - URL形式: `https://huggingface.co/datasets/{repo_id}/resolve/main/{path}`
327
+
328
+ ---
329
+
330
+ ### フェーズ3: FastAPI実装
331
+
332
+ #### ステップ3.1: リクエスト/レスポンスモデル定義
333
+
334
+ ```python
335
+ from pydantic import BaseModel, HttpUrl
336
+ from typing import Optional
337
+
338
+ class PdfToVideoRequest(BaseModel):
339
+ """PDF→動画変換リクエストモデル"""
340
+ pdf_url: HttpUrl
341
+ duration_per_page: int = 5 # デフォルト5秒
342
+ dpi: int = 150 # デフォルトDPI
343
+
344
+ class VideoResponse(BaseModel):
345
+ """動画生成レスポンスモデル"""
346
+ status: str
347
+ video_url: Optional[str] = None
348
+ message: str
349
+ total_pages: Optional[int] = None
350
+ video_duration: Optional[float] = None # 秒
351
+ ```
352
+
353
+ #### ステップ3.2: APIエンドポイント実装
354
+
355
+ ```python
356
+ from fastapi import FastAPI, HTTPException
357
+ from fastapi.middleware.cors import CORSMiddleware
358
+ import logging
359
+
360
+ # ロギング設定
361
+ logging.basicConfig(level=logging.INFO)
362
+ logger = logging.getLogger(__name__)
363
+
364
+ app = FastAPI(title="PDF to Video API")
365
+
366
+ # CORS設定
367
+ app.add_middleware(
368
+ CORSMiddleware,
369
+ allow_origins=["*"],
370
+ allow_credentials=True,
371
+ allow_methods=["*"],
372
+ allow_headers=["*"],
373
+ )
374
+
375
+ # グローバルなアップローダーインスタンス
376
+ video_uploader = VideoUploader()
377
+
378
+ @app.post(
379
+ "/api/pdf-to-video",
380
+ response_model=VideoResponse,
381
+ tags=["Video Generation"],
382
+ summary="PDFをスライドショー動画に変換",
383
+ description="指定されたURLからPDFをダウンロードし、各ページを画像化して動画を生成します。"
384
+ )
385
+ async def pdf_to_video(request: PdfToVideoRequest):
386
+ """PDF→動画変換APIエンドポイント"""
387
+ pdf_path = None
388
+ video_path = None
389
+
390
+ try:
391
+ logger.info(f"API リクエスト受信: {request.pdf_url}")
392
+
393
+ # 1. PDFダウンロード
394
+ pdf_path = download_pdf_from_url(str(request.pdf_url))
395
+
396
+ # 2. PDF→画像変換
397
+ images = convert_pdf_to_images(pdf_path, dpi=request.dpi)
398
+ total_pages = len(images)
399
+
400
+ # 3. 動画生成
401
+ video_path = create_video_from_images(
402
+ images,
403
+ duration_per_page=request.duration_per_page
404
+ )
405
+
406
+ # 4. Hugging Faceにアップロード
407
+ video_url = video_uploader.upload_video(video_path, prefix="pdf_video")
408
+
409
+ # 動画の総再生時間を計算
410
+ video_duration = total_pages * request.duration_per_page
411
+
412
+ logger.info(f"処理完了: {video_url}")
413
+
414
+ return VideoResponse(
415
+ status="success",
416
+ video_url=video_url,
417
+ message="動画の生成とアップロードに成功しました",
418
+ total_pages=total_pages,
419
+ video_duration=video_duration
420
+ )
421
+
422
+ except Exception as e:
423
+ logger.error(f"エラー発生: {e}", exc_info=True)
424
+ raise HTTPException(
425
+ status_code=500,
426
+ detail=f"動画生成に失敗しました: {str(e)}"
427
+ )
428
+
429
+ finally:
430
+ # 一時ファイルのクリーンアップ
431
+ if pdf_path and os.path.exists(pdf_path):
432
+ try:
433
+ os.remove(pdf_path)
434
+ logger.info(f"一時PDFファイル削除: {pdf_path}")
435
+ except Exception as e:
436
+ logger.warning(f"PDFファイル削除エラー: {e}")
437
+
438
+ if video_path and os.path.exists(video_path):
439
+ try:
440
+ os.remove(video_path)
441
+ logger.info(f"一時動画ファイル削除: {video_path}")
442
+ except Exception as e:
443
+ logger.warning(f"動画ファイル削除エラー: {e}")
444
+
445
+ @app.get("/health")
446
+ async def health_check():
447
+ """ヘルスチェックエンドポイント"""
448
+ return {"status": "healthy", "service": "PDF to Video API"}
449
+ ```
450
+
451
+ ---
452
+
453
+ ### フェーズ4: Gradio UI実装
454
+
455
+ ```python
456
+ import gradio as gr
457
+
458
+ def process_pdf_url(pdf_url, duration_per_page, dpi):
459
+ """Gradio UIからの処理関数"""
460
+ try:
461
+ if not pdf_url:
462
+ return None, "PDF URLを入力してください", None
463
+
464
+ # 内部的にAPI関数を呼び出し
465
+ request = PdfToVideoRequest(
466
+ pdf_url=pdf_url,
467
+ duration_per_page=duration_per_page,
468
+ dpi=dpi
469
+ )
470
+
471
+ # 同期的に処理(Gradioは非同期不要)
472
+ import asyncio
473
+ result = asyncio.run(pdf_to_video(request))
474
+
475
+ return (
476
+ result.video_url, # ビデオURL
477
+ f"✅ 成功: {result.total_pages}ページ、{result.video_duration}秒の動画を生成しました",
478
+ result.video_url # プレビュー用
479
+ )
480
+
481
+ except Exception as e:
482
+ logger.error(f"Gradio処理エラー: {e}")
483
+ return None, f"❌ エラー: {str(e)}", None
484
+
485
+ # Gradio UI定義
486
+ with gr.Blocks(title="PDF to Video Converter", theme=gr.themes.Soft()) as demo:
487
+ gr.Markdown("# 📄 PDF → 🎬 動画変換")
488
+ gr.Markdown("PDFのURLを指定すると、各ページをスライドショー動画に変換します。")
489
+
490
+ with gr.Row():
491
+ with gr.Column(scale=2):
492
+ pdf_url_input = gr.Textbox(
493
+ label="PDF URL",
494
+ placeholder="https://example.com/sample.pdf",
495
+ info="変換したいPDFファイルのURLを入力してください"
496
+ )
497
+
498
+ with gr.Row():
499
+ duration_slider = gr.Slider(
500
+ minimum=1,
501
+ maximum=10,
502
+ step=1,
503
+ value=5,
504
+ label="1ページあたりの表示秒数"
505
+ )
506
+
507
+ dpi_slider = gr.Slider(
508
+ minimum=72,
509
+ maximum=300,
510
+ step=1,
511
+ value=150,
512
+ label="画像解像度(DPI)",
513
+ info="高いほど高画質ですが処理時間が増加します"
514
+ )
515
+
516
+ convert_btn = gr.Button("🎬 動画生成", variant="primary", size="lg")
517
+
518
+ with gr.Column(scale=1):
519
+ status_output = gr.Textbox(
520
+ label="ステータス",
521
+ interactive=False
522
+ )
523
+ video_url_output = gr.Textbox(
524
+ label="動画URL",
525
+ interactive=False,
526
+ info="生成された動画のURL"
527
+ )
528
+
529
+ with gr.Row():
530
+ video_preview = gr.Video(
531
+ label="プレビュー",
532
+ interactive=False
533
+ )
534
+
535
+ # イベント設定
536
+ convert_btn.click(
537
+ fn=process_pdf_url,
538
+ inputs=[pdf_url_input, duration_slider, dpi_slider],
539
+ outputs=[video_url_output, status_output, video_preview]
540
+ )
541
+
542
+ # 使用例
543
+ gr.Markdown("""
544
+ ## 📖 使用方法
545
+ 1. PDFのURLを入力
546
+ 2. 1ページあたりの表示秒数を調整(デフォルト5秒)
547
+ 3. 解像度(DPI)を調整(デフォルト150)
548
+ 4. 「動画生成」ボタンをクリック
549
+ 5. 生成された動画URLをコピーして利用
550
+
551
+ ## ⚙️ 環境変数
552
+ - `HF_TOKEN`: Hugging Face認証トークン(必須)
553
+ - `HF_REPO_ID`: データセットリポジトリID(例: username/repo-name)
554
+
555
+ ## 🔗 APIエンドポイント
556
+ - `POST /api/pdf-to-video`: PDF→動画変換API
557
+ - `GET /health`: ヘルスチェック
558
+ """)
559
+
560
+ # FastAPIにGradioをマウント
561
+ app = gr.mount_gradio_app(app, demo, path="/")
562
+ ```
563
+
564
+ ---
565
+
566
+ ### フェーズ5: 設定ファイル作成
567
+
568
+ #### README.md (Hugging Face Space設定)
569
+
570
+ ```markdown
571
+ ---
572
+ title: PDF to Video Converter
573
+ emoji: 🎬
574
+ colorFrom: blue
575
+ colorTo: purple
576
+ sdk: gradio
577
+ sdk_version: 4.19.2
578
+ app_file: app.py
579
+ pinned: true
580
+ ---
581
+
582
+ # PDF to Video Converter
583
+
584
+ PDFファイルをスライドショー動画に変換するAPIです。
585
+
586
+ ## 機能
587
+ - PDF URLからのダウンロード
588
+ - 各ページを画像化
589
+ - 1ページ5秒のスライドショー動画生成
590
+ - Hugging Faceデータセットへの自動アップロード
591
+
592
+ ## API仕様
593
+ - `POST /api/pdf-to-video`
594
+
595
+ ## 環境変数
596
+ - `HF_TOKEN`: 必須
597
+ - `HF_REPO_ID`: データセットリポジトリID
598
+ ```
599
+
600
+ ---
601
+
602
+ ## 🚀 デプロイ手順
603
+
604
+ ### 1. Hugging Face Spaceの作成
605
+ 1. https://huggingface.co/new-space にアクセス
606
+ 2. Space名を入力(例: `pdf-to-video-converter`)
607
+ 3. SDKで「Gradio」を選択
608
+ 4. 「Create Space」をクリック
609
+
610
+ ### 2. ファイルのアップロード
611
+ ```bash
612
+ git clone https://huggingface.co/spaces/your-username/pdf-to-video-converter
613
+ cd pdf-to-video-converter
614
+
615
+ # ファイルをコピー
616
+ cp majin/v4_spinoff/HUG/app.py .
617
+ cp majin/v4_spinoff/HUG/requirements.txt .
618
+ cp majin/v4_spinoff/HUG/packages.txt .
619
+ cp majin/v4_spinoff/HUG/README.md .
620
+
621
+ # コミット&プッシュ
622
+ git add .
623
+ git commit -m "Initial implementation"
624
+ git push
625
+ ```
626
+
627
+ ### 3. 環境変数の設定
628
+ Space設定画面で以下を設定:
629
+ - `HF_TOKEN`: あなたのHugging Face Tokenを設定
630
+ - `HF_REPO_ID`: 動画保存先のデータセットリポジトリID
631
+
632
+ ### 4. 動作確認
633
+ - Space URLにアクセス
634
+ - サンプルPDF URLで動作テスト
635
+ - APIエンドポイント `/api/pdf-to-video` をテスト
636
+
637
+ ---
638
+
639
+ ## 🧪 テスト方法
640
+
641
+ ### curlでのAPIテスト
642
+
643
+ ```bash
644
+ curl -X POST "https://your-space-url.hf.space/api/pdf-to-video" \
645
+ -H "Content-Type: application/json" \
646
+ -d '{
647
+ "pdf_url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
648
+ "duration_per_page": 5,
649
+ "dpi": 150
650
+ }'
651
+ ```
652
+
653
+ ### Pythonでのテスト
654
+
655
+ ```python
656
+ import requests
657
+
658
+ response = requests.post(
659
+ "https://your-space-url.hf.space/api/pdf-to-video",
660
+ json={
661
+ "pdf_url": "https://example.com/sample.pdf",
662
+ "duration_per_page": 5,
663
+ "dpi": 150
664
+ }
665
+ )
666
+
667
+ print(response.json())
668
+ # {"status": "success", "video_url": "https://...", ...}
669
+ ```
670
+
671
+ ---
672
+
673
+ ## ⚠️ 注意事項
674
+
675
+ 1. **PDFサイズ制限**: 大きなPDFは処理に時間がかかります(目安: 10ページ以下を推奨)
676
+ 2. **タイムアウト**: Hugging Face Spacesのタイムアウト制限に注意
677
+ 3. **ストレージ**: データセットリポジトリの容量制限を確認
678
+ 4. **セキュリティ**: PDF URLは信頼できるソースのみを使用
679
+
680
+ ---
681
+
682
+ ## 🔧 トラブルシューティング
683
+
684
+ ### pdf2imageエラー
685
+ - `packages.txt`に`poppler-utils`が含まれているか確認
686
+
687
+ ### 動画エンコードエラー
688
+ - `packages.txt`に`ffmpeg`が含まれているか確認
689
+ - OpenCVのバージョンを確認
690
+
691
+ ### アップロードエラー
692
+ - `HF_TOKEN`が正しく設定されているか確認
693
+ - データセットリポジトリが存在するか確認
694
+ - トークンに書き込み権限があるか確認
695
+
696
+ ---
697
+
698
+ ## 📚 参考資料
699
+
700
+ - [pdf2image Documentation](https://github.com/Belval/pdf2image)
701
+ - [OpenCV Video I/O](https://docs.opencv.org/4.x/dd/d43/tutorial_py_video_display.html)
702
+ - [Hugging Face Hub API](https://huggingface.co/docs/huggingface_hub/guides/upload)
703
+ - [Gradio Documentation](https://www.gradio.app/docs/)
704
+
705
+ ---
706
+
707
+ ## 📝 更新履歴
708
+
709
+ - 2025-10-07: 初版作成