带语音时间和问候语的挂钟

本教程展示了如何使用创建 基于 Python 的挂钟 pygame,使用 pyttsx3Vosk 进行 文本转语音语音转文本。该应用程序侦听“时间”一词,并根据当前时间响应当前时间和问候语。

目录

  1. 介绍
  2. 特点概述
  3. 先决条件
  4. 安装依赖项

  5. 了解语音转文本 (Vosk)

  6. 了解文本转语音 (pyttsx3)

  7. 代码分解

  8. 运行应用程序
  9. 故障排除
  10. 完整源代码

介绍

该项目使用以下命令构建了一个漂亮的挂钟 GUI pygame——但有一个转折:

  • 它可以大声说出时间 …并且它可以使用语音识别听到您询问时间
  • 当您说“时间”时,应用程序将使用Vosk检测您的语音,使用pyttsx3说出当前时间,并在屏幕底部显示流畅的打字动画

特点概述

模拟挂钟

  • 光滑的秒针、分针和时针
  • 日期和星期显示
  • 可选的深色主题兼容

内置滴答声

  • 使用 NumPy 人工生成
  • 无需外部音频文件

语音检测(STT)

  • 使用 Vosk 离线语音识别
  • 无需互联网即可工作
  • 检测简单的关键字(“时间”)

文本转语音 (TTS)

  • 使用 pyttsx3(离线)
  • 自动说话: “下午好。现在是下午 03:25!”

打字动画

  • 显示问候语和时间
  • 平滑闪烁的光标
  • 几秒钟后自动清除

收听按钮

  • 切换连续麦克风监听
  • 在后台线程中运行识别

先决条件

您只需要:

  • Python 3.8+(最好使用Python 3.12)
  • 麦克风
  • 终端基本使用
  • 能够安装软件包

安装依赖项

视窗

python -m venv py312
py312\Scripts\activate

pip install pygame pyttsx3 sounddevice vosk numpy

下载 Vosk 模型: https://alphacephei.com/vosk/models

获取模型例如this

提取并重命名:

vosk-model-small-en-us-0.15

macOS

python3 -m venv py312
source py312/bin/activate
brew install portaudio
pip install pygame pyttsx3 sounddevice vosk numpy

下载与上面相同的英文模型。

Linux

python3 -m venv py312
source py312/bin/activate

pip install pygame pyttsx3 sounddevice vosk numpy
sudo apt update
sudo apt install -y libportaudio2 libportaudiocpp0 portaudio19-dev

了解语音转文本 (Vosk)

语音转文本 (STT) 是将口语转换为书面文本的过程。 Vosk 是最流行的离线 STT 引擎之一,以轻量、准确且易于在 Python 项目中使用而闻名。

以下是适合教程、文档或学习目的的详细说明。

为什么语音转文本很重要

语音转文本技术在现代软件中已变得至关重要,因为:

免提交互

用户可以使用语音控制应用程序,这对于时钟、助手和任何需要手动操作的场景(烹饪、驾驶等)很有用。

无障碍

STT 可以帮助有运动障碍或无法轻松打字的用户。

实时自动化

语音命令可以立即触发事件 - 例如, “启动计时器”、“停止音乐”、“现在几点了”。

无需屏幕即可工作

适用于 IoT 设备、Raspberry Pi 系统或嵌入式小工具。

离线安全

Vosk 完全离线工作,因此不会将语音数据发送到云端,从而增强了隐私性。

Vosk 的工作原理——理论(简化)

尽管 Vosk 感觉使用起来很简单,但它实际上使用了严格的语音处理理论。这是一个易于理解、适合初学者的解释:

  1. 音频采集
  • 您的麦克风记录原始音频波。
  • 这些波只是代表气压随时间变化的数字。
  1. 特征提取(MFCC)
  • 对于机器学习模型来说,原始音频过于详细且嘈杂。
  • Vosk 将原始音频转换为 MFCC 特征(梅尔倒谱系数)。

MFCC 代表:

  • 频率分布
  • 响度
  • 语气
  • 人类感知为言语的模式

