关于阿里云实时语音翻译-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

  • 等待更新。。。。。
相关推荐
懈尘2 小时前
【实战分享】智慧养老系统核心模块设计 —— 健康监测与自动紧急呼叫
java·后端·websocket·mysql·springboot·livekit
腾科IT教育2 小时前
广东广州华为认证考点在哪里
华为云·云计算·hcie·华为认证考试
热爱专研AI的学妹3 小时前
DataEyes 聚合平台对接 Claude 开发实战:从数据采集到智能分析全流程
大数据·人工智能·阿里云
旭日跑马踏云飞3 小时前
不需要账号、免登录使用ClaudeCode+国内模型
人工智能·阿里云·ai·腾讯云·ai编程
TG_yunshuguoji18 小时前
亚马逊云代理商:CloudWatch 日志查询实战 5 步精准定位 AWS 故障
服务器·云计算·aws
gaize121319 小时前
阿里云 GPU 云服务器|AI 训练渲染专用
服务器·人工智能·阿里云
TG_yunshuguoji20 小时前
阿里云代理商:百炼用AI重新定义图像的诞生
人工智能·阿里云·云计算
ZStack开发者社区21 小时前
技术解析:ZStack 计算 + 存储双利旧,破解数据中心异构纳管与资产浪费痛点
服务器·云计算
小哈里1 天前
【架构】Server-Survival,扮演云架构师的塔防游戏,生存策略
游戏·架构·云计算·架构师·策略