重生之我在这个世界的文本转音频API工程师的故事

简介: 重生之我在这个世界的文本转音频API工程师的故事

前言


在一个安静而又普通的午后,我坐在电脑前,思索着如何将一个看似遥不可及的愿望化为现实。那个愿望,是一个来自虚拟世界的幻想,一个关于“重生”的故事。


每个人都曾幻想过如果能重新来过会怎么样,纠正生命中的种种错误,抓住逝去的时光。但对于我,这个愿望似乎不再是仅仅停留在幻想中的奢望。作为一名文本转音频API工程师,我一直探索着将文字变成声音的可能性,将想象力融入现实。而这一切的开始,源自于一个神秘而神奇的机会。


我要讲述的是一个充满创意和技术的故事,一个在虚拟和现实之间穿梭的旅程。这是一个关于重生、创造力和坚持不懈的故事,一个我在这个世界中的探索之旅。


故事的主人公是我自己,一个普通的工程师,但这个故事也代表了许多人内心深处的渴望。随着故事的展开,我们将共同经历激动人心的时刻、挑战和成功,一起探索技术的奇妙,以及如何将一个虚拟世界的梦想转化为现实。


请跟随我,一同踏上这段充满未知的旅程,去探索那个无法触及的重生之梦,以及如何将文字转化为声音的神奇过程。这是我在这个世界的故事,也是你我共同的冒险。


故此《从零玩转系列之微信支付UNIAPP》文章当中的功能需要支付成功后提示用户支付成功, 并且提示语说动态变更的那么我就想到了 文本转音频 ,这里呢我就介绍使用讯飞的来玩玩!





配置


首先进入 讯飞官方网站 注册、配置信息

创建我的应用、一个只能创建一个





语音合成


可以看到 服务量、接口认证信息、在线语音合成API

每天可以使用 500次的服务量 晚上12点重置 良心~

⚠️二维码可别泄漏咯会扣除真实的服务次数




文档


点击在线语音合成API 旁边的文档按钮




接口要求


集成在线语音合成流式API时,需按照以下要求。

内容

说明

请求协议

wss(为提高安全性,强烈推荐wss)

请求地址

wss: //tts-api.xfyun.cn/v2/tts

请求行

GET /v2/tts HTTP/1.1

接口鉴权

签名机制,详情请参照下方接口鉴权

字符编码

UTF8、GB2312、GBK、BIG5、UNICODE、GB18030

响应格式

统一采用JSON格式

开发语言

任意,只要可以向讯飞云服务发起Websocket请求的均可

操作系统

任意

音频属性

采样率16k或8k

音频格式

pcm、mp3、speex(8k)、speex-wb(16k)

文本长度

单次调用长度需小于8000字节(约2000汉字)

发音人

中英粤多语种、川豫多方言、小语种、男女声多风格,可以在 这里 在线体验发音人效果



接口调用流程


  • 通过接口密钥基于hmac-sha256计算签名,向服务器端发送Websocket协议握手请求。详见下方 接口鉴权
  • 握手成功后,客户端通过Websocket连接同时上传和接收数据。数据上传完毕,客户端需要上传一次数据结束标识。详见下方 接口数据传输与接收
  • 接收到服务器端的结果全部返回标识后断开Websocket连接。

注: Websocket使用注意事项如下

  1. 服务端支持的websocket-version 为13,请确保客户端使用的框架支持该版本。
  2. 服务端返回的所有的帧类型均为TextMessage,对应于原生websocket的协议帧中opcode=1,请确保客户端解析到的帧类型一定为该类型,如果不是,请尝试升级客户端框架版本,或者更换技术框架。
  3. 如果出现分帧问题,即一个json数据包分多帧返回给了客户端,导致客户端解析json失败。出现这种问题大部分情况是客户端的框架对websocket协议解析存在问题,如果出现请先尝试升级框架版本,或者更换技术框架。
  4. 客户端会话结束后如果需要关闭连接,尽量保证传给服务端的错误码为websocket错误码1000(如果客户端框架没有提供关闭时传错误码的接口。则无需关注本条)





下载Demo


看看咋玩的

调用示例

注: demo只是一个简单的调用示例,不适合直接放在复杂多变的生产环境使用

语音合成流式API demo java语言(点我)

我们只是看看流程待会不使用这个方式



打开项目后可以看到使用了 Java-WebSocketokhttp 等依赖这两个是必须的


将认证信息配置全部填好、均到控制台-语音合成页面获取

public static final String appid = " ";
    public static final String apiSecret = " ";
    public static final String apiKey = " ";



