关于阿里云实时语音翻译-Gummy加WebSocket实现翻译功能

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


使用

提示:这里可以添加本文要记录的大概内容:

当前我们是进行了手机点击翻译按钮进行连接WebSocket

开始录音进行转换发送

传输base64 、16000Hz、16bit、单声道、PCM 格式

后端接收发送阿里实时翻译将最后内容返回前端

前端不点击按钮即可断开WebSocket连接


提示:以下是本篇文章正文内容,下面案例可供参考

一、配置

1.配置项

  1. 首先我们需要配置启动类的@ServletComponentScan注解
  2. 然后还需要配置 WebSocketConfig 的Bean
java 复制代码
package cn.chinaunicom.sdsi.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration("TranslationWebSocket")
public class WebSocketConfig {
    /**
     * 这个Bean会自动注册所有使用 @ServerEndpoint 注解的WebSocket端点
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

2.pom文件

  1. 这里是关于阿里的大部分依赖
java 复制代码
<!-- Alibaba FastJSON:高性能JSON解析和生成库,用于JSON数据的序列化与反序列化 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.51</version>
</dependency>

<!-- 阿里云内容安全(Green)2022版SDK:用于内容安全审核、违规内容检测等功能 -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>green20220302</artifactId>
    <version>3.2.1</version>
</dependency>

<!-- 阿里云媒体处理服务(MTS)SDK:用于音视频转码、处理、截图等媒体处理操作 -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>alibabacloud-mts20140618</artifactId>
    <version>4.0.2</version>
</dependency>

<!-- Apache HttpCore5 核心模块:HTTP协议基础实现,包含reactor包和ProtocolUpgradeHandler类 -->
<dependency>
    <groupId>org.apache.httpcomponents.core5</groupId>
    <artifactId>httpcore5</artifactId>
    <version>5.2.5</version>
</dependency>

<!-- Apache HttpCore5 HTTP/2 支持模块:提供HTTP/2协议的实现和支持 -->
<dependency>
    <groupId>org.apache.httpcomponents.core5</groupId>
    <artifactId>httpcore5-h2</artifactId>
    <version>5.2.5</version>
</dependency>

<!-- Apache HttpClient5:高性能的HTTP客户端工具,用于发送HTTP请求和处理响应 -->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.3</version>
</dependency>

<!-- 阿里云智能语音交互(NLS)TTS SDK:语音合成(文字转语音)功能开发包 -->
<dependency>
    <groupId>com.alibaba.nls</groupId>
    <artifactId>nls-sdk-tts</artifactId>
    <version>2.2.14</version>
</dependency>

<!-- 阿里云智能语音交互(NLS)通用SDK:语音交互基础公共组件,提供通用的工具和接口 -->
<dependency>
    <groupId>com.alibaba.nls</groupId>
    <artifactId>nls-sdk-common</artifactId>
    <version>2.2.14</version>
</dependency>

<!-- 阿里云Java SDK核心包:阿里云各产品SDK的基础依赖,提供身份认证、请求处理等核心功能 -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
    <version>3.7.1</version>
</dependency>

<!-- 阿里云智能语音交互(NLS)识别SDK:语音识别(语音转文字)功能开发包 -->
<dependency>
    <groupId>com.alibaba.nls</groupId>
    <artifactId>nls-sdk-recognizer</artifactId>
    <version>2.2.1</version>
</dependency>

二、使用步骤

1.代码部分利用Ai进行注释

java 复制代码
package cn.chinaunicom.sdsi.picture.utils;

import com.alibaba.dashscope.audio.asr.translation.TranslationRecognizerParam;
import com.alibaba.dashscope.audio.asr.translation.TranslationRecognizerRealtime;
import com.alibaba.dashscope.audio.asr.translation.results.TranslationRecognizerResult;
import com.alibaba.dashscope.common.ResultCallback;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 实时翻译WebSocket服务端组件 
 * <p>
 * 核心定位:基于阿里云DashScope实时语音翻译API,提供WebSocket协议的实时音频翻译服务
 * 适用场景:浏览器/客户端通过WebSocket推送音频流,服务端实时返回语音识别+翻译结果
 * 技术栈:JSR-356 WebSocket + 阿里云DashScope SDK + 并发容器 + 定时任务
 * </p>
 * 核心功能:
 * 1. 支持二进制PCM音频传输(推荐,低延迟/高性能)
 * 2. 兼容Base64编码音频字符串(适配前端不同传输方式,含双引号包裹场景处理)
 * 3. 支持JSON格式配置消息(动态切换源/目标语言)
 * 4. 心跳检测机制(保障连接稳定性,超时自动清理)
 * 5. 空闲连接清理(释放资源,防止内存泄漏)
 * 6. 完整的异常防护(空消息/格式错误/网络异常等友好提示)
 * 7. 音频缓存补发(翻译器初始化期间缓存音频,就绪后自动补发)
 */
@Slf4j // Lombok注解,自动生成日志对象
@Component // Spring组件注解,交由Spring容器管理
@ServerEndpoint(value = "/ws/translate") // WebSocket服务端点,客户端连接路径
public class TranslationWebSocket {

