吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 612|回复: 13
上一主题 下一主题
收起左侧

[Python 原创] 单词音频批量下载与合成工具附源码

[复制链接]
跳转到指定楼层
楼主
pythonfun 发表于 2026-7-7 20:46 回帖奖励
本帖最后由 pythonfun 于 2026-7-7 20:50 编辑

一、软件名称

单词音频批量下载与合成工具

二、软件简介

本软件主要解决英语学习者在制作听力练习材料、单词听写音频时手动逐个下载和拼接音频的繁琐问题。通过调用在线语音合成API,支持批量下载英音和美音单词音频,并按自定义顺序合成连续的MP3文件,同时内置音量标准化、听写纸导出、词库管理等功能,为教师、学生和自学者提供一站式的单词音频制作解决方案,让听力素材制作变得高效、专业。

三、软件功能

本软件是一款基于Python开发的单词音频批量处理工具,主要功能包括:

  1. 双音标下载:支持英音(uk)和美音(us)两种发音的批量下载,可单独或同时选择
  2. 灵活合成设置:支持自定义合成顺序(仅英音、仅美音、先英后美、先美后英)、单词间隔时长(0.5~5秒)和朗读遍数(1~5遍)
  3. 音量标准化:自动均衡各音频文件的音量,使合成后的整体响度一致,避免音量突变
  4. 智能单词管理:支持手动添加、批量导入(TXT/Excel)、从剪贴板导入(自动按空格、逗号、分号、换行、制表位分词)
  5. 词库持久化:支持以.vocab格式保存和加载单词列表,方便复用和分享
  6. 列表排序与调整:支持随机打乱、字母排序,以及上移/下移调整单词顺序
  7. 导出听写纸:自动生成含序号、填空横线和参考答案的听写练习纸(TXT格式),方便打印使用
  8. 合成音频管理:合成完成后可一键打开文件位置、复制到指定目录或删除音频
  9. 进度反馈:下载和合成过程中实时显示进度条和状态信息

四、使用方法和操作步骤

  1. 运行程序后,打开主界面,左侧为单词列表区域,右侧为控制面板
  2. 导入单词:可通过“导入TXT/XLSX”按钮选择文件,或在手动输入框中添加单词后点击“添加”,或点击“从剪贴板导入”粘贴文本(支持多种分隔符)
  3. 管理词库:使用“保存词库”和“加载词库”按钮保存当前列表为.vocab文件或加载已有词库
  4. 调整列表顺序:使用“随机顺序”、“字母顺序”或“↑上移/↓下移”按钮调整单词播放次序
  5. 下载音频:勾选需要的发音类型(英音/美音),点击“开始下载”,程序将自动为列表中的每个单词下载对应的MP3音频文件(存放于sounds/uksounds/us目录)
  6. 合成音频:在合成设置区选择合成顺序、间隔秒数和朗读遍数,可选“音频标准化”,点击“合成音频”按钮,等待合成完成
  7. 导出听写纸:合成成功后(或直接使用现有列表),点击“导出听写纸”按钮,选择保存路径,生成含填空和答案的TXT文件
  8. 操作合成结果:合成完成后,可点击“打开音频”播放、“复制音频”另存为其他位置,或“删除音频”清理文件
  9. 状态栏和进度条会实时提示当前操作进度和结果

五、运行环境

  • 系统要求:支持Windows、macOS、Linux等操作系统
  • Python环境:需要Python 3.6及以上,并安装以下依赖库:
    • tkinter(通常内置)
    • requests
    • beautifulsoup4
    • pydub(用于音频合成与标准化)
    • openpyxl(用于导入Excel文件)
  • 网络要求:下载音频功能需要互联网连接,以访问在线语音合成API
  • 音频文件目录:程序自动在运行目录下创建sounds/uksounds/us文件夹,用于存放下载的音频

六、技术特点说明

  • 数据获取技术:基于requests库调用在线语音合成API(海词/欧路词典接口),通过解析网页获取动态txt参数,实现高质量发音音频的批量拉取
  • 音频处理技术:利用pydub库进行音频合并、间隔插入和音量标准化(effects.normalize),确保输出音频的流畅性和一致性
  • 词库管理技术:采用JSON格式序列化单词列表,支持读写.vocab文件,便于数据持久化和跨设备迁移
  • 界面交互技术:基于tkinter构建图形界面,利用多线程(threading)实现后台下载和合成,避免界面卡顿
  • 剪贴板解析技术:通过正则表达式智能分割多种分隔符(空格、逗号、分号、换行、制表位),提升导入效率

七、特色亮点

  • 批量高效:一次性处理数百个单词,大幅节省手动搜索和下载时间
  • 灵活定制:合成顺序、间隔、遍数均可调节,满足不同教学场景(听写、跟读、考试练习)
  • 智能均衡:音量标准化功能消除不同单词音频间的响度差异,提升听觉体验
  • 无缝衔接:从导入、下载、合成到导出听写纸,全流程一体化,无需切换工具
  • 易用友好:直观的界面设计,清晰的进度反馈,并支持撤销、排序等辅助操作
  • 可复用性:词库保存功能支持建立个人词库库,便于多课程、多班级重复使用

