用Tkinter实现一个离线定时语音播报应用程序

2023-05-16

最近单位领导与我提起,说要做一个语音播报功能程序,意在定时提醒职工进行抄表工作。在下也是个刚毕业不久的小白,从头开始学习Python,对于这个程序虽说小,但也只是看起来而已,在细节方面还是查阅了很多的资料,毕竟新手上路,踩了不少坑。在此记录一下,以便于以后如果忘记了可以回来看看。

最初的设想呢,是使用Tkinter来做UI,pyttsx来做语音播报,因为没经费,值班室的电脑又是局域网不能连外网,只能调用电脑自带的声卡来播放,而不能考虑百度云等开放语音平台,所以决定用pyttsx库。又想到时间设置的问题,电脑上有别的软件在使用数据库,不允许私自动,在这个方面决定使用json文件做为时间配置文件。

话不多说,直接上代码。

配置文件

以json为配置文件。形式为“第xx次抄表”:“xx:xx” 值为时间,不规定日期,因为每天都要,只固定每天抄表时间。

import json

def getData():
    with open("config.json",encoding='utf-8-sig') as json_file:
        config = json.load(json_file)
    return config

导入json包后,通过load方法读取json文件,返回的config变量是一个dict类型的变量。在此注意,在windows系统下,要以utf-8-sig的解码形式打开文件,在其他系统不知道怎样。反正windows以记事本编辑任何文本文件保存后,都会变成BOM形式(就是其中有三个进制码,让操作系统识别这个用记事本编辑过)的文件,用其他文本编辑器倒不会出现这个问题。这个问题会直接导致你用记事本来修改完配置文件后程序无法识别其中的内容。这是第一个坑!

语音播报

配置文件弄好了,接下来就是试试能否播放文本内容。

import pyttsx3
import datetime


def saudi():
    curr_time = datetime.datetime.now()
    engine = pyttsx3.init()
    msg = '现在是,' + str(curr_time.year) + '年' + str(curr_time.month) + '月' + str(curr_time.day) + '日' + str(
        curr_time.hour) + '点' + str(curr_time.minute) + '分'+str(curr_time.second)+',抄表时间到'

    rate = engine.getProperty('rate')
    engine.setProperty('rate', rate - 50)
    volume = engine.getProperty('volume')
    engine.setProperty('volume', volume + 0.25)
    engine.say(msg)
    
    
    engine.runAndWait()

这个是pyttsx的固定代码形式了。倒是挺顺利的,导入所需的包,init()方法初始化。这里是以获取当前系统时间,提醒抄表的固定播报形式,用getproperty来获取属性,setproperty来设置属性,其中rate为语速,volume为音量,都可在方法内调整大小。最后用say方法存入要说的字符串或文本,用runAndWait()方法来启动语音播报。可以说是最顺利的环节,基本无坑,形式固定。

时间处理

既然上面说了,json文件拿出来的数据是dict类的,形式也要转换成时间格式,即要对json文件中的时间数据做格式化处理。同时要与当前时间做比对,如果时和分对得上,就进行播报。python中好像没有直接将dict转换为datatime的,所以只能自己写了。

import json
import datetime
import dateutil.parser
import decimal
from Data import getData


CONVERTERS = {
    'datetime': dateutil.parser.parse,
    'decimal': decimal.Decimal,
}

class MyJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime.datetime,)):
            return {"val": obj.isoformat(), "_spec_type": "datetime"}
        elif isinstance(obj, (decimal.Decimal,)):
            return {"val": str(obj), "_spec_type": "decimal"}
        else:
            return super().default(obj)

def object_hook(obj):
    _spec_type = obj.get('_spec_type')
    if not _spec_type:
        return obj

    if _spec_type in CONVERTERS:
        return CONVERTERS[_spec_type](obj['val'])
    else:
        raise Exception('Unknown {}'.format(_spec_type))

def getTime(i):

    data = getData()

    thing = json.dumps(data, cls=MyJSONEncoder)

    a = json.loads(thing, object_hook=object_hook)

    y = a[i]

    time = datetime.datetime.strptime(y, "%H:%M")
    
    return time.hour,time.minute

这个部分无非就是将json数组转为dict类型,我怎么越写越觉得我写得太麻烦了这个部分,反正不管了,懒得细分析了。最后返回的是datatime形式的一个时和分,用作对比。传入的i就是每个json的key。

线程比对

这部分就是用一个线程,定时获取当前系统时间,同时遍历json数组,进行比对,如果找到时间,就进行播报。

import threading

import datetime

import pythoncom

from gtt import getTime
from Data import getData
from audio import saudi



def fun():
    pythoncom.CoInitialize()
    a = rec()
    if a == 1:
        saudi()
    else:
        print(a)
    pythoncom.CoUninitialize()

    timer = threading.Timer(60, fun)
    timer.start()


def rec():
    for i in getData():
        h, m = getTime(i)
        curr_time = datetime.datetime.now()+datetime.timedelta(seconds=-7)
        if curr_time.hour == h and curr_time.minute == m and i in getData():
            #print("时间到" + str(datetime.datetime.now()))  用做测试,看是否显示正常
            return 1
        #print(i+str(curr_time)) 
    #print("------------------------"+"\n")

