first pass of tts and audio stuff

This commit is contained in:
legop3
2025-11-27 16:26:21 -05:00
parent 871e04a8ba
commit 9289fb3467
20 changed files with 425 additions and 60 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-A0W3Zfns.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DOoEkJaz.css">
<script type="module" crossorigin src="/assets/index-Ch0OrDUK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BxUpjkzh.css">
</head>
<body>
<div id="root"></div>
+44 -2
View File
@@ -6,6 +6,7 @@ const { getRole } = require('./roleService');
const { describeAssignment } = require('./assignmentService');
const roverManager = require('./roverManager');
const { getNickname } = require('./nicknameService');
const { issueCommand } = require('./commandService');
const RATE_LIMIT_WINDOW_MS = 8000;
const RATE_LIMIT_MAX = 5;
@@ -66,6 +67,7 @@ function buildMessage(socket, text, meta = {}) {
roverId,
fromDiscord: Boolean(meta.fromDiscord),
text,
tts: meta.tts || null,
};
}
@@ -73,7 +75,20 @@ function broadcastMessage(message) {
publishEvent({ source: 'chat', type: 'chat:message', payload: message });
}
function handleIncoming({ text } = {}, socket, cb = () => {}) {
function normalizeTtsOptions(raw = {}) {
if (!raw || typeof raw !== 'object') return null;
const speak = raw.speak !== false;
if (!speak) return null;
const engine = typeof raw.engine === 'string' && raw.engine.toLowerCase() === 'espeak' ? 'espeak' : 'flite';
const voice = typeof raw.voice === 'string' ? raw.voice.trim() : undefined;
let pitch = Number.isFinite(raw.pitch) ? Math.round(raw.pitch) : undefined;
if (typeof pitch === 'number') {
pitch = Math.max(0, Math.min(99, pitch));
}
return { speak, engine, voice, pitch };
}
function handleIncoming({ text, tts } = {}, socket, cb = () => {}) {
const role = getRole(socket);
// if (role === 'spectator') {
// cb({ error: 'Spectators cannot chat' });
@@ -104,12 +119,39 @@ function handleIncoming({ text } = {}, socket, cb = () => {}) {
cb({ error: 'Message looks like spam' });
return;
}
const message = buildMessage(socket, clean, { fromDiscord: false });
const roverId = resolveRoverId(socket?.id);
const ttsOptions = normalizeTtsOptions(tts);
const message = buildMessage(socket, clean, { fromDiscord: false, roverId, tts: ttsOptions });
logger.info('Chat message', { socket: socket.id, roverId: message.roverId });
broadcastMessage(message);
maybeSpeak(socket, message, ttsOptions);
cb({ success: true });
}
function maybeSpeak(socket, message, ttsOptions) {
if (!ttsOptions || !message?.roverId) return;
const record = roverManager.rovers.get(message.roverId);
const audio = record?.meta?.audio || {};
const ttsEnabled = Boolean(audio.ttsEnabled);
if (!ttsEnabled) return;
if (!roverManager.canDrive(message.roverId, socket)) return;
try {
issueCommand(message.roverId, {
type: 'tts',
tts: {
text: message.text,
engine: ttsOptions.engine,
voice: ttsOptions.voice,
pitch: ttsOptions.pitch,
speak: true,
},
});
logger.info('TTS sent', { roverId: message.roverId, engine: ttsOptions.engine, socket: socket.id });
} catch (err) {
logger.warn('TTS send failed', { roverId: message.roverId, error: err.message });
}
}
function sendExternalMessage({ text, nickname = 'Discord', role = 'admin', roverId = null }) {
const clean = typeof text === 'string' ? text.trim() : '';
if (!clean || clean.length > 256) {
+1
View File
@@ -118,6 +118,7 @@ function getRoster() {
maxWheelSpeed: record.meta?.maxWheelSpeed,
media: record.meta?.media,
cameraServo: record.meta?.cameraServo,
audio: record.meta?.audio,
locked: record.locked,
lockReason: record.lockReason,
lastSeen: record.lastSeen,