sharul20001 commited on
Commit
a1bf7ef
·
verified ·
1 Parent(s): bedb142

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -160
app.py CHANGED
@@ -4,64 +4,11 @@ import json
4
  import pathlib
5
  import requests
6
  import gradio as gr
7
- from fastapi import FastAPI, Request
8
- from typing import Optional, Tuple
9
 
10
- # ========== Konfigurasi dasar ==========
11
  BASE = "https://api.kie.ai/api/v1"
12
  CREATE_URL = f"{BASE}/jobs/createTask"
13
 
14
- # Global store untuk callback terakhir
15
- LAST_CALLBACK_JSON = {}
16
- LAST_VIDEO_URL = None
17
- LAST_TASK_ID = None
18
-
19
- # ========== FastAPI untuk menerima callback ==========
20
- fastapi_app = FastAPI()
21
-
22
- def try_extract_video_url(obj: dict) -> Optional[str]:
23
- # Coba berbagai nama field yang umum dipakai layanan
24
- for k in [
25
- "result_url", "output_url", "video_url", "download_url",
26
- # nested:
27
- "data.result_url", "data.output_url", "data.video_url", "data.download_url",
28
- ]:
29
- parts = k.split(".")
30
- cur = obj
31
- for p in parts:
32
- if isinstance(cur, dict) and p in cur:
33
- cur = cur[p]
34
- else:
35
- cur = None
36
- break
37
- if isinstance(cur, str) and cur.startswith("http"):
38
- return cur
39
- return None
40
-
41
- @fastapi_app.post("/api/callback")
42
- async def kie_callback(req: Request):
43
- global LAST_CALLBACK_JSON, LAST_VIDEO_URL, LAST_TASK_ID
44
- try:
45
- payload = await req.json()
46
- except Exception:
47
- payload = {"raw": await req.body()}
48
- LAST_CALLBACK_JSON = payload
49
- # Simpan task id jika ada
50
- for key in ["taskId", "task_id", "id"]:
51
- if isinstance(payload, dict) and key in payload:
52
- LAST_TASK_ID = payload[key]
53
- break
54
- if "data" in payload and isinstance(payload["data"], dict) and key in payload["data"]:
55
- LAST_TASK_ID = payload["data"][key]
56
- break
57
- # Ambil link video jika dikirim
58
- video_url = try_extract_video_url(payload)
59
- if video_url:
60
- LAST_VIDEO_URL = video_url
61
- return {"ok": True}
62
-
63
- # ========== Gradio helpers ==========
64
- def make_headers(user_key: Optional[str]):
65
  api_key = (user_key or os.getenv("KIE_API_KEY") or "").strip()
66
  if not api_key:
67
  raise gr.Error("Masukkan KIE API key atau set KIE_API_KEY di Secrets.")
@@ -70,26 +17,21 @@ def make_headers(user_key: Optional[str]):
70
  "Content-Type": "application/json",
71
  }
72
 
73
- def create_task(headers, prompt: str, aspect_ratio: str, duration: str | None, callback_url: str | None) -> Tuple[str, Optional[str], str]:
74
  payload = {
75
  "model": "sora-2-text-to-video",
76
  "input": {
77
  "prompt": prompt,
78
  "aspect_ratio": aspect_ratio,
79
- # Patuhi ToS: jangan hapus watermark
80
- "remove_watermark": False,
81
  },
82
  }
83
- if callback_url:
84
- payload["callBackUrl"] = callback_url.strip()
85
-
86
- # Durasi (opsional); hanya sertakan jika valid integer > 0
87
  if duration:
88
  try:
89
  d = int(duration)
90
  if d > 0:
91
  payload["input"]["duration"] = d
92
- except:
93
  pass
94
 
95
  r = requests.post(CREATE_URL, headers=headers, json=payload, timeout=60)