修改语音合成文件格式 mp3 默认说 pcm 需要专门的工具播放、我们不需要这玩意.

可以看到 aue 字段 需要传递 lame 参数表示mp3格式




修改aue



修改生成文件格式 mp3




测试

// 合成文本 public static final String TEXT = "欢迎来到讯飞开放平台";

如果需要更改文本则更改此处

点击运行✅可以看到资源文件夹生成了一个mp3音频




重生buff叠满


自己创建一个SpringBoot项目

新增依赖

<!--   Square为Java和Kotlin精心设计的HTTP客户端。-->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.8.1</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
        <!--    100%用Java编写的准系统WebSocket客户端和服务器实现    -->
        <dependency>
            <groupId>org.java-websocket</groupId>
            <artifactId>Java-WebSocket</artifactId>
            <version>1.5.3</version>
        </dependency>



新增认证配置修改 application.yml

xunfei:
  hostUrl: https://tts-api.xfyun.cn/v2/tts
  appid: xxxxxxxxx
  apisecret: xxxxxxxxx
  apikey: xxxxxxxxx



编写工具类,东西和刚刚写的demo一样

package com.yby6.utils;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import lombok.Getter;
import okhttp3.*;
import okio.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
/**
 * 讯飞WebApi语音合成
 *
 * @author Yang Buyi
 * Create By 2023/09/02
 */