    // ==================== 静态配置项(可抽取到配置文件) ====================
    /** 阿里云DashScope API密钥(需替换为实际有效密钥) */
    private static String apiKey = "sk-eb19********************3faf65";
    /** 阿里云实时翻译模型(gummy-realtime-v1为通用实时语音翻译模型) */
    private static String model = "gummy-realtime-v1";
    /** 音频采样率(固定16000Hz,阿里云模型要求) */
    private static Integer sampleRate = 16000;
    /** 音频格式(固定pcm,阿里云模型要求) */
    private static String audioFormat = "pcm";
    /** 最大空闲时间(秒):无音频传输超过该时间自动清理连接 */
    private static Integer maxIdleTime = 60;
    /** 心跳超时时间(秒):未收到心跳包超过该时间判定为连接异常 */
    private static Integer heartbeatTimeout = 30;
    /** 最大音频缓存包数:限制缓存队列大小,避免内存溢出(约2秒音频数据) */
    private static final int MAX_CACHE_SIZE = 100;

    // ==================== 全局常量/容器 ====================
    /** JSON序列化/反序列化工具(全局单例,避免重复创建) */
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    /** 翻译上下文缓存:key=taskId,value=翻译会话上下文(并发安全) */
    private static final Map<String, TranslationContext> CONTEXT_MAP = new ConcurrentHashMap<>();
    /** Session与taskId映射:key=WebSocket会话,value=任务ID(用于快速查找上下文) */
    private static final Map<Session, String> SESSION_TASK_ID_MAP = new ConcurrentHashMap<>();
    /** 心跳检测线程池:固定4个线程,处理心跳检查任务 */
    private static final ScheduledExecutorService HEARTBEAT_EXECUTOR = Executors.newScheduledThreadPool(4);
    /** 空闲连接清理线程池:单线程,避免并发清理冲突 */
    private static final ScheduledExecutorService CLEANUP_EXECUTOR = Executors.newScheduledThreadPool(1);
    /** Base64解码器(全局单例,处理Base64编码音频) */
    private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();

    // ==================== 内部上下文类 ====================
    /**
     * 翻译会话上下文(每个WebSocket连接对应一个上下文)
     * <p>
     * 职责:封装单个连接的所有状态信息,包括翻译器实例、音频缓存、时间戳、语言配置等
     * 设计:使用Atomic原子类保证并发安全,避免多线程竞争问题
     * </p>
     */
    private static class TranslationContext {
        /** 唯一任务ID(UUID生成,标识单次翻译会话) */
        private String taskId;
        /** WebSocket会话对象 */
        private Session session;
        /** 阿里云实时翻译器实例 */
        private TranslationRecognizerRealtime translator;
        /** 翻译器运行状态标记(true=运行中,false=未就绪) */
        private AtomicBoolean isTranslating = new AtomicBoolean(false);
        /** 累计接收音频字节数(统计用) */
        private AtomicLong audioBytesReceived = new AtomicLong(0);
        /** 累计接收音频包数(统计用) */
        private AtomicLong packetsReceived = new AtomicLong(0);
        /** 最后一次心跳时间戳(毫秒) */
        private AtomicLong lastHeartbeatTime = new AtomicLong(System.currentTimeMillis());
        /** 最后一次接收音频时间戳(毫秒) */
        private AtomicLong lastAudioTime = new AtomicLong(System.currentTimeMillis());
        /** 最后一次接收任意消息时间戳(毫秒) */
        private AtomicLong lastMessageTime = new AtomicLong(System.currentTimeMillis());
        /** 最后一次发送音频到阿里云时间戳(毫秒) */
        private AtomicLong lastSendTime = new AtomicLong(System.currentTimeMillis());
        /** 心跳失败次数(累计3次超时关闭连接) */
        private AtomicInteger heartbeatFailCount = new AtomicInteger(0);
        /** 源语言(默认auto=自动识别) */
        private String sourceLanguage = "auto";
        /** 目标语言(默认en=英语,支持zh=中文、ja=日语等) */
        private String targetLanguage = "en";
        /** 音频缓存队列(翻译器未就绪时缓存音频数据) */
        private Queue<ByteBuffer> audioCache = new ConcurrentLinkedQueue<>();

        /**
         * 构造方法:初始化上下文基本信息
         * @param taskId 任务ID
         * @param session WebSocket会话
         */
        public TranslationContext(String taskId, Session session) {
            this.taskId = taskId;
            this.session = session;
        }

        // 心跳重置:收到心跳包时更新时间戳并清零失败次数
        public void updateHeartbeat() {
            this.lastHeartbeatTime.set(System.currentTimeMillis());
            this.heartbeatFailCount.set(0);
        }

        // 更新音频接收时间:收到音频时调用
        public void updateAudioTime() {
            this.lastAudioTime.set(System.currentTimeMillis());
            this.lastMessageTime.set(System.currentTimeMillis());
        }

        // 更新消息接收时间:收到任意文本消息时调用
        public void updateMessageTime() {
            this.lastMessageTime.set(System.currentTimeMillis());
        }

