语音控制多房间音频:Snapcast与语音助手无缝集成指南

【免费下载链接】snapcast Synchronous multiroom audio player 【免费下载链接】snapcast 项目地址: https://gitcode.com/gh_mirrors/sn/snapcast

引言:告别手动操作的音频控制困境

你是否曾在厨房忙碌时,不得不擦干手去调整客厅的音乐音量?或者在深夜追剧时,摸索着寻找手机来切换卧室的播放列表?作为多房间音频系统的用户,这些场景或许早已司空见惯。Snapcast作为一款优秀的同步多房间音频播放器(Synchronous multiroom audio player),为我们提供了高品质的音频同步体验,但原生功能中缺乏与主流语音助手的直接集成。本文将带你一步步实现Snapcast与语音助手的无缝对接,彻底解放双手,用语音指令掌控整个音频系统。

读完本文后,你将能够:

  • 理解Snapcast的控制接口与语音助手集成的技术路径
  • 搭建基于Python的语音指令处理服务
  • 实现与Amazon Alexa、Google Assistant的技能集成
  • 定制个性化语音指令与自动化场景
  • 解决常见的同步延迟与权限问题

技术原理:Snapcast控制架构解析

Snapcast核心组件与通信流程

Snapcast系统由服务端(Snapserver)和客户端(Snapclient)组成,采用C/S架构实现音频流的同步播放。要实现语音控制,我们首先需要理解其控制接口的工作原理。

mermaid

Snapserver提供了两种主要控制方式:

  1. TCP控制接口:基于JSON-RPC 2.0协议,默认监听1705端口
  2. HTTP控制接口:REST风格API,默认监听1780端口

通过这些接口,我们可以发送播放、暂停、音量调节、音源切换等命令。例如,调整特定客户端音量的HTTP请求:

curl -X POST http://localhost:1780/jsonrpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"Client.SetVolume","params":{"id":"client_id","volume":{"percent":75}}}'

语音助手集成技术路径

将语音助手与Snapcast集成,通常有以下三种技术方案,各有其适用场景:

集成方案 实现难度 隐私性 依赖外部服务 适用场景
本地语音识别 + 直接控制 中等 注重隐私,局域网环境
云语音助手 + 本地中间服务 追求便利性,需要多平台支持
自定义技能 + 云函数 + 控制接口 多用户场景,远程控制需求

本文将重点介绍第二种方案,它平衡了实现复杂度与用户体验,适合大多数家庭用户。该方案的核心是构建一个中间服务,负责接收来自云语音助手的指令,解析后通过Snapcast的控制接口执行相应操作。

环境准备:开发与运行环境搭建

硬件与软件要求

  • 运行Snapserver的设备:推荐树莓派4B+或x86服务器(本文以Ubuntu 22.04 LTS为例)
  • 至少一台Snapclient设备
  • Python 3.8+环境(用于中间服务开发)
  • Node.js 14+(可选,用于Google Actions开发)
  • 公网可访问的服务器或内网穿透工具(用于云语音助手回调)

必要依赖安装

首先确保Snapserver已启用控制接口,检查配置文件/etc/snapserver.conf

[server]
# 确保控制服务器已启用
controlServer = 127.0.0.1:1705
httpServer = 8080

[stream]
# 配置默认音频流
source = pipe:///tmp/snapfifo?name=default

安装Python依赖库:

# 创建虚拟环境
python -m venv snapcast-voice-env
source snapcast-voice-env/bin/activate

# 安装核心依赖
pip install flask flask-ngrok python-dotenv requests pyaudio SpeechRecognition pyttsx3

克隆项目代码库(国内镜像):

git clone https://gitcode.com/gh_mirrors/sn/snapcast.git
cd snapcast/control

核心实现:构建语音指令处理服务

第一步:开发Snapcast控制API封装

创建snapcast_controller.py,封装Snapcast控制功能:

import requests
import json
from typing import Dict, Optional, List