@Component
public class XunFeiUtil {
    protected static final Logger log = LoggerFactory.getLogger(XunFeiUtil.class);
    //讯飞四个注入参数,保存在配置文件,便于复用和避免代码上传gitee后泄漏
    private static String hostUrl;
    @Value("${xunfei.hostUrl}")
    public void setHostUrl(String hostUrl) {
        XunFeiUtil.hostUrl = hostUrl;
    }
    private static String appid;
    @Value("${xunfei.appid}")
    public void setAppid(String appid) {
        XunFeiUtil.appid = appid;
    }
    private static String apiSecret;
    @Value("${xunfei.apisecret}")
    public void setApiSecret(String apiSecret) {
        XunFeiUtil.apiSecret = apiSecret;
    }
    private static String apiKey;
    @Value("${xunfei.apikey}")
    public void setApiKey(String apiKey) {
        XunFeiUtil.apiKey = apiKey;
    }
    public static final Gson json = new Gson();
    private static String base64 = "";
    private static volatile boolean lock = true;
    /**
     * 将文本转换为MP3格语音base64文件
     *
     * @param text 要转换的文本(如JSON串)
     * @return 转换后的base64文件
     */
    public static String convertText(String text) throws Exception {
        lock = true;
        base64 = "";
        // 构建鉴权url
        String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret);
        OkHttpClient client = new OkHttpClient.Builder().build();
        //将url中的 schema http://和https://分别替换为ws:// 和 wss://
        String url = authUrl.replace("http://", "ws://").replace("https://", "wss://");
        Request request = new Request.Builder().url(url).build();
        List<byte[]> list = new LinkedList<>();
        WebSocket webSocket = client.newWebSocket(request, new WebSocketListener() {
            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                super.onOpen(webSocket, response);
                log.info("链接开始合成音频:{}",response.body());
                //发送数据
                JsonObject frame = new JsonObject();
                JsonObject business = new JsonObject();
                JsonObject common = new JsonObject();
                JsonObject data = new JsonObject();
                // 填充common
                common.addProperty("app_id", appid);
                //填充business,AUE属性lame是MP3格式,raw是PCM格式
                business.addProperty("aue", "lame");
                business.addProperty("sfl", 1);
                business.addProperty("tte", "UTF8");//小语种必须使用UNICODE编码
                business.addProperty("vcn", "xiaoyan");//到控制台-我的应用-语音合成-添加试用或购买发音人,添加后即显示该发音人参数值,若试用未添加的发音人会报错11200
                business.addProperty("pitch", 50);
                business.addProperty("speed", 50);
                //填充data
                data.addProperty("status", 2);//固定位2
                data.addProperty("text", Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)));
                //使用小语种须使用下面的代码,此处的unicode指的是 utf16小端的编码方式,即"UTF-16LE"”
                //data.addProperty("text", Base64.getEncoder().encodeToString(text.getBytes("UTF-16LE")));
                //填充frame
                frame.add("common", common);
                frame.add("business", business);
                frame.add("data", data);
                webSocket.send(frame.toString());
            }
            @Override
            public void onMessage(WebSocket webSocket, String text) {
                super.onMessage(webSocket, text);
                //处理返回数据
                log.info("开始处理文本合成音频");
                ResponseData resp = null;
                try {
                    resp = json.fromJson(text, ResponseData.class);
                } catch (Exception e) {
                    log.error("异常:", e);
                }
                if (resp != null) {
                    if (resp.getCode() != 0) {
                        log.error("error=>" + resp.getMessage() + " sid=" + resp.getSid());
                        return;
                    }
                    if (resp.getData() != null) {
                        String result = resp.getData().audio;
                        byte[] audio = Base64.getDecoder().decode(result);
                        list.add(audio);
                        // 说明数据全部返回完毕,可以关闭连接,释放资源
                        if (resp.getData().status == 2) {
                            String is = base64Concat(list);
                            base64 = is;
                            lock = false;
                            webSocket.close(1000, "");
                        }
                    }
                }
            }
            @Override
            public void onMessage(WebSocket webSocket, ByteString bytes) {
                super.onMessage(webSocket, bytes);
            }
            @Override
            public void onClosing(WebSocket webSocket, int code, String reason) {
                super.onClosing(webSocket, code, reason);
            }
            @Override
            public void onClosed(WebSocket webSocket, int code, String reason) {
                super.onClosed(webSocket, code, reason);
            }
            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                super.onFailure(webSocket, t, response);
            }
        });
        while (lock) {
        }
        return base64;
    }
    /**
     * * base64拼接
     */
    static String base64Concat(List<byte[]> list) {
        int length = 0;
        for (byte[] b : list) {
            length += b.length;
        }
        int len = 0;
        byte[] retByte = new byte[length];
        for (byte[] b : list) {
            retByte = concat(len, retByte, b);
            len += b.length;
        }
        return cn.hutool.core.codec.Base64.encode(retByte);
    }
    static byte[] concat(int len, byte[] a, byte[] b) {
        for (int i = 0; i < b.length; i++) {
            a[len] = b[i];
            len++;
        }
        return a;
    }
    /**
     * * 获取权限地址
     * *
     * * @param hostUrl
     * * @param apiKey
     * * @param apiSecret
     * * @return
     */
    private static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception {
        URL url = new URL(hostUrl);
        SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        format.setTimeZone(TimeZone.getTimeZone("GMT"));
        String date = format.format(new Date());
        StringBuilder builder = new StringBuilder("host: ").append(url.getHost()).append("\n").
                append("date: ").append(date).append("\n").
                append("GET ").append(url.getPath()).append(" HTTP/1.1");
        Charset charset = StandardCharsets.UTF_8;
        Mac mac = Mac.getInstance("hmacsha256");
        SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(charset), "hmacsha256");
        mac.init(spec);
        byte[] hexDigits = mac.doFinal(builder.toString().getBytes(charset));
        String sha = Base64.getEncoder().encodeToString(hexDigits);
        String authorization = String.format("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha);
        HttpUrl httpUrl = HttpUrl.parse("https://" + url.getHost() + url.getPath()).newBuilder().
                addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(charset))).
                addQueryParameter("date", date).
                addQueryParameter("host", url.getHost()).
                build();
        return httpUrl.toString();
    }
    @Getter
    public static class ResponseData {
        private int code;
        private String message;
        private String sid;
        private Data data;
    }
    private static class Data {
        //标志音频是否返回结束  status=1,表示后续还有音频返回,status=2表示所有的音频已经返回
        private int status;
        //返回的音频,base64 编码
        private String audio;
        // 合成进度
        private String ced;
    }
}


创建 TextToAudioController

package com.yby6.controller;
import cn.hutool.core.lang.UUID;
import com.yby6.reponse.R;
import com.yby6.utils.MinIoUtil;
import com.yby6.utils.XunFeiUtil;
import io.minio.ObjectWriteResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.util.Base64;
/**
 * 语音合成
 *
 * @author Yang Buyi
 * Create By 2023/09/02
 */
@RequestMapping("/xunfei")
@RestController
@RequiredArgsConstructor
public class TextToAudioController {
    private static final Logger log = LoggerFactory.getLogger(TextToAudioController.class);
    /**
     * 生成语音返回流
     */
    @PostMapping(value = "textToAudio")
    public void textToAudio(@RequestParam String text, HttpServletResponse response) throws IOException {
        if (StringUtils.isNotBlank(text)) {
            //过滤图片,h5标签
            final byte[] audioByte = getAudioByte(text);
            response.setContentType("application/octet-stream;charset=UTF-8");
            OutputStream os = new BufferedOutputStream(response.getOutputStream());
            try {
                //音频流
                os.write(audioByte);
            } catch (IOException e) {
                log.error("音频数据异常", e);
            } finally {
                os.flush();
                os.close();
            }
        }
    }
    /**
     * 获取讯飞音频流
     * @return {@link byte[]}
     */
    private static byte[] getAudioByte(String text) {
        text = text.replaceAll("\\&[a-zA-Z]{1,10};", "").replaceAll("<[^>]*>", "").replaceAll("[(/>)<]", "").trim();
        //调用微服务接口获取音频base64
        String result = "";
        try {
            result = XunFeiUtil.convertText(text);
        } catch (Exception e) {
            log.error("【文字转语音接口调用异常】", e);
        }
        // 音频数据
        return Base64.getDecoder().decode(result);
    }
}