@@ -99,7 +41,6 @@ def create_task(headers, prompt: str, aspect_ratio: str, duration: str | None, c
99
  data = r.json()
100
  raw_preview = json.dumps(data)[:500]
101
 
102
- # Ambil task-id dan status_url kalau disediakan
103
  task_id = (
104
  data.get("task_id") or data.get("taskId") or data.get("id") or
105
  (data.get("data") or {}).get("taskId") or (data.get("data") or {}).get("id")
@@ -109,18 +50,30 @@ def create_task(headers, prompt: str, aspect_ratio: str, duration: str | None, c
109
  (data.get("data") or {}).get("status_url") or (data.get("data") or {}).get("statusUrl")
110
  )
111
  if not task_id:
112
- raise gr.Error(f"Tidak menemukan task_id pada response createTask: {raw_preview}")
113
 
114
  return task_id, status_url, raw_preview
115
 
116
  def infer_status_and_result(obj: dict):
117
  status = obj.get("status") or obj.get("state") or (obj.get("data") or {}).get("status")
118
  progress = obj.get("progress") or (obj.get("data") or {}).get("progress")
119
- video_url = try_extract_video_url(obj)
120
- return status, progress, video_url
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
- def probe_status_url(headers, task_id: str) -> Optional[str]:
123
- # Coba beberapa pola umum. Ganti jika dokumentasi Kie AI menyebutkan URL spesifik.
124
  candidates = [
125
  f"{BASE}/jobs/getTask?taskId={task_id}",
126
  f"{BASE}/jobs/get-task?taskId={task_id}",
@@ -143,7 +96,7 @@ def probe_status_url(headers, task_id: str) -> Optional[str]:
143
  return url
144
  except Exception:
145
  pass
146
- # Beberapa API memakai POST untuk getTask
147
  post_url = f"{BASE}/jobs/getTask"
148
  try:
149
  r = requests.post(post_url, headers=headers, json={"taskId": task_id}, timeout=30)
@@ -159,27 +112,24 @@ def probe_status_url(headers, task_id: str) -> Optional[str]:
159
  def poll_until_done(headers, status_url: str, task_id: str, interval=5, max_wait=60*40):
160
  waited = 0
161
  last_status = None
162
- used_post_mode = status_url.endswith("(POST)")
163
-
164
  while waited <= max_wait:
165
  try:
166
- if used_post_mode:
167
  r = requests.post(status_url.replace(" (POST)", ""), headers=headers, json={"taskId": task_id}, timeout=60)
168
  else:
169
  r = requests.get(status_url, headers=headers, timeout=60)
170
  except Exception as e:
171
  yield f"[WARN] Poll error: {e}", None
172
- time.sleep(interval)
173
- waited += interval
174
- continue
175
 
176
  if r.status_code == 404:
177
- yield f"[WARN] Poll 404 di {status_url} — mencari endpoint lain...", None
178
  new_url = probe_status_url(headers, task_id)
179
  if not new_url:
180
- raise gr.Error("Gagal menemukan endpoint status. Mohon cek dokumentasi Kie AI.")
181
  status_url = new_url
182
- used_post_mode = status_url.endswith("(POST)")
183
  continue
184
 
185
  if r.status_code >= 400:
@@ -188,8 +138,8 @@ def poll_until_done(headers, status_url: str, task_id: str, interval=5, max_wait
188
  try:
189
  data = r.json()
190
  except Exception:
191
- yield "[WARN] Response status bukan JSON yang valid.", None
192
  data = {}
 
193
 
194
  status, progress, result_url = infer_status_and_result(data)
195
  if status != last_status:
@@ -197,15 +147,13 @@ def poll_until_done(headers, status_url: str, task_id: str, interval=5, max_wait
197
  last_status = status
198
 
199
  s = (str(status) or "").lower()
200
- if s in {"succeeded", "completed", "success", "done"}:
201
- yield "Selesai (server) — mengunduh hasil...", result_url
202
  return
203
- if s in {"failed", "error", "canceled", "cancelled"}:
204
  raise gr.Error(f"Task gagal:\n{json.dumps(data, indent=2)}")
205
 
206
- time.sleep(interval)
207
- waited += interval
208
-
209
  raise gr.Error("Timeout menunggu task selesai.")
210
 
211
  def download_result(url: str, task_id: str):
@@ -218,102 +166,58 @@ def download_result(url: str, task_id: str):
218
  raise gr.Error(f"Gagal download hasil: {r.status_code} {r.text}")
219
  with open(out, "wb") as f:
220
  for chunk in r.iter_content(chunk_size=1 << 20):
221
- if chunk:
222
- f.write(chunk)
223
  return str(out)
224
 
225
- # ========== Gradio flows ==========
226
- def run_job(user_api_key, prompt, aspect_ratio, duration, callback_url):
227
  headers = make_headers(user_api_key)
 
 
228
 
229
- # 1) Buat task
230
- task_id, status_url, raw_preview = create_task(headers, prompt, aspect_ratio, duration, callback_url)
231
- yield f"Task dibuat: {task_id}\nStatus URL (server): {status_url or '-'}\nCreate resp: {raw_preview}", None
232
-
233
- # 2) Jika callback URL diisi, kita tunggu callback sambil tetap coba polling kalau status_url tersedia.
234
- result_link = None
235
-
236
- if status_url:
237
- for status_msg, maybe_url in poll_until_done(headers, status_url, task_id):
238
- yield status_msg, None
239
- if maybe_url:
240
- result_link = maybe_url
241
-
242
- if not result_link and callback_url:
243
- # Tunggu callback hingga video_url diterima
244
- yield "Menunggu callback dari Kie AI di endpoint /api/callback ...", None
245
- for _ in range(60 * 10 // 3): # ~10 menit
246
- if LAST_VIDEO_URL:
247
- result_link = LAST_VIDEO_URL
248
- break
249
- time.sleep(3)
250
- if not result_link:
251
- raise gr.Error("Belum menerima callback berisi video_url. Periksa bahwa callback URL publik benar.")
252
-
253
- if not result_link:
254
- # Tidak ada callback dan tidak ada status_url yang bisa dipakai
255
- # Coba auto-probe
256
  found = probe_status_url(headers, task_id)
257
  if not found:
258
- raise gr.Error("Tidak ada result_url. Endpoint status tidak diketahui. Coba isi Callback URL atau kirim response createTask lengkap.")
259
- for status_msg, maybe_url in poll_until_done(headers, found, task_id):
260
- yield status_msg, None
261
- if maybe_url:
262
- result_link = maybe_url
 
 
 
263
 
264
- # 3) Unduh & tampilkan
265
  path = download_result(result_link, task_id)
266
  yield f"Selesai ✅ (task {task_id})", path
267
 
268
- def show_callback():
269
- preview = json.dumps(LAST_CALLBACK_JSON, indent=2) if LAST_CALLBACK_JSON else "(belum ada)"
270
- hint = LAST_VIDEO_URL or "(belum ada video_url)"
271
- return f"Last callback JSON:\n{preview}\n\nExtracted video_url: {hint}"
272
-
273
  def runtime_info(user_api_key):
274
  key = (user_api_key or os.getenv("KIE_API_KEY") or "").strip()
275
  src = "BYOK (user input)" if user_api_key else ("ENV KIE_API_KEY" if os.getenv("KIE_API_KEY") else "NONE")
276
  masked = (key[:4] + "..." + key[-4:]) if key else "(none)"
277
- return f"Key source: {src}\nKey hint: {masked}\nBase: {BASE}\nCallback endpoint on this Space: /api/callback"
278
 
279
- # Gradio UI
280
- with gr.Blocks(title="Kie AI — Sora‑2 Text‑to‑Video (HF Space)") as demo:
281
- gr.Markdown("## Kie AI — Sora‑2 Text‑to‑Video\n- Gunakan API key Kie AI (bukan OpenAI).\n- Jika pakai callback, set ke URL publik Space ini: https://<subdomain>.hf.space/api/callback\n- remove_watermark diset False (patuh ToS).")
282
  api = gr.Textbox(label="KIE API Key (opsional, BYOK)", type="password", placeholder="kie-...")
 
 
 
 
 
 
 
 
 
283
  with gr.Row():
284
- prompt = gr.Textbox(
285
- label="Prompt",
286
- lines=7,
287
- value=(
288
- "Photorealistic handheld POV at night in a foggy tropical forest; "
289
- "a large animal silhouette crosses the path, eyes reflecting light; "
290
- "volumetric fog, wet leaves; vertical 9:16; no blood, no gore, no text, no watermark."
291
- ),
292
- )
293
- callback_url = gr.Textbox(
294
- label="Callback URL (opsional)",
295
- placeholder="https://<your-space>.hf.space/api/callback atau https://webhook.site/xxx",
296
- value="",
297
- )
298
- with gr.Row():
299
- aspect = gr.Dropdown(
300
- choices=["portrait", "landscape", "square", "9:16", "16:9"],
301
- value="portrait",
302
- label="Aspect ratio"
303
- )
304
  duration = gr.Textbox(value="", label="Duration (detik, opsional)", placeholder="mis. 12 (kosongkan jika ragu)")
305
  run = gr.Button("Generate")
306
  status = gr.Textbox(label="Status", lines=6)
307
  video = gr.Video(label="Hasil video")
308
- with gr.Row():
309
- check = gr.Button("Check runtime")
310
- info = gr.Textbox(label="Runtime info")
311
- see_cb = gr.Button("Show last callback")
312
- cb = gr.Textbox(label="Callback payload (preview)", lines=10)
313
 
314
- run.click(run_job, inputs=[api, prompt, aspect, duration, callback_url], outputs=[status, video])
315
  check.click(runtime_info, inputs=[api], outputs=[info])
316
- see_cb.click(lambda: (show_callback()), outputs=[cb])
317
 
318
- # Mount Gradio di FastAPI agar /api/callback bisa diakses dari luar
319
- app = gr.mount_gradio_app(fastapi_app, demo, path="/")
 
4
  import pathlib
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:
14
  raise gr.Error("Masukkan KIE API key atau set KIE_API_KEY di Secrets.")
 
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, # patuh ToS
 
27
  },
28
  }
 
 
 
 
29
  if duration:
30
  try:
31
  d = int(duration)
32
  if d > 0:
33
  payload["input"]["duration"] = d
34
+ except Exception:
35
  pass
36
 
37
  r = requests.post(CREATE_URL, headers=headers, json=payload, timeout=60)
 
41
  data = r.json()
42
  raw_preview = json.dumps(data)[:500]
43
 
 
44
  task_id = (
45
  data.get("task_id") or data.get("taskId") or data.get("id") or
46
  (data.get("data") or {}).get("taskId") or (data.get("data") or {}).get("id")
 
50
  (data.get("data") or {}).get("status_url") or (data.get("data") or {}).get("statusUrl")
51
  )
52
  if not task_id:
53
+ raise gr.Error(f"Tidak menemukan task_id: {raw_preview}")
54
 
55
  return task_id, status_url, raw_preview
56
 
57
  def infer_status_and_result(obj: dict):
58
  status = obj.get("status") or obj.get("state") or (obj.get("data") or {}).get("status")
59
  progress = obj.get("progress") or (obj.get("data") or {}).get("progress")
60
+ # cari link video pada beberapa kemungkinan field
61
+ for key in [
62
+ "result_url","output_url","video_url","download_url",
63
+ ]:
64
+ if key in obj and isinstance(obj[key], str) and obj[key].startswith("http"):
65
+ return status, progress, obj[key]
66
+ data = obj.get("data") or {}
67
+ for key in [
68
+ "result_url","output_url","video_url","download_url",
69
+ ]:
70
+ v = data.get(key)
71
+ if isinstance(v, str) and v.startswith("http"):
72
+ return status, progress, v
73
+ return status, progress, None
74
 
75
+ def probe_status_url(headers, task_id: str):
76
+ # Coba beberapa pola umum sampai ada yang OK
77
  candidates = [
78
  f"{BASE}/jobs/getTask?taskId={task_id}",
79
  f"{BASE}/jobs/get-task?taskId={task_id}",
 
96
  return url
97
  except Exception:
98
  pass
99
+ # Beberapa layanan pakai POST getTask
100
  post_url = f"{BASE}/jobs/getTask"
101
  try:
102
  r = requests.post(post_url, headers=headers, json={"taskId": task_id}, timeout=30)
 
112
  def poll_until_done(headers, status_url: str, task_id: str, interval=5, max_wait=60*40):
113
  waited = 0
114
  last_status = None
115
+ used_post = status_url.endswith("(POST)")
 
116
  while waited <= max_wait:
117
  try:
118
+ if used_post:
119
  r = requests.post(status_url.replace(" (POST)", ""), headers=headers, json={"taskId": task_id}, timeout=60)
120
  else:
121
  r = requests.get(status_url, headers=headers, timeout=60)
122
  except Exception as e:
123
  yield f"[WARN] Poll error: {e}", None
124
+ time.sleep(interval); waited += interval; continue
 
 
125
 
126
  if r.status_code == 404:
127
+ yield f"[WARN] Poll 404 di {status_url} — mencoba endpoint lain...", None
128
  new_url = probe_status_url(headers, task_id)
129
  if not new_url:
130
+ raise gr.Error("Tidak menemukan endpoint status. Mohon cek dokumentasi Kie AI.")
131
  status_url = new_url
132
+ used_post = status_url.endswith("(POST)")
133
  continue
134
 
135
  if r.status_code >= 400:
 
138
  try:
139
  data = r.json()
140
  except Exception:
 
141
  data = {}
142
+ yield "[WARN] Response status bukan JSON.", None
143
 
144
  status, progress, result_url = infer_status_and_result(data)
145
  if status != last_status:
 
147
  last_status = status
148
 
149
  s = (str(status) or "").lower()
150
+ if s in {"succeeded","completed","success","done"}:
151
+ yield "Selesai di server — mengunduh...", result_url
152
  return
153
+ if s in {"failed","error","canceled","cancelled"}:
154
  raise gr.Error(f"Task gagal:\n{json.dumps(data, indent=2)}")
155
 
156
+ time.sleep(interval); waited += interval
 
 
157
  raise gr.Error("Timeout menunggu task selesai.")
158
 
159
  def download_result(url: str, task_id: str):
 
166
  raise gr.Error(f"Gagal download hasil: {r.status_code} {r.text}")
167
  with open(out, "wb") as f:
168
  for chunk in r.iter_content(chunk_size=1 << 20):
169
+ if chunk: f.write(chunk)
 
170
  return str(out)
171
 
172
+ def run_job(user_api_key, prompt, aspect_ratio, duration):
 
173
  headers = make_headers(user_api_key)
174
+ task_id, status_url, preview = create_task(headers, prompt, aspect_ratio, duration)
175
+ yield f"Task dibuat: {task_id}\nStatus URL (server): {status_url or '-'}\nCreate resp: {preview}", None
176
 
177
+ if not status_url:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  found = probe_status_url(headers, task_id)
179
  if not found:
180
+ raise gr.Error("Tidak bisa menentukan endpoint status. Kirim response createTask penuh agar saya mappingkan field yang benar.")
181
+ status_url = found
182
+
183
+ result_link = None
184
+ for status_msg, maybe_url in poll_until_done(headers, status_url, task_id):
185
+ yield status_msg, None
186
+ if maybe_url:
187
+ result_link = maybe_url
188
 
 
189
  path = download_result(result_link, task_id)
190
  yield f"Selesai ✅ (task {task_id})", path
191
 
 
 
 
 
 
192
  def runtime_info(user_api_key):
193
  key = (user_api_key or os.getenv("KIE_API_KEY") or "").strip()
194
  src = "BYOK (user input)" if user_api_key else ("ENV KIE_API_KEY" if os.getenv("KIE_API_KEY") else "NONE")
195
  masked = (key[:4] + "..." + key[-4:]) if key else "(none)"
196
+ return f"Key source: {src}\nKey hint: {masked}\nBase: {BASE}"
197
 
198
+ # ============ Gradio UI (Pure Gradio) ============
199
+ with gr.Blocks(title="Kie AI — Sora‑2 Text‑to‑Video (Polling)") as demo:
200
+ gr.Markdown("## Kie AI — Sora‑2 Text‑to‑Video\n- Gunakan API key Kie AI (bukan OpenAI).\n- remove_watermark dimatikan (False).")
201
  api = gr.Textbox(label="KIE API Key (opsional, BYOK)", type="password", placeholder="kie-...")
202
+ prompt = gr.Textbox(
203
+ label="Prompt",
204
+ lines=7,
205
+ value=(
206
+ "Photorealistic handheld POV at night in a foggy tropical forest; "
207
+ "a large animal silhouette crosses the path, eyes reflecting light; "
208
+ "volumetric fog, wet leaves; vertical 9:16; no blood, no gore, no text, no watermark."
209
+ ),
210
+ )
211
  with gr.Row():
212
+ aspect = gr.Dropdown(["portrait", "landscape", "square", "9:16", "16:9"], value="portrait", label="Aspect ratio")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  duration = gr.Textbox(value="", label="Duration (detik, opsional)", placeholder="mis. 12 (kosongkan jika ragu)")
214
  run = gr.Button("Generate")
215
  status = gr.Textbox(label="Status", lines=6)
216
  video = gr.Video(label="Hasil video")
217
+ check = gr.Button("Check runtime")
218
+ info = gr.Textbox(label="Runtime info")
 
 
 
219
 
220
+ run.click(run_job, inputs=[api, prompt, aspect, duration], outputs=[status, video])
221
  check.click(runtime_info, inputs=[api], outputs=[info])
 
222
 
223
+ # Penting untuk Spaces: expose variable 'demo'