
Alibabacloud Avatar Video
- 124 installs
- 208 repo stars
- Updated August 4, 2026
- aliyun/alibabacloud-aiops-skills
Alibaba Cloud avatar-video is a Claude Code skill that generates AI video, images, and speech via the DashScope API and LingMou.
About
Alibaba Cloud avatar-video is a skill that generates AI video and speech through the DashScope API and LingMou. A developer uses it to make talking-head videos, full-body animation, text-to-image, image-to-video, and text-to-speech from images, audio, or plain text. It exposes seven capabilities across dedicated scripts and requires DashScope and OSS credentials plus ffmpeg.
- Seven capabilities: LivePortrait, EMO, AnimateAnyone, T2I, I2V, Qwen TTS, LingMou
- Text-to-video end-to-end via a T2I to I2V pipeline
- Scene-based auto voice and model selection for Qwen TTS
Alibabacloud Avatar Video by the numbers
- 124 all-time installs (skills.sh)
- Ranked #765 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
alibabacloud-avatar-video capabilities & compatibility
Requires a DashScope API key and Alibaba Cloud OSS credentials; DashScope and OSS usage billed by Alibaba Cloud.
- Capabilities
- video generation · image generation
- Use cases
- video generation · image generation
- Pricing
- Bring your own API key
What alibabacloud-avatar-video says it does
Use Alibaba Cloud DashScope API and LingMou to generate AI video and speech.
Trigger when the user needs talking-head, portrait, full-body animation, text-to-image, text-to-video, or speech synthesis.
npx skills add https://github.com/aliyun/alibabacloud-aiops-skills --skill alibabacloud-avatar-videoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 124 |
|---|---|
| repo stars | ★ 208 |
| Last updated | August 4, 2026 |
| Repository | aliyun/alibabacloud-aiops-skills ↗ |
What it does
Generate AI talking-head video, full-body animation, text-to-image, image-to-video, and speech via Alibaba Cloud DashScope.
Who is it for?
Producing talking-head or full-body avatar videos, generated images, and synthesized speech for content and media.
Skip if: Non-Alibaba-Cloud video pipelines; it depends on DashScope, OSS, and LingMou.
When should I use this skill?
The user needs talking-head, portrait, full-body animation, text-to-image, text-to-video, or speech synthesis.
What you get
Talking-head or full-body videos, generated images, and synthesized speech from image, audio, or text inputs.
- talking-head video
- full-body animation video
- generated image
By the numbers
- 7 generation capabilities
- 8 Qwen TTS voices
- audio input 1s-3min, < 15MB for LivePortrait
Files
Human Avatar — Alibaba Cloud AI Video & Speech
Capabilities overview
| Capability | Script | Model / API | Region | Summary |
|---|---|---|---|---|
| LivePortrait | live_portrait.py | liveportrait | cn-beijing | Portrait + audio/video → talking video, two steps |
| EMO | portrait_animate.py | emo-v1 | cn-beijing | Portrait + audio → talking head, detect + generate |
| AA (AnimateAnyone) | animate_anyone.py | animate-anyone-gen2 | cn-beijing | Full-body animation: detect → motion template → video |
| T2I | text_to_image.py | wan2.x-t2i | Multi-region | Text → image, default wan2.2-t2i-flash |
| I2V | image_to_video.py | wan2.x-i2v | Multi-region | Image → video; T2I→I2V pipeline supported; default wan2.7-i2v-flash |
| Qwen TTS | qwen_tts.py | qwen3-tts-* | cn-beijing / Singapore | Text → speech; auto model/voice by scene |
| LingMou | avatar_video.py | LingMou SDK | cn-beijing | Template-based digital-human broadcast video |
---
Quick selection guide
Talking head (have audio/video already) → LivePortrait
Talking head (no audio; synthesize first) → Qwen TTS → LivePortrait
Full-body dance / motion → AA (AnimateAnyone)
Text → image → T2I (text_to_image)
Image → video → I2V (image_to_video)
Text → video end-to-end → T2I → I2V (image_to_video --t2i-prompt)
Enterprise digital human / template news → LingMou (avatar_video)---
Environment setup
pip install requests==2.33.1 dashscope==1.25.15 oss2==2.19.1 numpy==1.26.4
# LingMou additionally:
pip install alibabacloud-lingmou20250527==1.7.0 alibabacloud-tea-openapi==0.4.4export DASHSCOPE_API_KEY=sk-xxxx # Beijing-region API key
export ALIBABA_CLOUD_ACCESS_KEY_ID=xxx # OSS upload
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=xxx
export OSS_BUCKET=your-bucket
export OSS_ENDPOINT=oss-cn-beijing.aliyuncs.com⚠️ API keys for cn-beijing and Singapore are not interchangeable; use the key for the correct region.OSS_ENDPOINTmay include or omit thehttps://prefix; scripts normalize it.
---
1. LivePortrait — talking-head video
When to use: You have a portrait photo + speech and want a talking-head video quickly.
Flow:
Step 1: liveportrait-detect (sync) → pass=true
↓
Step 2: liveportrait (async) → video_urlImage: Single person, front-facing portrait, clear face, no occlusion Audio: wav/mp3, < 15MB, 1s–3min Video input: Audio extracted automatically (ffmpeg)
# Image + audio file
python scripts/live_portrait.py \
--image ./portrait.jpg \
--audio ./speech.mp3 \
--template normal --download
# Image + video (extract audio)
python scripts/live_portrait.py \
--image ./portrait.jpg \
--video ./speech_video.mp4 \
--template active --download
# Public URLs
python scripts/live_portrait.py \
--image-url "https://..." \
--audio-url "https://..." \
--mouth-strength 1.2 --downloadMotion templates:
normal(default, moderate motion)calm(calm; news / storytelling)active(lively; singing / hosting)
---
2. Qwen TTS — text to speech
When to use: Generate speech files from text (for LivePortrait, EMO, etc.).
Default model: qwen3-tts-vd-realtime-2026-01-15
Auto model selection by scene
Scene --scene | Suggested model | Suggested voice |
|---|---|---|
default / brand | qwen3-tts-vd-realtime-2026-01-15 | Cherry |
news / documentary / advertising | qwen3-tts-instruct-flash-realtime | Serena / Ethan |
audiobook / drama | qwen3-tts-instruct-flash-realtime | Cherry / Dylan |
customer_service / chatbot / education | qwen3-tts-flash-realtime | Anna / Ethan |
ecommerce / short_video | qwen3-tts-flash-realtime | Cherry / Chelsie |
Available voices
| Voice | Character |
|---|---|
Cherry | Bright, sweet female; ads / audiobooks / dubbing |
Serena | Mature, intellectual female; news / explainers / corporate |
Ethan | Steady, warm male; education / documentary / training |
Dylan | Expressive male; radio drama / game VO |
Anna | Gentle, friendly female; support / assistant / daily |
Chelsie | Young, fresh female; short video / e-commerce |
Thomas | Deep, magnetic male; brand / ads |
Luna | Warm, soft female; meditation / storytelling |
# Default (qwen3-tts-vd-realtime + Cherry)
python scripts/qwen_tts.py --text "Hello, welcome to Qwen TTS." --download
# Match by scene
python scripts/qwen_tts.py --text "Today's market..." --scene news --download
python scripts/qwen_tts.py --text "Once upon a time..." --scene audiobook --download
# Style via instructions
python scripts/qwen_tts.py \
--text "Dear students..." \
--model qwen3-tts-instruct-flash-realtime \
--instructions "Warm tone, steady pace, suitable for teaching" \
--download
# List options
python scripts/qwen_tts.py --list-voices
python scripts/qwen_tts.py --list-models---
3. T2I — Wan 2.x text-to-image
When to use: Generate images from text (optionally feed into I2V).
# Default model (wan2.2-t2i-flash, fast)
python scripts/text_to_image.py \
--prompt "A woman in Hanfu in a peach blossom forest, cinematic, 4K, soft light" \
--size 960*1696 --download
# Higher quality
python scripts/text_to_image.py \
--prompt "..." --model wan2.2-t2i-plus --size 1280*1280 --download
# Latest (Wan 2.6)
python scripts/text_to_image.py \
--prompt "..." --model wan2.6-t2i --size 1280*1280 --n 1 --downloadModels:
wan2.2-t2i-flash(default, fast, good for tests)wan2.2-t2i-plus(higher quality)wan2.6-t2i(latest; more aspect ratios; sync call)
Common sizes: 1280*1280 (1:1) / 960*1696 (9:16) / 1696*960 (16:9)
---
4. I2V — Wan 2.x image-to-video
When to use: Turn an image into motion video; supports text-to-video via T2I first.
# Local image → video
python scripts/image_to_video.py \
--image ./portrait.jpg \
--prompt "She turns slowly and smiles; dress and petals drift gently" \
--model wan2.7-i2v \
--resolution 720P --duration 5 --download
# Pipeline: text → image → video
python scripts/image_to_video.py \
--t2i-prompt "A woman in Hanfu in a peach blossom forest" \
--prompt "She turns slowly; petals fall; poetic mood" \
--download --output result.mp4
# With background music
python scripts/image_to_video.py \
--image ./portrait.jpg \
--audio-url "https://..." \
--prompt "..." --downloadModels:
wan2.7-i2v(default; includes sound; 5s/10s)wan2.5-i2v-preview(high-quality preview)wan2.2-i2v-plus(no built-in audio; faster)
---
5. AA AnimateAnyone — full-body animation
When to use: Full-body photo + reference motion video → dance / motion video.
Requirements:
- Image: Single person, full body front, head to toe, aspect ratio 0.5–2.0
- Video: Full body in frame from first frame; mp4/avi/mov; fps ≥ 24; 2–60s
Three steps:
Step 1: animate-anyone-detect-gen2 (sync) → check_pass=true
↓
Step 2: animate-anyone-template-gen2 (async) → template_id (~3–5 min)
↓
Step 3: animate-anyone-gen2 (async) → video_url (~3–5 min)# Local files (auto convert + OSS upload)
python scripts/animate_anyone.py \
--image ./portrait_fullbody.jpg \
--video ./dance.mp4 \
--download --output result.mp4
# Use image as background
python scripts/animate_anyone.py \
--image ./portrait.jpg --video ./dance.mp4 \
--use-ref-img-bg --video-ratio 9:16 --download
# Skip Step 2 (existing template_id)
python scripts/animate_anyone.py \
--image ./portrait.jpg \
--template-id "AACT.xxx.xxx" --downloadAuto conversion: video webm/mkv/flv → mp4; image webp/heic → jpg; if fps is under 24, normalize to 24 fps
---
6. EMO — talking head (legacy)
Note: Prefer LivePortrait; EMO suits cases that need stricter lip-sync.
python scripts/portrait_animate.py \
--image ./portrait.jpg \
--audio ./speech.mp3 \
--download---
7. LingMou — enterprise template video
When to use: Corporate digital-human news, template-based broadcasts, scripted reads with optional character images.
New workflow (prefer no template_id)
- If the user provides `template_id`: use that template to generate.
- If no `template_id`:
1. List existing broadcast templates for the account. 2. If any exist, pick one at random for creation. 3. If none, fetch public templates and copy up to 3 into the account. 4. Pick one at random from the copy results and continue.
- Caveat: After a public template is copied, the copy may not yet be a fully “ready-to-render” template; some copies are still drafts and may lack clips, assets, or variable bindings—complete them in LingMou.
- If the user only gives an image and “make a talking video” without a script: confirm the spoken copy before generating.
What scripts/avatar_video.py supports
--list-templates: list account templates--list-public-templates: list public templates (SDK 1.7.0+)--copy-public-templates: copy up to 3 public templates (SDK 1.7.0+)- Omit
--template-id: random existing template - When local templates are empty: auto try public-template copy as fallback
--show-template-detail: template detail and replaceable variables- Fills input text into template text variables (prefers
text_content/test_text) - If generation fails right after copying a public template, surfaces a clear error that the template may still need completion (no silent failure)
# List templates
python scripts/avatar_video.py --list-templates
# Public templates (SDK 1.7.0+)
python scripts/avatar_video.py --list-public-templates
# Copy up to 3 public templates (SDK 1.7.0+)
python scripts/avatar_video.py --copy-public-templates
# No template_id — random existing template
python scripts/avatar_video.py \
--text "Hello, welcome to today's tech news." \
--download
# Specific template_id
python scripts/avatar_video.py \
--template-id "BS1b2WNnRMu4ouRzT4clY9Jhg" \
--text "Hello, welcome to today's tech news." \
--download
# Detail for randomly chosen template
python scripts/avatar_video.py \
--show-template-detail \
--text "This is a test script for broadcast."Conversational usage
When the user says things like:
- “Make a talking video from this image”
- “Digital-human broadcast for me”
- “Upload image and make a news read”
Do this: 1. Check whether they already gave copy/script ready to read. 2. If not, ask: “What is the exact script to read? You can give bullet points and I can turn them into broadcast-ready copy.” 3. With script in hand, run LingMou: prefer random existing template; if none locally, try public copy. 4. If they uploaded a portrait but the template API does not use it, explain: this path is template-driven; for image-driven talking head, use LivePortrait or EMO.
---
API reference links
- LivePortrait: https://help.aliyun.com/zh/model-studio/liveportrait-api
- EMO (emo-detect + emo-v1): references/emo-api.md
- AA (Animate Anyone): references/aa-api.md
- T2I (text-to-image v2): https://help.aliyun.com/zh/model-studio/text-to-image-v2-api-reference
- I2V (image-to-video): https://help.aliyun.com/zh/model-studio/image-to-video-api-reference/
- Qwen TTS: https://help.aliyun.com/zh/model-studio/qwen-tts-realtime
- LingMou: references/lingmou-api.md
- OSS upload: references/oss-upload.md
AA (AnimateAnyone Gen2) API reference
Official docs:
- Image detection: https://help.aliyun.com/zh/model-studio/animate-anyone-detect-api
- Motion template: https://help.aliyun.com/zh/model-studio/animate-anyone-template-api
- Video generation: https://help.aliyun.com/zh/model-studio/animateanyone-video-generation-api
Important
- Region is Beijing:
dashscope.aliyuncs.com; API key must be for Beijing. - Three steps total: Step 1 sync; Steps 2/3 async (poll
task_id).
---
Three-step pipeline
Step 1: aa-detect (sync) → check_pass=true or error
Step 2: aa-template (async) → template_id
Step 3: aa-generate (async) → video_url---
Step 1: Image detection (sync)
POST https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/aa-detect| Field | Description |
|---|---|
model | animate-anyone-detect-gen2 |
input.image_url | Public HTTP/HTTPS URL; jpg/jpeg/png/bmp; under 5MB; max edge ≤4096 |
Response:
{
"output": {
"check_pass": true,
"bodystyle": "full"
}
}(bodystyle: "full" = full body, "half" = half body.)
Image requirements (to pass):
- Single person, front or near-front, no strong profile
- Clear face, no occlusion
- Full body or at least waist-up visible
- Simple background preferred
---
Step 2: Motion template (async)
POST https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/aa-template-generation/
Header: X-DashScope-Async: enable| Field | Description |
|---|---|
model | animate-anyone-template-gen2 |
input.video_url | Public URL; mp4/avi/mov; H.264/H.265; fps≥24; 2–60s; ≤200MB |
Video requirements:
- Full body in frame, single continuous shot, no hard cuts
- First frame facing camera
- Subject visible from first frame
Poll GET /api/v1/tasks/{task_id}:
{
"output": {
"task_status": "SUCCEEDED",
"template_id": "AACT.xxx.xxx-xxx.xxx"
}
}---
Step 3: Video generation (async)
POST https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis/
Header: X-DashScope-Async: enable| Field | Description |
|---|---|
model | animate-anyone-gen2 |
input.image_url | Image URL that passed Step 1 |
input.template_id | template_id from Step 2 |
parameters.use_ref_img_bg | false (default, video background) / true (image background) |
parameters.video_ratio | "9:16" or "3:4" (only when use_ref_img_bg=true) |
Poll response:
{
"output": {
"task_status": "SUCCEEDED",
"video_url": "https://xxx/output.mp4"
}
}⚠️ video_url is valid for 24 hours after success—download promptly.---
Format conversion (ffmpeg)
| Input | Unsupported | Target | Command |
|---|---|---|---|
| Image | webp, heic, tif, bmp | jpg | ffmpeg -i input.webp -q:v 2 output.jpg |
| Video | webm, mkv, flv, wmv | mp4 (H.264) | ffmpeg -i input.webm -c:v libx264 -crf 22 -c:a aac output.mp4 |
| Video fps under 24 | — | 24 fps | ffmpeg -i input.mp4 -vf fps=24 -c:v libx264 output.mp4 |
animate_anyone.py performs this automatically.
---
Full examples
# Local files (convert + OSS)
python scripts/animate_anyone.py \
--image ./portrait.jpg \
--video ./dance.webm \
--download --output result.mp4
# Existing URLs
python scripts/animate_anyone.py \
--image-url "https://oss.../portrait.jpg?..." \
--video-url "https://oss.../dance.mp4?..." \
--download
# Existing template_id (skip Step 2)
python scripts/animate_anyone.py \
--image ./portrait.jpg \
--template-id "AACT.xxx.xxx" \
--download
# Image as background
python scripts/animate_anyone.py \
--image ./portrait.jpg --video ./dance.mp4 \
--use-ref-img-bg --video-ratio 9:16 --downloadEMO API (DashScope)
Official docs:
- EMO detect:
https://help.aliyun.com/zh/model-studio/emo-detect-api - EMO generate:
https://help.aliyun.com/zh/model-studio/emo-api
Auth and region
- Auth:
Authorization: Bearer $DASHSCOPE_API_KEY - Region: Beijing (
dashscope.aliyuncs.com) - Set header:
X-DashScope-Async: enable
1) Portrait detection (required first)
POST https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/face-detect
Body:
{
"model": "emo-detect-v1",
"input": {"image_url": "https://.../portrait.png"},
"parameters": {"ratio": "1:1"}
}Key success fields:
output.check_passoutput.face_bboxoutput.ext_bbox
2) Submit video generation
POST https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis
Body:
{
"model": "emo-v1",
"input": {
"image_url": "https://.../portrait.png",
"audio_url": "https://.../speech.mp3",
"face_bbox": [302,286,610,593],
"ext_bbox": [71,9,840,778]
},
"parameters": {"style_level": "normal"}
}Returns: output.task_id
3) Poll task
GET https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}
States: PENDING → RUNNING → SUCCEEDED / FAILED
On success:
output.results.video_url
Limits
- Image: min edge ≥ 400, max edge ≤ 7000
- Audio: wav/mp3, ≤ 15MB, ≤ 60 seconds
- URLs must be public http/https
LingMou API
Official docs:
- Create broadcast video from template:
https://help.aliyun.com/zh/avatar/avatar-application/developer-reference/api-lingmou-2025-05-27-createbroadcastvideofromtemplate
- Batch query broadcast videos:
https://help.aliyun.com/zh/avatar/avatar-application/developer-reference/api-lingmou-2025-05-27-listbroadcastvideosbyid
- Get broadcast template:
https://api.aliyun.com/api/LingMou/2025-05-27/GetBroadcastTemplate
- List broadcast templates:
https://api.aliyun.com/api/LingMou/2025-05-27/ListBroadcastTemplates
Auth and region
- Auth: Alibaba Cloud AK/SK (OpenAPI signature)
- Region:
cn-beijing - Endpoint:
lingmou.cn-beijing.aliyuncs.com - API version:
2025-05-27
Verified flow (SDK 1.6.0 in test env)
1. Call ListBroadcastTemplates for existing templates. 2. If user did not specify templateId, pick one at random. 3. Call GetBroadcastTemplate for details and variables. 4. Choose a replaceable text variable (prefer text_content). 5. Call CreateBroadcastVideoFromTemplate. 6. Poll ListBroadcastVideosById with returned id. 7. When status=SUCCESS, read videoURL.
New capabilities (verified with SDK 1.7.0 in venv)
Goals:
1. List public broadcast templates 2. Copy a public template into your account 3. Create video from the copied template
Intended workflow:
list broadcast templatefirst- If templates exist: pick one at random
- If none: list public templates, copy up to 3, then pick one at random to create video
Observed behavior:
alibabacloud-lingmou20250527==1.7.0adds:list_public_broadcast_scene_templatescopy_broadcast_scene_from_template- Both were called successfully in tests
- Creating video immediately after copy is not guaranteed; errors such as
100010031001-400(“no valid segments”) can occur - The new
BS...id may list variables but still lack a complete renderable scene - Production strategy:
- Prefer random selection among known-good account templates
- Only when local templates are empty, use public copy as fallback
- If generation still fails, tell the user the template may need completion in LingMou
Python SDK fields
Version differences
- System Python had
1.6.0without public-template APIs - For public-template tests, use your venv’s Python, e.g.:
.venv-human-avatar/bin/python scripts/avatar_video.py --list-public-templatesListBroadcastTemplates
Response includes:
data[].iddata[].namedata[].variables(often empty or minimal in list response)
Example (test account):
[
{"id": "BS1vs5wAhH7OvW7btG1M6VxEQ", "name": "boy-01"},
{"id": "BS1V_mn-IwR6uZTgxuiKoWdPw", "name": "girl-01"},
{"id": "BS1JqkX1Dm4VGjseLKkPkpmiw", "name": "boy-02"},
{"id": "BS1bR7OvVfFY2OkNEy591084A", "name": "girl-02"}
]GetBroadcastTemplate
Python SDK uses template_id, not id:
req = lm.GetBroadcastTemplateRequest()
req.template_id = "BS1vs5wAhH7OvW7btG1M6VxEQ"
resp = client.get_broadcast_template(req)Example response:
{
"id": "BS1vs5wAhH7OvW7btG1M6VxEQ",
"name": "boy-01",
"variables": [
{
"name": "text_content",
"type": "text"
}
]
}CreateBroadcastVideoFromTemplate (key payload)
{
"templateId": "BS1b2WNnRMu4ouRzT4clY9Jhg",
"name": "Broadcast video test",
"variables": [
{
"name": "text_content",
"type": "text",
"properties": {"content": "Script to read aloud"}
}
],
"videoOptions": {
"resolution": "720p",
"fps": 30,
"watermark": true
}
}ListBroadcastVideosById (key fields)
data[].status:SUCCESS/ERROR/PROCESSING/ …data[].videoURLdata[].captionURL
Variable types
text— textimage— image assetavatar— digital-human assetvoice— voice asset
Integration notes
- Do not require user-supplied
template_idin chat - If omitted, list templates and pick at random
- If no script was given, confirm script before generating
- If the user insists on “must use my uploaded photo for talking head”, steer to
LivePortraitorEMO; template broadcast is a different path from image-driven lip-sync
OSS upload (for EMO / AA)
Local files must be uploaded to OSS first; pass the public URL to DashScope.
Environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID=xxx
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=xxx
export OSS_BUCKET=your-bucket
export OSS_ENDPOINT=oss-cn-beijing.aliyuncs.comPython examples
Public bucket (direct public URL)
import os
import oss2
def upload_to_oss(local_path: str, oss_key: str) -> str:
auth = oss2.Auth(
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
)
bucket_name = os.environ["OSS_BUCKET"]
endpoint = os.environ.get("OSS_ENDPOINT", "oss-cn-beijing.aliyuncs.com")
bucket = oss2.Bucket(auth, f"https://{endpoint}", bucket_name)
bucket.put_object_from_file(oss_key, local_path)
return f"https://{bucket_name}.{endpoint}/{oss_key}"Private bucket (signed URL, default 3-day expiry)
import os
import oss2
DEFAULT_EXPIRES = 3 * 24 * 3600 # 3 days, seconds
def upload_to_oss(local_path: str, oss_key: str, expires: int = DEFAULT_EXPIRES) -> str:
"""
Upload a file to a private OSS bucket and return a signed temporary URL.
Args:
local_path: Local file path
oss_key: OSS object key (e.g. "avatars/face.jpg")
expires: Signed URL lifetime in seconds (default 3 days)
Returns:
Signed publicly reachable URL
"""
auth = oss2.Auth(
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
)
bucket_name = os.environ["OSS_BUCKET"]
endpoint = os.environ.get("OSS_ENDPOINT", "oss-cn-beijing.aliyuncs.com")
bucket = oss2.Bucket(auth, f"https://{endpoint}", bucket_name)
bucket.put_object_from_file(oss_key, local_path)
signed_url = bucket.sign_url("GET", oss_key, expires)
return signed_urlUsage
url = upload_to_oss("./face.jpg", "avatars/face.jpg")
# → https://your-bucket.oss-cn-beijing.aliyuncs.com/avatars/face.jpg?OSSAccessKeyId=...&Expires=...&Signature=...
# Custom expiry (7 days)
url = upload_to_oss("./speech.mp3", "audio/speech.mp3", expires=7 * 24 * 3600)Notes
- The URL must be reachable on the public internet (http/https); DashScope must download it directly.
- Signed URL lifetime should cover task duration (EMO jobs often 2–10 minutes; 3 days is plenty).
- For private buckets use a signed URL; do not pass intranet URLs or
oss://to DashScope. - Consider OSS lifecycle rules to purge temporary assets (e.g. delete after 7 days).
bucket.sign_url()returns astrusable asimage_url/audio_url.
RAM permission policies
This skill needs the following RAM permissions for Alibaba Cloud services. Prefer a least-privilege RAM role or user for the application.
Permission overview
| Service | Permission | Purpose | Scripts |
|---|---|---|---|
| DashScope API | AliyunDashScopeFullAccess or custom policy | Call AI APIs (video, TTS, image) | All scripts |
| OSS | AliyunOSSFullAccess or bucket-scoped | Upload media and issue signed URLs | live_portrait.py, animate_anyone.py, image_to_video.py, portrait_animate.py |
| LingMou | AliyunLingMouFullAccess or custom policy | Digital-human template video | avatar_video.py |
Details
1. DashScope API
Service: DashScope (Model Studio) Regions: cn-beijing (Beijing) / ap-southeast-1 (Singapore) Example policy:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dashscope:CallModel",
"dashscope:GetTask",
"dashscope:ListTasks"
],
"Resource": "*"
}
]
}Model APIs used:
liveportrait— LivePortrait talking headliveportrait-detect— portrait detectionemo-v1— EMO talking heademo-detect-v1— EMO detectionanimate-anyone-gen2— AnimateAnyone videoanimate-anyone-detect-gen2— AA image detectionanimate-anyone-template-gen2— AA motion templatewan2.x-t2i— Wan text-to-imagewan2.x-i2v— Wan image-to-videoqwen3-tts-*— Qwen real-time TTS
Env: DASHSCOPE_API_KEY
2. OSS
Service: Object Storage Service Region: cn-beijing or others Example policy:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:PutObject",
"oss:GetObject",
"oss:ListObjects"
],
"Resource": [
"acs:oss:*:*:your-bucket-name/human-avatar/*"
]
}
]
}Usage:
- Upload local images, audio, video to OSS
- Generate signed URLs for DashScope
- Example prefix:
human-avatar/
Env:
ALIBABA_CLOUD_ACCESS_KEY_IDALIBABA_CLOUD_ACCESS_KEY_SECRETOSS_BUCKETOSS_ENDPOINT(e.g.oss-cn-beijing.aliyuncs.com)
3. LingMou
Service: LingMou digital human Region: cn-beijing Example policy:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lingmou:ListBroadcastTemplates",
"lingmou:GetBroadcastTemplate",
"lingmou:CreateBroadcastVideoFromTemplate",
"lingmou:ListBroadcastVideosById",
"lingmou:ListPublicBroadcastSceneTemplates",
"lingmou:CopyBroadcastSceneFromTemplate"
],
"Resource": "*"
}
]
}Usage:
- List and inspect broadcast templates
- Create digital-human videos from templates
- Poll video job status
- Copy public templates
Env:
ALIBABA_CLOUD_ACCESS_KEY_IDALIBABA_CLOUD_ACCESS_KEY_SECRETLINGMOU_ENDPOINT— optional, defaultlingmou.cn-beijing.aliyuncs.comLINGMOU_REGION— optional, defaultcn-beijing
Minimal combined policy example
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dashscope:CallModel",
"dashscope:GetTask",
"dashscope:ListTasks"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"oss:PutObject",
"oss:GetObject"
],
"Resource": "acs:oss:*:*:your-bucket-name/human-avatar/*"
},
{
"Effect": "Allow",
"Action": [
"lingmou:ListBroadcastTemplates",
"lingmou:GetBroadcastTemplate",
"lingmou:CreateBroadcastVideoFromTemplate",
"lingmou:ListBroadcastVideosById",
"lingmou:ListPublicBroadcastSceneTemplates",
"lingmou:CopyBroadcastSceneFromTemplate"
],
"Resource": "*"
}
]
}Security:
- Replace your-bucket-name with your real bucket name- Rotate AccessKeys regularly
- Prefer RAM roles over root account keys
- Grant only actions you need
Checklist before deploy
- [ ] DashScope API key created and set in env
- [ ] OSS bucket exists and the key can write
- [ ] (Optional) LingMou enabled and permissions set
- [ ] All required env vars set
- [ ] Using RAM user/role, not root account
Related docs
#!/usr/bin/env python3
"""
AnimateAnyone Gen2 — 三步流水线
SECURITY NOTES:
- subprocess: used ONLY to invoke system ffmpeg for video/image format
conversion (e.g. webm→mp4, heic→jpg). No shell=True, no eval/exec.
- OSS credentials: read from environment variables, used ONLY to upload
user media to their own OSS bucket. Never transmitted to third parties.
- All API calls target dashscope.aliyuncs.com (Alibaba Cloud official).
Step 1: animate-anyone-detect-gen2 图像检测(同步)
Step 2: animate-anyone-template-gen2 动作模板生成(异步,得到 template_id)
Step 3: animate-anyone-gen2 视频生成(异步,得到 video_url)
支持多种输入格式,通过 ffmpeg 自动转换:
图片: webp/heic/tif/bmp → jpg
视频: webm/avi/mov/mkv/flv → mp4 (H.264, ≥24fps)
用法:
python animate_anyone.py --image ./face.jpg --video ./dance.webm --download
python animate_anyone.py --image-url https://... --video-url https://... --download
python animate_anyone.py --image ./face.jpg --template-id AACT.xxx --download # 跳过模板生成
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
import requests
from input_validation import (
mk_temp_path_for_ffmpeg,
resolve_under_cwd,
validate_http_https_url,
)
BASE_URL = os.getenv("DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com")
_OSS_SIGNED_URL_EXPIRES = int(os.environ.get("OSS_SIGNED_URL_EXPIRES", str(3 * 24 * 3600)))
# ── ffmpeg helpers ─────────────────────────────────────────────────────────────
def _find_ffmpeg() -> str:
"""Find ffmpeg in PATH or common install locations."""
for p in ["ffmpeg", "/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg"]:
if shutil.which(p):
return p
raise RuntimeError("ffmpeg not found. Install: apt install ffmpeg / brew install ffmpeg")
def convert_image(src: str) -> str:
"""
Convert image to jpg if not already jpg/jpeg/png.
Returns path to (possibly new) file. Temp files are tracked for cleanup.
"""
p = Path(src)
if p.suffix.lower() in (".jpg", ".jpeg", ".png"):
return src
ff = _find_ffmpeg()
dst = mk_temp_path_for_ffmpeg(".jpg", "aa_img_")
subprocess.run([ff, "-y", "-i", src, "-q:v", "2", dst],
check=True, capture_output=True)
print(f"[convert] {p.name} → {Path(dst).name}")
return dst
def convert_video(src: str) -> str:
"""
Convert video to mp4 (H.264) with ≥24fps if not already compatible.
Returns path to (possibly new) file.
Requirements: mp4/avi/mov, H.264 or H.265, fps≥24, bitrate reasonable.
"""
p = Path(src)
# probe fps
probe = subprocess.run(
["ffprobe", "-v", "quiet", "-select_streams", "v:0",
"-show_entries", "stream=codec_name,r_frame_rate,width,height",
"-of", "json", src],
capture_output=True, text=True
)
codec, fps_num, fps_den = "unknown", 25, 1
try:
info = json.loads(probe.stdout)
stream = info.get("streams", [{}])[0]
codec = stream.get("codec_name", "unknown")
fr = stream.get("r_frame_rate", "25/1").split("/")
fps_num, fps_den = int(fr[0]), max(int(fr[1]), 1)
except Exception:
pass
fps = fps_num / fps_den
need_convert = (
p.suffix.lower() not in (".mp4", ".avi", ".mov")
or codec not in ("h264", "hevc", "h265")
or fps < 24
)
if not need_convert:
return src
ff = _find_ffmpeg()
dst = mk_temp_path_for_ffmpeg(".mp4", "aa_vid_")
# ensure ≥24fps, H.264
vf = f"fps=max(fps\\,24)" if fps < 24 else None
cmd = [ff, "-y", "-i", src, "-c:v", "libx264", "-preset", "fast",
"-crf", "22", "-c:a", "aac", "-movflags", "+faststart"]
if vf:
cmd += ["-vf", f"fps=24"]
cmd.append(dst)
subprocess.run(cmd, check=True, capture_output=True)
print(f"[convert] {p.name} → {Path(dst).name} codec={codec} fps={fps:.1f}→converted")
return dst
# ── OSS upload ─────────────────────────────────────────────────────────────────
def upload_to_oss(local_path: str, expires: int = _OSS_SIGNED_URL_EXPIRES) -> str:
"""Upload file to OSS and return signed GET URL (default 3 days)."""
import oss2
auth = oss2.Auth(
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
)
bucket_name = os.environ["OSS_BUCKET"]
endpoint = os.environ.get("OSS_ENDPOINT", "oss-cn-beijing.aliyuncs.com")
endpoint = endpoint.replace("https://", "").replace("http://", "").rstrip("/")
bucket = oss2.Bucket(auth, f"https://{endpoint}", bucket_name)
key = f"human-avatar/{Path(local_path).name}"
print(f"[oss] uploading {Path(local_path).name} …")
bucket.put_object_from_file(key, local_path)
url = bucket.sign_url("GET", key, expires)
print(f"[oss] signed_url ok ({expires//3600}h)")
return url
# ── DashScope helpers ──────────────────────────────────────────────────────────
USER_AGENT = "AlibabaCloud-Agent-Skills/alibabacloud-avatar-video"
def _headers(async_mode: bool = False) -> dict:
key = os.environ.get("DASHSCOPE_API_KEY")
if not key:
raise RuntimeError("DASHSCOPE_API_KEY not set")
h = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
}
if async_mode:
h["X-DashScope-Async"] = "enable"
return h
def wait_task(task_id: str, interval: int = 10, max_wait: int = 1800) -> dict:
"""Poll until task SUCCEEDED/FAILED, return full output dict."""
url = f"{BASE_URL}/api/v1/tasks/{task_id}"
start = time.time()
while time.time() - start < max_wait:
r = requests.get(url, headers=_headers(), timeout=60)
r.raise_for_status()
data = r.json()
out = data.get("output", {})
status = out.get("task_status", "UNKNOWN")
elapsed = int(time.time() - start)
print(f" [{elapsed}s] task_id={task_id[:16]}… status={status}")
if status == "SUCCEEDED":
return out
if status in ("FAILED", "CANCELED", "UNKNOWN"):
raise RuntimeError(f"Task failed: {json.dumps(data, ensure_ascii=False)}")
time.sleep(interval)
raise TimeoutError(f"Task {task_id} timed out after {max_wait}s")
# ── Step 1: Image detect ───────────────────────────────────────────────────────
def aa_detect(image_url: str) -> dict:
"""
POST /aa-detect — 检测图像是否符合 AA 要求(同步)。
Returns output dict with check_pass, bodystyle.
"""
print(f"\n[step1] aa-detect …")
r = requests.post(
f"{BASE_URL}/api/v1/services/aigc/image2video/aa-detect",
headers=_headers(async_mode=False),
json={"model": "animate-anyone-detect-gen2", "input": {"image_url": image_url}},
timeout=60,
)
r.raise_for_status()
data = r.json()
out = data.get("output", {})
check_pass = out.get("check_pass", False)
bodystyle = out.get("bodystyle", "")
reason = out.get("reason", "")
if check_pass:
print(f" ✅ detect passed bodystyle={bodystyle}")
else:
print(f" ❌ detect FAILED reason={reason}")
raise ValueError(f"Image failed AA detect: {reason}")
return out
# ── Step 2: Template generation ────────────────────────────────────────────────
def aa_template(video_url: str) -> str:
"""
POST /aa-template-generation/ — 从视频提取动作模板(异步)。
Returns template_id str.
"""
print(f"\n[step2] aa-template-generation …")
r = requests.post(
f"{BASE_URL}/api/v1/services/aigc/image2video/aa-template-generation/",
headers=_headers(async_mode=True),
json={"model": "animate-anyone-template-gen2", "input": {"video_url": video_url}},
timeout=60,
)
r.raise_for_status()
data = r.json()
task_id = (data.get("output") or {}).get("task_id")
if not task_id:
raise RuntimeError(f"No task_id in template response: {json.dumps(data, ensure_ascii=False)}")
print(f" task_id={task_id}")
out = wait_task(task_id, interval=10)
template_id = out.get("template_id")
if not template_id:
raise RuntimeError(f"No template_id in result: {out}")
duration = (out.get("usage") or {}).get("video_duration", "?")
print(f" ✅ template_id={template_id} duration={duration}s")
return template_id
# ── Step 3: Video generation ────────────────────────────────────────────────────
def aa_generate(image_url: str, template_id: str,
use_ref_img_bg: bool = False, video_ratio: str = "9:16") -> str:
"""
POST /video-synthesis/ — 基于图像 + 动作模板生成视频(异步)。
Returns video_url str.
"""
print(f"\n[step3] aa-generate use_ref_img_bg={use_ref_img_bg} video_ratio={video_ratio} …")
payload = {
"model": "animate-anyone-gen2",
"input": {"image_url": image_url, "template_id": template_id},
"parameters": {"use_ref_img_bg": use_ref_img_bg, "video_ratio": video_ratio},
}
r = requests.post(
f"{BASE_URL}/api/v1/services/aigc/image2video/video-synthesis/",
headers=_headers(async_mode=True),
json=payload,
timeout=60,
)
r.raise_for_status()
data = r.json()
task_id = (data.get("output") or {}).get("task_id")
if not task_id:
raise RuntimeError(f"No task_id in generate response: {json.dumps(data, ensure_ascii=False)}")
print(f" task_id={task_id}")
out = wait_task(task_id, interval=15, max_wait=1800)
video_url = out.get("video_url")
if not video_url:
raise RuntimeError(f"No video_url in result: {out}")
print(f" ✅ video_url={video_url}")
return video_url
# ── Main ───────────────────────────────────────────────────────────────────────
def main():
p = argparse.ArgumentParser(
description="AnimateAnyone Gen2: image+video → animated video (3-step pipeline)"
)
p.add_argument("--image-url", help="图片公网 URL(已上传 OSS 等)")
p.add_argument("--video-url", help="动作视频公网 URL(已上传 OSS 等)")
p.add_argument("--image", help="本地图片路径(自动转换格式并上传 OSS)")
p.add_argument("--video", help="本地动作视频路径(自动转换格式并上传 OSS)")
p.add_argument("--template-id", help="已有 template_id,跳过 Step 2")
p.add_argument("--use-ref-img-bg", action="store_true",
help="以输入图片为背景生成(默认用视频背景)")
p.add_argument("--video-ratio", default="9:16", choices=["9:16", "3:4"],
help="视频画幅(use_ref_img_bg=true 时有效)")
p.add_argument("--download", action="store_true", help="下载输出视频")
p.add_argument("--output", default="aa_output.mp4", help="输出文件名")
p.add_argument("--skip-detect", action="store_true", help="跳过图像检测步骤")
args = p.parse_args()
_AA_TEMPLATE_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,256}$")
tmp_files = []
try:
# ── prepare image URL ──────────────────────────────────────────────
image_url = args.image_url
if image_url:
image_url = validate_http_https_url(image_url, field="--image-url")
if not image_url:
if not args.image:
p.error("需要 --image 或 --image-url")
converted = convert_image(args.image)
if converted != args.image:
tmp_files.append(converted)
image_url = upload_to_oss(converted)
# ── prepare video URL ──────────────────────────────────────────────
video_url = args.video_url
if video_url:
video_url = validate_http_https_url(video_url, field="--video-url")
if args.template_id and not _AA_TEMPLATE_ID_RE.fullmatch(args.template_id.strip()):
raise ValueError(f"Invalid --template-id format: {args.template_id!r}")
if not video_url and not args.template_id:
if not args.video:
p.error("需要 --video 或 --video-url 或 --template-id")
converted = convert_video(args.video)
if converted != args.video:
tmp_files.append(converted)
video_url = upload_to_oss(converted)
# ── Step 1: detect ─────────────────────────────────────────────────
if not args.skip_detect:
aa_detect(image_url)
# ── Step 2: template ───────────────────────────────────────────────
template_id = args.template_id
if not template_id:
template_id = aa_template(video_url)
# ── Step 3: generate ───────────────────────────────────────────────
final_url = aa_generate(
image_url, template_id,
use_ref_img_bg=args.use_ref_img_bg,
video_ratio=args.video_ratio,
)
print(f"\n✅ Done! video_url = {final_url}")
if args.download:
out_path = resolve_under_cwd(args.output, field="--output")
safe_url = validate_http_https_url(final_url, field="result video URL")
print(f"Downloading → {out_path} …")
with urllib.request.urlopen(safe_url, timeout=300) as response:
with open(out_path, 'wb') as f:
f.write(response.read())
size = out_path.stat().st_size
print(f"Saved {out_path} ({size//1024}KB)")
finally:
for f in tmp_files:
try:
os.unlink(f)
except Exception:
pass
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"\nERROR: {e}", file=sys.stderr)
sys.exit(1)
#!/usr/bin/env python3
"""灵眸:自动选择模板创建播报视频并轮询结果。
能力:
- 列出账号下已有播报模板
- 未指定 --template-id 时随机选择一个已有模板
- 自动读取模板 variables,并把文本填入可用的 text 变量(优先 text_content)
- 在 1.7.0+ SDK 下可列出公共模板,并在本地模板为空时复制最多 3 个公共模板
- 注意:复制公共模板成功 ≠ 一定可以立即直接生成视频;有些复制出来的是草稿模板,可能仍需人工补充有效片段/素材
"""
import argparse
import json
import os
import random
import sys
import time
import urllib.request
from typing import Optional
from input_validation import read_text_file_limited, resolve_under_cwd, validate_http_https_url
VENV_PYTHON = os.environ.get(
"LINGMOU_VENV_PYTHON",
"/data/xxx-workspace/.venv-alibabacloud-avatar-video/bin/python",
)
def create_client():
try:
from alibabacloud_lingmou20250527.client import Client
from alibabacloud_tea_openapi import models as open_api_models
except Exception as e:
raise RuntimeError(
"需要安装: pip install alibabacloud-lingmou20250527 alibabacloud-tea-openapi"
) from e
config = open_api_models.Config(
access_key_id=os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
access_key_secret=os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
endpoint=os.environ.get("LINGMOU_ENDPOINT", "lingmou.cn-beijing.aliyuncs.com"),
region_id=os.environ.get("LINGMOU_REGION", "cn-beijing"),
user_agent="AlibabaCloud-Agent-Skills/alibabacloud-avatar-video",
)
return Client(config)
def sdk_supports_public_templates(client) -> bool:
return hasattr(client, "list_public_broadcast_scene_templates") and hasattr(client, "copy_broadcast_scene_from_template")
def list_templates(client):
from alibabacloud_lingmou20250527 import models as lm
resp = client.list_broadcast_templates(lm.ListBroadcastTemplatesRequest())
return resp.body.data or []
def list_public_templates(client, size: int = 10):
from alibabacloud_lingmou20250527 import models as lm
if not sdk_supports_public_templates(client):
return []
req = lm.ListPublicBroadcastSceneTemplatesRequest(size=size)
resp = client.list_public_broadcast_scene_templates(req)
return resp.body.data or []
def copy_public_templates(client, public_templates, max_copy: int = 3):
from alibabacloud_lingmou20250527 import models as lm
copied = []
for idx, t in enumerate(public_templates[:max_copy], start=1):
req = lm.CopyBroadcastSceneFromTemplateRequest(
name=f"OpenClaw copied template {idx}",
ratio=getattr(t, "ratio", None) or "16:9",
template_id=t.id,
)
resp = client.copy_broadcast_scene_from_template(req)
copied.append(resp.body.data)
return copied
def get_template_detail(client, template_id: str):
from alibabacloud_lingmou20250527 import models as lm
req = lm.GetBroadcastTemplateRequest()
req.template_id = template_id
resp = client.get_broadcast_template(req)
return resp.body.data
def choose_template(client, explicit_template_id: Optional[str] = None, seed: Optional[int] = None, auto_copy_public: bool = True):
templates = list_templates(client)
if explicit_template_id:
for t in templates:
if t.id == explicit_template_id:
return t, False
class T:
pass
t = T()
t.id = explicit_template_id
t.name = explicit_template_id
return t, False
if templates:
rng = random.Random(seed)
return rng.choice(list(templates)), False
if auto_copy_public and sdk_supports_public_templates(client):
publics = list_public_templates(client, size=10)
if publics:
copied = copy_public_templates(client, publics, max_copy=3)
if copied:
rng = random.Random(seed)
return rng.choice(list(copied)), True
raise RuntimeError(
"当前账号下没有可用播报模板;且未能获得可直接使用的公共模板副本。请先在灵眸中准备至少一个可直接生成的视频模板。"
)
def build_variables(template_detail, text: str):
from alibabacloud_lingmou20250527 import models as lm
variables = getattr(template_detail, "variables", None) or []
if not variables:
return [
lm.TemplateVariable(
name="text_content",
type="text",
properties={"content": text},
)
]
text_vars = [v for v in variables if getattr(v, "type", None) == "text"]
if not text_vars:
raise RuntimeError("模板中没有可替换的 text 变量,暂时无法仅凭脚本生成口播视频")
preferred_names = ["text_content", "test_text", "content", "text"]
target = None
for name in preferred_names:
for v in text_vars:
if getattr(v, "name", None) == name:
target = v
break
if target:
break
if not target:
target = text_vars[0]
return [
lm.TemplateVariable(
name=target.name,
type="text",
properties={"content": text},
)
]
def submit_video(client, template_id: str, text: str, name: str, resolution: str, fps: int, watermark: bool):
from alibabacloud_lingmou20250527 import models as lm
template_detail = get_template_detail(client, template_id)
variables = build_variables(template_detail, text)
req = lm.CreateBroadcastVideoFromTemplateRequest(
template_id=template_id,
name=name,
variables=variables,
video_options=lm.CreateBroadcastVideoFromTemplateRequestVideoOptions(
resolution=resolution,
fps=fps,
watermark=watermark,
),
)
resp = client.create_broadcast_video_from_template(req)
video_id = resp.body.data.id
return video_id, template_detail, variables
def wait_video(client, video_id: str, interval: int = 3, max_wait: int = 1800):
from alibabacloud_lingmou20250527 import models as lm
start = time.time()
while time.time() - start < max_wait:
req = lm.ListBroadcastVideosByIdRequest(video_ids=[video_id])
resp = client.list_broadcast_videos_by_id(req)
data = resp.body.data or []
if not data:
time.sleep(interval)
continue
video = data[0]
status = video.status
print(f"status={status}")
if status == "SUCCESS":
return video.video_url
if status in ("ERROR", "FAILED"):
raise RuntimeError(f"LingMou task failed: {status}")
time.sleep(interval)
raise TimeoutError("LingMou polling timeout")
def main():
p = argparse.ArgumentParser()
p.add_argument("--template-id")
p.add_argument("--list-templates", action="store_true", help="列出账号下已有模板")
p.add_argument("--list-public-templates", action="store_true", help="列出公共模板(需 SDK 1.7.0+)")
p.add_argument("--copy-public-templates", action="store_true", help="复制最多 3 个公共模板到当前账号(需 SDK 1.7.0+)")
p.add_argument("--show-template-detail", action="store_true", help="输出所选模板详情")
p.add_argument("--seed", type=int, default=None, help="随机模板选择种子,便于复现")
p.add_argument("--text")
p.add_argument("--text-file")
p.add_argument("--name", default="OpenClaw Avatar Video")
p.add_argument("--resolution", default="720p", choices=["720p", "1080p"])
p.add_argument("--fps", type=int, default=30, choices=[15, 30])
p.add_argument("--watermark", action="store_true", default=False)
p.add_argument("--download", action="store_true")
p.add_argument("--output", default="lingmou_output.mp4")
args = p.parse_args()
client = create_client()
if args.list_templates:
templates = list_templates(client)
print(json.dumps([
{"id": t.id, "name": getattr(t, "name", None)} for t in templates
], ensure_ascii=False, indent=2))
if args.list_public_templates:
publics = list_public_templates(client, size=10)
print(json.dumps([
{"id": t.id, "name": getattr(t, "name", None), "ratio": getattr(t, "ratio", None), "desc": getattr(t, "desc", None)} for t in publics
], ensure_ascii=False, indent=2))
if args.copy_public_templates:
publics = list_public_templates(client, size=10)
copied = copy_public_templates(client, publics, max_copy=3)
print(json.dumps([
{"id": t.id, "name": getattr(t, "name", None), "ratio": getattr(t, "ratio", None), "status": getattr(t, "status", None)} for t in copied
], ensure_ascii=False, indent=2))
if not (args.text or args.text_file or args.template_id or args.show_template_detail):
return
if (args.list_templates or args.list_public_templates) and not (args.text or args.text_file or args.template_id or args.show_template_detail or args.copy_public_templates):
return
chosen, copied_from_public = choose_template(client, args.template_id, seed=args.seed, auto_copy_public=True)
print(f"template_id={chosen.id}")
print(f"template_name={getattr(chosen, 'name', '')}")
print(f"template_from_public_copy={str(copied_from_public).lower()}")
if args.show_template_detail:
detail = get_template_detail(client, chosen.id)
print(json.dumps(detail.to_map(), ensure_ascii=False, indent=2, default=str))
if not (args.text or args.text_file):
return
text = args.text
if args.text_file:
text = read_text_file_limited(args.text_file, field="--text-file")
if not text:
p.error("Need --text or --text-file")
try:
video_id, template_detail, variables = submit_video(
client,
template_id=chosen.id,
text=text,
name=args.name,
resolution=args.resolution,
fps=args.fps,
watermark=args.watermark,
)
except Exception as e:
if copied_from_public:
raise RuntimeError(
f"公共模板已复制,但直接生成失败:{e}。这通常表示复制出的模板仍是草稿或缺少有效片段/素材,需要先在灵眸侧完善模板。"
)
raise
print(f"video_id={video_id}")
print("variables=" + json.dumps([v.to_map() for v in variables], ensure_ascii=False))
if args.show_template_detail:
print("template_detail=" + json.dumps(template_detail.to_map(), ensure_ascii=False, default=str))
video_url = wait_video(client, video_id)
print(f"video_url={video_url}")
if args.download and video_url:
out_path = resolve_under_cwd(args.output, field="--output")
safe_url = validate_http_https_url(video_url, field="video URL")
with urllib.request.urlopen(safe_url, timeout=300) as response:
with open(out_path, 'wb') as f:
f.write(response.read())
print(f"saved={out_path}")
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
#!/usr/bin/env python3
"""
Human Avatar Demo Pipeline
统一入口,调用本 Skill 的三个脚本:
- EMO: portrait_animate.py
- AA: animate_anyone.py
- 灵眸: avatar_video.py
示例:
# EMO(本地图片+本地音频)
python demo_pipeline.py --mode emo --image ./face.jpg --audio ./speech.mp3 --download
# AA(URL)
python demo_pipeline.py --mode aa --image-url https://... --video-url https://... --download
# 灵眸(模板+文案)
python demo_pipeline.py --mode lingmou --template-id BSxxxx --text "大家好" --download
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
from input_validation import resolve_under_cwd, validate_http_https_url
ROOT = Path(__file__).resolve().parent
def run_cmd(cmd):
print("$", " ".join(cmd))
proc = subprocess.run(cmd)
if proc.returncode != 0:
sys.exit(proc.returncode)
def build_emo_cmd(args):
cmd = [sys.executable, str(ROOT / "portrait_animate.py")]
if args.image_url:
cmd += ["--image-url", args.image_url]
if args.audio_url:
cmd += ["--audio-url", args.audio_url]
if args.image:
cmd += ["--image", args.image]
if args.audio:
cmd += ["--audio", args.audio]
cmd += ["--ratio", args.ratio, "--style-level", args.style_level, "--output", args.output]
if args.download:
cmd += ["--download"]
return cmd
def build_aa_cmd(args):
cmd = [sys.executable, str(ROOT / "animate_anyone.py")]
if args.image_url:
cmd += ["--image-url", args.image_url]
if args.video_url:
cmd += ["--video-url", args.video_url]
if args.image:
cmd += ["--image", args.image]
if args.video:
cmd += ["--video", args.video]
cmd += ["--output", args.output]
if args.download:
cmd += ["--download"]
return cmd
def build_lingmou_cmd(args):
cmd = [
sys.executable,
str(ROOT / "avatar_video.py"),
"--template-id",
args.template_id,
"--name",
args.name,
"--resolution",
args.resolution,
"--fps",
str(args.fps),
"--output",
args.output,
]
if args.watermark:
cmd += ["--watermark"]
if args.text_file:
cmd += ["--text-file", args.text_file]
else:
cmd += ["--text", args.text]
if args.download:
cmd += ["--download"]
return cmd
def validate_args(args):
if args.download:
resolve_under_cwd(args.output, field="--output")
if args.mode == "emo":
if not ((args.image_url or args.image) and (args.audio_url or args.audio)):
raise SystemExit("EMO 需要 image(+url|file) 和 audio(+url|file)")
if args.image_url:
validate_http_https_url(args.image_url, field="--image-url")
if args.audio_url:
validate_http_https_url(args.audio_url, field="--audio-url")
elif args.mode == "aa":
if not ((args.image_url or args.image) and (args.video_url or args.video)):
raise SystemExit("AA 需要 image(+url|file) 和 video(+url|file)")
if args.image_url:
validate_http_https_url(args.image_url, field="--image-url")
if args.video_url:
validate_http_https_url(args.video_url, field="--video-url")
elif args.mode == "lingmou":
if not args.template_id:
raise SystemExit("灵眸模式必须提供 --template-id")
if not (args.text or args.text_file):
raise SystemExit("灵眸模式必须提供 --text 或 --text-file")
def print_env_hint(mode):
print("\n[env-check]")
if mode in ("emo", "aa"):
print("- 需要 DASHSCOPE_API_KEY(北京地域)")
if mode in ("emo", "aa") and not (os.getenv("DASHSCOPE_API_KEY")):
print(" ! 未检测到 DASHSCOPE_API_KEY")
if mode == "lingmou":
print("- 需要 ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET")
if not os.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"):
print(" ! 未检测到 ALIBABA_CLOUD_ACCESS_KEY_ID")
if not os.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"):
print(" ! 未检测到 ALIBABA_CLOUD_ACCESS_KEY_SECRET")
def main():
p = argparse.ArgumentParser(description="Human Avatar Demo Pipeline")
p.add_argument("--mode", required=True, choices=["emo", "aa", "lingmou"])
# 通用
p.add_argument("--download", action="store_true")
p.add_argument("--output", default="demo_output.mp4")
# EMO / AA 共用输入
p.add_argument("--image-url")
p.add_argument("--image")
# EMO
p.add_argument("--audio-url")
p.add_argument("--audio")
p.add_argument("--ratio", default="1:1", choices=["1:1", "3:4"])
p.add_argument("--style-level", default="normal", choices=["normal", "calm", "active"])
# AA
p.add_argument("--video-url")
p.add_argument("--video")
# 灵眸
p.add_argument("--template-id")
p.add_argument("--text")
p.add_argument("--text-file")
p.add_argument("--name", default="OpenClaw Avatar Video")
p.add_argument("--resolution", default="720p", choices=["720p", "1080p"])
p.add_argument("--fps", type=int, default=30, choices=[15, 30])
p.add_argument("--watermark", action="store_true")
args = p.parse_args()
validate_args(args)
print_env_hint(args.mode)
if args.mode == "emo":
cmd = build_emo_cmd(args)
elif args.mode == "aa":
cmd = build_aa_cmd(args)
else:
cmd = build_lingmou_cmd(args)
run_cmd(cmd)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
万相图生视频 (wan2.x-i2v)
SECURITY NOTES:
- subprocess: used ONLY for ffmpeg image format conversion. No shell=True.
- OSS credentials: env-only, used ONLY to upload user media to their own
OSS bucket and generate signed GET URLs for Alibaba Cloud API access.
- All API calls target dashscope.aliyuncs.com (Alibaba Cloud official).
默认模型:wan2.7-i2v
用法:
# wan2.7-i2v(默认)
python image_to_video.py --image ./portrait.jpg --prompt "她在草地上微笑跳舞" --download
python image_to_video.py --image ./portrait.jpg --resolution 1080P --duration 10 --download
# wan2.6-i2v-flash
python image_to_video.py --image ./portrait.jpg --prompt "..." --model wan2.6-i2v-flash --download
python image_to_video.py --image-url https://... --prompt "..." --model wan2.6-i2v-flash --resolution 720P --duration 5
# 带音频
python image_to_video.py --image ./portrait.jpg --audio-url https://.../bgm.mp3 --prompt "..." --download
# 先文生图再图生视频(一条龙)
python image_to_video.py --t2i-prompt "一位身着汉服的女性站在桃花林中" --prompt "她缓缓转身,花瓣飘落" --download
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
import requests
from input_validation import (
mk_temp_path_for_ffmpeg,
resolve_under_cwd,
validate_http_https_url,
)
BASE_URL = os.getenv("DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com")
_OSS_SIGNED_URL_EXPIRES = int(os.environ.get("OSS_SIGNED_URL_EXPIRES", str(3 * 24 * 3600)))
USER_AGENT = "AlibabaCloud-Agent-Skills/alibabacloud-avatar-video"
def _headers(async_mode: bool = True) -> dict:
key = os.environ.get("DASHSCOPE_API_KEY")
if not key:
raise RuntimeError("DASHSCOPE_API_KEY not set")
h = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
}
if async_mode:
h["X-DashScope-Async"] = "enable"
return h
def _wait_task(task_id: str, interval: int = 10, max_wait: int = 600) -> dict:
url = f"{BASE_URL}/api/v1/tasks/{task_id}"
start = time.time()
while time.time() - start < max_wait:
r = requests.get(url, headers=_headers(async_mode=False), timeout=30)
r.raise_for_status()
data = r.json()
out = data.get("output", {})
status = out.get("task_status", "UNKNOWN")
elapsed = int(time.time() - start)
print(f" [{elapsed}s] status={status}")
if status == "SUCCEEDED":
return out
if status in ("FAILED", "CANCELED", "UNKNOWN"):
raise RuntimeError(f"Task failed: {json.dumps(data, ensure_ascii=False)}")
time.sleep(interval)
raise TimeoutError(f"Task {task_id} timed out")
def upload_to_oss(local_path: str, expires: int = _OSS_SIGNED_URL_EXPIRES) -> str:
"""Upload to OSS, return signed GET URL (default 3 days)."""
import oss2
auth = oss2.Auth(
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
)
bucket_name = os.environ["OSS_BUCKET"]
endpoint = os.environ.get("OSS_ENDPOINT", "oss-cn-beijing.aliyuncs.com")
endpoint = endpoint.replace("https://", "").replace("http://", "").rstrip("/")
bucket = oss2.Bucket(auth, f"https://{endpoint}", bucket_name)
key = f"human-avatar/{Path(local_path).name}"
print(f"[oss] uploading {Path(local_path).name} …")
bucket.put_object_from_file(key, local_path)
url = bucket.sign_url("GET", key, expires)
print(f"[oss] signed_url ok ({expires//3600}h)")
return url
def convert_image(src: str) -> str:
"""Convert image to jpg/png if not compatible."""
p = Path(src)
if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".bmp", ".webp"):
return src
ff = shutil.which("ffmpeg") or "ffmpeg"
dst = mk_temp_path_for_ffmpeg(".jpg", "i2v_img_")
subprocess.run([ff, "-y", "-i", src, "-q:v", "2", dst], check=True, capture_output=True)
return dst
def image_to_video(
img_url: str,
prompt: str = "",
model: str = "wan2.7-i2v",
resolution: str = "720P",
duration: int = 5,
negative_prompt: str = "",
prompt_extend: bool = True,
audio_url: str | None = None,
audio: bool = True,
) -> str:
"""
Call 万相 image-to-video API. Returns video URL.
Args:
img_url: 首帧图像 URL(公网可访问)或 base64
prompt: 描述视频动作的提示词
model: 模型名,默认 wan2.7-i2v
resolution: 分辨率,wan2.7: 720P/1080P(默认 1080P),wan2.6: 480P/720P(默认 720P)
duration: 视频时长(秒),wan2.7: [2,15],wan2.6: 3/5/10s,默认 5
negative_prompt: 反向提示词
prompt_extend: 是否开启提示词智能改写
audio_url: 自定义背景音频 URL(可选)
audio: 是否生成音频(wan2.6/2.5 支持;wan2.7 未传入音频时自动生成)
"""
endpoint = f"{BASE_URL}/api/v1/services/aigc/video-generation/video-synthesis"
# 根据模型版本构建不同的请求结构
is_wan27 = model.startswith("wan2.7")
if is_wan27:
# wan2.7-i2v 使用 media 数组结构
media = [{"type": "first_frame", "url": img_url}]
if audio_url:
media.append({"type": "driving_audio", "url": audio_url})
inp = {"prompt": prompt, "media": media}
if negative_prompt:
inp["negative_prompt"] = negative_prompt
# wan2.7 默认 resolution=1080P, duration 支持 [2,15]
params = {
"resolution": resolution,
"prompt_extend": prompt_extend,
"duration": duration,
}
else:
# wan2.6 及更早版本使用 img_url 字段
inp = {"img_url": img_url}
if prompt:
inp["prompt"] = prompt
if negative_prompt:
inp["negative_prompt"] = negative_prompt
if audio_url:
inp["audio_url"] = audio_url
params = {
"resolution": resolution,
"prompt_extend": prompt_extend,
"duration": duration,
}
# wan2.7-i2v generates audio by default; set False to disable
if not audio:
params["audio"] = False
payload = {"model": model, "input": inp, "parameters": params}
print(f"\n[i2v] submit model={model} resolution={resolution} duration={duration}s")
r = requests.post(endpoint, headers=_headers(async_mode=True), json=payload, timeout=60)
r.raise_for_status()
data = r.json()
task_id = (data.get("output") or {}).get("task_id")
if not task_id:
raise RuntimeError(f"No task_id: {json.dumps(data, ensure_ascii=False)}")
print(f" task_id={task_id}")
out = _wait_task(task_id, interval=10, max_wait=600)
video_url = out.get("video_url")
if not video_url:
raise RuntimeError(f"No video_url in result: {out}")
print(f"\n✅ video_url = {video_url}")
return video_url
def main():
p = argparse.ArgumentParser(description="万相图生视频")
# image input
img_grp = p.add_mutually_exclusive_group()
img_grp.add_argument("--image", help="本地图片路径(自动上传 OSS)")
img_grp.add_argument("--image-url", help="图片公网 URL")
img_grp.add_argument("--t2i-prompt", help="先文生图再图生视频(调用 text_to_image)")
p.add_argument("--prompt", default="", help="描述视频内容的提示词")
p.add_argument("--model", default="wan2.7-i2v",
help="模型名 (default: wan2.7-i2v),可选: wan2.6-i2v-flash, wan2.5-i2v-preview, wan2.2-i2v-plus 等")
p.add_argument("--t2i-model", default="wan2.2-t2i-flash",
help="文生图模型(--t2i-prompt 模式下使用,default: wan2.2-t2i-flash)")
p.add_argument("--resolution", default=None,
help="分辨率,wan2.7: 720P/1080P(默认1080P),wan2.6: 480P/720P(默认720P)")
p.add_argument("--duration", type=int, default=None,
help="视频时长(秒),wan2.7: [2,15],wan2.6: 3/5/10,默认 5")
p.add_argument("--negative-prompt", default="", help="反向提示词")
p.add_argument("--no-prompt-extend", action="store_true", help="关闭提示词智能改写")
p.add_argument("--audio-url", default=None, help="背景音频 URL(wan2.6/2.5 支持)")
p.add_argument("--no-audio", action="store_true", help="生成无声视频")
p.add_argument("--download", action="store_true", help="下载生成视频到本地")
p.add_argument("--output", default="i2v_output.mp4", help="输出文件名")
args = p.parse_args()
# ── get image URL ────────────────────────────────────────────────────
img_url = args.image_url
if img_url:
img_url = validate_http_https_url(img_url, field="--image-url")
if args.audio_url:
args.audio_url = validate_http_https_url(args.audio_url, field="--audio-url")
tmp_files = []
if args.t2i_prompt:
# Generate image first using text_to_image
print(f"\n[step0] text-to-image model={args.t2i_model}")
from text_to_image import text_to_image
images = text_to_image(
prompt=args.t2i_prompt,
model=args.t2i_model,
size="960*1696", # 9:16 default for portrait
n=1,
prompt_extend=not args.no_prompt_extend,
)
img_url = validate_http_https_url(images[0], field="text-to-image URL")
print(f" t2i image_url = {img_url}")
elif args.image:
converted = convert_image(args.image)
if converted != args.image:
tmp_files.append(converted)
img_url = upload_to_oss(converted)
if not img_url:
p.error("需要 --image, --image-url 或 --t2i-prompt 之一")
# 根据模型版本设置默认值
is_wan27 = args.model.startswith("wan2.7")
if args.resolution is None:
args.resolution = "1080P" if is_wan27 else "720P"
if args.duration is None:
args.duration = 5 # 两个版本都支持 5 秒
# 验证参数
if is_wan27:
if args.resolution not in ["720P", "1080P"]:
p.error(f"wan2.7 支持的分辨率: 720P, 1080P,当前: {args.resolution}")
if args.duration < 2 or args.duration > 15:
p.error(f"wan2.7 支持的时长: [2,15] 秒,当前: {args.duration}")
else:
if args.resolution not in ["480P", "720P"]:
p.error(f"wan2.6 及更早版本支持的分辨率: 480P, 720P,当前: {args.resolution}")
if args.duration not in [3, 5, 10]:
p.error(f"wan2.6 及更早版本支持的时长: 3, 5, 10 秒,当前: {args.duration}")
# ── submit i2v ───────────────────────────────────────────────────────
try:
video_url = image_to_video(
img_url=img_url,
prompt=args.prompt,
model=args.model,
resolution=args.resolution,
duration=args.duration,
negative_prompt=args.negative_prompt,
prompt_extend=not args.no_prompt_extend,
audio_url=args.audio_url,
audio=not args.no_audio,
)
if args.download:
out_path = resolve_under_cwd(args.output, field="--output")
safe_url = validate_http_https_url(video_url, field="result video URL")
print(f"Downloading → {out_path} …")
with urllib.request.urlopen(safe_url, timeout=300) as response:
with open(out_path, 'wb') as f:
f.write(response.read())
size_kb = out_path.stat().st_size // 1024
print(f"Saved {out_path} ({size_kb}KB)")
finally:
for f in tmp_files:
try:
os.unlink(f)
except Exception:
pass
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"\nERROR: {e}", file=sys.stderr)
sys.exit(1)
"""Shared CLI input checks: path confinement, http(s) URLs, readable files."""
from __future__ import annotations
import os
from pathlib import Path
from urllib.parse import urlparse
_MAX_TEXT_FILE_BYTES = 10 * 1024 * 1024
def resolve_under_cwd(user_path: str, *, field: str = "path") -> Path:
"""Resolve a user path under the current working directory; reject traversal."""
base = Path.cwd().resolve(strict=False)
candidate = (base / user_path).resolve(strict=False)
try:
candidate.relative_to(base)
except ValueError as e:
raise ValueError(
f"{field} must resolve inside the current working directory ({base}): {user_path!r}"
) from e
return candidate
def validate_http_https_url(url: str, *, field: str = "URL") -> str:
u = url.strip()
parsed = urlparse(u)
if parsed.scheme not in ("http", "https"):
raise ValueError(f"{field} must use http or https scheme: {url!r}")
if not parsed.netloc:
raise ValueError(f"{field} must include a host: {url!r}")
return u
def validate_readable_file(path_str: str, *, field: str = "file") -> Path:
p = Path(path_str).expanduser()
if not p.is_file():
raise ValueError(f"{field} must be an existing regular file: {path_str!r}")
return p.resolve()
def read_text_file_limited(path_str: str, *, field: str = "--text-file") -> str:
path = validate_readable_file(path_str, field=field)
size = path.stat().st_size
if size > _MAX_TEXT_FILE_BYTES:
raise ValueError(
f"{field} exceeds max size ({_MAX_TEXT_FILE_BYTES // (1024 * 1024)} MiB): {path_str!r}"
)
return path.read_text(encoding="utf-8").strip()
def mk_temp_path_for_ffmpeg(suffix: str, prefix: str) -> str:
"""Create an empty file with a random name and return its path (fd closed for ffmpeg)."""
import tempfile
fd, path = tempfile.mkstemp(suffix=suffix, prefix=prefix)
try:
os.close(fd)
except OSError:
try:
os.unlink(path)
except OSError:
pass
raise
return path
#!/usr/bin/env python3
"""
LivePortrait 人像视频生成 — 两步流水线
SECURITY NOTES:
- subprocess: used ONLY to invoke system ffmpeg (audio/video format
conversion). All arguments are constructed from user-supplied local
file paths; no shell=True, no dynamic code execution.
- OSS credentials (ALIBABA_CLOUD_ACCESS_KEY_ID/SECRET): read from env,
used ONLY to upload media files to the user's own OSS bucket and
generate time-limited signed GET URLs. Never logged or sent elsewhere.
- All API calls go to dashscope.aliyuncs.com (Alibaba Cloud official).
Step 1: liveportrait-detect 图像检测(同步)
Step 2: liveportrait 视频生成(异步)
输入:
- 人像图片(jpeg/jpg/png/bmp/webp)
- 音频文件(wav/mp3,<15MB,1s~3min)
OR 视频文件(自动提取音频)
用法:
python live_portrait.py --image ./portrait.jpg --audio ./speech.mp3 --download
python live_portrait.py --image ./portrait.jpg --video ./speech.mp4 --download
python live_portrait.py --image-url https://... --audio-url https://... --download
python live_portrait.py --image ./portrait.jpg --audio ./speech.mp3 \\
--template active --mouth-strength 1.2 --head-strength 0.8 --download
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
import requests
from input_validation import (
mk_temp_path_for_ffmpeg,
resolve_under_cwd,
validate_http_https_url,
)
BASE_URL = os.getenv("DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com")
_OSS_SIGNED_URL_EXPIRES = int(os.environ.get("OSS_SIGNED_URL_EXPIRES", str(3 * 24 * 3600)))
# ── helpers ────────────────────────────────────────────────────────────────────
USER_AGENT = "AlibabaCloud-Agent-Skills/alibabacloud-avatar-video"
def _headers(async_mode: bool = False) -> dict:
key = os.environ.get("DASHSCOPE_API_KEY")
if not key:
raise RuntimeError("DASHSCOPE_API_KEY not set")
h = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
}
if async_mode:
h["X-DashScope-Async"] = "enable"
return h
def _wait_task(task_id: str, interval: int = 5, max_wait: int = 600) -> dict:
url = f"{BASE_URL}/api/v1/tasks/{task_id}"
start = time.time()
while time.time() - start < max_wait:
try:
r = requests.get(url, headers=_headers(), timeout=30)
r.raise_for_status()
data = r.json()
out = data.get("output", {})
status = out.get("task_status", "UNKNOWN")
elapsed = int(time.time() - start)
print(f" [{elapsed}s] status={status}")
if status == "SUCCEEDED":
return out
if status in ("FAILED", "CANCELED", "UNKNOWN"):
raise RuntimeError(f"Task failed: {json.dumps(data, ensure_ascii=False)}")
except RuntimeError:
raise
except Exception as e:
print(f" [poll error] {e}")
time.sleep(interval)
raise TimeoutError(f"Task {task_id} timed out after {max_wait}s")
def _find_ffmpeg() -> str:
for p in ["ffmpeg", "/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg"]:
if shutil.which(p):
return p
raise RuntimeError("ffmpeg not found. Install: apt install ffmpeg")
def upload_to_oss(local_path: str, expires: int = _OSS_SIGNED_URL_EXPIRES) -> str:
"""Upload local file to OSS, return signed GET URL (default 3 days)."""
import oss2
auth = oss2.Auth(
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
)
bucket_name = os.environ["OSS_BUCKET"]
endpoint = os.environ.get("OSS_ENDPOINT", "oss-cn-beijing.aliyuncs.com")
endpoint = endpoint.replace("https://", "").replace("http://", "").rstrip("/")
bucket = oss2.Bucket(auth, f"https://{endpoint}", bucket_name)
key = f"human-avatar/{Path(local_path).name}"
print(f"[oss] uploading {Path(local_path).name} …")
bucket.put_object_from_file(key, local_path)
url = bucket.sign_url("GET", key, expires)
print(f"[oss] signed_url ok ({expires // 3600}h)")
return url
def convert_image(src: str) -> str:
"""Convert image to jpg if format not natively supported (e.g. heic)."""
p = Path(src)
if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".bmp", ".webp"):
return src
ff = _find_ffmpeg()
dst = mk_temp_path_for_ffmpeg(".jpg", "lp_img_")
subprocess.run([ff, "-y", "-i", src, "-q:v", "2", dst], check=True, capture_output=True)
print(f"[convert] image {p.name} → {Path(dst).name}")
return dst
def extract_audio_from_video(video_path: str) -> str:
"""
Extract audio from video file to mp3.
LivePortrait requires wav or mp3, <15MB, 1s~3min.
"""
ff = _find_ffmpeg()
dst = mk_temp_path_for_ffmpeg(".mp3", "lp_audio_")
print(f"[ffmpeg] extracting audio from {Path(video_path).name} …")
subprocess.run(
[ff, "-y", "-i", video_path, "-vn",
"-ar", "44100", "-ac", "1", "-b:a", "128k",
"-t", "180", # cap at 3min
dst],
check=True, capture_output=True
)
size_mb = Path(dst).stat().st_size / 1024 / 1024
duration = float(subprocess.run(
["ffprobe", "-v", "quiet", "-select_streams", "a:0",
"-show_entries", "stream=duration", "-of", "csv=p=0", dst],
capture_output=True, text=True
).stdout.strip() or "0")
print(f"[ffmpeg] extracted: {Path(dst).name} {size_mb:.1f}MB {duration:.1f}s")
if duration < 1:
raise ValueError(f"Extracted audio is too short ({duration:.1f}s), minimum is 1s")
if size_mb > 14.5:
raise ValueError(f"Extracted audio is too large ({size_mb:.1f}MB), max 15MB")
return dst
def convert_audio(src: str) -> str:
"""Convert audio to mp3 if not wav/mp3."""
p = Path(src)
if p.suffix.lower() in (".wav", ".mp3"):
return src
ff = _find_ffmpeg()
dst = mk_temp_path_for_ffmpeg(".mp3", "lp_audio_")
subprocess.run(
[ff, "-y", "-i", src, "-vn", "-ar", "44100", "-ac", "1", "-b:a", "128k", dst],
check=True, capture_output=True
)
print(f"[convert] audio {p.name} → {Path(dst).name}")
return dst
# ── Step 1: detect ─────────────────────────────────────────────────────────────
def lp_detect(image_url: str) -> None:
"""
POST /face-detect — check image meets LivePortrait requirements (sync).
Raises ValueError if check fails.
"""
print(f"\n[step1] liveportrait-detect …")
r = requests.post(
f"{BASE_URL}/api/v1/services/aigc/image2video/face-detect",
headers=_headers(async_mode=False),
json={"model": "liveportrait-detect", "input": {"image_url": image_url}},
timeout=30,
)
r.raise_for_status()
data = r.json()
out = data.get("output", {})
passed = out.get("pass", False)
msg = out.get("message", "")
if passed:
print(f" ✅ detect passed")
else:
raise ValueError(f"Image failed LivePortrait detect: {msg}")
# ── Step 2: generate ────────────────────────────────────────────────────────────
def lp_generate(
image_url: str,
audio_url: str,
template_id: str = "normal",
eye_move_freq: float = 0.5,
video_fps: int = 24,
mouth_move_strength: float = 1.0,
paste_back: bool = True,
head_move_strength: float = 0.7,
) -> str:
"""
POST /video-synthesis/ — generate LivePortrait video (async).
Returns video_url.
"""
print(f"\n[step2] liveportrait generate template={template_id} fps={video_fps} …")
payload = {
"model": "liveportrait",
"input": {
"image_url": image_url,
"audio_url": audio_url,
},
"parameters": {
"template_id": template_id,
"eye_move_freq": eye_move_freq,
"video_fps": video_fps,
"mouth_move_strength": mouth_move_strength,
"paste_back": paste_back,
"head_move_strength": head_move_strength,
},
}
r = requests.post(
f"{BASE_URL}/api/v1/services/aigc/image2video/video-synthesis/",
headers=_headers(async_mode=True),
json=payload,
timeout=60,
)
r.raise_for_status()
data = r.json()
task_id = (data.get("output") or {}).get("task_id")
if not task_id:
raise RuntimeError(f"No task_id: {json.dumps(data, ensure_ascii=False)}")
print(f" task_id={task_id}")
out = _wait_task(task_id, interval=5, max_wait=600)
video_url = (out.get("results") or {}).get("video_url")
if not video_url:
raise RuntimeError(f"No video_url in result: {out}")
duration = (out.get("usage") or {}).get("video_duration", "?")
print(f" ✅ video_url={video_url} duration={duration}s")
return video_url
# ── Main ───────────────────────────────────────────────────────────────────────
def main():
p = argparse.ArgumentParser(
description="LivePortrait: portrait image + audio/video → animated portrait video"
)
# image
img_grp = p.add_mutually_exclusive_group()
img_grp.add_argument("--image", help="本地图片(自动上传 OSS)")
img_grp.add_argument("--image-url", help="图片公网 URL")
# audio / video
audio_grp = p.add_mutually_exclusive_group()
audio_grp.add_argument("--audio", help="本地音频文件(wav/mp3,自动上传 OSS)")
audio_grp.add_argument("--audio-url", help="音频公网 URL")
audio_grp.add_argument("--video", help="本地视频文件,自动提取音频并上传 OSS")
# generation params
p.add_argument("--template", default="normal", choices=["normal", "calm", "active"],
help="动作模板:normal(默认)/calm(播报)/active(演唱)")
p.add_argument("--eye-freq", type=float, default=0.5,
help="眨眼频率 0~1 (default: 0.5)")
p.add_argument("--fps", type=int, default=24,
help="输出帧率 15~30 (default: 24)")
p.add_argument("--mouth-strength", type=float, default=1.0,
help="嘴部动作幅度 0~1.5 (default: 1.0)")
p.add_argument("--head-strength", type=float, default=0.7,
help="头部动作幅度 0~1 (default: 0.7)")
p.add_argument("--no-paste-back", action="store_true",
help="仅输出人脸区域(不贴回原图)")
p.add_argument("--skip-detect", action="store_true",
help="跳过图像检测步骤")
p.add_argument("--download", action="store_true", help="下载生成的视频到本地")
p.add_argument("--output", default="lp_output.mp4", help="输出文件名 (default: lp_output.mp4)")
args = p.parse_args()
tmp_files = []
try:
# ── image URL ──────────────────────────────────────────────────────
image_url = args.image_url
if image_url:
image_url = validate_http_https_url(image_url, field="--image-url")
if not image_url:
if not args.image:
p.error("需要 --image 或 --image-url")
converted = convert_image(args.image)
if converted != args.image:
tmp_files.append(converted)
image_url = upload_to_oss(converted)
# ── audio URL ──────────────────────────────────────────────────────
audio_url = args.audio_url
if audio_url:
audio_url = validate_http_https_url(audio_url, field="--audio-url")
if not audio_url:
if args.video:
extracted = extract_audio_from_video(args.video)
tmp_files.append(extracted)
audio_url = upload_to_oss(extracted)
elif args.audio:
converted_audio = convert_audio(args.audio)
if converted_audio != args.audio:
tmp_files.append(converted_audio)
audio_url = upload_to_oss(converted_audio)
else:
p.error("需要 --audio, --audio-url 或 --video 之一")
# ── Step 1: detect ─────────────────────────────────────────────────
if not args.skip_detect:
lp_detect(image_url)
# ── Step 2: generate ───────────────────────────────────────────────
video_url = lp_generate(
image_url=image_url,
audio_url=audio_url,
template_id=args.template,
eye_move_freq=args.eye_freq,
video_fps=args.fps,
mouth_move_strength=args.mouth_strength,
paste_back=not args.no_paste_back,
head_move_strength=args.head_strength,
)
print(f"\n✅ Done! video_url = {video_url}")
if args.download:
out_path = resolve_under_cwd(args.output, field="--output")
safe_url = validate_http_https_url(video_url, field="result video URL")
print(f"Downloading → {out_path} …")
with urllib.request.urlopen(safe_url, timeout=300) as response:
with open(out_path, 'wb') as f:
f.write(response.read())
size_kb = out_path.stat().st_size // 1024
print(f"Saved {out_path} ({size_kb}KB)")
finally:
for f in tmp_files:
try:
os.unlink(f)
except Exception:
pass
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"\nERROR: {e}", file=sys.stderr)
sys.exit(1)
#!/usr/bin/env python3
"""EMO 口播视频(DashScope)
流程:face-detect -> video-synthesis -> 轮询 tasks/{task_id}
"""
import argparse
import json
import os
import sys
import time
import urllib.request
from pathlib import Path
import requests
from input_validation import resolve_under_cwd, validate_http_https_url
BASE_URL = os.getenv("DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com")
USER_AGENT = "AlibabaCloud-Agent-Skills/alibabacloud-avatar-video"
def _headers(async_mode: bool = False):
key = os.getenv("DASHSCOPE_API_KEY")
if not key:
raise RuntimeError("Missing DASHSCOPE_API_KEY")
h = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
}
if async_mode:
h["X-DashScope-Async"] = "enable"
return h
_OSS_SIGNED_URL_EXPIRES = int(os.environ.get("OSS_SIGNED_URL_EXPIRES", str(3 * 24 * 3600))) # 默认 3 天
def upload_to_oss(local_path: str, expires: int = _OSS_SIGNED_URL_EXPIRES) -> str:
"""上传文件到 OSS,返回签名 URL(私有 bucket)。有效期默认 3 天。"""
import oss2
auth = oss2.Auth(
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
)
bucket_name = os.environ["OSS_BUCKET"]
endpoint = os.environ.get("OSS_ENDPOINT", "oss-cn-beijing.aliyuncs.com")
# normalize: strip any existing scheme prefix to avoid double https://
endpoint = endpoint.replace("https://", "").replace("http://", "").rstrip("/")
bucket = oss2.Bucket(auth, f"https://{endpoint}", bucket_name)
key = f"human-avatar/{Path(local_path).name}"
bucket.put_object_from_file(key, local_path)
# 签名 URL,DashScope 可直接下载,过期时间默认 3 天
signed_url = bucket.sign_url("GET", key, expires)
return signed_url
def emo_detect(image_url: str, ratio: str = "1:1"):
url = f"{BASE_URL}/api/v1/services/aigc/image2video/face-detect"
payload = {
"model": "emo-detect-v1",
"input": {"image_url": image_url},
"parameters": {"ratio": ratio},
}
r = requests.post(url, headers=_headers(), json=payload, timeout=120)
r.raise_for_status()
data = r.json()
out = data.get("output", {})
if not out.get("check_pass"):
raise RuntimeError(f"EMO detect failed: {out.get('message', out)}")
return out["face_bbox"], out["ext_bbox"]
def emo_submit(image_url: str, audio_url: str, face_bbox, ext_bbox, style_level: str = "normal"):
url = f"{BASE_URL}/api/v1/services/aigc/image2video/video-synthesis"
payload = {
"model": "emo-v1",
"input": {
"image_url": image_url,
"audio_url": audio_url,
"face_bbox": face_bbox,
"ext_bbox": ext_bbox,
},
"parameters": {"style_level": style_level},
}
r = requests.post(url, headers=_headers(async_mode=True), json=payload, timeout=120)
r.raise_for_status()
data = r.json()
task_id = data.get("output", {}).get("task_id")
if not task_id:
raise RuntimeError(f"No task_id in response: {json.dumps(data, ensure_ascii=False)}")
return task_id
def wait_task(task_id: str, interval: int = 15, max_wait: int = 1800):
url = f"{BASE_URL}/api/v1/tasks/{task_id}"
start = time.time()
while time.time() - start < max_wait:
r = requests.get(url, headers=_headers(), timeout=60)
r.raise_for_status()
data = r.json()
status = data.get("output", {}).get("task_status")
print(f"status={status}")
if status == "SUCCEEDED":
return data.get("output", {}).get("results", {}).get("video_url")
if status in ("FAILED", "CANCELED", "UNKNOWN"):
raise RuntimeError(json.dumps(data, ensure_ascii=False))
time.sleep(interval)
raise TimeoutError("Task timeout")
def main():
p = argparse.ArgumentParser()
p.add_argument("--image-url")
p.add_argument("--audio-url")
p.add_argument("--image")
p.add_argument("--audio")
p.add_argument("--ratio", default="1:1", choices=["1:1", "3:4"])
p.add_argument("--style-level", default="normal", choices=["normal", "calm", "active"])
p.add_argument("--download", action="store_true")
p.add_argument("--output", default="emo_output.mp4")
args = p.parse_args()
image_url = args.image_url
if image_url:
image_url = validate_http_https_url(image_url, field="--image-url")
elif args.image:
image_url = upload_to_oss(args.image)
audio_url = args.audio_url
if audio_url:
audio_url = validate_http_https_url(audio_url, field="--audio-url")
elif args.audio:
audio_url = upload_to_oss(args.audio)
if not image_url or not audio_url:
p.error("Need --image-url/--image and --audio-url/--audio")
face_bbox, ext_bbox = emo_detect(image_url, ratio=args.ratio)
task_id = emo_submit(image_url, audio_url, face_bbox, ext_bbox, style_level=args.style_level)
print(f"task_id={task_id}")
video_url = wait_task(task_id)
print(f"video_url={video_url}")
if args.download and video_url:
out_path = resolve_under_cwd(args.output, field="--output")
safe_url = validate_http_https_url(video_url, field="result video URL")
with urllib.request.urlopen(safe_url, timeout=300) as response:
with open(out_path, 'wb') as f:
f.write(response.read())
print(f"saved={out_path}")
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
#!/usr/bin/env python3
"""
Qwen TTS — 文本生成语音(千问实时语音合成)
SECURITY NOTES:
- base64.b64decode: used ONLY to decode audio PCM chunks received from
Alibaba DashScope WebSocket API (response.audio.delta). No external
input is evaluated or executed.
- DASHSCOPE_API_KEY / environment variables: read-only, never logged or
transmitted to any third party. Used solely to authenticate with
dashscope.aliyuncs.com (Alibaba Cloud official endpoint).
- No subprocess calls, no file system writes beyond the output WAV file.
支持根据场景自动选择模型和角色,默认使用 qwen3-tts-vd-realtime-2026-01-15。
输出格式:WAV(内部采集 PCM 后自动转换)。
用法:
python qwen_tts.py --text "你好,欢迎来到未来。" --download
python qwen_tts.py --text "今日股市大涨..." --scene news --download
python qwen_tts.py --text "同学们,今天..." --scene education --voice Ethan --download
python qwen_tts.py --text "亲爱的顾客..." --scene customer_service --download
python qwen_tts.py --text "..." --model qwen3-tts-instruct-flash-realtime \
--instructions "语速较快,带有明显的上扬语调,适合介绍时尚产品" --download
依赖:
pip install dashscope scipy numpy
"""
import argparse
import base64
import io
import os
import struct
import sys
import threading
import time
from pathlib import Path
import dashscope
from input_validation import read_text_file_limited, resolve_under_cwd
from dashscope.audio.qwen_tts_realtime import (
QwenTtsRealtime,
QwenTtsRealtimeCallback,
AudioFormat,
)
# ── User-Agent 配置 ──────────────────────────────────────────────────────────────
USER_AGENT = "AlibabaCloud-Agent-Skills/alibabacloud-avatar-video"
# ── WSS endpoints ──────────────────────────────────────────────────────────────
WSS_URL_CN = "wss://dashscope.aliyuncs.com/api-ws/v1/realtime"
WSS_URL_INTL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime"
# ── Model selection guide ──────────────────────────────────────────────────────
# Default = qwen3-tts-vd-realtime-2026-01-15 (Voice Design, text-described tones)
DEFAULT_MODEL = "qwen3-tts-vd-realtime-2026-01-15"
MODEL_GUIDE = {
"vd": "qwen3-tts-vd-realtime-2026-01-15", # 声音设计(文本描述定制音色)
"vc": "qwen3-tts-vc-realtime-2026-01-15", # 声音复刻(音频样本复刻)
"instruct": "qwen3-tts-instruct-flash-realtime", # 指令控制(情感/角色/播音风格)
"flash": "qwen3-tts-flash-realtime", # 快速多语种(客服/对话机器人)
"legacy": "qwen-tts-realtime", # 旧版稳定
}
# 场景 → 推荐模型 映射
SCENE_TO_MODEL = {
"news": "qwen3-tts-instruct-flash-realtime", # 新闻播报
"documentary": "qwen3-tts-instruct-flash-realtime", # 纪录片
"advertising": "qwen3-tts-instruct-flash-realtime", # 广告宣传
"audiobook": "qwen3-tts-instruct-flash-realtime", # 有声书
"drama": "qwen3-tts-instruct-flash-realtime", # 广播剧/游戏配音
"customer_service": "qwen3-tts-flash-realtime", # 智能客服
"chatbot": "qwen3-tts-flash-realtime", # 对话机器人
"education": "qwen3-tts-flash-realtime", # 教育/讲解
"ecommerce": "qwen3-tts-flash-realtime", # 电商/直播带货
"short_video": "qwen3-tts-flash-realtime", # 短视频配音
"brand": DEFAULT_MODEL, # 品牌定制声音
"default": DEFAULT_MODEL,
}
# 场景 → 推荐 voice(系统音色)
SCENE_TO_VOICE = {
"news": "Serena", # 成熟女声,播报感强
"documentary": "Ethan", # 稳重男声
"advertising": "Cherry", # 活泼女声
"audiobook": "Cherry", # 温柔女声
"drama": "Dylan", # 富有表现力
"customer_service": "Anna", # 亲切女声
"chatbot": "Anna",
"education": "Ethan", # 清晰男声
"ecommerce": "Cherry", # 热情女声
"short_video": "Cherry",
"brand": "Cherry",
"default": "Cherry",
}
# 所有可用系统音色(附描述)
VOICES = {
"Cherry": "活泼甜美女声,中文优先,适合广告/有声书/配音",
"Serena": "成熟知性女声,适合新闻/讲解/企业形象",
"Ethan": "稳重亲切男声,适合教育/纪录片/培训",
"Dylan": "富有表现力男声,适合广播剧/游戏配音",
"Anna": "温柔亲切女声,适合客服/助手/日常",
"Chelsie": "年轻清新女声,适合短视频/电商",
"Thomas": "低沉磁性男声,适合品牌宣传/广告",
"Luna": "温暖柔和女声,适合冥想/故事叙述",
}
# ── PCM → WAV conversion ────────────────────────────────────────────────────────
def pcm_to_wav(pcm_bytes: bytes, sample_rate: int = 24000,
channels: int = 1, sample_width: int = 2) -> bytes:
"""Convert raw PCM bytes to WAV format."""
buf = io.BytesIO()
data_len = len(pcm_bytes)
byte_rate = sample_rate * channels * sample_width
block_align = channels * sample_width
buf.write(b"RIFF")
buf.write(struct.pack("<I", 36 + data_len))
buf.write(b"WAVE")
buf.write(b"fmt ")
buf.write(struct.pack("<I", 16)) # chunk size
buf.write(struct.pack("<H", 1)) # PCM format
buf.write(struct.pack("<H", channels))
buf.write(struct.pack("<I", sample_rate))
buf.write(struct.pack("<I", byte_rate))
buf.write(struct.pack("<H", block_align))
buf.write(struct.pack("<H", sample_width * 8))
buf.write(b"data")
buf.write(struct.pack("<I", data_len))
buf.write(pcm_bytes)
return buf.getvalue()
# ── TTS client ─────────────────────────────────────────────────────────────────
class _TtsCollector(QwenTtsRealtimeCallback):
def __init__(self):
self.pcm_chunks: list[bytes] = []
self.done_event = threading.Event()
self.error: Exception | None = None
def on_open(self) -> None:
pass
def on_close(self, code, msg) -> None:
self.done_event.set()
def on_event(self, response: dict) -> None:
try:
evt = response.get("type", "")
if evt == "response.audio.delta":
chunk = base64.b64decode(response["delta"])
self.pcm_chunks.append(chunk)
elif evt == "session.finished":
self.done_event.set()
elif evt == "error":
self.error = RuntimeError(str(response))
self.done_event.set()
except Exception as e:
self.error = e
self.done_event.set()
def collect(self) -> bytes:
return b"".join(self.pcm_chunks)
def synthesize(
text: str,
model: str = DEFAULT_MODEL,
voice: str = "Cherry",
instructions: str = "",
optimize_instructions: bool = True,
sample_rate: int = 24000,
speed: float = 1.0,
timeout: int = 120,
url: str = WSS_URL_CN,
) -> bytes:
"""
Synthesize text to speech using Qwen TTS Realtime.
Returns:
WAV bytes
"""
dashscope.api_key = os.environ.get("DASHSCOPE_API_KEY", dashscope.api_key)
if not dashscope.api_key:
raise RuntimeError("DASHSCOPE_API_KEY not set")
# Only PCM_24000HZ_MONO_16BIT is universally available; use it regardless of sample_rate param
audio_fmt = AudioFormat.PCM_24000HZ_MONO_16BIT
sample_rate = 24000 # force to match
# 设置 User-Agent
headers = {"User-Agent": USER_AGENT}
collector = _TtsCollector()
client = QwenTtsRealtime(
model=model,
callback=collector,
url=url,
headers=headers, # 传递自定义 headers
)
client.connect()
session_kwargs = dict(
voice=voice,
response_format=audio_fmt,
mode="server_commit",
)
if speed != 1.0:
session_kwargs["speed"] = speed
if instructions:
session_kwargs["instructions"] = instructions
session_kwargs["optimize_instructions"] = optimize_instructions
client.update_session(**session_kwargs)
# Send text in chunks (improves latency for long text)
max_chunk = 100
for i in range(0, len(text), max_chunk):
client.append_text(text[i:i + max_chunk])
time.sleep(0.05)
client.finish()
collector.done_event.wait(timeout=timeout)
if collector.error:
raise collector.error
pcm = collector.collect()
if not pcm:
raise RuntimeError("No audio received from TTS service")
return pcm_to_wav(pcm, sample_rate=sample_rate)
# ── Main ────────────────────────────────────────────────────────────────────────
def main():
p = argparse.ArgumentParser(
description="Qwen TTS — 文本生成语音(根据场景自动选择模型和音色)"
)
p.add_argument("--text", help="要合成的文本(也可用 --text-file)")
p.add_argument("--text-file", help="从文件读取文本")
p.add_argument("--scene", default="default",
choices=list(SCENE_TO_MODEL.keys()),
help="""场景选择(自动推荐模型和音色):
news 新闻播报
documentary 纪录片旁白
advertising 广告宣传
audiobook 有声书
drama 广播剧/游戏配音
customer_service 智能客服
chatbot 对话机器人
education 教育讲解
ecommerce 电商/直播
short_video 短视频配音
brand 品牌定制
default 默认(qwen3-tts-vd-realtime)""")
p.add_argument("--model", default=None,
help=f"指定模型(覆盖 --scene 的推荐)。默认: {DEFAULT_MODEL}")
p.add_argument("--voice", default=None,
help=f"指定音色(覆盖 --scene 的推荐)。可选: {', '.join(VOICES)}")
p.add_argument("--instructions", default="",
help="自然语言指令控制语音表现(需配合 qwen3-tts-instruct-flash-realtime 模型)")
p.add_argument("--speed", type=float, default=1.0,
help="语速倍率,0.5~2.0 (default: 1.0)")
p.add_argument("--sample-rate", type=int, default=24000,
choices=[16000, 24000, 48000],
help="采样率 (default: 24000)")
p.add_argument("--intl", action="store_true",
help="使用新加坡国际节点(需要国际区 API Key)")
p.add_argument("--list-voices", action="store_true", help="列出所有音色及描述")
p.add_argument("--list-models", action="store_true", help="列出模型选型指南")
p.add_argument("--download", action="store_true", help="保存音频到本地文件")
p.add_argument("--output", default="tts_output.wav", help="输出文件名 (default: tts_output.wav)")
args = p.parse_args()
if args.list_voices:
print("\n── 可用音色 ──")
for name, desc in VOICES.items():
print(f" {name:<10} {desc}")
return
if args.list_models:
print("\n── 模型选型指南 ──")
for scene, model in SCENE_TO_MODEL.items():
voice = SCENE_TO_VOICE.get(scene, "Cherry")
print(f" {scene:<20} model={model} voice={voice}")
return
# get text
text = args.text
if not text and args.text_file:
text = read_text_file_limited(args.text_file, field="--text-file")
if not text:
p.error("需要 --text 或 --text-file")
# resolve model and voice
model = args.model or SCENE_TO_MODEL.get(args.scene, DEFAULT_MODEL)
voice = args.voice or SCENE_TO_VOICE.get(args.scene, "Cherry")
url = WSS_URL_INTL if args.intl else WSS_URL_CN
print(f"[tts] model={model} voice={voice} scene={args.scene}")
print(f"[tts] text({len(text)}ch): {text[:60]}{'...' if len(text)>60 else ''}")
if args.instructions:
print(f"[tts] instructions: {args.instructions}")
t0 = time.time()
wav_bytes = synthesize(
text=text,
model=model,
voice=voice,
instructions=args.instructions,
sample_rate=args.sample_rate,
speed=args.speed,
url=url,
)
elapsed = time.time() - t0
size_kb = len(wav_bytes) // 1024
print(f"\n✅ Done {size_kb}KB {elapsed:.1f}s")
if args.download:
out = resolve_under_cwd(args.output, field="--output")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(wav_bytes)
print(f"Saved → {out} ({size_kb}KB)")
else:
# Write to stdout (for piping)
sys.stdout.buffer.write(wav_bytes)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n[interrupted]")
except Exception as e:
print(f"\nERROR: {e}", file=sys.stderr)
sys.exit(1)
#!/usr/bin/env python3
"""
万相文生图 V2 (wan2.x-t2i)
默认模型:wan2.2-t2i-flash
用法:
python text_to_image.py --prompt "一位优雅的女性站在樱花树下" --download
python text_to_image.py --prompt "..." --model wan2.6-t2i --size 960*1696 --n 1
python text_to_image.py --prompt "..." --negative-prompt "低质量,模糊" --download
"""
import argparse
import json
import os
import sys
import time
import urllib.request
from pathlib import Path
import requests
from input_validation import resolve_under_cwd, validate_http_https_url
BASE_URL = os.getenv("DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com")
# wan2.6 支持同步调用;wan2.5及以下需要异步
_SYNC_MODELS = {"wan2.6-t2i"}
USER_AGENT = "AlibabaCloud-Agent-Skills/alibabacloud-avatar-video"
def _headers(async_mode: bool = False) -> dict:
key = os.environ.get("DASHSCOPE_API_KEY")
if not key:
raise RuntimeError("DASHSCOPE_API_KEY not set")
h = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
}
if async_mode:
h["X-DashScope-Async"] = "enable"
return h
def _wait_task(task_id: str, interval: int = 5, max_wait: int = 300) -> dict:
"""Poll async task until SUCCEEDED."""
url = f"{BASE_URL}/api/v1/tasks/{task_id}"
start = time.time()
while time.time() - start < max_wait:
r = requests.get(url, headers=_headers(), timeout=30)
r.raise_for_status()
data = r.json()
status = data.get("output", {}).get("task_status", "UNKNOWN")
elapsed = int(time.time() - start)
print(f" [{elapsed}s] status={status}")
if status == "SUCCEEDED":
return data.get("output", {})
if status in ("FAILED", "CANCELED", "UNKNOWN"):
raise RuntimeError(f"Task failed: {json.dumps(data, ensure_ascii=False)}")
time.sleep(interval)
raise TimeoutError(f"Task {task_id} timed out")
def text_to_image(
prompt: str,
model: str = "wan2.2-t2i-flash",
size: str = "1280*1280",
n: int = 1,
negative_prompt: str = "",
prompt_extend: bool = True,
seed: int = None,
) -> list[str]:
"""
Call 万相 text-to-image API. Returns list of image URLs.
Args:
prompt: 正向提示词
model: 模型名,默认 wan2.2-t2i-flash
size: 分辨率,格式 宽*高,默认 1280*1280
n: 生成张数 1~4,默认 1
negative_prompt: 反向提示词
prompt_extend: 是否开启提示词智能改写,默认 True
seed: 随机种子(可选)
"""
is_sync_model = model in _SYNC_MODELS
payload = {
"model": model,
"input": {
"messages": [
{"role": "user", "content": [{"text": prompt}]}
]
},
"parameters": {
"size": size,
"n": n,
"prompt_extend": prompt_extend,
"watermark": False,
},
}
if negative_prompt:
payload["parameters"]["negative_prompt"] = negative_prompt
if seed is not None:
payload["parameters"]["seed"] = seed
if is_sync_model:
# Synchronous (wan2.6)
endpoint = f"{BASE_URL}/api/v1/services/aigc/multimodal-generation/generation"
print(f"[t2i] sync call model={model} size={size} n={n}")
r = requests.post(endpoint, headers=_headers(), json=payload, timeout=120)
r.raise_for_status()
data = r.json()
images = []
for choice in data.get("output", {}).get("choices", []):
for item in choice.get("message", {}).get("content", []):
if item.get("type") == "image":
images.append(item["image"])
if not images:
raise RuntimeError(f"No images in response: {json.dumps(data, ensure_ascii=False)}")
return images
else:
# Async (wan2.5 and below) — also works for wan2.6
endpoint = f"{BASE_URL}/api/v1/services/aigc/image-generation/generation"
print(f"[t2i] async call model={model} size={size} n={n}")
r = requests.post(endpoint, headers=_headers(async_mode=True), json=payload, timeout=60)
r.raise_for_status()
data = r.json()
task_id = data.get("output", {}).get("task_id")
if not task_id:
raise RuntimeError(f"No task_id: {json.dumps(data, ensure_ascii=False)}")
print(f" task_id={task_id}")
out = _wait_task(task_id)
results = out.get("results", [])
images = [r["url"] for r in results if r.get("url")]
if not images:
raise RuntimeError(f"No image URLs in result: {out}")
return images
def main():
p = argparse.ArgumentParser(description="万相文生图 V2")
p.add_argument("--prompt", required=True, help="正向提示词")
p.add_argument("--model", default="wan2.2-t2i-flash",
help="模型名 (default: wan2.2-t2i-flash),可选: wan2.6-t2i, wan2.5-t2i-preview, wan2.2-t2i-plus 等")
p.add_argument("--size", default="1280*1280",
help="分辨率 宽*高 (default: 1280*1280),推荐: 960*1696(9:16), 1696*960(16:9)")
p.add_argument("--n", type=int, default=1, help="生成张数 1~4 (default: 1)")
p.add_argument("--negative-prompt", default="", help="反向提示词")
p.add_argument("--no-prompt-extend", action="store_true", help="关闭提示词智能改写")
p.add_argument("--seed", type=int, default=None, help="随机种子")
p.add_argument("--download", action="store_true", help="下载生成图片到本地")
p.add_argument("--output-dir", default=".", help="图片保存目录 (default: 当前目录)")
args = p.parse_args()
images = text_to_image(
prompt=args.prompt,
model=args.model,
size=args.size,
n=args.n,
negative_prompt=args.negative_prompt,
prompt_extend=not args.no_prompt_extend,
seed=args.seed,
)
print(f"\n✅ Generated {len(images)} image(s):")
out_dir = resolve_under_cwd(args.output_dir, field="--output-dir")
out_dir.mkdir(parents=True, exist_ok=True)
for i, url in enumerate(images):
print(f" [{i+1}] {url}")
if args.download:
safe_url = validate_http_https_url(url, field="image URL")
filename = out_dir / f"t2i_{int(time.time())}_{i+1}.png"
with urllib.request.urlopen(safe_url, timeout=300) as response:
with open(filename, 'wb') as f:
f.write(response.read())
size_kb = filename.stat().st_size // 1024
print(f" → saved {filename} ({size_kb}KB)")
return images
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"\nERROR: {e}", file=sys.stderr)
sys.exit(1)
Related skills
FAQ
What can avatar-video generate?
Talking-head video (LivePortrait, EMO), full-body animation (AnimateAnyone), text-to-image, image-to-video, Qwen TTS speech, and LingMou digital-human template video.
What credentials does it need?
A DashScope API key plus Alibaba Cloud AccessKey and OSS bucket and endpoint, and ffmpeg installed.