        // 判断心跳是否超时:当前时间 - 最后心跳时间 > 心跳超时阈值
        public boolean isHeartbeatTimeout() {
            return System.currentTimeMillis() - lastHeartbeatTime.get() > heartbeatTimeout * 1000;
        }

        // 判断连接是否空闲:无音频传输超过最大空闲时间
        public boolean isIdle() {
            return System.currentTimeMillis() - lastAudioTime.get() > maxIdleTime * 1000;
        }

        // 心跳失败次数+1:心跳检查超时调用
        public void incrementHeartbeatFail() {
            heartbeatFailCount.incrementAndGet();
        }

        // Getters & Setters(仅暴露必要字段,保证封装性)
        public int getHeartbeatFailCount() { return heartbeatFailCount.get(); }
        public AtomicLong getLastSendTime() { return lastSendTime; }
        public Queue<ByteBuffer> getAudioCache() { return audioCache; }
        public String getTaskId() { return taskId; }
        public Session getSession() { return session; }
        public AtomicBoolean getIsTranslating() { return isTranslating; }
        public TranslationRecognizerRealtime getTranslator() { return translator; }
        public void setTranslator(TranslationRecognizerRealtime translator) { this.translator = translator; }
        public long getAudioBytesReceived() { return audioBytesReceived.get(); }
        public long getPacketsReceived() { return packetsReceived.get(); }
        public void incrementPackets() { packetsReceived.incrementAndGet(); }
        public void addBytes(long bytes) { audioBytesReceived.addAndGet(bytes); }
        public String getSourceLanguage() { return sourceLanguage; }
        public void setSourceLanguage(String sourceLanguage) { this.sourceLanguage = sourceLanguage; }
        public String getTargetLanguage() { return targetLanguage; }
        public void setTargetLanguage(String targetLanguage) { this.targetLanguage = targetLanguage; }
    }

    // ==================== 静态初始化(程序启动时执行) ====================
    static {
        // 心跳监控任务:延迟5秒启动,每5秒执行一次
        HEARTBEAT_EXECUTOR.scheduleAtFixedRate(() -> {
            // 遍历所有翻译上下文,逐个检查心跳状态
            for (Map.Entry<String, TranslationContext> entry : CONTEXT_MAP.entrySet()) {
                TranslationContext context = entry.getValue();
                try {
                    checkHeartbeat(context);
                } catch (Exception e) {
                    log.error("心跳检查异常 - taskId: {}", context.getTaskId(), e);
                }
            }
        }, 5, 5, TimeUnit.SECONDS);

        // 空闲连接清理任务:延迟30秒启动,每30秒执行一次
        CLEANUP_EXECUTOR.scheduleAtFixedRate(() -> {
            log.info("开始清理空闲连接,当前连接数: {}", CONTEXT_MAP.size());
            int cleanedCount = 0;
            // 遍历所有翻译上下文,清理空闲连接
            for (Map.Entry<String, TranslationContext> entry : CONTEXT_MAP.entrySet()) {
                TranslationContext context = entry.getValue();
                if (context.isIdle()) {
                    log.info("清理空闲连接 - taskId: {}, 最后音频时间: {}",
                            context.getTaskId(), context.lastAudioTime.get());
                    stopTranslation(context); // 停止翻译服务
                    CONTEXT_MAP.remove(entry.getKey()); // 移除上下文缓存
                    SESSION_TASK_ID_MAP.remove(context.getSession()); // 移除Session映射
                    cleanedCount++;
                }
            }
            if (cleanedCount > 0) {
                log.info("清理完成,移除了 {} 个空闲连接", cleanedCount);
            }
        }, 30, 30, TimeUnit.SECONDS);
    }

    // ==================== 心跳检查核心方法 ====================
    /**
     * 心跳检查:校验连接状态,超时则关闭连接
     * @param context 翻译会话上下文
     */
    private static void checkHeartbeat(TranslationContext context) {
        Session session = context.getSession();
        // 会话已关闭:清理上下文
        if (session == null || !session.isOpen()) {
            log.info("会话已关闭,清理上下文 - taskId: {}", context.getTaskId());
            stopTranslation(context);
            CONTEXT_MAP.remove(context.getTaskId());
            SESSION_TASK_ID_MAP.remove(session);
            return;
        }

        // 新连接未接收过音频:跳过心跳检查(避免误判)
        if (context.getPacketsReceived() == 0) {
            return;
        }

        // 心跳超时:累计失败次数,超过3次则关闭连接
        if (context.isHeartbeatTimeout()) {
            context.incrementHeartbeatFail();
            log.warn("心跳超时 - taskId: {}, 失败次数: {}, 最后心跳: {}",
                    context.getTaskId(), context.getHeartbeatFailCount(), context.lastHeartbeatTime.get());

            if (context.getHeartbeatFailCount() >= 3) {
                log.error("心跳连续失败3次,关闭连接 - taskId: {}", context.getTaskId());
                try {
                    // 异常关闭连接,附带原因
                    session.close(new CloseReason(CloseReason.CloseCodes.CLOSED_ABNORMALLY, "心跳超时"));
                } catch (IOException e) {
                    log.error("关闭会话失败 - taskId: {}", context.getTaskId(), e);
                }
                stopTranslation(context);
                CONTEXT_MAP.remove(context.getTaskId());
                SESSION_TASK_ID_MAP.remove(session);
            }
        }
    }

