文本转语音引擎使用问题
===========

#### 问题现象

问题步骤如下：

1.  启动程序进入App的主页。点击播报按钮，文本转语音（textToSpeech，以下简称：tts）实例开始播报。
2.  点击进入详情页面按钮，详情页面的tts实例开始播报。
3.  在详情页面点击返回到主页按钮，详情页面的tts实例在aboutToDisappear()方法中被释放。
4.  返回到主页后再次点击主页的播报按钮，主页的tts实例监听器回调错误信息，日志显示引擎未初始化的异常（1002300007）。

两个页面使用的是不同的tts实例对象，为什么详情页面的tts实例释放后会影响到主页tts实例的播报？引擎初始化代码如下：

private async init() {
  const extraParams: Record<string, Object\> = {
    "style": 'interaction-broadcast',
    "locate": 'CN',
  _  // 可选，设置引擎名称_
    "name": 'EngineName'
  };
  const params: textToSpeech.CreateEngineParams \= {
    language: 'zh-CN',
    online: 1,
    person: 0,
    extraParams: extraParams
  };
  this.\_ttsEngine \= await textToSpeech.createEngine(params);
  this.registerListener(
    () \=\> { this.\_listener?.onStart?.(); },
    () \=\> { this.\_listener?.onStop?.(); },
    () \=\> { this.\_listener?.onCompleted?.(); },
    (code, message) \=\> { this.\_listener?.onError?.(code, message); }
  );
}

释放引擎代码如下：

release() {
 _ // 释放资源_
  this.unregisterListener();
  this.\_ttsEngine?.shutdown();
  this.\_ttsEngine \= null;
}

#### 背景知识

