Skip to content

Commit

Permalink
Add TTS support for chat messages
Browse files Browse the repository at this point in the history
  • Loading branch information
n4ze3m committed Apr 8, 2024
1 parent b92dce7 commit 189e81b
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 9 deletions.
7 changes: 6 additions & 1 deletion app/widget/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ function App() {
} else {
setMessages([
...messages,
{ isBot: true, message: botStyle.data.first_message, sources: [] },
{
isBot: true,
message: botStyle.data.first_message,
sources: [],
id: "first-message",
},
]);
}
}
Expand Down
48 changes: 48 additions & 0 deletions app/widget/src/components/BotChatBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { PlayIcon, StopIcon } from "@heroicons/react/20/solid";
import { Message } from "../store";
import { BotStyle } from "../utils/types";
import BotSource from "./BotSource";
import Markdown from "./Markdown";
import { useVoice } from "../hooks/useVoice";

export default function BotChatBubble({
message,
Expand All @@ -10,6 +12,7 @@ export default function BotChatBubble({
message: Message;
botStyle: BotStyle;
}) {
const { cancel, isPlaying, loading, speak } = useVoice();
return (
<div className="mt-2 flex flex-col">
<div
Expand Down Expand Up @@ -38,6 +41,51 @@ export default function BotChatBubble({
<p className="text-sm">
<Markdown message={message.message} />
</p>
{botStyle.data.tts && message.isBot && message.id !== "temp-id" && (
<div className=" mt-3">
<button
onClick={() => {
if (!isPlaying) {
speak({
id: message.id,
});
} else {
cancel();
}
}}
className="flex bg-white shadow-md items-center border justify-center w-6 h-6 rounded-full transition-colors duration-200"
>
{!loading ? (
!isPlaying ? (
<PlayIcon className="w-4 h-4 text-gray-400 group-hover:text-gray-500" />
) : (
<StopIcon className="w-4 h-4 text-red-400 group-hover:text-red-500" />
)
) : (
<svg
className="animate-spin h-5 w-5 text-gray-400 group-hover:text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)}
</button>
</div>
)}
</div>
<div className="flex flex-wrap items-start justify-start space-x-3">
{botStyle.data.show_reference &&
Expand Down
1 change: 1 addition & 0 deletions app/widget/src/components/BotHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function BotHeader({
setMessages([
{
message: botStyle?.data?.first_message,
id: "first-message",
isBot: true,
sources: [],
},
Expand Down
7 changes: 4 additions & 3 deletions app/widget/src/hooks/useMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { notification } from "antd";

export type BotResponse = {
bot: {
id: string;
chat_id: string;
text: string;
sourceDocuments: any[];
};
Expand Down Expand Up @@ -70,7 +70,7 @@ export const useMessage = () => {
});
const data = response.data as BotResponse;
newMessage[newMessage.length - 1].message = data.bot.text;
newMessage[newMessage.length - 1].id = data.bot.id;
newMessage[newMessage.length - 1].id = data.bot.chat_id;
newMessage[newMessage.length - 1].sources = data.bot.sourceDocuments;
localStorage.setItem("DS_MESSAGE", JSON.stringify(newMessage));
localStorage.setItem("DS_HISTORY", JSON.stringify(data.history));
Expand Down Expand Up @@ -160,10 +160,11 @@ export const useMessage = () => {
count++;
} else if (type === "result") {
const responseData = JSON.parse(message) as BotResponse;
console.log(responseData);
newMessage[appendingIndex].message = responseData.bot.text;
newMessage[appendingIndex].sources =
responseData.bot.sourceDocuments;
newMessage[appendingIndex].id = responseData.bot.id;
newMessage[appendingIndex].id = responseData.bot.chat_id;
localStorage.setItem("DS_MESSAGE", JSON.stringify(newMessage));
localStorage.setItem(
"DS_HISTORY",
Expand Down
70 changes: 70 additions & 0 deletions app/widget/src/hooks/useVoice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useEffect, useRef, useState } from "react";
import { notification } from "antd";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { getUrl } from "../utils/getUrl";

type VoiceOptions = {
id: string;
};

export const useVoice = () => {
const audioRef = useRef<HTMLAudioElement | null>(null);
const [isPlaying, setIsPlaying] = useState(false);

const onSubmit = async (values: { id: string }) => {
const response = await axios.post(`${getUrl().split("?")[0]}/tts`, values, {
responseType: "arraybuffer",
});
return response.data;
};

const { mutateAsync: generateAudio, isLoading } = useMutation(onSubmit);

const speak = async (args: VoiceOptions) => {
try {
const data = await generateAudio({
id: args.id,
});

const blob = new Blob([data], { type: "audio/mpeg" });
const url = window.URL.createObjectURL(blob);
audioRef.current = new Audio(url);

audioRef.current.onended = () => {
setIsPlaying(false);
};

audioRef.current.play();
setIsPlaying(true);
} catch (error) {
console.log(error);

notification.error({
message: "Error",
description: "Something went wrong while trying to play the audio",
});
}
};

const cancel = () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
setIsPlaying(false);
}
};

useEffect(() => {
return () => {
cancel();
};
}, []);

return {
speak,
cancel,
isPlaying,
loading: isLoading,
};
};
1 change: 1 addition & 0 deletions app/widget/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export type BotStyle = {
text_color?: string;
};
first_message: string;
tts: boolean;
};
};
2 changes: 1 addition & 1 deletion server/src/handlers/api/v1/bot/appearance/get.handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { GetBotAppearanceById } from "./types";
import { getElevenLabTTS, getOpenAITTS } from "../../../../../utils/elevenlabs";
import { getElevenLabTTS, getOpenAITTS } from "../../../../../utils/voice";

export const getBotAppearanceByIdHandler = async (
request: FastifyRequest<GetBotAppearanceById>,
Expand Down
2 changes: 1 addition & 1 deletion server/src/handlers/api/v1/bot/playground/get.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "./types";
import {
getElevenLab,
} from "../../../../../utils/elevenlabs";
} from "../../../../../utils/voice";

export async function getPlaygroundHistoryByBotId(
request: FastifyRequest<GetPlaygroundBotById>,
Expand Down
2 changes: 1 addition & 1 deletion server/src/handlers/api/v1/voice/post.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
isElevenLabAPIKeyPresent,
isElevenLabAPIValid,
textToSpeech,
} from "../../../../utils/elevenlabs";
} from "../../../../utils/voice";

export const voiceTTSHandler = async (
request: FastifyRequest<ElevenLabTypes>,
Expand Down
3 changes: 2 additions & 1 deletion server/src/handlers/bot/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./post.handler"
export * from "./get.handler"
export * from "./api.handler"
export * from "./api.handler"
export * from "./voice.handler"
9 changes: 9 additions & 0 deletions server/src/handlers/bot/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ export interface ChatAPIRequest {
};
}

export interface ChatTTSRequest {
Params: {
id: string;
};

Body: {
id: string;
};
}
100 changes: 100 additions & 0 deletions server/src/handlers/bot/voice.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { ChatTTSRequest } from "./types";
import {
isElevenLabAPIKeyPresent,
isElevenLabAPIValid,
isOpenAIAPIKeyPresent,
textToSpeech,
textToSpeechOpenAI,
} from "../../utils/voice";

export const chatTTSHandler = async (
request: FastifyRequest<ChatTTSRequest>,
reply: FastifyReply
) => {
const bot_id = request.params.id;
const prisma = request.server.prisma;
const chat_id = request.body.id;

const isBotExist = await prisma.bot.findFirst({
where: {
publicId: bot_id,
},
});

if (!isBotExist) {
return reply.status(404).send({
message: "Bot not found",
});
}

let text = "";

const botAppearance = await prisma.botAppearance.findFirst({
where: {
bot_id: isBotExist.id,
},
});

if (!botAppearance) {
return reply.status(404).send({
message: "Bot configuration not found",
});
}

if (chat_id === "first-message") {
text = botAppearance?.first_message! || "";
} else {
const chat = await prisma.botWebHistory.findFirst({
where: {
id: chat_id,
},
});

if (!chat) {
return reply.status(404).send({
message: "Chat not found",
});
}

text = chat.bot!;
}

if (!botAppearance.tts) {
return reply.status(404).send({
message: "TTS not enabled for this bot",
});
}
if (botAppearance?.tts_provider === "eleven_labs") {
if (!isElevenLabAPIKeyPresent()) {
return reply.status(400).send({
message: "Eleven Labs API key not present",
});
}
const is11LabAPIVValid = await isElevenLabAPIValid();
if (!is11LabAPIVValid) {
return reply.status(400).send({
message: "Eleven Labs API key not valid",
});
}
const buffer = await textToSpeech(text, botAppearance.tts_voice!);
return reply.status(200).send(buffer);
} else if (botAppearance.tts_provider === "openai") {
const isOpenAIKeyPresent = isOpenAIAPIKeyPresent();
if (!isOpenAIKeyPresent) {
return reply.status(400).send({
message: "OpenAI API key not present",
});
}
const buffer = await textToSpeechOpenAI(
text,
botAppearance.tts_voice!,
botAppearance.tts_model!
);
return reply.status(200).send(buffer);
} else {
return reply.status(404).send({
message: "TTS provider not found",
});
}
};
10 changes: 9 additions & 1 deletion server/src/routes/bot/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import {
chatRequestStreamHandler,
getChatStyleByIdHandler,
chatRequestAPIHandler,
chatTTSHandler,
} from "../../handlers/bot";
import {
chatRequestSchema,
chatRequestStreamSchema,
chatStyleSchema,
chatAPIRequestSchema,
chatTTSSchema,
} from "../../schema/bot";

const root: FastifyPluginAsync = async (fastify, _): Promise<void> => {
Expand Down Expand Up @@ -37,7 +39,13 @@ const root: FastifyPluginAsync = async (fastify, _): Promise<void> => {
},
getChatStyleByIdHandler
);

fastify.post(
"/:id/tts",
{
schema: chatTTSSchema,
},
chatTTSHandler
);
fastify.post(
"/:id/api",
{
Expand Down

0 comments on commit 189e81b

Please sign in to comment.