Skip to content

Commit 1d3479e

Browse files
committed
fix(voice,server): orpheus TTS default + auto-port-fallback; bump to 1.0.0-alpha.2
- Voice: replace deprecated `playai-tts-english` default with `canopylabs/orpheus-v1-english` (PlayAI was sunset 2025-12-23 on Groq). Default voice updated to `troy` to match Orpheus's accepted set [autumn, diana, hannah, austin, daniel, troy]. Fixes 404 "model_not_found" on voice worker startup. - GroqTTS: drop dead playai entries from sample-rate / voice maps. - providers.ts: register orpheus-v1-english + orpheus-arabic-saudi as the Groq TTS choices in the model picker. - start.ts: on EADDRINUSE bind to port 0 (random) instead of crashing, and propagate the actual bound port to tunnel.start, banner URL, and autoOpen so the printed URL matches the real port. - Bump package.json to 1.0.0-alpha.2 so the next Publish workflow run ships these fixes and re-points npm `latest` at the alpha (publish.yml already promotes whatever it publishes to latest).
1 parent dbc5629 commit 1d3479e

6 files changed

Lines changed: 81 additions & 46 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "daemora",
3-
"version": "1.0.0-alpha.1",
3+
"version": "1.0.0-alpha.2",
44
"description": "Self-hosted AI agent platform — autonomous, multi-channel, multi-model.",
55
"type": "module",
66
"license": "AGPL-3.0-or-later",

src/cli/commands/start.ts

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
*/
77

88
import { exec } from "node:child_process";
9+
import type { Server } from "node:http";
10+
11+
import type { Express } from "express";
912