八、注意事项

  • 下载音频依赖外部API服务,需保持网络畅通;若API变动或失效,可能影响下载功能
  • 首次使用需安装pydubopenpyxl,可通过pip install pydub openpyxl安装
  • 音频文件保存于本地sounds目录,请勿手动修改文件命名,以免合成时找不到对应文件
  • 合成音频时建议根据单词长度和数量适当调整间隔时长,过短可能导致语音重叠
  • 听写纸导出功能仅生成TXT文本,如需打印可自行调整格式或导入Word编辑
  • 本工具仅供个人学习使用,请勿用于商业用途或大量爬取API资源

九、界面展示


打包软件下载:https://wwaws.lanzouq.com/ig2tN3uys1sf[/font]

十、源码展示


[Python] 纯文本查看 复制代码
# -*- coding: utf-8 -*-
"""
单词音频批量下载与合成工具(增强版)
新增功能:
- 音量标准化(自动均衡音量)
- 导出听写纸(含序号、空格、答案)
- 词库保存与加载(.vocab 格式)
- 从剪贴板导入(按空格、逗号、分号、换行、制表位分词)
布局优化:右侧面板分区更清晰,控件对齐与间距优化
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import requests
from bs4 import BeautifulSoup
import re
import os
import threading
import time
from datetime import datetime
import tempfile
import shutil
import subprocess
import sys
import json
import random

# 第三方库
try:
    import openpyxl
except ImportError:
    openpyxl = None

try:
    from pydub import AudioSegment
    from pydub import effects
except ImportError:
    AudioSegment = None
    effects = None


class AudioDownloaderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("单词音频批量下载与合成工具 - 增强版 (pythonfun)")
        self.root.geometry("1100x700")          # 略微调大窗口
        self.root.minsize(880, 680)

        # 全局字体
        default_font = ('宋体', 12)
        style = ttk.Style()
        style.theme_use('xpnative')
        style.configure('TButton', font=('宋体', 11), padding=(6, 3))
        style.configure('TLabel', font=default_font)
        style.configure('TCheckbutton', font=default_font)
        style.configure('TLabelFrame', font=('宋体', 12, 'bold'))
        self.root.option_add('*Font', default_font)

        # 数据
        self.word_list = []
        self.downloaded_uk = set()
        self.downloaded_us = set()

        # 目录
        self.base_dir = "sounds"
        self.uk_dir = os.path.join(self.base_dir, "uk")
        self.us_dir = os.path.join(self.base_dir, "us")
        self._ensure_dirs()

        # 状态变量
        self.is_downloading = False
        self.is_synthesizing = False
        self.last_output_path = None

        # 构建UI
        self._create_widgets()

        # 底部状态栏
        self.status_var = tk.StringVar(value="欢迎使用 | 请导入单词或手动添加")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=12, pady=(0, 10))

        # 启动时检查依赖并禁用相应按钮
        self._check_dependencies()

    # ------------------------------------------
    # 目录管理
    # ------------------------------------------
    def _ensure_dirs(self):
        os.makedirs(self.uk_dir, exist_ok=True)
        os.makedirs(self.us_dir, exist_ok=True)

    # ------------------------------------------
    # 依赖检查
    # ------------------------------------------
    def _check_dependencies(self):
        if openpyxl is None:
            self.btn_import.config(state=tk.DISABLED, text="导入(需openpyxl)")
        if AudioSegment is None:
            self.btn_synth.config(state=tk.DISABLED, text="合成(需pydub)")
            messagebox.showwarning("依赖缺失", "请安装 pydub 和 openpyxl\n命令行执行:pip install pydub openpyxl")

    # ------------------------------------------
    # 构建界面(左侧列表 + 右侧控制面板)
    # ------------------------------------------
    def _create_widgets(self):
        # 主容器(PanedWindow可调比例)
        main_pane = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main_pane.pack(fill=tk.BOTH, expand=True, padx=12, pady=12)

        # ---------- 左侧:单词列表 ----------
        left_frame = ttk.Frame(main_pane, padding="5")
        main_pane.add(left_frame, weight=1)

        self.list_title_var = tk.StringVar(value="单词列表(共 0 个)")
        ttk.Label(left_frame, textvariable=self.list_title_var, font=('宋体', 13, 'bold')).pack(anchor=tk.W, pady=(0, 8))

        list_frame = ttk.Frame(left_frame)
        list_frame.pack(fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(list_frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.word_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set,
                                       font=('宋体', 13), selectmode=tk.EXTENDED,
                                       activestyle='dotbox', height=10)
        self.word_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.config(command=self.word_listbox.yview)

        # 列表底部操作按钮
        list_btn_frame = ttk.Frame(left_frame)
        list_btn_frame.pack(fill=tk.X, pady=(8, 0))

        btn_del_selected = ttk.Button(list_btn_frame, text="删除选中", command=self._delete_selected)
        btn_del_selected.pack(side=tk.LEFT, padx=(0, 6))
        btn_clear_list = ttk.Button(list_btn_frame, text="清空列表", command=self._clear_list)
        btn_clear_list.pack(side=tk.LEFT)
        btn_up = ttk.Button(list_btn_frame, text="↑上移", command=self._move_up)
        btn_up.pack(side=tk.LEFT, padx=(6, 0))
        btn_down = ttk.Button(list_btn_frame, text="↓下移", command=self._move_down)
        btn_down.pack(side=tk.LEFT, padx=(6, 0))

        # ---------- 右侧:控制面板(分区) ----------
        right_frame = ttk.Frame(main_pane, padding="5")
        main_pane.add(right_frame, weight=2)

        # ----- 区域1:导入与词库管理 -----
        import_frame = ttk.LabelFrame(right_frame, text="导入与词库管理", padding="10")
        import_frame.pack(fill=tk.X, pady=(0, 12))

        # 第一行:文件导入 + 手动添加 + 粘贴导入
        row1 = ttk.Frame(import_frame)
        row1.pack(fill=tk.X, pady=4)
        self.btn_import = ttk.Button(row1, text="导入TXT/XLSX", command=self.import_file)
        self.btn_import.pack(side=tk.LEFT, padx=(0, 12))

        ttk.Label(row1, text="手动添加:").pack(side=tk.LEFT, padx=(12, 5))
        self.manual_entry = ttk.Entry(row1, width=14, font=('宋体', 12))
        self.manual_entry.pack(side=tk.LEFT, padx=(0, 5))
        self.manual_entry.bind('<Return>', lambda e: self._add_manual_word())
        btn_add = ttk.Button(row1, text="添加", command=self._add_manual_word)
        btn_add.pack(side=tk.LEFT, padx=(0, 12))

        btn_paste = ttk.Button(row1, text="从剪贴板导入", command=self._import_from_clipboard)
        btn_paste.pack(side=tk.LEFT)

        # 第二行:词库保存与加载
        row2 = ttk.Frame(import_frame)
        row2.pack(fill=tk.X, pady=4)
        btn_save_vocab = ttk.Button(row2, text="保存词库", command=self.save_vocabulary)
        btn_save_vocab.pack(side=tk.LEFT, padx=(0, 12))
        btn_load_vocab = ttk.Button(row2, text="加载词库", command=self.load_vocabulary)
        btn_load_vocab.pack(side=tk.LEFT)
        ttk.Label(row2, text="(*.vocab 文件)", font=('宋体', 10), foreground='gray').pack(side=tk.LEFT, padx=10)

        # 分隔线
        ttk.Separator(right_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=6)

        # ----- 区域2:下载设置 -----
        download_frame = ttk.LabelFrame(right_frame, text="&#11015; 下载音频", padding="10")
        download_frame.pack(fill=tk.X, pady=(0, 12))

        self.download_uk_var = tk.BooleanVar(value=True)
        self.download_us_var = tk.BooleanVar(value=True)
        chk_uk = ttk.Checkbutton(download_frame, text="英音(uk)", variable=self.download_uk_var)
        chk_uk.pack(side=tk.LEFT, padx=(0, 25))
        chk_us = ttk.Checkbutton(download_frame, text="美音(us)", variable=self.download_us_var)
        chk_us.pack(side=tk.LEFT)

        btn_download = ttk.Button(download_frame, text="&#11015;开始下载", command=self.start_download)
        btn_download.pack(side=tk.LEFT, padx=(25, 12))

        self.download_status = ttk.Label(download_frame, text="就绪", foreground='green')
        self.download_status.pack(side=tk.LEFT)

        self.progress = ttk.Progressbar(download_frame, orient=tk.HORIZONTAL, length=180, mode='determinate')
        self.progress.pack(side=tk.RIGHT, padx=(5, 0))

        # 分隔线
        ttk.Separator(right_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=6)

        # ----- 区域3:合成与高级设置 -----
        synth_frame = ttk.LabelFrame(right_frame, text="合成与高级设置", padding="10")
        synth_frame.pack(fill=tk.X, pady=(0, 12))

        # 第一行:顺序、间隔、遍数
        row3 = ttk.Frame(synth_frame)
        row3.pack(fill=tk.X, pady=4)

        ttk.Label(row3, text="合成顺序:").pack(side=tk.LEFT, padx=(0, 5))
        self.order_var = tk.StringVar(value="先英后美(uk_us)")
        order_combo = ttk.Combobox(row3, textvariable=self.order_var,
                                   values=["仅英音(uk)", "仅美音(us)", "先英后美(uk_us)", "先美后英(us_uk)"],
                                   state="readonly", width=16)
        order_combo.pack(side=tk.LEFT, padx=(0, 20))

        ttk.Label(row3, text="间隔(秒):").pack(side=tk.LEFT, padx=(0, 5))
        self.interval_var = tk.DoubleVar(value=1.0)
        spin_interval = ttk.Spinbox(row3, from_=0.5, to=5.0, increment=0.1,
                                    textvariable=self.interval_var, width=6)
        spin_interval.pack(side=tk.LEFT, padx=(0, 15))

        ttk.Label(row3, text="朗读遍数:").pack(side=tk.LEFT, padx=(0, 5))
        self.repeat_var = tk.IntVar(value=1)
        spin_repeat = ttk.Spinbox(row3, from_=1, to=5, increment=1,
                                  textvariable=self.repeat_var, width=6)
        spin_repeat.pack(side=tk.LEFT)

        # 第二行:音量标准化 + 导出听写纸 + 其他操作
        row4 = ttk.Frame(synth_frame)
        row4.pack(fill=tk.X, pady=4)

        self.normalize_var = tk.BooleanVar(value=True)
        chk_normalize = ttk.Checkbutton(row4, text="音频标准化", variable=self.normalize_var)
        chk_normalize.pack(side=tk.LEFT, padx=(0, 10))

        self.btn_export_script = ttk.Button(row4, text="导出听写纸", command=self.export_script, state=tk.DISABLED)
        self.btn_export_script.pack(side=tk.LEFT, padx=(0, 10))
        ttk.Label(row4, text="(需先合成)", font=('宋体', 10), foreground='gray').pack(side=tk.LEFT)

        # 新增:列表排序按钮行(放在导出听写纸后面)
        btn_shuffle = ttk.Button(row4, text="随机顺序", command=self._shuffle_list)
        btn_shuffle.pack(side=tk.LEFT, padx=(0, 10))
        btn_sort = ttk.Button(row4, text="字母顺序", command=self._sort_list)
        btn_sort.pack(side=tk.LEFT)
        ttk.Label(row4, text="(列表调整)", font=('宋体', 10), foreground='gray').pack(side=tk.LEFT, padx=10)
        # 第三行:合成按钮和状态 + 打开/复制/删除
        row5 = ttk.Frame(synth_frame)
        row5.pack(fill=tk.X, pady=6)

        self.btn_synth = ttk.Button(row5, text="合成音频", command=self.start_synthesis)
        self.btn_synth.pack(side=tk.LEFT, padx=(0, 20))

        self.synth_status = ttk.Label(row5, text="就绪", foreground='green')
        self.synth_status.pack(side=tk.LEFT)

        self.btn_open_audio = ttk.Button(row5, text="打开音频", command=self.open_audio, state=tk.DISABLED)
        self.btn_open_audio.pack(side=tk.LEFT, padx=(20, 0))
        self.btn_copy_audio = ttk.Button(row5, text="复制音频", command=self.copy_audio, state=tk.DISABLED)
        self.btn_copy_audio.pack(side=tk.LEFT, padx=(6, 0))
        self.btn_del_audio = ttk.Button(row5, text="删除音频", command=self.delete_audio, state=tk.DISABLED)
        self.btn_del_audio.pack(side=tk.LEFT, padx=(6, 0))

        # 底部提示
        ttk.Label(synth_frame, text="每个单词按指定顺序播放,间隔为单词间的停顿,朗读遍数控制重复次数",
                  font=('宋体', 10), foreground='gray').pack(anchor=tk.W, pady=(4, 0))

    # ------------------------------------------
    # 列表操作
    # ------------------------------------------
    def _update_listbox(self):
        self.word_listbox.delete(0, tk.END)
        for w in self.word_list:
            self.word_listbox.insert(tk.END, w)
        self.list_title_var.set(f"&#128221; 单词列表(共 {len(self.word_list)} 个)")

    def _move_up(self):
        selected = self.word_listbox.curselection()
        if not selected or selected[0] == 0:
            return
        idx = selected[0]
        self.word_list[idx], self.word_list[idx-1] = self.word_list[idx-1], self.word_list[idx]
        self._update_listbox()
        self.word_listbox.selection_set(idx-1)

    def _move_down(self):
        selected = self.word_listbox.curselection()
        if not selected or selected[0] == len(self.word_list)-1:
            return
        idx = selected[0]
        self.word_list[idx], self.word_list[idx+1] = self.word_list[idx+1], self.word_list[idx]
        self._update_listbox()
        self.word_listbox.selection_set(idx+1)

    def _add_manual_word(self):
        word = self.manual_entry.get().strip()
        if not word:
            messagebox.showinfo("提示", "请输入单词")
            return
        if not re.match(r'^[a-zA-Z\-]+$', word):
            messagebox.showwarning("提示", "只支持字母和连字符")
            return
        word = word.lower()
        if word in self.word_list:
            messagebox.showinfo("提示", f"单词 '{word}' 已在列表中")
            return
        self.word_list.append(word)
        self._update_listbox()
        self.manual_entry.delete(0, tk.END)
        self.status_var.set(f"已添加单词: {word}")

    def _delete_selected(self):
        selected = self.word_listbox.curselection()
        if not selected:
            messagebox.showinfo("提示", "请先选中要删除的单词")
            return
        for idx in reversed(selected):
            del self.word_list[idx]
        self._update_listbox()
        self.status_var.set(f"已删除 {len(selected)} 个单词")

    def _clear_list(self):
        if not self.word_list:
            return
        if messagebox.askyesno("确认", "确定要清空所有单词吗?"):
            self.word_list.clear()
            self._update_listbox()
            self.status_var.set("已清空列表")
            
    def _shuffle_list(self):
        """随机打乱单词列表顺序"""
        if len(self.word_list) < 2:
            messagebox.showinfo("提示", "列表单词数少于2个,无需随机")
            return
        random.shuffle(self.word_list)
        self._update_listbox()
        self.status_var.set("已随机打乱列表顺序")

    def _sort_list(self):
        """按字母升序排列单词列表"""
        if len(self.word_list) < 2:
            messagebox.showinfo("提示", "列表单词数少于2个,无需排序")
            return
        self.word_list.sort(key=str.lower)  # 不区分大小写排序
        self._update_listbox()
        self.status_var.set("已按字母顺序排列列表")

    # ------------------------------------------
    # 导入文件(原有)
    # ------------------------------------------
    def import_file(self):
        file_path = filedialog.askopenfilename(
            title="选择单词列表文件",
            filetypes=[("文本文件", "*.txt"), ("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
        )
        if not file_path:
            return

        append = messagebox.askyesno("导入方式", "是否追加到现有列表?\n(选“否”则覆盖现有列表)")
        words = []
        try:
            if file_path.endswith('.txt'):
                with open(file_path, 'r', encoding='utf-8') as f:
                    lines = f.readlines()
                for line in lines:
                    w = line.strip()
                    if w and re.match(r'^[a-zA-Z\-]+$', w):
                        words.append(w.lower())
            elif file_path.endswith('.xlsx'):
                if openpyxl is None:
                    messagebox.showerror("错误", "未安装 openpyxl,请执行: pip install openpyxl")
                    return
                wb = openpyxl.load_workbook(file_path, read_only=True)
                sheet = wb.active
                for row in sheet.iter_rows(values_only=True):
                    if row and row[0]:
                        cell_val = str(row[0]).strip()
                        if cell_val and re.match(r'^[a-zA-Z\-]+$', cell_val):
                            words.append(cell_val.lower())
                wb.close()
            else:
                messagebox.showwarning("提示", "不支持的文件格式,请选择 .txt 或 .xlsx")
                return

            if not words:
                messagebox.showinfo("提示", "未提取到有效单词(只接受字母和连字符)")
                return

            unique = []
            seen = set()
            for w in words:
                if w not in seen:
                    seen.add(w)
                    unique.append(w)

            if append:
                existing_set = set(self.word_list)
                for w in unique:
                    if w not in existing_set:
                        self.word_list.append(w)
                        existing_set.add(w)
            else:
                self.word_list = unique

            self._update_listbox()
            self.manual_entry.delete(0, tk.END)
            self.status_var.set(f"已{'追加' if append else '导入'} {len(self.word_list)} 个单词")

        except Exception as e:
            messagebox.showerror("导入错误", f"读取文件失败: {str(e)}")
            self.status_var.set("导入失败")

    # ------------------------------------------
    # 从剪贴板导入(新增)
    # ------------------------------------------
    def _import_from_clipboard(self):
        try:
            raw_text = self.root.clipboard_get()
        except tk.TclError:
            messagebox.showwarning("提示", "剪贴板为空或无法读取")
            return

        if not raw_text.strip():
            messagebox.showinfo("提示", "剪贴板内容为空")
            return

        # 按空格、逗号、分号、换行、制表位分割(支持中英文标点)
        tokens = re.split(r'[\s,;,;\t\n\r]+', raw_text.strip())
        words = []
        for token in tokens:
            token = token.strip()
            if token and re.match(r'^[a-zA-Z\-]+$', token):
                words.append(token.lower())

        if not words:
            messagebox.showinfo("提示", "未提取到有效单词(只接受字母和连字符)")
            return

        # 去重保留顺序
        unique = []
        seen = set()
        for w in words:
            if w not in seen:
                seen.add(w)
                unique.append(w)

        # 询问追加还是覆盖
        append = messagebox.askyesno("导入方式", f"从剪贴板提取到 {len(unique)} 个单词。\n是否追加到现有列表?\n(选“否”则覆盖现有列表)")
        if append:
            existing_set = set(self.word_list)
            for w in unique:
                if w not in existing_set:
                    self.word_list.append(w)
                    existing_set.add(w)
        else:
            self.word_list = unique

        self._update_listbox()
        self.status_var.set(f"已从剪贴板{'追加' if append else '导入'} {len(unique)} 个单词")
        messagebox.showinfo("导入完成", f"成功{'追加' if append else '导入'} {len(unique)} 个单词")

    # ------------------------------------------
    # 词库保存与加载(不变)
    # ------------------------------------------
    def save_vocabulary(self):
        if not self.word_list:
            messagebox.showwarning("提示", "单词列表为空,无需保存")
            return
        file_path = filedialog.asksaveasfilename(
            defaultextension=".vocab",
            filetypes=[("词库文件", "*.vocab"), ("所有文件", "*.*")],
            initialfile="my_vocab.vocab"
        )
        if file_path:
            try:
                data = {"words": self.word_list, "timestamp": datetime.now().isoformat()}
                with open(file_path, 'w', encoding='utf-8') as f:
                    json.dump(data, f, ensure_ascii=False, indent=2)
                messagebox.showinfo("保存成功", f"词库已保存至:{file_path}")
                self.status_var.set(f"词库已保存: {os.path.basename(file_path)}")
            except Exception as e:
                messagebox.showerror("保存失败", f"保存词库出错: {str(e)}")

    def load_vocabulary(self):
        file_path = filedialog.askopenfilename(
            title="加载词库文件",
            filetypes=[("词库文件", "*.vocab"), ("所有文件", "*.*")]
        )
        if not file_path:
            return
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            words = data.get("words", [])
            if not words:
                messagebox.showwarning("提示", "词库中没有单词")
                return
            self.word_list = words
            self._update_listbox()
            self.status_var.set(f"已加载词库: {os.path.basename(file_path)} ({len(words)} 个单词)")
            messagebox.showinfo("加载成功", f"成功加载 {len(words)} 个单词")
        except Exception as e:
            messagebox.showerror("加载失败", f"加载词库出错: {str(e)}")

    # ------------------------------------------
    # 下载功能(保持不变)
    # ------------------------------------------
    def start_download(self):
        if self.is_downloading:
            return
        if not self.word_list:
            messagebox.showwarning("提示", "单词列表为空")
            return
        if not self.download_uk_var.get() and not self.download_us_var.get():
            messagebox.showwarning("提示", "请至少选择一种发音")
            return

        self.is_downloading = True
        self.download_status.config(text="下载中...", foreground='blue')
        self.progress['value'] = 0
        self.progress['maximum'] = len(self.word_list)

        thread = threading.Thread(target=self._download_worker)
        thread.daemon = True
        thread.start()

    def _download_worker(self):
        download_uk = self.download_uk_var.get()
        download_us = self.download_us_var.get()

        existing_uk = {os.path.splitext(f)[0] for f in os.listdir(self.uk_dir) if f.endswith('.mp3')}
        existing_us = {os.path.splitext(f)[0] for f in os.listdir(self.us_dir) if f.endswith('.mp3')}

        success = 0
        failed = []

        for idx, word in enumerate(self.word_list):
            if not self.is_downloading:
                break

            self.root.after(0, lambda i=idx: self.progress.config(value=i+1))

            if download_uk and word not in existing_uk:
                ok = self._download_single_audio(word, 'uk')
                if ok:
                    success += 1
                    existing_uk.add(word)
                else:
                    failed.append(f"{word} (uk)")

            if download_us and word not in existing_us:
                ok = self._download_single_audio(word, 'us')
                if ok:
                    success += 1
                    existing_us.add(word)
                else:
                    failed.append(f"{word} (us)")

            time.sleep(0.2)

        self.root.after(0, self._download_finished, success, failed)

    def _download_single_audio(self, word, accent):
        try:
            txt_param = self._get_txt_param(word, accent)
            if not txt_param:
                return False

            if accent == 'uk':
                url = f"https://api.frdic.com/api/v2/speech/speakweb?langid=en&voicename=en_uk_male&txt={txt_param}"
                save_dir = self.uk_dir
            else:
                url = f"https://api.frdic.com/api/v2/speech/speakweb?langid=en&voicename=en_us_female&txt={txt_param}"
                save_dir = self.us_dir

            headers = {"User-Agent": "Mozilla/5.0", "Referer": "https://dict.eudic.net/"}
            resp = requests.get(url, headers=headers, timeout=15)
            if resp.status_code != 200:
                return False

            filename = os.path.join(save_dir, f"{word}.mp3")
            with open(filename, 'wb') as f:
                f.write(resp.content)
            return True
        except Exception:
            return False

    def _get_txt_param(self, word, accent):
        try:
            url = f"https://dict.eudic.net/dicts/en/{word}"
            headers = {"User-Agent": "Mozilla/5.0"}
            resp = requests.get(url, headers=headers, timeout=10)
            if resp.status_code != 200:
                return None
            soup = BeautifulSoup(resp.text, 'html.parser')
            target = 'en_uk_male' if accent == 'uk' else 'en_us_female'
            for btn in soup.find_all('a', class_='voice-js'):
                rel = btn.get('data-rel', '')
                if target in rel:
                    match = re.search(r'txt=([^&]+)', rel)
                    if match:
                        return match.group(1)
            return None
        except Exception:
            return None

    def _download_finished(self, success, failed):
        self.is_downloading = False
        self.progress['value'] = 0
        msg = f"下载完成!成功 {success} 个"
        if failed:
            msg += f",失败 {len(failed)} 个"
            self.download_status.config(text="部分失败", foreground='orange')
            messagebox.showwarning("下载完成", f"{msg}\n失败单词: {', '.join(failed[:10])}")
        else:
            self.download_status.config(text="下载完成", foreground='green')
            messagebox.showinfo("下载完成", msg)
        self.status_var.set(msg)

    # ------------------------------------------
    # 合成功能(新增音量标准化)
    # ------------------------------------------
    def start_synthesis(self):
        if self.is_synthesizing:
            return
        if not self.word_list:
            messagebox.showwarning("提示", "单词列表为空")
            return

        if AudioSegment is None:
            messagebox.showerror("错误", "未安装 pydub,请执行: pip install pydub")
            return

        order_str = self.order_var.get()
        if "仅英音" in order_str:
            order_list = ['uk']
        elif "仅美音" in order_str:
            order_list = ['us']
        elif "先英后美" in order_str:
            order_list = ['uk', 'us']
        elif "先美后英" in order_str:
            order_list = ['us', 'uk']
        else:
            order_list = ['uk', 'us']

        repeat_times = self.repeat_var.get()
        if repeat_times < 1:
            repeat_times = 1
        elif repeat_times > 5:
            repeat_times = 5

        missing = []
        for accent in order_list:
            dir_path = self.uk_dir if accent == 'uk' else self.us_dir
            for word in self.word_list:
                if not os.path.exists(os.path.join(dir_path, f"{word}.mp3")):
                    missing.append(f"{word} ({accent})")

        if missing:
            messagebox.showwarning("缺少音频",
                                   f"以下音频文件缺失,请先下载:\n" + "\n".join(missing[:20]) +
                                   (f"\n... 共 {len(missing)} 个" if len(missing) > 20 else ""))
            return

        self.is_synthesizing = True
        self.synth_status.config(text="合成中...", foreground='blue')
        self.btn_open_audio.config(state=tk.DISABLED)
        self.btn_copy_audio.config(state=tk.DISABLED)
        self.btn_del_audio.config(state=tk.DISABLED)
        self.btn_export_script.config(state=tk.DISABLED)
        thread = threading.Thread(target=self._synthesis_worker, args=(order_list, repeat_times))
        thread.daemon = True
        thread.start()

    def _synthesis_worker(self, order_list, repeat_times):
        try:
            interval_ms = int(self.interval_var.get() * 1000)
            normalize = self.normalize_var.get()

            combined = None
            silence = AudioSegment.silent(duration=interval_ms) if interval_ms > 0 else None

            total_steps = len(self.word_list) * repeat_times * len(order_list)
            step = 0
            self.root.after(0, lambda: self.progress.config(maximum=total_steps, value=0))

            for word in self.word_list:
                for _ in range(repeat_times):
                    for accent in order_list:
                        dir_path = self.uk_dir if accent == 'uk' else self.us_dir
                        file_path = os.path.join(dir_path, f"{word}.mp3")
                        seg = AudioSegment.from_mp3(file_path)
                        if normalize and effects is not None:
                            seg = effects.normalize(seg, headroom=1.0)
                        if combined is None:
                            combined = seg
                        else:
                            if silence:
                                combined += silence
                            combined += seg
                        step += 1
                        self.root.after(0, lambda s=step: self.progress.config(value=s))
                    if _ < repeat_times - 1 and silence:
                        combined += silence

            if combined is None:
                raise Exception("没有可合成的音频")

            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            suffix = "_".join(order_list)
            repeat_suffix = f"_x{repeat_times}" if repeat_times > 1 else ""
            filename = f"{timestamp}_{suffix}{repeat_suffix}.mp3"
            output_path = os.path.join(os.getcwd(), filename)

            combined.export(output_path, format="mp3")
            self.root.after(0, self._synthesis_finished, True, output_path)

        except Exception as e:
            self.root.after(0, self._synthesis_finished, False, str(e))

    def _synthesis_finished(self, success, info):
        self.is_synthesizing = False
        self.progress['value'] = 0
        if success:
            self.synth_status.config(text="合成完成", foreground='green')
            self.last_output_path = info
            self.btn_open_audio.config(state=tk.NORMAL)
            self.btn_copy_audio.config(state=tk.NORMAL)
            self.btn_del_audio.config(state=tk.NORMAL)
            self.btn_export_script.config(state=tk.NORMAL)
            messagebox.showinfo("合成完成", f"音频已保存至: {info}")
            self.status_var.set(f"合成成功: {info}")
        else:
            self.synth_status.config(text="合成失败", foreground='red')
            self.btn_open_audio.config(state=tk.DISABLED)
            self.btn_copy_audio.config(state=tk.DISABLED)
            self.btn_del_audio.config(state=tk.DISABLED)
            self.btn_export_script.config(state=tk.DISABLED)
            messagebox.showerror("合成失败", f"错误: {info}")
            self.status_var.set("合成失败")

    # ------------------------------------------
    # 导出听写纸
    # ------------------------------------------
    def export_script(self):
        if not self.word_list:
            messagebox.showwarning("提示", "单词列表为空,无法导出")
            return
        file_path = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
            initialfile="听写纸.txt"
        )
        if not file_path:
            return
        try:
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write("听写练习\n")
                f.write("=" * 30 + "\n\n")
                f.write("请听音频,写出对应的单词:\n\n")
                for i, word in enumerate(self.word_list, 1):
                    f.write(f"{i}. ___________\n")
                f.write("\n" + "=" * 30 + "\n")
                f.write("参考答案:\n")
                for i, word in enumerate(self.word_list, 1):
                    f.write(f"{i}. {word}\n")
            messagebox.showinfo("导出成功", f"听写纸已保存至:{file_path}")
            self.status_var.set(f"听写纸已导出: {os.path.basename(file_path)}")
        except Exception as e:
            messagebox.showerror("导出失败", f"导出听写纸出错: {str(e)}")

    # ------------------------------------------
    # 打开、复制、删除音频
    # ------------------------------------------
    def open_audio(self):
        if not self.last_output_path or not os.path.exists(self.last_output_path):
            messagebox.showerror("错误", "音频文件不存在")
            return
        try:
            if sys.platform.startswith('win'):
                os.startfile(self.last_output_path)
            elif sys.platform.startswith('darwin'):
                subprocess.Popen(['open', self.last_output_path])
            else:
                subprocess.Popen(['xdg-open', self.last_output_path])
        except Exception as e:
            messagebox.showerror("打开失败", f"无法打开音频文件: {str(e)}")

    def copy_audio(self):
        if not self.last_output_path or not os.path.exists(self.last_output_path):
            messagebox.showerror("错误", "没有可复制的音频文件")
            return
        save_path = filedialog.asksaveasfilename(
            defaultextension=".mp3",
            filetypes=[("MP3 文件", "*.mp3")],
            initialfile=os.path.basename(self.last_output_path)
        )
        if save_path:
            try:
                shutil.copy2(self.last_output_path, save_path)
                messagebox.showinfo("复制成功", f"音频已复制到:\n{save_path}")
                self.status_var.set(f"已复制音频至: {save_path}")
            except Exception as e:
                messagebox.showerror("复制失败", f"复制出错: {str(e)}")

    def delete_audio(self):
        if not self.last_output_path or not os.path.exists(self.last_output_path):
            messagebox.showerror("错误", "音频文件不存在")
            return
        if not messagebox.askyesno("确认删除", f"确定要删除音频文件吗?\n{self.last_output_path}"):
            return
        try:
            with open(self.last_output_path, 'r+b') as f:
                pass
            os.remove(self.last_output_path)
            self.last_output_path = None
            self.btn_open_audio.config(state=tk.DISABLED)
            self.btn_copy_audio.config(state=tk.DISABLED)
            self.btn_del_audio.config(state=tk.DISABLED)
            self.btn_export_script.config(state=tk.DISABLED)
            self.status_var.set("音频已删除")
            messagebox.showinfo("删除成功", "音频文件已删除")
        except PermissionError:
            messagebox.showerror("删除失败", "音频文件正在使用中,请关闭播放器后再删除")
        except Exception as e:
            messagebox.showerror("删除失败", f"删除出错: {str(e)}")

    # ------------------------------------------
    # 窗口关闭清理
    # ------------------------------------------
    def on_closing(self):
        if self.is_downloading or self.is_synthesizing:
            if not messagebox.askokcancel("确认", "正在后台处理,确定退出吗?"):
                return
        self.root.destroy()


if __name__ == "__main__":
    root = tk.Tk()
    app = AudioDownloaderApp(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()

免费评分

参与人数 4吾爱币 +4 热心值 +4 收起 理由
shuangluo + 1 + 1 我很赞同!
heavenman + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
CEXO3574 + 1 + 1 谢谢@Thanks!
yinduyang444 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
jacktong85 发表于 2026-7-7 21:59
这个批量下载方便,加强听音识别能力。比猫爪一个个下载好用多了

点评

感谢支持。  发表于 2026-7-7 22:07
推荐
jialife 发表于 2026-7-8 10:32
感谢分享,很好用,分流给有需要的大佬

百度:https://pan.baidu.com/s/1nbFgO-4RwwnKueSV5JkMGQ?pwd=h8jh 提取码: h8jh

夸克:https://pan.quark.cn/s/2ad5508feff4?pwd=f3cY 提取码:f3cY

迅雷:https://pan.xunlei.com/s/VOwzYdxEDTORSPUvU3u2UdTaA1?pwd=dqgg 提取码:dqgg
3#
zhu791023529 发表于 2026-7-7 22:19
本帖最后由 zhu791023529 于 2026-7-7 22:21 编辑

合成失败:错误:[WinError2]系统找不到指定的文件。
4#
sqqqqq 发表于 2026-7-8 00:01
下来试试,有时候得给孩子听写,但是有的不会发音
5#
hnxylhl 发表于 2026-7-8 08:12
已试过了,简直好的不要不要的,谢谢分享。

图片1.png (28.88 KB, 下载次数: 0)

图片1.png
6#
t9k9k9 发表于 2026-7-8 08:32
合成失败:错误:[WinError2]系统找不到指定的文件。
7#
CEXO3574 发表于 2026-7-8 08:49
好像很不错,给小孩子备一个。
8#
iawyxkdn8 发表于 2026-7-8 09:05
真的很好用,
9#
xiaoxino 发表于 2026-7-8 09:47
下来看看什么样的
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-8 12:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表