python线程的固定写法,用Timer方法来定义时间并开始线程,在此定一分钟检测一次。其实在我写好后播报时间设置间隔短的话(如一分钟),是非常正常的,但是间隔一长,有时候会报COM错误,有时候不会报,这让我百思不得其解。最后查阅资料后了解到如需创建单线程,需要用到pythoncom的CoInitialize方法来创建一个单线程。之后就稳定了,同时要记得以CoUninitialize方法结束。第二个坑!

程序UI

所有的小部件都做好了,剩下就是一一对应做出UI来。具体思路是第一个界面,是一个启动按钮,按钮只可按一次,点击后显示json文件中的配置项,其中数据不可编辑,只可复制。另设更新按钮,以便更改数据时不需要退出,只需要更新就能显示新配置。

import os
import tkinter as tk
import tkinter.font as tf
from Data import getData
from Th import fun

#允许Ctrl+C复制
def ctrlEvent(event):
    if (12 == event.state and event.keysym == 'c'):
        return
    else:
        return "break"

window = tk.Tk()

window.title('定时抄表服务')

# window.geometry('500x300')

window.resizable(False, False) #Text不允许编辑

ft = tf.Font(size=20) #设置字体大小

#启动按钮的函数
def begin(x):
    a = 1
    b = len(getData())
    msg = getData()

    for i in getData():

        # j =str(i)
        # print(msg[j])
        # t=tk.Text(window,width=10,height=1,font=ft)
        d = tk.Text(window, width=5, height=1, font=ft)
        #print(i)

        d.bind("<Key>", lambda a: ctrlEvent)
        d.insert("insert", msg[i])
        d["state"] = 'disabled'
        l = tk.Label(window, text=i,width=10, height=1)
        # t.grid(row=i,column=1)
        if a <= b:
            l.grid(row=1, column=a, padx=8, pady=4)
            d.grid(row=2, column=a, padx=8, pady=4)
            a += 1
    if x==1:
        d["state"] = 'normal'
        d.delete(0.0, tk.END)
        for j in getData():

            d.insert("insert", msg[i])
            d.update()
            d["state"] = 'disabled'
    else:
        bt2 = tk.Button(window, text='更新', width=5, height=1, command=new)
        bt2.grid(row=4, column=int(len(getData())/2)+2, padx=20, pady=20)
        fun()

def new():
    begin(1)

def out():
    oo = os.getpid()
    import subprocess
    subprocess.Popen("cmd.exe /k taskkill /F /T /PID %i" % oo, shell=True)

    # print(os.getpid())

def go():
    begin(0)

def mixed_action(btn):
    go()
    btn["state"] = 'disabled'

bt = tk.Button(window, text='启动', width=8, height=1)
bt.config(command = lambda: mixed_action(bt))
bt.grid(row=4, column=int(len(getData())/2)-2,padx=20, pady=20)

window.protocol("WM_DELETE_WINDOW", out)

window.mainloop()

这个实现了,按下启动按钮,加载数据,同时往begin函数传递一个参数,begin函数中,若传入非1的数,就加载一个更新按钮,同时"启动"按钮变为不可使用。之后的更新按钮会向begin函数传递1,先令Text变为可编辑,之后一个个插入新的时间配置。注意到我最后使用了protocol方法,对窗口的退出按钮绑定了out函数。仔细看out函数会发现,这个函数是用来查询到主程序的PID,以命令行的形式来结束该PID对应的程序。这么做的原因就是Python天生的缺陷所在…无法结束线程!!就算你的主程序退出了,线程依然在运行。只能找到PID来结束程序,这是第三个坑…大坑!

程序运行截图

程序打包

原以为终于踏过无数个坑,可以好好的打包成exe完成任务啦~结果没想到!!最大的坑是在这T_T。

打包是用pyinstaller。要以-D打包。打包成功后,打开就一直闪退,查看错误记录(pyinstaller打包结束会有个warn-XX.txt),发现一堆乱七八糟的包没拿过来。我本身挺懒的,所以找了一天有没有什么打包方式能解决,不想一个个拿包进去,真的有十几个包,累。然而没有。。。只能乖乖一个个在lib里查然后放进去了。放完后终于正常运行

唉…太累了,小白自学就是这样,不过我喜欢python的地方是确实有很多包用,不用那么繁琐的写程序。记录一下吧,继续加油。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

用Tkinter实现一个离线定时语音播报应用程序 的相关文章

