feat: enhance video generation responses with database ID and update dashboard to display pending and completed videos
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -91,7 +91,8 @@ class VideoFromImageRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class VideoResponse(BaseModel):
|
class VideoResponse(BaseModel):
|
||||||
id: str
|
id: str # This is the job_id from the provider
|
||||||
|
db_id: str | None = None # This is the UUID from our generated_videos table
|
||||||
model: str
|
model: str
|
||||||
status: str # "queued" | "processing" | "completed" | "failed"
|
status: str # "queued" | "processing" | "completed" | "failed"
|
||||||
polling_url: str | None = None
|
polling_url: str | None = None
|
||||||
|
|||||||
@@ -234,18 +234,22 @@ async def generate_video(
|
|||||||
polling_url = result.get("polling_url")
|
polling_url = result.get("polling_url")
|
||||||
job_status = result.get("status", "pending")
|
job_status = result.get("status", "pending")
|
||||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
db_id = None
|
||||||
async with get_write_lock():
|
async with get_write_lock():
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
conn.execute(
|
row = conn.execute(
|
||||||
"""INSERT INTO generated_videos (user_id, job_id, model_id, prompt, polling_url, status, created_at, updated_at)
|
"""INSERT INTO generated_videos (user_id, job_id, model_id, prompt, polling_url, status, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id""",
|
||||||
[user_id, job_id, body.model, body.prompt,
|
[user_id, job_id, body.model, body.prompt,
|
||||||
polling_url, job_status, now, now],
|
polling_url, job_status, now, now],
|
||||||
)
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
db_id = str(row[0])
|
||||||
|
|
||||||
urls = result.get("unsigned_urls") or result.get("video_urls")
|
urls = result.get("unsigned_urls") or result.get("video_urls")
|
||||||
return VideoResponse(
|
return VideoResponse(
|
||||||
id=job_id,
|
id=job_id,
|
||||||
|
db_id=db_id,
|
||||||
model=body.model,
|
model=body.model,
|
||||||
status=job_status,
|
status=job_status,
|
||||||
polling_url=polling_url,
|
polling_url=polling_url,
|
||||||
@@ -287,18 +291,22 @@ async def generate_video_from_image(
|
|||||||
polling_url = result.get("polling_url")
|
polling_url = result.get("polling_url")
|
||||||
job_status = result.get("status", "pending")
|
job_status = result.get("status", "pending")
|
||||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
db_id = None
|
||||||
async with get_write_lock():
|
async with get_write_lock():
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
conn.execute(
|
row = conn.execute(
|
||||||
"""INSERT INTO generated_videos (user_id, job_id, model_id, prompt, polling_url, status, created_at, updated_at)
|
"""INSERT INTO generated_videos (user_id, job_id, model_id, prompt, polling_url, status, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id""",
|
||||||
[user_id, job_id, body.model, body.prompt,
|
[user_id, job_id, body.model, body.prompt,
|
||||||
polling_url, job_status, now, now],
|
polling_url, job_status, now, now],
|
||||||
)
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
db_id = str(row[0])
|
||||||
|
|
||||||
urls = result.get("unsigned_urls") or result.get("video_urls")
|
urls = result.get("unsigned_urls") or result.get("video_urls")
|
||||||
return VideoResponse(
|
return VideoResponse(
|
||||||
id=job_id,
|
id=job_id,
|
||||||
|
db_id=db_id,
|
||||||
model=body.model,
|
model=body.model,
|
||||||
status=job_status,
|
status=job_status,
|
||||||
polling_url=polling_url,
|
polling_url=polling_url,
|
||||||
|
|||||||
+19
-4
@@ -217,11 +217,16 @@ def dashboard():
|
|||||||
images = img_resp.json() if img_resp.status_code == 200 else []
|
images = img_resp.json() if img_resp.status_code == 200 else []
|
||||||
gen_resp = _api("GET", "/generate/images", token=token)
|
gen_resp = _api("GET", "/generate/images", token=token)
|
||||||
generated_images = gen_resp.json() if gen_resp.status_code == 200 else []
|
generated_images = gen_resp.json() if gen_resp.status_code == 200 else []
|
||||||
|
|
||||||
vid_resp = _api("GET", "/generate/videos", token=token)
|
vid_resp = _api("GET", "/generate/videos", token=token)
|
||||||
generated_videos = vid_resp.json() if vid_resp.status_code == 200 else []
|
videos = vid_resp.json() if vid_resp.status_code == 200 else []
|
||||||
|
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("dashboard.html", user=user, images=images,
|
return render_template("dashboard.html", user=user, images=images,
|
||||||
generated_images=generated_images,
|
generated_images=generated_images,
|
||||||
generated_videos=generated_videos)
|
pending_videos=pending_videos,
|
||||||
|
completed_videos=completed_videos)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/gallery")
|
@app.get("/gallery")
|
||||||
@@ -405,7 +410,7 @@ def generate_image():
|
|||||||
@app.route("/generate/video", methods=["GET", "POST"])
|
@app.route("/generate/video", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def generate_video():
|
def generate_video():
|
||||||
result = error = None
|
error = None
|
||||||
token = session["access_token"]
|
token = session["access_token"]
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
mode = request.form.get("mode", "text")
|
mode = request.form.get("mode", "text")
|
||||||
@@ -413,6 +418,7 @@ def generate_video():
|
|||||||
duration = int(
|
duration = int(
|
||||||
duration_raw) if duration_raw.strip().isdigit() else None
|
duration_raw) if duration_raw.strip().isdigit() else None
|
||||||
resolution = request.form.get("resolution", "").strip() or None
|
resolution = request.form.get("resolution", "").strip() or None
|
||||||
|
|
||||||
if mode == "image":
|
if mode == "image":
|
||||||
resp = _api("POST", "/generate/video/from-image", token=token, json={
|
resp = _api("POST", "/generate/video/from-image", token=token, json={
|
||||||
"model": request.form.get("model", "").strip(),
|
"model": request.form.get("model", "").strip(),
|
||||||
@@ -430,12 +436,21 @@ def generate_video():
|
|||||||
"duration_seconds": duration,
|
"duration_seconds": duration,
|
||||||
"resolution": resolution,
|
"resolution": resolution,
|
||||||
})
|
})
|
||||||
|
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
|
# On success, redirect to the detail page to monitor progress
|
||||||
|
db_id = result.get("db_id")
|
||||||
|
if db_id:
|
||||||
|
return redirect(url_for("video_detail", video_id=db_id))
|
||||||
|
# Fallback for older backend versions
|
||||||
|
flash("Video job started.", "success")
|
||||||
|
return redirect(url_for("gallery"))
|
||||||
else:
|
else:
|
||||||
error = resp.json().get("detail", "Generation failed.")
|
error = resp.json().get("detail", "Generation failed.")
|
||||||
|
|
||||||
models = _load_models(token, "video")
|
models = _load_models(token, "video")
|
||||||
return render_template("generate_video.html", result=result, error=error, models=models)
|
return render_template("generate_video.html", error=error, models=models)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/generate/video/status")
|
@app.get("/generate/video/status")
|
||||||
|
|||||||
@@ -6,12 +6,42 @@ endblock %} {% block content %}
|
|||||||
<a href="{{ url_for('generate') }}" class="btn">Start generating</a>
|
<a href="{{ url_for('generate') }}" class="btn">Start generating</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if generated_images %}
|
{% if pending_videos %}
|
||||||
|
<div class="card mt-2">
|
||||||
|
<h2>Pending Video Jobs</h2>
|
||||||
|
<div class="image-grid">
|
||||||
|
{% for vid in pending_videos %}
|
||||||
|
<a
|
||||||
|
href="{{ url_for('video_detail', video_id=vid.id) }}"
|
||||||
|
class="image-grid-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="text-muted">{{ vid.status | capitalize }} …</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted" style="font-size: 0.75rem; margin-top: 0.25rem">
|
||||||
|
<strong>{{ vid.model_id }}</strong><br />{{ vid.prompt[:80] }}{% if
|
||||||
|
vid.prompt|length > 80 %}…{% endif %}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %} {% if generated_images %}
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
<h2>Generated images</h2>
|
<h2>Generated images</h2>
|
||||||
<div class="image-grid">
|
<div class="image-grid">
|
||||||
{% for img in generated_images %}
|
{% for img in generated_images %}
|
||||||
<div class="image-grid-item">
|
<a
|
||||||
|
href="{{ url_for('image_detail', image_id=img.id) }}"
|
||||||
|
class="image-grid-item"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src="{{ img.image_data }}"
|
src="{{ img.image_data }}"
|
||||||
alt="{{ img.prompt }}"
|
alt="{{ img.prompt }}"
|
||||||
@@ -22,16 +52,19 @@ endblock %} {% block content %}
|
|||||||
<strong>{{ img.model_id }}</strong><br />{{ img.prompt[:80] }}{% if
|
<strong>{{ img.model_id }}</strong><br />{{ img.prompt[:80] }}{% if
|
||||||
img.prompt|length > 80 %}…{% endif %}
|
img.prompt|length > 80 %}…{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %} {% if generated_videos %}
|
{% endif %} {% if completed_videos %}
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
<h2>Generated videos</h2>
|
<h2>Generated videos</h2>
|
||||||
<div class="image-grid">
|
<div class="image-grid">
|
||||||
{% for vid in generated_videos %}
|
{% for vid in completed_videos %}
|
||||||
<div class="image-grid-item">
|
<a
|
||||||
|
href="{{ url_for('video_detail', video_id=vid.id) }}"
|
||||||
|
class="image-grid-item"
|
||||||
|
>
|
||||||
{% if vid.video_url %}
|
{% if vid.video_url %}
|
||||||
<video controls style="max-width: 100%; border-radius: 6px">
|
<video controls style="max-width: 100%; border-radius: 6px">
|
||||||
<source src="{{ vid.video_url }}" />
|
<source src="{{ vid.video_url }}" />
|
||||||
@@ -54,7 +87,7 @@ endblock %} {% block content %}
|
|||||||
vid.prompt|length > 80 %}…{% endif %}<br />
|
vid.prompt|length > 80 %}…{% endif %}<br />
|
||||||
<em>{{ vid.status }}</em>
|
<em>{{ vid.status }}</em>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +96,10 @@ endblock %} {% block content %}
|
|||||||
<h2>Uploaded reference images</h2>
|
<h2>Uploaded reference images</h2>
|
||||||
<div class="image-grid">
|
<div class="image-grid">
|
||||||
{% for img in images %}
|
{% for img in images %}
|
||||||
<div class="image-grid-item">
|
<a
|
||||||
|
href="{{ url_for('upload_detail', image_id=img.id) }}"
|
||||||
|
class="image-grid-item"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src="{{ url_for('serve_uploaded_image', image_id=img.id) }}"
|
src="{{ url_for('serve_uploaded_image', image_id=img.id) }}"
|
||||||
alt="{{ img.filename }}"
|
alt="{{ img.filename }}"
|
||||||
@@ -73,7 +109,7 @@ endblock %} {% block content %}
|
|||||||
<p class="text-muted" style="font-size: 0.75rem; margin-top: 0.25rem">
|
<p class="text-muted" style="font-size: 0.75rem; margin-top: 0.25rem">
|
||||||
{{ img.filename }} — {{ (img.size_bytes / 1024) | round(1) }} KB
|
{{ img.filename }} — {{ (img.size_bytes / 1024) | round(1) }} KB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user