将 MFCC 视为神经网络可以理解的声音指纹。

3.声学模型(神经网络) 该模型采用 MFCC 特征并预测音素 — 最小的声音单位,例如: k a t ( = "cat" ) 声学模型经过数千小时的语音录音训练。

  1. 语言模型 人类不会以随机的音素序列说话。 因此,语言模型有助于预测哪些单词有意义。

例如: 如果声学模型检测到以下内容: d t a m p 语言模型引导它: → "time" 而不是胡言乱语。

  1. 解码器 解码器结合了:
  • 声学模型的预测
  • 来自语言模型的概率 and chooses the most likely final text output. Result: clear, readable text.

为什么开发人员喜欢 Vosk

  • 100% 离线
  • 没有互联网意味着: ✔ 隐私 ✔ 可靠性 ✔ 非常适合物联网或现场环境
  • 低CPU使用率

运行于:

  • 树莓派
  • 旧笔记本电脑
  • 中档电脑
  • 提供小型型号
  • 某些型号<50MB。
  • 快速且实时
  • 即使在普通的硬件上,它也可以立即转录。
  • 多语言支持

蜡模型类型

您可以根据您的设备进行选择:

小型号

  • <40MB
  • 最快
  • 精度较低
  • 非常适合 Raspberry Pi 或简单命令
  • 非常适合这个“语音时钟项目”

中型型号

  • 平衡精度+速度
  • 适用于台式机或笔记本电脑

大型机型

  • 最佳准确度
  • CPU负载较重
  • 对于简单的语音命令来说太过分了

从哪里获取语言模型

所有官方型号在这里: https://alphacephei.com/vosk/models

支持的语言

沃斯克支持:

语言型号
英语vosk-model-small-en-us-0.15
日语vosk-model-small-ja-0.22
中文vosk-model-small-cn-0.22
西班牙语vosk-model-small-es-0.42
法语vosk-model-small-fr-0.22
印地语vosk-model-small-hi-0.22

……还有更多。

初学者应该使用哪种模型?

使用小模型

  • 快速地
  • 低CPU使用率
  • 非常适合树莓派
  • 对于单字命令来说足够准确

小型号名称示例:

vosk-model-small-en-us-0.15 vosk-model-small-es-0.42 vosk-model-small-fr-0.22

了解文本转语音 (pyttsx3)

改变声音

在代码中:

engine = pyttsx3.init()
voices = engine.getProperty('voices')
engine.setProperty('voice', voices[0].id)

改变说话速度

engine.setProperty('rate', 150)

共同价值观:

  • 120(慢速)
  • 150(默认)
  • 180(快速)

代码分解

时钟渲染

时钟是手动绘制的:

  • 外圈
  • 小时数
  • 分钟刻度
  • 根据时间旋转指针

滴答声生成

而不是加载 .wav,我们生成音频:

  • 1500赫兹点击
  • 50 毫秒持续时间
  • 指数褪色

感谢 NumPy,时钟始终滴答作响,无需导入外部文件。

打字动画

问候语看起来就像真实的打字一样:

  • 人物逐渐出现
  • 光标闪烁
  • 4秒后,文字自动清除

监听按钮行为

  • 打开/关闭
  • 蓝色 → 闲置
  • 绿色 → 聆听
  • 在后台运行 Vosk 麦克风流

STT回调逻辑

当 Vosk 解码语音时:

  • 打印检测到的文本
  • 如果包含“时间”,则调用 speak_time()

运行应用程序

一切安装完毕后:

python main.py

步骤:

1.时钟出现

  1. 单击
  2. 说:“时间”
  3. 时钟会说出当前时间 5.底部出现文字动画

故障排除

❗ 未检测到麦克风

尝试:

pip install sounddevice

或者选择输入设备:

sd.default.device = 1

❗ 未检测到语音

使用模型;大的需要更多的CPU。 发音清晰,点击“听”后等待 1-2 秒。

❗ TTS 只能运行一次

确保每个 TTS 调用都会创建一个新引擎(已在提供的代码中完成)。

完整源代码

