一、摘要
具身智能正在重塑人机交互的边界,从GPT-4V的多模态理解,到Sora的文本生视频,大模型的能力正在以肉眼可见的速度进化。但当我第一次体验"对着一段文字就能生成专业级3D数字人视频"时,还是被这种端到端的工程化能力震撼到了。
作为一个长期关注AI落地应用的技术爱好者,我尝试用Qoder AI Coding工具,结合魔珐星云的参数流API,搭建了一个完整的行政服务助手视频生成平台。整个过程从环境配置到最终出片,不到2小时。
这篇文章,我会以第一视角分享:
- 技术底层:星云如何用参数流技术实现端到端≈500ms毫秒级响应
- 架构拆解:为什么端渲+端侧解算能同时做到低延时和高并发
- 实战教程:完整的Flask Web应用代码,复制就能跑
- 场景探索:行政服务、培训讲解、客服导览等场景的落地思考
- 真实体验:从注册到生成第一个视频的全流程感受
魔珐星云PC端官方链接: https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=
二、环境搭建:数字人视频生成API
魔珐星云数字人视频生成功能能够将文本、PPT转化为高质量数字人视频,助力开发者快速构建专业级视频生成能力
步骤一:创建视频应用
登录魔珐星云控制台,进入 视频生成 模块,点击 创建视频应用

步骤二:配置应用信息
填写应用名称和相关信息,完成应用创建

步骤三:配置形象
在形象管理中选择或上传数字人形象,记录形象ID

步骤四:配置音色
在音色管理中选择适合的配音音色,记录音色ID

步骤五:配置场景
在场景管理中选择视频背景场景,记录场景ID

步骤六:记录配置参数
保存配置好的形象ID 、音色ID 、场景ID,后续在项目中配置使用

步骤七:获取API凭证
进入应用设置页面,复制 AppID 和 AppSecret,用于API鉴权

