BUPT Meeseeks——BUPT程序设计(STEP2:北邮通服务器开发)

目录

一、设计思路

1、功能:

2、逻辑框架

二、文字转语音

三、语音转文字

四、错字过滤

1、模型训练

2、利用模型实现过滤错字

五、相关性判断

 1、词意词向量模型:

2、调用词意词向量进行相关性判断

六、whisper模型大小切换:

        1、读取模型大小:read.py

        2、修改大小:size_change.py

七、调用api生成回答:

        1、将关键信息填写为你的申请的api:api.py

        2、写一个生成回答的函数,方便调用:answer.py 

八、调用,生成本地服务器 


注:开源地址:https://github.com/VeinsureLee/School-Meeseeks?tab=readme-ov-file#2.1

一、设计思路

1、功能:

        文字转语音

        语音转文字

        错字过滤

        相关性判断

        调用api生成回答

2、逻辑框架

        考虑到前端有俩个页面,分别是以文字为主的类似微信聊天的页面,和以语音为主的类似gpt的界面,而在文字聊天界面中的播放按钮性质特殊,综合性考虑应该将服务器接受的信息分为两类:一类是用户提问的问题消息,如,下图中的text(chat)和voice(chat);一类是用户想要语音播放的文本信息,如,下图中的chat to voice。

        故逻辑框架为:

        服务器接收用户发送文本数据提问,保存到文本历史记录文件夹,判断相关性,生成回答,返回回答。

        服务器接收用户发送语音数据提问,保存到语音历史记录文件夹,识别为文本,纠正错字,保存到文本历史记录文件夹,返回提问的问题,判断相关性,生成回答,返回回答。

        用户请求语音播放,发送chat to voice请求,服务器在临时文件夹中(change文件夹)转化为语音,作为响应返回前端。

        

二、文字转语音

        调用python的pyttvx3进行文字转语音,textToVoice.py如下:

# 该函数编写文字转语音部分
import os
import pyttsx3
import datetime


def ttv(text, VOICE_FOLDER):
    engine = pyttsx3.init()
    engine.save_to_file(text, os.path.join(VOICE_FOLDER, 'voice.mp3'))
    engine.runAndWait()

三、语音转文字

        采用openai开源的whisper模型进行语音识别,voiceToText.py如下:

# 该模块编写函数进行语音转文字
import whisper


def vtt(file_path, LANGUAGE, size):
    model = whisper.load_model(size)
    result = model.transcribe(file_path, language=LANGUAGE)
    recognized_text = result["text"]
    print(result)
    return recognized_text

四、错字过滤

1、模型训练

        采用WordVec库,训练一个模型(训练集为corpus.txt),模型名称自定义。model_training.py:

# 训练模型(拼音向量),训练集:corpus.txt
import jieba
from gensim.models import Word2Vec
# 定义读取文件和分词的函数
def read_corpus(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file:
            # 使用jieba进行分词
            words = jieba.lcut(line.strip())
            # 对分词结果进行进一步处理,将自定义词典中的词按照自定义词典的方式分开
            processed_words = []
            for word in words:
                if word in ["北京邮电大学"]:
                    processed_words.extend(["北京", "邮电", "大学"])
                else:
                    processed_words.append(word)
            yield processed_words

# 定义文件路径(假设文件名为corpus.txt)
file_path = 'corpus.txt'

# # 读取语料库并分词
sentences = list(read_corpus(file_path))

# 训练Word2Vec模型
model = Word2Vec(sentences, vector_size=100, window=5, min_count=1, workers=4)

# 保存模型
MODEL_NAME = ""         # 此处填写你所想要的模型名称
model.save(MODEL_NAME)

2、利用模型实现过滤错字

        利用训练好的模型,完成拼音过滤(训练集训练数据要足够,否则训练模型的识别效果不强)。test.py:

# 调用模型,完成拼音过滤
from gensim.models import Word2Vec
import jieba
# 加载模型
# 填写模型名称
MODEL_NAME = ""
model = Word2Vec.load(MODEL_NAME)


# 替换关键词函数
def replace_similar_pronunciation_words(sentence, target_word, threshold=0.8):
    replaced_sentence = []
    sentence_split = jieba.lcut(sentence)
    for word in sentence_split:
        if word not in model.wv:
            print(word, "pass")
            replaced_sentence.append(word)
            continue
        # 计算词语与目标词语的相似度
        similarity = abs(model.wv.similarity(word, target_word))
        print(word, similarity)
        if similarity > threshold:
            replaced_sentence.append(target_word)
        else:
            replaced_sentence.append(word)

    return ''.join(replaced_sentence)


def correct(sentence, target_words, threshold=0.6):
    for target_word in target_words:
        print("\n", target_word, "检测:")
        sentence = replace_similar_pronunciation_words(sentence, target_word, threshold)
        print("纠正", target_word, "后的句子:", sentence)
    return sentence


def replace_with_most_similar_word(sentence, threshold=0.8):
    replaced_sentence = []
    sentence_split = jieba.lcut(sentence)

    for word in sentence_split:
        if word not in model.wv:
            print("pass", word)
            replaced_sentence.append(word)
            continue

        # 查询最相似的词
        similar_words = model.wv.most_similar(word, topn=1)
        if similar_words:
            similar_word, similarity = similar_words[0]
            print(word, similar_word, similarity)
            if similarity > threshold:
                replaced_sentence.append(similar_word)
            else:
                replaced_sentence.append(word)
        else:
            replaced_sentence.append(word)

    return ''.join(replaced_sentence)


if __name__ == '__main__':
    # 测试
    sentence = input("sentence:")
    target_word = "target words:"
    print(jieba.lcut(sentence))
    replaced = correct(sentence)
    print("原句:", sentence)
    print("替换后:", replaced)



五、相关性判断

 1、词意词向量模型:

        可以自己训练,这里给出一个已经训练好的模型下载地址:

https://ai.tencent.com/ailab/nlp/en/embedding.htmlhttps://ai.tencent.com/ailab/nlp/en/embedding.html

2、调用词意词向量进行相关性判断

        此词向量模型较大,初始化时间较长。词向量测试filter.py如下:

# 词意词向量,载入embedding初始化
import numpy as np


class EmbeddingSimilarity:
    def __init__(self, embedding_file):
        print("词向量初始化中")
        self.embeddings_index = self.load_embeddings(embedding_file)
        print("词向量初始化成功")

    # 读取词向量文件
    def load_embeddings(self, embedding_file):
        embeddings_index = {}
        with open(embedding_file, 'r', encoding='utf-8') as f:
            for line in f:
                values = line.strip().split(' ')
                word = values[0]
                coefs = np.asarray(values[1:], dtype='float32')
                embeddings_index[word] = coefs
        return embeddings_index

    # 计算两个向量的余弦相似度
    def cosine_similarity(self, vector1, vector2):
        dot_product = np.dot(vector1, vector2)
        norm1 = np.linalg.norm(vector1)
        norm2 = np.linalg.norm(vector2)
        return dot_product / (norm1 * norm2)

    # 计算两个词的余弦相似度
    def calculate_similarity(self, word1, word2):
        if word1 not in self.embeddings_index or word2 not in self.embeddings_index:
            return 0.0

        word1_embedding = self.embeddings_index[word1]
        word2_embedding = self.embeddings_index[word2]

        similarity = self.cosine_similarity(word1_embedding, word2_embedding)
        return similarity


if __name__ == "__main__":
    # 填写模型名称
    embedding_file = ""
    similarity_calculator = EmbeddingSimilarity(embedding_file)

    word1 = "北京"
    word2 = "上海"

    similarity = similarity_calculator.calculate_similarity(word1, word2)
    print(f"词 '{word1}' 和词 '{word2}' 的相关性:{similarity}")

    while True:
        word1 = input("请输入第一个词:")
        word2 = input("请输入第二个词:")

        similarity = similarity_calculator.calculate_similarity(word1, word2)
        print(f"词 '{word1}' 和词 '{word2}' 的相关性:{similarity}")


六、whisper模型大小切换:

        1、读取模型大小:read.py

# 读取项目文件夹
import os


def read_size_txt(folder_path, text_name):
    # 检查文件夹路径是否存在
    if not os.path.exists(folder_path):
        print("文件夹路径不存在")
        return None

    # 构造 size.txt 文件的完整路径
    file_path = os.path.join(folder_path, text_name)

    # 检查文件是否存在
    if not os.path.isfile(file_path):
        print("size.txt 文件不存在")
        return None

    # 读取 size.txt 文件的内容并返回
    with open(file_path, 'r') as f:
        return f.read()


def modify_size_txt(folder_path, new_content, text_name):
    # 构造 size.txt 文件的完整路径
    file_path = os.path.join(folder_path, text_name)

    # 检查文件是否存在
    if not os.path.isfile(file_path):
        print(text_name + "文件不存在")
        return

    # 写入新内容到 size.txt 文件中
    with open(file_path, 'w') as f:
        f.write(new_content)

    print(text_name + "文件内容已修改")

        2、修改大小:size_change.py

# 转化模型大小函数
def size_change(text):
    if text[:4] == "切换模型":
        if text[4:] == '微型':
            return 'tiny'
        elif text[4:] == '基本':
            return 'base'
        elif text[4:] == '小型':
            return 'small'
        elif text[4:] == '中型':
            return 'medium'
        elif text[4:] == '大型':
            return 'large'
        else:
            return 0
    return 0


七、调用api生成回答:

        1、将关键信息填写为你的申请的api:api.py

# coding: utf-8
import _thread as thread
import os
import time
import base64

import base64
import datetime
import hashlib
import hmac
import json
from urllib.parse import urlparse
import ssl
from datetime import datetime
from time import mktime
from urllib.parse import urlencode
from wsgiref.handlers import format_date_time

import websocket
import openpyxl
from concurrent.futures import ThreadPoolExecutor, as_completed
import os

myans = ""


class Ws_Param(object):

    # 初始化
    def __init__(self, APPID, APIKey, APISecret, gpt_url):
        self.APPID = APPID
        self.APIKey = APIKey
        self.APISecret = APISecret
        self.host = urlparse(gpt_url).netloc
        self.path = urlparse(gpt_url).path
        self.gpt_url = gpt_url

    # 生成url
    def create_url(self):
        # 生成RFC1123格式的时间戳
        now = datetime.now()
        date = format_date_time(mktime(now.timetuple()))

        # 拼接字符串
        signature_origin = "host: " + self.host + "\n"
        signature_origin += "date: " + date + "\n"
        signature_origin += "GET " + self.path + " HTTP/1.1"

        # 进行hmac-sha256进行加密
        signature_sha = hmac.new(self.APISecret.encode('utf-8'), signature_origin.encode('utf-8'),
                                 digestmod=hashlib.sha256).digest()

        signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding='utf-8')

        authorization_origin = f'api_key="{self.APIKey}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature_sha_base64}"'

        authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')

        # 将请求的鉴权参数组合为字典
        v = {
            "authorization": authorization,
            "date": date,
            "host": self.host
        }
        # 拼接鉴权参数,生成url
        url = self.gpt_url + '?' + urlencode(v)
        # 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致
        return url