1.Windows DPI 感知

import ctypes
try:
    ctypes.windll.user32.SetProcessDPIAware()
except:
    pass
  • 确保应用程序在 Windows 中的高 DPI 屏幕上正确显示。
  • 包裹在 try块以与其他操作系统兼容。

2. 进口

import pygame, math, datetime, sys, numpy as np, pyttsx3, threading, time
import sounddevice as sd
from vosk import Model, KaldiRecognizer
import json, os
  • pygame:GUI 和图形。
  • 数学:时钟指针的三角学。
  • 日期时间:时钟和问候语的当前时间。
  • numpy:生成人工滴答声。
  • pyttsx3:文本转语音引擎。
  • 线程:在后台运行 TTS/STT。
  • 声音设备和vosk:语音转文本识别。
  • json & os:解析 Vosk 输出并处理文件。

3.Pygame初始化

pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=2)
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("PyShine Wall Clock")
  • 初始化 Pygame音频混合器 以进行声音播放。
  • 设置屏幕尺寸和窗口标题

4. 常数和颜色

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (150, 150, 150)
DARK_GRAY = (50, 50, 50)
BUTTON_COLOR = (0, 128, 255)
BUTTON_HOVER = (0, 180, 255)
BUTTON_ACTIVE = (0, 200, 0)
LIME = (0, 255, 0)
  • 定义用于钟面、指针、按钮和文本的颜色

5. 时钟参数及字体

center_x, center_y = WIDTH // 2, HEIGHT // 2
clock_radius = 150
font = pygame.font.SysFont('Arial', 24, bold=True)
date_font = pygame.font.SysFont('Arial', 20)
button_font = pygame.font.SysFont('Arial', 20, bold=True)
time_str_font = pygame.font.SysFont('Arial', 28, bold=True)

center_x, center_y:时钟中心。 clock_radius:钟面的尺寸。

  • 数字、日期、按钮文本和 TTS 文本显示的字体。

6. 滴答声

def create_tick_sound():
    ...
    tick_sound = pygame.sndarray.make_sound(sound_array)
    tick_sound.set_volume(0.5)
    return tick_sound
  • 使用 NumPy 生成 短 1500Hz 的点击
  • 无需外部音频文件。
  • 用于播放每秒滴答声

7. 收听按钮

button_rect = pygame.Rect(WIDTH // 2 - 80, 80, 160, 50)
listening_active = False
def draw_button(mouse_pos):
    ...
  • 在屏幕上绘制按钮。 Error 500 (Server Error)!!1500.That’s an error.There was an error. Please try again later.That’s all we know.
  • 控制麦克风监听状态

8. 文本输入和 TTS

def speak_time():
    ...
    threading.Thread(target=tts_func, args=(spoken_time_str,), daemon=True).start()
  • 根据当前时间确定问候语
  • 格式语音文本:例如,"Good afternoon\nIt's 03:25 PM now!"
  • 在后台线程中启动文本转语音
  • 更新打字动画变量。

9.Wax 语音转文本设置

MODEL_PATH = "vosk-model-small-en-us-0.15"
vosk_model = Model(MODEL_PATH)
recognizer = KaldiRecognizer(vosk_model, 16000)
  • 加载离线 Vosk 模型
  • 识别器将音频字节转换为文本
  • 确保离线语音识别

STT 回调

def stt_callback(indata, frames, time_data, status):
    ...
    if "time" in result_text.lower():
        speak_time()
  • 处理来自麦克风的音频。
  • 将其转换为文本。
  • 触发器 speak_time()当检测到关键字“时间”时。

10. 时钟绘图函数

钟面

def draw_clock_face():
    ...
  • 绘制外圈、小时数字、分钟刻度
  • 区分小时刻度(较粗)和分钟刻度(较细)。

钟针

def draw_clock_hands():
    ...
  • 根据当前时间绘制时针、分针、秒针
  • 每秒播放滴答声
  • 绘制中心枢轴圆。

日期显示

def draw_date_display(now):
    ...
  • 显示当前日期星期几

打字动画

def draw_spoken_time():
    ...
  • 像打字一样逐渐显示问候语和时间
  • 光标闪烁
  • 4 秒后自动清除。

11. 主循环

def main():
    ...
  • 处理事件
  • 辞职
  • ESC键
  • 鼠标点击收听按钮
  • 更新:
  • 钟面
  • 日期
  • 输入问候语
  • 收听按钮
  • 30 FPS 运行。
  • 确保流畅的动画和交互

12. 入口点

if __name__ == "__main__":
    main()
  • 直接执行脚本时启动主循环

主要.py

完整的工作源代码在这里:

# Tutorial and Source Code available: www.pyshine.com

import ctypes
try:
    ctypes.windll.user32.SetProcessDPIAware()
except:
    pass
import pygame
import math
import datetime
import sys
import numpy as np
import pyttsx3
import threading
import time

#  VOSK STT IMPORTS 
import sounddevice as sd
from vosk import Model, KaldiRecognizer
import json
import os

# Initialize Pygame
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=2)