    // ==================== WebSocket生命周期方法 ====================
    /**
     * WebSocket连接建立时触发
     * @param session 新建立的WebSocket会话
     */
    @OnOpen
    public void onOpen(Session session) {
        // 生成唯一任务ID(去除横线,简化标识)
        String taskId = UUID.randomUUID().toString().replace("-", "");
        // 创建翻译上下文
        TranslationContext context = new TranslationContext(taskId, session);
        // 缓存上下文和Session映射
        CONTEXT_MAP.put(taskId, context);
        SESSION_TASK_ID_MAP.put(session, taskId);

        // 设置WebSocket会话最大空闲超时时间(与业务层空闲清理逻辑双层保障)
        session.setMaxIdleTimeout(maxIdleTime * 1000);

        log.info("WebSocket连接建立 - taskId: {}, sessionId: {}", taskId, session.getId());

        // 异步初始化翻译服务(避免阻塞WebSocket连接建立)
        new Thread(() -> {
            initTranslationService(context); // 初始化阿里云翻译器
            sendReadyMessage(context); // 发送就绪消息
            retryCachedAudio(context); // 补发缓存的音频数据
        }).start();

        // 同步发送欢迎消息
        sendWelcomeMessage(context);
    }

    /**
     * 接收二进制音频数据(优先推荐的传输方式)
     * @param audioData 二进制PCM音频数据
     * @param session WebSocket会话
     */
    @OnMessage
    public void onMessage(ByteBuffer audioData, Session session) {
        // 根据Session获取上下文
        TranslationContext context = getContextBySession(session);
        if (context == null) {
            log.warn("未找到会话上下文 - sessionId: {}", session.getId());
            return;
        }
        // 统一处理音频数据
        processAudioData(context, audioData);
    }

    /**
     * 接收文本消息(处理心跳/配置/Base64音频/结束标记)
     * @param message 文本消息内容
     * @param session WebSocket会话
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        TranslationContext context = getContextBySession(session);
        if (context == null) {
            log.warn("未找到会话上下文 - sessionId: {}", session.getId());
            return;
        }

        // 更新消息接收时间
        context.updateMessageTime();

        try {
            // 空消息防护:直接返回错误提示
            if (message == null || message.trim().isEmpty()) {
                log.warn("收到空文本消息 - taskId: {}", context.getTaskId());
                sendErrorMessage(context, "空消息不被处理");
                return;
            }

            // 清理首尾空格
            String cleanMsg = message.trim();

            // 核心修复:去除首尾双引号(解决前端JSON.stringify导致的Base64字符串被包裹问题)
            if (cleanMsg.startsWith("\"") && cleanMsg.endsWith("\"")) {
                cleanMsg = cleanMsg.substring(1, cleanMsg.length() - 1);
                log.debug("移除首尾双引号 - taskId: {}, 处理后长度: {}", context.getTaskId(), cleanMsg.length());
            }

            // 1. 处理心跳消息(ping -> 重置心跳)
            if ("ping".equals(cleanMsg)) {
                context.updateHeartbeat();
                sendTipMessage(context, "心跳已确认");
                return;
            }

            // 2. 处理音频结束标记(audio_end -> 停止翻译并重置)
            if ("audio_end".equals(cleanMsg)) {
                log.info("收到音频结束标记,停止翻译服务 - taskId: {}", context.getTaskId());
                stopTranslation(context);
                // 异步重置翻译器,准备下一次翻译
                new Thread(() -> initTranslationService(context)).start();
                sendTipMessage(context, "音频流已结束,翻译服务已重置");
                return;
            }

            // 3. 尝试识别并处理Base64编码音频
            if (isBase64Audio(cleanMsg)) {
                log.info("收到Base64编码音频,解码处理 - taskId: {}", context.getTaskId());
                try {
                    // Base64解码为字节数组
                    byte[] audioBytes = BASE64_DECODER.decode(cleanMsg);
                    // 封装为ByteBuffer,复用二进制音频处理逻辑
                    ByteBuffer audioData = ByteBuffer.wrap(audioBytes);
                    processAudioData(context, audioData);
                    return;
                } catch (IllegalArgumentException e) {
                    log.error("Base64音频解码失败 - taskId: {}", context.getTaskId(), e);
                    sendErrorMessage(context, "Base64音频解码失败:" + e.getMessage());
                }
            }

            // 4. 尝试解析JSON配置消息(语言切换等)
            try {
                Map<String, Object> config = OBJECT_MAPPER.readValue(cleanMsg, HashMap.class);
                if (config.isEmpty()) {
                    log.warn("收到空配置消息 - taskId: {}", context.getTaskId());
                    sendErrorMessage(context, "配置消息为空!正确示例:{\"sourceLanguage\":\"zh\",\"targetLanguage\":\"en\"}");
                    return;
                }
                // 处理配置更新
                handleConfigMessage(context, config);
            } catch (Exception e) {
                // JSON解析失败:提示支持的消息类型
                log.warn("非JSON配置消息,无法解析 - taskId: {}, 消息开头: {}...",
                        context.getTaskId(), cleanMsg.substring(0, Math.min(50, cleanMsg.length())));
                sendErrorMessage(context, "消息格式错误!支持:心跳(ping)、结束(audio_end)、JSON配置、Base64音频");
            }

        } catch (Exception e) {
            log.error("解析文本消息失败 - taskId: {}", context.getTaskId(), e);
            sendErrorMessage(context, "消息处理失败:" + e.getMessage());
        }
    }

    /**
     * WebSocket连接关闭时触发
     * @param session 关闭的WebSocket会话
     */
    @OnClose
    public void onClose(Session session) {
        TranslationContext context = getContextBySession(session);
        if (context != null) {
            log.info("WebSocket连接正常关闭 - taskId: {}, sessionId: {}",
                    context.getTaskId(), session.getId());
            stopTranslation(context); // 停止翻译服务
            CONTEXT_MAP.remove(context.getTaskId()); // 清理上下文
            SESSION_TASK_ID_MAP.remove(session); // 清理Session映射
        } else {
            log.info("WebSocket连接关闭 - sessionId: {}", session.getId());
            SESSION_TASK_ID_MAP.remove(session);
        }
    }

