基于Python和Tkinter实现文本文件转换为MP3语音文件

 更新时间:2026年02月11日 08:32:48   作者:温轻舟  
本文介绍了基于Python和Tkinter的图形界面应用程序,该程序用于将文本文件转换为MP3语音文件,功能包括文本转语音转换、文件处理、语音参数调节等,用户界面设计合理,具有多线程处理、自动保存和错误处理等技术特点,使用流程简单易懂,附带帮助文档

一:效果展示

本项目是基于Python和Tkinter的图形界面应用程序,用于将文本文件转换为MP3语音文件

二:功能描述

(1)文本转语音转换

  • 使用pyttsx3引擎将文本转换为语音并保存为MP3文件
  • 支持中文语音合成(默认使用"Microsoft Huihui"语音)

(2)文件处理

  • 单文件转换:选择单个文本文件进行转换
  • 批量转换:支持同时选择多个文本文件进行批量转换
  • 自动保存:在预览窗口中修改文本内容会自动保存到源文件

(3)语音参数调节

  • 语速调节(1-100范围)
  • 音量调节(1-100范围)

1. 用户界面功能

(1)主界面布局

  • 标题栏:显示应用名称"文本转语音转换器"和帮助按钮
  • 左右分栏:左侧为输入和设置面板,右侧为文本预览和转换面板
  • 状态栏:显示当前状态信息和版本信息

(2)左侧面板功能

  1. 文本文件选择
  • 通过文件对话框选择要转换的文本文件
  • 自动填充输出目录为文本文件所在目录
  • 自动加载并显示文本内容到预览区域
  1. 输出设置
  • 可自定义输出目录
  • 可设置输出文件名前缀(默认"wen_qing_zhou")
  1. 语音设置
  • 显示当前使用的语音名称
  • 语速调节滑块和实时显示
  • 音量调节滑块和实时显示

(3)右侧面板功能

  1. 文本预览
  • 显示选定文本文件的内容
  • 支持直接编辑文本内容(自动保存到源文件)
  • 带滚动条的可编辑文本框
  1. 转换控制
  • "转换为MP3"按钮:执行单文件转换
  • "批量转换"按钮:执行多文件批量转换
  • "打开输出目录"按钮:快速访问保存的MP3文件
  1. 进度指示
  • 转换过程中显示进度条
  • 状态栏显示转换进度信息

2. 技术特点

  1. 多线程处理
  • 使用单独的线程执行转换任务,避免界面冻结
  • 转换过程中禁用按钮防止重复操作
  1. 错误处理
  • 对文件操作、语音引擎配置等关键操作进行异常捕获
  • 通过消息框向用户显示错误信息
  1. 自适应参数映射
  • 将用户友好的1-100范围参数映射到语音引擎实际需要的参数范围
  • 语速映射采用非线性调整,提供更精细的慢速控制
  1. 跨平台支持
  • 使用pathlib处理路径,确保跨平台兼容性
  • 根据不同操作系统使用适当的方法打开目录
  1. UI美化
  • 使用ttk的clam主题
  • 自定义按钮样式(如绿色强调按钮)
  • 一致的字体设置(微软雅黑)

3. 使用流程

  1. 单文件转换
  • 点击"选择文件"选择文本文件
  • (可选)调整语音参数或修改文本内容
  • 点击"转换为MP3"开始转换
  • 转换完成后在输出目录查找MP3文件
  1. 批量转换
  • 点击"批量转换"选择多个文本文件
  • 设置统一的输出参数
  • 程序会自动为每个文件生成带序号的输出文件名
  1. 查看结果
  • 使用"打开输出目录"快速访问输出文件
  • 状态栏和消息框显示转换结果

4. 辅助功能

  • 帮助文档: 通过"帮助"按钮查看详细的使用说明
  • 自动保存: 预览区域的修改会自动保存到源文件
  • 状态反馈: 状态栏实时显示当前操作状态

三:完整代码