配置完成后,将获取的参数填入项目 config.py 文件即可开始使用!
三、基于Qoder搭建3D数字人视频:行政服务助手
3.1 config.py - 配置文件
这个文件存储应用凭证和默认参数。首次使用时需要填入从魔珐星云控制台获取的APP_ID和APP_SECRET。更换形象、音色或场景时,只需修改DEFAULT_CONFIG中的对应ID即可。
python
# 数字人视频生成配置文件
# 应用凭证
APP_ID = "你的APP_ID"
APP_SECRET = "你的APP_SECRET"
# API基础URL
HOST = "https://nebula-agent.xingyun3d.com"
# 默认参数配置
DEFAULT_CONFIG = {
"look_name": "你的形象名ID", # 形象名ID
"tts_vcn_name": "你的音色ID", # 音色ID
"studio_name": "你的演播室ID", # 演播室ID
"sub_title": "on", # 开启字幕
"output_resolution": "720P", # 视频清晰度: 540P/720P/1080P/2K/4K
"if_aigc_mark": True, # 是否添加AI生成标识
}
# 轮询配置
POLL_INTERVAL = 10 # 轮询间隔(秒)
MAX_POLL_TIMES = 120 # 最大轮询次数(约20分钟)
3.2 nebula_client.py - API客户端
这个文件封装了魔珐星云API的所有调用逻辑,包括鉴权签名计算、任务创建、状态查询等核心功能。使用时只需实例化NebulaClient类,调用对应方法即可。
python
"""
数字人视频生成API客户端
封装鉴权、请求发送等核心功能
"""
import time
import json
import hashlib
import requests
from urllib.parse import urljoin
from config import APP_ID, APP_SECRET, HOST
class NebulaClient:
"""魔珐星云API客户端"""
def __init__(self, app_id=None, secret=None, host=None):
self.app_id = app_id or APP_ID
self.secret = secret or APP_SECRET
self.host = host or HOST
def _generate_token(self, method, api_path, data):
"""
生成X-TOKEN签名
Args:
method: HTTP方法 (GET/POST)
api_path: API路径 (不包含host)
data: 请求数据字典
Returns:
dict: 包含鉴权信息的headers
"""
timestamp = int(time.time())
# 将data转换为排序后的JSON字符串
sort_json_str = json.dumps(dict(data), sort_keys=True).replace(' ', '')
# 按照规则拼接签名字符串
lower_api_path = api_path.lower()
lower_method = method.lower()
sign_str = f"{lower_api_path}{lower_method}{sort_json_str}{self.secret}{timestamp}"
# 计算MD5
token = hashlib.md5(sign_str.encode('utf-8')).hexdigest()
# 构建headers
headers = {
"X-APP-ID": self.app_id,
"X-TOKEN": token,
"X-TIMESTAMP": str(timestamp)
}
return headers
def _get_query_url(self, api_path, data):
"""构建带query参数的URL路径(用于GET请求签名)"""
if data:
params_str = "&".join([f"{k}={v}" for k, v in sorted(data.items())])
return f"{api_path}?{params_str}"
return api_path
def _request(self, method, api_path, data=None, files=None):
"""
发送API请求
Args:
method: HTTP方法
api_path: API路径
data: 请求数据
files: 文件数据(用于上传PPT)
Returns:
dict: API响应
"""
url = urljoin(self.host, api_path)
if method.upper() == "GET":
# GET请求: 签名时需要包含query参数
query_path = self._get_query_url(api_path, data)
headers = self._generate_token(method, query_path, {})
# 构建完整URL
if data:
params_str = "&".join([f"{k}={v}" for k, v in data.items()])
url = f"{url}?{params_str}"
response = requests.request(method, url, headers=headers, timeout=30)
else:
# POST请求: 签名时使用实际数据
headers = self._generate_token(method, api_path, data or {})
if files:
# 文件上传
headers.pop("content-type", None) # 让requests自动设置
response = requests.request(
method, url,
data=data,
headers=headers,
files=files,
timeout=30
)
else:
# JSON请求
headers["content-type"] = "application/json"
response = requests.request(
method, url,
json=data,
headers=headers,
timeout=30
)
result = response.json()
# 检查错误
if result.get("error_code") != 0:
raise Exception(f"API错误: {result.get('error_reason', '未知错误')}")
return result
def create_render_task_by_segment(self, segment, **kwargs):
"""
通过segment(SSML脚本)创建渲染任务
Args:
segment: SSML脚本数组
**kwargs: 其他参数(覆盖默认配置)
Returns:
int: task_id
"""
from config import DEFAULT_CONFIG
data = {**DEFAULT_CONFIG, **kwargs, "segment": segment}
# 移除None值
data = {k: v for k, v in data.items() if v is not None}
result = self._request(
"POST",
"/user/v1/video_synthesis_task/create_render_task",
data
)
return result["data"]["task_id"]
def parse_ppt_file(self, ppt_file_path):
"""
解析PPT文件
Args:
ppt_file_path: PPT文件路径
Returns:
str: parse_ppt_file_name
"""
with open(ppt_file_path, 'rb') as f:
files = [
('ppt_file', (ppt_file_path, f,
'application/vnd.openxmlformats-officedocument.presentationml.presentation'))
]
result = self._request(
"POST",
"/user/v1/video_synthesis_task/parse_ppt_file",
data={},
files=files
)
return result["data"]["parse_ppt_file_name"]
def create_render_task_by_ppt(self, parse_ppt_file_name, **kwargs):
"""
通过PPT创建渲染任务
Args:
parse_ppt_file_name: PPT解析名称
**kwargs: 其他参数
Returns:
int: task_id
"""
from config import DEFAULT_CONFIG
data = {
**DEFAULT_CONFIG,
**kwargs,
"parse_ppt_file_name": parse_ppt_file_name
}
# 移除segment(如果存在)
data.pop("segment", None)
# 移除None值
data = {k: v for k, v in data.items() if v is not None}
result = self._request(
"POST",
"/user/v1/video_synthesis_task/create_render_task",
data
)
return result["data"]["task_id"]
def get_render_task(self, task_id):
"""
查询视频任务状态
Args:
task_id: 任务ID
Returns:
dict: 任务信息
"""
result = self._request(
"GET",
"/user/v1/video_synthesis_task/get_render_task",
{"task_id": task_id}
)
return result["data"]
def wait_for_completion(self, task_id, callback=None):
"""
等待任务完成(轮询)
Args:
task_id: 任务ID
callback: 回调函数,接收任务信息
Returns:
dict: 最终任务信息
"""
from config import POLL_INTERVAL, MAX_POLL_TIMES
for i in range(MAX_POLL_TIMES):
task_info = self.get_render_task(task_id)
state = task_info.get("synth_state")
print(f"[{i+1}/{MAX_POLL_TIMES}] 任务状态: {state}")
if callback:
callback(task_info)
if state in ["finished", "error", "cancel"]:
return task_info
time.sleep(POLL_INTERVAL)
raise Exception("任务超时")
def cancel_render_task(self, task_id):
"""
取消渲染任务
Args:
task_id: 任务ID
Returns:
bool: 是否成功
"""
result = self._request(
"POST",
"/user/v1/video_synthesis_task/cancel_render_task",
{"task_id": task_id}
)
return result.get("error_code") == 0
def get_preview_url(self, task_id):
"""
获取预览URL
Args:
task_id: 任务ID
Returns:
str: 预览URL
"""
result = self._request(
"GET",
"/user/v1/video_synthesis_task/get_render_task_preview_url",
{"task_id": task_id}
)
return result["data"]["preview_url"]
def get_task_history(self):
"""
获取历史任务列表
Returns:
list: 历史任务列表
"""
result = self._request(
"GET",
"/user/v1/video_synthesis_task/get_render_task_history",
{}
)
return result["data"]
3.3 web_app.py - Web服务后端
这个文件是Flask Web应用的核心,提供HTTP API接口。包含任务创建、查询等路由,并实现后台线程轮询任务状态和自动保存到JSON文件的功能。
python
"""
数字人视频生成 - Web应用
"""
from flask import Flask, render_template, request, jsonify
import time
import threading
import os
import json
from nebula_client import NebulaClient
app = Flask(__name__, template_folder=os.path.dirname(os.path.abspath(__file__)))
# 任务数据文件
TASKS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tasks.json')
# 存储任务状态
tasks = {}
def load_tasks():
"""从文件加载任务数据"""
global tasks
if os.path.exists(TASKS_FILE):
try:
with open(TASKS_FILE, 'r', encoding='utf-8') as f:
tasks = json.load(f)
print(f"[INFO] 已加载 {len(tasks)} 个历史任务")
except Exception as e:
print(f"[WARN] 加载任务文件失败: {e}")
tasks = {}
else:
tasks = {}
def save_tasks():
"""保存任务数据到文件"""
try:
with open(TASKS_FILE, 'w', encoding='utf-8') as f:
json.dump(tasks, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"[ERROR] 保存任务文件失败: {e}")
@app.route('/')
def index():
"""首页"""
return render_template('index.html')
@app.route('/api/create_task', methods=['POST'])
def create_task():
"""创建视频生成任务"""
try:
data = request.json
# 解析SSML脚本
segment_text = data.get('segment', '')
segment = []
# 简单的JSON解析
import json
try:
segment = json.loads(segment_text)
except:
return jsonify({
'success': False,
'message': 'SSML脚本格式错误,请使用正确的JSON格式'
})
if not segment:
return jsonify({
'success': False,
'message': 'SSML脚本不能为空'
})
# 创建客户端
client = NebulaClient()
# 创建任务
task_id = client.create_render_task_by_segment(
segment=segment,
video_name=data.get('video_name', f'Web生成_{int(time.time())}'),
output_resolution=data.get('output_resolution', '720P'),
sub_title=data.get('sub_title', 'on'),
if_aigc_mark=data.get('if_aigc_mark', True)
)
# 存储任务信息
tasks[task_id] = {
'task_id': task_id,
'status': 'creating',
'created_at': time.time(),
'video_url': None,
'image_url': None,
'error': None
}
# 立即保存
save_tasks()
# 启动后台轮询
thread = threading.Thread(target=poll_task_status, args=(task_id,))
thread.daemon = True
thread.start()
return jsonify({
'success': True,
'task_id': task_id,
'message': '任务创建成功'
})
except Exception as e:
return jsonify({
'success': False,
'message': f'创建失败: {str(e)}'
})
@app.route('/api/task_status/<task_id>')
def task_status(task_id):
"""查询任务状态"""
try:
task_id = int(task_id)
if task_id not in tasks:
return jsonify({
'success': False,
'message': '任务不存在'
})
task_info = tasks[task_id]
return jsonify({
'success': True,
'data': task_info
})
except Exception as e:
return jsonify({
'success': False,
'message': f'查询失败: {str(e)}'
})
@app.route('/api/tasks')
def task_list():
"""获取所有任务列表"""
return jsonify({
'success': True,
'tasks': list(tasks.values())
})
def poll_task_status(task_id):
"""后台轮询任务状态"""
client = NebulaClient()
try:
while True:
task_info = client.get_render_task(task_id)
state = task_info.get('synth_state')
tasks[task_id]['status'] = state
tasks[task_id]['raw_data'] = task_info
if state == 'finished':
tasks[task_id]['video_url'] = task_info.get('render_video_oss')
tasks[task_id]['image_url'] = task_info.get('render_image_oss')
tasks[task_id]['amount'] = task_info.get('amount')
tasks[task_id]['synth_start_time'] = task_info.get('synth_start_time')
tasks[task_id]['synth_finish_time'] = task_info.get('synth_finish_time')
# 保存最终结果
save_tasks()
break
elif state in ['error', 'cancel']:
tasks[task_id]['error'] = task_info.get('error_reason')
# 保存错误状态
save_tasks()
break
# 定期保存进度
save_tasks()
time.sleep(10) # 每10秒查询一次
except Exception as e:
tasks[task_id]['error'] = str(e)
tasks[task_id]['status'] = 'error'
save_tasks()
if __name__ == '__main__':
# 启动时加载历史任务
load_tasks()
print("=" * 60)
print("数字人视频生成 Web应用")
print("=" * 60)
print("\n访问地址: http://localhost:5000")
print("\n按 Ctrl+C 停止服务\n")
app.run(host='0.0.0.0', port=5000, debug=True)
3.4 index.html - Web前端界面
这个文件是用户交互界面,包含SSML脚本编辑、参数配置、任务列表展示和视频播放功能。使用纯HTML/CSS/JS实现,每10秒自动刷新任务状态。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数字人视频生成平台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 36px;
margin-bottom: 10px;
}
.header p {
font-size: 16px;
opacity: 0.9;
}
.card {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.card-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
border-left: 4px solid #667eea;
padding-left: 12px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: 'Courier New', monospace;
resize: vertical;
transition: border-color 0.3s;
}
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.form-group select,
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group select:focus,
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.status-creating {
background: #fff3cd;
color: #856404;
}
.status-waiting,
.status-not_send,
.status-processing {
background: #cce5ff;
color: #004085;
}
.status-finished {
background: #d4edda;
color: #155724;
}
.status-error,
.status-cancel {
background: #f8d7da;
color: #721c24;
}
.task-list {
display: grid;
gap: 15px;
}
.task-item {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.task-id {
font-weight: bold;
color: #667eea;
}
.task-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-top: 10px;
font-size: 14px;
color: #666;
}
.video-preview {
margin-top: 15px;
}
.video-preview video {
width: 100%;
border-radius: 8px;
max-height: 400px;
}
.video-preview a {
display: inline-block;
margin-top: 10px;
padding: 8px 16px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
}
.video-preview a:hover {
background: #764ba2;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
}
.alert {
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-success {
background: #d4edda;
color: #155724;
border-left: 4px solid #28a745;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border-left: 4px solid #dc3545;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 20px;
opacity: 0.3;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎬 数字人视频生成平台</h1>
<p>基于魔珐星云API,快速生成高质量数字人视频</p>
</div>
<div class="card">
<div class="card-title">📝 创建新任务</div>
<div id="message"></div>
<form id="taskForm">
<div class="form-group">
<label for="segment">SSML脚本 (JSON格式) *</label>
<textarea id="segment" rows="10" placeholder='[
{
"text": "大家好,欢迎收看本期节目",
"media_url": "https://example.com/image.png"
}
]'>[
{
"text": "嘿。新入职的同学们,大家好!我是你们的行政主管文捷。各位放大镜准备好了吗! 我们的行政服务指南来咯~ \"私人侦探\"帮你找到隐藏的服务指南宝藏。让办事变得像寻宝一样轻松有趣~ 让我们一起解锁那些,省时省力的行政服务秘籍吧!",
"media_url": "https://media.xingyun3d.com/xingyun3d/general/videoscript/image/wupinzujieshuoming/279681_980d79a3d8414301a87a45.png"
},
{
"text": "公司可领物品有:签字笔,书写本,白板笔,橡皮擦,固体胶,工牌配件换新等日常办公用品。领用地点:17楼前台。",
"media_url": "https://media.xingyun3d.com/xingyun3d/general/videoscript/image/wupinzujieshuoming/279681_cecb2d7c56064dacbe4e51.png"
},
{
"text": "各层茶水间提供自助售货机,可按需购买零食或饮品 。",
"media_url": ""
}
]</textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="video_name">视频名称</label>
<input type="text" id="video_name" placeholder="可选,默认自动生成">
</div>
<div class="form-group">
<label for="output_resolution">视频清晰度</label>
<select id="output_resolution">
<option value="540P">540P</option>
<option value="720P" selected>720P</option>
<option value="1080P">1080P</option>
<option value="2K">2K</option>
<option value="4K">4K</option>
</select>
</div>
<div class="form-group">
<label for="sub_title">字幕</label>
<select id="sub_title">
<option value="on" selected>开启</option>
<option value="off">关闭</option>
</select>
</div>
<div class="form-group">
<label for="if_aigc_mark">AI标识</label>
<select id="if_aigc_mark">
<option value="true" selected>显示</option>
<option value="false">不显示</option>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary" id="submitBtn">
🚀 创建任务
</button>
</form>
</div>
<div class="card">
<div class="card-title">📋 任务列表</div>
<div id="taskList">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
<p>暂无任务,创建一个吧!</p>
</div>
</div>
</div>
</div>
<script>
// 创建任务
document.getElementById('taskForm').addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '创建中...';
const data = {
segment: document.getElementById('segment').value,
video_name: document.getElementById('video_name').value,
output_resolution: document.getElementById('output_resolution').value,
sub_title: document.getElementById('sub_title').value,
if_aigc_mark: document.getElementById('if_aigc_mark').value === 'true'
};
try {
const response = await fetch('/api/create_task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showMessage('success', `任务创建成功!任务ID: ${result.task_id}`);
loadTasks();
} else {
showMessage('error', result.message);
}
} catch (error) {
showMessage('error', '创建失败: ' + error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '🚀 创建任务';
}
});
// 显示消息
function showMessage(type, message) {
const messageDiv = document.getElementById('message');
messageDiv.innerHTML = `<div class="alert alert-${type}">${message}</div>`;
setTimeout(() => messageDiv.innerHTML = '', 5000);
}
// 加载任务列表
async function loadTasks() {
try {
const response = await fetch('/api/tasks');
const result = await response.json();
if (result.success && result.tasks.length > 0) {
renderTasks(result.tasks);
}
} catch (error) {
console.error('加载任务失败:', error);
}
}
// 渲染任务列表
function renderTasks(tasks) {
const taskList = document.getElementById('taskList');
taskList.innerHTML = tasks.map(task => `
<div class="task-item">
<div class="task-header">
<span class="task-id">任务 #${task.task_id}</span>
<span class="status-badge status-${task.status}">${getStatusText(task.status)}</span>
</div>
<div class="task-info">
<div>创建时间: ${new Date(task.created_at * 1000).toLocaleString('zh-CN')}</div>
${task.amount ? `<div>积分消耗: ${task.amount}</div>` : ''}
${task.synth_start_time ? `<div>开始时间: ${task.synth_start_time}</div>` : ''}
${task.synth_finish_time ? `<div>完成时间: ${task.synth_finish_time}</div>` : ''}
</div>
${task.error ? `<div style="color: #dc3545; margin-top: 10px;">错误: ${task.error}</div>` : ''}
${task.video_url ? ` <div class="video-preview"> <video controls> <source src="${task.video_url}" type="video/mp4"> </video> <a href="${task.video_url}" target="_blank">📥 下载视频</a> ${task.image_url ? `<a href="${task.image_url}" target="_blank" style="margin-left: 10px;">🖼️ 预览图</a>` : ''} </div> ` : ''}
</div>
`).join('');
}
// 获取状态文本
function getStatusText(status) {
const statusMap = {
'creating': '创建中',
'not_send': '排队中',
'waiting': '处理中',
'processing': '生成中',
'finished': '已完成',
'error': '失败',
'cancel': '已取消'
};
return statusMap[status] || status;
}
// 初始加载
loadTasks();
// 每10秒刷新一次任务状态
setInterval(loadTasks, 10000);
</script>
</body>
</html>
3.5 requirements.txt - 依赖清单
这个文件定义了项目所需的Python依赖包。运行 pip install -r requirements.txt 即可自动安装所有依赖。
bash
# 安装依赖
pip install -r requirements.txt
# 或者单独安装
pip install requests>=2.28.0 flask>=2.3.0
3.6 tasks.json - 任务数据文件
这个文件由系统自动生成和更新,用于持久化存储任务数据。服务重启时会自动加载此文件恢复任务状态。
字段说明:
- task_id:任务唯一标识
- status:任务状态(creating/waiting/processing/finished/error)
- created_at:创建时间(Unix时间戳)
- video_url:视频下载链接
- image_url:预览图链接
- amount:积分消耗
注意:此文件在首次创建任务后自动生成,无需手动编辑。
json
{
"5759": {
"task_id": 5759,
"status": "finished",
"created_at": 1782194768.4081519,
"video_url": "https://media.youyan.xyz/youyan/xihetask3.0/522880/826863/2026-06-23/4c3471abf2f44309a0589b9748482f8a.mp4",
"image_url": "https://media.youyan.xyz/youyan/xihetask3.0/522880/826863/2026-06-23/4c3471abf2f44309a0589b9748482f8a.jpg",
"error": null,
"raw_data": {
"task_id": 5759,
"synth_state": "finished",
"render_image_oss": "https://media.youyan.xyz/youyan/xihetask3.0/522880/826863/2026-06-23/4c3471abf2f44309a0589b9748482f8a.jpg",
"render_video_oss": "https://media.youyan.xyz/youyan/xihetask3.0/522880/826863/2026-06-23/4c3471abf2f44309a0589b9748482f8a.mp4",
"synth_start_time": "2026-06-23T06:06:39.543082Z",
"amount": 56.0,
"synth_finish_time": "2026-06-23T06:09:41.680675Z"
},
"amount": 56.0,
"synth_start_time": "2026-06-23T06:06:39.543082Z",
"synth_finish_time": "2026-06-23T06:09:41.680675Z"
}
}
四、效果展示
4.1 启动Web服务
在项目目录执行启动命令后,访问 http://localhost:5000 即可看到数字人视频生成平台首页。页面包含SSML脚本编辑区、参数配置区和任务列表区。