    /**
     * WebSocket连接异常时触发
     * @param session 异常的WebSocket会话
     * @param error 异常信息
     */
    @OnError
    public void onError(Session session, Throwable error) {
        TranslationContext context = getContextBySession(session);
        if (context != null) {
            log.error("WebSocket异常 - taskId: {}, 错误: {}", context.getTaskId(), error.getMessage());
            sendErrorMessage(context, "连接异常:" + error.getMessage());
            stopTranslation(context); // 停止翻译服务
            CONTEXT_MAP.remove(context.getTaskId()); // 清理上下文
            SESSION_TASK_ID_MAP.remove(session); // 清理Session映射
        } else {
            log.error("WebSocket异常 - sessionId: {}, 错误: {}", session.getId(), error.getMessage());
            SESSION_TASK_ID_MAP.remove(session);
        }
    }

    // ==================== 核心业务处理方法 ====================
    /**
     * 判断字符串是否为Base64编码的音频数据
     * @param message 待校验的文本消息
     * @return true=是Base64音频,false=否
     */
    private boolean isBase64Audio(String message) {
        if (message == null || message.isEmpty()) {
            return false;
        }
        // Base64特征校验:
        // 1. 长度是4的倍数(允许末尾1-2个等号)
        // 2. 长度不小于100(过滤短字符串,避免误判)
        // 3. 仅包含Base64合法字符(A-Za-z0-9+/=)
        if (message.length() % 4 > 2 || message.length() < 100) {
            return false;
        }
        return message.matches("^[A-Za-z0-9+/=]+$");
    }

    /**
     * 统一处理音频数据(二进制/Base64解码后)
     * @param context 翻译会话上下文
     * @param audioData 音频数据(ByteBuffer格式)
     */
    private void processAudioData(TranslationContext context, ByteBuffer audioData) {
        // 更新音频接收时间戳
        context.updateAudioTime();

        // 校验音频数据是否为空
        int audioSize = audioData.remaining();
        if (audioSize == 0) {
            log.warn("收到空音频数据 - taskId: {}", context.getTaskId());
            return;
        }

        // 校验音频包大小(16000Hz/16bit/单声道:20ms音频约640字节)
        // 异常大小提示:避免前端传输错误格式的音频
        int expectedSize = 16000 * 2 * 20 / 1000;
        if (audioSize > expectedSize * 2 || audioSize < 10) {
            log.warn("异常音频包大小 - taskId: {}, 大小: {} 字节(预期约640)",
                    context.getTaskId(), audioSize);
        }

        // 检查音频发送间隔(异常间隔提示:排查前端发送逻辑)
        long currentTime = System.currentTimeMillis();
        long interval = currentTime - context.getLastSendTime().get();
        if (interval > 50) {
            log.warn("音频发送间隔异常 - taskId: {}, 间隔: {}ms", context.getTaskId(), interval);
        }
        context.getLastSendTime().set(currentTime);

        // 翻译器未就绪:缓存音频数据
        if (!context.getIsTranslating().get() || context.getTranslator() == null) {
            log.warn("翻译器未就绪,缓存音频数据 - taskId: {}, 当前缓存数: {}",
                    context.getTaskId(), context.getAudioCache().size());

            // 缓存队列满:移除最早的音频包(避免内存溢出)
            if (context.getAudioCache().size() >= MAX_CACHE_SIZE) {
                log.warn("缓存队列已满,移除最早的音频包 - taskId: {}", context.getTaskId());
                context.getAudioCache().poll();
            }

            // 添加到缓存队列
            context.getAudioCache().add(audioData);
            sendTipMessage(context, "翻译服务初始化中,已缓存音频数据(当前" + context.getAudioCache().size() + "包)");
            return;
        }

        try {
            // 更新统计数据
            context.addBytes(audioSize);
            context.incrementPackets();

            // 发送音频数据到阿里云翻译器
            context.getTranslator().sendAudioFrame(audioData);

            // 每200包打印一次统计信息(便于监控传输状态)
            long packets = context.getPacketsReceived();
            if (packets % 200 == 0) {
                log.info("音频传输统计 - taskId: {}, 总包数: {}, 总字节数: {} KB",
                        context.getTaskId(), packets, context.getAudioBytesReceived() / 1024);
            }

        } catch (Exception e) {
            log.error("发送音频数据到阿里云失败 - taskId: {}", context.getTaskId(), e);
            sendErrorMessage(context, "音频发送失败:" + e.getMessage());
        }
    }

