本地搭建支持语音和文本的中英文翻译服务-含全部源代码

news2024/12/25 14:55:12

实现目标

1、支持文本中英文互译;
2、支持中文语音输入;
3、支持英文语言输入;
进阶(未实现)
4、优化web界面;
5、优化语音输入js实现逻辑;
6、增加语音输入自纠错模型,纠正语音识别输出;
7、增加中文文本转语音输出;
8、增加英语文本转语音输出。

环境

在实现语音识别前,需要获取符合语音识别模型格式的语音文件。按照要求,需要提供wav格式,采样频率为16000Hz的音频文件。而通过web API navigator.mediaDevices.getUserMedia获取到的音频文件是webm格式的。如果需要通过web获取wav格式的音频文件,可能比较复杂。因此,通过在后端使用ffmpeg将前端上传的webm格式的音频文件转为wav格式。

ffmpeg -i input_file.webm -ar 16000 output_file.wav

在这里插入图片描述
在实际测试过程中,可以将获取到的音频文件直接用notepad++打开查看文件开头,来判断文件的类型。以防外部修改导致的文件格式与文件后缀不一致导致的执行结果混乱的问题。

wav文件:
在这里插入图片描述
webm文件:
在这里插入图片描述

除了ffmpeg之外的其他重要环境:

Python 3.11.2
Flask 3.0.3
torch 2.3.0
torchaudio 2.3.0
transformers 4.41.2
PySoundFile 0.9.0.post1

实现逻辑

在这里插入图片描述

基本实现逻辑是web前端-文本-翻译-翻译结果-返回前端,为了使用的方便,增加了语音输入。语音输入的输出也相当于web前端的文本输入。

在语音输入实现当中,上传语言文件至输出结果最大时间由record.js文件控制。比如,record.js文件当中设置的最大语音识别时间是1秒。那么,如果语音识别结果在1秒之后输出,web页面将无法获取语音识别结果。(语音识别的实现不足:1、识别精准度不足;2、web页面动态更新识别结果实现不完善)

所有源代码

一、项目代码目录结构:
在这里插入图片描述

models文件夹:

ch-en中英文文本翻译模型
ch-voi-text中文语音识别模型
en-ch英中文文本翻译模型
en-voi-text英文语音识别模型
在这里插入图片描述

src文件夹:

上传语音文件和logo
在这里插入图片描述

static文件夹:

css文件、fonts字体文件、js文件
在这里插入图片描述
record.js

const recordBtn = document.querySelector(".record-btn")
const player = document.querySelector(".audio-player")
// const download = document.querySelector('#download')
function getLastSegment(url) {
    // 使用URL API来解析URL
    const urlObj = new URL(url);
    // 获取路径部分并去除开头的斜杠(如果有)
    const path = urlObj.pathname.replace(/^\//, '');
    // 分割路径并返回最后一段
    return path.split('/').pop();
}
function areLastSegmentsEqual(url1, url2) {
    // 获取两个URL的最后一段
    const segment1 = getLastSegment(url1);
    const segment2 = getLastSegment(url2);
    // 比较它们是否相同
    return segment1 === segment2;
}

if (navigator.mediaDevices.getUserMedia) {
    let audioChunks = []
    // 约束属性
    const constraints = {
        // 音频约束
        audio: {
            sampleRate: 16000, // 采样率
            sampleSize: 16, // 每个采样点大小的位数
            channelCount: 1, // 通道数
            volume: 1, // 从 0(静音)到 1(最大音量)取值,被用作每个样本值的乘数
            echoCancellation: true, // 开启回音消除
            noiseSuppression: true, // 开启降噪功能
        },
        // 视频约束
        video: false
    }
    // 请求获取音频流
    navigator.mediaDevices.getUserMedia(constraints)
        .catch(err => serverLog("ERROR mediaDevices.getUserMedia: ${err}"))
        .then(stream => {// 在此处理音频流
            // 创建 MediaRecorder 实例
            const mediaRecorder = new MediaRecorder(stream)
            // 点击按钮
            recordBtn.onclick = () => {
                if (mediaRecorder.state === "recording") {
                    // 录制完成后停止
                    mediaRecorder.stop()
                    recordBtn.textContent = "录音结束"
                }
                else {
                    // 开始录制
                    mediaRecorder.start()
                    recordBtn.textContent = "录音中..."
                }
            }
            mediaRecorder.ondataavailable = e => {
                audioChunks.push(e.data)
            }
            // 结束事件
            mediaRecorder.onstop = e => {
                // 将录制的数据组装成 Blob(binary large object) 对象(一个不可修改的存储二进制数据的容器)
                const blob = new Blob(audioChunks, { type: "audio/webm" })
                audioChunks = []
                const audioURL = window.URL.createObjectURL(blob)
                // 赋值给一个 <audio> 元素的 src 属性进行播放
                player.src = audioURL
                // // 添加下载功能
                // download.innerHTML = '下载'
                // download.href = audioURL
                // 将文件回传
                // 准备 FormData 对象用于文件上传
                const formData = new FormData();
                // 添加 Blob 到 FormData,并为其指定一个名称(这里假设服务器期望的字段名为 'audioFile')
                formData.append('audioFile', blob, 'recording.webm'); // 'recording.webm' 是文件的建议名称,不是必须
                // 使用 fetch API 发送文件到服务器
                fetch('/upload-url', { // 请替换为您的上传 URL
                    method: 'POST',
                    body: formData
                })
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Network response was not ok');
                    }
                    return response.text(); // 或者返回 response.json() 如果服务器返回 JSON
                })
                .then(data => {
                    console.log('Upload successful:', data);
                    let textarea = document.getElementById('inputQuestion');
                    textarea.readOnly = true;
                        setTimeout(function() {
                                window.location.reload();
                            }, 1000); // 等待 1 秒后刷新页面
                    })
                    // setInterval(function () {
                    //     const currentUrl = window.location.href;
                    //     alert(currentUrl)
                    // }, 2000); // 每秒/1000检查一次
                    // })
                .catch(error => {
                    console.error('There has been a problem with your fetch operation:', error);
                });
            }
        },
            () => {
                console.error("授权失败!");
            }
        );
} else {
    console.error("该浏览器不支持 getUserMedia!");
}

