
Chanjing Avatar
- 85 installs
- 18 repo stars
- Updated March 28, 2026
- chanjing-ai/chan-skills
Creates lip-synced digital-avatar videos via the Chanjing Avatar Open API by uploading audio/video and polling task results.
About
Uploads audio and video material to the Chanjing Avatar Open API, creates lip-syncing tasks, and polls for the result video URL. A developer uses it to produce digital-avatar videos with synchronized mouth movements.
- Upload audio/video, create lip-sync tasks, poll and fetch results
- Python HTTP/upload client against Chanjing Avatar Open API
Chanjing Avatar by the numbers
- 85 all-time installs (skills.sh)
- Ranked #808 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/chanjing-ai/chan-skills --skill chanjing-avatarAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 85 |
|---|---|
| repo stars | ★ 18 |
| Last updated | March 28, 2026 |
| Repository | chanjing-ai/chan-skills ↗ |
What it does
Creates lip-synced digital-avatar videos via the Chanjing Avatar Open API by uploading audio/video and polling task results.
Files
Chanjing Avatar (Lip-Syncing)
功能说明
调用蝉镜 Avatar Open API:上传音视频素材、创建对口型任务、轮询与获取结果链接。脚本为 Python HTTP/上传客户端,不依赖 ffmpeg/ffprobe。
运行依赖
- python3 与同仓库
scripts/*.py - 无 ffmpeg/ffprobe 门控
环境变量与机器可读声明
- 环境变量键名与说明:`manifest.yaml`(
environment段)及本文 - 变量、凭据、合规 `permissions`、`clientPermissions`、`agentPolicy`:`manifest.yaml`
使用命令
- ClawHub(slug 以注册表为准):
clawhub run chanjing-avatar - 本仓库:
python skills/chanjing-avatar/scripts/create_task.py …(流程见正文 How to Use)
---
登记与审稿(单一事实来源)
主凭据、上传/下载边界、浏览器引导等:以 `manifest.yaml` 为准。本篇 How to Use 起为 API 步骤说明。
When to Use This Skill
Use this skill when the user needs to create lip-syncing videos (digital avatar videos) with synchronized mouth movements.
Chanjing Avatar supports:
- Text-driven or audio-driven lip-syncing
- Multiple system voices for TTS
- Video resolution customization
- Task status polling and callback
How to Use This Skill
前置条件:执行本 Skill 前,必须先通过 chanjing-credentials-guard 完成 AK/SK 与 Token 校验。凭据与审稿对表见 `manifest.yaml`。
Security & credentials(引用)
详见 `manifest.yaml` 中 `credentials` 与 `clientPermissions`(含本地上传、结果 URL、浏览器行为;合规见顶层 `permissions`)。
Multiple APIs need to be invoked. All share the domain: "https://open-api.chanjing.cc". All requests communicate using json. You should use utf-8 to encode and decode text throughout this task.
1. Obtain an access_token, which is required for all subsequent API calls 2. Upload your video/audio files using the File Management API to get file_id 3. Create a lip-syncing task with video and audio/text using these file_id values 4. Poll the Query Task Detail API or use Task List API to check status 5. Download the generated video using the url in response when status is completed
Obtain AccessToken
从 ~/.chanjing/credentials.json 读取 app_id 和 secret_key,若无有效 Token 则调用:
POST /open/v1/access_token
Content-Type: application/json请求体(使用本地配置的 app_id、secret_key):
{
"app_id": "<从 credentials.json 读取>",
"secret_key": "<从 credentials.json 读取>"
}Response example:
{
"trace_id": "8ff3fcd57b33566048ef28568c6cee96",
"code": 0,
"msg": "success",
"data": {
"access_token": "1208CuZcV1Vlzj8MxqbO0kd1Wcl4yxwoHl6pYIzvAGoP3DpwmCCa73zmgR5NCrNu",
"expire_in": 1721289220
}
}Response field description:
| First-level Field | Second-level Field | Description |
|---|---|---|
| code | Response status code | |
| msg | Response message | |
| data | Response data | |
| access_token | Valid for one day, previous token will be invalidated | |
| expire_in | Token expiration time |
Response Status Code Description
| Code | Description |
|---|---|
| 0 | Success |
| 400 | Invalid parameter format |
| 40000 | Parameter error |
| 50000 | System internal error |
Upload Media Files (File Management)
Before creating a lip-syncing task, you must upload your video (and optional audio) files using the File Management API to obtain file_id values.
The full documentation is here: [File Management](https://doc.chanjing.cc/api/file/file-management.html).
Step 1: Get upload URL
GET /open/v1/common/create_upload_url
access_token: {{access_token}}Query params:
| Key | Example | Description |
|---|---|---|
| service | lip_sync_video / lip_sync_audio | File usage purpose. Use lip_sync_video for driving video, lip_sync_audio for audio (if audio-driven). |
| name | 1.mp4 | Original file name including extension |
You will get a response containing sign_url, mime_type, and file_id. Use the sign_url with HTTP PUT to upload the file, setting Content-Type to the returned mime_type. After the PUT completes, poll the file detail API until the file is ready (do not assume a fixed wait). Keep the returned file_id for video_file_id / audio_file_id below.
Polling: Call GET /open/v1/common/file_detail?id={{file_id}} with access_token until the response data.status indicates success (e.g. status === 2). Only then use the file_id for the create task API.
Create Lip-Syncing Task
Submit a lip-syncing video creation task, which returns a video ID for polling later.
POST /open/v1/video_lip_sync/create
access_token: {{access_token}}
Content-Type: application/jsonRequest body example (TTS-driven):
{
"video_file_id": "e284db4d95de4220afe78132158156b5",
"screen_width": 1080,
"screen_height": 1920,
"callback": "https://example.com/openapi/callback",
"model": 0,
"audio_type": "tts",
"tts_config": {
"text": "君不见黄河之水天上来,奔流到海不复回。",
"audio_man_id": "C-f2429d07554749839849497589199916",
"speed": 1,
"pitch": 1
}
}Request body example (Audio-driven):
{
"video_file_id": "e284db4d95de4220afe78132158156b5",
"screen_width": 1080,
"screen_height": 1920,
"model": 0,
"audio_type": "audio",
"audio_file_id": "audio_file_id_from_file_management"
}Request field description:
| Parameter Name | Type | Required | Description |
|---|---|---|---|
| video_file_id | string | Yes | Video file ID from File Management (data.file_id). Supports mp4, mov, webm |
| screen_width | int | No | Screen width, default 1080 |
| screen_height | int | No | Screen height, default 1920 |
| backway | int | No | Playback order when reaching end: 1-normal, 2-reverse. Default 1 |
| drive_mode | string | No | Drive mode: ""-normal, "random"-random frame. Default "" |
| callback | string | No | Callback URL for async notification |
| model | int | No | Model version: 0-basic, 1-high quality. Default 0 |
| audio_type | string | No | Audio type: "tts"-text driven, "audio"-audio driven. Default "tts" |
| tts_config | object | Yes (for tts) | TTS configuration when audio_type="tts" |
| tts_config.text | string | Yes (for tts) | Text to synthesize |
| tts_config.audio_man_id | string | Yes (for tts) | Voice ID |
| tts_config.speed | number | No | Speech speed: 0.5-2, default 1 |
| tts_config.pitch | number | No | Pitch, default 1 |
| audio_file_id | string | Yes (for audio) | Audio file ID from File Management (data.file_id) when audio_type="audio". Supports mp3, m4a, wav |
| volume | int | No | Volume: 1-100, default 100 |
Response example:
{
"trace_id": "8d10659438827bd4d59eaa2696f9d391",
"code": 0,
"msg": "success",
"data": "9499ed79995c4bdb95f0d66ca84419fd"
}Response field description:
| Field | Description |
|---|---|
| code | Response status code |
| msg | Response message |
| data | Video ID for subsequent polling |
Query Task List
Get a list of lip-syncing tasks.
POST /open/v1/video_lip_sync/list
access_token: {{access_token}}
Content-Type: application/jsonRequest body:
{
"page": 1,
"page_size": 10
}Request field description:
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | int | No | Page number, default 1 |
| page_size | int | No | Page size, default 10 |
Response example:
{
"trace_id": "8d10659438827bd4d59eaa2696f9d391",
"code": 0,
"msg": "success",
"data": {
"list": [
{
"id": "9499ed79995c4bdb95f0d66ca84419fd",
"status": 20,
"progress": 100,
"msg": "success",
"video_url": "https://res.chanjing.cc/xxx/lip-sync/9499ed79995c4bdb95f0d66ca84419fd.mp4",
"preview_url": "https://res.chanjing.cc/xxx/lip-sync/9499ed79995c4bdb95f0d66ca84419fd.jpg",
"duration": 300,
"create_time": 1738636800
}
],
"page_info": {
"page": 1,
"size": 10,
"total_count": 1,
"total_page": 1
}
}
}Response field description:
| First-level Field | Second-level Field | Description |
|---|---|---|
| code | Response status code | |
| msg | Response message | |
| data | Response data | |
| list | Task list | |
| id: Video ID | ||
| status: Task status (0-pending, 10-generating, 20-success, 30-failed) | ||
| progress: Progress 0-100 | ||
| msg: Task message | ||
| video_url: Video download URL | ||
| preview_url: Cover image URL | ||
| duration: Video duration in ms | ||
| create_time: Creation time (unix timestamp) | ||
| page_info | Pagination info |
Query Task Detail
Poll the following API to check task status until completed.
GET /open/v1/video_lip_sync/detail
access_token: {{access_token}}Query params:
| Parameter | Description |
|---|---|
| id | Video ID |
Example: GET /open/v1/video_lip_sync/detail?id=9499ed79995c4bdb95f0d66ca84419fd
Response example:
{
"trace_id": "8d10659438827bd4d59eaa2696f9d391",
"code": 0,
"msg": "success",
"data": {
"id": "9499ed79995c4bdb95f0d66ca84419fd",
"status": 20,
"progress": 100,
"msg": "success",
"video_url": "https://res.chanjing.cc/xxx/lip-sync/9499ed79995c4bdb95f0d66ca84419fd.mp4",
"preview_url": "https://res.chanjing.cc/xxx/lip-sync/9499ed79995c4bdb95f0d66ca84419fd.jpg",
"duration": 300,
"create_time": 1738636800
}
}Response field description:
| First-level Field | Second-level Field | Description |
|---|---|---|
| code | Response status code | |
| msg | Response message | |
| data | Response data | |
| id | Video ID | |
| status | Task status: 0-pending, 10-generating, 20-success, 30-failed | |
| progress | Progress 0-100 | |
| msg | Task message | |
| video_url | Video download URL | |
| preview_url | Cover image URL | |
| duration | Video duration in ms | |
| create_time | Creation time (unix timestamp) |
Callback Notification
When a callback URL is provided, the system will send a POST request when the task completes:
{
"trace_id": "8d10659438827bd4d59eaa2696f9d391",
"code": 0,
"msg": "success",
"data": {
"id": "9499ed79995c4bdb95f0d66ca84419fd",
"status": 20,
"progress": 100,
"msg": "success",
"video_url": "https://res.chanjing.cc/xxx/lip-sync/9499ed79995c4bdb95f0d66ca84419fd.mp4",
"preview_url": "https://res.chanjing.cc/xxx/lip-sync/9499ed79995c4bdb95f0d66ca84419fd.jpg",
"duration": 300,
"create_time": 1738636800
}
}Scripts
本 Skill 提供脚本(skills/chanjing-avatar/scripts/),与 chanjing-credentials-guard 使用同一配置文件(~/.chanjing/credentials.json)获取 Token。
| 脚本 | 说明 |
|---|---|
get_upload_url.py | 获取上传链接,输出 sign_url、mime_type、file_id |
upload_file.py | 上传本地文件,轮询 file_detail 直到就绪后输出 file_id |
create_task.py | 创建对口型任务(TTS 或音频驱动),输出视频任务 id |
poll_task.py | 轮询任务直到完成,输出 video_url |
示例(在项目根或 skill 目录下执行):
# 1. 上传驱动视频,得到 video_file_id
VIDEO_FILE_ID=$(python skills/chanjing-avatar/scripts/upload_file.py --service lip_sync_video --file ./my_video.mp4)
# 2. 创建 TTS 对口型任务(需先通过 list_common_audio 获取 audio_man_id)
TASK_ID=$(python skills/chanjing-avatar/scripts/create_task.py \
--video-file-id "$VIDEO_FILE_ID" \
--text "君不见黄河之水天上来" \
--audio-man-id "C-f2429d07554749839849497589199916")
# 3. 轮询直到完成,得到视频下载链接
python skills/chanjing-avatar/scripts/poll_task.py --id "$TASK_ID"音频驱动时:先上传音频得到 audio_file_id,再 create_task.py --video-file-id <id> --audio-file-id <audio_file_id>。
Response Status Code Description
| Code | Description |
|---|---|
| 0 | Response successful |
| 10400 | AccessToken verification failed |
| 40000 | Parameter error |
| 40001 | Exceeds RPM/QPS limit |
| 50000 | System internal error |
# 合规:根目录 合规规则.md §1–§2
name: chanjing-avatar
version: 0.1.0
vendor: chanjing
runtime:
interpreter: python3
dependencies: []
env:
required: []
optional:
- CHANJING_OPENAPI_CREDENTIALS_DIR
- CHANJING_OPENAPI_BASE_URL
permissions:
network_mode: allowlist
allowed_hosts:
- open-api.chanjing.cc
- www.chanjing.cc
filesystem:
read_roots:
- "${WORKSPACE_ROOT}"
- "${SKILL_DIR}"
- "${CHANJING_OPENAPI_CREDENTIALS_DIR}"
write_roots:
- "${WORKSPACE_ROOT}"
- "${CHANJING_OPENAPI_CREDENTIALS_DIR}"
allowed_commands:
- python3
schemaVersion: 1
skill:
id: chanjing-avatar
author: chan-skills
category: 媒体处理
tags:
- Avatar
- 对口型
- 数字人
- ChanjingAPI
- 蝉镜
summary: >-
Chanjing Avatar API:上传音视频、创建对口型任务、轮询与获取结果。
skillDoc: SKILL.md
environment:
variables:
- name: CHANJING_OPENAPI_CREDENTIALS_DIR
required: false
description: 存放 credentials.json 的目录(兼容 CHANJING_CONFIG_DIR),默认 ~/.chanjing
- name: CHANJING_OPENAPI_BASE_URL
required: false
description: Open API 基址(兼容 CHANJING_API_BASE),默认 https://open-api.chanjing.cc
credentials:
model: credentials_json
defaultPath: "~/.chanjing/credentials.json"
directoryEnv: CHANJING_OPENAPI_CREDENTIALS_DIR
fileName: credentials.json
sensitiveFields:
- app_id
- secret_key
- access_token
- expire_in
persistAccessTokenOnDisk: true
primaryEnvIntentionallyOmitted: true
doNotCommitToVcs:
- credentials.json
clientPermissions:
network:
httpsOutbound: true
documentedHosts:
- open-api.chanjing.cc
filesystem:
read:
- "${CHANJING_OPENAPI_CREDENTIALS_DIR or CHANJING_CONFIG_DIR or ~/.chanjing}/credentials.json"
- user_provided_media_paths
write:
- "${CHANJING_OPENAPI_CREDENTIALS_DIR or CHANJING_CONFIG_DIR or ~/.chanjing}/credentials.json"
browser:
mayOpenForAuth: true
documentedHosts:
- www.chanjing.cc
subprocess:
allowedPatterns:
- python3
userContent:
mayDownloadFromApiResponseUrls: true
metadata:
openclaw:
homepage: https://doc.chanjing.cc
agentPolicy:
alwaysSkill: false
modifiesOtherSkillsOrGlobalAgent: false
# 鉴权:与 chanjing-credentials-guard 使用同一配置文件(CONFIG_DIR/credentials.json)
# 无 AK/SK 时执行 open_login_page.py 打开注册/登录页
import json
import os
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
_DEFAULT_OPENAPI_BASE = "https://open-api.chanjing.cc"
def credentials_config_dir() -> Path:
raw = os.environ.get("CHANJING_OPENAPI_CREDENTIALS_DIR") or os.environ.get("CHANJING_CONFIG_DIR")
return Path(raw).expanduser() if raw else Path.home() / ".chanjing"
def openapi_base_url() -> str:
return (
os.environ.get("CHANJING_OPENAPI_BASE_URL")
or os.environ.get("CHANJING_API_BASE")
or _DEFAULT_OPENAPI_BASE
).rstrip("/")
CONFIG_DIR = credentials_config_dir()
CONFIG_FILE = CONFIG_DIR / "credentials.json"
API_BASE = openapi_base_url()
BUFFER_SECONDS = 300
LOGIN_URL = "https://www.chanjing.cc/openapi/login"
NO_CREDENTIALS_MSG = """已在浏览器打开蝉镜登录/注册页。
获取秘钥后请执行:
python skills/chanjing-credentials-guard/scripts/chanjing_config.py --ak <你的app_id> --sk <你的secret_key>
设置完毕后请重新执行您之前的操作。"""
def _run_open_login_page():
"""执行 credentials-guard 的 open_login_page.py,在默认浏览器打开注册/登录页。"""
try:
skills_dir = Path(__file__).resolve().parent.parent.parent
script = skills_dir / "chanjing-credentials-guard" / "scripts" / "open_login_page.py"
if script.exists():
subprocess.run([sys.executable, str(script)], check=False, timeout=5)
else:
import webbrowser
webbrowser.open(LOGIN_URL)
except Exception:
try:
import webbrowser
webbrowser.open(LOGIN_URL)
except Exception:
pass
def read_config():
if CONFIG_FILE.exists():
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def write_config(data):
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def get_token():
"""返回 (token, None) 或 (None, error_msg)。"""
data = read_config()
app_id = (data.get("app_id") or "").strip()
secret_key = (data.get("secret_key") or "").strip()
if not app_id or not secret_key:
_run_open_login_page()
return None, NO_CREDENTIALS_MSG
now = int(time.time())
token = data.get("access_token")
expire_in = data.get("expire_in")
try:
expire_in = int(expire_in) if expire_in is not None else 0
except (ValueError, TypeError):
expire_in = 0
if token and expire_in > now + BUFFER_SECONDS:
return token, None
url = API_BASE + "/open/v1/access_token"
req = urllib.request.Request(
url,
data=json.dumps({"app_id": app_id, "secret_key": secret_key}).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
body = json.loads(resp.read().decode("utf-8"))
except Exception as e:
return None, str(e)
if body.get("code") != 0:
return None, body.get("msg", "获取 Token 失败")
d = body.get("data", {})
new_token = d.get("access_token")
if not new_token:
return None, "API 返回无 access_token"
data["access_token"] = new_token
data["expire_in"] = d.get("expire_in")
write_config(data)
return new_token, None
#!/usr/bin/env python3
"""
创建对口型视频任务。与 chanjing-credentials-guard 使用同一配置文件获取 Token。
用法:
TTS 驱动: create_task --video-file-id <id> --text "台词" --audio-man-id <声音id> [--width 1080] [--height 1920]
音频驱动: create_task --video-file-id <id> --audio-file-id <id> [--width 1080] [--height 1920]
输出: 视频任务 id(一行)或错误到 stderr
"""
import argparse
import json
import sys
import urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _auth import get_token
API_BASE = (__import__("os").environ.get("CHANJING_OPENAPI_BASE_URL") or __import__("os").environ.get("CHANJING_API_BASE") or "https://open-api.chanjing.cc").rstrip("/")
def main():
parser = argparse.ArgumentParser(description="创建对口型视频任务")
parser.add_argument("--video-file-id", required=True, help="视频 file_id(来自 upload_file)")
parser.add_argument("--text", help="TTS 文本(audio_type=tts 时必填)")
parser.add_argument("--audio-man-id", help="声音 ID(TTS 时必填)")
parser.add_argument("--audio-file-id", help="音频 file_id(audio_type=audio 时必填)")
parser.add_argument("--width", type=int, default=1080, help="screen_width,默认 1080")
parser.add_argument("--height", type=int, default=1920, help="screen_height,默认 1920")
parser.add_argument("--callback", help="任务完成回调 URL")
parser.add_argument("--model", type=int, default=0, help="0-基础 1-高清,默认 0")
parser.add_argument("--speed", type=float, default=1, help="TTS 语速,默认 1")
parser.add_argument("--pitch", type=float, default=1, help="TTS 语调,默认 1")
args = parser.parse_args()
if args.audio_file_id:
body = {
"video_file_id": args.video_file_id,
"screen_width": args.width,
"screen_height": args.height,
"model": args.model,
"audio_type": "audio",
"audio_file_id": args.audio_file_id,
}
elif args.text and args.audio_man_id:
body = {
"video_file_id": args.video_file_id,
"screen_width": args.width,
"screen_height": args.height,
"model": args.model,
"audio_type": "tts",
"tts_config": {
"text": [args.text],
"audio_man_id": args.audio_man_id,
"speed": args.speed,
"pitch": args.pitch,
},
}
else:
print("请指定 --text + --audio-man-id(TTS)或 --audio-file-id(音频驱动)", file=sys.stderr)
sys.exit(1)
if args.callback:
body["callback"] = args.callback
token, err = get_token()
if err:
print(err, file=sys.stderr)
sys.exit(1)
url = f"{API_BASE}/open/v1/video_lip_sync/create"
req = urllib.request.Request(
url,
data=json.dumps(body).encode("utf-8"),
headers={"access_token": token, "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
res = json.loads(resp.read().decode("utf-8"))
if res.get("code") != 0:
print(res.get("msg", res), file=sys.stderr)
sys.exit(1)
video_id = res.get("data")
if not video_id:
print("响应无 data", file=sys.stderr)
sys.exit(1)
print(video_id)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
获取上传链接。与 chanjing-credentials-guard 使用同一配置文件获取 Token。
用法: get_upload_url.py --service lip_sync_video --name 1.mp4
输出: JSON { "sign_url", "mime_type", "file_id" } 或错误到 stderr
"""
import argparse
import json
import sys
import urllib.parse
import urllib.request
from pathlib import Path
# 同目录 _auth
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _auth import get_token
API_BASE = (__import__("os").environ.get("CHANJING_OPENAPI_BASE_URL") or __import__("os").environ.get("CHANJING_API_BASE") or "https://open-api.chanjing.cc").rstrip("/")
def main():
parser = argparse.ArgumentParser(description="获取蝉镜文件上传链接")
parser.add_argument("--service", required=True, help="lip_sync_video 或 lip_sync_audio")
parser.add_argument("--name", required=True, help="文件名,如 1.mp4")
args = parser.parse_args()
token, err = get_token()
if err:
print(err, file=sys.stderr)
sys.exit(1)
qs = urllib.parse.urlencode({"service": args.service, "name": args.name})
url = f"{API_BASE}/open/v1/common/create_upload_url?{qs}"
req = urllib.request.Request(url, headers={"access_token": token}, method="GET")
with urllib.request.urlopen(req, timeout=30) as resp:
body = json.loads(resp.read().decode("utf-8"))
if body.get("code") != 0:
print(body.get("msg", body), file=sys.stderr)
sys.exit(1)
data = body.get("data", {})
print(json.dumps(data, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
轮询对口型视频任务状态,直到完成或失败。与 chanjing-credentials-guard 使用同一配置文件获取 Token。
用法: poll_task.py --id <视频任务id> [--interval 10]
输出: 成功时打印 video_url(一行),失败时打印错误到 stderr 并 exit 1
状态: 0-待处理 10-生成中 20-成功 30-失败
"""
import argparse
import json
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _auth import get_token
API_BASE = (__import__("os").environ.get("CHANJING_OPENAPI_BASE_URL") or __import__("os").environ.get("CHANJING_API_BASE") or "https://open-api.chanjing.cc").rstrip("/")
def main():
parser = argparse.ArgumentParser(description="轮询对口型视频任务直到完成")
parser.add_argument("--id", required=True, help="视频任务 ID(来自 create_task)")
parser.add_argument("--interval", type=int, default=10, help="轮询间隔秒数,默认 10")
args = parser.parse_args()
token, err = get_token()
if err:
print(err, file=sys.stderr)
sys.exit(1)
url = f"{API_BASE}/open/v1/video_lip_sync/detail?id={urllib.parse.quote(args.id)}"
while True:
req = urllib.request.Request(url, headers={"access_token": token}, method="GET")
with urllib.request.urlopen(req, timeout=30) as resp:
body = json.loads(resp.read().decode("utf-8"))
if body.get("code") != 0:
print(body.get("msg", body), file=sys.stderr)
sys.exit(1)
data = body.get("data", {})
status = data.get("status")
progress = data.get("progress", 0)
if status == 20:
video_url = data.get("video_url")
if video_url:
print(video_url)
return 0
print("任务成功但无 video_url", file=sys.stderr)
sys.exit(1)
if status == 30:
print(f"任务失败: {data.get('msg', 'unknown')}", file=sys.stderr)
sys.exit(1)
# 0 或 10:继续轮询
time.sleep(args.interval)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
获取上传链接、PUT 本地文件,并轮询文件详情接口直到上传成功,最后输出 file_id。
与 chanjing-credentials-guard 使用同一配置文件获取 Token。
用法: upload_file.py --service lip_sync_video --file /path/to/video.mp4 [--poll-interval 5]
输出: file_id(一行)或错误到 stderr
"""
import argparse
import json
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _auth import get_token
API_BASE = (__import__("os").environ.get("CHANJING_OPENAPI_BASE_URL") or __import__("os").environ.get("CHANJING_API_BASE") or "https://open-api.chanjing.cc").rstrip("/")
# 官方文件管理文档里 1 表示文件可用;保留 2 以兼容历史状态定义。
FILE_READY_STATUSES = {1, 2}
POLL_INTERVAL_DEFAULT = 5
POLL_TIMEOUT_DEFAULT = 300
def get_file_detail(token, file_id):
"""GET /open/v1/common/file_detail?id=file_id, return (data_dict, None) or (None, error_msg)."""
url = f"{API_BASE}/open/v1/common/file_detail?id={urllib.parse.quote(file_id)}"
req = urllib.request.Request(url, headers={"access_token": token}, method="GET")
with urllib.request.urlopen(req, timeout=30) as resp:
body = json.loads(resp.read().decode("utf-8"))
if body.get("code") != 0:
return None, body.get("msg", "file_detail failed")
return body.get("data"), None
def poll_file_ready(token, file_id, interval=5, timeout=300):
"""Poll file_detail until the uploaded file is ready to use."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
data, err = get_file_detail(token, file_id)
if err:
return False, err
if data is None:
return False, "no data"
status = data.get("status")
if status in FILE_READY_STATUSES:
return True, None
if status not in (0, None):
return False, data.get("msg") or f"file status={status}"
time.sleep(interval)
return False, "poll timeout"
def main():
parser = argparse.ArgumentParser(description="上传文件并轮询直到就绪,返回 file_id")
parser.add_argument("--service", required=True, help="lip_sync_video 或 lip_sync_audio")
parser.add_argument("--file", required=True, help="本地文件路径")
parser.add_argument("--poll-interval", type=int, default=POLL_INTERVAL_DEFAULT, help="轮询 file_detail 间隔秒数")
parser.add_argument("--poll-timeout", type=int, default=POLL_TIMEOUT_DEFAULT, help="轮询超时秒数")
args = parser.parse_args()
path = Path(args.file)
if not path.is_file():
print(f"文件不存在: {path}", file=sys.stderr)
sys.exit(1)
token, err = get_token()
if err:
print(err, file=sys.stderr)
sys.exit(1)
qs = urllib.parse.urlencode({"service": args.service, "name": path.name})
url = f"{API_BASE}/open/v1/common/create_upload_url?{qs}"
req = urllib.request.Request(url, headers={"access_token": token}, method="GET")
with urllib.request.urlopen(req, timeout=30) as resp:
body = json.loads(resp.read().decode("utf-8"))
if body.get("code") != 0:
print(body.get("msg", body), file=sys.stderr)
sys.exit(1)
data = body.get("data", {})
sign_url = data.get("sign_url")
mime_type = data.get("mime_type", "application/octet-stream")
file_id = data.get("file_id")
if not sign_url or not file_id:
print("响应缺少 sign_url 或 file_id", file=sys.stderr)
sys.exit(1)
# PUT 上传
with open(path, "rb") as f:
content = f.read()
put_req = urllib.request.Request(
sign_url,
data=content,
headers={"Content-Type": mime_type},
method="PUT",
)
try:
with urllib.request.urlopen(put_req, timeout=120) as put_resp:
if put_resp.status not in (200, 204):
print(f"上传返回状态: {put_resp.status}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"上传失败: {e}", file=sys.stderr)
sys.exit(1)
# Poll file_detail until file is ready (status success)
ready, err = poll_file_ready(token, file_id, interval=args.poll_interval, timeout=args.poll_timeout)
if not ready:
print(f"文件未就绪: {err}", file=sys.stderr)
sys.exit(1)
print(file_id)
if __name__ == "__main__":
main()