# Screen dimensions
WIDTH, HEIGHT = 400, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("PyShine Wall Clock")

# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (150, 150, 150)
DARK_GRAY = (50, 50, 50)
BUTTON_COLOR = (0, 128, 255)
BUTTON_HOVER = (0, 180, 255)
BUTTON_ACTIVE = (0, 200, 0)
LIME = (0, 255, 0)

# Clock parameters
center_x, center_y = WIDTH // 2, HEIGHT // 2
clock_radius = 150

# Fonts
font = pygame.font.SysFont('Arial', 24, bold=True)
date_font = pygame.font.SysFont('Arial', 20)
button_font = pygame.font.SysFont('Arial', 20, bold=True)
time_str_font = pygame.font.SysFont('Arial', 28, bold=True)

# Tick sound
def create_tick_sound():
    sample_rate = 44100
    duration = 0.05
    n_samples = int(sample_rate * duration)
    t = np.linspace(0, duration, n_samples, False)
    envelope = np.exp(-50 * t)
    waveform = 0.5 * envelope * np.sign(np.sin(2 * np.pi * 1500 * t))
    waveform_int16 = np.int16(waveform * 3276)
    sound_array = np.column_stack([waveform_int16, waveform_int16])
    tick_sound = pygame.sndarray.make_sound(sound_array)
    tick_sound.set_volume(0.5)
    return tick_sound

tick = create_tick_sound()
last_second = -1