templates文件夹:

所有web页面的html文件
在这里插入图片描述

home.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>开始页面</title>
    <link rel="stylesheet" href="/static/css/bootstrap.css">
  <style>
    /*static 文件夹是默认用于存放静态文件的,比如 CSS、JavaScript、图片和字体文件等。
    Flask 会自动为 static 文件夹下的所有文件提供静态文件的路由,使得这些文件可以被直接访问,
    而不需要你为每个文件单独编写路由。*/
    @font-face {
        font-family: 'KingHwa'; /* 自定义字体名称 */
        /*此处将字体文件加入到static文件夹当中,就省去了编写路由的工作,ttf文件对应路由格式truetype*/
        src: url('../static/fonts/KingHwa_OldSong.ttf') format('truetype');/* 字体文件路径和格式 */
        font-weight: normal;
        font-style: normal;
    }
    body {
      background-color: rgba(173, 216, 230, 0.5); /*设置页面背景颜色*/
      font-family: "KingHwa", sans-serif; /*设置字体*/
    }
    .center-image {
      /*position: fixed;*/
      display: block;
      margin-top: 4%;
      margin-left: 40%;
      margin-right: 40%;
      border-radius: 4%; /* 设置圆角大小 */
      width: 20%; /* 你可以根据需要调整宽度 */
    }
    .center-bnt {
      /*position: fixed;*/
      display: block;
      {#margin-top: 10%;#}
      margin-top: 5%;
      margin-left: 45%;
      margin-right: 45%;
      width: 10%; /* 你可以根据需要调整宽度 */
    }
    .rounded-font {
      display: block;
      margin-top: 8%;
      border-radius: 2%; /* 设置圆角大小 */
      font-size: 360%; /* 设置字体大小 */
      text-align: center; /* 将文本居中 */
    }
    #backToTop {
      position: fixed;
      bottom: 20px;
      right: 30px;
      z-index: 99;
      border: none;
      outline: 1px solid black;/*设置轮廓*/
      background-color: rgba(0, 0, 230, 0.5);
      color: white;
      cursor: pointer;
      padding: 4px 5px;
      border-radius: 2px;/*设置圆角*/
    }
  </style>
</head>
<h1 class="rounded-font">中英文翻译</h1>
<img src="{{ url_for('send_image', path='src/translate.jpg') }}"
     alt="中英文翻译"
     class="center-image"
     style="margin-bottom:5%">
<body>
    <form action='/home' style="width:70%; margin:0 auto;" method="post">
      <button type="submit"
              class="btn btn-primary btn-dark"
              style="font-size: 300%; width: 30%; margin-left:15%; margin-right:5%;"
              name="choice" value="ch2en">中译英</button>
      <button type="submit"
              class="btn btn-primary btn-light"
              style="font-size: 300%; width: 30%; margin-left:5%;"
              name="choice" value="en2ch">英译中</button>
    </form>
</body>
</html>