    /**
     * 初始化阿里云实时翻译服务
     * @param context 翻译会话上下文
     */
    private void initTranslationService(TranslationContext context) {
        // 翻译器已运行:直接返回
        if (context.getIsTranslating().get()) {
            sendTipMessage(context, "⚠️ 翻译服务已在运行中");
            return;
        }

        try {
            log.info("初始化翻译服务 - taskId: {}", context.getTaskId());

            // 构建阿里云翻译参数
            TranslationRecognizerParam param = TranslationRecognizerParam.builder()
                    .apiKey(apiKey) // API密钥
                    .model(model) // 翻译模型
                    .format(audioFormat) // 音频格式
                    .sampleRate(sampleRate) // 采样率
                    .sourceLanguage(context.getSourceLanguage()) // 源语言
                    .translationEnabled(true) // 启用翻译
                    .translationLanguages(new String[]{context.getTargetLanguage()}) // 目标语言
                    .build();

            // 翻译结果回调处理
            ResultCallback<TranslationRecognizerResult> callback = new ResultCallback<TranslationRecognizerResult>() {
                /**
                 * 收到翻译结果时触发(仅处理句子结束的最终结果)
                 * @param result 翻译结果对象
                 */
                @Override
                public void onEvent(TranslationRecognizerResult result) {
                    // 非句子结束的中间结果:忽略(减少前端数据量)
                    if (!result.isSentenceEnd()) {
                        return;
                    }

                    try {
                        // 构造翻译结果消息
                        Map<String, Object> resultMsg = new HashMap<>();
                        resultMsg.put("type", "translation_result"); // 消息类型
                        resultMsg.put("taskId", context.getTaskId()); // 任务ID
                        // 识别文本(语音转文字结果)
                        resultMsg.put("text", result.getTranscriptionResult() != null
                                ? result.getTranscriptionResult().getText() : "");
                        // 翻译文本(目标语言结果)
                        resultMsg.put("translate", result.getTranslationResult() != null
                                ? result.getTranslationResult().getTranslation(context.getTargetLanguage()).getText() : "");
                        resultMsg.put("isFinal", true); // 标记为最终结果
                        resultMsg.put("timestamp", System.currentTimeMillis()); // 时间戳

                        // 发送JSON格式结果到前端
                        sendJsonMessage(context, resultMsg);

                        log.info("[{}][最终结果] 识别:{} | 翻译:{}",
                                context.getTaskId(), resultMsg.get("text"), resultMsg.get("translate"));

                    } catch (Exception e) {
                        log.error("推送翻译结果失败 - taskId: {}", context.getTaskId(), e);
                    }
                }

                /**
                 * 翻译任务完成时触发
                 */
                @Override
                public void onComplete() {
                    log.info("翻译任务完成 - taskId: {}", context.getTaskId());
                    sendTipMessage(context, "✅ 翻译任务已完成");
                }

                /**
                 * 翻译异常时触发
                 * @param e 异常信息
                 */
                @Override
                public void onError(Exception e) {
                    log.error("翻译异常 - taskId: {}, 错误: {}", context.getTaskId(), e.getMessage());
                    sendErrorMessage(context, "❌ 翻译出错:" + e.getMessage());
                }
            };

            // 初始化阿里云实时翻译器
            TranslationRecognizerRealtime translator = new TranslationRecognizerRealtime();
            // 启动翻译器(异步执行)
            translator.call(param, callback);

            // 更新上下文状态
            context.setTranslator(translator);
            context.getIsTranslating().set(true);

            log.info("翻译服务初始化成功 - taskId: {}", context.getTaskId());

        } catch (Exception e) {
            log.error("初始化翻译服务失败 - taskId: {}, 错误: {}", context.getTaskId(), e);
            sendErrorMessage(context, "❌ 初始化翻译服务失败:" + e.getMessage());
            context.getIsTranslating().set(false);
        }
    }