以上代码演示了如何在Spring Boot应用程序中使用XunFeiUtil工具类来将文本转换为语音,并且返回了音频流到前端




重生的画面


我这里就使用从零玩转系列之微信支付的工程前端来发送请求测试

新增语音合成API

import request from '@/utils/request';
export function textToAudio(params) {
  return request({
    url: '/xunfei/textToAudio',
    method: 'post',
    data: params,
    responseType: "blob"//后台返回的为语音的流数据
  });
}





⚠️ 响应拦截器处理





页面编写

<template>
  <div class="app-container">
    <h1>文本转语音Demo</h1>
    <div style="width: 600px;">
      <el-input
        type="textarea"
        :autosize="{minRows:3,maxRows:5}"
        placeholder="请输入内容"
        v-model="textArea">
      </el-input>
      <el-badge class="item" style="margin-right: 12px" v-loading="audioLoading">
        <el-button v-if="!audioPlay" style="margin: 10px 10px;"
                   @click="getAudio(textArea)">转换</el-button>
        <el-button v-if="audioPlay"  style="margin: 10px 10px;"
                   @click="audioPause">暂停播放</el-button>
        <el-button @click="reload">重新播放</el-button>
      </el-badge>
    </div>
  </div>
</template>
<script>
import {textToAudio} from '@/api/audio'
export default {
  name: "Audio",
  props: {},
  components: {},
  data() {
    return {
      text: '',
      //文件组件
      textArea: '',
      //语音组件
      audioObj: {},
      //转换时loading设置
      audioLoading: false,
      audioPlay: false,
    }
  },
  mounted() {
    this.audioObj = new Audio();//在VUE中使用audio标签
  },
  methods: {
    reload() {
      if (this.audioObj.src) {
        // 将当前时间设置为0(重新开始)
        this.audioObj.currentTime = 0;
        // 播放音频
        this.audioObj.play();
      }
    },
    //调用后台讯飞语音转换
    getAudio(text) {
      if (this.text === text && this.audioObj.src) {
        //已有声音直接播放
        this.audioObj.play()
      } else {
        //判断输入框内容是否改变,如果是则重新发请求
        this.text = text;
        if (text) {
          this.audioLoading = true
          let formData = new FormData()
          formData.append('text', text)
          textToAudio(formData).then(response => {
            let url = URL.createObjectURL(response)//通过这个API让语音数据转为成一个url地址
            console.log(url);
            this.audioObj.src = url//设置audio的src为上面生成的url
            let playPromiser = this.audioObj.play()//进行播放
            //在谷歌内核中,audio.play()会返回一个promise的值,在IE内核中就不会返回任何的值
            //所以如果你要分浏览器,可以判断playPromiser的值来进行操作哦
            this.audioObj.onended = () => {
            }
            this.audioLoading = false
          }).catch(err => {
            console.log(err);
          })
          this.audioPlay = true
        }
      }
    },
    // 播放暂停
    audioPause() {
      this.audioObj.pause()
      this.audioPlay = false
    }
  }
}
</script>
<style scoped>
.audio {
  width: 90%;
  position: absolute;
  top: 20px;
  left: 20px;
  font-size: 26px;
}
</style>




页面代码讲解