class SnapcastController:
    def __init__(self, server_ip: str = "localhost", port: int = 1780):
        self.base_url = f"http://{server_ip}:{port}/jsonrpc"
        self.headers = {"Content-Type": "application/json"}
        self.client_ids = self._get_client_ids()
        
    def _send_request(self, method: str, params: Optional[Dict] = None) -> Dict:
        """发送JSON-RPC请求到Snapserver"""
        payload = {
            "jsonrpc": "2.0",
            "id": 1,
            "method": method,
            "params": params or {}
        }
        
        try:
            response = requests.post(
                self.base_url,
                headers=self.headers,
                data=json.dumps(payload),
                timeout=5
            )
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            print(f"API请求失败: {e}")
            return {"error": str(e)}
    
    def _get_client_ids(self) -> Dict[str, str]:
        """获取所有客户端ID与名称映射"""
        result = self._send_request("Server.GetStatus")
        if "result" in result and "clients" in result["result"]:
            return {
                client["name"]: client["id"] 
                for client in result["result"]["clients"]
            }
        return {}
    
    def set_volume(self, client_name: str, volume_percent: int) -> Dict:
        """设置指定客户端音量"""
        if client_name not in self.client_ids:
            return {"error": f"客户端 {client_name} 不存在"}
            
        client_id = self.client_ids[client_name]
        return self._send_request(
            "Client.SetVolume",
            {
                "id": client_id,
                "volume": {"percent": volume_percent}
            }
        )
    
    def play_pause(self, client_name: str = None) -> Dict:
        """切换播放/暂停状态"""
        params = {}
        if client_name:
            if client_name not in self.client_ids:
                return {"error": f"客户端 {client_name} 不存在"}
            params["id"] = self.client_ids[client_name]
            
        return self._send_request("Client.PlayPause", params)
    
    def switch_stream(self, stream_name: str) -> Dict:
        """切换音频流"""
        return self._send_request(
            "Server.SetActiveStream",
            {"id": stream_name}
        )
    
    def get_status(self) -> Dict:
        """获取服务器状态"""
        return self._send_request("Server.GetStatus")

# 使用示例
if __name__ == "__main__":
    controller = SnapcastController("127.0.0.1", 1780)
    print("客户端列表:", controller.client_ids)
    print("设置客厅音量为70%:", controller.set_volume("客厅", 70))

第二步:实现语音识别与指令解析

创建voice_processor.py,处理语音输入并转换为控制指令:

import speech_recognition as sr
import pyttsx3
from snapcast_controller import SnapcastController

class VoiceProcessor:
    def __init__(self, language: str = "zh-CN"):
        self.recognizer = sr.Recognizer()
        self.microphone = sr.Microphone()
        self.language = language
        self.controller = SnapcastController()
        self.engine = pyttsx3.init()
        # 设置中文语音
        voices = self.engine.getProperty('voices')
        for voice in voices:
            if 'chinese' in voice.id.lower():
                self.engine.setProperty('voice', voice.id)
                break
    
    def listen(self) -> str:
        """监听麦克风输入并识别文本"""
        with self.microphone as source:
            print("正在监听...")
            self.recognizer.adjust_for_ambient_noise(source)
            audio = self.recognizer.listen(source, timeout=5)
        
        try:
            return self.recognizer.recognize_google(
                audio, language=self.language
            )
        except sr.UnknownValueError:
            return "无法理解语音指令"
        except sr.RequestError as e:
            return f"语音识别服务出错: {e}"
    
    def speak(self, text: str) -> None:
        """文本转语音反馈"""
        self.engine.say(text)
        self.engine.runAndWait()
    
    def parse_command(self, command: str) -> bool:
        """解析指令并执行相应操作"""
        command = command.lower()
        response = "指令执行成功"
        
        # 音量控制指令
        if "音量" in command:
            # 提取数字
            import re
            numbers = re.findall(r'\d+', command)
            if not numbers:
                self.speak("请说出具体音量数值")
                return False
                
            volume = int(numbers[0])
            if volume < 0 or volume > 100:
                self.speak("音量值必须在0到100之间")
                return False
                
            # 判断客户端
            client_name = "默认"
            if "客厅" in command:
                client_name = "客厅"
            elif "卧室" in command:
                client_name = "卧室"
            elif "厨房" in command:
                client_name = "厨房"
                
            result = self.controller.set_volume(client_name, volume)
            if "error" in result:
                response = result["error"]
            else:
                response = f"{client_name}音量已设置为{volume}%"
        
        # 播放暂停控制
        elif "播放" in command or "暂停" in command:
            client_name = None
            if "客厅" in command:
                client_name = "客厅"
            elif "卧室" in command:
                client_name = "卧室"
                
            result = self.controller.play_pause(client_name)
            if "error" in result:
                response = result["error"]
            else:
                response = "已执行播放暂停操作"
        
        # 切换音频流
        elif "切换" in command and "音乐" in command:
            if "流行" in command:
                result = self.controller.switch_stream("pop_music")
                response = "已切换到流行音乐流"
            elif "古典" in command:
                result = self.controller.switch_stream("classical")
                response = "已切换到古典音乐流"
            else:
                response = "未识别的音乐类型"
        
        else:
            response = "未识别的指令"
        
        self.speak(response)
        print(response)
        return True