    /**
     * 停止翻译服务(释放资源)
     * @param context 翻译会话上下文
     */
    private static void stopTranslation(TranslationContext context) {
        // 翻译器未运行:直接返回
        if (!context.getIsTranslating().get()) {
            return;
        }

        log.info("停止翻译服务 - taskId: {}", context.getTaskId());
        // 更新运行状态
        context.getIsTranslating().set(false);

        // 关闭翻译器并清理缓存
        TranslationRecognizerRealtime translator = context.getTranslator();
        if (translator != null) {
            try {
                translator.stop(); // 停止翻译器
                context.getAudioCache().clear(); // 清空音频缓存
            } catch (Exception e) {
                log.error("关闭翻译器失败 - taskId: {}", context.getTaskId(), e);
            }
            context.setTranslator(null); // 置空翻译器引用
        }
    }

    /**
     * 翻译器就绪后,补发缓存的音频数据
     * @param context 翻译会话上下文
     */
    private void retryCachedAudio(TranslationContext context) {
        // 翻译器未就绪:跳过补发
        if (context.getTranslator() == null || !context.getIsTranslating().get()) {
            log.warn("翻译器未就绪,跳过缓存音频补发 - taskId: {}", context.getTaskId());
            return;
        }

        Queue<ByteBuffer> cache = context.getAudioCache();
        // 缓存为空:直接返回
        if (cache.isEmpty()) {
            return;
        }

        log.info("开始补发缓存音频 - taskId: {}, 缓存数量: {}", context.getTaskId(), cache.size());
        int successCount = 0;
        int totalCount = cache.size();

        // 遍历缓存队列,逐个补发音频包
        while (!cache.isEmpty()) {
            ByteBuffer audioData = cache.poll();
            try {
                context.getTranslator().sendAudioFrame(audioData);
                successCount++;
            } catch (Exception e) {
                log.error("补发单包缓存音频失败 - taskId: {}", context.getTaskId(), e);
            }
        }

        log.info("缓存音频补发完成 - taskId: {}, 成功: {} / {}", context.getTaskId(), successCount, totalCount);
        sendTipMessage(context, "缓存音频补发完成:成功" + successCount + "/" + totalCount + "包");
    }

    /**
     * 处理配置消息(动态切换源/目标语言)
     * @param context 翻译会话上下文
     * @param config 配置参数(sourceLanguage/targetLanguage)
     */
    private void handleConfigMessage(TranslationContext context, Map<String, Object> config) {
        log.info("收到配置消息 - taskId: {}, config: {}", context.getTaskId(), config);

        // 更新目标语言
        if (config.containsKey("targetLanguage")) {
            String newTarget = (String) config.get("targetLanguage");
            context.setTargetLanguage(newTarget);
            sendTipMessage(context, "目标语言已切换为: " + newTarget);
        }

        // 更新源语言
        if (config.containsKey("sourceLanguage")) {
            String newSource = (String) config.get("sourceLanguage");
            context.setSourceLanguage(newSource);
            sendTipMessage(context, "源语言已切换为: " + newSource);
        }

        // 构造配置确认消息
        Map<String, Object> ackMsg = new HashMap<>();
        ackMsg.put("type", "config_ack"); // 消息类型:配置确认
        ackMsg.put("taskId", context.getTaskId()); // 任务ID

        // 当前配置信息
        Map<String, Object> currentConfig = new HashMap<>();
        currentConfig.put("sourceLanguage", context.getSourceLanguage());
        currentConfig.put("targetLanguage", context.getTargetLanguage());
        ackMsg.put("currentConfig", currentConfig);

        // 发送确认消息到前端
        sendJsonMessage(context, ackMsg);
    }

    // ==================== 消息发送工具方法 ====================
    /**
     * 发送欢迎消息(连接建立时)
     * @param context 翻译会话上下文
     */
    private void sendWelcomeMessage(TranslationContext context) {
        Map<String, Object> msg = new HashMap<>();
        msg.put("type", "welcome");
        msg.put("taskId", context.getTaskId());
        msg.put("message", "翻译服务已连接");
        msg.put("timestamp", System.currentTimeMillis());
        sendJsonMessage(context, msg);
    }

    /**
     * 发送就绪消息(翻译器初始化完成时)
     * @param context 翻译会话上下文
     */
    private void sendReadyMessage(TranslationContext context) {
        Map<String, Object> msg = new HashMap<>();
        msg.put("type", "ready");
        msg.put("taskId", context.getTaskId());
        msg.put("message", "翻译服务已就绪,可发送音频数据");
        msg.put("timestamp", System.currentTimeMillis());
        sendJsonMessage(context, msg);
    }

    /**
     * 发送提示消息(非错误类通知)
     * @param context 翻译会话上下文
     * @param message 提示内容
     */
    private void sendTipMessage(TranslationContext context, String message) {
        Map<String, Object> msg = new HashMap<>();
        msg.put("type", "tip");
        msg.put("taskId", context.getTaskId());
        msg.put("message", message);
        msg.put("timestamp", System.currentTimeMillis());
        sendJsonMessage(context, msg);
    }

    /**
     * 发送错误消息(异常提示)
     * @param context 翻译会话上下文
     * @param message 错误内容
     */
    private void sendErrorMessage(TranslationContext context, String message) {
        Map<String, Object> msg = new HashMap<>();
        msg.put("type", "error");
        msg.put("taskId", context.getTaskId());
        msg.put("message", message);
        msg.put("timestamp", System.currentTimeMillis());
        sendJsonMessage(context, msg);
    }

