Python实现屏幕时间监控工具:从计时到AI语音提醒的完整开发指南
1. 项目概述与核心思路
作为一个长期和代码打交道的开发者,我深知长时间盯着屏幕对眼睛和身体的消耗有多大。有时候一坐就是几个小时,回过神来才觉得眼睛干涩、脖子僵硬。市面上有不少屏幕时间管理软件,但要么功能复杂,要么提醒方式生硬,用几次就关掉了。最近看到一个挺有意思的项目,一个13岁的小开发者用Python做了一个“屏幕时间监察官”,它的核心创意在于用带点幽默和“压迫感”的AI语音(比如模拟“愤怒的亚洲妈妈”口吻)和图片来提醒你该休息了,而不是冷冰冰的弹窗。这个点子让我眼前一亮,决定沿着这个思路,结合我自己的开发经验,从头到尾实现并优化一个更健壮、更实用的版本。这不仅仅是一个编程练习,更是打造一个真正能帮到自己和同事的健康小工具。
这个程序的核心目标很明确:在后台安静运行,从你开启电脑或启动程序时开始计时。随着连续使用电脑的时间累积,程序会分阶段、逐步升级提醒的“强度”。初期可能是温和的语音提示,后期则会加入图片、GIF甚至循环播放严厉的语音,直到你决定休息并关闭程序或电脑。技术栈选择Python,因为它拥有极其丰富的库,能让我们轻松实现定时任务、音频播放和网页/图片展示。我们将主要用到 time 模块进行计时, winsound 模块(Windows平台)或跨平台的 playsound 、 pygame 库来播放警告音频,以及 webbrowser 模块来打开本地或网络的图片/动图进行视觉提醒。整个项目的价值在于,它用一种略带趣味性和强干预性的方式,将健康用眼的理念转化为可执行、可感知的日常程序。
2. 项目核心设计与架构解析
2.1 功能逻辑与流程设计
在动手写代码之前,我们必须把程序的工作流程想清楚。一个可靠的屏幕时间监控程序,其核心逻辑应该是一个清晰的状态机。参考原项目的思路并加以完善,我设计了以下工作流程:
- 初始化与启动 :程序启动,初始化计时器(
start_time = time.time()),并加载配置(如总时长阈值、各阶段提醒的时间点、对应的音频/图片文件路径)。 - 后台循环监控 :程序进入一个主循环,在后台持续运行。在循环中,它不断计算自开始计时以来经过的时间(
elapsed_time = time.time() - start_time)。 - 阶段判断与触发提醒 :将经过的时间与预设的多个时间阈值进行比较。例如:
- 30分钟:触发第一阶段提醒,播放一段温和的语音(如“已经看屏幕30分钟啦,起来活动一下脖子吧”)。
- 50分钟:触发第二阶段提醒,播放语气更紧迫的语音,并弹出一张休息提示的图片。
- 60分钟(或用户设定的专注时长):触发最终警告,循环播放严厉的语音提醒,并可能打开一个全屏或无法轻易关闭的警告页面,直到用户主动中断程序。
- 提醒执行 :根据判断结果,调用相应的函数来执行“提醒”动作,如播放
wav音频文件、用默认浏览器打开一张提醒图片的URL或本地文件路径。 - 循环与退出 :主循环以一定的间隔(如每秒)检查一次时间,避免CPU占用过高。退出条件可以是用户手动关闭程序,或者(如果我们设计的话)在触发最终警告后,用户完成了一段休息时间,程序重置计时。
这个流程的关键在于“渐进式”和“非侵入式”到“强干预式”的过渡。一开始的提醒要友好,避免引起反感;后期的提醒则需要足够有力,能打断用户当前的沉浸状态。
2.2 技术选型与工具准备
原项目提到了PyCharm和 winsound ,这是一个很好的起点,但为了程序的健壮性和跨平台潜力,我们需要考虑更多。
-
开发环境与IDE :任何Python环境都可以。PyCharm、VS Code、甚至Jupyter Notebook(用于原型验证)都行。我个人习惯用VS Code,轻量且插件丰富。关键是创建一个独立的虚拟环境(
venv),方便管理依赖。# 在项目目录下创建虚拟环境 python -m venv venv # 激活虚拟环境 (Windows) venv\Scripts\activate # 激活虚拟环境 (macOS/Linux) source venv/bin/activate -
核心库选择 :
- 计时与循环 :Python标准库中的
time模块是唯一选择,time.time()获取时间戳既简单又精准。 - 音频播放 :
winsound:仅适用于Windows,最简单,但功能有限(主要播放.wav)。原项目使用它,对于纯Windows环境是OK的。playsound:一个跨平台的纯Python库,可以播放MP3、WAV等格式,API极其简单(playsound(‘sound.mp3’))。这是实现跨平台兼容性的推荐选择。pygame:功能强大的多媒体库,可以更精细地控制音频(如音量、循环播放),但重量也更大。如果我们需要复杂的音频混合或循环播放警告,pygame是更好的选择。
- 视觉提醒 :
webbrowser:Python标准库,webbrowser.open(‘file:///path/to/image.jpg’)可以用默认图片查看器打开本地图片,webbrowser.open(‘https://…’)可以打开网络图片。它的优点是简单、依赖少。缺点是会弹出浏览器或图片查看器窗口,可能会干扰工作。PIL (Pillow)+tkinter/PyQt:如果想创建自定义的、更美观的提醒窗口(例如一个始终置顶的半透明弹窗),则需要用到图形界面库。这复杂度会高很多,但体验也更好。
- 配置管理 :使用
json或yaml文件来存储用户配置(如提醒时间点、音频文件路径、总时长等),这样不用修改代码就能调整行为。
- 计时与循环 :Python标准库中的
注意 :考虑到项目的初衷是简单、有趣且易于理解和修改,我们将主要采用
time+playsound(跨平台) +webbrowser的方案。如果确定只在Windows上使用,winsound也是一个轻量选项。
- 素材准备(AI语音与图片) :
- AI语音 :原项目作者使用了在线TTS服务。现在这类服务很多,如国内的百度AI开放平台、阿里云、腾讯云都有免费的TTS额度,国外有Google Cloud TTS、Microsoft Azure TTS等。我们可以用这些服务生成不同语气(温和、提醒、警告)的语音文件,保存为
mp3或wav格式。 切记要遵守服务条款,生成的内容需符合公序良俗。 - 图片/GIF :可以自己制作或寻找一些有趣的、表达“休息”、“眼睛疲劳”主题的图片或动图。例如,一个旋转的“休息一下”的标语,或者一个搞笑的、捂着眼睛的表情包。将这些素材放在项目目录的
assets文件夹下。
- AI语音 :原项目作者使用了在线TTS服务。现在这类服务很多,如国内的百度AI开放平台、阿里云、腾讯云都有免费的TTS额度,国外有Google Cloud TTS、Microsoft Azure TTS等。我们可以用这些服务生成不同语气(温和、提醒、警告)的语音文件,保存为
3. 分步实现与核心代码详解
3.1 项目结构与配置定义
首先,我们来搭建一个清晰的项目结构。这能让代码更易维护。
screen_time_inspector/
├── config.json # 配置文件
├── main.py # 主程序入口
├── requirements.txt # 项目依赖
└── assets/ # 资源文件夹
├── audio/
│ ├── reminder_30min.mp3
│ ├── warning_50min.mp3
│ └── final_warning_60min.mp3
└── images/
├── take_a_break.png
└── eye_exercise.gif
config.json 文件内容示例:
{
"total_duration_minutes": 60,
"reminder_stages": [
{
"trigger_minute": 30,
"audio_file": "assets/audio/reminder_30min.mp3",
"image_file": "",
"message": "已经连续使用电脑30分钟,建议远眺20秒。"
},
{
"trigger_minute": 50,
"audio_file": "assets/audio/warning_50min.mp3",
"image_file": "assets/images/take_a_break.png",
"message": "您已使用50分钟!请立刻起身活动,做一下伸展运动。"
},
{
"trigger_minute": 60,
"audio_file": "assets/audio/final_warning_60min.mp3",
"image_file": "assets/images/eye_exercise.gif",
"is_final": true,
"loop_audio": true,
"blocking": true
}
],
"check_interval_seconds": 1
}
这个配置文件定义了总时长、多个提醒阶段(触发时间、对应的音频和图片)、以及程序检查的时间间隔。 is_final , loop_audio , blocking 等字段用于控制最终警告的行为。
3.2 核心计时与状态监控循环
主程序 main.py 的核心是一个循环,它不断检查经过的时间并触发相应事件。
import time
import json
import os
from pathlib import Path
# 我们将使用playsound进行跨平台音频播放
try:
from playsound import playsound
USE_PLAYSOUND = True
except ImportError:
# 如果playsound安装失败,且是Windows,则回退到winsound
import sys
if sys.platform == 'win32':
import winsound
USE_PLAYSOUND = False
else:
print("错误:非Windows系统请安装 'playsound' 库。")
sys.exit(1)
import webbrowser
class ScreenTimeInspector:
def __init__(self, config_path='config.json'):
self.load_config(config_path)
self.start_time = time.time()
self.triggered_stages = set() # 记录已触发过的阶段,避免重复触发
self.is_running = True
self.final_warning_active = False
def load_config(self, config_path):
"""加载配置文件"""
with open(config_path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
# 将分钟转换为秒,便于内部计算
self.total_duration = self.config['total_duration_minutes'] * 60
self.check_interval = self.config['check_interval_seconds']
for stage in self.config['reminder_stages']:
stage['trigger_second'] = stage['trigger_minute'] * 60
def play_audio(self, audio_path):
"""播放音频文件,处理跨平台兼容性"""
if not os.path.exists(audio_path):
print(f"警告:音频文件不存在 {audio_path}")
return
try:
if USE_PLAYSOUND:
playsound(audio_path)
else:
# Windows winsound 只支持 .wav
if audio_path.endswith('.wav'):
winsound.PlaySound(audio_path, winsound.SND_FILENAME)
else:
print(f"警告:winsound不支持播放 {audio_path},请转换为.wav格式或安装playsound。")
except Exception as e:
print(f"播放音频时出错: {e}")
def show_image(self, image_path):
"""使用默认程序打开图片或GIF"""
if not os.path.exists(image_path):
print(f"警告:图片文件不存在 {image_path}")
return
# 使用 file:// 协议打开本地文件
webbrowser.open(f'file://{os.path.abspath(image_path)}')
def execute_reminder_stage(self, stage):
"""执行单个提醒阶段的所有动作"""
print(f"\n[提醒] {stage['message']}")
if stage.get('audio_file'):
self.play_audio(stage['audio_file'])
if stage.get('image_file'):
self.show_image(stage['image_file'])
# 如果是最终警告阶段,启动一个阻塞循环
if stage.get('is_final', False):
self.final_warning_active = True
print("!!! 最终警告已激活 !!! 请立即离开电脑休息。")
if stage.get('loop_audio', False) and stage.get('audio_file'):
# 注意:这里循环播放会阻塞主线程。更优解是使用线程,但为简化,我们先这样处理。
# 在实际中,可以考虑用 pygame.mixer 实现后台循环播放。
while self.final_warning_active:
self.play_audio(stage['audio_file'])
time.sleep(5) # 每5秒重复一次警告语音
def check_and_trigger(self):
"""检查时间并触发相应的提醒阶段"""
current_time = time.time()
elapsed_seconds = current_time - self.start_time
# 检查是否超过总时长,触发最终阶段(如果配置了)
if elapsed_seconds >= self.total_duration:
final_stages = [s for s in self.config['reminder_stages'] if s.get('is_final')]
for final_stage in final_stages:
if final_stage['trigger_second'] not in self.triggered_stages:
self.execute_reminder_stage(final_stage)
self.triggered_stages.add(final_stage['trigger_second'])
return
# 检查每个提醒阶段
for stage in self.config['reminder_stages']:
trigger_sec = stage['trigger_second']
# 如果当前时间超过该阶段触发点,且该阶段未被触发过
if elapsed_seconds >= trigger_sec and trigger_sec not in self.triggered_stages:
self.execute_reminder_stage(stage)
self.triggered_stages.add(trigger_sec)
def run(self):
"""主运行循环"""
print(f"屏幕时间监察官已启动。总时长设定为 {self.config['total_duration_minutes']} 分钟。")
print("程序将在后台运行,按 Ctrl+C 可终止。")
try:
while self.is_running and not self.final_warning_active:
self.check_and_trigger()
# 休眠指定间隔,降低CPU占用
time.sleep(self.check_interval)
except KeyboardInterrupt:
print("\n程序被用户中断。")
finally:
print("屏幕时间监察官已退出。")
if __name__ == "__main__":
inspector = ScreenTimeInspector()
inspector.run()
这段代码构建了一个完整的监控程序骨架。 ScreenTimeInspector 类封装了所有功能:加载配置、计时、播放音频、展示图片以及主循环。使用类的方式使得代码结构清晰,未来也容易扩展(例如添加GUI、网络同步等功能)。
3.3 音频与视觉提醒的实现细节
音频和视觉提醒是用户体验的核心。上面代码中已经实现了基本功能,但还有一些细节可以优化。
-
音频播放的优化 :
- 异步播放 :
playsound和winsound.PlaySound默认都是阻塞的,即播放完毕前程序会卡住。对于较长的提醒音频,这会导致主循环暂停。我们可以使用线程来异步播放。
import threading def play_audio_async(audio_path): def _play(): playsound(audio_path) thread = threading.Thread(target=_play) thread.daemon = True # 设置为守护线程,主程序退出时自动结束 thread.start()- 音量与格式 :
playsound不支持调节音量。如果需要更精细的控制,pygame.mixer是更好的选择,它可以设置音量、循环播放,并且也是跨平台的。
- 异步播放 :
-
视觉提醒的优化 :
- 使用
webbrowser.open的问题 :它会打开系统默认的图片查看器或浏览器。如果用户正在全屏工作,这个新窗口可能会被盖住,起不到提醒作用。 - 更醒目的提醒方案 :
- 使用
tkinter创建简单弹窗 :虽然会引入GUI依赖,但可以创建始终置顶(topmost)的窗口。
import tkinter as tk def show_popup(image_path): root = tk.Tk() root.attributes('-topmost', True) # 置顶 root.overrideredirect(True) # 无边框窗口 # 加载并显示图片 (需要PIL/Pillow) from PIL import Image, ImageTk img = Image.open(image_path) photo = ImageTk.PhotoImage(img) label = tk.Label(root, image=photo) label.pack() # 10秒后自动关闭 root.after(10000, root.destroy) root.mainloop()- 使用系统通知 :对于macOS (
osascript)、Linux (notify-send) 和 Windows (win10toast库),可以发送原生系统通知,更轻量且不干扰当前窗口。
- 使用
- 使用
实操心得 :在初期原型验证时,用
webbrowser是最快的。但如果你想做一个真正“讨人嫌”、无法忽视的提醒,学习一点tkinter来做个置顶小弹窗是非常值得的。或者,结合系统通知和音频,也是一种平衡的方案。
4. 功能增强与个性化定制
基础版本已经能工作,但我们可以让它更智能、更贴心。
4.1 实现可配置化与用户交互
目前的配置是写死在 json 文件里的。我们可以做一个简单的命令行接口(CLI)或配置文件生成器,让用户无需编辑JSON就能设置。
# cli_config.py - 一个简单的命令行配置工具
import json
import sys
def create_config():
print("=== 屏幕时间监察官配置向导 ===")
total_min = int(input("请输入您希望的总专注时长(分钟): "))
stages = []
add_stage = True
while add_stage:
print(f"\n--- 添加第 {len(stages)+1} 个提醒阶段 ---")
trigger = int(input(" 在多少分钟时触发?: "))
audio = input(" 音频文件路径(留空则无): ").strip()
image = input(" 图片/GIF文件路径(留空则无): ").strip()
message = input(" 提醒消息: ")
is_final = input(" 是否为最终警告阶段?(y/N): ").lower() == 'y'
stage = {
"trigger_minute": trigger,
"audio_file": audio if audio else "",
"image_file": image if image else "",
"message": message,
"is_final": is_final
}
if is_final:
stage["loop_audio"] = input(" 是否循环播放警告音频?(y/N): ").lower() == 'y'
stages.append(stage)
add_stage = input("是否继续添加提醒阶段?(y/N): ").lower() == 'y'
config = {
"total_duration_minutes": total_min,
"reminder_stages": stages,
"check_interval_seconds": 1
}
with open('my_config.json', 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
print("配置已保存至 'my_config.json'。")
if __name__ == "__main__":
create_config()
这样,用户运行 python cli_config.py 就可以通过问答方式生成自己的配置文件。
4.2 休息计时与自动重置
一个完整的健康周期应该是“工作 -> 提醒 -> 休息 -> 重置”。我们可以在最终警告触发后,不仅循环提醒,还启动一个“强制休息”计时器。
# 在 ScreenTimeInspector 类中添加
def start_break_timer(self, break_minutes=5):
"""启动休息计时器"""
print(f"\n[系统] 进入强制休息时间,时长 {break_minutes} 分钟。")
break_end = time.time() + break_minutes * 60
while time.time() < break_end:
remaining = int(break_end - time.time())
mins, secs = divmod(remaining, 60)
# 可以在这里显示一个倒计时,或者播放舒缓的音乐
time.sleep(1)
print("[系统] 休息时间结束!计时器将重置。")
self.reset_timer()
def reset_timer(self):
"""重置所有计时和状态"""
self.start_time = time.time()
self.triggered_stages.clear()
self.final_warning_active = False
print("计时器已重置。")
然后在 execute_reminder_stage 中,如果遇到最终阶段,除了循环警告,还可以调用 start_break_timer 。
4.3 系统托盘与后台静默运行
对于需要长时间后台运行的程序,一个系统托盘图标是更好的选择,它让程序存在感更低,也方便用户随时暂停、退出或查看状态。我们可以使用 pystray 库(跨平台)来实现。
# 这是一个高级功能示例,需要安装 pystray 和 PIL
# pip install pystray Pillow
import pystray
from PIL import Image, ImageDraw
import threading
def create_tray_icon(inspector):
"""创建系统托盘图标和菜单"""
# 创建一个简单的图标
image = Image.new('RGB', (64, 64), color='white')
draw = ImageDraw.Draw(image)
draw.ellipse([16, 16, 48, 48], fill='red', outline='black')
draw.text((24, 24), 'ST', fill='white') # ST for Screen Time
# 定义菜单项
menu = (
pystray.MenuItem(f"已运行: {int(time.time()-inspector.start_time)//60}分", lambda: None, enabled=False),
pystray.MenuItem("暂停监控", lambda: toggle_pause(inspector)),
pystray.MenuItem("立即重置", lambda: inspector.reset_timer()),
pystray.MenuItem("退出", lambda: stop_program(inspector, icon)),
)
icon = pystray.Icon("screen_time_inspector", image, "屏幕时间监察官", menu)
return icon
def toggle_pause(inspector):
inspector.is_running = not inspector.is_running
status = "已暂停" if not inspector.is_running else "已恢复"
print(f"[系统] {status}")
def stop_program(inspector, icon):
inspector.is_running = False
icon.stop()
print("程序正在退出...")
# 在主程序中,需要将主循环放在一个线程中运行,以免阻塞系统托盘
if __name__ == "__main__":
inspector = ScreenTimeInspector()
# 启动监控循环线程
monitor_thread = threading.Thread(target=inspector.run)
monitor_thread.daemon = True
monitor_thread.start()
# 启动系统托盘
icon = create_tray_icon(inspector)
icon.run()
这样,程序启动后会在系统托盘区显示一个图标,右键点击可以查看状态、暂停、重置或退出,非常优雅。
5. 常见问题排查与优化建议
在实际开发和运行中,你可能会遇到以下问题。这里我整理了一份排查清单和优化建议。
5.1 音频播放失败
- 问题 :程序运行无错误,但没有声音。
- 排查 :
- 检查文件路径 :确保
config.json中的音频文件路径正确。使用绝对路径或相对于主程序main.py的路径。打印出程序尝试加载的完整路径进行检查。 - 检查文件格式 :如果使用
winsound,它只支持.wav格式。确保你的音频文件是未压缩的PCM WAV格式。playsound支持mp3和wav,但需要系统有对应的解码器。 - 检查音量与设备 :确认系统音量未静音,且输出设备选择正确。
- 检查库安装 :确保
playsound已正确安装 (pip install playsound)。在Windows上,它依赖系统媒体播放器;在macOS/Linux上,它可能依赖gstreamer等。
- 检查文件路径 :确保
- 解决方案 :
- 统一使用
.wav格式以确保最大兼容性。可以使用在线转换工具或pydub库进行转换。 - 尝试使用
pygame.mixer作为备选方案,它通常更可靠。
import pygame pygame.mixer.init() pygame.mixer.music.load(‘audio.wav’) pygame.mixer.music.play() - 统一使用
5.2 图片/GIF无法打开或显示异常
- 问题 :
webbrowser.open没有反应,或打开了错误的程序。 - 排查 :
- 检查文件路径和权限 :同音频文件。
- 检查默认程序 :
webbrowser.open使用系统默认关联程序打开文件。确保图片格式(如.png,.jpg,.gif)有默认的查看器。 - URL格式 :打开本地文件时,路径需要是
file://开头的URL。os.path.abspath可以获取绝对路径。
- 解决方案 :
- 使用
subprocess调用特定命令打开图片,可能更可控(但牺牲跨平台性)。
import subprocess, os, sys if sys.platform == 'darwin': # macOS subprocess.call(['open', image_path]) elif sys.platform == 'win32': # Windows os.startfile(image_path) else: # Linux subprocess.call(['xdg-open', image_path])- 如前所述,考虑使用
tkinter或系统通知来展示提醒,避免依赖外部程序。
- 使用
5.3 程序CPU占用过高或过低
- 问题 :程序导致电脑风扇狂转,或者提醒严重延迟。
- 原因与解决 :
- 主循环休眠间隔 (
check_interval_seconds) 设置得太小(如0.01秒),会导致循环执行过于频繁,消耗CPU。通常设置为0.5到2秒之间是完全足够的,对提醒精度几乎没有影响。 - 阻塞操作 :如果音频播放是阻塞的(且音频很长),主循环会卡住。务必使用 异步播放 (如线程)。
- 最终警告循环 :如果最终警告阶段是
while循环播放音频且没有休眠,会占用大量CPU。在循环内添加time.sleep()。
- 主循环休眠间隔 (
5.4 如何实现开机自启动?
这是一个常见的需求,让程序在用户登录后自动运行。
- Windows :
- 创建程序的快捷方式。
- 按
Win + R,输入shell:startup,打开启动文件夹。 - 将快捷方式拖入此文件夹。
- macOS :
- 系统设置 -> 通用 -> 登录项。
- 点击“+”号,添加你的Python脚本或打包后的应用。
- Linux (GNOME) :
- 在
/etc/xdg/autostart/或用户目录的~/.config/autostart/下创建一个.desktop文件。 - 文件内容示例:
[Desktop Entry] Type=Application Name=Screen Time Inspector Exec=python3 /path/to/your/main.py Hidden=false NoDisplay=false X-GNOME-Autostart-enabled=true
- 在
重要提醒 :实现开机自启动前,请确保你的程序已经过充分测试,不会导致系统启动错误或冲突。最好提供一个清晰的选项让用户选择是否启用自启动。
5.5 提升提醒的“有效性”与“趣味性”
原项目的“愤怒的亚洲妈妈”创意是趣味性的核心。我们可以把这个点子发扬光大:
- 语音内容库 :不要只用一两条语音。建立一个语音库,每次触发时随机选择一条,增加新鲜感。内容可以从温和劝导到幽默吐槽再到“严厉警告”。
- 视觉素材库 :同样,建立一个图片/GIF库,随机展示。可以是可爱的动物、励志名言、搞笑的梗图,或者简单的护眼科普图。
- 互动式休息 :在强制休息期间,弹窗可以显示一些简单的、能在座位上完成的伸展运动动画或指导,鼓励用户真正动起来。
- 数据统计 :记录每天的使用数据和休息次数,每周生成一个简单的报告,让用户看到自己的进步。
这个项目的魅力在于它介于工具和玩具之间。通过Python,我们不仅实现了一个健康辅助工具,更探索了如何用代码与人进行有趣、有效的交互。从简单的计时循环,到音频播放、图形界面、系统集成,每一步都涉及不同的编程知识点,是一个非常好的综合练习。希望你在实现它的过程中,既能保护好自己的眼睛,也能享受到编程的乐趣。
更多推荐
所有评论(0)