import os import time import threading import random import tkinter as tk from tkinter import ttk, filedialog, messagebox import requests import json import warnings # 忽略requests的警告 warnings.filterwarnings("ignore") class YoudaoVoicePlayer: """使用有道词典API获取单词读音和释义""" def __init__(self): self.cache_dir = "voice_cache" # 创建缓存目录 if not os.path.exists(self.cache_dir): os.makedirs(self.cache_dir) def get_chinese_meaning(self, word): """获取单词的中文释义 - 只返回一个简洁的意思""" # 方法1: 使用有道翻译API meaning = self.get_meaning_from_youdao(word) if meaning: # 如果返回多个意思,只取第一个 if ';' in meaning or ';' in meaning or ',' in meaning or ',' in meaning: # 按常见分隔符分割,取第一个 for sep in [';', ';', ',', ',', '、', ' ']: if sep in meaning: meaning = meaning.split(sep)[0].strip() break return meaning # 方法2: 使用本地词库 meaning = self.get_local_meaning(word) if meaning: return meaning # 方法3: 返回空字符串 return "" def get_meaning_from_youdao(self, word): """从有道词典获取释义""" try: # 使用有道词典的单词查询API url = f"http://dict.youdao.com/suggest?num=1&doctype=json&q={word}" headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } response = requests.get(url, headers=headers, timeout=3) if response.status_code == 200: data = response.json() if data and 'data' in data and 'entries' in data['data'] and len(data['data']['entries']) > 0: entry = data['data']['entries'][0] if 'explain' in entry: return entry['explain'] except Exception as e: # 静默失败,不打印错误信息 pass return None def get_local_meaning(self, word): """本地词库 - 只保留最常用的意思""" common_words = { # 基础词汇 'apple': '苹果', 'banana': '香蕉', 'cat': '猫', 'dog': '狗', 'book': '书', 'car': '汽车', 'house': '房子', 'water': '水', 'food': '食物', 'school': '学校', 'teacher': '老师', 'student': '学生', 'mother': '母亲', 'father': '父亲', 'brother': '兄弟', 'sister': '姐妹', 'friend': '朋友', 'love': '爱', 'like': '喜欢', 'good': '好', 'bad': '坏', 'big': '大', 'small': '小', 'hot': '热', 'cold': '冷', 'new': '新', 'old': '旧', 'young': '年轻', 'beautiful': '美丽', 'happy': '快乐', 'sad': '悲伤', 'run': '跑', 'walk': '走', 'eat': '吃', 'drink': '喝', 'sleep': '睡觉', 'work': '工作', 'study': '学习', 'play': '玩', 'read': '读', 'write': '写', 'speak': '说', 'listen': '听', 'see': '看见', 'look': '看', 'give': '给', 'take': '拿', 'help': '帮助', 'make': '制作', 'use': '使用', 'find': '找到', 'lose': '丢失', 'buy': '买', 'sell': '卖', 'come': '来', 'go': '去', 'begin': '开始', 'finish': '结束', 'open': '打开', 'close': '关闭', 'put': '放', 'get': '得到', 'have': '有', 'do': '做', 'say': '说', 'tell': '告诉', 'ask': '问', 'think': '想', 'know': '知道', 'understand': '理解', 'learn': '学习', 'forget': '忘记', 'remember': '记住', 'want': '想要', 'need': '需要', 'can': '能', 'may': '可以', 'must': '必须', 'will': '将', 'time': '时间', 'day': '天', 'night': '夜晚', 'morning': '早晨', 'afternoon': '下午', 'evening': '晚上', 'week': '周', 'month': '月', 'year': '年', 'today': '今天', 'tomorrow': '明天', 'yesterday': '昨天', 'now': '现在', 'red': '红', 'blue': '蓝', 'green': '绿', 'yellow': '黄', 'black': '黑', 'white': '白', 'one': '一', 'two': '二', 'three': '三', 'four': '四', 'five': '五', 'six': '六', 'seven': '七', 'eight': '八', 'nine': '九', 'ten': '十' } word_lower = word.lower() if word_lower in common_words: return common_words[word_lower] return "" def play_with_windows(self, word): """使用Windows语音朗读""" try: # 转义引号 text = word.replace('"', '`"') # 使用PowerShell调用系统语音 cmd = f'powershell -Command "Add-Type -AssemblyName System.Speech; $synth = New-Object System.Speech.Synthesis.SpeechSynthesizer; $synth.Rate = 1; $synth.Speak(\'{text}\')"' os.system(cmd) time.sleep(0.1) return True except: return False def speak(self, word): """朗读单词""" try: return self.play_with_windows(word) except: return False class SimpleDictationApp: def __init__(self, root): self.root = root self.root.title("英语单词听写助手") self.root.geometry("1000x900") # 稍微减小高度 # 设置窗口背景渐变 self.root.configure(bg='#2b3a4a') # 创建渐变背景画布 self.canvas = tk.Canvas(self.root, highlightthickness=0) self.canvas.place(x=0, y=0, relwidth=1, relheight=1) self.create_gradient() self.words = [] self.start_index = 0 self.is_playing = False self.is_paused = False self.stop_playback = False self.redo_clicked = False self.pause_condition = threading.Condition() self.current_thread = None self.player = YoudaoVoicePlayer() self.dictation_mode = "notebook" self.user_input = "" self.expected_word = "" self.is_waiting_for_input = False # 正确率统计 self.total_answered = 0 self.correct_count = 0 self.wrong_count = 0 # 50种鲜艳的颜色 self.colors = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2', '#F8C471', '#82E0AA', '#F1948A', '#85C1E2', '#D7BDE2', '#FAD7A0', '#A9DFBF', '#F9E79F', '#D2B4DE', '#AED6F1', '#F5B7B1', '#A3E4D7', '#FADBD8', '#D5DBDB', '#E8DAEF', '#FCF3CF', '#D4EFDF', '#D6EAF8', '#FAD7A0', '#EBDEF0', '#F9EBEA', '#E8F8F5', '#FEF9E7', '#E8F4F8', '#F5EEF8', '#FFE5B4', '#E0BBE4', '#B5EAD7', '#C7CEEA', '#FFDAC1', '#FF9AA2', '#B5EAD7', '#C7CEEA', '#FFDAC1', '#FFB7B2', '#C06C84', '#6C5B7B', '#355C7D', '#F67280', '#F8B195' ] self.create_widgets() self.apply_styles() # 忽略requests的警告 warnings.filterwarnings("ignore") def create_gradient(self): """创建渐变背景""" width = self.root.winfo_width() or 1000 height = self.root.winfo_height() or 900 for i in range(height): r = int(43 + (i / height) * 30) g = int(58 + (i / height) * 30) b = int(74 + (i / height) * 30) color = f'#{r:02x}{g:02x}{b:02x}' self.canvas.create_line(0, i, width, i, fill=color, width=1) def apply_styles(self): """应用样式""" style = ttk.Style() style.theme_use('clam') style.configure("Custom.Horizontal.TProgressbar", background='#4CAF50', troughcolor='#E0E0E0', bordercolor='#FFFFFF', lightcolor='#4CAF50', darkcolor='#45A049', thickness=20) def create_widgets(self): # ========== 主容器 ========== main_container = tk.Frame(self.root, bg='#ffffff', bd=3, relief=tk.RAISED) main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) # ========== 标题区 ========== title_frame = tk.Frame(main_container, bg='#ffffff', height=60) title_frame.pack(fill=tk.X, padx=15, pady=(10, 5)) title_frame.pack_propagate(False) title = tk.Label(title_frame, text="英语单词听写助手", font=("微软雅黑", 20, "bold"), bg='#ffffff', fg='#2C3E50') title.pack(expand=True) # ========== 模式切换区 ========== mode_frame = tk.Frame(main_container, bg='#F0F0F0', height=50, relief=tk.GROOVE, bd=2) mode_frame.pack(fill=tk.X, padx=15, pady=5) mode_frame.pack_propagate(False) tk.Label(mode_frame, text="✨ 当前模式:", font=("微软雅黑", 11, "bold"), bg='#F0F0F0', fg='#2C3E50').pack(side=tk.LEFT, padx=15) self.mode_var = tk.StringVar(value="notebook") self.notebook_radio = tk.Radiobutton(mode_frame, text="📓 本子默写", variable=self.mode_var, value="notebook", font=("微软雅黑", 10, "bold"), bg='#F0F0F0', fg='#27AE60', selectcolor='#F0F0F0', command=self.switch_mode) self.notebook_radio.pack(side=tk.LEFT, padx=15) self.computer_radio = tk.Radiobutton(mode_frame, text="💻 电脑默写", variable=self.mode_var, value="computer", font=("微软雅黑", 10, "bold"), bg='#F0F0F0', fg='#8E44AD', selectcolor='#F0F0F0', command=self.switch_mode) self.computer_radio.pack(side=tk.LEFT, padx=15) # ========== 按钮区 ========== btn_frame = tk.Frame(main_container, bg='#ffffff', height=100) btn_frame.pack(fill=tk.X, padx=15, pady=5) btn_frame.pack_propagate(False) # 第一行按钮 btn_row1 = tk.Frame(btn_frame, bg='#ffffff') btn_row1.pack(expand=True, fill=tk.BOTH, pady=2) self.start_btn = tk.Button(btn_row1, text="▶ 开始听写", font=("微软雅黑", 12, "bold"), bg="#4CAF50", fg="white", width=12, height=1, bd=0, cursor="hand2", command=self.start_dictation, state=tk.DISABLED) self.start_btn.pack(side=tk.LEFT, padx=5, expand=True) self.pause_btn = tk.Button(btn_row1, text="⏸️ 暂停", font=("微软雅黑", 12, "bold"), bg="#FF9800", fg="white", width=12, height=1, bd=0, cursor="hand2", command=self.toggle_pause, state=tk.DISABLED) self.pause_btn.pack(side=tk.LEFT, padx=5, expand=True) self.redo_btn = tk.Button(btn_row1, text="⟲ 重新开始", font=("微软雅黑", 12, "bold"), bg="#f44336", fg="white", width=12, height=1, bd=0, cursor="hand2", command=self.redo_dictation, state=tk.DISABLED) self.redo_btn.pack(side=tk.LEFT, padx=5, expand=True) # 第二行按钮 btn_row2 = tk.Frame(btn_frame, bg='#ffffff') btn_row2.pack(expand=True, fill=tk.BOTH, pady=2) self.upload_btn = tk.Button(btn_row2, text="📁 上传单词", font=("微软雅黑", 11, "bold"), bg="#2196F3", fg="white", width=12, height=1, bd=0, cursor="hand2", command=self.upload_words) self.upload_btn.pack(side=tk.LEFT, padx=5, expand=True) self.test_btn = tk.Button(btn_row2, text="🔊 测试语音", font=("微软雅黑", 11, "bold"), bg="#FF9800", fg="white", width=12, height=1, bd=0, cursor="hand2", command=self.test_voice) self.test_btn.pack(side=tk.LEFT, padx=5, expand=True) # ========== 从指定序号开始输入区 ========== start_frame = tk.Frame(main_container, bg='#F8F9FA', height=80, relief=tk.GROOVE, bd=2) start_frame.pack(fill=tk.X, padx=15, pady=5) start_frame.pack_propagate(False) start_input_frame = tk.Frame(start_frame, bg='#F8F9FA') start_input_frame.pack(expand=True) # 序号输入 tk.Label(start_input_frame, text="🔢 从第几个开始:", font=("微软雅黑", 10, "bold"), bg='#F8F9FA', fg='#2C3E50').grid(row=0, column=0, padx=5, pady=5) self.start_number_entry = tk.Entry(start_input_frame, font=("微软雅黑", 10), bg='#ffffff', fg='#2C3E50', bd=2, relief=tk.GROOVE, width=6) self.start_number_entry.grid(row=0, column=1, padx=5, pady=5, ipady=2) self.start_number_entry.bind('', lambda e: self.set_start_by_number()) self.set_number_btn = tk.Button(start_input_frame, text="✓ 设置", font=("微软雅黑", 9, "bold"), bg="#607D8B", fg="white", width=5, height=1, bd=0, cursor="hand2", command=self.set_start_by_number, state=tk.DISABLED) self.set_number_btn.grid(row=0, column=2, padx=5, pady=5) # 分隔线 tk.Label(start_input_frame, text="│", font=("微软雅黑", 14, "bold"), bg='#F8F9FA', fg='#CCCCCC').grid(row=0, column=3, padx=10) # 单词输入 tk.Label(start_input_frame, text="📝 或输入单词:", font=("微软雅黑", 10, "bold"), bg='#F8F9FA', fg='#2C3E50').grid(row=0, column=4, padx=5, pady=5) self.start_word_entry = tk.Entry(start_input_frame, font=("微软雅黑", 10), bg='#ffffff', fg='#2C3E50', bd=2, relief=tk.GROOVE, width=10) self.start_word_entry.grid(row=0, column=5, padx=5, pady=5, ipady=2) self.start_word_entry.bind('', lambda e: self.set_start_by_word()) self.set_word_btn = tk.Button(start_input_frame, text="✓ 设置", font=("微软雅黑", 9, "bold"), bg="#607D8B", fg="white", width=5, height=1, bd=0, cursor="hand2", command=self.set_start_by_word, state=tk.DISABLED) self.set_word_btn.grid(row=0, column=6, padx=5, pady=5) # 显示当前设置 self.start_position_label = tk.Label(start_frame, text="", font=("微软雅黑", 9, "italic"), bg='#F8F9FA', fg='#27AE60') self.start_position_label.pack(side=tk.BOTTOM, pady=2) # ========== 状态显示区 ========== status_frame = tk.Frame(main_container, bg='#F8F9FA', relief=tk.GROOVE, bd=2) status_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=5) # 状态标签 self.status_label = tk.Label(status_frame, text="✨ 请上传单词文件", font=("微软雅黑", 12, "bold"), bg='#F8F9FA', fg='#5D6D7E', pady=5) self.status_label.pack() # 当前播放状态显示 display_frame = tk.Frame(status_frame, bg='#ffffff', relief=tk.GROOVE, bd=2, height=80) display_frame.pack(fill=tk.X, padx=15, pady=5) display_frame.pack_propagate(False) self.current_label = tk.Label(display_frame, text="✨ 准备就绪", font=("微软雅黑", 18, "bold"), bg='#ffffff') self.current_label.pack(expand=True) # 中文提示显示 self.meaning_label = tk.Label(status_frame, text="", font=("微软雅黑", 14, "bold"), bg='#F8F9FA', fg='#E67E22') self.meaning_label.pack(pady=3) # 重复次数显示 self.repeat_label = tk.Label(status_frame, text="", font=("微软雅黑", 11, "bold"), bg='#F8F9FA', fg='#2C3E50') self.repeat_label.pack() # 进度条 self.progress = ttk.Progressbar(status_frame, style="Custom.Horizontal.TProgressbar", length=400, mode='determinate') self.progress.pack(pady=5) self.progress.pack_forget() # ========== 电脑默写输入区 ========== self.computer_frame = tk.Frame(status_frame, bg='#ffffff', relief=tk.GROOVE, bd=2) # 正确率显示 - 放在最上面 accuracy_frame = tk.Frame(self.computer_frame, bg='#E8F5E8', height=35) accuracy_frame.pack(fill=tk.X, padx=10, pady=(5, 2)) accuracy_frame.pack_propagate(False) # 正确率图标和文字 tk.Label(accuracy_frame, text="📊 正确率:", font=("微软雅黑", 10, "bold"), bg='#E8F5E8', fg='#2C3E50').pack(side=tk.LEFT, padx=(5, 2)) self.accuracy_label = tk.Label(accuracy_frame, text="0% (0/0)", font=("微软雅黑", 11, "bold"), bg='#E8F5E8', fg='#27AE60') self.accuracy_label.pack(side=tk.LEFT, padx=2) # 输入标签 input_label = tk.Label(self.computer_frame, text="✏️ 请输入单词:", font=("微软雅黑", 12, "bold"), bg='#ffffff', fg='#2C3E50') input_label.pack(pady=(10, 2)) # 输入框 input_frame = tk.Frame(self.computer_frame, bg='#ffffff') input_frame.pack(pady=5) self.input_entry = tk.Entry(input_frame, font=("微软雅黑", 16), bg='#ffffff', fg='#2C3E50', bd=2, relief=tk.GROOVE, width=25) self.input_entry.pack(ipady=8) self.input_entry.bind('', lambda e: self.check_answer()) # 提交按钮 self.check_btn = tk.Button(self.computer_frame, text="✅ 提交答案", font=("微软雅黑", 11, "bold"), bg="#4CAF50", fg="white", width=12, height=1, bd=0, cursor="hand2", command=self.check_answer, state=tk.DISABLED) self.check_btn.pack(pady=5) # 结果显示 self.result_label = tk.Label(self.computer_frame, text="", font=("微软雅黑", 12, "bold"), bg='#ffffff', fg='#27AE60') self.result_label.pack(pady=3) # 初始隐藏 self.computer_frame.pack_forget() # ========== 单词数量显示 ========== info_frame = tk.Frame(main_container, bg='#F8F9FA', height=35, relief=tk.GROOVE, bd=2) info_frame.pack(fill=tk.X, padx=15, pady=5) info_frame.pack_propagate(False) self.word_count_label = tk.Label(info_frame, text="📊 当前单词数量: 0", font=("微软雅黑", 11, "bold"), bg='#F8F9FA', fg='#2C3E50') self.word_count_label.pack(expand=True) # 绑定窗口大小变化事件 self.root.bind('', self.on_window_resize) def on_window_resize(self, event): """窗口大小变化时更新渐变""" if event.widget == self.root: self.canvas.delete("all") self.create_gradient() def switch_mode(self): """切换听写模式""" self.dictation_mode = self.mode_var.get() if self.dictation_mode == "computer": self.computer_frame.pack(fill=tk.X, padx=10, pady=5) self.status_label.config(text="💻 电脑默写模式 - 听完后输入单词", fg='#8E44AD') else: self.computer_frame.pack_forget() self.status_label.config(text="📓 本子默写模式 - 请在纸上书写", fg='#27AE60') def toggle_pause(self): """暂停/继续播放""" if self.is_paused: self.is_paused = False self.pause_btn.config(text="⏸️ 暂停", bg="#FF9800") self.status_label.config(text="▶ 继续播放...") with self.pause_condition: self.pause_condition.notify() else: self.is_paused = True self.pause_btn.config(text="▶ 继续", bg="#4CAF50") self.status_label.config(text="⏸️ 已暂停") def set_start_by_number(self): """通过序号设置开始位置""" if not self.words: messagebox.showwarning("警告", "请先上传单词文件") return try: num = int(self.start_number_entry.get().strip()) if 1 <= num <= len(self.words): self.start_index = num - 1 self.start_position_label.config(text=f"✅ 当前: 从第 {num} 个单词开始") self.start_number_entry.delete(0, tk.END) self.status_label.config(text=f"✅ 已设置从第 {num} 个单词开始") else: messagebox.showwarning("警告", f"序号必须在 1-{len(self.words)} 之间") except ValueError: messagebox.showwarning("警告", "请输入有效的数字") def set_start_by_word(self): """通过单词设置开始位置""" if not self.words: messagebox.showwarning("警告", "请先上传单词文件") return input_word = self.start_word_entry.get().strip() if not input_word: messagebox.showwarning("警告", "请输入单词") return input_word_lower = input_word.lower() found = False for i, word in enumerate(self.words): if word.lower() == input_word_lower: self.start_index = i found = True break if found: self.start_position_label.config(text=f"✅ 当前: 从第 {self.start_index+1} 个单词开始") self.start_word_entry.delete(0, tk.END) self.status_label.config(text=f"✅ 已设置从第 {self.start_index+1} 个单词开始") else: messagebox.showwarning("警告", f"单词 '{input_word}' 不在列表中") def test_voice(self): """测试语音是否正常""" try: self.status_label.config(text="🔊 正在测试语音...") self.current_label.config(text="🔊 测试中...", bg='#FDEBD0') self.root.update() # 测试单词 test_word = "hello" meaning = self.player.get_chinese_meaning(test_word) success = self.player.speak(test_word) if success: self.status_label.config(text=f"✅ 语音测试成功") self.current_label.config(text="✨ 准备就绪", bg='#ffffff') if meaning: self.meaning_label.config(text=f"📖 {meaning}") messagebox.showinfo("成功", f"✅ 语音测试成功!\n\n单词: {test_word}\n释义: {meaning if meaning else '未找到释义'}") else: self.status_label.config(text="❌ 语音测试失败") messagebox.showwarning("警告", "语音测试失败") except Exception as e: messagebox.showerror("错误", f"语音测试失败: {e}") self.status_label.config(text="❌ 语音测试失败") def upload_words(self): """上传单词文件""" file_path = filedialog.askopenfilename( title="选择单词文件", filetypes=[("Text files", "*.txt"), ("All files", "*.*")] ) if file_path: try: encodings = ['utf-8', 'gbk', 'gb2312', 'ansi'] content = None for encoding in encodings: try: with open(file_path, 'r', encoding=encoding) as f: content = f.read() break except UnicodeDecodeError: continue if content is None: messagebox.showerror("错误", "无法读取文件") return words = [] lines = content.split('\n') for line in lines: line = line.strip() if len(line) > 0: words.append(line) if len(words) > 0: self.words = words self.start_index = 0 self.total_answered = 0 self.correct_count = 0 self.wrong_count = 0 self.redo_clicked = False self.word_count_label.config(text=f"📊 当前单词数量: {len(words)} 个") self.status_label.config(text=f"✅ 成功加载 {len(words)} 个单词") self.current_label.config(text="✨ 准备就绪", bg='#ffffff') self.start_position_label.config(text="") self.meaning_label.config(text="") self.start_btn.config(state=tk.NORMAL, bg="#4CAF50") self.redo_btn.config(state=tk.NORMAL, bg="#f44336") self.pause_btn.config(state=tk.NORMAL, bg="#FF9800") self.set_number_btn.config(state=tk.NORMAL, bg="#607D8B") self.set_word_btn.config(state=tk.NORMAL, bg="#607D8B") messagebox.showinfo("成功", f"✅ 成功加载 {len(words)} 个单词") else: messagebox.showwarning("警告", "文件中没有有效的单词") except Exception as e: messagebox.showerror("错误", f"读取失败: {str(e)}") def update_accuracy(self): """更新正确率显示""" if self.total_answered > 0: accuracy = (self.correct_count / self.total_answered) * 100 self.accuracy_label.config( text=f"{accuracy:.1f}% ({self.correct_count}/{self.total_answered})", fg='#27AE60' if accuracy >= 70 else '#E74C3C' ) def get_performance_comment(self): """获取成绩评语""" if self.total_answered == 0: return "还没有完成任何单词哦" accuracy = (self.correct_count / self.total_answered) * 100 if accuracy >= 95: return "🌟 太棒了!你是单词大师!" elif accuracy >= 85: return "✨ 非常优秀!继续加油!" elif accuracy >= 75: return "📚 良好,再练习会更完美" elif accuracy >= 60: return "📝 及格了,还需要多练习哦" elif accuracy >= 40: return "📖 需要加强练习,别灰心!" else: return "💪 加油!多听几遍会更好" def check_answer(self): """检查答案并立即清除输入框""" if not self.is_waiting_for_input: return user_input = self.input_entry.get().strip().lower() expected = self.expected_word.lower() self.total_answered += 1 if user_input == expected: self.correct_count += 1 self.result_label.config(text="✅ 正确!", fg='#27AE60') self.status_label.config(text="✅ 正确!") else: self.wrong_count += 1 self.result_label.config(text=f"❌ 错误!正确答案是: {self.expected_word}", fg='#E74C3C') self.status_label.config(text="❌ 错误,请记住正确答案") # 更新正确率 self.update_accuracy() # 立即清除输入框并禁用 self.input_entry.delete(0, tk.END) self.input_entry.config(state=tk.DISABLED) self.check_btn.config(state=tk.DISABLED) self.is_waiting_for_input = False # 1.5秒后自动继续 self.root.after(1500, self.continue_to_next) def continue_to_next(self): """继续下一个单词""" with self.pause_condition: self.pause_condition.notify() def playback_thread(self): """播放线程""" total_words = len(self.words) total_plays = (total_words - self.start_index) * 2 current_play = 0 # 重置重新开始标记 self.redo_clicked = False self.root.after(0, lambda: self.progress.config(maximum=total_plays)) for i in range(self.start_index, total_words): if self.stop_playback or self.redo_clicked: break self.expected_word = self.words[i] # 获取中文释义(简洁版) meaning = self.player.get_chinese_meaning(self.expected_word) # 更新显示当前单词序号 self.root.after(0, lambda idx=i, m=meaning: self.update_word_display(idx, m)) # 播放2遍 for repeat in range(2): if self.stop_playback or self.redo_clicked: break with self.pause_condition: while self.is_paused and not self.stop_playback and not self.redo_clicked: self.pause_condition.wait(0.1) if self.stop_playback or self.redo_clicked: break current_play += 1 self.root.after(0, lambda r=repeat+1, p=current_play: self.update_repeat_display(r, p)) self.player.speak(self.expected_word) # 电脑默写模式等待输入 if self.dictation_mode == "computer" and not self.stop_playback and not self.redo_clicked: self.root.after(0, self.show_computer_input) with self.pause_condition: self.is_waiting_for_input = True while self.is_waiting_for_input and not self.stop_playback and not self.redo_clicked: self.pause_condition.wait(0.05) if not self.redo_clicked: self.root.after(0, self.playback_finished) def show_computer_input(self): """显示电脑输入框""" self.computer_frame.pack(fill=tk.X, padx=10, pady=5) self.input_entry.config(state=tk.NORMAL) self.check_btn.config(state=tk.NORMAL) self.input_entry.delete(0, tk.END) self.input_entry.focus() self.result_label.config(text="") self.status_label.config(text="✏️ 请输入单词") def update_word_display(self, index, meaning): """更新状态显示 - 显示简洁中文提示""" color = random.choice(self.colors) self.current_label.config( text=f"🎤 正在听写第 {index+1}/{len(self.words)} 个", bg=color ) # 显示简洁中文提示 if meaning: self.meaning_label.config(text=f"📖 {meaning}") else: self.meaning_label.config(text="") def update_repeat_display(self, repeat, progress): """更新重复次数显示""" icons = ['①', '②'] self.repeat_label.config(text=f"{icons[repeat-1]} 第 {repeat}/2 遍") self.progress['value'] = progress def playback_finished(self): """播放结束""" self.is_playing = False self.is_paused = False self.stop_playback = False self.redo_clicked = False self.is_waiting_for_input = False self.current_thread = None # 获取成绩评语 comment = self.get_performance_comment() # 显示最终正确率和评语 if self.total_answered > 0: accuracy = (self.correct_count / self.total_answered) * 100 self.current_label.config( text=f"✅ 正确率: {accuracy:.1f}% ({self.correct_count}/{self.total_answered})", bg='#E8F5E8' ) self.status_label.config(text=f"✨ {comment}", fg='#2C3E50') self.meaning_label.config(text="") # 显示评语弹窗 messagebox.showinfo("🎯 你的成绩", f"正确率: {accuracy:.1f}%\n" f"正确: {self.correct_count} 个\n" f"错误: {self.wrong_count} 个\n" f"总计: {self.total_answered} 个\n\n" f"📝 {comment}") else: self.current_label.config(text="✅ 听写完成!", bg='#E8F5E8') self.status_label.config(text="✨ 听写完成", fg='#2C3E50') self.meaning_label.config(text="") self.repeat_label.config(text="") self.computer_frame.pack_forget() self.input_entry.config(state=tk.DISABLED) self.check_btn.config(state=tk.DISABLED) self.start_btn.config(state=tk.NORMAL, bg="#4CAF50") self.redo_btn.config(state=tk.NORMAL, bg="#f44336") self.pause_btn.config(state=tk.DISABLED, bg="#A5D6A5") self.upload_btn.config(state=tk.NORMAL) self.test_btn.config(state=tk.NORMAL) self.set_number_btn.config(state=tk.NORMAL, bg="#607D8B") self.set_word_btn.config(state=tk.NORMAL, bg="#607D8B") self.progress.pack_forget() def stop_current_playback(self): """停止当前播放""" if self.is_playing: self.stop_playback = True self.is_waiting_for_input = False with self.pause_condition: self.pause_condition.notify_all() if self.current_thread and self.current_thread.is_alive(): self.current_thread.join(timeout=1) def start_dictation(self): """开始听写""" if not self.words: messagebox.showwarning("警告", "请先上传单词文件") return self.stop_current_playback() time.sleep(0.1) self.is_playing = True self.is_paused = False self.stop_playback = False self.redo_clicked = False self.start_btn.config(state=tk.DISABLED, bg="#A5D6A5") self.redo_btn.config(state=tk.NORMAL, bg="#f44336") self.pause_btn.config(state=tk.NORMAL, text="⏸️ 暂停", bg="#FF9800") self.upload_btn.config(state=tk.DISABLED) self.test_btn.config(state=tk.DISABLED) self.set_number_btn.config(state=tk.DISABLED) self.set_word_btn.config(state=tk.DISABLED) # 如果是本子模式,确保电脑输入框隐藏 if self.dictation_mode == "notebook": self.computer_frame.pack_forget() self.progress.pack(pady=5) self.progress['maximum'] = (len(self.words) - self.start_index) * 2 self.progress['value'] = 0 self.current_label.config(text="🎤 正在准备...", bg='#FDEBD0') self.status_label.config(text=f"▶ 从第 {self.start_index+1} 个单词开始") self.meaning_label.config(text="") self.current_thread = threading.Thread(target=self.playback_thread) self.current_thread.daemon = True self.current_thread.start() def redo_dictation(self): """重新听写""" if not self.words: messagebox.showwarning("警告", "请先上传单词文件") return # 如果已经点击过重新开始,则不再执行 if self.redo_clicked: messagebox.showinfo("提示", "已经点击过重新开始,请稍等...") return # 设置重新开始标记 self.redo_clicked = True # 禁用重新开始按钮 self.redo_btn.config(state=tk.DISABLED, bg="#A5D6A5") self.stop_current_playback() time.sleep(0.1) self.start_index = 0 self.start_position_label.config(text="") # 重置统计 self.total_answered = 0 self.correct_count = 0 self.wrong_count = 0 self.accuracy_label.config(text="0% (0/0)") self.start_dictation() if __name__ == "__main__": root = tk.Tk() app = SimpleDictationApp(root) root.mainloop()