当调用getAudio方法时,会执行以下步骤:


  1. 首先,方法会检查当前文本(text)是否等于之前已经转换为音频并正在播放的文本。如果是,说明已经有对应的音频文件在播放,因此直接调用this.audioObj.play()来播放该音频文件。
  2. 如果当前文本不等于之前已经转换为音频并正在播放的文本,说明需要重新发送请求将新的文本转换为语音。方法会将输入的文本赋值给this.text,并通过if (text)条件判断语句进入下一步操作。
  3. 在下一步操作中,方法会创建一个FormData对象,并将文本作为参数通过formData.append('text', text)添加到该对象中。
  4. 然后,方法会调用textToAudio(formData)函数将文本转换为语音,并返回一个Promise对象。该Promise对象在成功转换语音后会被解析为响应数据,因此可以通过.then()方法访问响应数据。
  5. .then()方法中,首先会创建一个新的URL对象,通过将响应数据作为参数调用URL.createObjectURL(response)。这个URL对象表示转换后的语音数据的URL地址。
  6. 然后,方法会将这个URL地址赋值给this.audioObj.src,从而将音频文件的源设置为转换后的语音数据的URL地址。
  7. 接着,方法会调用this.audioObj.play()尝试播放音频文件。在大多数现代浏览器中,播放音频会返回一个Promise对象,因此可以将播放音频的返回值赋值给playPromiser变量。
  8. 如果音频播放成功,那么playPromiser的值会是Promise { <fulfilled> true },可以在控制台输出该值。如果音频播放失败,那么playPromiser的值会是Promise { <rejected> Error },同样可以在控制台输出该值。
  9. 最后,方法会将this.audioLoading设置为false,表示音频转换和播放已经完成,并且可以通过this.audioObj.onended设置音频播放结束时的处理程序。




如果在转换语音或播放音频时出现错误,那么可以通过.catch()方法捕获错误信息并打印出来。





总结


通过本文,你学会了如何使用Java工具类来实现讯飞WebApi语音合成。这个工具类可以帮助你将文本转换为MP3格式的语音文件,为你的应用程序增加语音合成功能。记得在配置文件中保存讯飞相关的参数,以确保顺利使用这个功能。希望本文对你有所帮助,祝你顺利实现讯飞语音合成功能!

本期结束咱们下次再见👋~ ,关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗

相关文章
|
2月前
|
机器学习/深度学习 人工智能 API
人工智能应用工程师技能提升系列2、——TensorFlow2——keras高级API训练神经网络模型
人工智能应用工程师技能提升系列2、——TensorFlow2——keras高级API训练神经网络模型
34 0
|
2月前
|
小程序 IDE Java
社区每周丨订单中心模板更新及基础API增加音频与动画(5.15-5.19)
社区每周丨订单中心模板更新及基础API增加音频与动画(5.15-5.19)
31 0
|
2月前
|
小程序 IDE Java
社区每周丨订单中心模板更新及基础API增加音频与动画(5.15-5.19)
社区每周丨订单中心模板更新及基础API增加音频与动画(5.15-5.19)
29 0
|
9月前
|
移动开发 JavaScript 前端开发
数据可视化大屏百度地图手机端标注开发实战案例解析(jsAPI接口、标注分类图片、文本标签、分类筛选、自适应高度信息弹窗、PHP后端API)
数据可视化大屏百度地图手机端标注开发实战案例解析(jsAPI接口、标注分类图片、文本标签、分类筛选、自适应高度信息弹窗、PHP后端API)
147 0
|
6月前
|
SQL 存储 API
SAP CRM 系统使用 API 和 open sql 读取订单长文本的两种方式比较
SAP CRM 系统使用 API 和 open sql 读取订单长文本的两种方式比较
43 0
|
10月前
|
小程序 JavaScript API
微信小程序|API音频与视频组件的插入使用
微信小程序|API音频与视频组件的插入使用
332 0
|
10月前
|
前端开发 JavaScript 数据可视化
基于 React Flow 与 Web Audio API 的音频应用开发
今天我们来学习通过 React Flow 和 Web Audio API 来创建一个可交互的语音广场
134 0
基于 React Flow 与 Web Audio API 的音频应用开发
|
10月前
|
XML JSON 缓存
翻译文本 API说明示例
翻译文本 API说明示例
|
18天前
|
缓存 前端开发 API
API接口封装系列
API(Application Programming Interface)接口封装是将系统内部的功能封装成可复用的程序接口并向外部提供,以便其他系统调用和使用这些功能,通过这种方式实现系统之间的通信和协作。下面将介绍API接口封装的一些关键步骤和注意事项。
|
25天前
|
监控 前端开发 JavaScript
实战篇:商品API接口在跨平台销售中的有效运用与案例解析
随着电子商务的蓬勃发展,企业为了扩大市场覆盖面,经常需要在多个在线平台上展示和销售产品。然而,手工管理多个平台的库存、价格、商品描述等信息既耗时又容易出错。商品API接口在这一背景下显得尤为重要,它能够帮助企业在不同的销售平台之间实现商品信息的高效同步和管理。本文将通过具体的淘宝API接口使用案例,展示如何在跨平台销售中有效利用商品API接口,以及如何通过代码实现数据的统一管理。