文本转语音类，用于执行文本转语音过程中的相关操作。在调用[TextToSpeechEngine](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#section185526396124)的方法前，需要先通过[createEngine()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#section53411946183318)方法创建一个TextToSpeechEngine实例。结束使用后，可以调用[shutdown()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#section444415855615)方法关闭引擎，释放引擎资源。

#### 解决方案

该问题是两个页面引擎名称相同导致的，可以在引擎参数的name属性中使用UUID，或是其他命名方式，只需保证引擎名称不重复即可，此时第二个页面使用this.textToSpeech.release()就会只释放当前页面的引擎资源且不影响第一个页面。创建引擎参数修改参考如下：

_//_ _初始化引擎_
async init() {
  _// 1. 生成唯一 UUID，确保每个页面的引擎名称不重复_
  let uuid \= util.generateRandomUUID(true);

  const extraParams: Record<string, Object\> = {
    "style": 'interaction-broadcast',
    "locate": 'CN',
  _  // 【关键点】设置唯一的引擎名称，避免多实例冲突_
    "name": \`Engine-${this.\_pageName}\-${uuid}\`
  };

  const params: textToSpeech.CreateEngineParams \= {
    language: 'zh-CN',
    online: 1,
    person: 0,
    extraParams: extraParams
  };

  try {
   _ // 创建引擎_
    this.\_ttsEngine \= await textToSpeech.createEngine(params);
    console.info(\`\[${this.\_pageName}\] TTS 引擎初始化成功, Name: ${extraParams.name}\`);
  } catch (err) {
    let error \= err as BusinessError;
    console.error(\`\[${this.\_pageName}\] TTS 初始化失败: ${error.code}, ${error.message}\`);
  }
}

完整示例参考如下：

import { textToSpeech } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS'; _// 用于生成UUID_

class TtsWrapper {
  private \_ttsEngine: textToSpeech.TextToSpeechEngine | null \= null;
  private \_pageName: string \= '';

  constructor(pageName: string) {
    this.\_pageName \= pageName;
  }

  _// 初始化引擎_
  async init() {
    _// 1._ _生成唯一 UUID，确保每个页面的引擎名称不重复_
    let uuid \= util.generateRandomUUID(true);

    const extraParams: Record<string, Object\> = {
      "style": 'interaction-broadcast',
      "locate": 'CN',
      _// 【关键点】设置唯一的引擎名称，避免多实例冲突_
      "name": \`Engine-${this.\_pageName}\-${uuid}\`
    };

    const params: textToSpeech.CreateEngineParams \= {
      language: 'zh-CN',
      online: 1,
      person: 0,
      extraParams: extraParams
    };

    try {
   _   // 创建引擎_
      this.\_ttsEngine \= await textToSpeech.createEngine(params);
      console.info(\`\[${this.\_pageName}\] TTS 引擎初始化成功, Name: ${extraParams.name}\`);
    } catch (err) {
      let error \= err as BusinessError;
      console.error(\`\[${this.\_pageName}\] TTS 初始化失败: ${error.code}, ${error.message}\`);
    }
  }

 _ // 播报文本_
  speak(text: string, ctx: UIContext) {
    if (!this.\_ttsEngine) {
      console.error(\`\[${this.\_pageName}\] 引擎为空\`);
      return;
    }

    let speakParams: textToSpeech.SpeakParams \= {
      requestId: util.generateRandomUUID(true),
      extraParams: {
        "speed": 1.0,
        "volume": 1.0,
        "pitch": 1.0,
        "audioType": 1
      }
    };

    this.\_ttsEngine.speak(text, speakParams);
    try {
      ctx.getPromptAction().showToast({ message: \`\[${this.\_pageName}\] 正在播报...\` });
    } catch (error) {
      console.error(\`显示提示失败: ${error.code}, ${error.message}\`);
    }
  }

  _// 释放资源_
  release() {
    if (this.\_ttsEngine) {
      this.\_ttsEngine.shutdown();
      this.\_ttsEngine \= null;
      console.info(\`\[${this.\_pageName}\] TTS 引擎已释放\`);
    }
  }
}

@Entry
@Component
struct Index {
 _ // 路由栈_
  @Provide('pageInfos') pageInfos: NavPathStack \= new NavPathStack();

 _ // 主页面的 TTS 实例_
  private homeTts: TtsWrapper \= new TtsWrapper('HomePage');

  async aboutToAppear() {
   _ // 初始化主页 TTS_
    await this.homeTts.init();
  }

  @Builder
  PageMap(name: string) {
    if (name \=== 'DetailPage') {
      DetailComponent();
    }
  }

  build() {
    Navigation(this.pageInfos) {
      Column({ space: 20 }) {
        Text('TTS 多实例冲突解决演示')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 20 })

      _  // 1. 主页播报测试_
        Button('1. 主页播报 (Home Speak)')
          .width('80%')
          .onClick(() \=\> {
            this.homeTts.speak('这是主页的语音播报，引擎正常工作。', this.getUIContext());
          })

        _// 2. 跳转到详情页_
        Button('2. 进入详情页 (Go to Detail)')
          .width('80%')
          .backgroundColor(Color.Orange)
          .onClick(() \=\> {
            this.pageInfos.pushPath({ name: 'DetailPage' });
          })
      }
      .width('100%')
      .height('100%')
    }
    .mode(NavigationMode.Stack)
    .navDestination(this.PageMap)
    .title('Home Page')
  }
}

@Component
struct DetailComponent {
  @Consume('pageInfos') pageInfos: NavPathStack;
 _ // 详情页的 TTS 实例_
  private detailTts: TtsWrapper \= new TtsWrapper('DetailPage');

  async aboutToAppear() {
   _ // 初始化详情页 TTS_
    await this.detailTts.init();
  }

 _ // 【关键】页面销毁时释放资源_
  aboutToDisappear() {
    console.info('详情页销毁，释放详情页 TTS 资源');
    this.detailTts.release();
  }

  build() {
    NavDestination() {
      Column({ space: 20 }) {
        Text('详情页面')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 50 })

        Button('3. 详情页播报 (Detail Speak)')
          .width('80%')
          .onClick(() \=\> {
            this.detailTts.speak('这是详情页的播报。返回后，我将被销毁。', this.getUIContext());
          })

        Button('返回主页 (Back)')
          .width('80%')
          .backgroundColor(Color.Gray)
          .onClick(() \=\> {
            this.pageInfos.pop();
          })
      }
      .width('100%')
      .height('100%')
    }
    .title('Detail Page')
  }
}

#### 常见FAQ

Q：文本转语音组件在文字内容未完全播放完成时，会执行onComplete回调。

A：合成或播报结束后，[onComplete](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#section2023291912560)监听都会触发，可以确认此时的[CompleteResponse](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#section26265304523)的type值（0：合成结束，1：播报结束）。

Q：文本转语音引擎在调用speak()方法开始说话时就回调了onComplete，导致回调状态错乱。

A：speak()方法开始会有个合成语音的动作，合成或播报结束后分别回调[onComplete](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#section2023291912560)接口，可以根据返回type值（0：合成结束，1：播报结束）的不同进行接下来不同逻辑处理的区分。

Q：文本转语音怎么实现暂停和继续播报？

A：当需要停止合成及停止播报时，可调用ttsEngine.stop()方法，再次开始使用ttsEngine.speak()方法。若要实现暂停后继续播放，有两种方式：

1.  speak()获取的是音频流，根据音频流做对应的估算。
2.  可以参考[Speech Kit](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/speech-api)(场景化语音服务)，该服务提供一套基于[Core Speech Kit](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-speech-api)(基础语音服务)的播控UI组件，可满足暂停后继续播放的需求。

Q：文本转语音如何实现后台播报？

A：文本转语音当前支持后台播报，在[CreateEngineParams](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#section1638144844811)参数中的extraParams字段，设置'isBackStage': true时，tts支持后台播报。

Q：文本转语音时，文本过长时如何顺畅，有序播放？

A：tts是一边播一边合成的，文本过长的情况，可以先分割文本，建议以逗号、句号或者换行符为分隔符，用分割后的文本分次合成。因为onData模式不能保证数据按序到达，需要对收到的数据，根据数据内容进行排序，然后合成。