ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python构建列车到站自动播报系统:配置驱动与语音合成实践

用Python构建列车到站自动播报系统:配置驱动与语音合成实践 在实际项目里列车到站播报并不是一个复杂功能但它非常依赖时间精度、文案规则和语音合成三者的配合。以“列车即将到站下一站——至冬”这句典型的到站提示为例我们可以把它扩展成一个可运行的自动播报系统读取时刻表配置、在正确的时间点生成文案、通过语音合成播放并在日志中记录每一次播报。本文会从零搭建这个系统覆盖环境准备、目录设计、核心代码、运行验证、常见问题排查和生产化思路。适合有一定 Python 基础、想练习配置文件驱动逻辑、定时任务和语音合成的读者。整个项目会保持最小可用一个 JSON 文件描述车次和站点一组 Python 模块负责加载配置、生成文案、合成语音、调度触发。跑通之后再替换成真实线路和真实站点名即可。1. 先理解列车到站播报系统要处理哪些核心问题1.1 到站播报本质上是“时间到文案”的映射列车、地铁、公交的到站播报听起来只是几句话但拆开看要回答四个问题什么时候播到达前提前多少秒发车后是否补播。播什么内容到站提示、发车提示、终点站提示文案规则不同。怎么播出来扬声器、语音合成、LED 屏幕文字。播了有没有记录哪一趟车、哪个站、哪类事件、有没有失败。“列车即将到站下一站——至冬”这句文案属于“到达前提示”。它不能等到列车真正停稳才播否则乘客来不及准备。所以系统里必须有一个“提前量”参数比如到达前 30 秒触发。1.2 技术选型为什么用 Python JSON pyttsx3这里选用 Python 是因为它上手快标准库已经覆盖文件读取、时间处理、线程调度等核心需求不需要引入重量级框架。时刻表用 JSON 文件保存优点是结构清晰、容易修改不用改代码就能调整车次和站点。语音合成用 pyttsx3这是一个跨平台的离线文本转语音库支持 Windows、macOS、Linux在本地环境就能播放声音。调度逻辑用轮询方式实现程序每秒检查一次当前时间判断是否有事件需要触发。相比定时器轮询在处理多个事件时更直观也方便补上“启动后只播未来事件”的过滤逻辑。1.3 系统运行流程程序启动后按以下顺序工作读取 config/timetable.json校验必填字段。根据站点到达时间和 announceBefore 提前量生成全部播报事件。记录当前时间作为启动阈值只保留晚于当前时间的事件。每秒检查一次当前时间匹配到事件就生成文案并调用语音播报。播报结果写入控制台和日志文件。这里的关键设计是“事件表”。程序不直接判断“现在该不该播”而是先把全天所有播报点计算出来再逐个比对。这样逻辑更清晰也方便测试。模块职责本文实现方式配置加载读取并校验时刻表JSON 文件 字段校验文案生成根据事件类型生成播报文本MessageBuilder 类语音合成把文本转成声音pyttsx3调度器决定什么时间触发什么事件每秒轮询事件表2. 环境准备与项目骨架设计2.1 环境要求与依赖安装建议使用 Python 3.10 或更高版本。项目只依赖一个第三方库 pyttsx3先创建虚拟环境再安装依赖。mkdir train-announcer cd train-announcer python -m venv venvWindows 激活虚拟环境venv\Scripts\activatemacOS / Linux 激活虚拟环境source venv/bin/activate安装依赖pip install pyttsx3 pip freeze requirements.txt不同操作系统的语音引擎支持情况不完全一样建议先确认当前环境能正常发声再继续操作系统语音引擎说明WindowsSAPI5通常开机即用需要系统带有中文语音包macOSNSSpeechSynthesizer系统自带语音合成能力Linuxespeak / espeak-ng可能需要额外安装系统语音引擎在 Linux 上如果发现 pyttsx3 初始化失败可以先安装 espeak-ng。不同发行版安装命令不同这里只说明排查方向实际命令按当前系统文档执行。2.2 项目目录结构项目按功能拆分模块而不是把所有代码塞进一个文件。目录结构如下train-announcer/ ├── main.py ├── requirements.txt ├── config/ │ └── timetable.json ├── announcer/ │ ├── __init__.py │ ├── config_loader.py │ ├── message_builder.py │ ├── voice.py │ └── scheduler.py └── logs/ └── announcer.logmain.py 是入口负责装配各个模块。announcer 目录放业务逻辑后续要加新功能比如 LED 大屏显示、Web 控制面板都可以在独立模块里扩展。config 目录放配置文件和代码分离修改线路时不需要碰 Python 代码。logs 目录保存运行日志程序首次启动时会自动创建。2.3 准备时刻表配置文件config/timetable.json 是整条线路的数据源。下面这个配置包含四站最后一站就是“至冬”用来演示终点站提示。{ trainNo: T2025, announceBefore: 30, voiceRate: 180, stations: [ { name: 中央广场, arrive: 08:00, depart: 08:02 }, { name: 湖畔公园, arrive: 08:20, depart: 08:22 }, { name: 云岭站, arrive: 08:40, depart: 08:42 }, { name: 至冬, arrive: 09:10 } ] }配置字段说明字段必填类型说明trainNo是string车次编号记录日志时使用announceBefore否int到达前提前多少秒播报到站提示默认 30voiceRate否int语音播放速度默认 180stations是array站点列表按列车运行顺序排列stations[].name是string站点名称stations[].arrive是string到达时间格式 HH:MMstations[].depart否string发车时间格式 HH:MM终点站可以不写这里要注意首站的 arrive 在真实场景里通常表示“开始上客”的时间而不是列车到站时间。为了演示方便这个项目对首站不生成到达提示只生成发车提示。实际项目里可以给每个站点增加一个 announceArrive 布尔字段由配置决定是否播报。3. 用 Python 实现核心模块3.1 配置加载和数据校验配置校验的目的是“尽早失败”。如果配置写错应该在启动时直接报错而不是跑到一半才发现某个站点没有到达时间。import json from pathlib import Path def load_config(path: str) - dict: config_path Path(path) if not config_path.exists(): raise FileNotFoundError(f配置文件不存在: {config_path.resolve()}) with config_path.open(r, encodingutf-8) as f: data json.load(f) check_config(data) return data def check_config(data: dict) - None: required [trainNo, stations] for field in required: if field not in data: raise ValueError(f配置缺少必填字段: {field}) stations data[stations] if not isinstance(stations, list) or len(stations) 2: raise ValueError(stations 必须是至少包含两个站点的数组) for index, station in enumerate(stations): if name not in station or arrive not in station: raise ValueError(f第 {index 1} 个站点缺少 name 或 arrive 字段) if depart in station and station[depart] : raise ValueError(f第 {index 1} 个站点的 depart 不能为空字符串)读取文件时指定 encodingutf-8避免在 Windows 上遇到中文编码问题。校验时逐个检查站点字段保证后面使用的时候不会突然出现 None。3.2 播报文案生成器文案生成单独做成一个类方便以后把模板抽到配置文件里。这个类不负责播放声音只负责返回文本这样单元测试时可以只测文案不依赖语音环境。class MessageBuilder: def __init__(self, config: dict): self.config config self.stations config[stations] def arrival_message(self, index: int) - str: station self.stations[index] return f列车即将到站下一站——{station[name]}请下车的旅客提前做好准备。 def departure_message(self, index: int) - str: station self.stations[index] is_last index len(self.stations) - 1 if is_last: return f终点站{station[name]}到了感谢您乘坐本次列车。 next_name self.stations[index 1][name] return f列车已从{station[name]}站开出下一站是{next_name}。到达提示使用的就是本文开头的句式“列车即将到站下一站——至冬”。发车提示会带上下一站名称方便乘客知道方向。3.3 语音播报模块语音模块封装 pyttsx3。初始化时尽量选择中文语音如果系统里没有中文语音包就降级为默认语音同时打印提示。import pyttsx3 class VoiceAnnouncer: def __init__(self, rate: int 180, volume: float 1.0): self.engine pyttsx3.init() self.engine.setProperty(rate, rate) self.engine.setProperty(volume, volume) self._set_chinese_voice() def _set_chinese_voice(self): voices self.engine.getProperty(voices) for voice in voices: name (voice.name or ).lower() voice_id (voice.id or ).lower() if chinese in name or zh in name or chinese in voice_id or zh in voice_id: self.engine.setProperty(voice, voice.id) return print(未找到中文语音包将使用默认语音) def speak(self, text: str) - None: self.engine.say(text) self.engine.runAndWait()rate 控制语速180 是比较适中的中文播报速度。runAndWait 是阻塞调用会一直等到这句话播完才返回。在这个项目中播报事件之间通常间隔比较长阻塞不会造成明显问题但如果是高频播报场景就要把语音播放放到独立线程并用队列串行执行。3.4 调度器决定什么时候该播什么调度器是整个系统的核心。它先把配置转成事件表事件表里保存的是“绝对秒数”再通过每秒轮询判断是否触发。from datetime import datetime import time import threading class AnnounceScheduler: def __init__(self, config: dict, message_builder, voice, loggerNone): self.config config self.message_builder message_builder self.voice voice self.logger logger self.stations config[stations] self.advance_seconds int(config.get(announceBefore, 30)) self._stop_event threading.Event() staticmethod def _to_seconds(value: str) - int: hour, minute value.strip().split(:) return int(hour) * 3600 int(minute) * 60 def _now_seconds(self) - int: now datetime.now() return now.hour * 3600 now.minute * 60 now.second def _build_events(self): events [] for index, station in enumerate(self.stations): if index 0: arrive_second self._to_seconds(station[arrive]) - self.advance_seconds events.append((arrive_second, arrive, index)) if station.get(depart): depart_second self._to_seconds(station[depart]) events.append((depart_second, depart, index)) events.sort(keylambda item: item[0]) return events def _dispatch(self, event): _, event_type, index event if event_type arrive: message self.message_builder.arrival_message(index) else: message self.message_builder.departure_message(index) line f播报 {self.stations[index][name]} {event_type}: {message} if self.logger: self.logger.info(line) else: print(line) try: self.voice.speak(message) except Exception as exc: if self.logger: self.logger.error(f语音播报失败已降级为文字播报: {exc}) else: print(f语音播报失败已降级为文字播报: {exc}) def run(self): events self._build_events() cutoff self._now_seconds() pending_events [e for e in events if e[0] cutoff] fired set() if self.logger: self.logger.info(播报调度已启动) else: print(播报调度已启动) while not self._stop_event.is_set(): now_sec self._now_seconds() for event in pending_events: key (event[0], event[1], event[2]) if key not in fired and now_sec event[0]: self._dispatch(event) fired.add(key) time.sleep(1) def stop(self): self._stop_event.set()要重点解释两个设计首站不生成到达事件因为始发站没有“即将到站”的概念。cutoff 过滤是必要的。如果程序在 09:00 启动而 08:40 的事件还没播不对这些过去事件做过滤程序会在一秒内把所有历史事件全部补播一遍这显然不是期望行为。事件表使用“当天秒数”而不是字符串比较避免 “08:40” 和 “09:10” 这种字符串比较带来的误解。缺点是跨午夜运行的线路会出现事件倒序问题生产环境需要改用完整日期时间这里作为演示先不处理。4. 串联主程序并验证效果4.1 主程序入口main.py 负责配置日志、加载配置、装配模块然后启动调度器。import logging from pathlib import Path from announcer.config_loader import load_config from announcer.message_builder import MessageBuilder from announcer.voice import VoiceAnnouncer from announcer.scheduler import AnnounceScheduler def setup_logger() - logging.Logger: log_dir Path(logs) log_dir.mkdir(exist_okTrue) logger logging.getLogger(train-announcer) logger.setLevel(logging.INFO) formatter logging.Formatter(%(asctime)s [%(levelname)s] %(message)s) file_handler logging.FileHandler(log_dir / announcer.log, encodingutf-8) file_handler.setFormatter(formatter) console_handler logging.StreamHandler() console_handler.setFormatter(formatter) logger.addHandler(file_handler) logger.addHandler(console_handler) return logger def main(): logger setup_logger() config load_config(config/timetable.json) message_builder MessageBuilder(config) voice VoiceAnnouncer(rateconfig.get(voiceRate, 180)) scheduler AnnounceScheduler(config, message_builder, voice, logger) try: scheduler.run() except KeyboardInterrupt: scheduler.stop() logger.info(播报调度已停止) if __name__ __main__: main()日志同时写到控制台和 logs/announcer.log。文件日志使用 UTF-8 编码避免中文乱码。KeyboardInterrupt 捕获后先停止调度器方便以后在 stop 方法里做资源清理。4.2 运行结果与预期输出在项目根目录执行python main.py如果当前时间接近配置中的播报时间会看到类似输出2025-01-01 08:19:30 [INFO] 播报调度已启动 2025-01-01 08:19:30 [INFO] 播报 湖畔公园 arrive: 列车即将到站下一站——湖畔公园请下车的旅客提前做好准备。 2025-01-01 08:22:00 [INFO] 播报 湖畔公园 depart: 列车已从湖畔公园站开出下一站是云岭站。 2025-01-01 09:09:30 [INFO] 播报 至冬 arrive: 列车即将到站下一站——至冬请下车的旅客提前做好准备。如果不方便等到播报时间可以用下面这条命令单独验证文案生成逻辑python -c from announcer.config_loader import load_config; from announcer.message_builder import MessageBuilder; c load_config(config/timetable.json); print(MessageBuilder(c).arrival_message(3))预期输出列车即将到站下一站——至冬请下车的旅客提前做好准备。4.3 验证清单完成运行后按这个清单逐项确认配置文件能被正常加载没有抛 FileNotFoundError 或 ValueError。控制台在预定的“到达时间减 30 秒”打印了到站提示。语音模块播放了对应中文内容。logs/announcer.log 中能查到每次播报的时间、站点、事件类型。按 CtrlC 后程序能正常退出日志中有“播报调度已停止”记录。如果程序启动后没有任何输出先检查系统当前时间是否接近配置里的播报时间。项目设计为“到点才播”启动后不播是正常现象。5. 常见问题排查5.1 常见问题排查表问题现象可能原因检查方式处理建议语音没有声音系统音量过低、pyttsx3 初始化失败、语音引擎缺失播放系统声音测试观察 pyttsx3.init() 是否报错先检查声卡和音量Linux 环境确认 espeak-ng 已安装中文被读成英文或乱码系统没有中文语音包选中了默认英文语音打印 voices 列表查找 zh 或 chinese 关键字安装中文语音包在 VoiceAnnouncer 中强制指定中文 voice id播报时间不准确系统时间未同步、sleep 累积误差、语音阻塞主循环对比系统时间和标准时间查看日志时间戳开启系统时间自动同步语音播放放到独立线程启动后把已过时间全部播了一遍没有过滤启动前的事件查看启动日志时间和事件时间使用 cutoff 过滤只保留晚于启动时间的事件JSON 解析报错文件编码不是 UTF-8、多写了逗号、路径不对用 python -m json.tool 校验文件保存为 UTF-8 无 BOM删除多余逗号确认执行目录控制台中文乱码Windows 控制台编码问题查看日志文件是否正常日志文件没问题时可设置 PYTHONIOENCODINGutf-85.2 按链路排查的推荐顺序遇到问题时不要直接怀疑代码。推荐的排查顺序是先确认配置文件能加载运行 load_config 看是否报错。再确认时间计算把事件表打印出来检查每个事件的秒数是否符合预期。再确认文案用 MessageBuilder 单独生成文本检查拼接是否正常。再确认语音手动调用 voice.speak 测试一句话确认中文语音可用。最后看日志日志里没有记录说明事件根本没触发日志有记录但没有声音说明问题出在语音模块。如果事件触发了但文字和语音都正常只是时间不对优先检查系统时间和 announceBefore 的数值。6. 从学习项目到生产环境的距离6.1 生产环境必须补的工程能力这个项目跑通的是核心链路距离生产环境还有一段距离。如果要在真实车站或企业内部场景使用至少需要补齐以下能力配置外置和热更新。当前配置在启动时加载修改时刻表必须重启进程。生产环境建议把配置放到独立配置中心或远程文件支持监听变更后重新生成事件表。日志结构化。当前日志适合人读不适合检索。生产环境建议输出 JSON 日志记录车次、站点、事件类型、触发时间、播报时长、结果状态。异常隔离。单条播报失败不能中断整个调度循环。当前代码已经用 try except 包裹语音播放但还要考虑日志落盘失败、配置文件被占用等情况。重复防止。如果部署多个实例同一个事件可能被播报多次。生产环境需要引入分布式锁或“事件已发生”的状态存储。时间源统一。调度系统依赖系统时钟必须开启 NTP 时间同步否则时间漂移会导致播报提前或延迟。语音降级。没有语音设备或语音合成失败时要自动降级为文字播报、LED 屏显示或其他提示方式。6.2 可扩展方向这个项目的模块边界清晰扩展点很多增加 Web 控制面板用 Flask 展示事件表和播报记录。通过 WebSocket 或 MQTT 把播报内容推送到远程设备。接入真实时刻表 API自动生成事件表。增加倒计时提示比如“距离下一站还有 2 分钟”。支持多语言模板把文案模板抽成单独配置文件。增加单元测试重点覆盖 MessageBuilder 和事件表计算逻辑。这些方向里最容易上手的是把播报模板抽成 JSON 文件因为不需要改调度核心只改文案生成模块。7. 最佳实践与练习建议7.1 可以直接使用的检查清单Python 3.10 或更高版本已安装虚拟环境已激活。pip install pyttsx3 成功导入不报错。config/timetable.json 存在使用 UTF-8 编码。站点数量不少于 2每个站点都有非空 name 和 arrive。announceBefore 设置合理按秒配置建议 15 到 60 秒之间。系统时间已开启自动同步。中文语音包可用测试过 voice.speak 能朗读中文。日志目录可写logs/announcer.log 正常生成。语音播报失败时会降级为文字输出不会导致程序崩溃。7.2 练习建议如果想把“列车到站播报系统”从演示项目练成自己的完整作品可以按顺序做这几个练习把文案模板改成从模板文件读取比如用 str.format 或 string.Template观察配置驱动的好处。给 MessageBuilder 写单元测试用 pytest 验证到达提示、发车提示、终点站提示三条分支。把时间匹配从“秒级轮询”改成“下一次事件触发前休眠”减少无意义的每秒轮询。增加 dry-run 模式只打印文案不播放语音方便在无语音环境下调试。把调度器改造成可被打断的服务注册系统信号处理平滑停止。这个项目的价值不在于代码量而在于它把配置、时间、文案、语音、日志这几个环节完整串起来了。把这个链路理解清楚以后做消息推送、定时任务、告警通知甚至是自动化运维脚本都会更容易上手。
返回列表