Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,11 +5,9 @@ import pathlib
|
|
| 5 |
import requests
|
| 6 |
import gradio as gr
|
| 7 |
|
| 8 |
-
# Endpoint Kie AI
|
| 9 |
BASE = "https://api.kie.ai/api/v1"
|
| 10 |
CREATE_URL = f"{BASE}/jobs/createTask"
|
| 11 |
|
| 12 |
-
# Utility: headers dengan API key (BYOK atau dari Secrets)
|
| 13 |
def make_headers(user_key: str | None):
|
| 14 |
api_key = (user_key or os.getenv("KIE_API_KEY") or "").strip()
|
| 15 |
if not api_key:
|
|
@@ -19,20 +17,15 @@ def make_headers(user_key: str | None):
|
|
| 19 |
"Content-Type": "application/json",
|
| 20 |
}
|
| 21 |
|
| 22 |
-
# Buat task text-to-video
|
| 23 |
def create_task(headers, prompt: str, aspect_ratio: str, duration: str | None):
|
| 24 |
payload = {
|
| 25 |
"model": "sora-2-text-to-video",
|
| 26 |
"input": {
|
| 27 |
"prompt": prompt,
|
| 28 |
-
# Sesuaikan dengan yang didukung Kie AI (portrait/landscape/9:16/16:9)
|
| 29 |
"aspect_ratio": aspect_ratio,
|
| 30 |
-
|
| 31 |
-
"remove_watermark": False,
|
| 32 |
},
|
| 33 |
}
|
| 34 |
-
|
| 35 |
-
# Durasi opsional — hanya sertakan kalau diisi. Jika API Kie AI menolak, hapus field ini.
|
| 36 |
if duration:
|
| 37 |
try:
|
| 38 |
d = int(duration)
|
|
@@ -43,10 +36,11 @@ def create_task(headers, prompt: str, aspect_ratio: str, duration: str | None):
|
|
| 43 |
|
| 44 |
r = requests.post(CREATE_URL, headers=headers, json=payload, timeout=60)
|
| 45 |
if r.status_code >= 400:
|
| 46 |
-
raise gr.Error(f"
|
| 47 |
|
| 48 |
data = r.json()
|
| 49 |
-
|
|
|
|
| 50 |
task_id = (
|
| 51 |
data.get("task_id") or data.get("taskId") or data.get("id") or
|
| 52 |
data.get("data", {}).get("taskId") or data.get("data", {}).get("id")
|
|
@@ -55,19 +49,18 @@ def create_task(headers, prompt: str, aspect_ratio: str, duration: str | None):
|
|
| 55 |
data.get("status_url") or data.get("statusUrl") or
|
| 56 |
data.get("data", {}).get("status_url") or data.get("data", {}).get("statusUrl")
|
| 57 |
)
|
|
|
|
| 58 |
|
| 59 |
if not task_id:
|
| 60 |
-
raise gr.Error(f"Tidak menemukan task_id
|
| 61 |
-
|
| 62 |
-
# Jika status_url tidak disediakan, coba pattern umum (cek docs Kie AI jika berbeda)
|
| 63 |
-
if not status_url:
|
| 64 |
-
status_url = f"{BASE}/jobs/task/{task_id}"
|
| 65 |
|
| 66 |
-
return task_id, status_url,
|
| 67 |
|
| 68 |
-
# Baca status + result url dari berbagai bentuk response
|
| 69 |
def infer_status_and_result(obj: dict):
|
| 70 |
-
status =
|
|
|
|
|
|
|
|
|
|
| 71 |
progress = obj.get("progress") or obj.get("data", {}).get("progress")
|
| 72 |
result_url = (
|
| 73 |
obj.get("result_url") or obj.get("output_url") or obj.get("video_url") or
|
|
@@ -76,16 +69,80 @@ def infer_status_and_result(obj: dict):
|
|
| 76 |
)
|
| 77 |
return status, progress, result_url
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
waited = 0
|
| 82 |
last_status = None
|
|
|
|
|
|
|
| 83 |
while waited <= max_wait:
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
if r.status_code >= 400:
|
| 86 |
yield f"[WARN] Poll failed: {r.status_code} {r.text}", None
|
| 87 |
else:
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
status, progress, result_url = infer_status_and_result(data)
|
| 90 |
if status != last_status:
|
| 91 |
yield f"{status} - progress: {progress}", None
|
|
@@ -96,14 +153,13 @@ def poll_until_done(headers, status_url: str, interval=5, max_wait=60*40):
|
|
| 96 |
yield "Selesai (server) — mengunduh hasil...", result_url
|
| 97 |
return
|
| 98 |
if s in {"failed", "error", "canceled", "cancelled"}:
|
| 99 |
-
raise gr.Error(f"Task gagal:
|
| 100 |
|
| 101 |
time.sleep(interval)
|
| 102 |
waited += interval
|
| 103 |
|
| 104 |
raise gr.Error("Timeout menunggu task selesai.")
|
| 105 |
|
| 106 |
-
# Unduh file hasil
|
| 107 |
def download_result(url: str, task_id: str):
|
| 108 |
if not url:
|
| 109 |
raise gr.Error("Server tidak mengirimkan URL hasil.")
|
|
@@ -118,22 +174,29 @@ def download_result(url: str, task_id: str):
|
|
| 118 |
f.write(chunk)
|
| 119 |
return str(out)
|
| 120 |
|
| 121 |
-
# Gradio handler utama
|
| 122 |
def run_job(user_api_key, prompt, aspect_ratio, duration):
|
| 123 |
headers = make_headers(user_api_key)
|
| 124 |
|
| 125 |
# 1) create task
|
| 126 |
-
task_id, status_url,
|
| 127 |
-
yield f"Task dibuat: {task_id}\
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
-
#
|
| 130 |
result_link = None
|
| 131 |
-
for status_msg, maybe_url in poll_until_done(headers, status_url):
|
| 132 |
yield status_msg, None
|
| 133 |
if maybe_url:
|
| 134 |
result_link = maybe_url
|
| 135 |
|
| 136 |
-
#
|
| 137 |
path = download_result(result_link, task_id)
|
| 138 |
yield f"Selesai ✅ (task {task_id})", path
|
| 139 |
|
|
@@ -141,10 +204,10 @@ def runtime_info(user_api_key):
|
|
| 141 |
key = (user_api_key or os.getenv("KIE_API_KEY") or "").strip()
|
| 142 |
src = "BYOK (user input)" if user_api_key else ("ENV KIE_API_KEY" if os.getenv("KIE_API_KEY") else "NONE")
|
| 143 |
masked = (key[:4] + "..." + key[-4:]) if key else "(none)"
|
| 144 |
-
return f"Key source: {src}\nKey hint: {masked}\nBase
|
| 145 |
|
| 146 |
-
with gr.Blocks(title="Kie AI — Sora
|
| 147 |
-
gr.Markdown("## Kie AI — Sora‑2 Text‑to‑Video (Polling
|
| 148 |
api = gr.Textbox(label="KIE API Key (opsional, BYOK)", type="password", placeholder="kie-...")
|
| 149 |
prompt = gr.Textbox(
|
| 150 |
label="Prompt",
|
|
@@ -161,7 +224,7 @@ with gr.Blocks(title="Kie AI — Sora 2 Text-to-Video") as demo:
|
|
| 161 |
value="portrait",
|
| 162 |
label="Aspect ratio"
|
| 163 |
)
|
| 164 |
-
duration = gr.Textbox(value="", label="Duration (detik, opsional)", placeholder="
|
| 165 |
run = gr.Button("Generate")
|
| 166 |
status = gr.Textbox(label="Status")
|
| 167 |
video = gr.Video(label="Hasil video")
|
|
|
|
| 5 |
import requests
|
| 6 |
import gradio as gr
|
| 7 |
|
|
|
|
| 8 |
BASE = "https://api.kie.ai/api/v1"
|
| 9 |
CREATE_URL = f"{BASE}/jobs/createTask"
|
| 10 |
|
|
|
|
| 11 |
def make_headers(user_key: str | None):
|
| 12 |
api_key = (user_key or os.getenv("KIE_API_KEY") or "").strip()
|
| 13 |
if not api_key:
|
|
|
|
| 17 |
"Content-Type": "application/json",
|
| 18 |
}
|
| 19 |
|
|
|
|
| 20 |
def create_task(headers, prompt: str, aspect_ratio: str, duration: str | None):
|
| 21 |
payload = {
|
| 22 |
"model": "sora-2-text-to-video",
|
| 23 |
"input": {
|
| 24 |
"prompt": prompt,
|
|
|
|
| 25 |
"aspect_ratio": aspect_ratio,
|
| 26 |
+
"remove_watermark": False, # patuhi ToS
|
|
|
|
| 27 |
},
|
| 28 |
}
|
|
|
|
|
|
|
| 29 |
if duration:
|
| 30 |
try:
|
| 31 |
d = int(duration)
|
|
|
|
| 36 |
|
| 37 |
r = requests.post(CREATE_URL, headers=headers, json=payload, timeout=60)
|
| 38 |
if r.status_code >= 400:
|
| 39 |
+
raise gr.Error(f"createTask gagal: {r.status_code} {r.text}")
|
| 40 |
|
| 41 |
data = r.json()
|
| 42 |
+
|
| 43 |
+
# Ekstrak task_id & status_url jika disediakan
|
| 44 |
task_id = (
|
| 45 |
data.get("task_id") or data.get("taskId") or data.get("id") or
|
| 46 |
data.get("data", {}).get("taskId") or data.get("data", {}).get("id")
|
|
|
|
| 49 |
data.get("status_url") or data.get("statusUrl") or
|
| 50 |
data.get("data", {}).get("status_url") or data.get("data", {}).get("statusUrl")
|
| 51 |
)
|
| 52 |
+
raw_preview = json.dumps(data)[:400]
|
| 53 |
|
| 54 |
if not task_id:
|
| 55 |
+
raise gr.Error(f"Tidak menemukan task_id pada response createTask: {raw_preview}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
return task_id, status_url, raw_preview
|
| 58 |
|
|
|
|
| 59 |
def infer_status_and_result(obj: dict):
|
| 60 |
+
status = (
|
| 61 |
+
obj.get("status") or obj.get("state") or
|
| 62 |
+
obj.get("task_status") or obj.get("data", {}).get("status")
|
| 63 |
+
)
|
| 64 |
progress = obj.get("progress") or obj.get("data", {}).get("progress")
|
| 65 |
result_url = (
|
| 66 |
obj.get("result_url") or obj.get("output_url") or obj.get("video_url") or
|
|
|
|
| 69 |
)
|
| 70 |
return status, progress, result_url
|
| 71 |
|
| 72 |
+
def probe_status_url(headers, task_id: str):
|
| 73 |
+
# Coba beberapa pola umum sampai ada yang 200 OK
|
| 74 |
+
candidates = [
|
| 75 |
+
f"{BASE}/jobs/getTask?taskId={task_id}",
|
| 76 |
+
f"{BASE}/jobs/get-task?taskId={task_id}",
|
| 77 |
+
f"{BASE}/jobs/task?taskId={task_id}",
|
| 78 |
+
f"{BASE}/jobs/{task_id}",
|
| 79 |
+
f"{BASE}/jobs/status/{task_id}",
|
| 80 |
+
f"{BASE}/jobs/getTask/{task_id}",
|
| 81 |
+
f"{BASE}/jobs/task/{task_id}",
|
| 82 |
+
]
|
| 83 |
+
for url in candidates:
|
| 84 |
+
try:
|
| 85 |
+
r = requests.get(url, headers=headers, timeout=30)
|
| 86 |
+
if r.status_code < 400:
|
| 87 |
+
# pastikan respons memang status task
|
| 88 |
+
try:
|
| 89 |
+
data = r.json()
|
| 90 |
+
except Exception:
|
| 91 |
+
continue
|
| 92 |
+
status, prog, _ = infer_status_and_result(data)
|
| 93 |
+
if status is not None:
|
| 94 |
+
return url, f"[INFO] Menemukan status URL: {url} (status={status}, progress={prog})"
|
| 95 |
+
except Exception:
|
| 96 |
+
pass
|
| 97 |
+
# Beberapa API memerlukan POST untuk getTask
|
| 98 |
+
post_url = f"{BASE}/jobs/getTask"
|
| 99 |
+
try:
|
| 100 |
+
r = requests.post(post_url, headers=headers, json={"taskId": task_id}, timeout=30)
|
| 101 |
+
if r.status_code < 400:
|
| 102 |
+
data = r.json()
|
| 103 |
+
status, prog, _ = infer_status_and_result(data)
|
| 104 |
+
if status is not None:
|
| 105 |
+
return post_url + " (POST)", "[INFO] Menemukan status via POST /jobs/getTask"
|
| 106 |
+
except Exception:
|
| 107 |
+
pass
|
| 108 |
+
return None, "[ERROR] Tidak menemukan endpoint status yang cocok. Mohon cek dokumentasi Kie AI."
|
| 109 |
+
|
| 110 |
+
def poll_until_done(headers, status_url: str, task_id: str, interval=5, max_wait=60*40):
|
| 111 |
waited = 0
|
| 112 |
last_status = None
|
| 113 |
+
used_post_mode = status_url.endswith("(POST)") # indikator dari probe
|
| 114 |
+
|
| 115 |
while waited <= max_wait:
|
| 116 |
+
try:
|
| 117 |
+
if used_post_mode:
|
| 118 |
+
r = requests.post(status_url.replace(" (POST)", ""), headers=headers, json={"taskId": task_id}, timeout=60)
|
| 119 |
+
else:
|
| 120 |
+
r = requests.get(status_url, headers=headers, timeout=60)
|
| 121 |
+
except Exception as e:
|
| 122 |
+
yield f"[WARN] Poll error: {e}", None
|
| 123 |
+
time.sleep(interval)
|
| 124 |
+
waited += interval
|
| 125 |
+
continue
|
| 126 |
+
|
| 127 |
+
if r.status_code == 404:
|
| 128 |
+
yield f"[WARN] Poll 404 di {status_url} — mencoba cari endpoint lain...", None
|
| 129 |
+
new_url, msg = probe_status_url(headers, task_id)
|
| 130 |
+
yield msg, None
|
| 131 |
+
if not new_url:
|
| 132 |
+
raise gr.Error("Gagal menemukan endpoint status. Cek logs dan dokumentasi Kie AI.")
|
| 133 |
+
status_url = new_url
|
| 134 |
+
used_post_mode = status_url.endswith("(POST)")
|
| 135 |
+
continue
|
| 136 |
+
|
| 137 |
if r.status_code >= 400:
|
| 138 |
yield f"[WARN] Poll failed: {r.status_code} {r.text}", None
|
| 139 |
else:
|
| 140 |
+
try:
|
| 141 |
+
data = r.json()
|
| 142 |
+
except Exception:
|
| 143 |
+
yield "[WARN] Response status bukan JSON yang valid.", None
|
| 144 |
+
data = {}
|
| 145 |
+
|
| 146 |
status, progress, result_url = infer_status_and_result(data)
|
| 147 |
if status != last_status:
|
| 148 |
yield f"{status} - progress: {progress}", None
|
|
|
|
| 153 |
yield "Selesai (server) — mengunduh hasil...", result_url
|
| 154 |
return
|
| 155 |
if s in {"failed", "error", "canceled", "cancelled"}:
|
| 156 |
+
raise gr.Error(f"Task gagal:\n{json.dumps(data, indent=2)}")
|
| 157 |
|
| 158 |
time.sleep(interval)
|
| 159 |
waited += interval
|
| 160 |
|
| 161 |
raise gr.Error("Timeout menunggu task selesai.")
|
| 162 |
|
|
|
|
| 163 |
def download_result(url: str, task_id: str):
|
| 164 |
if not url:
|
| 165 |
raise gr.Error("Server tidak mengirimkan URL hasil.")
|
|
|
|
| 174 |
f.write(chunk)
|
| 175 |
return str(out)
|
| 176 |
|
|
|
|
| 177 |
def run_job(user_api_key, prompt, aspect_ratio, duration):
|
| 178 |
headers = make_headers(user_api_key)
|
| 179 |
|
| 180 |
# 1) create task
|
| 181 |
+
task_id, status_url, raw_preview = create_task(headers, prompt, aspect_ratio, duration)
|
| 182 |
+
yield f"Task dibuat: {task_id}\nStatus URL (server): {status_url or '-'}\nCreate resp: {raw_preview}", None
|
| 183 |
+
|
| 184 |
+
# 2) tentukan status_url valid
|
| 185 |
+
if not status_url:
|
| 186 |
+
found, msg = probe_status_url(headers, task_id)
|
| 187 |
+
yield msg, None
|
| 188 |
+
if not found:
|
| 189 |
+
raise gr.Error("Tidak bisa menentukan endpoint status. Mohon kirim potongan full response createTask di sini.")
|
| 190 |
+
status_url = found
|
| 191 |
|
| 192 |
+
# 3) poll
|
| 193 |
result_link = None
|
| 194 |
+
for status_msg, maybe_url in poll_until_done(headers, status_url, task_id):
|
| 195 |
yield status_msg, None
|
| 196 |
if maybe_url:
|
| 197 |
result_link = maybe_url
|
| 198 |
|
| 199 |
+
# 4) download & tampilkan
|
| 200 |
path = download_result(result_link, task_id)
|
| 201 |
yield f"Selesai ✅ (task {task_id})", path
|
| 202 |
|
|
|
|
| 204 |
key = (user_api_key or os.getenv("KIE_API_KEY") or "").strip()
|
| 205 |
src = "BYOK (user input)" if user_api_key else ("ENV KIE_API_KEY" if os.getenv("KIE_API_KEY") else "NONE")
|
| 206 |
masked = (key[:4] + "..." + key[-4:]) if key else "(none)"
|
| 207 |
+
return f"Key source: {src}\nKey hint: {masked}\nBase: {BASE}"
|
| 208 |
|
| 209 |
+
with gr.Blocks(title="Kie AI — Sora‑2 Text‑to‑Video") as demo:
|
| 210 |
+
gr.Markdown("## Kie AI — Sora‑2 Text‑to‑Video (Polling + Auto-probe status URL)")
|
| 211 |
api = gr.Textbox(label="KIE API Key (opsional, BYOK)", type="password", placeholder="kie-...")
|
| 212 |
prompt = gr.Textbox(
|
| 213 |
label="Prompt",
|
|
|
|
| 224 |
value="portrait",
|
| 225 |
label="Aspect ratio"
|
| 226 |
)
|
| 227 |
+
duration = gr.Textbox(value="", label="Duration (detik, opsional)", placeholder="mis. 12 (kosongkan jika ragu)")
|
| 228 |
run = gr.Button("Generate")
|
| 229 |
status = gr.Textbox(label="Status")
|
| 230 |
video = gr.Video(label="Hasil video")
|