diff --git a/frontend/app/main.py b/frontend/app/main.py
index f72ac9d..5017329 100644
--- a/frontend/app/main.py
+++ b/frontend/app/main.py
@@ -174,7 +174,8 @@ def generate_video():
mode = request.form.get("mode", "text")
token = session["access_token"]
duration_raw = request.form.get("duration_seconds", "")
- duration = int(duration_raw) if duration_raw.strip().isdigit() else None
+ duration = int(
+ duration_raw) if duration_raw.strip().isdigit() else None
resolution = request.form.get("resolution", "").strip() or None
if mode == "image":
resp = _api("POST", "/generate/video/from-image", token=token, json={
diff --git a/frontend/app/static/app.js b/frontend/app/static/app.js
index 3c5996f..9d9d8e5 100644
--- a/frontend/app/static/app.js
+++ b/frontend/app/static/app.js
@@ -37,4 +37,48 @@ document.addEventListener("DOMContentLoaded", () => {
if (panel) panel.classList.add("active");
});
});
+
+ // ── Video status polling ───────────────────────────────
+ const pollDiv = document.getElementById("video-poll-status");
+ if (pollDiv) {
+ const pollingUrl = pollDiv.dataset.pollingUrl;
+ const statusText = document.getElementById("poll-status-text");
+ const videoContainer = document.getElementById("poll-video-container");
+
+ const interval = setInterval(async () => {
+ try {
+ const resp = await fetch(
+ "/generate/video/status?polling_url=" +
+ encodeURIComponent(pollingUrl),
+ );
+ if (!resp.ok) return;
+ const data = await resp.json();
+
+ if (statusText) {
+ statusText.innerHTML = "Status: " + data.status + "";
+ }
+
+ if (data.status === "completed") {
+ clearInterval(interval);
+ if (data.video_url && videoContainer) {
+ const vid = document.createElement("video");
+ vid.src = data.video_url;
+ vid.controls = true;
+ vid.className = "generated-video";
+ videoContainer.appendChild(vid);
+ const msg = pollDiv.querySelector("p");
+ if (msg) msg.textContent = "Video ready!";
+ }
+ } else if (data.status === "failed") {
+ clearInterval(interval);
+ pollDiv.innerHTML =
+ '
Generation failed: ' +
+ (data.error || "Unknown error") +
+ "
";
+ }
+ } catch (e) {
+ console.error("Video polling error:", e);
+ }
+ }, 5000);
+ }
});
diff --git a/frontend/app/templates/generate_video.html b/frontend/app/templates/generate_video.html
index 7c5e7a8..2c54bec 100644
--- a/frontend/app/templates/generate_video.html
+++ b/frontend/app/templates/generate_video.html
@@ -46,6 +46,30 @@ endblock %} {% block content %}
+
+
+
+
+
+
@@ -93,6 +117,30 @@ endblock %} {% block content %}
+
+
+
+
+
+
@@ -103,17 +151,29 @@ endblock %} {% block content %}
{% endif %} {% if result %}
Video job
-
Status: {{ result.status }}
- {% if result.get('video_url') %}
+
Job ID: {{ result.id }}
+ {% if result.status in ('queued', 'processing') and result.polling_url %}
+
+
+ Status: {{ result.status }}
+ — checking for updates every 5 s…
+
+
+
+ {% elif result.video_url %}
+ {% elif result.status == 'failed' %}
+
+ Generation failed: {{ result.error or 'Unknown error' }}
+
{% else %}
-
- Video is being processed. Check back later.
-
+
Status: {{ result.status }}
{% endif %}
{% endif %}
diff --git a/frontend/tests/test_frontend.py b/frontend/tests/test_frontend.py
index b090745..f98cd54 100644
--- a/frontend/tests/test_frontend.py
+++ b/frontend/tests/test_frontend.py
@@ -226,7 +226,8 @@ def test_generate_video_text_mode(client):
with patch("frontend.app.main.httpx.request", return_value=mock):
resp = client.post("/generate/video", data={
"mode": "text", "model": "openai/sora-2-pro",
- "prompt": "A sunset", "aspect_ratio": "16:9"
+ "prompt": "A sunset", "aspect_ratio": "16:9",
+ "duration_seconds": "10", "resolution": "720p",
})
assert resp.status_code == 200
assert b"queued" in resp.data
@@ -347,3 +348,73 @@ def test_profile_update_failure(client):
"/users/profile", data={"email": "bad", "password": ""})
# redirects regardless, flash message shown on next GET
assert resp.status_code == 302
+
+
+# ---------------------------------------------------------------------------
+# GET /generate/video/status (polling proxy)
+# ---------------------------------------------------------------------------
+
+def test_video_status_proxy_completed(client):
+ _set_auth(client)
+ mock = _mock_response(200, {
+ "id": "v1", "model": "", "status": "completed",
+ "video_url": "https://example.com/video.mp4",
+ "unsigned_urls": ["https://example.com/video.mp4"],
+ })
+ with patch("frontend.app.main.httpx.request", return_value=mock):
+ resp = client.get(
+ "/generate/video/status",
+ query_string={
+ "polling_url": "https://openrouter.ai/api/v1/videos/v1"},
+ )
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["status"] == "completed"
+ assert data["video_url"] == "https://example.com/video.mp4"
+
+
+def test_video_status_proxy_processing(client):
+ _set_auth(client)
+ mock = _mock_response(
+ 200, {"id": "v1", "model": "", "status": "processing"})
+ with patch("frontend.app.main.httpx.request", return_value=mock):
+ resp = client.get(
+ "/generate/video/status",
+ query_string={
+ "polling_url": "https://openrouter.ai/api/v1/videos/v1"},
+ )
+ assert resp.status_code == 200
+ assert resp.get_json()["status"] == "processing"
+
+
+def test_video_status_proxy_requires_login(client):
+ resp = client.get(
+ "/generate/video/status",
+ query_string={"polling_url": "https://openrouter.ai/api/v1/videos/v1"},
+ )
+ assert resp.status_code == 302
+ assert "/login" in resp.headers["Location"]
+
+
+def test_video_status_proxy_missing_url(client):
+ _set_auth(client)
+ resp = client.get("/generate/video/status")
+ assert resp.status_code == 400
+ assert b"polling_url" in resp.data
+
+
+def test_video_generate_renders_polling_ui(client):
+ """When response has polling_url, template shows polling div."""
+ _set_auth(client)
+ mock = _mock_response(200, {
+ "id": "v1", "model": "openai/sora-2-pro", "status": "queued",
+ "polling_url": "https://openrouter.ai/api/v1/videos/v1",
+ })
+ with patch("frontend.app.main.httpx.request", return_value=mock):
+ resp = client.post("/generate/video", data={
+ "mode": "text", "model": "openai/sora-2-pro",
+ "prompt": "A sunset", "aspect_ratio": "16:9",
+ })
+ assert resp.status_code == 200
+ assert b"video-poll-status" in resp.data
+ assert b"openrouter.ai/api/v1/videos/v1" in resp.data