# 本地测试
if __name__ == "__main__":
    processor = VoiceProcessor()
    while True:
        command = processor.listen()
        print(f"识别结果: {command}")
        if "退出" in command:
            processor.speak("再见")
            break
        processor.parse_command(command)

第三步:构建Web服务接收语音助手回调

创建app.py,使用Flask构建Web服务:

from flask import Flask, request, jsonify
from flask_ngrok import run_with_ngrok  # 用于本地开发的内网穿透
from snapcast_controller import SnapcastController
import os
from dotenv import load_dotenv

load_dotenv()  # 加载环境变量

app = Flask(__name__)
run_with_ngrok(app)  # 仅用于开发环境

controller = SnapcastController(
    os.getenv("SNAPSERVER_IP", "localhost"),
    int(os.getenv("SNAPSERVER_PORT", 1780))
)

# 验证令牌
VALID_TOKENS = set(os.getenv("VALID_TOKENS", "").split(","))

def verify_token(token: str) -> bool:
    """验证请求令牌"""
    return token in VALID_TOKENS or not VALID_TOKENS  # 为空时不验证

@app.route("/api/volume", methods=["POST"])
def handle_volume():
    """处理音量调整请求"""
    data = request.json
    
    # 验证令牌
    if not verify_token(request.headers.get("Authorization", "")):
        return jsonify({"error": "未授权访问"}), 401
        
    # 验证参数
    if not all(k in data for k in ["client", "volume"]):
        return jsonify({"error": "缺少必要参数"}), 400
        
    result = controller.set_volume(data["client"], data["volume"])
    return jsonify(result)

@app.route("/api/playpause", methods=["POST"])
def handle_playpause():
    """处理播放暂停请求"""
    data = request.json
    
    if not verify_token(request.headers.get("Authorization", "")):
        return jsonify({"error": "未授权访问"}), 401
        
    client = data.get("client")
    result = controller.play_pause(client)
    return jsonify(result)

@app.route("/api/switchstream", methods=["POST"])
def handle_switchstream():
    """处理切换流请求"""
    data = request.json
    
    if not verify_token(request.headers.get("Authorization", "")):
        return jsonify({"error": "未授权访问"}), 401
        
    if "stream" not in data:
        return jsonify({"error": "缺少流名称参数"}), 400
        
    result = controller.switch_stream(data["stream"])
    return jsonify(result)

@app.route("/health", methods=["GET"])
def health_check():
    """健康检查接口"""
    return jsonify({"status": "ok", "timestamp": int(time.time())})

if __name__ == "__main__":
    import time
    app.run(host="0.0.0.0", port=int(os.getenv("PORT", 5000)))

创建.env配置文件:

SNAPSERVER_IP=127.0.0.1
SNAPSERVER_PORT=1780
PORT=5000
VALID_TOKENS=your_secure_token_here,another_token_if_needed

语音助手集成:从技能开发到部署

Amazon Alexa技能开发

1. 创建Alexa技能
  1. 访问Amazon Developer Console并创建新技能
  2. 选择"自定义"模型,语言选择"中文(中国)"
  3. 在"交互模型"中定义以下意图(Intent):

VolumeIntent

  • 样本话语:
    • "把{Room}的音量调到{Volume}%"
    • "设置{Room}音量为{Volume}%"
    • "调整{Room}音量到{Volume}%"
  • 槽位(Slot):
    • Room: 类型为自定义列表,值包括"客厅"、"卧室"、"厨房"等
    • Volume: 类型为AMAZON.NUMBER

