Compare commits

..

1 Commits

Author SHA1 Message Date
raojianb
64bb29760e feat: simplify api 2024-09-22 01:53:18 -07:00
10 changed files with 75 additions and 8674 deletions

5
.env
View File

@@ -1,2 +1,3 @@
HEYGEN_API_KEY="your Heygen API key"
NEXT_PUBLIC_BASE_API_URL=https://api.heygen.com
HEYGEN_API_KEY=your Heygen API key
OPENAI_API_KEY=your OpenAI API key
NEXT_PUBLIC_OPENAI_API_KEY=your OpenAI API key

2
.npmrc
View File

@@ -1 +1 @@
package-lock=false

View File

@@ -41,6 +41,24 @@ After you see Monica appear on the screen, you can enter text into the input lab
If you want to see a different Avatar or try a different voice, you can close the session and enter the IDs and then 'start' the session again. Please see below for information on where to retrieve different Avatar and voice IDs that you can use.
### Connecting to OpenAI
A common use case for a Interactive Avatar is to use it as the 'face' of an LLM that users can interact with. In this demo we have included functionality to showcase this by both accepting user input via voice (using OpenAI's Whisper library) and also sending that input to an OpenAI LLM model (using their Chat Completions endpoint).
Both of these features of this demo require an OpenAI API Key. If you do not have a paid OpenAI account, you can learn more on their website: [https://openai.com/index/openai-api/]
Without an OpenAI API Key, this functionality will not work, and the Interactive Avatar will only be able to repeat text input that you provide, and not demonstrate being the 'face' of an LLM. Regardless, this demo is meant to demonstrate what kinds of apps and experiences you can build with our Interactive Avatar SDK, so you can code your own connection to a different LLM if you so choose.
To add your Open AI API Key, fill copy it to the `OPENAI_API_KEY` and `NEXT_PUBLIC_OPENAI_API_KEY` variables in the `.env` file.
### How does the integration with OpenAI / ChatGPT work?
In this demo, we are calling the Chat Completions API from OpenAI in order to come up with some response to user input. You can see the relevant code in components/InteractiveAvatar.tsx.
In the initialMessages parameter, you can replace the content of the 'system' message with whatever 'knowledge base' or context that you would like the GPT-4o model to reply to the user's input with.
You can explore this API and the different parameters and models available here: [https://platform.openai.com/docs/guides/text-generation/chat-completions-api]
### Which Avatars can I use with this project?
By default, there are several Public Avatars that can be used in Interactive Avatar. (AKA Interactive Avatars.) You can find the Avatar IDs for these Public Avatars by navigating to [app.heygen.com/interactive-avatar](https://app.heygen.com/interactive-avatar) and clicking 'Select Avatar' and copying the avatar id.

16
app/api/chat/route.ts Normal file
View File

@@ -0,0 +1,16 @@
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai("gpt-4-turbo"),
messages,
});
return result.toAIStreamResponse();
}

View File

@@ -5,16 +5,18 @@ export async function POST() {
if (!HEYGEN_API_KEY) {
throw new Error("API key is missing from .env");
}
const baseApiUrl = process.env.NEXT_PUBLIC_BASE_API_URL;
const res = await fetch(`${baseApiUrl}/v1/streaming.create_token`, {
method: "POST",
headers: {
"x-api-key": HEYGEN_API_KEY,
const res = await fetch(
"https://api.heygen.com/v1/streaming.create_token",
{
method: "POST",
headers: {
"x-api-key": HEYGEN_API_KEY,
},
},
});
);
const data = await res.json();
return new Response(data.data.token, {
status: 200,
});

View File

@@ -1,53 +1,22 @@
export const AVATARS = [
{
avatar_id: "Ann_Therapist_public",
name: "Ann Therapist",
avatar_id: "Eric_public_pro2_20230608",
name: "Edward in Blue Shirt",
},
{
avatar_id: "Shawn_Therapist_public",
name: "Shawn Therapist",
avatar_id: "Tyler-incasualsuit-20220721",
name: "Tyler in Casual Suit",
},
{
avatar_id: "Bryan_FitnessCoach_public",
name: "Bryan Fitness Coach",
avatar_id: "Anna_public_3_20240108",
name: "Anna in Brown T-shirt",
},
{
avatar_id: "Dexter_Doctor_Standing2_public",
name: "Dexter Doctor Standing",
avatar_id: "Susan_public_2_20240328",
name: "Susan in Black Shirt",
},
{
avatar_id: "Elenora_IT_Sitting_public",
name: "Elenora Tech Expert",
avatar_id: "josh_lite3_20230714",
name: "Joshua Heygen CEO",
},
];
export const STT_LANGUAGE_LIST = [
{ label: 'Bulgarian', value: 'bg', key: 'bg' },
{ label: 'Chinese', value: 'zh', key: 'zh' },
{ label: 'Czech', value: 'cs', key: 'cs' },
{ label: 'Danish', value: 'da', key: 'da' },
{ label: 'Dutch', value: 'nl', key: 'nl' },
{ label: 'English', value: 'en', key: 'en' },
{ label: 'Finnish', value: 'fi', key: 'fi' },
{ label: 'French', value: 'fr', key: 'fr' },
{ label: 'German', value: 'de', key: 'de' },
{ label: 'Greek', value: 'el', key: 'el' },
{ label: 'Hindi', value: 'hi', key: 'hi' },
{ label: 'Hungarian', value: 'hu', key: 'hu' },
{ label: 'Indonesian', value: 'id', key: 'id' },
{ label: 'Italian', value: 'it', key: 'it' },
{ label: 'Japanese', value: 'ja', key: 'ja' },
{ label: 'Korean', value: 'ko', key: 'ko' },
{ label: 'Malay', value: 'ms', key: 'ms' },
{ label: 'Norwegian', value: 'no', key: 'no' },
{ label: 'Polish', value: 'pl', key: 'pl' },
{ label: 'Portuguese', value: 'pt', key: 'pt' },
{ label: 'Romanian', value: 'ro', key: 'ro' },
{ label: 'Russian', value: 'ru', key: 'ru' },
{ label: 'Slovak', value: 'sk', key: 'sk' },
{ label: 'Spanish', value: 'es', key: 'es' },
{ label: 'Swedish', value: 'sv', key: 'sv' },
{ label: 'Turkish', value: 'tr', key: 'tr' },
{ label: 'Ukrainian', value: 'uk', key: 'uk' },
{ label: 'Vietnamese', value: 'vi', key: 'vi' },
];

View File

@@ -3,9 +3,6 @@ import type { StartAvatarResponse } from "@heygen/streaming-avatar";
import StreamingAvatar, {
AvatarQuality,
StreamingEvents,
TaskMode,
TaskType,
VoiceEmotion,
} from "@heygen/streaming-avatar";
import {
Button,
@@ -26,7 +23,7 @@ import { useMemoizedFn, usePrevious } from "ahooks";
import InteractiveAvatarTextInput from "./InteractiveAvatarTextInput";
import { AVATARS, STT_LANGUAGE_LIST } from "@/app/lib/constants";
import { AVATARS } from "@/app/lib/constants";
export default function InteractiveAvatar() {
const [isLoadingSession, setIsLoadingSession] = useState(false);
@@ -35,8 +32,6 @@ export default function InteractiveAvatar() {
const [debug, setDebug] = useState<string>();
const [knowledgeId, setKnowledgeId] = useState<string>("");
const [avatarId, setAvatarId] = useState<string>("");
const [language, setLanguage] = useState<string>("en");
const [data, setData] = useState<StartAvatarResponse>();
const [text, setText] = useState<string>("");
const mediaStream = useRef<HTMLVideoElement>(null);
@@ -44,10 +39,6 @@ export default function InteractiveAvatar() {
const [chatMode, setChatMode] = useState("text_mode");
const [isUserTalking, setIsUserTalking] = useState(false);
function baseApiUrl() {
return process.env.NEXT_PUBLIC_BASE_API_URL;
}
async function fetchAccessToken() {
try {
const response = await fetch("/api/get-access-token", {
@@ -71,7 +62,6 @@ export default function InteractiveAvatar() {
avatar.current = new StreamingAvatar({
token: newToken,
basePath: baseApiUrl(),
});
avatar.current.on(StreamingEvents.AVATAR_START_TALKING, (e) => {
console.log("Avatar started talking", e);
@@ -99,26 +89,12 @@ export default function InteractiveAvatar() {
const res = await avatar.current.createStartAvatar({
quality: AvatarQuality.Low,
avatarName: avatarId,
knowledgeId: knowledgeId, // Or use a custom `knowledgeBase`.
voice: {
rate: 1.5, // 0.5 ~ 1.5
emotion: VoiceEmotion.EXCITED,
// elevenlabsSettings: {
// stability: 1,
// similarity_boost: 1,
// style: 1,
// use_speaker_boost: false,
// },
},
language: language,
disableIdleTimeout: true,
knowledgeId: knowledgeId,
});
setData(res);
// default to voice mode
await avatar.current?.startVoiceChat({
useSilencePrompt: false,
});
await avatar.current?.startVoiceChat();
setChatMode("voice_mode");
} catch (error) {
console.error("Error starting avatar session:", error);
@@ -133,9 +109,8 @@ export default function InteractiveAvatar() {
return;
}
// speak({ text: text, task_type: TaskType.REPEAT })
await avatar.current
.speak({ text: text, taskType: TaskType.REPEAT, taskMode: TaskMode.SYNC })
.speak({ text: text })
.catch((e) => {
setDebug(e.message);
});
@@ -147,12 +122,19 @@ export default function InteractiveAvatar() {
return;
}
await avatar.current.interrupt().catch((e) => {
setDebug(e.message);
});
await avatar.current
.interrupt()
.catch((e) => {
setDebug(e.message);
});
}
async function endSession() {
await avatar.current?.stopAvatar();
if (!avatar.current) {
setDebug("Avatar API not initialized");
return;
}
await avatar.current.stopAvatar();
setStream(undefined);
}
@@ -265,19 +247,6 @@ export default function InteractiveAvatar() {
</SelectItem>
))}
</Select>
<Select
label="Select language"
placeholder="Select language"
className="max-w-xs"
selectedKeys={[language]}
onChange={(e) => {
setLanguage(e.target.value);
}}
>
{STT_LANGUAGE_LIST.map((lang) => (
<SelectItem key={lang.key}>{lang.label}</SelectItem>
))}
</Select>
</div>
<Button
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 w-full text-white"

View File

@@ -28,7 +28,7 @@ export default function NavBar() {
<Link
isExternal
color="foreground"
href="https://labs.heygen.com/interactive-avatar"
href="https://app.heygen.com/interactive-avatar"
>
Avatars
</Link>

View File

@@ -10,7 +10,7 @@
},
"dependencies": {
"@ai-sdk/openai": "^0.0.34",
"@heygen/streaming-avatar": "^2.0.10",
"@heygen/streaming-avatar": "^2.0.0-beta.1",
"@nextui-org/button": "2.0.34",
"@nextui-org/chip": "^2.0.32",
"@nextui-org/code": "2.0.29",
@@ -32,6 +32,7 @@
"ahooks": "^3.8.1",
"ai": "^3.2.15",
"clsx": "2.1.1",
"framer-motion": "~11.1.1",
"intl-messageformat": "^10.5.0",
"next": "14.2.4",
"next-themes": "^0.2.1",

8575
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff