Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"name": "tts-cli",
"dependencies": {
"@clack/prompts": "^0.10.0",
"bun": "^1.2.2",
"clipanion": "^4.0.0-rc.4",
"debug": "^4.4.0",
"hume": "^0.10.3",
Expand All @@ -27,28 +26,6 @@

"@clack/prompts": ["@clack/[email protected]", "", { "dependencies": { "@clack/core": "0.4.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ=="],

"@oven/bun-darwin-aarch64": ["@oven/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wEUBqSD7KDUx6xcOlNs/lDcbzJYH9CEPVZzAaJ+F3Zp8hORQYJ07byidQ0YgNMc1QhJq4xLaClMDLga6u2abIQ=="],

"@oven/bun-darwin-x64": ["@oven/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-kBeKYLNAwQrcJ1HPcuDGo3PGEfDvzHbL5N5kiaui1+eluyCS2yJZvuy2ddg9b0vqQkgzNu7D1keiRLpdR2Wneg=="],

"@oven/bun-darwin-x64-baseline": ["@oven/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-oYqGnocQ7eY5wfthGivpXd0lzLYklXU4rEKXU4+oLaiFeFnY3lyszv85UHjywuB5bl7xybmSIE8tN8ie36WY1w=="],

"@oven/bun-linux-aarch64": ["@oven/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-hbi7ykhLAtoBZl7mox5Qg6xVy9FcnPNibo/h6A1oElXBl4pRaVGNFDsuDmb80f9sFf4yZL0vwYyb47hL89MiGw=="],

"@oven/bun-linux-aarch64-musl": ["@oven/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-nSB6FU29OW+8zUP5jRrQEAXl9m6Jd8vp5Swplh3prc0L0niYdtan39wouCxDVFNJafC0DaeKQme4UMQJ65s9qw=="],

"@oven/bun-linux-x64": ["@oven/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-oxubd+zJW8+1H02bwzne8lj2EtvgoRU6w245hWMtwjo15IET/YGc01ndFRd42FX02bwo926N+7Jm7NFzS8GOHQ=="],

"@oven/bun-linux-x64-baseline": ["@oven/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-PrkC18s81ImpHMdqgFXvyq9Oxed8mzlYb709DsuuoV9NL3nrzQzfqq8JStz/g3dYMc4obrljG/EKXGfMaJMJVA=="],

"@oven/bun-linux-x64-musl": ["@oven/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-481HyvBKKXrKH2VZsSBVsqsqxcnncqpsKz7qgTqnSEToCSyNqc3bGrI6pN6haI9nLawJpIh+oNXCg4JISUi6bg=="],

"@oven/bun-linux-x64-musl-baseline": ["@oven/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-1VsFwYMOryIsnTGPG70N1HAXoa8Fa7s+XAtP2Y010tao0omgpM0S6Go0Uc7VJ8g+yHKyIw79leRMwUs35ysQZA=="],

"@oven/bun-windows-x64": ["@oven/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-z3G7jft1hbiiX47rY666i3HFJZBqCLgddr7e0ccoE/kUk4U5IB08mqJZYW0vaaL8zy0wuEF2h5VkK0S/iryLWw=="],

"@oven/bun-windows-x64-baseline": ["@oven/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-pvtDgWRclz/bxLEWUAppj854MklfTu01DfHAvKzV1UAHbBOlurC8+StpE7EhITAHC/jBK9U39eVYnZNvumVU9w=="],

"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],

"@types/debug": ["@types/[email protected]", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
Expand All @@ -67,8 +44,6 @@

"buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],

"bun": ["[email protected]", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.2.3", "@oven/bun-darwin-x64": "1.2.3", "@oven/bun-darwin-x64-baseline": "1.2.3", "@oven/bun-linux-aarch64": "1.2.3", "@oven/bun-linux-aarch64-musl": "1.2.3", "@oven/bun-linux-x64": "1.2.3", "@oven/bun-linux-x64-baseline": "1.2.3", "@oven/bun-linux-x64-musl": "1.2.3", "@oven/bun-linux-x64-musl-baseline": "1.2.3", "@oven/bun-windows-x64": "1.2.3", "@oven/bun-windows-x64-baseline": "1.2.3" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bun.exe" } }, "sha512-i8F+I3wxmt9omnLh3lOg0APQtqxZaMU0x6xuC+9YH4Eg/7fu8SM4YbdwyvV8Z6sy3OMtGFB0SYOjU2STuId4FQ=="],

"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="],

"bundle-name": ["[email protected]", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
Expand Down
90 changes: 90 additions & 0 deletions src/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ApiKeyNotSetError, debug, getSettings, type CommonOpts } from './common';
import { withStdinAudioPlayer } from './play_audio';
import { spawn } from 'child_process';

export type ChatOpts = CommonOpts & {
configId: string;
};

export class Evi {
getSettings = getSettings;

private ffmpegArgs(): string[] {
if (process.platform === 'win32') {
return ['-f', 'dshow', '-i', 'audio=default'];
}
if (process.platform === 'darwin') {
return ['-f', 'avfoundation', '-i', ':0'];
}
return ['-f', 'alsa', '-i', 'default'];
}

async chat(opts: ChatOpts) {
const { hume, reporter } = await this.getSettings(opts);
if (!hume) {
throw new ApiKeyNotSetError();
}

reporter.info('Connecting to EVI...');
debug('Connecting with config %s', opts.configId);
const socket = hume.empathicVoice.chat.connect({
configId: opts.configId,
debug: !!opts.debug,
});

await withStdinAudioPlayer(null, async (writeAudio) => {
const rec = spawn('ffmpeg', [
...this.ffmpegArgs(),
'-ac',
'1',
'-ar',
'16000',
'-f',
's16le',
'-acodec',
'pcm_s16le',
'-',
], { stdio: ['ignore', 'pipe', 'ignore'] });

socket.on('message', (message) => {
debug('message: %s', message.type);
if (message.type === 'audio_output') {
const data = Buffer.from(message.data, 'base64');
writeAudio(data);
}
if (message.type === 'assistant_message') {
reporter.info(message.message.content || '');
}
});

socket.on('open', () => {
reporter.info('Connected');
socket.sendSessionSettings({
audio: { channels: 1, encoding: 'linear16', sampleRate: 16000 },
});
});

socket.on('close', () => {
reporter.info('Connection closed');
rec.kill('SIGINT');
});

socket.on('error', (err) => {
reporter.warn('Socket error: ' + err.message);
});

rec.stdout.on('data', (data: Buffer) => {
socket.sendAudioInput({ data: data.toString('base64') });
});

process.on('SIGINT', () => {
rec.kill('SIGINT');
socket.close();
process.exit();
});

await socket.tillSocketOpen();
await new Promise((resolve) => socket.on('close', resolve));
});
}
}
32 changes: 31 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ const usageDescriptions = {
'tts.streaming': 'Use streaming mode for TTS generation (default: true)',
'tts.instantMode':
'Enable ultra-low latency mode for significantly faster generation (requires streaming=true, a voice, and incurs 10% higher cost)',
'chat.configId': 'ID of the EVI configuration to use',
apiKey: 'Override the default API key',
json: 'Output in JSON format',
pretty: 'Output in human-readable format',
};
import { Tts } from './tts';
import * as t from 'typanion';
import { Voices } from './voices';
import { Evi } from './chat';
import {
configValidators,
endSession,
Expand Down Expand Up @@ -461,6 +463,31 @@ class TtsCommand extends Command {
}
}

class EviChatCommand extends Command {
static paths = [['chat']];
static usage = Command.Usage({
description: 'Start an EVI chat',
details: "Connects to Hume's Empathic Voice Interface using the provided configuration ID.",
examples: [['Start a chat', 'chat <config-id>']],
});

configId = Option.String({
required: true,
name: 'config-id',
description: usageDescriptions['chat.configId'],
});
apiKey = Option.String('--api-key', { description: usageDescriptions.apiKey });
baseUrl = Option.String('--base-url', {
description: 'Override the default API base URL (for testing purposes)',
});
debug = Option.Boolean('--debug');

async execute() {
const evi = new Evi();
await evi.chat(this);
}
}

// Root command that shows help by default
class RootCommand extends Command {
static paths = [Command.Default];
Expand All @@ -483,7 +510,9 @@ class HelpCommand extends Builtins.HelpCommand {
* \`hume voices list --help\` - List available voices

* \`hume voices delete --help\` - Delete a saved voice


* \`hume chat --help\` - Interact with EVI using your microphone

* \`hume session --help\` - Save settings temporarily so you don't have to repeat yourself

* \`hume config --help\` - Save settings more permanently
Expand All @@ -509,6 +538,7 @@ class LoginCommand extends Command {

cli.register(RootCommand);
cli.register(TtsCommand);
cli.register(EviChatCommand);
cli.register(LoginCommand);
cli.register(SessionRootCommand);
cli.register(SaveVoiceCommand);
Expand Down
Loading