# 收到websocket错误的处理
def on_error(ws, error):
    print("### error:", error)


# 收到websocket关闭的处理
def on_close(ws):
    print("### closed ###")


# 收到websocket连接建立的处理
def on_open(ws):
    thread.start_new_thread(run, (ws,))


def run(ws, *args):
    data = json.dumps(gen_params(appid=ws.appid, query=ws.query, domain=ws.domain))
    ws.send(data)


# 收到websocket消息的处理

def on_message(ws, message):
    global myans
    # print(message)
    data = json.loads(message)
    code = data['header']['code']
    if code != 0:
        print(f'请求错误: {code}, {data}')
        ws.close()
    else:
        choices = data["payload"]["choices"]
        status = choices["status"]
        content = choices["text"][0]["content"]

        myans += content

        print(content, end='')
        if status == 2:
            print("#### 关闭会话")
            ws.close()


def gen_params(appid, query, domain):
    """
    通过appid和用户的提问来生成请参数
    """

    data = {
        "header": {
            "app_id": appid,
            "uid": "1234",
            # "patch_id": []    #接入微调模型,对应服务发布后的resourceid
        },
        "parameter": {
            "chat": {
                "domain": domain,
                "temperature": 0.5,
                "max_tokens": 512,
                "auditing": "default",
            }
        },
        "payload": {
            "message": {
                "text": [
                    {"role": "system", "content": "你是北京邮电大学的导游,和你对话的是北邮的新生,你只能回答与北邮相关的问题,"
                                                  "其余的问题一律回答:抱歉,请提问与北邮有关的问题,我很乐意为你解答。"
                                                  "请注意,你是一个专业的导游,用词应该热情、严谨。"
                                                  "如果提问是一个开放的问题,请默认将背景设定为北邮。"},
                    {"role": "user", "content": query}
                ]
            }
        }
    }
    return data


def require_ans(appid, api_secret, api_key, gpt_url, domain, query):
    global myans
    wsParam = Ws_Param(appid, api_key, api_secret, gpt_url)
    websocket.enableTrace(False)
    wsUrl = wsParam.create_url()

    ws = websocket.WebSocketApp(wsUrl, on_message=on_message, on_error=on_error, on_close=on_close, on_open=on_open)
    ws.appid = appid
    ws.query = query
    ws.domain = domain
    ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
    cur_ans = myans
    # ttv(cur_ans)
    myans = ""
    return cur_ans


