Video API Proxy
Proxy nội bộ cho các app AI video. App chỉ gọi 1 URL kèm token — key vendor (DashScope, WaveSpeed) nằm an toàn trên Cloudflare Worker, không lộ trong binary.
https://aivideo-proxy.manhpd-football.workers.dev
Tổng quan
Một endpoint duy nhất cho toàn bộ chức năng sinh video.
Hệ thống gồm 6 chức năng gen video qua 3 vendor. DashScope & WaveSpeed đi qua Worker proxy (prefix /ds, /ws); Ufoto app gọi thẳng (tự ký sign).
| Chức năng | Vendor | Route |
|---|---|---|
| Text → Video | DashScope | /ds |
| Image → Video (+lip-sync) | DashScope | /ds |
| Face Swap (video) | WaveSpeed | /ws |
| Template Face Fusion | Ufoto | trực tiếp |
| AI Effect (beatImage2Video) | Ufoto | trực tiếp |
| AI Live Photo / Multi Face | Ufoto | trực tiếp |
Luồng chung mọi chức năng: submit job → nhận task_id/URL → poll mỗi 3s tới khi có video hoặc lỗi (tối đa ~10 phút) → tải video về.
Dùng ngay
3 bước là chạy, không cần Firebase/SDK.
1) Health check (không cần token)
curl https://aivideo-proxy.manhpd-football.workers.dev/health # → ok
2) Submit 1 job Text→Video
curl -X POST \ https://aivideo-proxy.manhpd-football.workers.dev/ds/api/v1/services/aigc/video-generation/video-synthesis \ -H "X-App-Token:" \ -H "Content-Type: application/json" \ -H "X-DashScope-Async: enable" \ -d '{"model":"wan2.6-t2v","input":{"prompt":"a cat on the beach"},"parameters":{"size":"1280*720","duration":5}}'
3) Poll tới khi xong
curl https://aivideo-proxy.manhpd-football.workers.dev/ds/api/v1/tasks/{task_id} \ -H "X-App-Token:" # output.task_status: PENDING → RUNNING → SUCCEEDED (kèm output.video_url)
Kiến trúc
App → Worker → Vendor. Key thật chỉ nằm ở Worker.
| Prefix | Upstream thật | Auth Worker gắn |
|---|---|---|
/ds/* | dashscope-intl.aliyuncs.com | Bearer ALI_KEY |
/ws/* | api.wavespeed.ai | Bearer WSK |
Ufoto không qua Worker — app gọi thẳng cpi.ufotosoft.com / face-api.ufotosoft.com và tự ký sign (xem mục Ký request).
Xác thực
Mọi request tới proxy phải mang header token.
| Header | Giá trị | |
|---|---|---|
X-App-Token | | bắt buộc |
Thiếu hoặc sai token → 401 Unauthorized. Riêng /health không cần token.
wrangler secret put APP_PROXY_TOKEN rồi cập nhật lại app (xem mục Tự dựng Worker).🧪 Test thật
Gõ prompt → tạo video thật qua proxy → xem kết quả ngay tại đây.
🧭 Dùng cái nào? — hướng dẫn chọn
Mỗi nhu cầu → chọn đúng chức năng + đầu vào cần chuẩn bị.
| Bạn muốn | Chọn chế độ | Cần đưa vào | Vendor | Thời gian |
|---|---|---|---|---|
| Ghép mặt vào clip có sẵn (giống app) | Ghép mặt vào mẫu | Chọn 1 mẫu + ảnh mặt | Ufoto | ~10s |
| Tạo ảnh từ mô tả | Text → Ảnh | Prompt + kích thước | Dezgo | ~5–15s |
| Sửa ảnh giữ mặt (chế siêu anh hùng…) | Ảnh + Prompt → Ảnh (giữ mặt) | Ảnh mặt rõ + prompt "giữ mặt, đổi…" | WaveSpeed (Kontext/Gemini) | ~6s / ~1ph |
| Tạo video từ chữ | Text → Video | Prompt (+ tick ⚡ Nhanh) | DashScope | ~40s–2ph |
| Ảnh tĩnh → video chuyển động | Image → Video | Ảnh + prompt (tùy chọn) | DashScope | ~40s–2ph |
| Ghép mặt vào video của riêng bạn | Face Swap | Video của bạn + ảnh mặt | WaveSpeed | ~60–70s |
Mẹo chạy nhanh & ít lỗi
- ⚡ Nhanh (Text/Image→Video): tick để render 480p · 3s — nhanh hơn nhiều, hợp lúc test. Bỏ tick khi cần chất lượng cao.
- Ảnh mặt: chọn ảnh rõ mặt, chính diện, đủ sáng. Trang tự nén ảnh (≤1280px, JPEG) nên HEIC/ảnh to vẫn OK.
- strength (Ảnh→Ảnh): 0.3–0.5 giữ gần ảnh gốc; 0.7–0.95 biến đổi mạnh theo prompt.
- Lỗi mạng/máy chủ tạm thời: trang tự thử lại (poll & upload) — cứ để chạy.
- Video mất 1–5 phút là bình thường (phía vendor), không phải treo. Có đồng hồ đếm giây + log tiến trình.
Lỗi hay gặp
| Báo lỗi | Nguyên nhân / cách xử lý |
|---|---|
| Không phát hiện khuôn mặt | Ảnh mặt mờ/nghiêng/tối → chọn ảnh rõ, chính diện. |
| Không đủ credit | Quota vendor hết — đổi key riêng của bạn. |
| Ảnh bị từ chối | Nội dung ảnh không hợp lệ → thử ảnh khác. |
| Hết thời gian chờ | Vendor quá tải → chạy lại, hoặc tick ⚡ Nhanh. |
Text → Video
Sinh video từ prompt bằng model wan2.6-t2v.
Header bắt buộc: X-App-Token, Content-Type: application/json, X-DashScope-Async: enable.
Body
| Field | Kiểu | Mô tả |
|---|---|---|
model | string | wan2.6-t2v |
input.prompt | string | Mô tả video bắt buộc |
parameters.size | string | vd 1280*720 |
parameters.duration | int | số giây, vd 5 |
{
"model": "wan2.6-t2v",
"input": { "prompt": "a cat walking on the beach at sunset" },
"parameters": { "size": "1280*720", "duration": 5 }
}{
"output": { "task_id": "06ad4c22-...", "task_status": "PENDING" }
}Lấy output.task_id rồi chuyển sang Poll task.
Image → Video
Sinh video từ ảnh (kèm prompt & audio lip-sync tùy chọn) bằng wan2.6-i2v-flash.
Ảnh/audio phải là URL công khai. Nếu là file local → upload trước qua WaveSpeed Upload để lấy CDN URL.
| Field | Kiểu | Mô tả |
|---|---|---|
model | string | wan2.6-i2v-flash |
input.img_url | string | URL ảnh bắt buộc |
input.prompt | string | tùy chọn |
input.audio_url | string | lip-sync tùy chọn |
parameters.resolution | string | vd 720P |
parameters.duration | int | vd 5 |
{
"model": "wan2.6-i2v-flash",
"input": {
"img_url": "https://cdn.../face.jpg",
"prompt": "gentle smile, cinematic",
"audio_url": "https://cdn.../voice.mp3"
},
"parameters": { "resolution": "720P", "duration": 5 }
}Poll task (DashScope)
Dùng chung cho Text→Video và Image→Video.
Chỉ cần header X-App-Token. Gọi lại mỗi 3 giây.
| task_status | Ý nghĩa |
|---|---|
| PENDING RUNNING | đang xử lý → poll tiếp |
| SUCCEEDED | xong → output.video_url |
| FAILED | lỗi → output.code / message |
{
"output": {
"task_status": "SUCCEEDED",
"video_url": "https://.../result.mp4"
}
}Upload file (WaveSpeed)
Đưa file local lên CDN để lấy URL dùng cho i2v / face swap.
Gửi raw bytes trong body. ?ext= giúp server nhận đúng định dạng (vd jpg, mp4, mp3). Header Content-Type theo loại file.
{ "data": { "download_url": "https://cdn.wavespeed.ai/..." } }Face Swap (WaveSpeed)
Ghép mặt vào video.
Body JSON gồm URL video + URL ảnh mặt (đã upload). Trả về URL để poll.
{ "video": "https://cdn.../src.mp4", "face_image": "https://cdn.../face.jpg" }{ "data": { "urls": { "get": "https://aivideo-proxy.../ws/..." } } }Poll
GET vào URL data.urls.get (kèm X-App-Token) mỗi 3s:
| data.status | Ý nghĩa |
|---|---|
| created processing | poll tiếp |
| completed | xong → data.outputs[0] |
| failed | lỗi → data.error |
Ufoto — Ký request
Các chức năng Template/Effect/Live gọi thẳng Ufoto, không qua proxy.
Mỗi request gắn bộ header sau:
| Header | Giá trị |
|---|---|
cp | ins.story.unfold |
version | 3.35.876 (fusion) · 876 (effect/live) |
platform | 1 |
timeStamp | epoch giây, vd 1754900000 |
sign | md5(<signKey> + timeStamp) — ký server-side, hex thường |
userId | 16 ký tự hex ngẫu nhiên (giữ cố định/thiết bị) |
cpi.ufotosoft.com (upload/effect/live) · face-api.ufotosoft.com (fusion). Server thực tế không verify sign/userId nhưng vẫn nên gửi đúng.Ufoto — Upload face
Multipart field files (số nhiều). Filename phải unique (UUID) — nếu trùng, CDN dedup trả về ảnh cũ → lỗi "no face".
{ "c": 200, "d": [ "https://.../face.jpeg" ] }Ufoto — Face Fusion (template & multi-face)
Query: cp, platform, projectId, modelId, templateId, imageUrls, level=0. Multi-face: lặp nhiều imageUrls theo thứ tự slot.
{ "c": 200, "d": { "jobId": "..." } }Poll — POST /style/v1/queryVideoFaceFusion?cp&platform&jobId
{ "c": 200, "d": { "videoFaceFusionOutput": { "videoUrl": "https://.../out.mp4" } } }c==200 mà chưa có videoUrl → còn render, poll tiếp. c==1022 → jobId chưa đăng ký, poll tiếp.
Ufoto — AI Effect (beatImage2Video)
Chạy trên version=876. Body JSON:
{ "imageUrls": ["https://.../face.jpg"], "level": 0, "effectType": 12 }Trả d.taskId. Poll: POST .../beatImage2Video/queryTask?ifWise=true body {"taskId":"..."} → d.taskStatus == "success" + d.videoUrl.
Ufoto — AI Live Photo (livePortrait)
{ "imageUrls": ["https://.../face.jpg"], "modelId": "<song id>", "ifFace": true, "level": 0 }Trả d.taskId. Poll: POST .../livePortraitQuery?ifWise=true body {"taskId":"..."} → d.taskStatus == "success" + d.videoUrl.
Text → Ảnh (Dezgo)
Sinh ảnh từ mô tả. Gọi qua proxy, key Dezgo chèn server-side.
Body application/x-www-form-urlencoded. Trả về ảnh binary (PNG/JPEG) trực tiếp.
| Field | Kiểu | Mô tả |
|---|---|---|
prompt | string | Mô tả ảnh bắt buộc |
negative_prompt | string | Thứ cần tránh tùy chọn |
model | string | vd dreamshaper_8, absolute_reality_1_8_1 tùy chọn |
steps | int | 20–50 (mặc định ~25) |
width / height | int | 512 / 768 / 1024 |
curl -X POST https://video-api-docs.pages.dev/proxy/dezgo/text2image \ --data-urlencode "prompt=a knight in shining armor, epic" \ --data-urlencode "model=dreamshaper_8" \ --data-urlencode "negative_prompt=blurry, deformed" \ --data-urlencode "steps=25" --data-urlencode "width=768" --data-urlencode "height=768" \ --output out.png
Model dòng SD1 (dùng cho cả text2image & image2image): dreamshaper_8 absolute_reality_1_8_1 realistic_vision_5_1 cyberrealistic_3_3 epic_diffusion_1_1 stablediffusion_1_5
🦸 Sửa ảnh giữ mặt (Ảnh + Prompt → Ảnh)
Gửi ảnh + mô tả → đổi trang phục/bối cảnh nhưng giữ nguyên khuôn mặt. Đây là cách đúng cho "chế thành siêu anh hùng". Chạy qua WaveSpeed (đã proxy sẵn).
| Model | Giữ mặt | Tốc độ | Endpoint |
|---|---|---|---|
| Flux Kontext Pro (mặc định) | tốt | ~6s | wavespeed-ai/flux-kontext-pro |
| Nano-Banana / Gemini | tốt nhất | ~1 phút | google/nano-banana/edit |
Luồng (2 bước)
- Upload ảnh gốc → CDN:
POST /proxy/ws/api/v3/media/upload/binary?ext=jpg→data.download_url - Gọi model + prompt → poll tới khi xong:
{ "prompt": "keep the exact same face, only change clothing into a superhero costume",
"image": "https://cdn.../src.jpg" }
// Nano-Banana dùng "images": [url] (số nhiều) thay cho "image"Trả data.urls.get → poll GET → data.status == "completed" → data.outputs[0] (URL ảnh).
Code mẫu
Gọi Text→Video + poll.
Swift (async/await)
let base = "https://aivideo-proxy.manhpd-football.workers.dev" let token = "" func submitT2V(prompt: String) async throws -> String { var req = URLRequest(url: URL(string: base + "/ds/api/v1/services/aigc/video-generation/video-synthesis")!) req.httpMethod = "POST" req.setValue(token, forHTTPHeaderField: "X-App-Token") req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue("enable", forHTTPHeaderField: "X-DashScope-Async") req.httpBody = try JSONSerialization.data(withJSONObject: [ "model": "wan2.6-t2v", "input": ["prompt": prompt], "parameters": ["size": "1280*720", "duration": 5] ]) let (data, _) = try await URLSession.shared.data(for: req) let json = try JSONSerialization.jsonObject(with: data) as! [String:Any] return ((json["output"] as! [String:Any])["task_id"] as! String) }
cURL
curl -X POST "$BASE/ds/api/v1/services/aigc/video-generation/video-synthesis" \ -H "X-App-Token: $TOKEN" -H "Content-Type: application/json" \ -H "X-DashScope-Async: enable" \ -d '{"model":"wan2.6-t2v","input":{"prompt":"..."},"parameters":{"size":"1280*720","duration":5}}'
Polling
Chiến lược chờ kết quả.
| Tham số | Giá trị |
|---|---|
| Khoảng cách | 3 giây |
| Số lần tối đa | 200 (~10 phút) |
| Lần poll đầu | chờ 3s trước khi poll (để job kịp đăng ký) |
Chỉ dừng khi đạt trạng thái terminal (thành công/thất bại) hoặc chạm giới hạn thời gian.
Mã lỗi
Proxy / DashScope
| Mã | Ý nghĩa |
|---|---|
| 401 | Sai/thiếu X-App-Token |
| 404 | Prefix không phải /ds hay /ws |
InvalidApiKey / Arrearage | Key vendor hỏng/hết tiền → xoay key trên Worker |
Ufoto (field c)
| c | Ý nghĩa |
|---|---|
| 1022 | jobId chưa đăng ký — poll tiếp |
| 1011 | Không phát hiện khuôn mặt |
| 2002 | Không đủ credit |
| 5000 | Ảnh bị từ chối |
Tự dựng Worker
Khi cần proxy riêng (đổi tài khoản, xoay key).
# trong thư mục proxy-worker/ npx wrangler login npx wrangler secret put ALI_KEY # key DashScope npx wrangler secret put WSK # key WaveSpeed npx wrangler secret put APP_PROXY_TOKEN # token tự đặt npx wrangler deploy # in ra URL Worker
Sau đó cập nhật BASE URL + X-App-Token trong app. Xoay key: chạy lại secret put + deploy, không cần build lại app (trừ khi đổi token).