import pathlib
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog, messagebox
import pyttsx3
from PIL import Image, ImageTk
import threading
import os
import platform
import webbrowser
from datetime import datetime

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("文本转语音转换器")
        self.geometry("1200x700")
        self.minsize(700, 400)
        self.resizable(True, True)

        self.minsize(700, 400)

        self.engine = pyttsx3.init()
        self.file_path = tk.StringVar()
        self.output_dir = tk.StringVar()

        self.rate_var = tk.DoubleVar(value=50.00)
        self.volume_var = tk.DoubleVar(value=50.00)

        self.display_rate_var = tk.StringVar()
        self.display_volume_var = tk.StringVar()
        self.update_display_vars()

        self.rate_var.trace_add('write', self.update_display_vars)
        self.volume_var.trace_add('write', self.update_display_vars)

        self.target_voice = "Microsoft Huihui"
        self.voice_id = None

        self.style = ttk.Style()
        self.style.theme_use('clam')

        self.style.configure('TButton', font=('微软雅黑', 10), padding=5)
        self.style.configure('TEntry', font=('微软雅黑', 10))
        self.style.configure('TLabel', font=('微软雅黑', 10))
        self.style.configure('TFrame', background='#f0f0f0')
        self.style.configure('TLabelframe', font=('微软雅黑', 10, 'bold'))
        self.style.configure('Accent.TButton', foreground='white', background='#4CAF50', font=('微软雅黑', 11, 'bold'))
        self.style.map('Accent.TButton', background=[('active', '#45a049'), ('!active', '#4CAF50')])

        self.init_ui()
        self.load_voices()

    def init_ui(self):
        main_frame = ttk.Frame(self, padding=20)
        main_frame.pack(fill=tk.BOTH, expand=True)

        title_frame = tk.Frame(main_frame, bd=0, highlightthickness=0, bg=self.cget('bg'))
        title_frame.pack(fill=tk.X, pady=(0, 20))

        title_label = tk.Label(
            title_frame,
            text="文本转语音转换器",
            font=('微软雅黑', 16, 'bold'),
            bg=self.cget('bg')
        )
        title_label.pack(side=tk.LEFT)

        help_btn = ttk.Button(title_frame, text="帮助", command=self.show_help, width=8)
        help_btn.pack(side=tk.RIGHT, padx=(10, 0))

        pw = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
        pw.pack(fill=tk.BOTH, expand=True)

        left_panel = ttk.Labelframe(pw, text="输入和设置", padding=15)
        pw.add(left_panel, weight=2)

        right_panel = ttk.Labelframe(pw, text="文本预览和转换", padding=15)
        pw.add(right_panel, weight=3)

        self.create_left_panel(left_panel)

        self.create_right_panel(right_panel)

        self.status_var = tk.StringVar()

        self.status_var.set("准备就绪")
        status_frame = ttk.Frame(main_frame)
        status_frame.pack(fill=tk.X, pady=(10, 0))

        status_label = ttk.Label(status_frame, textvariable=self.status_var, foreground='#666')
        status_label.pack(side=tk.LEFT)

        version_label = ttk.Label(status_frame, text="温轻舟", foreground='#999')
        version_label.pack(side=tk.RIGHT)

    def update_display_vars(self, *args):
        self.display_rate_var.set(f"{self.rate_var.get():.2f}")
        self.display_volume_var.set(f"{self.volume_var.get():.2f}")

    def create_left_panel(self, parent):
        file_frame = ttk.LabelFrame(parent, text="文本文件", padding=10)
        file_frame.pack(fill=tk.X, pady=(0, 10))

        entry_frame = ttk.Frame(file_frame)
        entry_frame.pack(fill=tk.X, expand=True)

        self.txt_path = ttk.Entry(entry_frame, textvariable=self.file_path, width=30)
        self.txt_path.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))

        btn_sel = ttk.Button(entry_frame, text="选择文件", command=self.select_file, width=10)
        btn_sel.pack(side=tk.RIGHT)

        output_frame = ttk.LabelFrame(parent, text="输出设置", padding=10)
        output_frame.pack(fill=tk.X, pady=(0, 10))

        dir_frame = ttk.Frame(output_frame)
        dir_frame.pack(fill=tk.X, expand=True)

        self.txt_output_dir = ttk.Entry(dir_frame, textvariable=self.output_dir, width=30)
        self.txt_output_dir.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))

        btn_dir = ttk.Button(dir_frame, text="选择目录", command=self.select_output_dir, width=10)
        btn_dir.pack(side=tk.RIGHT)

        prefix_frame = ttk.Frame(output_frame)
        prefix_frame.pack(fill=tk.X, pady=(5, 0))

        ttk.Label(prefix_frame, text="文件名前缀:").pack(side=tk.LEFT)
        self.prefix_entry = ttk.Entry(prefix_frame, width=15)
        self.prefix_entry.pack(side=tk.LEFT, padx=(5, 0))
        self.prefix_entry.insert(0, "wen_qing_zhou")

        voice_frame = ttk.LabelFrame(parent, text="语音设置", padding=10)
        voice_frame.pack(fill=tk.X, pady=(0, 10))

        ttk.Label(voice_frame, text=f"当前语音: {self.target_voice}").pack(side=tk.LEFT)

        rate_frame = ttk.Frame(voice_frame)
        rate_frame.pack(fill=tk.X, pady=(5, 0))

        ttk.Label(rate_frame, text="语速:").pack(side=tk.LEFT)
        self.rate_scale = ttk.Scale(rate_frame, from_=1, to=100, variable=self.rate_var, orient=tk.HORIZONTAL)
        self.rate_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0))
        self.rate_label = ttk.Label(rate_frame, textvariable=self.display_rate_var, width=6)
        self.rate_label.pack(side=tk.LEFT)

        volume_frame = ttk.Frame(voice_frame)
        volume_frame.pack(fill=tk.X, pady=(5, 0))

        ttk.Label(volume_frame, text="音量:").pack(side=tk.LEFT)
        self.volume_scale = ttk.Scale(volume_frame, from_=1, to=100, variable=self.volume_var, orient=tk.HORIZONTAL)
        self.volume_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0))
        self.volume_label = ttk.Label(volume_frame, textvariable=self.display_volume_var, width=6)
        self.volume_label.pack(side=tk.LEFT)

    def create_right_panel(self, parent):
        preview_frame = ttk.LabelFrame(parent, text="文本预览", padding=10)
        preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))

        self.text_preview = tk.Text(preview_frame, height=8, font=('微软雅黑', 10), wrap=tk.WORD)
        self.text_preview.pack(fill=tk.BOTH, expand=True)

        self.text_preview.bind('<<Modified>>', self.on_text_modified)

        scrollbar = ttk.Scrollbar(preview_frame, orient=tk.VERTICAL, command=self.text_preview.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.text_preview.configure(yscrollcommand=scrollbar.set)

        convert_frame = ttk.Frame(parent)
        convert_frame.pack(fill=tk.X, pady=(10, 0))

        self.btn_convert = ttk.Button(
            convert_frame,
            text="转换为MP3",
            command=self.convert_to_mp3,
            style='Accent.TButton'
        )
        self.btn_convert.pack(fill=tk.X, pady=(10, 0))

        btn_batch = ttk.Button(
            convert_frame,
            text="批量转换",
            command=self.batch_convert,
            style='Accent.TButton'
        )
        btn_batch.pack(fill=tk.X, pady=(5, 0))

        btn_open_dir = ttk.Button(
            convert_frame,
            text="打开输出目录",
            command=self.open_output_dir
        )
        btn_open_dir.pack(fill=tk.X, pady=(5, 0))

        self.progress = ttk.Progressbar(convert_frame, mode='indeterminate', length=200)

    def load_voices(self):
        try:
            voices = self.engine.getProperty('voices')
            for voice in voices:
                if self.target_voice in voice.name:
                    self.voice_id = voice.id
                    break

            if not self.voice_id:
                if voices:
                    self.voice_id = voices[0].id
                    self.status_var.set(f"未找到 {self.target_voice} 语音,将使用 {voices[0].name}")
                else:
                    raise Exception("未找到任何可用语音")
        except Exception as e:
            self.status_var.set(f"加载语音列表失败: {str(e)}")

    def select_file(self):
        current_file = self.file_path.get()
        if current_file and os.path.exists(current_file):
            initial_dir = os.path.dirname(current_file)
        else:
            initial_dir = str(pathlib.Path.home())

        txt_file = filedialog.askopenfilename(
            initialdir=initial_dir,
            title="选择文本文件",
            filetypes=(('文本文件', '*.txt'), ('所有文件', '*.*'))
        )
        if txt_file:
            self.file_path.set(txt_file)
            file_dir = os.path.dirname(txt_file)
            self.output_dir.set(file_dir)

            self.status_var.set(f"已选择文件: {pathlib.Path(txt_file).name}")
            try:
                with open(txt_file, 'r', encoding='utf8') as f:
                    content = f.read()
                    self.text_preview.delete(1.0, tk.END)
                    self.text_preview.insert(tk.END, content)
                    self.text_preview.edit_modified(False)
            except Exception as e:
                messagebox.showwarning("警告", f"无法读取文件内容: {str(e)}")

    def select_output_dir(self):
        current_file = self.file_path.get()
        if current_file and os.path.exists(current_file):
            initial_dir = os.path.dirname(current_file)
        elif self.output_dir.get():
            initial_dir = self.output_dir.get()
        else:
            initial_dir = str(pathlib.Path.home())

        directory = filedialog.askdirectory(
            initialdir=initial_dir,
            title="选择输出目录"
        )
        if directory:
            self.output_dir.set(directory)
            self.status_var.set(f"输出目录设置为: {directory}")

    def on_text_modified(self, event):
        if self.text_preview.edit_modified():
            current_file = self.file_path.get()
            if current_file and os.path.exists(current_file):
                try:
                    content = self.text_preview.get(1.0, tk.END)
                    with open(current_file, 'w', encoding='utf8') as f:
                        f.write(content)
                    self.status_var.set(f"已自动保存修改到文件: {os.path.basename(current_file)}")
                except Exception as e:
                    self.status_var.set(f"自动保存失败: {str(e)}")
            self.text_preview.edit_modified(False)

    def configure_engine(self):
        try:
            if self.voice_id:
                self.engine.setProperty('voice', self.voice_id)

            rate = self.rate_var.get()
            volume = self.volume_var.get()

            if rate <= 50:
                progress = rate / 50.0
                mapped_rate = 20 + (150 - 20) * (progress ** 1.5)
            else:
                progress = (rate - 50) / 50.0
                mapped_rate = 150 + (300 - 150) * progress

            mapped_volume = volume / 100.0

            mapped_rate = max(20, min(300, mapped_rate))
            mapped_volume = max(0.0, min(1.0, mapped_volume))

            self.engine.setProperty('rate', int(mapped_rate))
            self.engine.setProperty('volume', mapped_volume)

        except Exception as e:
            messagebox.showwarning("警告", f"配置语音引擎失败: {str(e)}")

    def convert_to_mp3(self):
        if not self.file_path.get():
            messagebox.showwarning("警告", "请先选择文本文件!")
            return

        def convert_thread():
            try:
                self.btn_convert.state(['disabled'])
                self.progress.pack(fill=tk.X, pady=(10, 0))
                self.progress.start()
                self.status_var.set("正在转换...")
                self.update()

                text = self.text_preview.get(1.0, tk.END)
                if not text.strip():
                    raise ValueError("文本内容为空!")

                self.configure_engine()

                prefix = self.prefix_entry.get() or "TTS_Output"
                timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                output_name = f"{prefix}_{timestamp}.mp3"

                output_dir = self.output_dir.get() or os.path.dirname(self.file_path.get()) or str(pathlib.Path.home())
                output_path = pathlib.Path(output_dir) / output_name

                self.engine.save_to_file(text, str(output_path))
                self.engine.runAndWait()

                self.progress.stop()
                self.progress.pack_forget()
                self.status_var.set(f"转换成功! 文件已保存为: {output_path.name}")
                messagebox.showinfo("成功", f"文件转换成功!\n已保存为: {output_path.name}")

            except Exception as e:
                self.progress.stop()
                self.progress.pack_forget()
                self.status_var.set("转换失败")
                messagebox.showerror("错误", f"转换过程中出错:\n{str(e)}")
            finally:
                self.btn_convert.state(['!disabled'])

        threading.Thread(target=convert_thread, daemon=True).start()

    def batch_convert(self):
        files = filedialog.askopenfilenames(
            initialdir=os.path.dirname(self.file_path.get()) if self.file_path.get() else str(pathlib.Path.home()),
            title="选择多个文本文件",
            filetypes=(('文本文件', '*.txt'), ('所有文件', '*.*'))
        )

        if not files:
            return

        def batch_thread():
            try:
                self.btn_convert.state(['disabled'])
                self.progress.pack(fill=tk.X, pady=(10, 0))
                self.progress.start()
                self.status_var.set("正在批量转换...")
                self.update()

                success_count = 0
                prefix = self.prefix_entry.get() or "TTS_Output"
                timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

                self.configure_engine()

                for i, file_path in enumerate(files, 1):
                    if not file_path:
                        continue

                    try:
                        with open(file_path, 'r', encoding='utf8') as f:
                            text = f.read()
                            if not text.strip():
                                continue

                            file_prefix = f"{prefix}_batch_{timestamp}_{i}"
                            output_name = f"{file_prefix}.mp3"

                            output_dir = self.output_dir.get() or os.path.dirname(file_path) or str(pathlib.Path.home())
                            output_path = pathlib.Path(output_dir) / output_name

                            self.engine.save_to_file(text, str(output_path))
                            self.engine.runAndWait()
                            success_count += 1
                            self.status_var.set(f"正在批量转换... 已完成 {i}/{len(files)}")
                            self.update()

                    except Exception as e:
                        self.status_var.set(f"转换 {os.path.basename(file_path)} 失败: {str(e)}")
                        continue

                self.progress.stop()
                self.progress.pack_forget()
                self.status_var.set(f"批量转换完成! 成功转换 {success_count}/{len(files)} 个文件")
                messagebox.showinfo("完成", f"批量转换完成!\n成功转换 {success_count}/{len(files)} 个文件")

            except Exception as e:
                self.progress.stop()
                self.progress.pack_forget()
                self.status_var.set("批量转换失败")
                messagebox.showerror("错误", f"批量转换过程中出错:\n{str(e)}")
            finally:
                self.btn_convert.state(['!disabled'])

    def open_output_dir(self):
        dir_path = self.output_dir.get()
        if not dir_path and self.file_path.get():
            dir_path = os.path.dirname(self.file_path.get())

        if not dir_path or not os.path.isdir(dir_path):
            messagebox.showwarning("警告", "无效的输出目录!")
            return

        try:
            if platform.system() == "Windows":
                os.startfile(dir_path)
            elif platform.system() == "Darwin":
                webbrowser.open(f"file://{dir_path}")
            else:
                webbrowser.open(dir_path)
        except Exception as e:
            messagebox.showerror("错误", f"无法打开目录:\n{str(e)}")

    def show_help(self):
        help_text = """文本转语音转换器使用说明

1. 基本使用:
   - 点击"选择文件"按钮选择要转换的文本文件
   - 在右侧预览区域可以查看和编辑文件内容(修改后会自动保存)
   - 点击"转换为MP3"进行转换(使用编辑后的内容)

2. 高级设置:
   - 输出目录: 默认使用当前文件所在目录,可手动修改
   - 文件名前缀: 设置输出文件的前缀名称
   - 语音设置: 固定使用Microsoft Huihui语音,可调整语速和音量

3. 批量转换:
   - 点击"批量转换"按钮可以选择多个文本文件进行批量转换
   - 转换后的文件会添加序号以避免重名

4. 其他功能:
   - 打开输出目录: 快速访问保存的MP3文件
"""

        messagebox.showinfo("帮助", help_text)

if __name__ == "__main__":
    app = Application()
    app.mainloop()

以上就是基于Python和Tkinter实现文本文件转换为MP3语音文件的详细内容,更多关于Python文本文件转MP3语音文件的资料请关注脚本之家其它相关文章!

相关文章

  • Python处理浮点数的实用技巧分享

    Python处理浮点数的实用技巧分享

    四舍五入是一种常见的数学操作,它用于将数字舍入到指定的精度,Python 提供了多种方法来实现四舍五入操作,本文将详细介绍这些方法,希望对大家有所帮助
    2024-12-12
  • Python利用pdfplumber实现读取PDF写入Excel

    Python利用pdfplumber实现读取PDF写入Excel

    pdfplumber专注PDF内容提取,例如文本(位置、字体及颜色等)和形状(矩形、直线、曲线),还有解析表格的功能。本文主要为大家介绍如何利用pdfplumber实现读取PDF写入Excel,需要的可以参考一下
    2022-06-06
  • Python入门

    Python入门

    Python入门...
    2007-02-02
  • python 画二维、三维点之间的线段实现方法

    python 画二维、三维点之间的线段实现方法

    今天小编就为大家分享一篇python 画二维、三维点之间的线段实现方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-07-07
  • python爬虫 requests-html的使用

    python爬虫 requests-html的使用

    这篇文章主要介绍了python爬虫 requests-html的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • Python中列表和元组的相关语句和方法讲解

    Python中列表和元组的相关语句和方法讲解

    这篇文章主要介绍了Python中列表和元组的相关语句和方法讲解,是Python入门学习中的基础知识,需要的朋友可以参考下
    2015-08-08
  • Windows 下更改 jupyterlab 默认启动位置的教程详解

    Windows 下更改 jupyterlab 默认启动位置的教程详解

    这篇文章主要介绍了Windows 下更改 jupyterlab 默认启动位置,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05
  • python验证多组数据之间有无显著差异

    python验证多组数据之间有无显著差异

    这篇文章主要介绍了python验证多组数据之间有无显著差异,利用方差分析和卡方分布验证多组数据之间的某些属性有无显著性差异,对于连续性属性可以用方差分析,对于离散型属性可以用卡方检验。下面文章详细内容需要的小伙伴可以参考一下
    2022-01-01
  • 详解Python如何制作自动发送微信的程序

    详解Python如何制作自动发送微信的程序

    这篇文章主要介绍了如何利用Python中的apscheduler和pyautogui模块,制作一个自动发送微信的程序。感兴趣的小伙伴可以跟随小编一起动手试一试
    2022-01-01
  • Python之freegames 零代码的22个小游戏集合

    Python之freegames 零代码的22个小游戏集合

    这篇文章主要介绍了,Python之freegames 零代码的22个小游戏集合,文章内容详细,简单易懂,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2023-01-01

最新评论