基于魔珐星云 3D 数字人能力:依托 Qoder 开发行政服务助手及视频生成系统

一、摘要

具身智能正在重塑人机交互的边界,从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凭证

进入应用设置页面,复制 AppIDAppSecret,用于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=