Skip to content

Commit

Permalink
add evi chat client
Browse files Browse the repository at this point in the history
  • Loading branch information
fern-bot committed Apr 23, 2024
1 parent 6252d81 commit b93412c
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/wrapper/HumeClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HumeClient as FernClient } from "../Client";
import { EmpathicVoice } from "./empathicVoice/EmpathicVoiceClient";
import { ExpressionMeasurement } from "./expressionMeasurement/ExpressionMeasurementClient";

export class HumeClient extends FernClient {
Expand All @@ -7,4 +8,10 @@ export class HumeClient extends FernClient {
public get expressionMeasurement(): ExpressionMeasurement {
return (this._expressionMeasurement ??= new ExpressionMeasurement(this._options));
}

protected _empathicVoice: EmpathicVoice | undefined;

public get empathicVoice(): EmpathicVoice {
return (this._empathicVoice ??= new EmpathicVoice(this._options));
}
}
File renamed without changes.
11 changes: 11 additions & 0 deletions src/wrapper/empathicVoice/EmpathicVoiceClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { EmpathicVoice as FernClient } from "../../api/resources/empathicVoice/client/Client";
import { ChatClient } from "./chat/ChatClient";

export class EmpathicVoice extends FernClient {

protected _chat: ChatClient | undefined;

public get chat(): ChatClient {
return (this._chat ??= new ChatClient(this._options));
}
}
146 changes: 146 additions & 0 deletions src/wrapper/empathicVoice/chat/ChatClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import * as Hume from "../../../api";
import * as serializers from "../../../serialization";
import * as core from "../../../core";
import * as errors from "../../../errors";
import qs from "qs";
import WebSocket from "ws";
import { base64Encode } from "../../base64Encode";
import { StreamSocket } from "./StreamSocket";

export declare namespace ChatClient {
interface Options {
apiKey?: core.Supplier<string | undefined>;
clientSecret?: core.Supplier<string | undefined>;
}

interface ConnectArgs {
/** The ID of the configuration. */
configId: string;

/** The version of the configuration. */
configVersion: string;

onOpen?: (event: WebSocket.Event) => void;
onMessage?: (message: Hume.empathicVoice.SubscribeEvent) => void;
onError?: (error: Hume.empathicVoice.Error_) => void;
onClose?: (event: WebSocket.Event) => void;
}
}

export class ChatClient {
constructor(protected readonly _options: ChatClient.Options) {}

public async connect(args: ChatClient.ConnectArgs): Promise<StreamSocket> {
const queryParams: Record<string, string | string[] | object | object[]> = {};

queryParams["accessToken"] = await this.fetchAccessToken();
queryParams["apiKey"] = core.Supplier.get(this._options.apiKey);
queryParams["config_id"] = args.configId;
queryParams["config_id"] = args.configVersion;

const websocket = new WebSocket(`wss://api.hume.ai/v0/evi/chat${qs.stringify(queryParams)}`, {
timeout: 10
});

websocket.addEventListener("open", (event) => {
args.onOpen?.(event);
});

websocket.addEventListener("error", (e) => {
args.onError?.({
type: "error",
code: e.type,
message: e.message,
slug: "websocket-error"
});
});

websocket.addEventListener("message", async ({ data }) => {
parse(data, {
onMessage: args.onMessage,
onError: args.onError,
});
});

websocket.addEventListener("close", (event) => {
args.onClose?.(event);
});

return new StreamSocket({
websocket,
});
}


private async fetchAccessToken(): Promise<string> {
const apiKey = await core.Supplier.get(this._options.apiKey);
const clientSecret = await core.Supplier.get(this._options.clientSecret);

const authString = `${apiKey}:${clientSecret}`;
const encoded = base64Encode(authString);

const response = await core.fetcher({
url: `https://api.hume.ai/oauth2-cc/token`,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${encoded}`,
},
body: new URLSearchParams({
grant_type: "client_credentials",
}).toString(),
});

if (!response.ok) {
if (response.error.reason === "status-code") {
throw new errors.HumeError({
statusCode: response.error.statusCode,
body: response.error.body,
});
}

switch (response.error.reason) {
case "non-json":
throw new errors.HumeError({
statusCode: response.error.statusCode,
body: response.error.rawBody,
});
case "timeout":
throw new errors.HumeTimeoutError();
case "unknown":
throw new errors.HumeError({
message: response.error.errorMessage,
});
}
}

return (response.body as any).access_token as string;
};
}

export async function parse(
data: WebSocket.Data,
args: {
onMessage?: (message: Hume.empathicVoice.SubscribeEvent) => void;
onError?: (error: Hume.empathicVoice.Error_) => void;
} = {}
): Promise<Hume.empathicVoice.SubscribeEvent | undefined> {
const message = JSON.parse(data as string);

const parsedResponse = await serializers.empathicVoice.SubscribeEvent.parse(message, {
unrecognizedObjectKeys: "passthrough",
allowUnrecognizedUnionMembers: true,
allowUnrecognizedEnumValues: true,
breadcrumbsPrefix: ["response"],
});
if (parsedResponse.ok) {
args.onMessage?.(parsedResponse.value);

if (parsedResponse.value.type === "error") {
args.onError?.(parsedResponse.value);
}

return parsedResponse.value;
}
}

79 changes: 79 additions & 0 deletions src/wrapper/empathicVoice/chat/StreamSocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import WebSocket from "ws";
import * as Hume from "../../../api";
import * as serializers from "../../../serialization";

export declare namespace StreamSocket {
interface Args {
websocket: WebSocket;
}
}

export class StreamSocket {
public readonly websocket: WebSocket;

constructor({ websocket }: StreamSocket.Args) {
this.websocket = websocket;
}

/**
* Send audio input
**/
public async sendAudioInput(message: Hume.empathicVoice.AudioInput): Promise<void> {
await this.send(message);
}

/**
* Send session settings
*/
public async sendSessionSettings(message: Hume.empathicVoice.SessionSettings): Promise<void> {
await this.send(message);
}

/**
* Send text input
*/
public async sendTextInput(message: Hume.empathicVoice.TextInput): Promise<void> {
await this.send(message);
}

/**
*
* Send TTS input
*
*/
public async sendTtsInput(message: Hume.empathicVoice.TtsInput): Promise<void> {
await this.send(message);
}

/**
* Closes the underlying socket.
*/
public close(): void {
this.websocket.close();
}

private async send(
payload: Hume.empathicVoice.PublishEvent
): Promise<void> {
await this.tillSocketOpen();
const jsonPayload = await serializers.expressionMeasurement.StreamData.jsonOrThrow(payload, {
unrecognizedObjectKeys: "strip",
});
this.websocket.send(JSON.stringify(jsonPayload));
}

private async tillSocketOpen(): Promise<WebSocket> {
if (this.websocket.readyState === WebSocket.OPEN) {
return this.websocket;
}
return new Promise((resolve, reject) => {
this.websocket.addEventListener("open", () => {
resolve(this.websocket);
});

this.websocket.addEventListener("error", (event) => {
reject(event);
});
});
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import WebSocket from "ws";
import { v4 as uuid } from "uuid";
import { parse } from "./StreamingClient";
import { base64Encode } from "./base64Encode";
import { base64Encode } from "../../base64Encode";
import * as Hume from "../../../api";
import * as errors from "../../../errors";
import * as serializers from "../../../serialization";
Expand Down
18 changes: 18 additions & 0 deletions tests/expressionMeasurement/batch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { HumeClient } from "../../src/"

describe("Streaming Expression Measurement", () => {
it("Emotional Language Text", async () => {
const hume = new HumeClient({
apiKey: "<>"
});
const job = await hume.expressionMeasurement.batch.startInferenceJob({
models: {
face: {}
},
urls: ["https://hume-tutorials.s3.amazonaws.com/faces.zip"]
});
await job.awaitCompletion();
const predictions = await hume.expressionMeasurement.batch.getJobPredictions(job.jobId);
console.log(JSON.stringify(predictions, null, 2));
});
});
25 changes: 25 additions & 0 deletions tests/expressionMeasurement/streaming.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { HumeClient } from "../../src/"

const samples = [
"Mary had a little lamb,",
"Its fleece was white as snow.",
"Everywhere the child went,",
"The little lamb was sure to go."
];

describe("Streaming Expression Measurement", () => {
it.skip("Emotional Language Text", async () => {
const hume = new HumeClient({
apiKey: "<>"
});
const socket = hume.expressionMeasurement.stream.connect({
config: {
language: {}
}
})
for (const sample of samples) {
const result = await socket.sendText({ text: sample })
console.log(result)
}
}, 100000);
});

0 comments on commit b93412c

Please sign in to comment.