TTs whisper
This commit is contained in:
9
backend/.env.example
Normal file
9
backend/.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# Raspberry Pi connection config
|
||||
RASPBERRY_PI_HOST=http://raspberrypi.local
|
||||
RASPBERRY_PI_PORT=8000
|
||||
|
||||
# Auth token for API endpoints
|
||||
QUIBOT_TOKEN=MY_SECRET_TOKEN
|
||||
|
||||
# Backend server config
|
||||
PORT=3000
|
||||
6
backend/.gitignore
vendored
6
backend/.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
__pycache__/
|
||||
venv/
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
|
||||
210
backend/main.py
210
backend/main.py
@@ -1,210 +0,0 @@
|
||||
from fastapi import FastAPI, File, Form, UploadFile, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel
|
||||
import RPi.GPIO as GPIO
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
INCOMING_DIR = Path("/tmp/quibot-audio/incoming")
|
||||
LOCKED_DIR = Path("/tmp/quibot-audio/locked")
|
||||
PROCESSED_DIR = Path("/tmp/quibot-audio/processed")
|
||||
INCOMING_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LOCKED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# -------------------------
|
||||
# GPIO SETUP
|
||||
# -------------------------
|
||||
STEP = 23
|
||||
DIR = 24
|
||||
EN = 25
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(STEP, GPIO.OUT)
|
||||
GPIO.setup(DIR, GPIO.OUT)
|
||||
GPIO.setup(EN, GPIO.OUT)
|
||||
|
||||
GPIO.output(EN, GPIO.LOW)
|
||||
|
||||
|
||||
motor_thread = None
|
||||
|
||||
def step_motor(steps, direction, delay=0.001):
|
||||
GPIO.output(DIR, direction)
|
||||
|
||||
for _ in range(steps):
|
||||
GPIO.output(STEP, GPIO.HIGH)
|
||||
time.sleep(delay)
|
||||
GPIO.output(STEP, GPIO.LOW)
|
||||
time.sleep(delay)
|
||||
|
||||
def motor_step(dir):
|
||||
dir_pin = GPIO.HIGH if dir == "forward" else GPIO.LOW
|
||||
time.sleep(0.02) # small delay before starting
|
||||
print("Motor running...")
|
||||
step_motor(200, dir_pin, 0.001)
|
||||
|
||||
|
||||
# -------------------------
|
||||
# SAFE COMMAND WHITELIST
|
||||
# -------------------------
|
||||
COMMANDS = {
|
||||
"restart_nginx": ["sudo", "systemctl", "restart", "nginx"],
|
||||
"uptime": ["uptime"],
|
||||
"update": ["sudo", "apt", "update"]
|
||||
}
|
||||
|
||||
|
||||
# -------------------------
|
||||
# API ENDPOINTS
|
||||
# -------------------------
|
||||
|
||||
@app.post("/run")
|
||||
def run_task(task: str, token: str):
|
||||
if token != "MY_SECRET_TOKEN":
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
if task not in COMMANDS:
|
||||
raise HTTPException(status_code=400, detail="Invalid task")
|
||||
|
||||
try:
|
||||
result = subprocess.check_output(COMMANDS[task], text=True)
|
||||
return {"output": result}
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {"error": e.output}
|
||||
|
||||
|
||||
@app.post("/motor/step/forward")
|
||||
def start_motor(token: str):
|
||||
global motor_thread
|
||||
|
||||
if token != "MY_SECRET_TOKEN":
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
|
||||
motor_thread = threading.Thread(target=motor_step, args=("forward",), daemon=True)
|
||||
motor_thread.start()
|
||||
|
||||
return {"status": "motor started"}
|
||||
|
||||
@app.post("/motor/step/backwards")
|
||||
def start_motor(token: str):
|
||||
global motor_thread
|
||||
|
||||
if token != "MY_SECRET_TOKEN":
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
|
||||
motor_thread = threading.Thread(target=motor_step, args=("backwards",), daemon=True)
|
||||
motor_thread.start()
|
||||
|
||||
return {"status": "motor started"}
|
||||
|
||||
@app.post("/motor/stop")
|
||||
def stop_motor(token: str):
|
||||
if token != "MY_SECRET_TOKEN":
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
GPIO.output(EN, GPIO.HIGH) # disable driver
|
||||
|
||||
return {"status": "motor stopped"}
|
||||
|
||||
|
||||
@app.post("/audio/upload")
|
||||
async def upload_audio(file: UploadFile = File(...), format: str = "wav"):
|
||||
raw_content = await file.read()
|
||||
|
||||
checksum = hashlib.sha256(raw_content).hexdigest()[:16]
|
||||
filename = f"{checksum[:10]}-{uuid.uuid4().hex[:8]}.wav"
|
||||
|
||||
filepath = INCOMING_DIR / filename
|
||||
filepath.write_bytes(raw_content)
|
||||
|
||||
return {"status": "received", "filename": str(filepath), "lock_url": f"/audio/lock/{filepath.name}"}
|
||||
|
||||
|
||||
@app.get("/audio/incoming")
|
||||
def list_incoming():
|
||||
files = []
|
||||
for f in sorted(INCOMING_DIR.iterdir()):
|
||||
meta = f.stat()
|
||||
files.append({
|
||||
"filename": f.name,
|
||||
"size_bytes": meta.st_size,
|
||||
"modified_iso": time.ctime(meta.st_mtime),
|
||||
})
|
||||
return {"count": len(files), "files": files}
|
||||
|
||||
|
||||
@app.post("/audio/lock/{filename}")
|
||||
def lock_audio(filename: str):
|
||||
src = INCOMING_DIR / filename
|
||||
dst = LOCKED_DIR / filename
|
||||
|
||||
if not src.exists():
|
||||
raise HTTPException(status_code=404, detail=f"File {filename} not found")
|
||||
|
||||
if dst.exists():
|
||||
return {"status": "already_locked", "filename": filename}
|
||||
|
||||
os.rename(str(src), str(dst))
|
||||
return {"status": "locked", "filename": filename}
|
||||
|
||||
|
||||
@app.post("/audio/unlock/{filename}")
|
||||
def unlock_audio(filename: str):
|
||||
src = LOCKED_DIR / filename
|
||||
dst = INCOMING_DIR / filename
|
||||
|
||||
if not src.exists():
|
||||
raise HTTPException(status_code=404, detail=f"File {filename} not found")
|
||||
|
||||
os.rename(str(src), str(dst))
|
||||
return {"status": "unlocked", "filename": filename}
|
||||
|
||||
|
||||
@app.post("/audio/cancel/{filename}")
|
||||
def cancel_audio(filename: str):
|
||||
src = LOCKED_DIR / filename
|
||||
dst = INCOMING_DIR / filename
|
||||
|
||||
if not src.exists():
|
||||
raise HTTPException(status_code=404, detail=f"File {filename} not found")
|
||||
|
||||
os.rename(str(src), str(dst))
|
||||
return {"status": "cancelled", "filename": filename}
|
||||
|
||||
|
||||
@app.post("/audio/process/{filename}")
|
||||
def process_audio(filename: str):
|
||||
locked = LOCKED_DIR / filename
|
||||
processed = PROCESSED_DIR / filename
|
||||
|
||||
if not locked.exists():
|
||||
raise HTTPException(status_code=404, detail=f"File {filename} not found")
|
||||
|
||||
os.rename(str(locked), str(processed))
|
||||
return {"status": "processed", "filename": filename}
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
def shutdown():
|
||||
global motor_running
|
||||
motor_running = False
|
||||
GPIO.output(EN, GPIO.HIGH)
|
||||
GPIO.cleanup()
|
||||
1919
backend/package-lock.json
generated
Normal file
1919
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
backend/package.json
Normal file
28
backend/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "quibot-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "QuiBot robot controller backend - runs on local laptop",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"openai": "^6.44.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.19.21",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
1
backend/quibot-audio-1781783002989.txt
Normal file
1
backend/quibot-audio-1781783002989.txt
Normal file
@@ -0,0 +1 @@
|
||||
Col·la, pítalo, la ola, ola.
|
||||
1
backend/quibot-audio-1781783032108.txt
Normal file
1
backend/quibot-audio-1781783032108.txt
Normal file
@@ -0,0 +1 @@
|
||||
Hola, què tal, hola, hola, hola, hola...
|
||||
1
backend/quibot-audio-1781783047628.txt
Normal file
1
backend/quibot-audio-1781783047628.txt
Normal file
@@ -0,0 +1 @@
|
||||
Hola, que tal, bon dia.
|
||||
34
backend/src/config.ts
Normal file
34
backend/src/config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
let _raspberryHost = process.env.RASPBERRY_PI_HOST ?? 'http://raspberrypi.local';
|
||||
let _raspberryPort = Number(process.env.RASPBERRY_PI_PORT) || 8000;
|
||||
let _token = process.env.QUIBOT_TOKEN ?? 'MY_SECRET_TOKEN';
|
||||
const APP_PORT = Number(process.env.PORT) || 5000;
|
||||
|
||||
export const getRaspberryHost = () => _raspberryHost;
|
||||
export const getRaspberryPort = () => _raspberryPort;
|
||||
export const getToken = () => _token;
|
||||
|
||||
export function setRaspberryHost(host: string) {
|
||||
_raspberryHost = host;
|
||||
}
|
||||
|
||||
export function setRaspberryPort(port: number) {
|
||||
_raspberryPort = port;
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
_token = token;
|
||||
}
|
||||
|
||||
export const getConfig = () => ({
|
||||
raspberryPi: {
|
||||
host: getRaspberryHost(),
|
||||
port: getRaspberryPort(),
|
||||
},
|
||||
token: getToken(),
|
||||
});
|
||||
|
||||
export const getAppPort = () => APP_PORT;
|
||||
119
backend/src/controllers/audio.controller.ts
Normal file
119
backend/src/controllers/audio.controller.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { execFile } from 'child_process';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { writeFile, unlink } from 'fs';
|
||||
import { raspiService } from '../services/raspi.service.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const writeFileAsync = promisify(writeFile);
|
||||
const unlinkAsync = promisify(unlink);
|
||||
|
||||
const router = Router();
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
router.get('/incoming', async (_req, res) => {
|
||||
try {
|
||||
const result = await raspiService.listIncomingAudio();
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `List incoming failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/lock/:filename', async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const result = await raspiService.lockAudio({ filename });
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Lock audio failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/unlock/:filename', async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const result = await raspiService.unlockAudio({ filename });
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Unlock audio failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/cancel/:filename', async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const result = await raspiService.cancelAudio({ filename });
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Cancel audio failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/process/:filename', async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const result = await raspiService.processAudio({ filename });
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Process audio failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
const whisperModel = process.env.WHISPER_MODEL ?? 'base';
|
||||
const whisperLanguage = process.env.WHISPER_LANGUAGE ?? 'ca';
|
||||
|
||||
router.post('/upload', upload.single('file'), async (req, res) => {
|
||||
let tmpFile: string | undefined;
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No audio file provided' });
|
||||
}
|
||||
|
||||
const ext = req.file.originalname.split('.').pop()?.toLowerCase() || 'wav';
|
||||
tmpFile = join(tmpdir(), `quibot-audio-${Date.now()}.${ext}`);
|
||||
await writeFileAsync(tmpFile, req.file.buffer);
|
||||
|
||||
console.log(`[whisper] Model: ${whisperModel}, Language: ${whisperLanguage}, File: ${tmpFile}`);
|
||||
|
||||
const { stdout, stderr } = await execFileAsync('whisper', [
|
||||
tmpFile,
|
||||
'--model', whisperModel,
|
||||
'--language', whisperLanguage,
|
||||
'--output_format', 'txt',
|
||||
], { maxBuffer: 50 * 1024 * 1024 });
|
||||
|
||||
if (stderr) {
|
||||
console.log(`[whisper] stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
const transcription = stdout.trim();
|
||||
|
||||
res.json({
|
||||
transcription,
|
||||
originalFilename: req.file.originalname,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Audio transcription failed: ${message}` });
|
||||
} finally {
|
||||
if (tmpFile) {
|
||||
try {
|
||||
await unlinkAsync(tmpFile);
|
||||
} catch {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
20
backend/src/controllers/command.controller.ts
Normal file
20
backend/src/controllers/command.controller.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Router } from 'express';
|
||||
import { raspiService } from '../services/raspi.service.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const { task } = req.body;
|
||||
if (!task) {
|
||||
return res.status(400).json({ error: 'Task name is required' });
|
||||
}
|
||||
try {
|
||||
const result = await raspiService.runTask({ task });
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Run task failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
53
backend/src/controllers/motor.controller.ts
Normal file
53
backend/src/controllers/motor.controller.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { raspiService } from '../services/raspi.service.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Multer config - store in memory for proxying to RasPi
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
router.post('/step/forward', async (_req, res) => {
|
||||
try {
|
||||
const result = await raspiService.motorStepForward();
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Motor step forward failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/step/backward', async (_req, res) => {
|
||||
try {
|
||||
const result = await raspiService.motorStepBackward();
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Motor step backward failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/stop', async (_req, res) => {
|
||||
try {
|
||||
const result = await raspiService.motorStop();
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Motor stop failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/upload', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No audio file provided' });
|
||||
}
|
||||
const result = await raspiService.uploadAudio(req.file.buffer, req.file.originalname);
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: `Audio upload failed: ${message}` });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
25
backend/src/controllers/settings.controller.ts
Normal file
25
backend/src/controllers/settings.controller.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Router } from 'express';
|
||||
import { getConfig, setRaspberryHost, setRaspberryPort, setToken } from '../config.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
const settings = getConfig();
|
||||
res.json(settings);
|
||||
});
|
||||
|
||||
router.put('/', (req, res) => {
|
||||
const { raspberryPi, token } = req.body;
|
||||
if (raspberryPi?.host !== undefined) {
|
||||
setRaspberryHost(raspberryPi.host);
|
||||
}
|
||||
if (raspberryPi?.port !== undefined) {
|
||||
setRaspberryPort(Number(raspberryPi.port));
|
||||
}
|
||||
if (token !== undefined) {
|
||||
setToken(token);
|
||||
}
|
||||
res.json(getConfig());
|
||||
});
|
||||
|
||||
export default router;
|
||||
25
backend/src/index.ts
Normal file
25
backend/src/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import router from './routes/router.js';
|
||||
import { getAppPort, getConfig } from './config.js';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Handle multipart in motor controller separately
|
||||
app.use('/audio', express.json());
|
||||
app.use('/motor', express.json());
|
||||
app.use('/commands', express.json());
|
||||
|
||||
app.use(router);
|
||||
|
||||
app.get('/health', (_req, res) => {
|
||||
const settings = getConfig();
|
||||
res.json({ status: 'ok', settings });
|
||||
});
|
||||
|
||||
app.listen(getAppPort(), () => {
|
||||
console.log(`QuiBot backend listening on port ${getAppPort()}`);
|
||||
});
|
||||
14
backend/src/routes/router.ts
Normal file
14
backend/src/routes/router.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import motorController from '../controllers/motor.controller.js';
|
||||
import audioController from '../controllers/audio.controller.js';
|
||||
import commandController from '../controllers/command.controller.js';
|
||||
import settingsController from '../controllers/settings.controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/motor', motorController);
|
||||
router.use('/audio', audioController);
|
||||
router.use('/commands', commandController);
|
||||
router.use('/settings', settingsController);
|
||||
|
||||
export default router;
|
||||
77
backend/src/services/raspi.service.ts
Normal file
77
backend/src/services/raspi.service.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import axios from 'axios';
|
||||
import { getRaspberryHost, getRaspberryPort, getToken } from '../config.js';
|
||||
|
||||
interface RunTaskParams {
|
||||
task: string;
|
||||
}
|
||||
|
||||
interface AudioLockParams {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export const raspiService = {
|
||||
async runTask(params: RunTaskParams) {
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/run`, null, {
|
||||
params: { task: params.task, token: getToken() },
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async motorStepForward() {
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/motor/step/forward`, null, {
|
||||
params: { token: getToken() },
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async motorStepBackward() {
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/motor/step/backwards`, null, {
|
||||
params: { token: getToken() },
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async motorStop() {
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/motor/stop`, null, {
|
||||
params: { token: getToken() },
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async uploadAudio(buffer: Buffer, filename?: string) {
|
||||
const fname = filename || 'audio.wav';
|
||||
const ext = fname.split('.').pop()?.toLowerCase() || 'wav';
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob([buffer]), fname);
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/audio/upload`, formData, {
|
||||
params: { format: ext },
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async listIncomingAudio() {
|
||||
const res = await axios.get(`${getRaspberryHost()}:${getRaspberryPort()}/audio/incoming`);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async lockAudio(params: AudioLockParams) {
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/audio/lock/${params.filename}`);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async unlockAudio(params: AudioLockParams) {
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/audio/unlock/${params.filename}`);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async cancelAudio(params: AudioLockParams) {
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/audio/cancel/${params.filename}`);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async processAudio(params: AudioLockParams) {
|
||||
const res = await axios.post(`${getRaspberryHost()}:${getRaspberryPort()}/audio/process/${params.filename}`);
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
20
backend/tsconfig.json
Normal file
20
backend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user