From 56af39e13f395b896b3318f0bc0f92b6267c7fbc Mon Sep 17 00:00:00 2001 From: Nickolay Kiselyov Date: Sat, 18 Jan 2025 03:37:46 +0400 Subject: [PATCH] Move voice conversion to ffmpeg on hass --- README.md | 1 - api/embassy/routers/devices.ts | 6 ++-- config/default.json | 3 +- config/schema.d.ts | 1 + deploy/Containerfile | 2 -- deploy/embassy-api/docker-compose.yml | 1 + services/hass.ts | 43 +++++++++------------------ utils/network.ts | 13 ++++++++ 8 files changed, 34 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 3d59f7a5..fb488e7d 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ The bot is hosted on a VPS located at gateway.hackem.cc. The service, embassy-ap Node v20.10.0 All main dependencies in the cloud and internal service are installed using npm i -[Deprecated] To convert the doorcam stream to jpg, you need to install ffmpeg on the bot service and add it to PATH. ## Local deployment diff --git a/api/embassy/routers/devices.ts b/api/embassy/routers/devices.ts index 6e2db0dd..f3668b4f 100644 --- a/api/embassy/routers/devices.ts +++ b/api/embassy/routers/devices.ts @@ -27,9 +27,9 @@ function linkMacPage(mac: string) { Press to set your mac to ${mac}\ + ":", + "-" + )}>Press to set your mac to ${mac}\ \ `; } diff --git a/config/default.json b/config/default.json index 8dfb8b9e..9df6a0c0 100644 --- a/config/default.json +++ b/config/default.json @@ -151,7 +151,8 @@ "entity": "media_player.lenovodash", "ttspath": "/api/services/script/announce_in_space", "playpath": "/api/services/media_player/play_media", - "stoppath": "/api/services/media_player/media_stop" + "stoppath": "/api/services/media_player/media_stop", + "voicepath": "/api/services/script/convert_voice_and_play" }, "browser": { "target": "aee0b5cb4cd6bef1303d9e2c4be1a22e", diff --git a/config/schema.d.ts b/config/schema.d.ts index fce1803f..90426645 100644 --- a/config/schema.d.ts +++ b/config/schema.d.ts @@ -220,6 +220,7 @@ export interface SpeakerConfig { ttspath: string; playpath: string; stoppath: string; + voicepath: string; } export interface DevicesConfig { diff --git a/deploy/Containerfile b/deploy/Containerfile index 7d2c1133..103a44c0 100644 --- a/deploy/Containerfile +++ b/deploy/Containerfile @@ -17,8 +17,6 @@ RUN npm run build FROM docker.io/library/node:lts-alpine WORKDIR /app -RUN apk add --update --no-cache ffmpeg - COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./ diff --git a/deploy/embassy-api/docker-compose.yml b/deploy/embassy-api/docker-compose.yml index 2c7ff908..9729b29d 100644 --- a/deploy/embassy-api/docker-compose.yml +++ b/deploy/embassy-api/docker-compose.yml @@ -10,6 +10,7 @@ services: - data-volume:/app/data/db - ./sec:/app/config/sec - ./static:/app/static + - /root/.ssh:/root/.ssh env_file: - .env restart: unless-stopped diff --git a/services/hass.ts b/services/hass.ts index 1737bf79..c70dc47f 100644 --- a/services/hass.ts +++ b/services/hass.ts @@ -1,15 +1,10 @@ -import path from "node:path"; -import { promises as fs } from "fs"; +import os from "os"; import config from "config"; import fetch, { Response } from "node-fetch"; import { CamConfig, EmbassyApiConfig } from "@config"; -import { getBufferFromResponse } from "@utils/network"; -import { downloadTmpFile } from "@utils/filesystem"; -import { convertMedia } from "@utils/media"; - -import { EmbassyBaseIP } from "./embassy"; +import { getBufferFromResponse, runSSHCommand } from "@utils/network"; // Configs const embassyApiConfig = config.get("embassy-api"); @@ -90,33 +85,23 @@ export async function sayInSpace(text: string): Promise { if (response.status !== 200) throw Error("Speaker request failed"); } -async function serveStaticFile(localPath: string, urlPath: string): Promise { - const staticRootPath = path.join(__dirname, "..", embassyApiConfig.service.static); - const staticFilePath = path.join(staticRootPath, urlPath); - - await fs.mkdir(path.parse(staticFilePath).dir, { recursive: true }); - await fs.copyFile(localPath, staticFilePath); - - return `${EmbassyBaseIP}/${urlPath}`; -} - export async function playInSpace(link: string): Promise { - const requiresConversion = link.endsWith(".oga"); - - let linkToPlay = link; - - if (requiresConversion) { - const { tmpPath, cleanup } = await downloadTmpFile(link, ".oga"); - const convertedFilePath = await convertMedia(tmpPath, "mp3"); - - linkToPlay = await serveStaticFile(convertedFilePath, `/tmp/${Date.now()}.mp3`); - - cleanup(); + if (link.endsWith(".oga")) { + // ignore errors, ffmpeg writes to stderr + await runSSHCommand( + "hass.lan", + 22269, + "hassio", + os.homedir() + "/.ssh/hass", + `wget -O /media/tmp/voice.oga ${link}` + ).catch(() => null); + await postToHass(embassyApiConfig.speaker.voicepath, {}); + return; } const response = await postToHass(embassyApiConfig.speaker.playpath, { entity_id: embassyApiConfig.speaker.entity, - media_content_id: linkToPlay, + media_content_id: link, media_content_type: "music", }); diff --git a/utils/network.ts b/utils/network.ts index bff02939..7e026ad5 100644 --- a/utils/network.ts +++ b/utils/network.ts @@ -104,6 +104,19 @@ export async function arp(ip: string, networkRange: string): Promise { return mac; } +export function runSSHCommand(host: string, port: number, username: string, key: string, command: string) { + const ssh = new NodeSSH(); + + return ssh + .connect({ + host, + username, + port, + privateKeyPath: key, + }) + .then(() => ssh.exec(command, [""]).finally(() => ssh.dispose())); +} + export class NeworkDevicesLocator { static async getDevicesFromKeenetic(routerip: string, username: string, password: string) { const ssh = new NodeSSH();