if __name__ == "__main__":
    require_ans(
        # 填写你的 相关调用
        appid="",
        api_secret="",
        api_key="",
        # appid、api_secret、api_key三个服务认证信息请前往开放平台控制台查看(https://console.xfyun.cn/services/bm35)
        gpt_url="wss://spark-api.xf-yun.com/v3.5/chat",
        # Spark_url = "ws://spark-api.xf-yun.com/v3.1/chat"  # v3.0环境的地址
        # Spark_url = "ws://spark-api.xf-yun.com/v2.1/chat"  # v2.0环境的地址
        # Spark_url = "ws://spark-api.xf-yun.com/v1.1/chat"  # v1.5环境的地址
        domain="generalv3.5",
        # domain = "generalv3"    # v3.0版本
        # domain = "generalv2"    # v2.0版本
        # domain = "general"    # v2.0版本
        # query="请帮我写一篇100字的《平凡的世界》的读后感"
        query=input()
    )

        2、写一个生成回答的函数,方便调用:answer.py 

# 生成回答函数,方便调用
from api import *


def answer(recognized_text):
    my_ans = require_ans(
        appid="",
        api_secret="",
        api_key="",
        # appid、api_secret、api_key三个服务认证信息请前往开放平台控制台查看(https://console.xfyun.cn/services/bm35)
        gpt_url="wss://spark-api.xf-yun.com/v3.5/chat",
        # Spark_url = "ws://spark-api.xf-yun.com/v3.1/chat"  # v3.0环境的地址
        # Spark_url = "ws://spark-api.xf-yun.com/v2.1/chat"  # v2.0环境的地址
        # Spark_url = "ws://spark-api.xf-yun.com/v1.1/chat"  # v1.5环境的地址
        domain="generalv3.5",
        # domain = "generalv3"    # v3.0版本
        # domain = "generalv2"    # v2.0版本
        # domain = "general"    # v2.0版本
        query=recognized_text
    )
    return my_ans

八、调用,生成本地服务器 

        备注:此处在接受前端信息时,如果前端想切换whisper识别的模型(即想要更快或者更精准的识别结果时),可以直接发送:切换模型+模型尺寸即可进行切换。

        server.py:

# 最终服务器
from http.server import BaseHTTPRequestHandler, HTTPServer
import cgi
from api import *
from filter import filter_text
from textToVoice import ttv
from voiceToText import vtt
from answer import answer
from size_change import size_change
from read import read_size_txt
from read import modify_size_txt
from wordVec import EmbeddingSimilarity
from test import correct


# 设置文件夹
HISTORY_FOLDER                      = 'history'
HISTORY_ASK_FOLDER                  = 'history/ask'
HISTORY_ANSWER_TEXT_FOLDER          = 'history/answer/text'
HISTORY_ANSWER_VOICE_FOLDER         = 'history/answer/voice'
HISTORY_ASK_TEXT_FOLDER             = 'history/ask/text'
HISTORY_ASK_VOICE_FOLDER            = 'history/ask/voice'
CHANGE_FOLDER                       = 'change'
CHANGE_TEXT_FOLDER                  = 'change/text'
CHANGE_VOICE_FOLDER                 = 'change/voice'
MODEL_SIZE                          = 'model_size'
MODEL_SIZE_TXT                      = 'size.txt'
EMBEDDINGFILE_TXT                   = ""                    # 查看wordVectxt文件夹里面的下载链接
WRONG_WORDS_TXT                     = ""                    # 快速查询错误单词
RIGHT_WORDS_TXT                     = ""                    # 快速查询正确单词
BETWEEN_R_W_WORDS_TXT               = ""                    # 模糊词