en2ch.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>英译中</title>
    <link rel="stylesheet" href="/static/css/bootstrap.css">
  <style>
    /*static 文件夹是默认用于存放静态文件的,比如 CSS、JavaScript、图片和字体文件等。
    Flask 会自动为 static 文件夹下的所有文件提供静态文件的路由,使得这些文件可以被直接访问,
    而不需要你为每个文件单独编写路由。*/
    @font-face {
        font-family: 'KingHwa'; /* 自定义字体名称 */
        /*此处将字体文件加入到static文件夹当中,就省去了编写路由的工作,ttf文件对应路由格式truetype*/
        src: url('../static/fonts/KingHwa_OldSong.ttf') format('truetype');/* 字体文件路径和格式 */
        font-weight: normal;
        font-style: normal;
    }
    body {
      background-color: rgba(173, 216, 230, 0.5); /*设置页面背景颜色*/
      font-family: "KingHwa", sans-serif; /*设置字体*/
    }
    .center-image {
      /*position: fixed;*/
      display: block;
      margin-top: 4%;
      margin-left: 40%;
      margin-right: 40%;
      border-radius: 4%; /* 设置圆角大小 */
      width: 20%; /* 你可以根据需要调整宽度 */
    }
    .center-bnt {
      /*position: fixed;*/
      display: block;
      margin-left: 45%;
      margin-right: 45%;
      width: 10%; /* 你可以根据需要调整宽度 */
    }
    .rounded-font {
      display: block;
      margin-top: 4%;
      border-radius: 2%; /* 设置圆角大小 */
      font-size: 360%; /* 设置字体大小 */
      text-align: center; /* 将文本居中 */
    }
    #backToTop {
      position: fixed;
      bottom: 20px;
      right: 30px;
      z-index: 99;
      border: none;
      outline: 1px solid black;/*设置轮廓*/
      background-color: rgba(0, 0, 230, 0.5);
      color: white;
      cursor: pointer;
      padding: 4px 5px;
      border-radius: 2px;/*设置圆角*/
    }
    .default-img {
      /*position: fixed;*/
      display: block;
      {#margin-top: 10%;#}
      {#margin-top: 5%;#}
      margin-left: 30%;
      margin-right: 30%;
      width: 20%; /* 你可以根据需要调整宽度 */
      border-radius: 2%;/*设置圆角*/
    }
    .back-home {
        position: fixed;
        bottom: 15px; /* 初始时,将元素移出视口 */
        right: 100px;
        /* 其他样式 */
    }
    .bottom_left {
        position: fixed;
        bottom: 15px; /* 初始时,将元素移出视口 */
        left: 100px;
        /* 其他样式 */
    }
  </style>
</head>
<h1 class="rounded-font">英译中</h1>
<body>
    <form action="/en2ch" method="post" enctype = "multipart/form-data">
      <div class="row" style="margin-left:5%;">
        <div class="mb-3">
          <label for="inputQuestion" class="form-label">输入:</label>
          <textarea class="form-control"
                    id="inputQuestion"
                    rows="10" style="width: 90%;"
                    name="inputTxt">{{ data.input }}</textarea>
        </div>
        <div class="mb-3">
          <audio controls class="audio-player"
                 style="width: 20%; margin-left: 1%; vertical-align: middle;"></audio>
          <button type="button"
                  style="font-size: 150%; width:10%; margin-left: 1%;"
                  class="btn btn-primary record-btn">录音</button>
          <button type="submit"
                  class="btn btn-primary"
                  style="font-size: 150%; width: 10%; margin-left: 46%;">提交文本</button>
        </div>
        <div class="mb-3">
          <label for="outputQuestion" class="form-label">输出:</label>
          <textarea class="form-control"
                    id="outputQuestion"
                    rows="10" style="width: 90%;"
                    readonly>{{ data.output }}</textarea>
        </div>
      </div>
    </form>
    <br/>
    <br/>
    <button onclick="goToLink()"
            class="btn btn-primary btn-info center-bnt">返回首页</button>
    <script>
    function goToLink() {
        window.location.href = "{{ url_for('home') }}"
    }
    </script>
    <script src="../static/js/record.js"></script>
</body>
</html>

ch2en.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>中译英</title>
    <link rel="stylesheet" href="/static/css/bootstrap.css">
  <style>
    /*static 文件夹是默认用于存放静态文件的,比如 CSS、JavaScript、图片和字体文件等。
    Flask 会自动为 static 文件夹下的所有文件提供静态文件的路由,使得这些文件可以被直接访问,
    而不需要你为每个文件单独编写路由。*/
    @font-face {
        font-family: 'KingHwa'; /* 自定义字体名称 */
        /*此处将字体文件加入到static文件夹当中,就省去了编写路由的工作,ttf文件对应路由格式truetype*/
        src: url('../static/fonts/KingHwa_OldSong.ttf') format('truetype');/* 字体文件路径和格式 */
        font-weight: normal;
        font-style: normal;
    }
    body {
      background-color: rgba(173, 216, 230, 0.5); /*设置页面背景颜色*/
      font-family: "KingHwa", sans-serif; /*设置字体*/
    }
    .center-image {
      /*position: fixed;*/
      display: block;
      margin-top: 4%;
      margin-left: 40%;
      margin-right: 40%;
      border-radius: 4%; /* 设置圆角大小 */
      width: 20%; /* 你可以根据需要调整宽度 */
    }
    .center-bnt {
      /*position: fixed;*/
      display: block;
      margin-left: 45%;
      margin-right: 45%;
      width: 10%; /* 你可以根据需要调整宽度 */
    }
    .rounded-font {
      display: block;
      margin-top: 4%;
      border-radius: 2%; /* 设置圆角大小 */
      font-size: 360%; /* 设置字体大小 */
      text-align: center; /* 将文本居中 */
    }
    #backToTop {
      position: fixed;
      bottom: 20px;
      right: 30px;
      z-index: 99;
      border: none;
      outline: 1px solid black;/*设置轮廓*/
      background-color: rgba(0, 0, 230, 0.5);
      color: white;
      cursor: pointer;
      padding: 4px 5px;
      border-radius: 2px;/*设置圆角*/
    }
    .default-img {
      /*position: fixed;*/
      display: block;
      {#margin-top: 10%;#}
      {#margin-top: 5%;#}
      margin-left: 30%;
      margin-right: 30%;
      width: 20%; /* 你可以根据需要调整宽度 */
      border-radius: 2%;/*设置圆角*/
    }
    .back-home {
        position: fixed;
        bottom: 15px; /* 初始时,将元素移出视口 */
        right: 100px;
        /* 其他样式 */
    }
    .bottom_left {
        position: fixed;
        bottom: 15px; /* 初始时,将元素移出视口 */
        left: 100px;
        /* 其他样式 */
    }
  </style>
</head>
<h1 class="rounded-font">中译英</h1>
<body>
    <form action="/ch2en" method="post" enctype = "multipart/form-data">
      <div class="row" style="margin-left:5%;">
        <div class="mb-3">
          <label for="inputQuestion" class="form-label">输入:</label>
          <textarea class="form-control"
                    id="inputQuestion"
                    rows="10" style="width: 90%;"
                    name="inputTxt">{{ data.input }}</textarea>
        </div>
        <div class="mb-3">
          <audio controls class="audio-player"
                 style="width: 20%; margin-left: 1%; vertical-align: middle;"></audio>
          <button type="button"
                  style="font-size: 150%; width:10%; margin-left: 1%;"
                  class="btn btn-primary record-btn">录音</button>
          <button type="submit"
                  class="btn btn-primary"
                  style="font-size: 150%; width: 10%; margin-left: 46%;">提交文本</button>
        </div>
        <div class="mb-3">
          <label for="outputQuestion" class="form-label">输出:</label>
          <textarea class="form-control"
                    id="outputQuestion"
                    rows="10" style="width: 90%;"
                    readonly>{{ data.output }}</textarea>
        </div>
      </div>
    </form>
    <br/>
    <br/>
    <button onclick="goToLink()"
            class="btn btn-primary btn-info center-bnt">返回首页</button>
    <script>
    function goToLink() {
        window.location.href = "{{ url_for('home') }}"
    }
    </script>
    <script src="../static/js/record.js"></script>
</body>
</html>

app.py

import os
from flask import Flask, redirect, render_template, request, send_file, session, url_for
from voi_2_text import *
from translate_ch5en import *
from my_util import Logger

loger = Logger()

app = Flask(__name__)
app.secret_key = 'RyVzs9ObLV5wsTDHN0h6X1VP1jmi6UgYNGWZXPgNwKI='

UPLOAD_FOLDER = os.path.join(os.path.join(os.getcwd(), 'src', 'upload-audio'))
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

init_voice_recognize_models()  # 初始化中英语音识别模型
init_text_translate_models()  # 初始化中英文翻译模型

@app.route('/src/<path:path>')#网页的所有文件都是来自服务器
def send_image(path):
    return send_file(path, mimetype='image/jpeg')

@app.route('/')
def hello_world():  # put application's code here
    session.clear()
    loger.debug('Hello World!')
    return redirect('/home')


@app.route('/home', methods=['GET', 'POST'])
def home():
    if request.method == 'POST':
        session.clear()
        if request.form.get('choice') == 'ch2en':
            loger.info('choice is chinese translate to english')
            return redirect('/ch2en')
        elif request.form.get('choice') == 'en2ch':
            loger.info('choice is english translate to chinese')
            return redirect('/en2ch')
        else:
            loger.info('unsupported choice')
            return redirect('/home')
    return render_template('home.html')

@app.route('/upload-url', methods=['POST'])  # 访问的路径
def upload_url(): # put application's code here# '
    if request.method == "POST":
        if request.files.get("audioFile"): # !!!!!!!!!!!注意保存的录音文件还不是wav格式的,应该是哪里出错了20240526-2221
            audio_file = request.files["audioFile"]
            if audio_file is not None:
                loger.info(f"get voice ok, file length {audio_file.content_length}")
                loger.info(f"get file name {audio_file.filename}")
                # 设置保存文件的路径
                file_path = os.path.join(app.config['UPLOAD_FOLDER'], audio_file.filename)
                # 保存文件
                audio_file.save(file_path)
                loger.info(f"save file path {file_path}")
                loger.info(f"save file {audio_file.filename} length: {os.path.getsize(file_path)} bytes")
                if session.get('previous_route', 'unknown') == "ch2en":
                    result = convert_ch_voi2text(file_path)
                    loger.info(f"recognized [ch2en] result: {result[0]}")
                    session['input'] = result[0]
                elif session.get('previous_route', 'unknown') == "en2ch":
                    result = convert_en_voi2text(file_path)
                    loger.info(f"recognized [en2ch] result: {result[0]}")
                    session['input'] = result[0]
                else:
                    loger.warning(f"unsupported previous route: {session.get('previous_route', 'unknown')}")
            else:
                loger.error("empty")
    loger.debug(f"previous route is {session.get('previous_route', 'unknown')}")
    return redirect(url_for(session.get('previous_route', 'unknown')))

@app.route('/ch2en', methods=['GET', 'POST'])
def ch2en():
    loger.info(f'now in chinese translate to english')
    data = {'input': session.get('input', '你好'), 'output': 'hello'}
    if request.method == 'POST':
        session.clear()
        question = request.form.get("inputTxt")
        loger.info(f"get input text {question}")
        translate = translate_ch2en(question)
        data = {'input': question, 'output': translate}
    session['previous_route'] = 'ch2en'
    return render_template('ch2en.html', data=data)

@app.route('/en2ch', methods=['GET', 'POST'])
def en2ch():
    loger.info(f'now in english translate to chinese')
    data = {'input': session.get('input', 'hello'), 'output': '你好'}
    if request.method == 'POST':
        session.clear()
        question = request.form.get("inputTxt")
        loger.info(f"get input text {question}")
        translate = translate_en2ch(question)
        data = {'input': question, 'output': translate}
    session['previous_route'] = 'en2ch'
    return render_template('en2ch.html', data=data)


if __name__ == '__main__':
    app.run(debug=False)

my_util.py

#进度条
import os
import sys
import time
import shutil
import logging
import time
from datetime import datetime

def print_progress_bar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█', print_end="\r"):
    """
    调用在Python终端中打印自定义进度条的函数
    iteration - 当前迭代(Int)
    total - 总迭代(Int)
    prefix - 前缀字符串(Str)
    suffix - 后缀字符串(Str)
    decimals - 正数的小数位数(Int)
    length - 进度条的长度(Int)
    fill - 进度条填充字符(Str)
    print_end - 行尾字符(Str)
    """
    percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
    filled_length = int(length * iteration // total)
    bar = fill * filled_length + '-' * (length - filled_length)
    print(f'\r{prefix} |{bar}| {percent}% {suffix}', end=print_end)
    # 打印新行,完成进度条
    if iteration == total:
        print()

class Logger(object):
    """
    终端打印不同颜色的日志
    """
    ch = logging.StreamHandler()  # 创建日志处理器对象,在__init__外创建,是类当中的静态属性,不是__init__中的实例属性

    # #创建静态的日志处理器可以减少内存消耗

    # # 创建 FileHandler 实例,指定日志文件路径
    # ch = logging.FileHandler(filename='app1.log')

    def __init__(self):
        self.logger = logging.getLogger()  # 创建日志记录对象
        self.logger.setLevel(logging.INFO)  # 设置日志等级info,其他低于此等级的不打印

    def debug(self, message):
        self.fontColor('\033[0;37m%s\033[0m')
        self.logger.debug(message)

    def info(self, message):
        self.fontColor('\033[0;32m%s\033[0m')
        self.logger.info(message)

    def warning(self, message):
        self.fontColor('\033[0;33m%s\033[0m')
        self.logger.warning(message)

    def error(self, message):
        self.fontColor('\033[0;31m%s\033[0m')
        self.logger.error(message)

    def fontColor(self, color):
        formatter = logging.Formatter(color % '%(asctime)s - %(name)s - %(levelname)s - %(message)s')  # 控制日志输出颜色
        self.ch.setFormatter(formatter)
        self.logger.addHandler(self.ch)  # 向日志记录对象中加入日志处理器对象


def delete_files(folder_path, max_files):
    """
    监控指定文件夹中的文件数量,并在超过max_files时删除最旧的文件。
    """
    print("进入删除图片文件夹"+folder_path)
    print("需要删除文件数量")
    print(max_files)
    if True:
        # 获取文件夹中的文件列表
        files = os.listdir(folder_path)
        file_count = len(files)
        print(f"当前文件夹 {folder_path} 中的文件数量: {file_count}")

        # 如果文件数量超过max_files,则删除最旧的文件
        if file_count > max_files:
            # 获取文件夹中所有文件的完整路径,并带上修改时间
            file_paths_with_mtime = [(os.path.join(folder_path, f), os.path.getmtime(os.path.join(folder_path, f))) for
                                     f in files]
            # 按修改时间排序
            sorted_files = sorted(file_paths_with_mtime, key=lambda x: x[1])

            # 删除最旧的文件,直到文件数量在阈值以下
            for file_path, mtime in sorted_files[:file_count - max_files]:
                try:
                    os.remove(file_path)
                    print(f"已删除文件: {file_path}")
                except OSError as e:
                    print(f"删除文件时出错: {e.strerror}")

def copy_file(src, dst):
    shutil.copy2(src, dst)  # copy2会尝试保留文件的元数据


def end_sentence(text, max_length):
    '''
    保证在max_length长度前以句号或点号结束文本
    :param text: 文本
    :param max_length: 最大长度
    :return:
    '''
    # 如果文本长度已经超过最大长度,则直接截断
    if len(text) > max_length:
        text = text[:max_length]

    # print("结果长度 {}".format(len(text)))

    # 查找句号的位置(en)
    period_index = max(text.rfind('.'), text.rfind(','),
                       text.rfind(':'), text.rfind(';'),
                       text.rfind('!'), text.rfind('?'))  # 从后往前找,找到最后一个句号
    # 如果找到了句号且它在最大长度内
    if period_index != -1 and (period_index + 1 < max_length or
                               max_length == -1):
        # 如果需要替换,则替换句号
        text = text[:period_index] + '.'

    # 查找句号的位置(cn)
    period_index = max(text.rfind('。'), text.rfind(','),
                       text.rfind(':'), text.rfind(';'),
                       text.rfind('!'), text.rfind('?'))  # 从后往前找,找到最后一个句号
    # 如果找到了句号且它在最大长度内
    if period_index != -1 and (period_index + 1 < max_length or
                               max_length == -1):
        # 如果需要替换,则替换句号
        text = text[:period_index] + '。'

    return text


import base64


def encode_base64(input_string):
    """
    对字符串进行Base64编码
    """
    encoded_bytes = base64.b64encode(input_string.encode('utf-8'))
    encoded_string = encoded_bytes.decode('utf-8')
    return encoded_string


def decode_base64(input_string):
    """
    对Base64编码的字符串进行解码
    """
    decoded_bytes = base64.b64decode(input_string.encode('utf-8'))
    decoded_string = decoded_bytes.decode('utf-8')
    return decoded_string

translate_ch5en.py

# 项目模型来自hugging face镜像网站,HF Mirror
# 中译文模型:https://hf-mirror.com/Helsinki-NLP/opus-mt-zh-en/tree/main
# 英译中模型:https://hf-mirror.com/Helsinki-NLP/opus-mt-en-zh/tree/main
import os
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
from transformers import pipeline
from my_util import Logger

loger = Logger()

def init_text_translate_models():

    try:
        # 加载中译英模型
        model_cn2en_path = os.path.join(os.getcwd(), 'models', 'ch-en')
        # 创建tokenizer
        tokenizer = AutoTokenizer.from_pretrained(model_cn2en_path)
        # 创建模型
        model = AutoModelForSeq2SeqLM.from_pretrained(model_cn2en_path)
        # 创建pipeline
        global pipeline_ch2en
        pipeline_ch2en = pipeline("translation", model=model, tokenizer=tokenizer)

        # 加载英译中模型
        model_en2cn_path = os.path.join(os.getcwd(), 'models', 'en-ch')
        # 创建tokenizer
        tokenizer = AutoTokenizer.from_pretrained(model_en2cn_path)
        # 创建模型
        model = AutoModelForSeq2SeqLM.from_pretrained(model_en2cn_path)
        # 创建pipeline
        global pipeline_en2ch
        pipeline_en2ch = pipeline("translation", model=model, tokenizer=tokenizer)
    except Exception as e:
        # 捕获所有异常,并打印错误信息
        loger.error(f"An error occurred: {e}")
    finally:
        loger.info(f"load text translate models success")
        return


def translate_ch2en(sentence):
    english_res = "unknown"
    try:
        result = pipeline_ch2en(sentence)
        english_res = result[0]['translation_text']
    except Exception as e:
        # 捕获所有异常,并打印错误信息
        loger.error(f"An error occurred: {e}")
    finally:
        loger.info(f"translate {sentence} to {english_res}")
        return english_res

def translate_en2ch(sentence):
    chinese_res = "未知"
    try:
        result = pipeline_en2ch(sentence)
        chinese_res = result[0]['translation_text']
    except Exception as e:
        # 捕获所有异常,并打印错误信息
        loger.error(f"An error occurred: {e}")
    finally:
        loger.info(f"translate {sentence} to {chinese_res}")
        return chinese_res

# if __name__ == "__main__":
#     init_translate_model()
#     print("initializing translation models final")
#     chinese = """
#     六岁时,我家在荷兰的莱斯韦克,房子的前面有一片荒地,
#     我称其为“那地方”,一个神秘的所在,那里深深的草木如今只到我的腰际,
#     当年却像是一片丛林,即便现在我还记得:“那地方”危机四伏,
#     洒满了我的恐惧和幻想。
#     """
#     result = pipeline_ch2en(chinese)
#     english = result[0]['translation_text']
#     print(english)
#
#     result = pipeline_en2ch(english)
#     print(result[0]['translation_text'])

voi_2_text.py

import os
import torch
import shlex
from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor
import torchaudio
import subprocess
from my_util import Logger

loger = Logger()

# 模型镜像
# https://hf-mirror.com/jonatasgrosman/wav2vec2-large-xlsr-53-chinese-zh-cn
CH_MODEL_ID = os.path.join(os.getcwd(), 'models', 'ch-voi-text')
# 模型镜像
# https://hf-mirror.com/jonatasgrosman/wav2vec2-large-xlsr-53-english/tree/main
EN_MODEL_ID = os.path.join(os.getcwd(), 'models', 'en-voi-text')

def convert_webm_to_wav(input_file, output_file):
    """
    使用ffmpeg将WebM文件转换为WAV文件。
    参数:
    input_file (str): 输入的WebM文件名。
    output_file (str): 输出的WAV文件名。
    """
    loger.debug(f"input file {input_file}")
    loger.debug(f"output file {output_file}")

    try:
        if os.path.exists(output_file):
            os.remove(output_file)
            loger.debug(f"file {output_file} remove success")
        else:
            loger.warning(f"file {output_file} not exist")
    except PermissionError:
        loger.error(f"cant remove file {output_file}, access denied")
    except Exception as e:
        loger.error(f"remove file {output_file} meet error: {e}")

    try:
        # FFmpeg命令行参数
        # cmd = [
        #     'ffmpeg',
        #     '-i', input_file,  # 输入文件
        #     '-ar', '16000',  # 输出音频采样率为16000Hz
        #     output_file  # 输出文件名
        # ]
        input_file = input_file.replace('\\', '\\\\')
        output_file = output_file.replace('\\', '\\\\')
        cli_cmd = f'ffmpeg -i {input_file} -ar 16000 {output_file}'
        cmd = shlex.split(cli_cmd)
        loger.debug(f"shlex.split {cmd}")

        # 执行FFmpeg命令并等待其完成
        result = subprocess.run(cmd)
        loger.debug(f"subprocess result {result}")
        loger.debug(f"Successfully converted {input_file} to {output_file}")
    except subprocess.CalledProcessError as e:
        loger.error(f"Error occurred: {e}")
    finally:
        return


def init_voice_recognize_models():

    try:
        # 加载中文语音识别模型
        global process_ch2en
        global model_ch2en
        process_ch2en = Wav2Vec2Processor.from_pretrained(CH_MODEL_ID)
        model_ch2en = Wav2Vec2ForCTC.from_pretrained(CH_MODEL_ID)

        # 加载英语语音识别模型
        global process_en2ch
        global model_en2ch
        process_en2ch = Wav2Vec2Processor.from_pretrained(EN_MODEL_ID)
        model_en2ch = Wav2Vec2ForCTC.from_pretrained(EN_MODEL_ID)
    except Exception as e:
        # 捕获所有异常,并打印错误信息
        loger.error(f"An error occurred: {e}")
    finally:
        loger.info(f"load voice recognize models success")
        return



def convert_ch_voi2text(webm_voi):
    audio_path = os.path.join(os.path.dirname(webm_voi), "result.wav")
    convert_webm_to_wav(webm_voi, audio_path)

    # pip install pysoundfile 除了安装torchaudio,还需要安装pysoundfile
    waveform, sample_rate = torchaudio.load(audio_path)
    loger.debug(f"audio file {audio_path} waveform is {waveform}")
    loger.debug(f"audio file {audio_path} sample rate is {sample_rate}")

    # 模型期望的采样率是16000Hz,而你的音频是44100Hz,则需要进行重采样
    if sample_rate != 16000:  # 这个模型的采样率是16000Hz,20240536-2229
        resampler = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)
        waveform = resampler(waveform)
        sample_rate = 16000

    # 使用特征提取器处理音频数据
    input_values = process_ch2en(waveform, sampling_rate=sample_rate, return_tensors="pt", padding=True).input_values

    # 获取预测结果(logits)
    with torch.no_grad():
        logits = model_ch2en(input_values.squeeze(0)).logits

    # print(logits)

    predicted_ids = torch.argmax(logits, dim=-1)
    predicted_sentences = process_ch2en.batch_decode(predicted_ids)

    loger.info(predicted_sentences)

    return predicted_sentences

def convert_en_voi2text(webm_voi):
    audio_path = os.path.join(os.path.dirname(webm_voi), "result.wav")
    convert_webm_to_wav(webm_voi, audio_path)

    # pip install pysoundfile 除了安装torchaudio,还需要安装pysoundfile
    waveform, sample_rate = torchaudio.load(audio_path)
    loger.debug(f"audio file {audio_path} waveform is {waveform}")
    loger.debug(f"audio file {audio_path} sample rate is {sample_rate}")

    # 模型期望的采样率是16000Hz,而你的音频是44100Hz,则需要进行重采样
    if sample_rate != 16000:  # 这个模型的采样率是16000Hz,20240536-2229
        resampler = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)
        waveform = resampler(waveform)
        sample_rate = 16000

    # 使用特征提取器处理音频数据
    input_values = process_en2ch(waveform, sampling_rate=sample_rate, return_tensors="pt", padding=True).input_values

    # 获取预测结果(logits)
    with torch.no_grad():
        logits = model_en2ch(input_values.squeeze(0)).logits

    # print(logits)

    predicted_ids = torch.argmax(logits, dim=-1)
    predicted_sentences = process_en2ch.batch_decode(predicted_ids)

    loger.info(predicted_sentences)

    return predicted_sentences

整体实现效果

项目代码厂库

page1

在这里插入图片描述

page2

在这里插入图片描述

page3

在这里插入图片描述

录音中

在这里插入图片描述

录音结束

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1809646.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【PR2019】怎样批量添加转场效果及修改默认持续时间

一&#xff0c;设置“交叉溶解”效果到所有素材 选择效果&#xff0c;右击“将所选过渡设置为默认过渡”&#xff1a; 框选所有素材&#xff0c;“Ctrl D”&#xff1a; 每个素材中间有有了交叉溶解的效果&#xff1a; 二&#xff0c;修改效果属性 2.1&#xff0c;单个修…

从零开始,手把手教你文旅产业策划全攻略

如果你想深入了解文旅策划的世界&#xff0c;那么有很多途径可以获取知识和灵感。 首先&#xff0c;阅读一些专业书籍也是一个不错的选择。书店或图书馆里有许多关于文旅策划的书籍&#xff0c;它们通常涵盖了策划的基本理论、方法和实践案例。通过阅读这些书籍&#xff0c;你…

激光点云配准算法——Cofinet / GeoTransforme / MAC

激光点云配准算法——Cofinet / GeoTransformer / MAC GeoTransformer MAC是当前最SOTA的点云匹配算法&#xff0c;在之前我用总结过视觉特征匹配的相关算法 视觉SLAM总结——SuperPoint / SuperGlue 本篇博客对Cofinet、GeoTransformer、MAC三篇论文进行简单总结 1. Cofine…

热题系列章节5

169. 多数元素 给定一个大小为 n 的数组&#xff0c;找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1: 输入: [3,2,3] 输出: 3 示例 2: 输入: [2,2,1,1,1,2,2] 输出:…

【C语言】11.字符函数和字符串函数

文章目录 1.字符分类函数2.字符转换函数3.strlen的使用和模拟实现4.strcpy的使用和模拟实现5.strcat的使用和模拟实现6.strcmp的使用和模拟实现7.strncpy函数的使用8.strncat函数的使用9.strncmp函数的使用10.strstr的使用和模拟实现11.strtok函数的使用12.strerror函数的使用 …

【MySQL】聊聊唯一索引是如何加锁的

首先我们要明确&#xff0c;加锁的对象是索引&#xff0c;加锁的基本单位是next-key lock&#xff0c;由记录锁和间隙锁组成。next-key是前开后闭区间&#xff0c;间隙锁是前开后开区间。根据不同的查询条件next-key 可能会退化成记录锁或间隙锁。 在能使用记录锁或者间隙锁就…

认识Spring中的BeanFactoryPostProcessor

先看下AI的介绍 在Spring 5.3.x中&#xff0c;BeanFactoryPostProcessor是一个重要的接口&#xff0c;用于在Spring IoC容器实例化任何bean之前&#xff0c;读取bean的定义&#xff08;配置元数据&#xff09;&#xff0c;并可能对其进行修改。以下是关于BeanFactoryPostProce…

31-捕获异常(NoSuchElementException)

在定位元素的时候&#xff0c;经常会遇到各种异常&#xff0c;遇到异常又该如何处理呢&#xff1f;本篇通过学习selenium的exceptions模块&#xff0c;了解异常发生的原因。 一、发生异常 打开百度搜索首页&#xff0c;定位搜索框&#xff0c;此元素id"kw"。为了故意…

定个小目标之刷LeetCode热题(15)

这道题直接就采用两数相加的规则&#xff0c;维护一个进阶值&#xff08;n&#xff09;即可&#xff0c;代码如下 class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {// 新建一个值为0的头结点ListNode newHead new ListNode(0);// 创建几个指针用于…

央视频官方出品,AI高考智友助你成就高考梦想

大家好&#xff0c;我是小麦。今天分享一款由央视频官方出品的AI工具套件&#xff0c;不仅支持直接使用&#xff0c;同时还具备了开发能力&#xff0c;是一款非常不错的AI产品工具&#xff0c;该软件的名称叫做扣子。 扣子是新一代 AI 应用开发平台。无论你是否有编程基础&…

Python图像处理入门学习——基于霍夫变换的车道线和路沿检测

文章目录 前言一、实验内容与方法二、视频的导入、拆分、合成2.1 视频时长读取2.2 视频的拆分2.3 视频的合成 三、路沿检测3.1 路沿检测算法整体框架3.2 尝试3.3 图像处理->边缘检测(原理)3.4 Canny算子边缘检测(原理)3.5 Canny算子边缘检测(实现)3.5.1 高斯滤波3.5.2 图像转…

网络学习(二)DNS域名解析原理、DNS记录

目录 一、为什么要使用DNS&#xff1f;二、因特网的域名结构三、DNS域名解析原理【含详细图解】四、DNS记录&#xff08;A记录、AAAA记录、CNAME记录等&#xff09; 一、为什么要使用DNS&#xff1f; 我们知道&#xff0c;TCP/IP 协议中是使用 IP 地址和端口号来确定网络上的某…

Unity 设置默认字体(支持老版及新版TMP)

普通UI-Text设置 &#xff08;同一unity版本设置一次即可&#xff09; 1.首先工程的Resources目录下创建Fonts文件夹用于存放字体 如下图所示 2.找到Unity的安装目录下的Editor\Data\Resources\PackageManager\BuiltInPackages\com.unity.ugui\Runtime\UI\Core\Text.cs文件 …

msfconsole利用Windows server2008cve-2019-0708漏洞入侵

一、环境搭建 Windows系列cve-2019-0708漏洞存在于Windows系统的Remote Desktop Services&#xff08;远程桌面服务&#xff09;&#xff08;端口3389&#xff09;中&#xff0c;未经身份验证的攻击者可以通过发送特殊构造的数据包触发漏洞&#xff0c;可能导致远程无需用户验…

C语言之main函数的返回值(在linux中执行shell脚本并且获取返回值)

一&#xff1a;函数为什么要返回值 &#xff08;1&#xff09;函数 在设计的时候是设计了参数和返回值&#xff0c;参数是函数的输入&#xff0c;返回值是函数的输出 &#xff08;2&#xff09;因为函数需要对外输出数据&#xff08;实际上是函数运行的一些结果值&#xff09;…

「51媒体」江苏媒体宣传报道,邀请媒体报道资源汇总

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 江苏作为中国东部的重要省份&#xff0c;拥有丰富的媒体资源&#xff0c;包括电视台、广播电台、报纸以及网络媒体。 电视台 江苏卫视&#xff1a;作为江苏省唯一的省级卫视台&#xff…

支持YUV和RGB格式两路视频同时播放

1.头文件&#xff1a; sdlqtrgb.h #pragma once #include <QtWidgets/QWidget> #include "ui_sdlqtrgb.h" #include <thread> class SdlQtRGB : public QWidget {Q_OBJECTpublic:SdlQtRGB(QWidget* parent Q_NULLPTR);~SdlQtRGB(){is_exit_ true;//等…

ruoyi vue 集成积木报表真实记录

按官方文档集成即可 积木报表官方集成文档 集成问题 1.注意 idea 配置的 maven 需要设置成 本地配置&#xff0c;不可以使用 idea 自带的 maven,自带 maven 会导致私有源调用不到 后端代码 新建 base 模块 maven配置 <project xmlns"http://maven.apache.org/POM/…

33-unittest数据驱动(ddt)

所谓数据驱动&#xff0c;是指利用不同的测试数据来测试相同的场景。为了提高代码的重用性&#xff0c;增加代码效率而采用一种代码编写的方法&#xff0c;叫数据驱动&#xff0c;也就是参数化。达到测试数据和测试业务相分离的效果。 比如登录这个功能&#xff0c;操…

Linux shell编程学习笔记58:cat /proc/mem 获取系统内存信息

0 前言 在开展系统安全检查的过程中&#xff0c;除了收集cpu信息&#xff0c;我们还需要收集内存信息。在Linux中&#xff0c;获取内存信息的命令很多&#xff0c;这里我们着重研究 cat /proc/mem命令。 1 cat /proc/mem命令 /proc/meminfo 文件提供了有关系统内存的使用情况…