PlayPauseIntent

  • 样本话语:
    • "{Action}音乐"
    • "{Action}{Room}的音乐"
  • 槽位:
    • Action: 类型为自定义列表,值包括"播放"、"暂停"
    • Room: 同上

SwitchStreamIntent

  • 样本话语:
    • "切换到{Stream}音乐"
    • "播放{Stream}流"
  • 槽位:
    • Stream: 类型为自定义列表,值包括"流行"、"古典"、"摇滚"等
2. 实现Lambda处理函数

创建AWS Lambda函数(Python 3.8+运行时):

import json
import requests
import os

# 从环境变量获取配置
WEBHOOK_URL = os.environ.get("WEBHOOK_URL")
AUTH_TOKEN = os.environ.get("AUTH_TOKEN")

def lambda_handler(event, context):
    """处理Alexa技能请求"""
    print("事件:", json.dumps(event))
    
    # 提取请求信息
    request_type = event["request"]["type"]
    
    # 处理启动请求
    if request_type == "LaunchRequest":
        return build_response("欢迎使用Snapcast语音控制,请说出您的指令")
    
    # 处理意图请求
    elif request_type == "IntentRequest":
        intent = event["request"]["intent"]["name"]
        
        # 音量控制意图
        if intent == "VolumeIntent":
            return handle_volume_intent(event)
        
        # 播放暂停意图
        elif intent == "PlayPauseIntent":
            return handle_play_pause_intent(event)
            
        # 切换流意图
        elif intent == "SwitchStreamIntent":
            return handle_switch_stream_intent(event)
            
        # 帮助意图
        elif intent == "AMAZON.HelpIntent":
            return build_response("您可以说:把客厅音量调到70%,或者播放卧室音乐")
            
        # 退出意图
        elif intent in ["AMAZON.CancelIntent", "AMAZON.StopIntent"]:
            return build_response("再见")
            
    return build_response("抱歉,我无法理解您的指令")

def handle_volume_intent(event):
    """处理音量控制意图"""
    slots = event["request"]["intent"]["slots"]
    
    # 获取槽位值
    room = slots.get("Room", {}).get("value", "默认")
    volume = slots.get("Volume", {}).get("value")
    
    if not volume:
        return build_response("请告诉我具体的音量数值")
        
    # 调用Web服务
    url = f"{WEBHOOK_URL}/api/volume"
    headers = {
        "Content-Type": "application/json",
        "Authorization": AUTH_TOKEN
    }
    payload = {
        "client": room,
        "volume": int(volume)
    }
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        return build_response(f"{room}音量已设置为{volume}%")
    except Exception as e:
        print(f"API调用失败: {e}")
        return build_response("操作失败,请稍后再试")

def handle_play_pause_intent(event):
    """处理播放暂停意图"""
    slots = event["request"]["intent"]["slots"]
    action = slots.get("Action", {}).get("value", "播放")
    room = slots.get("Room", {}).get("value")
    
    url = f"{WEBHOOK_URL}/api/playpause"
    headers = {
        "Content-Type": "application/json",
        "Authorization": AUTH_TOKEN
    }
    payload = {}
    if room:
        payload["client"] = room
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        return build_response(f"{action}{room if room else ''}音乐")
    except Exception as e:
        print(f"API调用失败: {e}")
        return build_response("操作失败,请稍后再试")

def handle_switch_stream_intent(event):
    """处理切换流意图"""
    slots = event["request"]["intent"]["slots"]
    stream = slots.get("Stream", {}).get("value")
    
    if not stream:
        return build_response("请告诉我要切换的音乐类型")
        
    # 映射流名称到实际ID
    stream_map = {
        "流行": "pop_music",
        "古典": "classical",
        "摇滚": "rock",
        "爵士": "jazz"
    }
    
    if stream not in stream_map:
        return build_response(f"不支持的音乐类型: {stream}")
        
    url = f"{WEBHOOK_URL}/api/switchstream"
    headers = {
        "Content-Type": "application/json",
        "Authorization": AUTH_TOKEN
    }
    payload = {"stream": stream_map[stream]}
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        return build_response(f"已切换到{stream}音乐")
    except Exception as e:
        print(f"API调用失败: {e}")
        return build_response("操作失败,请稍后再试")

