一、前言

        实验课要求搭建的Chat对话网页应用。先看结果:

二、Chat

        中间对话组件使用的Semi-ui-vue,非常方便,参考文档如下:

Introduction 介绍 | semi-design-vueVue3 UI components based on semi-design and Vuehttps://semi-ui-vue.netlify.app/zh-CN/start/introduction/        要使用chat组件,需要较高的semi版本,同时vue的版本也要很高,建议使用最新的,不然很多坑,安装semi-ui-vue:

npm i @kousum/semi-ui-vue

 找到chat组件,选择熟悉的代码 直接拷贝下来

 接入APi调用deepseek重点是需要修改函数:

    const onMessageSend = (message) => {
    chats.value.push({
        role: 'system',
        id: count++,
        createAt: new Date().getTime(),
        content: '',
        status: 'loading',
    });
    const eventSource = new EventSource(
        `http://localhost:8081/aiapi/stream?message=${encodeURIComponent(message)}`
    );
    eventSource.onmessage = (event) => {
        let response = event.data;
        if (response == 'end') {
            chats.value[chats.value.length - 1].status = 'complete';
            SystemResult.value = chats.value[chats.value.length - 1].content
            uploadtexttotts(SystemResult.value)
            console.log(SystemResult.value);
        close();
        return;
        }
        response = JSON.parse(response).content;
        chats.value[chats.value.length - 1].status = 'incomplete';
        chats.value[chats.value.length - 1].content += response;
    };
        eventSource.onerror = (error) => {
        console.log(error);
        chats.value[chats.value.length - 1].status = 'error';
        close();
    };

    const close = () => {
        eventSource.close();
    };
    };

里面包含了服务请求地址,使用了 SSE(Server-Sent Events) 技术实现流式输出 GPT 对话内容。在前后端交互中,必须确保服务端返回的响应数据与用户原始请求正确绑定,以便前端能够正确显示对话内容,并维持会话的连贯性。

三、服务端Api

springboot调用deepapi,Api接口如下:


@RestController
@RequestMapping("/aiapi")
public class SeekController {
    private static final Logger logger = LoggerFactory.getLogger(SeekController.class);

    private static final String DONE = "[DONE]";
    private static final Integer timeout = 60;

    private static final String AI_URL = "https://api.deepseek.com/chat/completions";
//    private static final String AI_URL = "http://127.0.0.1/v1/chat-messages";

    @Value("${ai-api.password:}")
    private String apiPassword;

    @GetMapping(value = "/stream")
    public void handleSse(String message, HttpServletResponse response) {
        System.out.println(message);
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("utf-8");
        try (PrintWriter pw = response.getWriter()) {
            getAiResult(pw, message);
            pw.write("data:end\n\n");
            pw.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void getAiResult(PrintWriter pw, String content) throws InterruptedException {
        Map<String, Object> params = new HashMap<>();
        params.put("model", "4.0Ultra");
        //result.put("user", "4.0Ultra");

        Map<String, String> message = new HashMap<>();
        message.put("role", "user");
        message.put("content", content);

        List<Map> messages = new ArrayList<>();
        messages.add(message);
        params.put("messages", messages);
        params.put("stream", true);
        params.put("model", "deepseek-chat");
        String jsonParams = JsonUtils.convertObj2Json(params);

        Request.Builder builder = new Request.Builder().url(AI_URL);
        builder.addHeader("Authorization", " Bearer " + apiPassword);
        builder.addHeader("Accept", "text/event-stream");
        RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonParams);
        Request request = builder.post(body).build();
        OkHttpClient client = new OkHttpClient.Builder().connectTimeout(timeout, TimeUnit.SECONDS).writeTimeout(timeout, TimeUnit.SECONDS).readTimeout(timeout,
                TimeUnit.SECONDS).build();

        // 实例化EventSource,注册EventSource监听器 -- 创建一个用于处理服务器发送事件的实例,并定义处理事件的回调逻辑
        CountDownLatch eventLatch = new CountDownLatch(1);

        RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {
            @Override
            public void onEvent(EventSource eventSource, String id, String type, String data) {
                if (DONE.equals(data)) {
                    return;
                }
                String content = getContent(data);
                pw.write("data:" + JsonUtils.convertObj2Json(new ContentDto(content)) + "\n\n");
                pw.flush();
            }

            @Override
            public void onClosed(EventSource eventSource) {
                super.onClosed(eventSource);
                eventLatch.countDown();
            }

            @Override
            public void onFailure(EventSource eventSource, Throwable t, Response response) {
                logger.info("调用接口失败{}", t);
                if (eventLatch != null) {
                    eventLatch.countDown();
                }
            }
        });
        // 与服务器建立连接
        realEventSource.connect(client);
        // await() 方法被调用来阻塞当前线程,直到 CountDownLatch 的计数变为0。
        eventLatch.await();
    }

    private static String getContent(String data) {
        AiResult aiResult = JsonUtils.convertJson2Obj(data, AiResult.class);
        return aiResult.getChoices().get(0).getDelta().getContent();
    }
}

需要再application.yaml中配置好自己的api-key,配置完成后即可被用户端正常访问到。

四、前端语音输入

        