4.2 创建视频任务
填写SSML脚本(或使用预填的行政服务指南示例),选择视频清晰度、字幕等参数后,点击"创建任务"按钮。系统会立即在任务列表中显示新任务,状态为"创建中"或"处理中"。

4.3 等待视频生成
任务提交后,系统会在后台轮询魔珐星云API的生成进度。此时可在魔珐星云控制台查看视频生成状态,通常需要等待3-5分钟。任务状态会依次显示:排队中 → 处理中 → 生成中。

4.4 视频生成完成
当任务状态变为"已完成"时,页面会自动显示视频播放器,可直接在线播放生成的好的3D数字人视频。同时提供视频下载和预览图下载按钮。

4.5 视频效果预览
生成的数字人视频质量清晰,口型与语音同步,形象动作自然。支持540P至4K多种清晰度选择,满足不同场景需求。


行政服务助手数字人
五、总结
这次我使用Qoder AI Coding工具,结合魔珐星云的参数流API,从注册到生成第一个行政服务指南视频,全程不到2小时。
依托自研参数流架构 + AI 端渲和解算架构创新,云端仅下发轻量化驱动参数,终端完成全流程画面生成,整套链路标准响应为端到端≤500ms,同步实现低延迟、高并发、低成本三大优势。一分半时长的行政讲解脚本仅消耗 56 积分,渲染周期可控,可满足行政宣讲、企业培训、客服导览等场景批量产出标准化讲解视频的需求。
具身智能交互的核心价值不是"替代真人",而是让专业服务可规模化复制。数字人具备表情、口型、微动作的实时驱动能力,配合SSML脚本精确控制,这才是"具身智能数字人开放平台"的真正意义。
产品还在快速迭代中,小问题存在但不影响核心体验。作为目前最快落地的数字人视频方案之一,魔珐星云值得尝试。
魔珐星云PC端官方链接 :https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=