def build_response(speech_text):
    """构建Alexa响应"""
    return {
        "version": "1.0",
        "response": {
            "outputSpeech": {
                "type": "PlainText",
                "text": speech_text
            },
            "shouldEndSession": True
        }
    }

Google Assistant集成

对于Google Assistant,我们将使用Actions on Google平台和Dialogflow构建对话代理。

1. 创建Dialogflow代理
  1. 访问Dialogflow Console创建新代理
  2. 在"意图"页面创建与Alexa技能类似的意图结构
  3. 设置实体(Entity):@room@volume@stream
2. 实现Webhook

扩展之前的Flask应用以支持Dialogflow Webhook:

# 在app.py中添加
from flask import request, jsonify

@app.route("/webhook/dialogflow", methods=["POST"])
def dialogflow_webhook():
    """处理Dialogflow Webhook请求"""
    req = request.get_json()
    intent = req["queryResult"]["intent"]["displayName"]
    
    if intent == "VolumeIntent":
        return handle_dialogflow_volume(req)
    elif intent == "PlayPauseIntent":
        return handle_dialogflow_playpause(req)
    elif intent == "SwitchStreamIntent":
        return handle_dialogflow_switchstream(req)
    else:
        return jsonify({
            "fulfillmentText": "抱歉,我无法理解您的指令"
        })

def handle_dialogflow_volume(req):
    """处理音量意图"""
    parameters = req["queryResult"]["parameters"]
    room = parameters.get("room", "默认")
    volume = parameters.get("volume")
    
    if not volume:
        return jsonify({"fulfillmentText": "请告诉我具体的音量数值"})
        
    url = f"{WEBHOOK_URL}/api/volume"  # 使用环境变量
    headers = {
        "Content-Type": "application/json",
        "Authorization": AUTH_TOKEN
    }
    payload = {
        "client": room,
        "volume": int(volume)
    }
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        return jsonify({"fulfillmentText": f"{room}音量已设置为{volume}%"})
    except:
        return jsonify({"fulfillmentText": "操作失败,请稍后再试"})

# 类似实现handle_dialogflow_playpause和handle_dialogflow_switchstream函数

本地语音助手:无需云服务的解决方案

对于注重隐私的用户,我们可以使用本地语音识别引擎(如Vosk、CMU Sphinx)构建完全离线的解决方案。

安装必要依赖:

pip install vosk pyaudio

下载中文模型(从Vosk模型库):

wget https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip
unzip vosk-model-small-cn-0.22.zip -d model

修改voice_processor.py以支持Vosk:

# 添加Vosk识别支持
from vosk import Model, KaldiRecognizer
import wave
import json

class VoskRecognizer:
    def __init__(self, model_path: str = "model"):
        self.model = Model(model_path)
        self.recognizer = KaldiRecognizer(self.model, 16000)
        
    def recognize_audio(self, audio_data) -> str:
        """识别音频数据"""
        wf = wave.open(audio_data, "rb")
        if wf.getnchannels() != 1 or wf.getsampwidth() != 2 or wf.getcomptype() != "NONE":
            return "仅支持单声道PCM音频"
            
        result = ""
        while True:
            data = wf.readframes(4000)
            if len(data) == 0:
                break
            if self.recognizer.AcceptWaveform(data):
                res = json.loads(self.recognizer.Result())
                result += res.get("text", "")
        
        # 获取最终结果
        res = json.loads(self.recognizer.FinalResult())
        result += res.get("text", "")
        
        return result

高级应用:自动化场景与故障排除

智能场景联动

结合智能家居系统,我们可以创建更复杂的自动化场景。例如:

# 日出/日落自动调整音量
def check_sun_time():
    """根据日出日落调整音量"""
    import ephem
    observer = ephem.Observer()
    observer.lat = '39.9042'  # 你的纬度
    observer.lon = '116.4074'  # 你的经度
    
    sun = ephem.Sun(observer)
    sunrise = observer.next_rising(sun).datetime()
    sunset = observer.next_setting(sun).datetime()
    
    now = datetime.now()
    is_night = now < sunrise or now > sunset
    
    # 晚上自动降低音量
    if is_night:
        controller.set_volume("所有房间", 30)
    else:
        controller.set_volume("所有房间", 60)

# 定时任务
from apscheduler.schedulers.background import BackgroundScheduler