class RequestHandler(BaseHTTPRequestHandler):
    embedding_sim = None

    def _set_headers(self, status_code=200, content_type='application/json'):
        self.send_response(status_code)
        self.send_header('Content-type', content_type)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.end_headers()

    def do_OPTIONS(self):
        self._set_headers()

    # 服务器向前端传输数据
    def do_GET(self):
        if self.path.startswith(CHANGE_VOICE_FOLDER + '/voice.mp3'):
            try:
                with open(os.path.join(CHANGE_VOICE_FOLDER, 'voice.mp3'), 'rb') as f:
                    self._set_headers(status_code=200, content_type='audio/mpeg')
                    self.wfile.write(f.read())
            except FileNotFoundError:
                self._set_headers(status_code=404)
                self.wfile.write(json.dumps({"error": "File not found"}).encode('utf-8'))
        else:
            self._set_headers(404)
            self.wfile.write(json.dumps({"error": "Not found"}).encode('utf-8'))

    def do_POST(self):
        # 上传数据为文本时,将其语音播放(文字转语音)
        if self.path == CHANGE_TEXT_FOLDER:
            content_length = int(self.headers['Content-Length'])
            post_data = self.rfile.read(content_length)
            data = json.loads(post_data.decode('utf-8'))

            text = data.get('text')
            print(text)

            if text:
                # 保存
                file_text_name = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.txt'
                file_text_path = f'./change/{file_text_name}'
                with open(file_text_path, 'w', encoding='utf-8') as file:
                    file.write(text)

                # 转化
                ttv(text, CHANGE_VOICE_FOLDER)

                self._set_headers()
                self.wfile.write(json.dumps({"message": "Text received and processed"}).encode('utf-8'))
            else:
                self._set_headers(400)
                self.wfile.write(json.dumps({"error": "Text not provided"}).encode('utf-8'))

        # 上传数据为语音时将其转化为文本并保存
        elif self.path == HISTORY_ASK_VOICE_FOLDER:
            self._set_headers()
            form = cgi.FieldStorage(
                fp=self.rfile,
                headers=self.headers,
                environ={'REQUEST_METHOD': 'POST'}
            )
            uploaded_file = form['audio']
            file_voice_name = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.mp3'
            file_voice_path = f'./history/ask/voice/{file_voice_name}'
            with open(file_voice_path, 'wb') as f:
                f.write(uploaded_file.file.read())

            # 使用 Whisper 模型进行语音识别
            size = read_size_txt(MODEL_SIZE, MODEL_SIZE_TXT)
            recognized_text = vtt(file_voice_path, "Chinese", size)
            recognized_text = correct(recognized_text)
            file_text_name = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.txt'
            file_text_path = f'./history/ask/text/{file_text_name}'
            with open(file_text_path, 'w', encoding='utf-8') as file:
                file.write(recognized_text)

            # 将识别出的文本作为响应发送给前端
            self.wfile.write(recognized_text.encode())
        # 上传数据处理为文本后,调用api生成回答
        elif self.path == HISTORY_ASK_TEXT_FOLDER:
            content_length = int(self.headers['Content-Length'])
            post_data = self.rfile.read(content_length)
            data = json.loads(post_data.decode('utf-8'))

            text = data.get('text')

            file_text_name = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.txt'
            file_text_path = f'./history/ask/text/{file_text_name}'
            with open(file_text_path, 'w', encoding='utf-8') as file:
                file.write(text)

            if text:
                if size_change(text) != 0:
                    modify_size_txt(MODEL_SIZE, size_change(text), MODEL_SIZE_TXT)
                    ans = '成功切换模型' + size_change(text)
                else:
                    # 检查问题是否与指定内容相关
                    similarity_result = filter_text(HISTORY_ASK_TEXT_FOLDER, file_text_name, self.embedding_sim,
                                                    RIGHT_WORDS_TXT, WRONG_WORDS_TXT, BETWEEN_R_W_WORDS_TXT)
                    if similarity_result == 1:
                        ans = answer(text)
                    else:
                        ans = '抱歉,我只能回答与   的问题'   # 空格处替换为关键词
                file_answer_name = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.txt'
                file_text_path = f'./history/answer/text/{file_answer_name}'
                with open(file_text_path, 'w', encoding='utf-8') as file:
                    file.write(ans)
                # 将回答作为json响应返回
                self._set_headers()
                self.wfile.write(json.dumps(ans).encode('utf-8'))
            else:
                self._set_headers(400)
                self.wfile.write(json.dumps({"error": "Text not provided"}).encode('utf-8'))
        else:
            self._set_headers(404)
            self.wfile.write(json.dumps({"error": "Not found"}).encode('utf-8'))


def run(server_class=HTTPServer, handler_class=RequestHandler, port=8000):
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    print(f'Starting server on port {port}')
    # 初始化 EmbeddingSimilarity 类
    embedding_file = EMBEDDINGFILE_TXT
    RequestHandler.embedding_sim = EmbeddingSimilarity(embedding_file)
    httpd.serve_forever()


if __name__ == '__main__':
    run()
  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值