feat: add gallery page with image and video details, including upload and generation status
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -186,6 +186,56 @@ async def list_generated_images(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/images/{image_id}")
|
||||||
|
async def get_generated_image(
|
||||||
|
image_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
) -> dict:
|
||||||
|
"""Return details for a single generated image."""
|
||||||
|
user_id = current_user.get("id") or current_user.get("sub")
|
||||||
|
conn = get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"""SELECT id, model_id, prompt, image_data, created_at
|
||||||
|
FROM generated_images
|
||||||
|
WHERE id = ? AND user_id = ?""",
|
||||||
|
[image_id, user_id],
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
return {
|
||||||
|
"id": str(row[0]),
|
||||||
|
"model_id": row[1],
|
||||||
|
"prompt": row[2],
|
||||||
|
"image_data": row[3],
|
||||||
|
"created_at": row[4].isoformat() if row[4] else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/images/{image_id}")
|
||||||
|
async def get_generated_image(
|
||||||
|
image_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
) -> dict:
|
||||||
|
"""Return details for a single generated image."""
|
||||||
|
user_id = current_user.get("id") or current_user.get("sub")
|
||||||
|
conn = get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"""SELECT id, model_id, prompt, image_data, created_at
|
||||||
|
FROM generated_images
|
||||||
|
WHERE id = ? AND user_id = ?""",
|
||||||
|
[image_id, user_id],
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
return {
|
||||||
|
"id": str(row[0]),
|
||||||
|
"model_id": row[1],
|
||||||
|
"prompt": row[2],
|
||||||
|
"image_data": row[3],
|
||||||
|
"created_at": row[4].isoformat() if row[4] else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/video", response_model=VideoResponse)
|
@router.post("/video", response_model=VideoResponse)
|
||||||
async def generate_video(
|
async def generate_video(
|
||||||
body: VideoRequest,
|
body: VideoRequest,
|
||||||
@@ -358,3 +408,32 @@ async def list_generated_videos(
|
|||||||
}
|
}
|
||||||
for r in rows
|
for r in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/videos/{video_id}")
|
||||||
|
async def get_generated_video(
|
||||||
|
video_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
) -> dict:
|
||||||
|
"""Return details for a single video generation job."""
|
||||||
|
user_id = current_user.get("id") or current_user.get("sub")
|
||||||
|
conn = get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"""SELECT id, job_id, model_id, prompt, polling_url, status, video_url, created_at, updated_at
|
||||||
|
FROM generated_videos
|
||||||
|
WHERE id = ? AND user_id = ?""",
|
||||||
|
[video_id, user_id],
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Video job not found")
|
||||||
|
return {
|
||||||
|
"id": str(row[0]),
|
||||||
|
"job_id": row[1],
|
||||||
|
"model_id": row[2],
|
||||||
|
"prompt": row[3],
|
||||||
|
"polling_url": row[4],
|
||||||
|
"status": row[5],
|
||||||
|
"video_url": row[6],
|
||||||
|
"created_at": row[7].isoformat() if row[7] else None,
|
||||||
|
"updated_at": row[8].isoformat() if row[8] else None,
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,6 +93,36 @@ async def list_images(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{image_id}", status_code=status.HTTP_200_OK)
|
||||||
|
async def get_image_details(
|
||||||
|
image_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
) -> dict:
|
||||||
|
"""Return metadata for a single uploaded image."""
|
||||||
|
conn = get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, filename, content_type, size_bytes, created_at
|
||||||
|
FROM uploaded_images
|
||||||
|
WHERE id = ? AND user_id = ?
|
||||||
|
""",
|
||||||
|
[image_id, current_user["id"]],
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Image not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": str(row[0]),
|
||||||
|
"filename": row[1],
|
||||||
|
"content_type": row[2],
|
||||||
|
"size_bytes": row[3],
|
||||||
|
"created_at": row[4].isoformat() if row[4] else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{image_id}/file", status_code=status.HTTP_200_OK)
|
@router.get("/{image_id}/file", status_code=status.HTTP_200_OK)
|
||||||
async def serve_image(
|
async def serve_image(
|
||||||
image_id: str,
|
image_id: str,
|
||||||
@@ -106,12 +136,15 @@ async def serve_image(
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if row is None:
|
if row is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found.")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Image not found.")
|
||||||
if str(row[2]) != current_user["id"]:
|
if str(row[2]) != current_user["id"]:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied.")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail="Access denied.")
|
||||||
|
|
||||||
file_path: str = row[0]
|
file_path: str = row[0]
|
||||||
if not os.path.isfile(file_path):
|
if not os.path.isfile(file_path):
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image file missing.")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Image file missing.")
|
||||||
|
|
||||||
return FileResponse(file_path, media_type=row[1])
|
return FileResponse(file_path, media_type=row[1])
|
||||||
|
|||||||
@@ -187,6 +187,63 @@ def dashboard():
|
|||||||
generated_videos=generated_videos)
|
generated_videos=generated_videos)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/gallery")
|
||||||
|
@login_required
|
||||||
|
def gallery():
|
||||||
|
token = session["access_token"]
|
||||||
|
|
||||||
|
# Fetch all content types
|
||||||
|
uploads_resp = _api("GET", "/images/", token=token)
|
||||||
|
uploads = uploads_resp.json() if uploads_resp.status_code == 200 else []
|
||||||
|
|
||||||
|
gen_images_resp = _api("GET", "/generate/images", token=token)
|
||||||
|
generated_images = gen_images_resp.json(
|
||||||
|
) if gen_images_resp.status_code == 200 else []
|
||||||
|
|
||||||
|
videos_resp = _api("GET", "/generate/videos", token=token)
|
||||||
|
videos = videos_resp.json() if videos_resp.status_code == 200 else []
|
||||||
|
|
||||||
|
# Separate pending videos
|
||||||
|
pending_videos = [v for v in videos if v.get(
|
||||||
|
"status") not in ("completed", "failed")]
|
||||||
|
completed_videos = [v for v in videos if v.get("status") == "completed"]
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"gallery.html",
|
||||||
|
uploads=uploads,
|
||||||
|
generated_images=generated_images,
|
||||||
|
pending_videos=pending_videos,
|
||||||
|
completed_videos=completed_videos,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/gallery/image/<image_id>")
|
||||||
|
@login_required
|
||||||
|
def image_detail(image_id: str):
|
||||||
|
token = session["access_token"]
|
||||||
|
resp = _api("GET", f"/generate/images/{image_id}", token=token)
|
||||||
|
image = resp.json() if resp.status_code == 200 else None
|
||||||
|
return render_template("image_detail.html", image=image)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/gallery/video/<video_id>")
|
||||||
|
@login_required
|
||||||
|
def video_detail(video_id: str):
|
||||||
|
token = session["access_token"]
|
||||||
|
resp = _api("GET", f"/generate/videos/{video_id}", token=token)
|
||||||
|
video = resp.json() if resp.status_code == 200 else None
|
||||||
|
return render_template("video_detail.html", video=video)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/gallery/upload/<image_id>")
|
||||||
|
@login_required
|
||||||
|
def upload_detail(image_id: str):
|
||||||
|
token = session["access_token"]
|
||||||
|
resp = _api("GET", f"/images/{image_id}", token=token)
|
||||||
|
image = resp.json() if resp.status_code == 200 else None
|
||||||
|
return render_template("upload_detail.html", image=image)
|
||||||
|
|
||||||
|
|
||||||
# ── Generate ──────────────────────────────────────────────────────────────
|
# ── Generate ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.get("/images/<image_id>/file")
|
@app.get("/images/<image_id>/file")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
{% if session.get('access_token') %}
|
{% if session.get('access_token') %}
|
||||||
<a href="{{ url_for('dashboard') }}">Dashboard</a>
|
<a href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||||
|
<a href="{{ url_for('gallery') }}">Gallery</a>
|
||||||
|
|
||||||
<a href="{{ url_for('generate_text') }}">Generate Text</a>
|
<a href="{{ url_for('generate_text') }}">Generate Text</a>
|
||||||
<a href="{{ url_for('generate_image') }}">Generate Image</a>
|
<a href="{{ url_for('generate_image') }}">Generate Image</a>
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}My Gallery{% endblock %} {% block
|
||||||
|
content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-6">My Gallery</h1>
|
||||||
|
|
||||||
|
<!-- Pending Creations -->
|
||||||
|
{% if pending_videos %}
|
||||||
|
<div class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||||
|
Pending Creations
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||||
|
>
|
||||||
|
{% for video in pending_videos %}
|
||||||
|
<a
|
||||||
|
href="{{ url_for('video_detail', video_id=video.id) }}"
|
||||||
|
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<div class="p-4">
|
||||||
|
<p class="font-bold text-lg truncate">{{ video.prompt }}</p>
|
||||||
|
<p class="text-sm text-gray-400">
|
||||||
|
Video Job Status:
|
||||||
|
<span class="font-semibold text-yellow-400"
|
||||||
|
>{{ video.status }}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
Started: {{ video.created_at | fromisoformat | humantime }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Generated Images -->
|
||||||
|
<div class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||||
|
Generated Images
|
||||||
|
</h2>
|
||||||
|
{% if generated_images %}
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||||
|
>
|
||||||
|
{% for image in generated_images %}
|
||||||
|
<a
|
||||||
|
href="{{ url_for('image_detail', image_id=image.id) }}"
|
||||||
|
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="{{ image.image_data }}"
|
||||||
|
alt="{{ image.prompt }}"
|
||||||
|
class="w-full h-48 object-cover"
|
||||||
|
/>
|
||||||
|
<div class="p-4">
|
||||||
|
<p class="text-sm truncate">{{ image.prompt }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-gray-400">
|
||||||
|
You haven't generated any images yet.
|
||||||
|
<a
|
||||||
|
href="{{ url_for('generate_image') }}"
|
||||||
|
class="text-blue-400 hover:underline"
|
||||||
|
>Generate one now</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generated Videos -->
|
||||||
|
<div class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||||
|
Generated Videos
|
||||||
|
</h2>
|
||||||
|
{% if completed_videos %}
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||||
|
>
|
||||||
|
{% for video in completed_videos %}
|
||||||
|
<a
|
||||||
|
href="{{ url_for('video_detail', video_id=video.id) }}"
|
||||||
|
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<div class="w-full h-48 bg-black flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
class="w-12 h-12 text-gray-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<p class="text-sm truncate">{{ video.prompt }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-gray-400">
|
||||||
|
You haven't generated any videos yet.
|
||||||
|
<a
|
||||||
|
href="{{ url_for('generate_video') }}"
|
||||||
|
class="text-blue-400 hover:underline"
|
||||||
|
>Generate one now</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Uploaded Images -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||||
|
My Uploads
|
||||||
|
</h2>
|
||||||
|
{% if uploads %}
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||||
|
>
|
||||||
|
{% for image in uploads %}
|
||||||
|
<a
|
||||||
|
href="{{ url_for('upload_detail', image_id=image.id) }}"
|
||||||
|
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="{{ url_for('serve_uploaded_image', image_id=image.id) }}"
|
||||||
|
alt="{{ image.filename }}"
|
||||||
|
class="w-full h-48 object-cover"
|
||||||
|
/>
|
||||||
|
<div class="p-4">
|
||||||
|
<p class="text-sm truncate">{{ image.filename }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-gray-400">You haven't uploaded any images.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Generated Image{% endblock %} {%
|
||||||
|
block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<a
|
||||||
|
href="{{ url_for('gallery') }}"
|
||||||
|
class="text-blue-400 hover:underline mb-4 inline-block"
|
||||||
|
>← Back to Gallery</a
|
||||||
|
>
|
||||||
|
|
||||||
|
{% if image %}
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Generated Image</h1>
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<img
|
||||||
|
src="{{ image.image_data }}"
|
||||||
|
alt="{{ image.prompt }}"
|
||||||
|
class="w-full object-contain"
|
||||||
|
/>
|
||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">Prompt</h2>
|
||||||
|
<p class="text-gray-300 bg-gray-900 p-3 rounded-md">{{ image.prompt }}</p>
|
||||||
|
<div class="mt-4 text-sm text-gray-400">
|
||||||
|
<p><strong>Model:</strong> {{ image.model_id }}</p>
|
||||||
|
<p>
|
||||||
|
<strong>Created:</strong> {{ image.created_at | fromisoformat |
|
||||||
|
humantime }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h1 class="text-2xl font-bold">Image not found</h1>
|
||||||
|
<p class="text-gray-400 mt-2">Could not find details for this image.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Uploaded Image{% endblock %} {% block
|
||||||
|
content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<a
|
||||||
|
href="{{ url_for('gallery') }}"
|
||||||
|
class="text-blue-400 hover:underline mb-4 inline-block"
|
||||||
|
>← Back to Gallery</a
|
||||||
|
>
|
||||||
|
|
||||||
|
{% if image %}
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Uploaded Image</h1>
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<img
|
||||||
|
src="{{ url_for('serve_uploaded_image', image_id=image.id) }}"
|
||||||
|
alt="{{ image.filename }}"
|
||||||
|
class="w-full object-contain"
|
||||||
|
/>
|
||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">Details</h2>
|
||||||
|
<div class="mt-4 text-sm text-gray-400">
|
||||||
|
<p><strong>Filename:</strong> {{ image.filename }}</p>
|
||||||
|
<p><strong>Content Type:</strong> {{ image.content_type }}</p>
|
||||||
|
<p>
|
||||||
|
<strong>Size:</strong> {{ (image.size_bytes / 1024) | round(2) }} KB
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Uploaded:</strong> {{ image.created_at | fromisoformat |
|
||||||
|
humantime }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h1 class="text-2xl font-bold">Image not found</h1>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
Could not find details for this uploaded image.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Generated Video{% endblock %} {%
|
||||||
|
block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<a
|
||||||
|
href="{{ url_for('gallery') }}"
|
||||||
|
class="text-blue-400 hover:underline mb-4 inline-block"
|
||||||
|
>← Back to Gallery</a
|
||||||
|
>
|
||||||
|
|
||||||
|
{% if video %}
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Video Generation Job</h1>
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||||
|
{% if video.status == 'completed' and video.video_url %}
|
||||||
|
<video src="{{ video.video_url }}" controls class="w-full"></video>
|
||||||
|
{% elif video.status in ('queued', 'processing') and video.polling_url %}
|
||||||
|
<div
|
||||||
|
class="w-full bg-black aspect-video flex flex-col items-center justify-center p-6 text-center"
|
||||||
|
id="video-poll-status"
|
||||||
|
data-polling-url="{{ video.polling_url }}"
|
||||||
|
>
|
||||||
|
<p class="text-xl font-semibold">
|
||||||
|
Status: <strong id="poll-status-text">{{ video.status }}</strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
Your video is being processed. This page will update automatically when
|
||||||
|
it's ready.
|
||||||
|
</p>
|
||||||
|
<div class="spinner mt-4"></div>
|
||||||
|
</div>
|
||||||
|
{% elif video.status == 'failed' %}
|
||||||
|
<div
|
||||||
|
class="w-full bg-black aspect-video flex flex-col items-center justify-center p-6 text-center"
|
||||||
|
>
|
||||||
|
<p class="text-xl font-semibold text-red-500">Generation Failed</p>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
{{ video.error or 'An unknown error occurred.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div
|
||||||
|
class="w-full bg-black aspect-video flex flex-col items-center justify-center p-6 text-center"
|
||||||
|
>
|
||||||
|
<p class="text-xl font-semibold">Video Not Available</p>
|
||||||
|
<p class="text-gray-400 mt-2">Status: {{ video.status }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">Prompt</h2>
|
||||||
|
<p class="text-gray-300 bg-gray-900 p-3 rounded-md">{{ video.prompt }}</p>
|
||||||
|
<div class="mt-4 text-sm text-gray-400">
|
||||||
|
<p><strong>Model:</strong> {{ video.model_id }}</p>
|
||||||
|
<p><strong>Job ID:</strong> <code>{{ video.job_id }}</code></p>
|
||||||
|
<p>
|
||||||
|
<strong>Created:</strong> {{ video.created_at | fromisoformat |
|
||||||
|
humantime }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Last Update:</strong> {{ video.updated_at | fromisoformat |
|
||||||
|
humantime }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h1 class="text-2xl font-bold">Video job not found</h1>
|
||||||
|
<p class="text-gray-400 mt-2">Could not find details for this video job.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user