1、魔珐星云官网

https://www.xingyun3d.com/
进入官网后点击登陆注册
在这里插入图片描述

2、输入邀请码

XDKAT457YU
输入邀请码会赠送1000积分使用

3、创建应用

在这里插入图片描述

4、任意选择一个,然后保存

在这里插入图片描述

5、保存APP信息

点击接入SDK
保存这两个信息
在这里插入图片描述

6、代码中接入SDK

下面这个代码可以集成在自己网页中需要的位置,也可以单独运行,是一个简单的Demo。
在代码的258行和259行“配置区:换成你的应用凭证”,填入上一步保存的凭证即可。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>魔珐星云 · 数字人播报控制台</title>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
  :root{
    --ink:#0b1417; --panel:#111e23; --panel2:#0e191d;
    --line:rgba(110,175,185,.16); --line-strong:rgba(110,175,185,.32);
    --amber:#ffb454; --cyan:#4dd6c1; --green:#5ad68d; --red:#ff6b6b;
    --text:#d9e7ea; --dim:#7e959d;
    --display:'Chakra Petch',sans-serif;
    --body:'Noto Sans SC',system-ui,sans-serif;
  }
  *{margin:0;padding:0;box-sizing:border-box}
  body{
    font-family:var(--body); color:var(--text); min-height:100vh;
    background:
      radial-gradient(900px 600px at 8% -10%, rgba(255,180,84,.07), transparent 60%),
      radial-gradient(1000px 700px at 105% 110%, rgba(77,214,193,.08), transparent 60%),
      linear-gradient(rgba(110,175,185,.045) 1px, transparent 1px),
      linear-gradient(90deg, rgba(110,175,185,.045) 1px, transparent 1px),
      var(--ink);
    background-size:auto,auto,44px 44px,44px 44px,auto;
  }
  /* ── 顶栏 ─────────────────────────── */
  header{
    display:flex;align-items:center;justify-content:space-between;
    padding:14px 28px;border-bottom:1px solid var(--line);
    background:rgba(14,25,29,.7);backdrop-filter:blur(6px);
    position:sticky;top:0;z-index:10;
  }
  .brand{display:flex;align-items:center;gap:12px}
  .brand .dot{width:10px;height:10px;background:var(--green);border-radius:50%;
    box-shadow:0 0 10px var(--green);animation:pulse 2s infinite}
  .brand h1{font-family:var(--display);font-size:17px;letter-spacing:.12em;font-weight:600}
  .brand h1 em{font-style:normal;color:var(--amber)}
  .brand small{display:block;font-size:11px;color:var(--dim);letter-spacing:.28em;font-weight:400}
  .top-right{display:flex;align-items:center;gap:18px;font-family:var(--display)}
  #clock{font-size:15px;color:var(--cyan);letter-spacing:.1em}
  .led{display:flex;align-items:center;gap:7px;font-size:11px;letter-spacing:.2em;color:var(--dim)}
  .led i{width:8px;height:8px;border-radius:50%;background:#3a4d52;transition:.3s}
  .led.on i{background:var(--green);box-shadow:0 0 8px var(--green)}
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}

  /* ── 主区布局 ─────────────────────── */
  main{display:grid;grid-template-columns:minmax(320px,440px) minmax(0,1fr);
    gap:26px;padding:26px 28px;max-width:1280px;margin:0 auto;align-items:start}
  @media(max-width:900px){main{grid-template-columns:1fr}}

  /* ── 舞台(数字人监视器) ──────────── */
  .stage-wrap{position:sticky;top:80px}
  @media(max-width:900px){.stage-wrap{position:static}}
  .stage{
    position:relative;aspect-ratio:9/16;max-height:78vh;width:100%;
    background:var(--panel2);border:1px solid var(--line-strong);
    box-shadow:0 24px 60px rgba(0,0,0,.5), inset 0 0 80px rgba(0,0,0,.5);
    overflow:hidden;
  }
  .stage .corner{position:absolute;width:22px;height:22px;border:2px solid var(--amber);z-index:5;transition:.3s}
  .corner.tl{top:8px;left:8px;border-right:0;border-bottom:0}
  .corner.tr{top:8px;right:8px;border-left:0;border-bottom:0}
  .corner.bl{bottom:8px;left:8px;border-right:0;border-top:0}
  .corner.br{bottom:8px;right:8px;border-left:0;border-top:0}
  .stage:hover .corner{width:30px;height:30px}
  .scanlines{position:absolute;inset:0;z-index:4;pointer-events:none;opacity:.5;
    background:repeating-linear-gradient(0deg,rgba(255,255,255,.022) 0 1px,transparent 1px 3px)}
  #sdk{width:100%;height:100%}

  .stage-overlay{
    position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;
    align-items:center;justify-content:center;gap:16px;text-align:center;
    background:rgba(10,18,21,.86);transition:opacity .6s;padding:24px;
  }
  .stage-overlay.hide{opacity:0;pointer-events:none}
  .stage-overlay .big{font-family:var(--display);font-size:22px;letter-spacing:.3em;color:var(--cyan)}
  .stage-overlay .sub{font-size:12px;color:var(--dim);letter-spacing:.15em}
  .pbar{width:70%;height:6px;background:#1a2a30;border:1px solid var(--line);position:relative;overflow:hidden}
  .pbar i{position:absolute;inset:0;width:0%;background:linear-gradient(90deg,var(--cyan),var(--amber));transition:width .3s}
  .pbar::after{content:'';position:absolute;inset:0;
    background:repeating-linear-gradient(45deg,rgba(255,255,255,.12) 0 6px,transparent 6px 12px);
    animation:slide 1s linear infinite}
  @keyframes slide{to{transform:translateX(17px)}}
  #pct{font-family:var(--display);font-size:34px;font-weight:700;color:var(--amber)}

  .rec{position:absolute;top:14px;right:40px;z-index:5;display:none;align-items:center;gap:6px;
    font-family:var(--display);font-size:11px;letter-spacing:.25em;color:var(--red)}
  .rec i{width:9px;height:9px;border-radius:50%;background:var(--red);animation:pulse 1s infinite}
  .rec.on{display:flex}
  .live-tag{position:absolute;bottom:14px;left:40px;z-index:5;font-family:var(--display);
    font-size:10px;letter-spacing:.3em;color:var(--dim)}
  .eq{position:absolute;bottom:14px;right:40px;z-index:5;display:none;align-items:flex-end;gap:3px;height:18px}
  .eq.on{display:flex}
  .eq i{width:4px;background:var(--cyan);animation:eq .7s ease-in-out infinite}
  .eq i:nth-child(1){height:40%;animation-delay:0s}
  .eq i:nth-child(2){height:90%;animation-delay:.15s}
  .eq i:nth-child(3){height:60%;animation-delay:.3s}
  .eq i:nth-child(4){height:100%;animation-delay:.1s}
  .eq i:nth-child(5){height:50%;animation-delay:.25s}
  @keyframes eq{0%,100%{transform:scaleY(.4)}50%{transform:scaleY(1)}}

  /* ── 控制台面板 ───────────────────── */
  .panel{background:var(--panel);border:1px solid var(--line);padding:20px 22px;margin-bottom:20px;
    transition:border-color .3s}
  .panel:hover{border-color:var(--line-strong)}
  .panel h2{font-family:var(--display);font-size:12px;letter-spacing:.3em;color:var(--dim);
    font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:10px}
  .panel h2 b{color:var(--amber);font-weight:700}
  .panel h2::after{content:'';flex:1;height:1px;background:var(--line)}

  textarea{
    width:100%;height:96px;resize:vertical;background:var(--panel2);color:var(--text);
    border:1px solid var(--line);padding:12px 14px;font-family:var(--body);font-size:15px;
    line-height:1.7;transition:border-color .25s, box-shadow .25s;
  }
  textarea:focus{outline:none;border-color:var(--cyan);box-shadow:0 0 0 3px rgba(77,214,193,.12)}
  .ta-foot{display:flex;justify-content:space-between;align-items:center;margin-top:8px}
  #counter{font-family:var(--display);font-size:11px;color:var(--dim);letter-spacing:.1em}
  .hint{font-size:11px;color:var(--dim)}
  .hint kbd{background:#1a2a30;border:1px solid var(--line);padding:1px 6px;font-family:var(--display);font-size:10px}

  .chips{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 18px}
  .chip{background:transparent;border:1px solid var(--line-strong);color:var(--dim);
    padding:6px 14px;font-size:12px;cursor:pointer;font-family:var(--body);
    transition:.2s;letter-spacing:.05em}
  .chip:hover{border-color:var(--amber);color:var(--amber);transform:translateY(-2px);
    box-shadow:0 4px 12px rgba(255,180,84,.15)}

  .btn-row{display:flex;gap:12px;flex-wrap:wrap}
  button.act{
    font-family:var(--display);letter-spacing:.2em;font-size:13px;font-weight:600;
    padding:12px 28px;cursor:pointer;border:1px solid;transition:.2s;background:transparent;
  }
  .btn-speak{background:var(--amber);border-color:var(--amber);color:#1a1206}
  .btn-speak:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 6px 20px rgba(255,180,84,.35)}
  .btn-speak:disabled{opacity:.35;cursor:not-allowed}
  .btn-destroy{border-color:rgba(255,107,107,.5);color:var(--red)}
  .btn-destroy:hover{background:rgba(255,107,107,.1);transform:translateY(-2px)}
  .btn-reload{border-color:rgba(77,214,193,.5);color:var(--cyan);display:none}
  .btn-reload:hover{background:rgba(77,214,193,.1);transform:translateY(-2px)}
  .btn-auto{border-color:rgba(90,214,141,.5);color:var(--green)}
  .btn-auto:hover:not(:disabled){background:rgba(90,214,141,.1);transform:translateY(-2px)}
  .btn-auto:disabled{opacity:.35;cursor:not-allowed}
  .btn-auto.running{background:var(--green);border-color:var(--green);color:#061a0c;animation:pulse 1.4s infinite}
  .auto-prog{margin-top:12px;font-family:var(--display);font-size:12px;color:var(--dim);letter-spacing:.1em}
  .auto-prog b{color:var(--green)}
  button.act:active{transform:translateY(0) scale(.98)}
  .shake{animation:shake .4s}
  @keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}

  .gauges{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px}
  .gauge{background:var(--panel2);border:1px solid var(--line);padding:14px 16px;transition:.25s}
  .gauge:hover{border-color:var(--line-strong);transform:translateY(-2px)}
  .gauge label{display:block;font-size:10px;letter-spacing:.25em;color:var(--dim);margin-bottom:8px}
  .gauge .val{font-family:var(--display);font-size:24px;font-weight:700;color:var(--text);line-height:1}
  .gauge .val small{font-size:11px;color:var(--dim);font-weight:400;margin-left:4px}
  .gauge .val.ok{color:var(--green)} .gauge .val.warn{color:var(--amber)} .gauge .val.bad{color:var(--red)}
  .gauge.speaking .val{color:var(--cyan)}

  #log{
    height:200px;overflow-y:auto;background:var(--panel2);border:1px solid var(--line);
    padding:12px 14px;font-family:ui-monospace,'Courier New',monospace;font-size:12px;line-height:1.9;
  }
  #log::-webkit-scrollbar{width:6px}
  #log::-webkit-scrollbar-thumb{background:#22363d}
  .log-line{animation:login .3s ease-out}
  @keyframes login{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
  .log-line time{color:#4a616a;margin-right:10px}
  .log-info{color:var(--cyan)} .log-ok{color:var(--green)}
  .log-warn{color:var(--amber)} .log-err{color:var(--red)} .log-sys{color:var(--dim)}

  footer{padding:14px 28px 30px;text-align:center;font-size:11px;color:#4a616a;letter-spacing:.1em}
  footer b{color:var(--amber)}
</style>
</head>
<body>

<header>
  <div class="brand">
    <span class="dot"></span>
    <h1>XMOV <em>NEBULA</em><small>魔珐星云 · 数字人播报控制台</small></h1>
  </div>
  <div class="top-right">
    <div class="led" id="connLed"><i></i><span>SESSION</span></div>
    <div id="clock">--:--:--</div>
  </div>
</header>

<main>
  <!-- 左:数字人舞台 -->
  <div class="stage-wrap">
    <div class="stage">
      <span class="corner tl"></span><span class="corner tr"></span>
      <span class="corner bl"></span><span class="corner br"></span>
      <div id="sdk"></div>
      <div class="scanlines"></div>
      <div class="rec" id="rec"><i></i>SPEAKING</div>
      <div class="live-tag">LIVE · XINGYUN3D</div>
      <div class="eq" id="eq"><i></i><i></i><i></i><i></i><i></i></div>
      <div class="stage-overlay" id="overlay">
        <div class="big" id="ovTitle">INITIALIZING</div>
        <div class="sub" id="ovSub">正在连接魔珐星云并下载数字人资源…</div>
        <div id="pct">0%</div>
        <div class="pbar"><i id="pfill"></i></div>
      </div>
    </div>
  </div>

  <!-- 右:控制台 -->
  <div>
    <section class="panel">
      <h2><b>01</b> 播报控制 / SPEAK</h2>
      <textarea id="txt" maxlength="200" placeholder="输入要让数字人说的话…">欢迎使用魔珐星云,我是你的数字人助手。</textarea>
      <div class="ta-foot">
        <span class="hint"><kbd>Ctrl</kbd> + <kbd>Enter</kbd> 快速播报</span>
        <span id="counter">0 / 200</span>
      </div>
      <div class="chips">
        <button class="chip">欢迎使用魔珐星云</button>
        <button class="chip">你好,很高兴为您服务</button>
        <button class="chip">今天天气不错,适合出去走走</button>
        <button class="chip">感谢观看,我们下次再见</button>
      </div>
      <div class="btn-row">
        <button class="act btn-speak" id="btnSpeak" disabled>▶ 立即播报</button>
        <button class="act btn-auto" id="btnAuto" disabled>↻ 自动播报</button>
        <button class="act btn-destroy" id="btnDestroy">■ 断开连接</button>
        <button class="act btn-reload" id="btnReload">↻ 重新连接</button>
      </div>
      <div class="auto-prog" id="autoProg">自动播报:未开始(共100条)</div>
    </section>

    <section class="panel">
      <h2><b>02</b> 运行状态 / STATUS</h2>
      <div class="gauges">
        <div class="gauge"><label>会话状态</label><div class="val" id="gStatus" style="font-size:17px">连接中…</div></div>
        <div class="gauge" id="gVoiceBox"><label>语音状态</label><div class="val" id="gVoice" style="font-size:17px">待机</div></div>
        <div class="gauge"><label>网络延迟</label><div class="val" id="gRtt">--<small>ms</small></div></div>
        <div class="gauge"><label>下载带宽</label><div class="val" id="gDl">--<small>MB/s</small></div></div>
      </div>
    </section>

    <section class="panel">
      <h2><b>03</b> 系统日志 / LOG</h2>
      <div id="log"></div>
    </section>
  </div>
</main>

<footer>⚠ 安全提示:App Secret 已暴露在源码中,仅适合本地测试 · 正式环境请通过后端代理调用并<b>重新生成密钥</b></footer>

<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
<script>
/* ══════════ 配置区(换密钥改这里) ══════════ */
const CONFIG = {
  appId:     'e345b90e891844f69cea27a6938328ca',
  appSecret: '2d2722746b184e51ba883f66980a9593',
  gatewayServer: 'https://nebula-agent.xingyun3d.com/user/v1/ttsa/session',
};

/* ══════════ 工具 ══════════ */
const $ = id => document.getElementById(id);
const STATUS_MAP = {0:'在线',1:'离线',2:'网络恢复',3:'网络断开',4:'已关闭',5:'隐身',6:'可见',7:'已停止'};
const ERR_MAP = {10001:'容器不存在',10002:'Socket连接错误',10003:'会话启动失败',10004:'会话停止失败',
  20001:'视频抽帧错误',20002:'Worker初始化错误',20003:'视频流处理错误',20004:'表情处理错误',
  30001:'背景图加载错误',30002:'表情数据加载错误',30003:'body数据异常',30004:'视频下载错误',
  40001:'音频解码错误',40002:'表情解码错误',40003:'视频解码错误',40004:'事件解码错误',
  40005:'数据结构错误',40006:'TTSA下行异常',50001:'离线',50002:'在线',50003:'网络重试',50004:'网络断开'};

function log(msg, type='sys'){
  const box = $('log'), line = document.createElement('div');
  line.className = 'log-line log-' + type;
  line.innerHTML = `<time>${new Date().toTimeString().slice(0,8)}</time>${msg}`;
  box.appendChild(line);
  box.scrollTop = box.scrollHeight;
}
let ready = false;
let speaking = false;

/* ══════════ 时钟 ══════════ */
setInterval(() => $('clock').textContent = new Date().toTimeString().slice(0,8), 1000);

/* ══════════ 创建 SDK 实例 ══════════ */
if (typeof XmovAvatar === 'undefined') {
  $('ovTitle').textContent = 'SDK LOAD FAILED';
  $('ovSub').textContent = '无法加载SDK脚本,请检查网络后刷新';
  log('SDK脚本加载失败(可能是网络问题)', 'err');
} else {
  const sdk = new XmovAvatar({
    containerId: '#sdk',
    appId: CONFIG.appId,
    appSecret: CONFIG.appSecret,
    gatewayServer: CONFIG.gatewayServer,
    hardwareAcceleration: 'prefer-hardware',

    onMessage(m){
      const label = ERR_MAP[m.code] ? `(${ERR_MAP[m.code]})` : '';
      const type = m.code >= 40000 ? 'err' : (m.code >= 10000 ? 'warn' : 'info');
      log(`SDK消息 #${m.code}${label}:${m.message}`, type);
    },
    onStateChange(s){ log(`数字人状态 → ${s}`, 'info'); },
    onStatusChange(s){
      const name = STATUS_MAP[s] ?? ('未知(' + s + ')');
      $('gStatus').textContent = name;
      log(`会话状态 → ${name}`, s === 0 || s === 6 ? 'ok' : 'warn');
      $('connLed').classList.toggle('on', s === 0 || s === 2 || s === 6);
    },
    onVoiceStateChange(v){
      speaking = v === 'start';
      $('gVoice').textContent = speaking ? '播报中' : '待机';
      $('gVoiceBox').classList.toggle('speaking', speaking);
      $('rec').classList.toggle('on', speaking);
      $('eq').classList.toggle('on', speaking);
      log(speaking ? '🔊 开始播报' : '播报结束', speaking ? 'ok' : 'sys');
    },
    onNetworkInfo(n){
      const rtt = $('gRtt');
      rtt.innerHTML = n.rtt + '<small>ms</small>';
      rtt.className = 'val ' + (n.rtt < 150 ? 'ok' : n.rtt < 300 ? 'warn' : 'bad');
      $('gDl').innerHTML = n.downlink + '<small>MB/s</small>';
    },
    onStateRenderChange(s, d){ log(`状态 ${s} 首帧渲染耗时 ${d}ms`, 'sys'); },
    onStartSessionWarning(w){ log('配置警告:' + JSON.stringify(w), 'warn'); },
    onWidgetEvent(d){ log('Widget事件:' + JSON.stringify(d), 'sys'); },
    enableLogger: false,
  });

  /* ══════════ 初始化连接 ══════════ */
  log('正在创建会话…', 'info');
  sdk.init({
    initModel: 'normal',
    onDownloadProgress(p){
      $('pfill').style.width = p + '%';
      $('pct').textContent = Math.floor(p) + '%';
      if (p >= 100 && !ready){
        ready = true;
        $('overlay').classList.add('hide');
        $('btnSpeak').disabled = false;
        $('btnAuto').disabled = false;
        log('✅ 资源加载完成,数字人就绪!', 'ok');
      }
    }
  });

  /* ══════════ 交互 ══════════ */
  const txt = $('txt');
  const updateCounter = () => $('counter').textContent = `${txt.value.length} / 200`;
  txt.addEventListener('input', updateCounter); updateCounter();

  function doSpeak(){
    const text = txt.value.trim();
    if (!ready){
      log('数字人还未就绪,请稍候…', 'warn');
      txt.classList.add('shake'); setTimeout(() => txt.classList.remove('shake'), 400);
      return;
    }
    if (!text){ log('播报内容不能为空', 'warn'); return; }
    sdk.speak(text, true, true);
    log(`发送播报:「${text}」`, 'info');
  }
  $('btnSpeak').onclick = doSpeak;
  txt.addEventListener('keydown', e => { if (e.ctrlKey && e.key === 'Enter') doSpeak(); });

  document.querySelectorAll('.chip').forEach(c => c.onclick = () => {
    txt.value = c.textContent; updateCounter(); doSpeak();
  });

  /* ══════════ 自动循环播报(100条不重复) ══════════ */
  const AUTO_TEXTS = (() => {
    const list = [
      '欢迎使用魔珐星云数字人播报系统','今天也是充满希望的一天','愿你被这世界温柔以待',
      '读书是最好的独处方式','保持微笑,好运自然会来','每天进步一点点,就是最大的成功',
      '健康是人生最大的财富','心怀感恩,所遇皆温柔','生活不止眼前的苟且,还有诗和远方',
      '早睡早起,精神百倍','多喝水少熬夜,身体才会好','愿你眼里有光,心中有梦',
      '种一棵树最好的时间是十年前,其次是现在','善良是最高级的智慧','今天的努力,是明天的底气',
      '与其抱怨,不如改变','岁月不居,时节如流','千里之行,始于足下','一日之计在于晨',
      '海纳百川,有容乃大','静以修身,俭以养德','宝剑锋从磨砺出,梅花香自苦寒来',
      '业精于勤,荒于嬉','少壮不努力,老大徒伤悲','欲穷千里目,更上一层楼',
      '会当凌绝顶,一览众山小','长风破浪会有时,直挂云帆济沧海','山重水复疑无路,柳暗花明又一村',
      '不识庐山真面目,只缘身在此山中','春风又绿江南岸,明月何时照我还','科技改变生活,创新引领未来',
      '人工智能正在让世界变得更智能','数字人技术让虚拟与现实更近一步','每一次发声,都是科技的温度',
      '云端渲染让高清数字人触手可及','愿你的每一天都充满阳光','生活明朗,万物可爱',
      '人间值得,未来可期','愿你三冬暖,愿你春不寒','愿你心想事成,万事如意','身体健康,万事顺遂',
      '工作顺利,家庭和睦','笑口常开,好运自然来','心若向阳,无谓悲伤','愿所有的美好都如期而至',
      '人生没有白走的路,每一步都算数','你只管努力,剩下的交给时间','星光不问赶路人,时光不负有心人',
      '越努力,越幸运','做最好的自己,遇见最好的别人',
      '春有百花秋有月,夏有凉风冬有雪','落霞与孤鹜齐飞,秋水共长天一色','接天莲叶无穷碧,映日荷花别样红',
      '两个黄鹂鸣翠柳,一行白鹭上青天','停车坐爱枫林晚,霜叶红于二月花','柴门闻犬吠,风雪夜归人',
      '独在异乡为异客,每逢佳节倍思亲','举头望明月,低头思故乡','海内存知己,天涯若比邻',
      '劝君更尽一杯酒,西出阳关无故人','桃花潭水深千尺,不及汪伦送我情','莫愁前路无知己,天下谁人不识君',
      '天生我材必有用,千金散尽还复来','抽刀断水水更流,举杯消愁愁更愁','两岸猿声啼不住,轻舟已过万重山',
      '飞流直下三千尺,疑是银河落九天','窗含西岭千秋雪,门泊东吴万里船','黄沙百战穿金甲,不破楼兰终不还',
      '醉卧沙场君莫笑,古来征战几人回','羌笛何须怨杨柳,春风不度玉门关','日出江花红胜火,春来江水绿如蓝',
      '野火烧不尽,春风吹又生','同是天涯沦落人,相逢何必曾相识','在天愿作比翼鸟,在地愿为连理枝',
      '天长地久有时尽,此恨绵绵无绝期','曾经沧海难为水,除却巫山不是云','身无彩凤双飞翼,心有灵犀一点通',
      '春蚕到死丝方尽,蜡炬成灰泪始干','采菊东篱下,悠然见南山','问君何能尔,心远地自偏',
      '此中有真意,欲辨已忘言','盛年不重来,一日难再晨','及时当勉励,岁月不待人','羁鸟恋旧林,池鱼思故渊',
      '久在樊笼里,复得返自然','晨兴理荒秽,带月荷锄归','人生无根蒂,飘如陌上尘','落地为兄弟,何必骨肉亲',
      '得欢当作乐,斗酒聚比邻','奇文共欣赏,疑义相与析','结庐在人境,而无车马喧',
      '问渠那得清如许,为有源头活水来','等闲识得东风面,万紫千红总是春','纸上得来终觉浅,绝知此事要躬行',
      '山外青山楼外楼,西湖歌舞几时休','暖风熏得游人醉,直把杭州作汴州','粉骨碎身浑不怕,要留清白在人间',
      '千锤万凿出深山,烈火焚烧若等闲','咬定青山不放松,立根原在破岩中','竹外桃花三两枝,春江水暖鸭先知',
    ];
    const uniq = [...new Set(list)];
    while (uniq.length < 100) uniq.push(`这是第 ${uniq.length+1} 条播报内容,保持节奏,继续前行`);
    return uniq.slice(0, 100);
  })();

  let autoRunning = false, autoIdx = 0;
  const btnAuto = $('btnAuto'), autoProg = $('autoProg');
  const wait = ms => new Promise(r => setTimeout(r, ms));
  function waitUntil(fn, timeout=60000){
    return new Promise(res => {
      const start = Date.now();
      const t = setInterval(() => {
        if (fn()){ clearInterval(t); res(true); }
        else if (Date.now() - start > timeout){ clearInterval(t); res(false); }
      }, 100);
    });
  }
  async function startAuto(){
    if (!ready || autoRunning) return;
    autoRunning = true; autoIdx = 0;
    btnAuto.classList.add('running');
    btnAuto.textContent = '■ 停止自动';
    btnSpeak.disabled = true;
    log('开始自动循环播报(共100条)', 'info');
    while (autoRunning && autoIdx < AUTO_TEXTS.length){
      const text = AUTO_TEXTS[autoIdx];
      await waitUntil(() => !speaking, 30000);   // 等空闲
      if (!autoRunning) break;
      try { sdk.speak(text, true, true); }
      catch(e){ log('speak 异常:' + e.message, 'err'); break; }
      autoProg.innerHTML = `自动播报:<b>${autoIdx+1} / 100</b>`;
      log(`自动 ${autoIdx+1}/100:${text}`, 'info');
      autoIdx++;
      await waitUntil(() => speaking, 15000);    // 等开始说话
      await waitUntil(() => !speaking, 60000);   // 等说完
      await wait(500);                            // 句间间隔
    }
    stopAuto(autoIdx >= AUTO_TEXTS.length);
  }
  function stopAuto(done){
    autoRunning = false;
    btnAuto.classList.remove('running');
    btnAuto.textContent = '↻ 自动播报';
    if (ready) btnSpeak.disabled = false;
    autoProg.innerHTML = done
      ? `✅ 已播报完 100`
      : `已停止(共播报 ${autoIdx} 条)`;
    log(done ? '✅ 100 条全部播报完毕' : '已停止自动播报', done ? 'ok' : 'warn');
  }
  btnAuto.onclick = () => autoRunning ? stopAuto(false) : startAuto();

  $('btnDestroy').onclick = () => {
    if (autoRunning) stopAuto(false);
    sdk.destroy();
    ready = false;
    $('btnSpeak').disabled = true;
    $('btnAuto').disabled = true;
    $('connLed').classList.remove('on');
    $('gStatus').textContent = '已停止';
    $('ovTitle').textContent = 'SESSION CLOSED';
    $('ovSub').textContent = '会话已断开,可重新连接';
    $('pct').textContent = '--';
    $('overlay').classList.remove('hide');
    $('btnReload').style.display = 'inline-block';
    log('已销毁实例,断开连接', 'warn');
  };
  $('btnReload').onclick = () => location.reload();
}
</script>
</body>
</html>

总结

上述是完整的Demo使用步骤,需要进阶的更多玩法,可以看一下官方手册。
https://www.xingyun3d.com/developers/52-183

Logo

一座年轻的奋斗人之城,一个温馨的开发者之家。在这里,代码改变人生,开发创造未来!

更多推荐