Skip to content

Commit 898e2a7

Browse files
authored
入力画像のサムネイル生成機能を追加し、/resultエンドポイントで動画URLと共にBase64エンコードされたサムネイルを返却するように改修 (#10)
* 入力画像のサムネイル生成機能を追加し、/resultエンドポイントで動画URLと共にBase64エンコードされたサムネイルを返却するように改修 * ResultResponseモデルのthumbnail_base64フィールドをOptionalとして明示化 * サムネイルダウンロードエンドポイントが不要であることを明記 * サムネイルのBase64エンコードを含むレスポンスを返却するようにテストを修正し、サムネイルファイルの存在確認を追加
1 parent 3acd8c7 commit 898e2a7

4 files changed

Lines changed: 231 additions & 84 deletions

File tree

api/api.py

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
import traceback
66
import asyncio
77
import json
8+
import base64 # 追加: Base64エンコード用
9+
import mimetypes # 追加: MIMEタイプ判定用
810
from contextlib import asynccontextmanager
911
from fastapi import FastAPI, HTTPException, BackgroundTasks, UploadFile, File, Form, Request # Request を追加
10-
from fastapi.responses import FileResponse, StreamingResponse
12+
from fastapi.responses import FileResponse, StreamingResponse # JSONResponse を削除
1113
from fastapi.middleware.cors import CORSMiddleware
1214
from pydantic import BaseModel, Field
1315
from PIL import Image
@@ -142,6 +144,11 @@ class LoraListResponse(BaseModel):
142144
loras: List[str]
143145

144146

147+
class ResultResponse(BaseModel):
148+
video_url: str
149+
thumbnail_base64: Optional[str] = None
150+
151+
145152
# --- Background Worker ---
146153
def background_worker_task():
147154
global worker_running, currently_processing_job_id
@@ -353,7 +360,7 @@ async def event_generator():
353360
# Send final status event if it hasn't been sent already
354361
if current_data_json != last_data_sent:
355362
yield f"event: progress\ndata: {current_data_json}\n\n"
356-
last_data_sent = current_data_json # Ensure last_data_sent is updated even for the final message
363+
last_data_sent = current_data_json # Ensure last_data_sent is updated even for the final message
357364
print(f"Sent final progress update for job {job_id}: Status {job.status}")
358365

359366
# Send a dedicated 'status' event to signal completion/failure/cancellation
@@ -377,22 +384,65 @@ async def event_generator():
377384
return StreamingResponse(event_generator(), media_type="text/event-stream")
378385

379386

380-
@app.get("/result/{job_id}")
381-
async def get_job_result(job_id: str):
382-
# Implementation needed: Check job status, return video file if completed
387+
@app.get("/result/{job_id}", response_model=ResultResponse)
388+
async def get_job_result(job_id: str, request: Request): # requestを追加してURLを構築
389+
"""
390+
Returns the download URL for the completed video and the Base64 encoded thumbnail.
391+
"""
383392
job = queue_manager.get_job_by_id(job_id)
384393
output_file = os.path.join(settings.OUTPUTS_DIR, f"{job_id}.mp4")
394+
is_completed = (job and job.status == "completed") or (not job and os.path.exists(output_file))
385395

386-
if job and job.status == "completed" and os.path.exists(output_file):
387-
return FileResponse(output_file, media_type="video/mp4", filename=f"{job_id}.mp4")
388-
elif not job and os.path.exists(output_file):
389-
# If job not in queue but file exists, assume completed
390-
print(f"Job {job_id} not in queue, but result file found. Serving file.")
396+
if not is_completed:
397+
if job:
398+
raise HTTPException(status_code=404, detail=f"Job '{job_id}' is not completed yet (status: {job.status}).")
399+
else:
400+
raise HTTPException(status_code=404, detail=f"Job '{job_id}' not found or result file does not exist.")
401+
402+
# --- サムネイル処理 ---
403+
thumbnail_base64 = None
404+
if job and job.thumbnail and os.path.exists(job.thumbnail):
405+
try:
406+
with open(job.thumbnail, "rb") as f:
407+
thumbnail_data = f.read()
408+
thumbnail_base64_data = base64.b64encode(thumbnail_data).decode("utf-8")
409+
# MIMEタイプを推測 (例: image/jpeg)
410+
mime_type, _ = mimetypes.guess_type(job.thumbnail)
411+
if mime_type:
412+
thumbnail_base64 = f"data:{mime_type};base64,{thumbnail_base64_data}"
413+
else:
414+
# MIMEタイプが不明な場合はデフォルトを使用(またはエラー処理)
415+
thumbnail_base64 = f"data:image/jpeg;base64,{thumbnail_base64_data}" # デフォルトをJPEGに
416+
print(f"Job {job_id}: Encoded thumbnail from {job.thumbnail}")
417+
except Exception as e:
418+
print(f"Job {job_id}: Error reading or encoding thumbnail {job.thumbnail}: {e}")
419+
# サムネイルの読み込み/エンコードに失敗してもエラーにはしない
420+
421+
# --- 動画URL構築 ---
422+
# request.url を使用して絶対URLまたは相対URLを構築
423+
video_url = str(request.url_for('download_video', job_id=job_id))
424+
425+
return ResultResponse(
426+
video_url=video_url,
427+
thumbnail_base64=thumbnail_base64
428+
)
429+
430+
431+
# --- Download Endpoints ---
432+
433+
@app.get("/download/video/{job_id}")
434+
async def download_video(job_id: str):
435+
"""Downloads the generated video file."""
436+
output_file = os.path.join(settings.OUTPUTS_DIR, f"{job_id}.mp4")
437+
if os.path.exists(output_file):
391438
return FileResponse(output_file, media_type="video/mp4", filename=f"{job_id}.mp4")
392-
elif job:
393-
raise HTTPException(status_code=404, detail=f"Job '{job_id}' is not completed yet (status: {job.status}).")
394439
else:
395-
raise HTTPException(status_code=404, detail=f"Job '{job_id}' not found or result file does not exist.")
440+
# Optionally check job status again here if needed
441+
job = queue_manager.get_job_by_id(job_id)
442+
if job:
443+
raise HTTPException(status_code=404, detail=f"Video file for job '{job_id}' not found, status is '{job.status}'.")
444+
else:
445+
raise HTTPException(status_code=404, detail=f"Video file for job '{job_id}' not found.")
396446

397447

398448
@app.get("/input_image/{job_id}")

api/worker.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,11 @@ def worker(job: queue_manager.QueuedJob, models: dict):
8585
lora_path = job.lora_path
8686
original_exif = job.original_exif # Get Exif data from job object
8787

88-
# Update job status to processing
89-
queue_manager.update_job_status(job_id, "processing")
88+
thumbnail_path = None # Initialize thumbnail_path
89+
90+
# Update job status to processing, including the thumbnail path (will be updated again if thumbnail generated)
91+
# We update here initially in case thumbnail generation fails later
92+
queue_manager.update_job_status(job_id, "processing", thumbnail=thumbnail_path)
9093

9194
# Load models from the dictionary
9295
vae = models["vae"]
@@ -120,17 +123,37 @@ def update_progress(step_info: str, percentage: float = 0.0, current_step: int =
120123
try:
121124
# Load input image
122125
try:
123-
pil_input_image = Image.open(input_image_path)
124-
# logging.info(f"[Job {job_id}] Exif in input_image after open: {pil_input_image.info.get('exif') is not None}") # DEBUG: Removed
125-
input_image = np.array(pil_input_image)
126+
pil_input_image = Image.open(input_image_path) # Keep this line to load the image
127+
# logging.info(f"[Job {job_id}] Exif in input_image after open: {pil_input_image.info.get('exif') is not None}") # DEBUG: Removed
128+
129+
# --- Thumbnail Generation (Moved Here) ---
130+
try:
131+
# Generate thumbnail from the loaded input image (pil_input_image)
132+
thumb_size = (128, 128) # Define thumbnail size (adjust as needed)
133+
thumb_img = pil_input_image.copy()
134+
thumb_img.thumbnail(thumb_size, Image.Resampling.LANCZOS)
135+
thumbnail_filename = f"thumb_{job_id}.jpg"
136+
# Use previously initialized thumbnail_path variable
137+
thumbnail_path = os.path.join(settings.TEMP_QUEUE_IMAGES_DIR, thumbnail_filename)
138+
thumb_img.save(thumbnail_path, "JPEG", quality=85) # Save as JPEG
139+
print(f"Job {job_id}: Thumbnail saved to {thumbnail_path}")
140+
# Update job status again with the actual thumbnail path
141+
queue_manager.update_job_status(job_id, "processing", thumbnail=thumbnail_path)
142+
except Exception as thumb_e:
143+
print(f"Job {job_id}: Warning - Failed to generate thumbnail: {thumb_e}")
144+
thumbnail_path = None # Ensure path is None if generation fails (already set initially)
145+
146+
# input_image = np.array(pil_input_image) # Moved numpy conversion later
147+
126148
except FileNotFoundError:
127149
print(f"Error: Input image not found at {input_image_path}")
128150
queue_manager.update_job_status(job_id, "failed - image not found")
129151
return
130152
except Exception as e:
131153
print(f"Error loading image {input_image_path}: {e}")
132154
queue_manager.update_job_status(job_id, f"failed - image load error: {e}")
133-
return # Added return here
155+
return
156+
134157
total_latent_sections = (total_second_length * 30) / (latent_window_size * 4)
135158
total_latent_sections = int(max(round(total_latent_sections), 1))
136159

@@ -238,7 +261,8 @@ def update_progress(step_info: str, percentage: float = 0.0, current_step: int =
238261
print(f"Job {job_id}: No LoRA path specified, skipping LoRA loading.")
239262
# --- End LoRA Loading ---
240263

241-
# Processing input image
264+
# Processing input image (Convert to numpy array here if needed for processing)
265+
input_image = np.array(pil_input_image) # Convert PIL image to numpy array now
242266
update_progress("Image processing ...", 12, 0, steps) # Progress update (percentage adjusted)
243267
input_image = np.squeeze(input_image) # Ensure 3D
244268
if input_image.ndim != 3 or input_image.shape[2] != 3:

api_thumbnail_plan.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# API改修計画: /result エンドポイントでの入力画像サムネイル返却
2+
3+
## 1. 目的
4+
5+
FastAPIアプリケーションの `/result/{job_id}` エンドポイントを改修し、生成された動画ファイルのダウンロードURLに加えて、ジョブに使用された**入力画像のサムネイル画像データ(Base64エンコード)**もレスポンスに含めるようにする。
6+
7+
## 2. 改修内容
8+
9+
### 2.1. `worker.py`
10+
11+
* **サムネイル生成処理の追加:**
12+
* `worker` 関数の入力画像読み込み後 (`Image.open(input_image_path)`) に、サムネイル画像を生成する処理を追加する。
13+
* サムネイル画像は `PIL` を使用してリサイズし、ファイル名 `thumb_{job_id}.jpg` として `settings.TEMP_QUEUE_IMAGES_DIR` ディレクトリに保存する。
14+
* **サムネイルパス保存依頼の追加:**
15+
* ジョブステータスを `"processing"` に更新する `queue_manager.update_job_status` 呼び出し時に、`thumbnail` 引数に生成したサムネイル画像のフルパスを渡す。
16+
17+
### 2.2. `queue_manager.py`
18+
19+
* **変更なし:** 既存の `QueuedJob` データクラスの `thumbnail` フィールドと `update_job_status` 関数の `thumbnail` 引数をそのまま利用する。
20+
21+
### 2.3. `api.py`
22+
23+
* **レスポンスモデルの定義:**
24+
* `/result/{job_id}` 用の新しいPydanticレスポンスモデル (例: `ResultResponse`) を定義する。
25+
* モデルには、動画ダウンロードURL (`video_url: str`) とサムネイルのBase64データ (`thumbnail_base64: str`) を含める。
26+
* **`/result/{job_id}` エンドポイントの改修:**
27+
* レスポンスモデルを `FileResponse` から定義した `ResultResponse` に変更する。
28+
* `queue_manager.get_job_by_id` でジョブ情報を取得する。
29+
* `job.thumbnail` に保存されているサムネイル画像のパスを取得する。
30+
* サムネイルファイルを読み込み、Base64エンコードする。
31+
* 適切なData URIスキーム (`data:image/jpeg;base64,...`) を作成する。
32+
* 動画ダウンロード用URL (`/download/video/{job_id}`) とBase64エンコードされたサムネイルデータを含むJSONレスポンスを返す。
33+
* **`/download/video/{job_id}` エンドポイントの新設:**
34+
* 指定された `job_id` に対応する動画ファイル (`outputs/{job_id}.mp4`) を `FileResponse` で返すエンドポイントを追加する。
35+
36+
## 3. 処理フロー図 (Mermaid)
37+
38+
```mermaid
39+
sequenceDiagram
40+
participant Client
41+
participant API (api.py)
42+
participant Worker (worker.py)
43+
participant QueueManager (queue_manager.py)
44+
participant FileSystem
45+
46+
Client->>+API: POST /generate (画像アップロード)
47+
API->>+QueueManager: add_to_queue(image, ...)
48+
QueueManager->>FileSystem: save_image_to_temp (queue_image_{job_id}.jpg)
49+
QueueManager-->>-API: job_id
50+
API-->>-Client: {job_id: ...}
51+
52+
Note over Worker, QueueManager: Background Worker picks up job
53+
Worker->>+QueueManager: get_next_job()
54+
QueueManager->>FileSystem: load_queue_from_file()
55+
QueueManager-->>-Worker: job
56+
Worker->>FileSystem: Image.open(job.image_path)
57+
Worker->>Worker: Generate Thumbnail (thumb_{job_id}.jpg)
58+
Worker->>FileSystem: Save Thumbnail (temp_queue_images/thumb_{job_id}.jpg)
59+
Worker->>+QueueManager: update_job_status(job_id, "processing", thumbnail_path) # サムネイルパスを保存
60+
QueueManager->>FileSystem: load_queue_from_file()
61+
QueueManager->>FileSystem: save_queue() (update status & thumbnail path)
62+
QueueManager-->>-Worker: True
63+
Note over Worker: Video Generation Process...
64+
Worker->>FileSystem: Save Video (outputs/{job_id}.mp4)
65+
Worker->>+QueueManager: update_job_status(job_id, "completed")
66+
QueueManager->>FileSystem: load_queue_from_file()
67+
QueueManager->>FileSystem: save_queue() (update status)
68+
QueueManager-->>-Worker: True
69+
70+
Client->>+API: GET /result/{job_id}
71+
API->>+QueueManager: get_job_by_id(job_id)
72+
QueueManager->>FileSystem: load_queue_from_file()
73+
QueueManager-->>-API: job (with thumbnail path)
74+
API->>FileSystem: Read thumbnail file (job.thumbnail)
75+
API->>API: Encode thumbnail to Base64
76+
API->>API: Construct video_url (pointing to download endpoint)
77+
API-->>-Client: JSON { video_url: "/download/video/...", thumbnail_base64: "data:image/jpeg;base64,..." }
78+
79+
Client->>+API: GET /download/video/{job_id}
80+
API->>FileSystem: Check outputs/{job_id}.mp4 exists
81+
API-->>-Client: FileResponse (video/mp4)
82+
83+
# Note: Thumbnail download endpoint is no longer needed

0 commit comments

Comments
 (0)