scheduler = BackgroundScheduler()
scheduler.add_job(check_sun_time, 'cron', hour='6,18')  # 每天6点和18点检查
scheduler.start()

常见问题解决

1. 指令响应延迟
  • 问题分析:语音识别、网络传输、指令处理等环节都可能引入延迟
  • 解决方案
    # 优化:使用本地缓存客户端ID
    def _get_client_ids(self, force_refresh: bool = False) -> Dict[str, str]:
        """获取所有客户端ID与名称映射,带缓存"""
        if not force_refresh and self.client_ids:
            return self.client_ids
    
        # 实际获取逻辑...
        return {}
    
    • 启用API请求缓存
    • 优化语音识别引擎的响应时间
    • 考虑使用MQTT等轻量级协议替代HTTP
2. 权限与安全问题
  • 解决方案
    • 使用HTTPS加密传输
    • 实现IP白名单
    • 添加指令频率限制
    # 添加请求频率限制
    from flask_limiter import Limiter
    from flask_limiter.util import get_remote_address
    
    limiter = Limiter(
        app,
        key_func=get_remote_address,
        default_limits=["200 per day", "50 per hour"]
    )
    
    # 为关键接口添加更严格的限制
    @app.route("/api/volume", methods=["POST"])
    @limiter.limit("10 per minute")
    def handle_volume():
        # 原有代码...
    
3. 多用户与设备冲突
  • 解决方案:实现会话管理与优先级机制
    # 记录最近活跃设备
    active_sessions = {}
    
    def update_session(device_id):
        active_sessions[device_id] = datetime.now()
    
        # 保留最近5个会话
        if len(active_sessions) > 5:
            oldest = min(active_sessions.items(), key=lambda x: x[1])
            del active_sessions[oldest[0]]
    

总结与未来展望

通过本文介绍的方法,我们成功构建了Snapcast与语音助手的集成方案,实现了用语音指令控制音频系统的目标。从技术架构上,我们采用了"语音助手→中间服务→Snapcast控制接口"的三层架构,既保证了系统的灵活性,又降低了直接修改Snapcast源码的复杂度。

项目扩展方向

  1. 自定义唤醒词:集成Snowboy等离线唤醒词引擎
  2. 多语言支持:扩展语音识别与合成支持多语言
  3. 情感识别:根据语音语调调整音乐风格
  4. 本地LLM集成:使用开源大语言模型处理更复杂的自然语言指令

最终部署清单

  •  Snapserver控制接口已启用并测试通过
  •  中间服务已部署并配置自动启动
  •  语音助手技能已创建并发布
  •  HTTPS与令牌认证已配置
  •  基础指令(音量、播放/暂停、切换流)测试通过
  •  自动化场景已按需求配置
  •  系统日志与监控已设置

现在,你可以彻底告别手动操作的繁琐,用自然的语音指令掌控整个多房间音频系统。无论是清晨唤醒时的轻柔音乐,还是家庭聚会时的氛围营造,Snapcast与语音助手的完美结合都能为你带来更加智能、便捷的音频体验。

如果你在实施过程中遇到任何问题,或有更好的集成方案,欢迎在项目的GitHub仓库提交Issue或Pull Request,让我们共同完善这个开源生态系统。

附录:项目资源与代码

完整代码已开源,可通过以下方式获取:

git clone https://gitcode.com/gh_mirrors/sn/snapcast.git
cd snapcast/control/voice-assistant

项目结构说明:

voice-assistant/
├── snapcast_controller.py  # Snapcast控制API封装
├── voice_processor.py      # 语音识别与指令解析
├── app.py                  # Web服务与API接口
├── dialogflow_webhook.py   # Google Assistant集成
├── alexa_lambda.py         # Amazon Alexa集成
├── requirements.txt        # 依赖列表
└── README.md               # 部署指南

依赖安装:

pip install -r requirements.txt

启动服务:

python app.py

如果觉得本文对你有帮助,请点赞、收藏并关注作者,获取更多智能家居与开源项目的技术分享。下期预告:《Snapcast高级主题:低延迟音频同步与音质优化》

【免费下载链接】snapcast Synchronous multiroom audio player 【免费下载链接】snapcast 项目地址: https://gitcode.com/gh_mirrors/sn/snapcast

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