1013
import { ChannelManager } from "../../channels/ChannelManager.js";
1114
import { ChannelRegistry } from "../../channels/ChannelRegistry.js";
@@ -374,15 +377,6 @@ export async function startCommand(): Promise<void> {
374377
let publicUrl = configuredPublic.length > 0
375378
? configuredPublic.replace(/\/$/, "")
376379
: `http://localhost:${cfg.env.port}`;
377-
if (!configuredPublic) {
378-
tunnel.start({ port: cfg.env.port }).then((res) => {
379-
if (res.kind !== "none") {
380-
publicUrl = res.url;
381-
log.info({ kind: res.kind, url: res.url }, "public URL updated from tunnel");
382-
}
383-
}).catch((e) => log.warn({ err: (e as Error).message }, "tunnel start crashed"));
384-
}
385-
log.info({ authEnabled, publicUrl }, "auth + webhooks ready");
386380

387381
const app = createApp({
388382
cfg, agent, runner, sessions, memory, declarativeMemory,
@@ -398,19 +392,42 @@ export async function startCommand(): Promise<void> {
398392
customSkillsDir,
399393
});
400394

401-
const server = app.listen(cfg.env.port, () => {
402-
const url = `http://localhost:${cfg.env.port}`;
403-
const banner = box([
404-
`Daemora-TS running at`,
405-
` ${url}`,
406-
cfg.vault.exists() ? "Vault detected — unlock from the UI to start chatting." : "Open the URL above to set up your first provider.",
407-
]);
408-
console.log("\n" + banner + "\n");
409-
410-
if (process.stdout.isTTY && !cfg.env.daemonMode && process.env["DAEMORA_NO_OPEN"] !== "1") {
411-
autoOpen(url);
412-
}
413-
});
395+
// Bind the HTTP server. If the configured port is already in use,
396+
// fall back to an OS-assigned random port so `daemora start` never
397+
// hard-fails just because something else is on 8081. The actual bound
398+
// port is then threaded through tunnel/banner/autoOpen so the URL
399+
// printed to the user matches reality.
400+
const server = await listenWithFallback(app, cfg.env.port, log);
401+
const addr = server.address();
402+
const boundPort = typeof addr === "object" && addr ? addr.port : cfg.env.port;
403+
if (boundPort !== cfg.env.port) {
404+
log.warn({ requested: cfg.env.port, bound: boundPort }, "configured port in use — fell back to random port");
405+
}
406+
if (configuredPublic.length === 0) {
407+
publicUrl = `http://localhost:${boundPort}`;
408+
}
409+
410+
if (!configuredPublic) {
411+
tunnel.start({ port: boundPort }).then((res) => {
412+
if (res.kind !== "none") {
413+
publicUrl = res.url;
414+
log.info({ kind: res.kind, url: res.url }, "public URL updated from tunnel");
415+
}
416+
}).catch((e) => log.warn({ err: (e as Error).message }, "tunnel start crashed"));
417+
}
418+
log.info({ authEnabled, publicUrl, port: boundPort }, "auth + webhooks ready");
419+
420+
const url = `http://localhost:${boundPort}`;
421+
const banner = box([
422+
`Daemora-TS running at`,
423+
` ${url}`,
424+
cfg.vault.exists() ? "Vault detected — unlock from the UI to start chatting." : "Open the URL above to set up your first provider.",
425+
]);
426+
console.log("\n" + banner + "\n");
427+
428+
if (process.stdout.isTTY && !cfg.env.daemonMode && process.env["DAEMORA_NO_OPEN"] !== "1") {
429+
autoOpen(url);
430+
}
414431

415432
const shutdown = (signal: NodeJS.Signals) => {
416433
log.info({ signal }, "shutting down");
@@ -461,6 +478,29 @@ function box(lines: readonly string[]): string {
461478
return `${top}\n${middle}\n${bot}`;
462479
}
463480

481+
/**
482+
* Bind the HTTP server to `port`. If that port is already in use,
483+
* fall back to port 0 (OS-assigned random) instead of crashing —
484+
* caller reads `server.address().port` to discover the actual port.
485+
* Any other listen error (EACCES, etc.) is propagated as-is.
486+
*/
487+
function listenWithFallback(app: Express, port: number, log: { warn: (o: object, m: string) => void }): Promise<Server> {
488+
return new Promise<Server>((resolve, reject) => {
489+
const server = app.listen(port);
490+
server.once("listening", () => resolve(server));
491+
server.once("error", (err: NodeJS.ErrnoException) => {
492+
if (err.code !== "EADDRINUSE") {
493+
reject(err);
494+
return;
495+
}
496+
log.warn({ port, err: err.message }, "port in use — retrying on a random port");
497+
const fallback = app.listen(0);
498+
fallback.once("listening", () => resolve(fallback));
499+
fallback.once("error", reject);
500+
});
501+
});
502+
}
503+
464504
function autoOpen(url: string): void {
465505
const cmd =
466506
process.platform === "darwin" ? `open "${url}"` :

src/models/providers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,8 @@ export const PROVIDER_CATALOG: readonly ProviderDef[] = [
266266
{ id: "whisper-large-v3-turbo", name: "Whisper v3 Turbo", tier: "fast" },
267267
],
268268
ttsModels: [
269-
{ id: "playai-tts", name: "Orpheus TTS", tier: "fast" },
269+
{ id: "canopylabs/orpheus-v1-english", name: "Orpheus v1 English", tier: "fast" },
270+
{ id: "canopylabs/orpheus-arabic-saudi", name: "Orpheus Arabic (Saudi)", tier: "fast" },
270271
],
271272
ttsVoices: [
272273
{ id: "troy", name: "Troy" }, { id: "hannah", name: "Hannah" },

src/voice/GroqTTS.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,13 @@ import { type APIConnectOptions, AudioByteStream, tts } from "@livekit/agents";
2121

2222
/**
2323
* Default per-model sample rates. Groq's `/v1/audio/speech` returns
24-
* WAV at different rates depending on the model — PlayAI is 48 kHz,
25-
* Orpheus is 24 kHz. If LiveKit is told the wrong rate, playback runs
26-
* at the wrong speed (24 kHz played as 48 kHz = double-speed chipmunk).
24+
* WAV at 24 kHz for the current Orpheus models. (PlayAI was 48 kHz but
25+
* was deprecated 2025-12-23.) If LiveKit is told the wrong rate, playback
26+
* runs at the wrong speed (24 kHz played as 48 kHz = chipmunk).
2727
*/
2828
const SAMPLE_RATE_BY_MODEL: Record<string, number> = {
29-
"playai-tts": 48_000,
30-
"playai-tts-english": 48_000,
31-
"playai-tts-arabic": 48_000,
3229
"canopylabs/orpheus-v1-english": 24_000,
30+
"canopylabs/orpheus-arabic-saudi": 24_000,
3331
"orpheus-v1-english": 24_000,
3432
};
3533
const DEFAULT_SAMPLE_RATE = 24_000;
@@ -38,23 +36,15 @@ const WAV_HEADER_BYTES = 44;
3836

3937
/**
4038
* Default voice per Groq TTS model. Groq rejects requests without a voice
41-
* with `400 voice is required`, and the right voice depends on the model
42-
* family (PlayAI uses "Fritz-PlayAI" / "Celeste-PlayAI" / etc., Orpheus
43-
* uses "tara" / "leah" / etc.). Picking the wrong family also 400s, so a
44-
* single global default isn't safe — match by model.
39+
* with `400 voice is required`. Orpheus accepts only the six voices in
40+
* ORPHEUS_VOICES below — picking anything else 400s.
4541
*/
4642
const DEFAULT_VOICE_BY_MODEL: Record<string, string> = {
47-
"playai-tts": "Fritz-PlayAI",
48-
"playai-tts-english": "Fritz-PlayAI",
49-
"playai-tts-arabic": "Ahmad-PlayAI",
50-
// Groq Orpheus accepts only [autumn, diana, hannah, austin, daniel, troy]
51-
// — older "tara" / "leah" names from upstream Orpheus are rejected
52-
// (400 invalid_request_error). Picking `troy` as a neutral male voice;
53-
// override per-deploy via TTS_VOICE setting.
5443
"canopylabs/orpheus-v1-english": "troy",
44+
"canopylabs/orpheus-arabic-saudi": "troy",
5545
"orpheus-v1-english": "troy",
5646
};
57-
const FALLBACK_VOICE = "Fritz-PlayAI";
47+
const FALLBACK_VOICE = "troy";
5848

5949
/** Voices Groq's Orpheus model currently accepts. Used to validate TTS_VOICE. */
6050
const ORPHEUS_VOICES = new Set(["autumn", "diana", "hannah", "austin", "daniel", "troy"]);

src/voice/VoiceAgent.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,15 @@ async function buildTTS(): Promise<ttsNs.TTS> {
223223
// pipeline.
224224
const apiKey = process.env["GROQ_API_KEY"];
225225
if (!apiKey) throw new Error("GROQ_API_KEY not set");
226+
// Groq deprecated `playai-tts` on 2025-12-23; the current Groq TTS
227+
// model is `canopylabs/orpheus-v1-english` (Orpheus). Default voice
228+
// must be one of [autumn, diana, hannah, austin, daniel, troy] —
229+
// anything else 400s.
226230
const { GroqTTS } = await import("./GroqTTS.js");
227231
return new GroqTTS({
228232
apiKey,
229-
model: model ?? "playai-tts-english",
230-
voice: voice ?? "Fritz-PlayAI",
233+
model: model ?? "canopylabs/orpheus-v1-english",
234+
voice: voice ?? "troy",
231235
});
232236
}
233237
case "openai":

0 commit comments

Comments
 (0)