网页端调用麦克风语音输入使用了Recoder包,具体细节参考自博客:【Recorder】在 vue3 中使用 Recorder 实现录音并上传(mp3、wav)兼容 PC 和移动端_vue3 录音-CSDN博客

安装Recoder运行:

npm install recorder-core

需要写好自己的utils.js,以正常调用语音录入功能,下面三个函数是我主要使用到的,recOpen请求打开麦克风,recStart开始录音,recStop停止录音,Stop函数中调用了Upload,这个函数会直接将录到的语音上传服务端,调用SenseVoice模型,识别为中文。

// 使用模块化的全局变量
let rec = null;
let recBlob = null;
let ChatResult = null;



// 打开录音

function recOpen() {
    return new Promise((resolve, reject) => {
        // 创建录音对象
        rec = Recorder({
            type: 'wav',
            sampleRate: 16000,
            bitRate: 16,
        });

        if (!rec) {
            reject(new Error('当前浏览器不支持录音功能'));
            return;
        }

        // 请求麦克风权限
        rec.open(
            function () {
                console.log('麦克风权限已获取');
                resolve();
            },
            function (msg, isUserNotAllow) {
                const errorMsg = isUserNotAllow ? '用户拒绝了麦克风权限' : '无法访问麦克风: ' + msg;
                reject(new Error(errorMsg));
            }
        );
    });
}


// 开始录音
function recStart() {
    if (!rec) {
        const error = new Error('未打开录音');
        console.error(error);
        throw error;
    }
    rec.start();
    console.log('已开始录音');

}
// 结束录音
function recStop() {
    if (!rec) {
        console.error('未打开录音');
        return;
    }
    rec.stop(
        (blob, duration) => {
            //blob就是我们要的录音文件对象,可以上传,或者本地播放
            recBlob = blob;
            //简单利用URL生成本地文件地址,此地址只能本地使用,比如赋值给audio.src进行播放,赋值给a.href然后a.click()进行下载(a需提供download="xxx.mp3"属性)
            const localUrl = (window.URL || window.webkitURL).createObjectURL(blob);
            console.log('录音成功', blob, localUrl, '时长:' + duration + 'ms');
            upload(blob)
            // rec.close(); //关闭录音,释放录音资源,当然可以不释放,后面可以连续调用start
            // rec = null;
        },
        (err) => {
            console.error('结束录音出错:' + err);
            rec.close(); //关闭录音,释放录音资源,当然可以不释放,后面可以连续调用start
            rec = null;
        },
    );
}


// 上传录音
async function upload(blob) {
    if (!blob) {
        throw new Error('无效的录音文件');
    }

    try {
        const formData = new FormData();
        formData.append('vfile', blob, 'recording.wav');
        const voiceUrl = "http://127.0.0.1:5005/voiceapi"
        axios.post(voiceUrl, formData, {
            headers: { 'Content-Type': 'multipart/form-data' },
        }).then(res => {
            console.log('上传成功!');
            console.log(res.data);
            ChatResult = res.data
        })
    } catch (error) {
        console.error('上传失败:', error);
        throw error;
    }
}

五、语音合成Chat模型回答内容

        在调用完服务端api请求得到大模型回答后,会先将文本内容显示到客户端,与此同时回答内容会发送到flask后端调用语音合成大模型,生成语音文件,代码如下:


const uploadtexttotts = (text) => {

    console.log(text);
    axios.get("https://api/tts", {
        params: {  // 正确传递GET参数
            text: text 
        },
        headers: {  // 可选:添加请求头
            'Content-Type': 'application/json'
        }
    }).then(res => {
        console.log('响应数据:', res.data);
        let filename = res.data
        let filename_get = filename.split('/')
        voiceUrl.value = "https://api/"+filename_get[filename_get.length-1]
        shouldAutoPlay.value = true; 

            // 销毁并重新创建组件
        showAudio.value = false;
        nextTick();
        showAudio.value = true;
    }).catch(error => {
        console.error('请求失败:', error);
    });
    
}

然后获取到语音文件地址会被绑定到标签上,实现自动播放,    <AudioPlayer>也是semi-ui-vue中一个组件

    <div v-if="showAudio" class="voice_box">
    <AudioPlayer
      :audioUrl="voiceUrl"
      :autoPlay="true"
      theme="light"
      @ended="shouldAutoPlay = false"
    />
        </div>

六、TTS服务、STT服务相关博客参考

Flask部署阿里开源语音大模型(STT)以及调用通义千问语音合成模型(TTS)-CSDN博客文章浏览阅读121次,点赞2次,收藏3次。为了满足实验课对人机对话系统的设计要求,我们在现有Chat对话模型的基础上进行了功能扩展。当前系统已实现基于文本输入的人机交互功能,现计划通过以下技术升级优化用户体验:首先,集成语音识别(STT)模型,将用户的语音提问实时转换为文本;然后将识别结果输入预训练的聊天大模型进行处理;最后通过语音合成(TTS)技术将模型的文本回复转换为自然语音输出。这种端到端的语音交互方案能够显著提升人机对话的自然度和沉浸感,更贴近真实的人际交流场景。 https://blog.csdn.net/ldlno/article/details/147520978?spm=1011.2415.3001.5331相关代码已开源:

springboot:

https://gitee.com/ldlnii/aichatspringboot.git

vue:

https://gitee.com/ldlnii/aichatvueproject.git

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