# Button
button_rect = pygame.Rect(WIDTH // 2 - 80,  80, 160, 50)
listening_active = False  # Button state
printed=False
def draw_button(mouse_pos):
    global printed
    if listening_active:
        color = BUTTON_ACTIVE
        text_str = "LISTENING..."
        if  printed==False:
            print('Start listening...')
            printed=True
    else:
        color = BUTTON_HOVER if button_rect.collidepoint(mouse_pos) else BUTTON_COLOR
        text_str = "LISTEN"
        printed=False
    pygame.draw.rect(screen, color, button_rect, border_radius=10)
    text = button_font.render(text_str, True, WHITE)
    text_rect = text.get_rect(center=button_rect.center)
    screen.blit(text, text_rect)

# Shared variables
spoken_time_str = ""
typed_text = ""
typing_start_time = 0
typing_speed = 8
cursor_visible = True
last_cursor_toggle = 0
text_display_complete_time = None

# Speak time and trigger typing
def speak_time():
    global spoken_time_str, typed_text, typing_start_time, text_display_complete_time
    now = datetime.datetime.now()
    hour, minute = now.hour, now.minute

    # Determine AM/PM
    am_pm = "AM" if hour < 12 else "PM"
    hour_display = hour % 12
    hour_display = 12 if hour_display == 0 else hour_display

    # Determine greeting based on hour
    if 5 <= hour < 12:
        greeting = "Good morning"
    elif 12 <= hour < 17:
        greeting = "Good afternoon"
    elif 17 <= hour < 21:
        greeting = "Good evening"
    else:
        greeting = "Good night"

    # Combine greeting and time as two lines
    spoken_time_str = f"{greeting}\nIt's {hour_display:02d}:{minute:02d} {am_pm} now!"

    typed_text = ""  # Reset typing
    typing_start_time = time.time()
    text_display_complete_time = None

    # Speak TTS
    def tts_func(text):
        tts_engine = pyttsx3.init()
        tts_engine.setProperty('rate', 150)
        tts_engine.say(text.replace("\n", ". "))  # Speak as single sentence
        tts_engine.runAndWait()

    threading.Thread(target=tts_func, args=(spoken_time_str,), daemon=True).start()

#  VOSK STT SETUP
MODEL_PATH = "vosk-model-small-en-us-0.15"
if not os.path.exists(MODEL_PATH):
    print(f"Missing model folder '{MODEL_PATH}'")
    sys.exit(1)

vosk_model = Model(MODEL_PATH)
recognizer = KaldiRecognizer(vosk_model, 16000)
sd_stream = None  # Global reference to microphone stream

def audio_to_bytes(indata):
    try:
        return bytes(indata)
    except:
        return indata.tobytes()

def stt_listen_loop():
    global sd_stream
    try:
        with sd.RawInputStream(
            samplerate=16000,
            blocksize=8000,
            dtype='int16',
            channels=1,
            callback=stt_callback
        ) as stream:
            sd_stream = stream
            while listening_active:
                time.sleep(0.1)
    except Exception as e:
        print("Microphone error:", e)

def stt_callback(indata, frames, time_data, status):
    if status:
        print("Audio status:", status)
    data = audio_to_bytes(indata)
    if recognizer.AcceptWaveform(data):
        result_text = json.loads(recognizer.Result()).get("text", "")
        if result_text.strip():  # Only print non-empty text
            print(f"Detected: {result_text}")
        if "time" in result_text.lower():
            speak_time()

# CLOCK DRAWING FUNCTIONS

def draw_clock_face():
    pygame.draw.circle(screen, WHITE, (center_x, center_y), clock_radius, 2)
    pygame.draw.circle(screen, DARK_GRAY, (center_x, center_y), clock_radius - 5, 2)
    for hour in range(1, 13):
        angle = math.radians(hour * 30 - 90)
        number_x = center_x + (clock_radius - 30) * math.cos(angle) - 10
        number_y = center_y + (clock_radius - 30) * math.sin(angle) - 10
        number_text = font.render(str(hour), True, WHITE)
        screen.blit(number_text, (number_x, number_y))
        tick_start_x = center_x + (clock_radius - 15) * math.cos(angle)
        tick_start_y = center_y + (clock_radius - 15) * math.sin(angle)
        tick_end_x = center_x + (clock_radius - 5) * math.cos(angle)
        tick_end_y = center_y + (clock_radius - 5) * math.sin(angle)
        pygame.draw.line(screen, WHITE, (tick_start_x, tick_start_y), (tick_end_x, tick_end_y), 3)
    for minute in range(60):
        if minute % 5 != 0:
            angle = math.radians(minute * 6 - 90)
            tick_start_x = center_x + (clock_radius - 10) * math.cos(angle)
            tick_start_y = center_y + (clock_radius - 10) * math.sin(angle)
            tick_end_x = center_x + (clock_radius - 5) * math.cos(angle)
            tick_end_y = center_y + (clock_radius - 5) * math.sin(angle)
            pygame.draw.line(screen, GRAY, (tick_start_x, tick_start_y), (tick_end_x, tick_end_y), 1)

def draw_clock_hands():
    global last_second
    now = datetime.datetime.now()
    hour, minute, second = now.hour % 12, now.minute, now.second
    if second != last_second:
        tick.play()
        last_second = second
    hour_angle = math.radians(hour * 30 + minute * 0.5 - 90)
    minute_angle = math.radians(minute * 6 + second * 0.1 - 90)
    second_angle = math.radians(second * 6 - 90)
    hour_x = center_x + clock_radius * 0.5 * math.cos(hour_angle)
    hour_y = center_y + clock_radius * 0.5 * math.sin(hour_angle)
    pygame.draw.line(screen, WHITE, (center_x, center_y), (hour_x, hour_y), 6)
    minute_x = center_x + clock_radius * 0.7 * math.cos(minute_angle)
    minute_y = center_y + clock_radius * 0.7 * math.sin(minute_angle)
    pygame.draw.line(screen, WHITE, (center_x, center_y), (minute_x, minute_y), 4)
    second_x = center_x + clock_radius * 0.8 * math.cos(second_angle)
    second_y = center_y + clock_radius * 0.8 * math.sin(second_angle)
    pygame.draw.line(screen, RED, (center_x, center_y), (second_x, second_y), 2)
    pygame.draw.circle(screen, RED, (center_x, center_y), 8)
    pygame.draw.circle(screen, WHITE, (center_x, center_y), 8, 2)
    return now

def draw_date_display(now):
    date_text = date_font.render(now.strftime("%Y-%m-%d"), True, WHITE)
    day_text = date_font.render(now.strftime("%A").upper(), True, WHITE)
    date_rect = date_text.get_rect(midtop=(center_x, center_y - clock_radius + 70))
    day_rect = day_text.get_rect(midtop=date_rect.midbottom)
    screen.blit(date_text, date_rect)
    screen.blit(day_text, day_rect)

def draw_spoken_time():
    global typed_text, last_cursor_toggle, cursor_visible, text_display_complete_time, spoken_time_str
    if spoken_time_str:
        elapsed = time.time() - typing_start_time
        # Split into lines
        lines = spoken_time_str.split("\n")
        chars_to_show = min(int(elapsed * typing_speed), sum(len(line) for line in lines))
  
        # Determine how many chars to show per line
        display_lines = []
        chars_remaining = chars_to_show
        for line in lines:
            if chars_remaining > len(line):
                display_lines.append(line)
                chars_remaining -= len(line)
            else:
                display_lines.append(line[:chars_remaining])
                break
  
        # Clear after 4 seconds of full display
        if chars_to_show == sum(len(line) for line in lines) and text_display_complete_time is None:
            text_display_complete_time = time.time()
        if text_display_complete_time and (time.time() - text_display_complete_time > 4):
            spoken_time_str = ""
            typed_text = ""
            return

        # Cursor blink timer
        if time.time() - last_cursor_toggle > 0.5:
            cursor_visible = not cursor_visible
            last_cursor_toggle = time.time()

        # Render each line
        y_offset = HEIGHT - 130
        for i, line in enumerate(display_lines):
            text_surface = time_str_font.render(line, True, LIME)
            text_rect = text_surface.get_rect(center=(WIDTH // 2, y_offset + i*35))
            screen.blit(text_surface, text_rect)

        # Draw cursor at end of last line
        if cursor_visible and display_lines:
            last_line = display_lines[-1]
            text_surface = time_str_font.render(last_line, True, LIME)
            text_rect = text_surface.get_rect(center=(WIDTH // 2, y_offset + (len(display_lines)-1)*35))
            cursor_x = text_rect.right + 2
            cursor_y = text_rect.top + 4
            cursor_height = text_rect.height - 2
            pygame.draw.rect(screen, LIME, (cursor_x, cursor_y-4, 3, cursor_height))


# MAIN LOOP
def main():
    global listening_active
    clock = pygame.time.Clock()
    running = True
    stt_thread = None

    while running:
        mouse_pos = pygame.mouse.get_pos()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                running = False
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                if button_rect.collidepoint(event.pos):
                    listening_active = not listening_active
                    if listening_active:
                        # Start background STT listening
                        stt_thread = threading.Thread(target=stt_listen_loop, daemon=True)
                        stt_thread.start()
                    else:
                        # Stop listening
                        print("Stopping listening...")
                        sd_stream = None

        screen.fill(BLACK)
        draw_clock_face()
        now = draw_clock_hands()
        draw_date_display(now)
        draw_spoken_time()
        draw_button(mouse_pos)
        pygame.display.flip()
        clock.tick(30)

    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()

网站: https://www.pyshine.com 作者: PyShine