sharul20001 commited on
Commit
9ea4826
·
verified ·
1 Parent(s): 81e1375

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +97 -34
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
- # Demi kepatuhan, jangan bantu hapus watermark
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"Gagal createTask: {r.status_code} {r.text}")
47
 
48
  data = r.json()
49
- # Ambil task id dan (jika ada) status_url yang diberikan server
 
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 di response: {json.dumps(data)[:500]}")
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, data
67
 
68
- # Baca status + result url dari berbagai bentuk response
69
  def infer_status_and_result(obj: dict):
70
- status = obj.get("status") or obj.get("state") or obj.get("task_status") or obj.get("data", {}).get("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
- # Polling status sampai selesai
80
- def poll_until_done(headers, status_url: str, interval=5, max_wait=60*40):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  waited = 0
82
  last_status = None
 
 
83
  while waited <= max_wait:
84
- r = requests.get(status_url, headers=headers, timeout=60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  if r.status_code >= 400:
86
  yield f"[WARN] Poll failed: {r.status_code} {r.text}", None
87
  else:
88
- data = r.json()
 
 
 
 
 
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: {json.dumps(data, indent=2)}")
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, raw_create = create_task(headers, prompt, aspect_ratio, duration)
127
- yield f"Task dibuat: {task_id}\nPolling: {status_url}\nResponse: {json.dumps(raw_create)[:400]}...", None
 
 
 
 
 
 
 
 
128
 
129
- # 2) poll
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
- # 3) download & tampilkan
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 URL: {BASE}"
145
 
146
- with gr.Blocks(title="Kie AI — Sora 2 Text-to-Video") as demo:
147
- gr.Markdown("## Kie AI — Sora‑2 Text‑to‑Video (Polling)\n- Gunakan API key Kie AI (bukan OpenAI).")
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="contoh: 12 (kosongkan jika ragu)")
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 — Sora2 TexttoVideo") 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")