随机推荐

  • RocketMQ——NameServer和Broker

    RocketMQ NameServer和Broker 文章目录 RocketMQ NameServer和BrokerNameServerNameServer功能为什么不用zookeeper xff1f BrokerBroker消息存储Bro
  • CSS sprites

    CSS sprites是什么 xff1f CSS Sprites是一种网页图片应用处理方式 xff0c 就是把网页中一些背景图片整合到一张图片文件中 xff0c 再利用CSS的 background image xff0c backgrou
  • SpringBoot集成WebSocket以及可能遇到的部分问题的解决方式

    1 集成 1 1 首先导入pom依赖 核心是 64 ServerEndpoint这个注解 这个注解是Javaee标准里的注解 xff0c tomcat7以上已经对其进行了实现 xff0c 如果是用传统方法使用tomcat发布项目 xff0c
  • centos7重启network报错:network.service: control process exited, code=exited status=1

    centos7虚拟机中修改固定ip地址后 xff0c 重启 xff08 命令为 xff1a systemctl restart network xff09 network服务时异常 xff1b 使用systemctl status netw
  • 用Future与CountDownLatch实现多线程执行多个异步任务,任务全部完成后返回结果

    span class token keyword import span span class token namespace java span class token punctuation span util span class t
  • gradle (v7.5) 使用

    gradle v7 5 使用 gradle 和 maven 都是项目构建工具 xff0c Gradle和Maven两种构建方式存在一些根本差异 Gradle基于任务依赖关系图 其中任务就是工作 xff0c 而Maven是基于固定的过程和线性
  • 麒麟/linux下安装MySQL,修改root用户密码报错汇总(附安装方法)

    本来用apt get一行代码装好的事情 xff0c 但是出现了大大小小的麻烦 xff0c 所以记录一下 xff0c 供有需要的朋友查看 一 Mysql的安装 apt span class token operator span get in
  • 一篇文章快速搞懂 AOP和SpringAOP

    往期相关文档 最全SpringAop切面 10分钟入门SpringAOP 读不在三更五鼓 xff0c 功只怕一曝十寒 郭沫若 文章目录 一 导言二 AOP2 1 代理模式2 2 AOP核心概念2 2 1 Aspect 切面2 2 1 1 P
  • 最通俗易懂的HashMap深度解析

    文章目录 导言Hash表什么是Hash表为什么要Hash表Hash表核心原理核心概念Hash表hash函数 常见冲突解决方法开放地址法 再散列法 再哈希法链地址法 xff08 拉链法 xff09 java HashMap原理浅析java H
  • SFTP连接失败问题解决小tips

    前几天安装了jumpserver之后 xff0c sftp服务莫名奇妙的挂了 xff0c 也不知道是不是这方面的原因 vsftpd服务检查没有问题 防火墙端口配置检查没有问题 端口监听检查没有问题 我们知道SFTP走的是SSH的端口 xff
  • Error:java: 服务配置文件不正确, 或构造处理程序对象

    在学习注解处理器使用的时候 xff0c 依据这个学习 xff0c 完成后build xff0c 报了个错误 Error java 服务配置文件不正确 或构造处理程序对象javax annotation processing Processo
  • 无自动化测试系统设计方法论

    灵活 敏捷 迭代 自动化测试 辩思 测试必不可少 想想看没有充分测试的代码 哪一次是一次过的 哪一次不需要经历下测试的鞭挞 不要以为软件代码容易改 就对于质量不切实际的自信 那是自大 不适用自动化测试的case 遗留系统 太多的依赖方 不想
  • 模仿 java Optional 设计 c# Optional

    模仿 java Optional 设计 c Optional Unity 环境下 using System span class token punctuation span using JetBrains span class token
  • 关于中台的思考和复盘

    数据中台可以做 xff0c 业务中台不能做 能力共享和聚合的入口可以做 强嵌入的业务中台不能做 中台 中台不是只能是微服务 xff0c 中台还可以是代码复用框架 xff0c 允许业务自己扩展 迭代code as service xff0c
  • 三范式分解算法

    三范式是BC范式的放宽 三范式条件 满足一个即可 gt 是平凡的函数依赖 除了子集和父集的函数依赖 大多的函数依赖都是非平凡的 是关系模式R的一个超码 属性集里的所有属性都被包含在 R的candidate key里 注意 的属性集里的所有元
  • 关系数据库设计 函数依赖 逻辑蕴含

    函数依赖 属性集 决定属性集 则称有函数依赖 to 逻辑蕴含 F能推出 原不直观存在于 函数依赖集F 中的函数依赖
  • 斯密特正交化(matlab)

    斯密特正交化 matlab 数学过程 伪代码如下 function b 61 Gram Schmidt Orthogonalization a row col 61 size a b 1 61 a 1 for i in 2 col for
  • autohotkey[启动][发送键击][click][常用窗口命令]

    启动程序或文档 run命令 run exe file in environment path Run Notepad 不在环境变量中的程序或文档 Run A ProgramFiles Winamp Winamp exe open file
  • 通过键盘移动鼠标光标 autohotkey

    通过键盘移动鼠标光标 MouseMove键 参数定义 MouseMove X Y Speed R 鼠标移动的目标位置的 x y 坐标 可以为 表达式 坐标相对于活动窗口Speed 移动鼠标的速度 xff0c 介于 0 xff08 最快 xf
  • 用Tkinter实现一个离线定时语音播报应用程序

    最近单位领导与我提起 xff0c 说要做一个语音播报功能程序 xff0c 意在定时提醒职工进行抄表工作 在下也是个刚毕业不久的小白 xff0c 从头开始学习Python 对于这个程序虽说小 xff0c 但也只是看起来而已 xff0c 在细节