1. 项目概述:当电话亭开口说话
“Talk to me: Phone booth”,这个项目听起来就充满了复古与未来交织的浪漫感。它不是一个简单的旧物改造,而是一次关于公共空间、人际连接与声音艺术的深度探索。在智能手机几乎成为人体器官延伸的今天,我们习惯了指尖的滑动和屏幕的闪烁,却逐渐遗忘了“声音”作为最原始、最直接交流媒介的魔力。这个项目的核心,就是将一个几乎被时代遗忘的红色电话亭,重新激活为一个可以“对话”的公共艺术装置。
想象一下,你路过街角,看到一个熟悉的红色电话亭。走进去,拿起听筒,里面传来的可能不是拨号音,而是一段陌生人的留言、一首即兴的诗、一个来自过去的故事,或者,它甚至会向你提出一个问题,等待你的回答。这个电话亭不再是一个功能单一的通讯工具,它变成了一个城市的声音信箱,一个情感的树洞,一个连接陌生人之间微妙共鸣的节点。它解决的不仅仅是怀旧,更是现代都市生活中普遍存在的“附近的消失”与“深度交流的匮乏”。它适合所有对城市文化、公共艺术、交互设计以及人类情感连接感兴趣的创作者、社区活动家,或者仅仅是一个好奇的路人。
2. 核心设计思路:从硬件到体验的全链路拆解
要把一个废弃的电话亭变成能“对话”的智能体,不能只靠情怀,更需要一套清晰、可行的技术实现路径。整个项目的设计思路可以拆解为三个核心层次:感知层、处理层与体验层。
2.1 感知层:让电话亭“听见”与“感知”
这是项目与用户发生物理交互的第一触点。目标是精准、可靠地捕获用户的“对话”意图。
拾音与触发机制:这是最关键的一环。我们不能让装置一直处于录音状态,那样会耗尽存储且毫无意义。最经典的方案是利用电话听筒的挂钩开关。传统电话听筒被拿起时,会压下一个物理开关,接通线路。我们可以改造这个开关,将其作为一个高可靠的触发信号。当听筒被拿起,开关闭合,向主控板(如树莓派或Arduino)发送一个高电平信号,从而唤醒整个系统。这种方式比声音触发(如“嗨,Siri”)更稳定,避免了环境噪音误触发,也最符合用户对电话亭的心理预期——拿起听筒,准备通话。
音频输入硬件:在听筒内部,我们需要安装一个高质量的驻极体麦克风。选择时需注意信噪比和指向性。为了获得更清晰的人声,可以选用心形指向的麦克风,并做好物理隔震,减少拿起/放下听筒时的碰撞噪音。麦克风的信号需要经过一个简单的运算放大器电路进行初步放大,再送入主控板的音频输入接口。
环境感知扩展(可选但推荐):为了创造更沉浸的体验,可以增加一些传感器。例如,在电话亭内部安装一个人体红外传感器(PIR),当检测到有人进入时,可以控制一盏柔和的LED灯缓缓亮起,营造出“欢迎光临”的氛围。或者安装一个光线传感器,在夜晚自动调整内部照明亮度。这些细微的交互能极大提升装置的“生命感”。
2.2 处理层:大脑与记忆中枢
这一层负责处理声音、生成回应、并管理所有的对话数据。
主控单元选型:树莓派4B/5是几乎完美的选择。它性能足够强大,能流畅运行Python脚本和轻量级AI模型;拥有丰富的GPIO接口连接传感器;自带音频输入输出;而且能连接网络。相比Arduino,树莓派能本地处理更复杂的逻辑,是项目的“大脑”。
音频处理流程:
- 录音:使用Python的
sounddevice或pyaudio库,在听筒触发后开始录音。可以设置一个最大录音时长(如3分钟),并在检测到长时间静音后自动停止,以节省资源。 - 降噪与增强:录制的声音可能包含街道噪音。可以使用
noisereduce这样的Python库进行简单的频谱降噪。更进阶的做法是录制一段电话亭内的环境噪音作为样本,进行针对性消除。 - 语音识别(STT):将用户的语音转为文本。对于离线、低延迟的场景,可以选用Vosk离线语音识别库,它支持多种语言,模型小巧,在树莓派上运行良好。如果网络条件允许且需要更高精度,可以调用在线API(如科大讯飞、Azure Speech Services的免费额度),但需考虑网络稳定性和隐私问题。
- 录音:使用Python的
对话逻辑与内容生成:这是项目的灵魂,有几种实现路径:
- 预设音频库(最简单):预先录制好几十段音频(故事、诗歌、问题、音乐片段),当用户拿起听筒时,随机播放一段。或者设计成“故事线”模式,根据用户上次的“选择”(通过按键或语音)播放不同的下一段。
- 关键词触发式:对识别出的文本进行简单的关键词匹配。如果用户说“今天心情不好”,则播放一段舒缓的音乐或鼓励的话;如果说“讲个故事”,则从故事库中随机选取。
- AI对话引擎(高阶):这是实现“真正对话”的关键。可以在树莓派上部署一个轻量级的开源大语言模型(LLM),例如Phi-2、TinyLlama或Qwen1.5-1.8B,并使用Ollama或llama.cpp进行本地化部署和推理。将用户语音识别后的文本输入给LLM,让它生成富有同理心和创意的文本回复,再通过TTS播放出来。这能带来独一无二、不可预测的对话体验。
语音合成(TTS):将系统生成的文本回复转化为语音。离线方案可以选择espeak(声音机械但轻量)或Piper(质量更高,更自然)。在线方案则可以选择微软、谷歌的TTS服务,音质更优。
数据存储与记忆:如果希望电话亭能有“记忆”,记得之前的对话者,可以设计一个匿名化的对话日志系统。将每次对话的文本(去除任何个人身份信息)和生成的回应用一个时间戳保存到本地SQLite数据库或简单的文本文件中。甚至可以设计一种机制,让当前用户听到上一位用户留下的“一句话”(经过审核),形成跨越时间的接力对话。
2.3 体验层:营造沉浸式的对话氛围
硬件和软件是骨架,体验设计才是血肉。
声学设计:电话亭本身是一个相对封闭的金属/玻璃空间,容易产生回声。需要在内部粘贴一些吸音海绵,尤其是在麦克风附近,以提升录音质量。扬声器的位置也很讲究,最好安装在听筒的耳承部分,模拟真实电话听感,或者安装在亭内顶部,营造环绕感。
灯光与视觉反馈:灯光是情绪的放大器。可以在听筒拿起时,让电话亭的顶部灯光或内部灯带变为柔和的暖黄色,表示“正在聆听”;当系统在思考或生成回复时,灯光变为缓慢呼吸的蓝色;播放回复时,灯光保持稳定。这给予了用户即时的系统状态反馈。
引导与提示:在电话亭内部或窗口,可以贴上一张富有设计感的“使用说明”,不是冰冷的步骤,而是一句诗或一个问题,例如:“拿起听筒,说出你的秘密或倾听一个故事。” 在系统首次启动或长时间无人使用时,听筒里可以播放一段简短的引导语音:“你好,我是一个会说话的电话亭。你可以对我诉说,也可以静静聆听。”
3. 硬件搭建与核心电路解析
理论清晰后,我们进入动手环节。硬件搭建是项目稳定的基石。
3.1 核心物料清单
- 电话亭本体:可以在二手市场、复古商店或拆迁工地寻找。确保结构完好,尤其是门和玻璃。红色英式K6电话亭最具标志性,但其他款式也可。
- 主控板:树莓派4B 4GB/8GB 或树莓派5,配一张32GB以上的高速Micro SD卡。
- 音频模块:USB声卡(确保树莓派兼容)或树莓派专用HAT音频扩展板,以获得更好的输入输出质量。
- 麦克风:高灵敏度驻极体麦克风模块(带模拟输出)或USB接口的会议麦克风。
- 扬声器:一个小型的有源音箱或高质量的3.5mm接口桌面扬声器。功率不需要太大,5W-10W足以充满电话亭空间。
- 传感器:
- 微动开关(用于听筒挂钩触发)。
- 人体红外传感器模块(HC-SR501)。
- 光线传感器模块(如BH1750)。
- 电源系统:
- 大容量移动电源(20000mAh以上)或直接接入市电(需做好安全绝缘)。
- 5V/3A的USB电源适配器为树莓派供电。
- 一个多口USB充电器为其他模块供电。
- 连接线材:杜邦线(公对公、公对母)、音频线、电源线等。
- 其他工具:电烙铁、焊锡、热熔胶枪、螺丝刀套装、绝缘胶布、吸音棉。
3.2 核心电路连接详解
这里重点讲解最关键的“听筒触发电路”和“音频回路”。
听筒触发电路:电话亭原有的听筒挂钩下通常是一个机械开关。我们将其两条引线引出,连接到树莓派的某个GPIO引脚(例如GPIO17)和GND之间。在树莓派上,将该引脚设置为上拉输入模式。当听筒放下时,开关断开,GPIO引脚通过内部上拉电阻读到高电平(1)。当听筒被拿起,开关闭合,引脚直接连接到GND,读到低电平(0)。我们只需要在Python脚本中持续检测这个引脚的电平变化,一旦从高变低,就触发录音程序。
注意:务必在GPIO引脚和开关之间串联一个约330欧姆的电阻,作为限流保护,防止意外短路损坏树莓派。这是硬件操作中必须遵守的安全准则。
音频连接:方案一(简单):使用一个USB声卡,将麦克风插入声卡的麦克风输入孔,将扬声器插入声卡的音频输出孔。树莓派通过USB识别该声卡为默认音频设备。 方案二(质量更优):使用树莓派专用的I2S音频HAT(如HiFiBerry DAC+ ADC),它能提供比板载音频或普通USB声卡好得多的音质。麦克风连接到HAT的输入端子,扬声器连接到输出端子。
传感器连接:PIR传感器和光线传感器通常都有数字或I2C接口,按照模块说明连接到树莓派的对应GPIO或I2C引脚即可。例如,HC-SR501输出数字信号,接GPIO;BH1750是I2C设备,接SDA和SCL。
3.3 供电与布线的实战心得
- 供电隔离:数字电路(树莓派、传感器)和音频放大电路(如果使用独立功放)最好由不同的电源或不同的USB口供电,避免扬声器大音量时产生的电流噪声通过电源串入音频系统,导致“滋滋”的底噪。
- 走线整洁:电话亭内部空间有限。使用扎带和线槽将所有线材整理固定,避免缠绕。特别是连接到可活动部件(如听筒)的线,要留出足够的余量并做好应力保护,防止反复弯折导致内部断裂。
- 散热考虑:树莓派在持续进行音频处理和AI推理时会产生热量。确保其放置在通风处,可以加装一个小型散热风扇或散热片,避免因过热降频导致系统卡顿。
- 防潮与安全:如果电话亭将放置在户外或半户外,必须做好防水防潮处理。所有电路板接口和接线处可以用热熔胶或绝缘胶进行密封。电源部分必须严格绝缘,最好使用防水接线盒。安全永远是第一位的。
4. 软件架构与核心代码实现
硬件是躯体,软件是灵魂。我们将构建一个模块化、易于维护的Python应用程序。
4.1 项目目录结构与依赖
首先创建项目目录,并安装核心依赖包。
# 创建项目目录 mkdir talking_phone_booth && cd talking_phone_booth # 创建虚拟环境(推荐) python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 安装核心依赖 pip install sounddevice numpy scipy # 音频录制与处理 pip install vosk # 离线语音识别 pip install openai-whisper # 备用:高精度语音识别(需要一定性能) pip install pyttsx3 # 离线TTS # 如果使用Piper TTS,需要从其GitHub仓库安装 # 如果使用Ollama运行本地LLM,需要单独安装Ollama并拉取模型 pip install RPi.GPIO # 控制树莓派GPIO(仅在树莓派上需要) pip install sqlite3 # 内置于Python标准库,用于数据存储4.2 核心模块代码解析
我们创建几个核心的Python模块文件。
main.py- 主程序入口这是整个系统的调度中心,负责监听触发事件、协调各个模块工作。
#!/usr/bin/env python3 import time import threading from gpio_listener import GpioListener from audio_recorder import AudioRecorder from speech_processor import SpeechProcessor from dialogue_engine import DialogueEngine from tts_engine import TTSEngine import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class TalkingBooth: def __init__(self): logger.info("初始化会说话的电话亭系统...") self.gpio_listener = GpioListener(callback=self.on_handset_lifted) self.audio_recorder = AudioRecorder() self.speech_processor = SpeechProcessor() self.dialogue_engine = DialogueEngine() self.tts_engine = TTSEngine() self.is_processing = False def on_handset_lifted(self): """当听筒被拿起时的回调函数""" if self.is_processing: logger.warning("系统正忙,忽略此次触发。") return self.is_processing = True logger.info("检测到听筒被拿起,开始对话流程。") # 在一个新线程中处理,避免阻塞GPIO监听 thread = threading.Thread(target=self.conversation_flow) thread.start() def conversation_flow(self): """完整的对话流程""" try: # 1. 播放欢迎音或等待提示音 self.tts_engine.play("welcome.wav") # 播放预录的欢迎音 # 2. 录制用户语音 logger.info("开始录音...") audio_data, sample_rate = self.audio_recorder.record(duration=30) # 最长30秒 if audio_data is None: logger.error("录音失败。") self.end_conversation() return # 3. 语音识别 logger.info("进行语音识别...") user_text = self.speech_processor.recognize(audio_data, sample_rate) if not user_text: user_text = "[未能识别语音]" logger.info(f"识别结果: {user_text}") # 4. 生成回复 logger.info("生成对话回复...") reply_text = self.dialogue_engine.generate_reply(user_text) logger.info(f"系统回复: {reply_text}") # 5. 语音合成并播放 logger.info("合成并播放语音...") self.tts_engine.speak(reply_text) # 6. (可选)记录本次对话 self.dialogue_engine.log_conversation(user_text, reply_text) except Exception as e: logger.error(f"对话流程出错: {e}") finally: self.end_conversation() def end_conversation(self): """结束对话,重置状态""" time.sleep(1) # 给用户一点反应时间 logger.info("本次对话结束,等待下一次触发。") self.is_processing = False def run(self): """启动系统""" logger.info("系统启动,等待听筒被拿起...") self.gpio_listener.start_listening() try: while True: time.sleep(0.1) except KeyboardInterrupt: logger.info("收到中断信号,关闭系统。") self.gpio_listener.cleanup() if __name__ == "__main__": booth = TalkingBooth() booth.run()gpio_listener.py- GPIO监听模块负责检测听筒挂钩开关的状态变化。
import RPi.GPIO as GPIO import time import logging logger = logging.getLogger(__name__) class GpioListener: def __init__(self, pin=17, callback=None): self.pin = pin self.callback = callback self.setup_gpio() def setup_gpio(self): GPIO.setmode(GPIO.BCM) GPIO.setup(self.pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) # 启用内部上拉电阻 logger.info(f"GPIO {self.pin} 已初始化为上拉输入模式,用于监听听筒状态。") def handset_lifted_callback(self, channel): """GPIO中断回调函数""" # 消除抖动 time.sleep(0.05) if GPIO.input(self.pin) == GPIO.LOW: # 确认是低电平(被拿起) logger.debug("检测到听筒拿起(下降沿触发)。") if self.callback: self.callback() def start_listening(self): """开始监听GPIO变化""" # 添加下降沿检测,当引脚从高电平变为低电平时触发 GPIO.add_event_detect(self.pin, GPIO.FALLING, callback=self.handset_lifted_callback, bouncetime=300) logger.info("GPIO监听已启动。") def cleanup(self): GPIO.cleanup(self.pin) logger.info("GPIO资源已清理。")speech_processor.py- 语音处理模块整合录音、降噪和语音识别功能。
import sounddevice as sd import numpy as np import scipy.signal as sp import noisereduce as nr import json from vosk import Model, KaldiRecognizer import logging logger = logging.getLogger(__name__) class SpeechProcessor: def __init__(self, model_path="vosk-model-small-en-us-0.15"): # 初始化Vosk语音识别模型(需提前下载模型文件) try: self.model = Model(model_path) self.recognizer = KaldiRecognizer(self.model, 16000) logger.info(f"Vosk模型 '{model_path}' 加载成功。") except Exception as e: logger.error(f"加载Vosk模型失败: {e}. 请确保模型路径正确。") self.model = None self.recognizer = None def record(self, duration=30, sample_rate=16000): """录制音频""" logger.info(f"开始录制{duration}秒音频...") try: audio_data = sd.rec(int(duration * sample_rate), samplerate=sample_rate, channels=1, dtype='float32') sd.wait() # 等待录制完成 logger.info("录音完成。") return audio_data.flatten(), sample_rate except Exception as e: logger.error(f"录音失败: {e}") return None, sample_rate def reduce_noise(self, audio_data, sample_rate, noise_duration=0.5): """使用noisereduce库进行降噪""" # 假设录音的前 noise_duration 秒是环境噪音 noise_sample_len = int(noise_duration * sample_rate) if len(audio_data) > noise_sample_len: noise_clip = audio_data[:noise_sample_len] reduced_noise = nr.reduce_noise(y=audio_data, sr=sample_rate, y_noise=noise_clip, prop_decrease=0.9) logger.debug("音频降噪处理完成。") return reduced_noise else: return audio_data def recognize(self, audio_data, sample_rate): """使用Vosk进行语音识别""" if self.recognizer is None: return "语音识别引擎未就绪。" # 确保音频数据是16kHz,16位整型(Vosk所需格式) audio_data_int16 = (audio_data * 32767).astype(np.int16) # 识别 if self.recognizer.AcceptWaveform(audio_data_int16.tobytes()): result = json.loads(self.recognizer.Result()) text = result.get("text", "") else: partial_result = json.loads(self.recognizer.PartialResult()) text = partial_result.get("partial", "") logger.info(f"识别文本: '{text}'") return text if text else "[静音或无法识别]"dialogue_engine.py- 对话引擎模块这是项目的智能核心,决定如何回复用户。
import random import sqlite3 from datetime import datetime import logging logger = logging.getLogger(__name__) class DialogueEngine: def __init__(self, db_path="conversations.db"): self.db_path = db_path self.init_database() # 预设回复库 - 可以根据关键词或情绪扩展 self.preset_responses = [ "我听到了你的声音,这真美妙。", "这座城市里充满了故事,而你的声音是其中之一。", "有时,沉默也是一种回答。你今天想分享些什么呢?", "我是一个来自过去的回声,却想倾听你的现在。", "试试对我说一个秘密,我会把它变成一阵风。", ] # 简单关键词-回复映射 self.keyword_responses = { "hello": ["你好呀,陌生人。", "嗨,很高兴‘听’到你。"], "story": ["从前有个电话亭,它学会了倾听...", "你想听一个关于月亮的故事吗?"], "sad": ["我听见了雨声。没关系,有时悲伤也需要被听见。", "送你一片虚拟的阳光,希望能照亮你的角落。"], "love": ["爱是世界上最古老的通讯方式,无需信号。", "心有回响,爱有回声。"], } def init_database(self): """初始化SQLite数据库,用于记录对话(可选)""" conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS conversations (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, user_input TEXT, system_response TEXT)''') conn.commit() conn.close() logger.info("对话数据库初始化完成。") def generate_reply(self, user_input): """根据用户输入生成回复文本""" user_input_lower = user_input.lower() # 1. 检查是否为问候或简单指令 for keyword, responses in self.keyword_responses.items(): if keyword in user_input_lower: logger.debug(f"触发关键词 '{keyword}'") return random.choice(responses) # 2. 检查是否为空或无法识别 if not user_input or user_input == "[未能识别语音]" or len(user_input.strip()) < 2: return random.choice(["我在这里。", "你可以说点什么。", "……"]) # 3. 默认回复:从预设库随机选择,或尝试构建一个简单的关联回复 # 这里可以替换为调用本地LLM的逻辑 # 例如:return self._ask_llm(user_input) return self._generate_with_llm_fallback(user_input) def _generate_with_llm_fallback(self, user_input): """一个简单的回复生成逻辑,可替换为真正的LLM调用""" # 这是一个非常基础的示例:提取用户输入中的最后一个词,并围绕它构造回复 words = user_input.strip().split() if words: last_word = words[-1].rstrip('.,!?') possible_replies = [ f"你提到了‘{last_word}’,这让我想起...", f"关于‘{last_word}’,每个路过的人都有不同的看法。", f"‘{last_word}’是一个很好的词。", ] return random.choice(possible_replies) else: return random.choice(self.preset_responses) # 未来扩展:集成Ollama调用本地LLM # def _ask_llm(self, user_input): # import requests # try: # response = requests.post('http://localhost:11434/api/generate', # json={'model': 'tinyllama', # 'prompt': f'You are a poetic, philosophical telephone booth. Respond briefly and thoughtfully to this: {user_input}', # 'stream': False}) # return response.json()['response'].strip() # except Exception as e: # logger.error(f"LLM调用失败: {e}") # return random.choice(self.preset_responses) def log_conversation(self, user_input, system_response): """将对话记录到数据库""" try: conn = sqlite3.connect(self.db_path) c = conn.cursor() timestamp = datetime.now().isoformat() # 注意:在实际部署中,应对user_input进行严格的匿名化清洗,避免记录任何个人信息。 cleaned_input = user_input[:500] # 简单截断,生产环境需更复杂的处理 c.execute("INSERT INTO conversations (timestamp, user_input, system_response) VALUES (?, ?, ?)", (timestamp, cleaned_input, system_response)) conn.commit() conn.close() logger.debug("对话已记录到数据库。") except Exception as e: logger.error(f"记录对话失败: {e}")tts_engine.py- 语音合成模块负责将文本转化为语音并播放。
import pyttsx3 import subprocess import os import logging logger = logging.getLogger(__name__) class TTSEngine: def __init__(self, use_piper=False, piper_model_path=None): self.use_piper = use_piper self.piper_model_path = piper_model_path if not use_piper: self.init_pyttsx3() def init_pyttsx3(self): """初始化pyttsx3离线TTS引擎""" try: self.engine = pyttsx3.init() # 设置语速和音量 self.engine.setProperty('rate', 150) self.engine.setProperty('volume', 0.9) voices = self.engine.getProperty('voices') # 尝试选择一个听起来更自然的语音(如果有的话) for voice in voices: if 'english' in voice.name.lower(): self.engine.setProperty('voice', voice.id) break logger.info("pyttsx3 TTS引擎初始化成功。") except Exception as e: logger.error(f"初始化pyttsx3失败: {e}") self.engine = None def speak(self, text): """合成并播放语音""" if not text: return logger.info(f"TTS合成: {text}") if self.use_piper and self.piper_model_path: self._speak_with_piper(text) elif self.engine: self._speak_with_pyttsx3(text) else: logger.error("没有可用的TTS引擎。") # 可以在这里播放一个备用的提示音 def _speak_with_pyttsx3(self, text): """使用pyttsx3播放""" try: self.engine.say(text) self.engine.runAndWait() except Exception as e: logger.error(f"pyttsx3播放失败: {e}") def _speak_with_piper(self, text): """使用Piper TTS播放(更高质量,需要额外安装)""" # 假设Piper已安装,并且模型文件可用 # 命令示例:echo \"Your text\" | piper --model model.onnx --output-raw | aplay -r 22050 -f S16_LE -t raw - try: cmd = f'echo "{text}" | piper --model {self.piper_model_path} --output-raw | aplay -r 22050 -f S16_LE -t raw - 2>/dev/null' subprocess.run(cmd, shell=True, check=True) except Exception as e: logger.error(f"Piper TTS播放失败: {e}") # 降级到pyttsx3 if self.engine: self._speak_with_pyttsx3(text) def play(self, audio_file_path): """播放一个预录的音频文件(如欢迎音)""" if os.path.exists(audio_file_path): try: subprocess.run(['aplay', audio_file_path], check=False) # Linux # 对于macOS: subprocess.run(['afplay', audio_file_path]) # 对于Windows: subprocess.run(['start', audio_file_path], shell=True) except Exception as e: logger.error(f"播放音频文件失败: {e}")4.3 系统配置与自启动
为了让项目在树莓派上电后自动运行,我们需要配置系统服务。
创建系统服务文件:
sudo nano /etc/systemd/system/talking-booth.service写入以下内容(根据你的实际路径修改):
[Unit] Description=Talking Phone Booth Service After=network.target sound.target [Service] Type=simple User=pi WorkingDirectory=/home/pi/talking_phone_booth Environment="PATH=/home/pi/talking_phone_booth/venv/bin" ExecStart=/home/pi/talking_phone_booth/venv/bin/python /home/pi/talking_phone_booth/main.py Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target启用并启动服务:
sudo systemctl daemon-reload sudo systemctl enable talking-booth.service sudo systemctl start talking-booth.service # 检查状态 sudo systemctl status talking-booth.service # 查看日志 sudo journalctl -u talking-booth.service -f
5. 部署、调试与问题排查实录
将代码部署到电话亭内的树莓派上,才是真正的挑战开始。以下是实战中积累的经验和常见问题。
5.1 现场部署流程
- 系统烧录与基础配置:使用Raspberry Pi Imager将Raspbian Lite系统烧录到SD卡,并预先配置好Wi-Fi和国家设置。首次启动后,通过SSH连接进行后续操作。
- 环境搭建:在树莓派上重复上述的Python虚拟环境和依赖安装步骤。特别注意,一些音频库(如
portaudio)可能需要额外安装系统包:sudo apt install portaudio19-dev python3-dev。 - 硬件连接检查:在通电前,用万用表通断档检查所有接线,确保没有短路。特别是GPIO引脚,确认上拉电阻已正确连接。
- 音频设备配置:运行
arecord -l和aplay -l列出音频设备。在/home/pi/.asoundrc文件中设置默认的录音和播放设备,指向你的USB声卡或HAT。 - 权限问题:确保运行服务的用户(如
pi)有权限访问音频设备。通常需要将其加入audio组:sudo usermod -a -G audio pi。 - 功耗与稳定性测试:连接好所有设备,上电后持续运行压力测试脚本(如连续进行录音-识别-播放循环),观察树莓派温度(
vcgencmd measure_temp)和系统是否稳定运行数小时。
5.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 拿起听筒无反应 | 1. GPIO引脚配置错误。 2. 微动开关接触不良或接线松动。 3. 主程序未运行或崩溃。 | 1. 运行python -c “import RPi.GPIO as GPIO; GPIO.setmode(GPIO.BCM); GPIO.setup(17, GPIO.IN); print(GPIO.input(17))”,拿起/放下听筒观察值是否变化(1->0)。2. 用万用表检查开关通断。 3. 检查服务状态 sudo systemctl status talking-booth和日志journalctl -u talking-booth -f。 |
| 录音全是噪音或无声 | 1. 默认音频设备错误。 2. 麦克风损坏或增益过低。 3. 录音采样率与识别模型不匹配。 | 1. 确认arecord -l并正确设置.asoundrc。2. 用 arecord -D hw:1,0 -f cd test.wav(设备号根据实际情况改)直接测试录音。3. Vosk模型通常需要16kHz单声道,确保 AudioRecorder中sample_rate=16000。 |
| 语音识别率极低 | 1. 环境噪音过大。 2. 麦克风音质差或距离嘴太远。 3. 模型语言不匹配。 | 1. 加强电话亭内部隔音,优化降噪算法参数(prop_decrease)。2. 确保麦克风正对用户,并尝试在代码中增加音频增益。 3. 下载与目标语言对应的Vosk模型(如中文模型)。 |
| TTS播放有爆音或断断续续 | 1. 树莓派CPU占用过高,音频缓冲区欠载。 2. 扬声器或功放电源功率不足。 3. 多个音频进程冲突。 | 1. 使用htop查看CPU占用,优化代码(如将LLM推理放在独立线程)。考虑使用性能更强的树莓派5。2. 为音频功放提供独立电源。 3. 确保在播放前停止所有其他播放进程。 |
| 系统运行一段时间后卡死 | 1. 内存泄漏。 2. SD卡读写错误(特别是频繁写日志)。 3. 过热降频。 | 1. 检查Python代码,确保在循环中正确释放资源(如关闭数据库连接)。 2. 将日志写入到内存文件系统( /tmp)或减少日志频率。使用高质量、高耐久度的工业级SD卡。3. 改善散热,加装风扇。 |
| LLM(如果使用)响应慢 | 1. 模型太大,树莓派算力不足。 2. Ollama服务未启动或内存不足。 | 1. 选择更小的模型(如Phi-2, TinyLlama)。考虑使用量化版本(q4_k_m)。 2. 增加树莓派交换空间: sudo dphys-swapfile swapoff && sudo nano /etc/dphys-swapfile修改CONF_SWAPSIZE=2048,然后重启服务。 |
5.3 提升体验的进阶技巧
- 状态指示灯:除了灯光,可以在电话亭外增加一个小型LCD屏或LED点阵,显示简单的状态文字,如“Listening...”、“Thinking...”、“Speaking...”,让交互更透明。
- 对话记忆与延续:在
DialogueEngine中,为每个会话创建一个简单的ID(如基于时间戳的哈希),并在数据库中将同一会话的多次交互关联起来。这样,当用户短时间内再次拿起听筒,系统可以接着说:“我们刚才聊到了...”。 - 远程管理与监控:使用
Telegram Bot或Flask搭建一个简单的Web后台,让你能远程查看电话亭的对话日志、系统状态(CPU温度、内存使用率),甚至远程更新回复词库或重启服务。 - 内容众筹:开放一个简单的网页表单,让公众可以提交他们希望电话亭“说出”的句子或故事,经过审核后自动加入系统的回复库,让这个装置真正成为社区共创的作品。
- 电力续航:如果使用移动电源,可以接入一个USB电压电流检测模块,并通过树莓派读取其电量。当电量低于20%时,让电话亭在对话结束时播报:“我的能量即将耗尽,需要休息充电了。”,并在完全关机前,自动安全地关闭树莓派系统。
这个“会说话的电话亭”项目,从技术上看是传感器、嵌入式系统、音频处理和AI的集合体,但从体验上看,它是一次对公共空间诗意化的尝试。最难的部分往往不是代码调试,而是在街头巷尾部署时,应对潮湿、温差、电源不稳定以及人们无意中的好奇触碰。每一次成功的对话,都像是给这座冰冷的城市机器注入了一小段温暖的代码。当你听到第一个路人对着听筒惊讶地笑出声时,你会觉得所有的折腾都值了。最后一个小建议:在正式放置前,不妨先邀请朋友或邻居来一次小范围的测试,他们的真实反馈,是优化体验最宝贵的资源。