    /**
     * 发送JSON格式消息到前端
     * @param context 翻译会话上下文
     * @param data 消息内容
     */
    private void sendJsonMessage(TranslationContext context, Map<String, Object> data) {
        Session session = context.getSession();
        // 会话已关闭:跳过发送
        if (session == null || !session.isOpen()) {
            log.debug("会话已关闭,跳过消息发送 - taskId: {}", context.getTaskId());
            return;
        }

        try {
            // 序列化为JSON字符串
            String json = OBJECT_MAPPER.writeValueAsString(data);
            // 同步发送文本消息
            session.getBasicRemote().sendText(json);
        } catch (JsonProcessingException e) {
            log.error("JSON序列化失败 - taskId: {}", context.getTaskId(), e);
        } catch (IOException e) {
            log.error("消息发送失败 - taskId: {}", context.getTaskId(), e);
        }
    }

    // ==================== 辅助工具方法 ====================
    /**
     * 根据WebSocket会话获取翻译上下文
     * @param session WebSocket会话
     * @return 翻译上下文(null=未找到)
     */
    private TranslationContext getContextBySession(Session session) {
        String taskId = SESSION_TASK_ID_MAP.get(session);
        if (taskId == null) {
            log.warn("未找到Session对应的TaskId - sessionId: {}", session.getId());
            return null;
        }
        return CONTEXT_MAP.get(taskId);
    }
}

2、文档级总结

  1. 组件核心定位
    该组件是基于 WebSocket 协议和阿里云 DashScope 实时语音翻译 API 实现的实时音频翻译服务端,主要用于接收前端推送的音频流(二进制 PCM/Base64 编码),实时返回语音识别 + 翻译结果,适用于需要低延迟实时语音翻译的场景(如跨境会议、实时客服等)。

  1. 核心设计亮点
设计点 实现方式 解决的问题
多格式音频支持 二进制 PCM 优先 + Base64 兼容处理 适配前端不同音频传输方式,兼容 JSON 序列化导致的 Base64 字符串包裹问题
连接稳定性保障 心跳检测 + 空闲连接清理 防止无效连接占用资源,心跳超时自动关闭异常连接
并发安全设计 ConcurrentHashMap/Atomic 原子类 / ConcurrentLinkedQueue 解决多线程(WebSocket 会话 / 定时任务)并发访问数据的线程安全问题
异常防护机制 空消息校验 / 格式校验 / 异常捕获 / 友好提示 避免单个异常导致服务崩溃,提升用户体验
音频缓存补发 翻译器初始化期间缓存音频,就绪后自动补发 解决翻译器启动延迟导致的音频丢失问题
  1. 核心交互流程

  2. 关键配置与扩展建议

  • 可配置化优化:当前静态配置(apiKey、超时时间等)建议抽取到 Spring 配置文件(application.yml),通过@Value注入,提升可维护性;
  • API 密钥安全:生产环境需将 apiKey 存储在配置中心 / 环境变量,避免硬编码;
  • 监控扩展:可增加连接数、音频传输量、翻译成功率等监控指标,便于运维排查问题;
  • 容灾处理:增加阿里云 API 调用失败的重试机制,或降级为本地提示,提升服务可用性;
  • 语言扩展:支持更多目标语言(阿里云支持的语言列表),可维护语言映射表。

三、前端

目前是

录音 + 转 Base64

前端操作(核心)

① 用浏览器MediaRecorder录制音频(配置:16000Hz、16bit、单声道、PCM 格式)

② 每 20ms 左右截取一段音频二进制数据

③ 将二进制转成 Base64 字符串
前端注意事项

① 音频格式必须和后端一致(16000Hz/PCM)

② 分片大小控制在 640 字节左右(20ms)

③ 避免连续发送间隔超过 50ms

  • 等待更新。。。。。
相关推荐
做运维的阿瑞1 小时前
公有云算力布局的技术逻辑与工程实践
微服务·云计算·csdn开发云
朱包林2 小时前
iTerm2终端配置指南
linux·javascript·python·云原生·容器·kubernetes·云计算
酒神dnspup5 小时前
IPv6 网站访问异常怎么查?DNSPup 双栈检测、DNS 与端口排障指南
运维·云计算·域名·dns·dnspup
workflower20 小时前
矿用机器人人工智能安全与治理体系
人工智能·安全·机器学习·机器人·云计算·无人机
郭老二21 小时前
【docker】使用阿里云的容器镜像服务(ACR)构建自己的容器仓库
阿里云·docker·容器
怪奇云呼军1 天前
教育试听预约后没到课,闪电智能 Voice Agent 如何做分层回访?
人工智能·python·算法·云计算·音视频
yichengerp1 天前
国内中小电子工厂用哪个erp系统好?
大数据·运维·人工智能·云计算·制造
sbjdhjd1 天前
云安全 | 从特权容器到旧版 runC:Docker 宿主机访问与运行时缺陷复盘
运维·网络安全·docker·云原生·kubernetes·云计算·云安全