Spaces:
Sleeping
Sleeping
Upload 23 files
Browse files- .gitattributes +1 -0
- Dockerfile +66 -0
- README.md +54 -10
- app/api/get-access-token/route.ts +30 -0
- app/error.tsx +31 -0
- app/layout.tsx +59 -0
- app/lib/constants.ts +53 -0
- app/page.tsx +15 -0
- app/providers.tsx +22 -0
- components/Icons.tsx +81 -0
- components/InteractiveAvatar.tsx +284 -0
- components/InteractiveAvatarCode.tsx +99 -0
- components/InteractiveAvatarTextInput.tsx +77 -0
- components/NavBar.tsx +70 -0
- components/ThemeSwitch.tsx +80 -0
- next.config.js +6 -0
- package.json +66 -0
- postcss.config.js +6 -0
- public/demo.png +3 -0
- public/heygen-logo.png +0 -0
- styles/globals.css +9 -0
- tailwind.config.js +20 -0
- tsconfig.json +28 -0
- yarn.lock +0 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
public/demo.png filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# syntax=docker.io/docker/dockerfile:1
|
2 |
+
|
3 |
+
FROM node:18-alpine AS base
|
4 |
+
|
5 |
+
# Install dependencies only when needed
|
6 |
+
FROM base AS deps
|
7 |
+
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
8 |
+
RUN apk add --no-cache libc6-compat
|
9 |
+
WORKDIR /app
|
10 |
+
|
11 |
+
# Install dependencies based on the preferred package manager
|
12 |
+
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
|
13 |
+
RUN \
|
14 |
+
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
15 |
+
elif [ -f package-lock.json ]; then npm ci; \
|
16 |
+
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
17 |
+
else echo "Lockfile not found." && exit 1; \
|
18 |
+
fi
|
19 |
+
|
20 |
+
|
21 |
+
# Rebuild the source code only when needed
|
22 |
+
FROM base AS builder
|
23 |
+
WORKDIR /app
|
24 |
+
COPY --from=deps /app/node_modules ./node_modules
|
25 |
+
COPY . .
|
26 |
+
|
27 |
+
# Next.js collects completely anonymous telemetry data about general usage.
|
28 |
+
# Learn more here: https://nextjs.org/telemetry
|
29 |
+
# Uncomment the following line in case you want to disable telemetry during the build.
|
30 |
+
# ENV NEXT_TELEMETRY_DISABLED=1
|
31 |
+
|
32 |
+
RUN \
|
33 |
+
if [ -f yarn.lock ]; then yarn run build; \
|
34 |
+
elif [ -f package-lock.json ]; then npm run build; \
|
35 |
+
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
36 |
+
else echo "Lockfile not found." && exit 1; \
|
37 |
+
fi
|
38 |
+
|
39 |
+
# Production image, copy all the files and run next
|
40 |
+
FROM base AS runner
|
41 |
+
WORKDIR /app
|
42 |
+
|
43 |
+
ENV NODE_ENV=production
|
44 |
+
# Uncomment the following line in case you want to disable telemetry during runtime.
|
45 |
+
# ENV NEXT_TELEMETRY_DISABLED=1
|
46 |
+
|
47 |
+
RUN addgroup --system --gid 1001 nodejs
|
48 |
+
RUN adduser --system --uid 1001 nextjs
|
49 |
+
|
50 |
+
COPY --from=builder /app/public ./public
|
51 |
+
|
52 |
+
# Automatically leverage output traces to reduce image size
|
53 |
+
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
54 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
55 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
56 |
+
|
57 |
+
USER nextjs
|
58 |
+
|
59 |
+
EXPOSE 3000
|
60 |
+
|
61 |
+
ENV PORT=3000
|
62 |
+
|
63 |
+
# server.js is created by next build from the standalone output
|
64 |
+
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
|
65 |
+
ENV HOSTNAME="0.0.0.0"
|
66 |
+
CMD ["node", "server.js"]
|
README.md
CHANGED
@@ -1,10 +1,54 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# HeyGen Interactive Avatar NextJS Demo
|
2 |
+
|
3 |
+

|
4 |
+
|
5 |
+
This is a sample project and was bootstrapped using [NextJS](https://nextjs.org/).
|
6 |
+
Feel free to play around with the existing code and please leave any feedback for the SDK [here](https://github.com/HeyGen-Official/StreamingAvatarSDK/discussions).
|
7 |
+
|
8 |
+
## Getting Started FAQ
|
9 |
+
|
10 |
+
### Setting up the demo
|
11 |
+
|
12 |
+
1. Clone this repo
|
13 |
+
|
14 |
+
2. Navigate to the repo folder in your terminal
|
15 |
+
|
16 |
+
3. Run `npm install` (assuming you have npm installed. If not, please follow these instructions: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/)
|
17 |
+
|
18 |
+
4. Enter your HeyGen Enterprise API Token or Trial Token in the `.env` file. Replace `HEYGEN_API_KEY` with your API key. This will allow the Client app to generate secure Access Tokens with which to create interactive sessions.
|
19 |
+
|
20 |
+
You can retrieve either the API Key or Trial Token by logging in to HeyGen and navigating to this page in your settings: [https://app.heygen.com/settings?nav=API]. NOTE: use the trial token if you don't have an enterprise API token yet.
|
21 |
+
|
22 |
+
5. (Optional) If you would like to use the OpenAI features, enter your OpenAI Api Key in the `.env` file.
|
23 |
+
|
24 |
+
6. Run `npm run dev`
|
25 |
+
|
26 |
+
### Difference between Trial Token and Enterprise API Token
|
27 |
+
|
28 |
+
The HeyGen Trial Token is available to all users, not just Enterprise users, and allows for testing of the Interactive Avatar API, as well as other HeyGen API endpoints.
|
29 |
+
|
30 |
+
Each Trial Token is limited to 3 concurrent interactive sessions. However, every interactive session you create with the Trial Token is free of charge, no matter how many tasks are sent to the avatar. Please note that interactive sessions will automatically close after 10 minutes of no tasks sent.
|
31 |
+
|
32 |
+
If you do not 'close' the interactive sessions and try to open more than 3, you will encounter errors including stuttering and freezing of the Interactive Avatar. Please endeavor to only have 3 sessions open at any time while you are testing the Interactive Avatar API with your Trial Token.
|
33 |
+
|
34 |
+
### Starting sessions
|
35 |
+
|
36 |
+
NOTE: Make sure you have enter your token into the `.env` file and run `npm run dev`.
|
37 |
+
|
38 |
+
To start your 'session' with a Interactive Avatar, first click the 'start' button. If your HeyGen API key is entered into the Server's .env file, then you should see our demo Interactive Avatar (Monica!) appear.
|
39 |
+
|
40 |
+
After you see Monica appear on the screen, you can enter text into the input labeled 'Repeat', and then hit Enter. The Interactive Avatar will say the text you enter.
|
41 |
+
|
42 |
+
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.
|
43 |
+
|
44 |
+
### Which Avatars can I use with this project?
|
45 |
+
|
46 |
+
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.
|
47 |
+
|
48 |
+
In order to use a private Avatar created under your own account in Interactive Avatar, it must be upgraded to be a Interactive Avatar. Only 1. Finetune Instant Avatars and 2. Studio Avatars are able to be upgraded to Interactive Avatars. This upgrade is a one-time fee and can be purchased by navigating to [app.heygen.com/interactive-avatar] and clicking 'Select Avatar'.
|
49 |
+
|
50 |
+
Please note that Photo Avatars are not compatible with Interactive Avatar and cannot be used.
|
51 |
+
|
52 |
+
### Where can I read more about enterprise-level usage of the Interactive Avatar API?
|
53 |
+
|
54 |
+
Please read our Interactive Avatar 101 article for more information on pricing and how to increase your concurrent session limit: https://help.heygen.com/en/articles/9182113-interactive-avatar-101-your-ultimate-guide
|
app/api/get-access-token/route.ts
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const HEYGEN_API_KEY = "YTBjYzk5MjI1ZWQzNGViOTgxMTI0NjM2OGQ5NDc2OGEtMTczMzk5MjA5MA=="
|
2 |
+
|
3 |
+
export async function POST() {
|
4 |
+
try {
|
5 |
+
if (!HEYGEN_API_KEY) {
|
6 |
+
throw new Error("API key is missing from .env");
|
7 |
+
}
|
8 |
+
|
9 |
+
const res = await fetch(
|
10 |
+
"https://api.heygen.com/v1/streaming.create_token",
|
11 |
+
{
|
12 |
+
method: "POST",
|
13 |
+
headers: {
|
14 |
+
"x-api-key": HEYGEN_API_KEY,
|
15 |
+
},
|
16 |
+
},
|
17 |
+
);
|
18 |
+
const data = await res.json();
|
19 |
+
|
20 |
+
return new Response(data.data.token, {
|
21 |
+
status: 200,
|
22 |
+
});
|
23 |
+
} catch (error) {
|
24 |
+
console.error("Error retrieving access token:", error);
|
25 |
+
|
26 |
+
return new Response("Failed to retrieve access token", {
|
27 |
+
status: 500,
|
28 |
+
});
|
29 |
+
}
|
30 |
+
}
|
app/error.tsx
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { useEffect } from "react";
|
4 |
+
|
5 |
+
export default function Error({
|
6 |
+
error,
|
7 |
+
reset,
|
8 |
+
}: {
|
9 |
+
error: Error;
|
10 |
+
reset: () => void;
|
11 |
+
}) {
|
12 |
+
useEffect(() => {
|
13 |
+
// Log the error to an error reporting service
|
14 |
+
/* eslint-disable no-console */
|
15 |
+
console.error(error);
|
16 |
+
}, [error]);
|
17 |
+
|
18 |
+
return (
|
19 |
+
<div>
|
20 |
+
<h2>Something went wrong!</h2>
|
21 |
+
<button
|
22 |
+
onClick={
|
23 |
+
// Attempt to recover by trying to re-render the segment
|
24 |
+
() => reset()
|
25 |
+
}
|
26 |
+
>
|
27 |
+
Try again
|
28 |
+
</button>
|
29 |
+
</div>
|
30 |
+
);
|
31 |
+
}
|
app/layout.tsx
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import "@/styles/globals.css";
|
2 |
+
import clsx from "clsx";
|
3 |
+
import { Metadata, Viewport } from "next";
|
4 |
+
|
5 |
+
import { Providers } from "./providers";
|
6 |
+
|
7 |
+
import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google";
|
8 |
+
import NavBar from "@/components/NavBar";
|
9 |
+
|
10 |
+
const fontSans = FontSans({
|
11 |
+
subsets: ["latin"],
|
12 |
+
variable: "--font-sans",
|
13 |
+
});
|
14 |
+
|
15 |
+
const fontMono = FontMono({
|
16 |
+
subsets: ["latin"],
|
17 |
+
variable: "--font-geist-mono",
|
18 |
+
});
|
19 |
+
|
20 |
+
export const metadata: Metadata = {
|
21 |
+
title: {
|
22 |
+
default: "tvam policy assistant",
|
23 |
+
template: `%s - HeyGen Interactive Avatar SDK Demo`,
|
24 |
+
},
|
25 |
+
icons: {
|
26 |
+
icon: "/heygen-logo.png",
|
27 |
+
},
|
28 |
+
};
|
29 |
+
|
30 |
+
export const viewport: Viewport = {
|
31 |
+
themeColor: [
|
32 |
+
{ media: "(prefers-color-scheme: light)", color: "white" },
|
33 |
+
{ media: "(prefers-color-scheme: dark)", color: "black" },
|
34 |
+
],
|
35 |
+
};
|
36 |
+
|
37 |
+
export default function RootLayout({
|
38 |
+
children,
|
39 |
+
}: {
|
40 |
+
children: React.ReactNode;
|
41 |
+
}) {
|
42 |
+
return (
|
43 |
+
<html
|
44 |
+
suppressHydrationWarning
|
45 |
+
lang="en"
|
46 |
+
className={`${fontSans.variable} ${fontMono.variable} font-sans`}
|
47 |
+
>
|
48 |
+
<head />
|
49 |
+
<body className={clsx("min-h-screen bg-background antialiased")}>
|
50 |
+
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
51 |
+
<main className="relative flex flex-col h-screen w-screen">
|
52 |
+
<NavBar />
|
53 |
+
{children}
|
54 |
+
</main>
|
55 |
+
</Providers>
|
56 |
+
</body>
|
57 |
+
</html>
|
58 |
+
);
|
59 |
+
}
|
app/lib/constants.ts
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const AVATARS = [
|
2 |
+
{
|
3 |
+
avatar_id: "Eric_public_pro2_20230608",
|
4 |
+
name: "Edward in Blue Shirt",
|
5 |
+
},
|
6 |
+
{
|
7 |
+
avatar_id: "Tyler-incasualsuit-20220721",
|
8 |
+
name: "Tyler in Casual Suit",
|
9 |
+
},
|
10 |
+
{
|
11 |
+
avatar_id: "Anna_public_3_20240108",
|
12 |
+
name: "Anna in Brown T-shirt",
|
13 |
+
},
|
14 |
+
{
|
15 |
+
avatar_id: "Susan_public_2_20240328",
|
16 |
+
name: "Susan in Black Shirt",
|
17 |
+
},
|
18 |
+
{
|
19 |
+
avatar_id: "josh_lite3_20230714",
|
20 |
+
name: "Joshua Heygen CEO",
|
21 |
+
},
|
22 |
+
];
|
23 |
+
|
24 |
+
export const STT_LANGUAGE_LIST = [
|
25 |
+
{ label: 'Bulgarian', value: 'bg', key: 'bg' },
|
26 |
+
{ label: 'Chinese', value: 'zh', key: 'zh' },
|
27 |
+
{ label: 'Czech', value: 'cs', key: 'cs' },
|
28 |
+
{ label: 'Danish', value: 'da', key: 'da' },
|
29 |
+
{ label: 'Dutch', value: 'nl', key: 'nl' },
|
30 |
+
{ label: 'English', value: 'en', key: 'en' },
|
31 |
+
{ label: 'Finnish', value: 'fi', key: 'fi' },
|
32 |
+
{ label: 'French', value: 'fr', key: 'fr' },
|
33 |
+
{ label: 'German', value: 'de', key: 'de' },
|
34 |
+
{ label: 'Greek', value: 'el', key: 'el' },
|
35 |
+
{ label: 'Hindi', value: 'hi', key: 'hi' },
|
36 |
+
{ label: 'Hungarian', value: 'hu', key: 'hu' },
|
37 |
+
{ label: 'Indonesian', value: 'id', key: 'id' },
|
38 |
+
{ label: 'Italian', value: 'it', key: 'it' },
|
39 |
+
{ label: 'Japanese', value: 'ja', key: 'ja' },
|
40 |
+
{ label: 'Korean', value: 'ko', key: 'ko' },
|
41 |
+
{ label: 'Malay', value: 'ms', key: 'ms' },
|
42 |
+
{ label: 'Norwegian', value: 'no', key: 'no' },
|
43 |
+
{ label: 'Polish', value: 'pl', key: 'pl' },
|
44 |
+
{ label: 'Portuguese', value: 'pt', key: 'pt' },
|
45 |
+
{ label: 'Romanian', value: 'ro', key: 'ro' },
|
46 |
+
{ label: 'Russian', value: 'ru', key: 'ru' },
|
47 |
+
{ label: 'Slovak', value: 'sk', key: 'sk' },
|
48 |
+
{ label: 'Spanish', value: 'es', key: 'es' },
|
49 |
+
{ label: 'Swedish', value: 'sv', key: 'sv' },
|
50 |
+
{ label: 'Turkish', value: 'tr', key: 'tr' },
|
51 |
+
{ label: 'Ukrainian', value: 'uk', key: 'uk' },
|
52 |
+
{ label: 'Vietnamese', value: 'vi', key: 'vi' },
|
53 |
+
];
|
app/page.tsx
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import InteractiveAvatar from "@/components/InteractiveAvatar";
|
4 |
+
export default function App() {
|
5 |
+
|
6 |
+
return (
|
7 |
+
<div className="w-screen h-screen flex flex-col">
|
8 |
+
<div className="w-[900px] flex flex-col items-start justify-start gap-5 mx-auto pt-4 pb-20">
|
9 |
+
<div className="w-full">
|
10 |
+
<InteractiveAvatar />
|
11 |
+
</div>
|
12 |
+
</div>
|
13 |
+
</div>
|
14 |
+
);
|
15 |
+
}
|
app/providers.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import * as React from "react";
|
4 |
+
import { NextUIProvider } from "@nextui-org/system";
|
5 |
+
import { useRouter } from "next/navigation";
|
6 |
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
7 |
+
import { ThemeProviderProps } from "next-themes/dist/types";
|
8 |
+
|
9 |
+
export interface ProvidersProps {
|
10 |
+
children: React.ReactNode;
|
11 |
+
themeProps?: ThemeProviderProps;
|
12 |
+
}
|
13 |
+
|
14 |
+
export function Providers({ children, themeProps }: ProvidersProps) {
|
15 |
+
const router = useRouter();
|
16 |
+
|
17 |
+
return (
|
18 |
+
<NextUIProvider navigate={router.push}>
|
19 |
+
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
20 |
+
</NextUIProvider>
|
21 |
+
);
|
22 |
+
}
|
components/Icons.tsx
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function HeyGenLogo() {
|
2 |
+
return <img src="/heygen-logo.png" className="h-8" alt="HeyGen Logo" />;
|
3 |
+
}
|
4 |
+
|
5 |
+
type IconSvgProps = {
|
6 |
+
size?: number;
|
7 |
+
width?: number;
|
8 |
+
height?: number;
|
9 |
+
className?: string;
|
10 |
+
};
|
11 |
+
|
12 |
+
export function GithubIcon({
|
13 |
+
size = 24,
|
14 |
+
width,
|
15 |
+
height,
|
16 |
+
...props
|
17 |
+
}: IconSvgProps) {
|
18 |
+
return (
|
19 |
+
<svg
|
20 |
+
height={size || height}
|
21 |
+
viewBox="0 0 24 24"
|
22 |
+
width={size || width}
|
23 |
+
{...props}
|
24 |
+
>
|
25 |
+
<path
|
26 |
+
clipRule="evenodd"
|
27 |
+
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
|
28 |
+
fill="currentColor"
|
29 |
+
fillRule="evenodd"
|
30 |
+
/>
|
31 |
+
</svg>
|
32 |
+
);
|
33 |
+
}
|
34 |
+
|
35 |
+
export function MoonFilledIcon({
|
36 |
+
size = 24,
|
37 |
+
width,
|
38 |
+
height,
|
39 |
+
...props
|
40 |
+
}: IconSvgProps) {
|
41 |
+
return (
|
42 |
+
<svg
|
43 |
+
aria-hidden="true"
|
44 |
+
focusable="false"
|
45 |
+
height={size || height}
|
46 |
+
role="presentation"
|
47 |
+
viewBox="0 0 24 24"
|
48 |
+
width={size || width}
|
49 |
+
{...props}
|
50 |
+
>
|
51 |
+
<path
|
52 |
+
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
|
53 |
+
fill="currentColor"
|
54 |
+
/>
|
55 |
+
</svg>
|
56 |
+
);
|
57 |
+
}
|
58 |
+
|
59 |
+
export function SunFilledIcon({
|
60 |
+
size = 24,
|
61 |
+
width,
|
62 |
+
height,
|
63 |
+
...props
|
64 |
+
}: IconSvgProps) {
|
65 |
+
return (
|
66 |
+
<svg
|
67 |
+
aria-hidden="true"
|
68 |
+
focusable="false"
|
69 |
+
height={size || height}
|
70 |
+
role="presentation"
|
71 |
+
viewBox="0 0 24 24"
|
72 |
+
width={size || width}
|
73 |
+
{...props}
|
74 |
+
>
|
75 |
+
<g fill="currentColor">
|
76 |
+
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
|
77 |
+
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
|
78 |
+
</g>
|
79 |
+
</svg>
|
80 |
+
);
|
81 |
+
}
|
components/InteractiveAvatar.tsx
ADDED
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { StartAvatarResponse } from "@heygen/streaming-avatar";
|
2 |
+
|
3 |
+
import StreamingAvatar, {
|
4 |
+
AvatarQuality,
|
5 |
+
StreamingEvents,
|
6 |
+
TaskMode,
|
7 |
+
TaskType,
|
8 |
+
VoiceEmotion,
|
9 |
+
} from "@heygen/streaming-avatar";
|
10 |
+
import {
|
11 |
+
Button,
|
12 |
+
Card,
|
13 |
+
CardBody,
|
14 |
+
CardFooter,
|
15 |
+
Divider,
|
16 |
+
Spinner,
|
17 |
+
Chip,
|
18 |
+
Tabs,
|
19 |
+
Tab,
|
20 |
+
} from "@nextui-org/react";
|
21 |
+
import { useEffect, useRef, useState } from "react";
|
22 |
+
import { useMemoizedFn, usePrevious } from "ahooks";
|
23 |
+
|
24 |
+
import InteractiveAvatarTextInput from "./InteractiveAvatarTextInput";
|
25 |
+
|
26 |
+
export default function InteractiveAvatar() {
|
27 |
+
const [isLoadingSession, setIsLoadingSession] = useState(false);
|
28 |
+
const [isLoadingRepeat, setIsLoadingRepeat] = useState(false);
|
29 |
+
const [stream, setStream] = useState<MediaStream>();
|
30 |
+
const [debug, setDebug] = useState<string>();
|
31 |
+
|
32 |
+
// Hardcoded values for avatarId and knowledgeId
|
33 |
+
const [knowledgeId] = useState<string>("c60c40259b034917b235da7d007002b6");
|
34 |
+
const [avatarId] = useState<string>("eb0a8cc8046f476da551a5559fbb5c82");
|
35 |
+
const [language] = useState<string>("en");
|
36 |
+
|
37 |
+
const [data, setData] = useState<StartAvatarResponse>();
|
38 |
+
const [text, setText] = useState<string>("");
|
39 |
+
const mediaStream = useRef<HTMLVideoElement>(null);
|
40 |
+
const avatar = useRef<StreamingAvatar | null>(null);
|
41 |
+
const [chatMode, setChatMode] = useState("text_mode");
|
42 |
+
const [isUserTalking, setIsUserTalking] = useState(false);
|
43 |
+
|
44 |
+
async function fetchAccessToken() {
|
45 |
+
try {
|
46 |
+
const response = await fetch("/api/get-access-token", {
|
47 |
+
method: "POST",
|
48 |
+
});
|
49 |
+
const token = await response.text();
|
50 |
+
|
51 |
+
console.log("Access Token:", token);
|
52 |
+
return token;
|
53 |
+
} catch (error) {
|
54 |
+
console.error("Error fetching access token:", error);
|
55 |
+
}
|
56 |
+
|
57 |
+
return "";
|
58 |
+
}
|
59 |
+
|
60 |
+
async function startSession() {
|
61 |
+
setIsLoadingSession(true);
|
62 |
+
const newToken = await fetchAccessToken();
|
63 |
+
|
64 |
+
avatar.current = new StreamingAvatar({
|
65 |
+
token: newToken,
|
66 |
+
});
|
67 |
+
|
68 |
+
avatar.current.on(StreamingEvents.AVATAR_START_TALKING, (e) => {
|
69 |
+
console.log("Avatar started talking", e);
|
70 |
+
});
|
71 |
+
|
72 |
+
avatar.current.on(StreamingEvents.AVATAR_STOP_TALKING, (e) => {
|
73 |
+
console.log("Avatar stopped talking", e);
|
74 |
+
});
|
75 |
+
|
76 |
+
avatar.current.on(StreamingEvents.STREAM_DISCONNECTED, () => {
|
77 |
+
console.log("Stream disconnected");
|
78 |
+
endSession();
|
79 |
+
});
|
80 |
+
|
81 |
+
avatar.current?.on(StreamingEvents.STREAM_READY, (event) => {
|
82 |
+
console.log(">>>>> Stream ready:", event.detail);
|
83 |
+
setStream(event.detail);
|
84 |
+
});
|
85 |
+
|
86 |
+
avatar.current?.on(StreamingEvents.USER_START, (event) => {
|
87 |
+
console.log(">>>>> User started talking:", event);
|
88 |
+
setIsUserTalking(true);
|
89 |
+
});
|
90 |
+
|
91 |
+
avatar.current?.on(StreamingEvents.USER_STOP, (event) => {
|
92 |
+
console.log(">>>>> User stopped talking:", event);
|
93 |
+
setIsUserTalking(false);
|
94 |
+
});
|
95 |
+
|
96 |
+
try {
|
97 |
+
const res = await avatar.current.createStartAvatar({
|
98 |
+
quality: AvatarQuality.Low,
|
99 |
+
avatarName: avatarId,
|
100 |
+
knowledgeId: knowledgeId,
|
101 |
+
voice: {
|
102 |
+
rate: 1.5, // 0.5 ~ 1.5
|
103 |
+
emotion: VoiceEmotion.EXCITED,
|
104 |
+
},
|
105 |
+
language: language,
|
106 |
+
disableIdleTimeout: true,
|
107 |
+
});
|
108 |
+
|
109 |
+
setData(res);
|
110 |
+
await avatar.current?.startVoiceChat({
|
111 |
+
useSilencePrompt: false,
|
112 |
+
});
|
113 |
+
setChatMode("voice_mode");
|
114 |
+
} catch (error) {
|
115 |
+
console.error("Error starting avatar session:", error);
|
116 |
+
} finally {
|
117 |
+
setIsLoadingSession(false);
|
118 |
+
}
|
119 |
+
}
|
120 |
+
|
121 |
+
async function handleSpeak() {
|
122 |
+
setIsLoadingRepeat(true);
|
123 |
+
if (!avatar.current) {
|
124 |
+
setDebug("Avatar API not initialized");
|
125 |
+
return;
|
126 |
+
}
|
127 |
+
|
128 |
+
await avatar.current
|
129 |
+
.speak({ text: text, taskType: TaskType.REPEAT, taskMode: TaskMode.SYNC })
|
130 |
+
.catch((e) => {
|
131 |
+
setDebug(e.message);
|
132 |
+
});
|
133 |
+
setIsLoadingRepeat(false);
|
134 |
+
}
|
135 |
+
|
136 |
+
async function handleInterrupt() {
|
137 |
+
if (!avatar.current) {
|
138 |
+
setDebug("Avatar API not initialized");
|
139 |
+
return;
|
140 |
+
}
|
141 |
+
await avatar.current.interrupt().catch((e) => {
|
142 |
+
setDebug(e.message);
|
143 |
+
});
|
144 |
+
}
|
145 |
+
|
146 |
+
async function endSession() {
|
147 |
+
await avatar.current?.stopAvatar();
|
148 |
+
setStream(undefined);
|
149 |
+
}
|
150 |
+
|
151 |
+
const handleChangeChatMode = useMemoizedFn(async (v) => {
|
152 |
+
if (v === chatMode) {
|
153 |
+
return;
|
154 |
+
}
|
155 |
+
if (v === "text_mode") {
|
156 |
+
avatar.current?.closeVoiceChat();
|
157 |
+
} else {
|
158 |
+
await avatar.current?.startVoiceChat();
|
159 |
+
}
|
160 |
+
setChatMode(v);
|
161 |
+
});
|
162 |
+
|
163 |
+
const previousText = usePrevious(text);
|
164 |
+
|
165 |
+
useEffect(() => {
|
166 |
+
if (!previousText && text) {
|
167 |
+
avatar.current?.startListening();
|
168 |
+
} else if (previousText && !text) {
|
169 |
+
avatar?.current?.stopListening();
|
170 |
+
}
|
171 |
+
}, [text, previousText]);
|
172 |
+
|
173 |
+
useEffect(() => {
|
174 |
+
return () => {
|
175 |
+
endSession();
|
176 |
+
};
|
177 |
+
}, []);
|
178 |
+
|
179 |
+
useEffect(() => {
|
180 |
+
if (stream && mediaStream.current) {
|
181 |
+
mediaStream.current.srcObject = stream;
|
182 |
+
mediaStream.current.onloadedmetadata = () => {
|
183 |
+
mediaStream.current!.play();
|
184 |
+
setDebug("Playing");
|
185 |
+
};
|
186 |
+
}
|
187 |
+
}, [mediaStream, stream]);
|
188 |
+
|
189 |
+
return (
|
190 |
+
<div className="w-full flex flex-col gap-4">
|
191 |
+
<Card>
|
192 |
+
<CardBody className="h-[500px] flex flex-col justify-center items-center">
|
193 |
+
{stream ? (
|
194 |
+
<div className="h-[500px] w-[900px] justify-center items-center flex rounded-lg overflow-hidden">
|
195 |
+
<video
|
196 |
+
ref={mediaStream}
|
197 |
+
autoPlay
|
198 |
+
playsInline
|
199 |
+
style={{
|
200 |
+
width: "100%",
|
201 |
+
height: "100%",
|
202 |
+
objectFit: "contain",
|
203 |
+
}}
|
204 |
+
>
|
205 |
+
<track kind="captions" />
|
206 |
+
</video>
|
207 |
+
<div className="flex flex-col gap-2 absolute bottom-3 right-3">
|
208 |
+
<Button
|
209 |
+
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 text-white rounded-lg"
|
210 |
+
size="md"
|
211 |
+
variant="shadow"
|
212 |
+
onClick={handleInterrupt}
|
213 |
+
>
|
214 |
+
Interrupt task
|
215 |
+
</Button>
|
216 |
+
<Button
|
217 |
+
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 text-white rounded-lg"
|
218 |
+
size="md"
|
219 |
+
variant="shadow"
|
220 |
+
onClick={endSession}
|
221 |
+
>
|
222 |
+
End session
|
223 |
+
</Button>
|
224 |
+
</div>
|
225 |
+
</div>
|
226 |
+
) : !isLoadingSession ? (
|
227 |
+
<div className="h-full justify-center items-center flex flex-col gap-8 w-[500px] self-center">
|
228 |
+
<Button
|
229 |
+
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 w-full text-white"
|
230 |
+
size="md"
|
231 |
+
variant="shadow"
|
232 |
+
onClick={startSession}
|
233 |
+
>
|
234 |
+
Start session
|
235 |
+
</Button>
|
236 |
+
</div>
|
237 |
+
) : (
|
238 |
+
<Spinner color="default" size="lg" />
|
239 |
+
)}
|
240 |
+
</CardBody>
|
241 |
+
<Divider />
|
242 |
+
<CardFooter className="flex flex-col gap-3 relative">
|
243 |
+
<Tabs
|
244 |
+
aria-label="Options"
|
245 |
+
selectedKey={chatMode}
|
246 |
+
onSelectionChange={(v) => {
|
247 |
+
handleChangeChatMode(v);
|
248 |
+
}}
|
249 |
+
>
|
250 |
+
<Tab key="text_mode" title="Text mode" />
|
251 |
+
<Tab key="voice_mode" title="Voice mode" />
|
252 |
+
</Tabs>
|
253 |
+
{chatMode === "text_mode" ? (
|
254 |
+
<div className="w-full flex relative">
|
255 |
+
<InteractiveAvatarTextInput
|
256 |
+
disabled={!stream}
|
257 |
+
input={text}
|
258 |
+
label="Chat"
|
259 |
+
loading={isLoadingRepeat}
|
260 |
+
placeholder="Type something for the avatar to respond"
|
261 |
+
setInput={setText}
|
262 |
+
onSubmit={handleSpeak}
|
263 |
+
/>
|
264 |
+
{text && (
|
265 |
+
<Chip className="absolute right-16 top-3">Listening</Chip>
|
266 |
+
)}
|
267 |
+
</div>
|
268 |
+
) : (
|
269 |
+
<div className="w-full text-center">
|
270 |
+
<Button
|
271 |
+
isDisabled={!isUserTalking}
|
272 |
+
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 text-white"
|
273 |
+
size="md"
|
274 |
+
variant="shadow"
|
275 |
+
>
|
276 |
+
{isUserTalking ? "Listening" : "Voice chat"}
|
277 |
+
</Button>
|
278 |
+
</div>
|
279 |
+
)}
|
280 |
+
</CardFooter>
|
281 |
+
</Card>
|
282 |
+
</div>
|
283 |
+
);
|
284 |
+
}
|
components/InteractiveAvatarCode.tsx
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Card, CardBody } from "@nextui-org/react";
|
2 |
+
import { langs } from "@uiw/codemirror-extensions-langs";
|
3 |
+
import ReactCodeMirror from "@uiw/react-codemirror";
|
4 |
+
|
5 |
+
export default function InteractiveAvatarCode() {
|
6 |
+
return (
|
7 |
+
<div className="w-full flex flex-col gap-2">
|
8 |
+
<p>This SDK supports the following behavior:</p>
|
9 |
+
<ul>
|
10 |
+
<li>
|
11 |
+
<div className="flex flex-row gap-2">
|
12 |
+
<p className="text-indigo-400 font-semibold">Start:</p> Start the
|
13 |
+
Interactive Avatar session
|
14 |
+
</div>
|
15 |
+
</li>
|
16 |
+
<li>
|
17 |
+
<div className="flex flex-row gap-2">
|
18 |
+
<p className="text-indigo-400 font-semibold">Close:</p> Close the
|
19 |
+
Interactive Avatar session
|
20 |
+
</div>
|
21 |
+
</li>
|
22 |
+
<li>
|
23 |
+
<div className="flex flex-row gap-2">
|
24 |
+
<p className="text-indigo-400 font-semibold">Speak:</p> Repeat the
|
25 |
+
input
|
26 |
+
</div>
|
27 |
+
</li>
|
28 |
+
</ul>
|
29 |
+
<Card>
|
30 |
+
<CardBody>
|
31 |
+
<ReactCodeMirror
|
32 |
+
editable={false}
|
33 |
+
extensions={[langs.typescript()]}
|
34 |
+
height="700px"
|
35 |
+
theme="dark"
|
36 |
+
value={TEXT}
|
37 |
+
/>
|
38 |
+
</CardBody>
|
39 |
+
</Card>
|
40 |
+
</div>
|
41 |
+
);
|
42 |
+
}
|
43 |
+
|
44 |
+
const TEXT = `
|
45 |
+
export default function App() {
|
46 |
+
// Media stream used by the video player to display the avatar
|
47 |
+
const [stream, setStream] = useState<MediaStream> ();
|
48 |
+
const mediaStream = useRef<HTMLVideoElement>(null);
|
49 |
+
|
50 |
+
// Instantiate the Interactive Avatar api using your access token
|
51 |
+
const avatar = useRef(new StreamingAvatarApi(
|
52 |
+
new Configuration({accessToken: '<REPLACE_WITH_ACCESS_TOKEN>'})
|
53 |
+
));
|
54 |
+
|
55 |
+
// State holding Interactive Avatar session data
|
56 |
+
const [sessionData, setSessionData] = useState<NewSessionData>();
|
57 |
+
|
58 |
+
// Function to start the Interactive Avatar session
|
59 |
+
async function start(){
|
60 |
+
const res = await avatar.current.createStartAvatar(
|
61 |
+
{ newSessionRequest:
|
62 |
+
|
63 |
+
// Define the session variables during creation
|
64 |
+
{ quality: "medium", // low, medium, high
|
65 |
+
avatarName: <REPLACE_WITH_AVATAR_ID>,
|
66 |
+
voice:{voiceId: <REPLACE_WITH_VOICE_ID>}
|
67 |
+
}
|
68 |
+
|
69 |
+
});
|
70 |
+
setSessionData(res);
|
71 |
+
}
|
72 |
+
|
73 |
+
// Function to stop the Interactive Avatar session
|
74 |
+
async function stop(){
|
75 |
+
await avatar.current.stopAvatar({stopSessionRequest: {sessionId: sessionData?.sessionId}});
|
76 |
+
}
|
77 |
+
|
78 |
+
// Function which passes in text to the avatar to repeat
|
79 |
+
async function handleSpeak(){
|
80 |
+
await avatar.current.speak({taskRequest: {text: <TEXT_TO_SAY>, sessionId: sessionData?.sessionId}}).catch((e) => {
|
81 |
+
});
|
82 |
+
}
|
83 |
+
|
84 |
+
useEffect(()=>{
|
85 |
+
// Handles the display of the Interactive Avatar
|
86 |
+
if(stream && mediaStream.current){
|
87 |
+
mediaStream.current.srcObject = stream;
|
88 |
+
mediaStream.current.onloadedmetadata = () => {
|
89 |
+
mediaStream.current!.play();
|
90 |
+
}
|
91 |
+
}
|
92 |
+
}, [mediaStream, stream])
|
93 |
+
|
94 |
+
return (
|
95 |
+
<div className="w-full">
|
96 |
+
<video playsInline autoPlay width={500} ref={mediaStream}/>
|
97 |
+
</div>
|
98 |
+
)
|
99 |
+
}`;
|
components/InteractiveAvatarTextInput.tsx
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Input, Spinner, Tooltip } from "@nextui-org/react";
|
2 |
+
import { Airplane, ArrowRight, PaperPlaneRight } from "@phosphor-icons/react";
|
3 |
+
import clsx from "clsx";
|
4 |
+
|
5 |
+
interface StreamingAvatarTextInputProps {
|
6 |
+
label: string;
|
7 |
+
placeholder: string;
|
8 |
+
input: string;
|
9 |
+
onSubmit: () => void;
|
10 |
+
setInput: (value: string) => void;
|
11 |
+
endContent?: React.ReactNode;
|
12 |
+
disabled?: boolean;
|
13 |
+
loading?: boolean;
|
14 |
+
}
|
15 |
+
|
16 |
+
export default function InteractiveAvatarTextInput({
|
17 |
+
label,
|
18 |
+
placeholder,
|
19 |
+
input,
|
20 |
+
onSubmit,
|
21 |
+
setInput,
|
22 |
+
endContent,
|
23 |
+
disabled = false,
|
24 |
+
loading = false,
|
25 |
+
}: StreamingAvatarTextInputProps) {
|
26 |
+
function handleSubmit() {
|
27 |
+
if (input.trim() === "") {
|
28 |
+
return;
|
29 |
+
}
|
30 |
+
onSubmit();
|
31 |
+
setInput("");
|
32 |
+
}
|
33 |
+
|
34 |
+
return (
|
35 |
+
<Input
|
36 |
+
endContent={
|
37 |
+
<div className="flex flex-row items-center h-full">
|
38 |
+
{endContent}
|
39 |
+
<Tooltip content="Send message">
|
40 |
+
{loading ? (
|
41 |
+
<Spinner
|
42 |
+
className="text-indigo-300 hover:text-indigo-200"
|
43 |
+
size="sm"
|
44 |
+
color="default"
|
45 |
+
/>
|
46 |
+
) : (
|
47 |
+
<button
|
48 |
+
type="submit"
|
49 |
+
className="focus:outline-none"
|
50 |
+
onClick={handleSubmit}
|
51 |
+
>
|
52 |
+
<PaperPlaneRight
|
53 |
+
className={clsx(
|
54 |
+
"text-indigo-300 hover:text-indigo-200",
|
55 |
+
disabled && "opacity-50"
|
56 |
+
)}
|
57 |
+
size={24}
|
58 |
+
/>
|
59 |
+
</button>
|
60 |
+
)}
|
61 |
+
</Tooltip>
|
62 |
+
</div>
|
63 |
+
}
|
64 |
+
label={label}
|
65 |
+
placeholder={placeholder}
|
66 |
+
size="sm"
|
67 |
+
value={input}
|
68 |
+
onKeyDown={(e) => {
|
69 |
+
if (e.key === "Enter") {
|
70 |
+
handleSubmit();
|
71 |
+
}
|
72 |
+
}}
|
73 |
+
onValueChange={setInput}
|
74 |
+
isDisabled={disabled}
|
75 |
+
/>
|
76 |
+
);
|
77 |
+
}
|
components/NavBar.tsx
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import {
|
4 |
+
Link,
|
5 |
+
Navbar,
|
6 |
+
NavbarBrand,
|
7 |
+
NavbarContent,
|
8 |
+
NavbarItem,
|
9 |
+
} from "@nextui-org/react";
|
10 |
+
import { GithubIcon, HeyGenLogo } from "./Icons";
|
11 |
+
import { ThemeSwitch } from "./ThemeSwitch";
|
12 |
+
|
13 |
+
export default function NavBar() {
|
14 |
+
return (
|
15 |
+
<Navbar className="w-full">
|
16 |
+
<NavbarBrand>
|
17 |
+
<Link isExternal aria-label="HeyGen" href="https://app.heygen.com/">
|
18 |
+
<HeyGenLogo />
|
19 |
+
</Link>
|
20 |
+
<div className="bg-gradient-to-br from-sky-300 to-indigo-500 bg-clip-text ml-4">
|
21 |
+
<p className="text-xl font-semibold text-transparent">
|
22 |
+
tvam policy assistant
|
23 |
+
</p>
|
24 |
+
</div>
|
25 |
+
</NavbarBrand>
|
26 |
+
{/* <NavbarContent justify="center">
|
27 |
+
<NavbarItem className="flex flex-row items-center gap-4">
|
28 |
+
<Link
|
29 |
+
isExternal
|
30 |
+
color="foreground"
|
31 |
+
href="https://labs.heygen.com/interactive-avatar"
|
32 |
+
>
|
33 |
+
Avatars
|
34 |
+
</Link>
|
35 |
+
<Link
|
36 |
+
isExternal
|
37 |
+
color="foreground"
|
38 |
+
href="https://docs.heygen.com/reference/list-voices-v2"
|
39 |
+
>
|
40 |
+
Voices
|
41 |
+
</Link>
|
42 |
+
<Link
|
43 |
+
isExternal
|
44 |
+
color="foreground"
|
45 |
+
href="https://docs.heygen.com/reference/new-session-copy"
|
46 |
+
>
|
47 |
+
API Docs
|
48 |
+
</Link>
|
49 |
+
<Link
|
50 |
+
isExternal
|
51 |
+
color="foreground"
|
52 |
+
href="https://help.heygen.com/en/articles/9182113-interactive-avatar-101-your-ultimate-guide"
|
53 |
+
>
|
54 |
+
Guide
|
55 |
+
</Link>
|
56 |
+
<Link
|
57 |
+
isExternal
|
58 |
+
aria-label="Github"
|
59 |
+
href="https://github.com/HeyGen-Official/StreamingAvatarSDK"
|
60 |
+
className="flex flex-row justify-center gap-1 text-foreground"
|
61 |
+
>
|
62 |
+
<GithubIcon className="text-default-500" />
|
63 |
+
SDK
|
64 |
+
</Link>
|
65 |
+
<ThemeSwitch />
|
66 |
+
</NavbarItem> */}
|
67 |
+
{/* </NavbarContent> */}
|
68 |
+
</Navbar>
|
69 |
+
);
|
70 |
+
}
|
components/ThemeSwitch.tsx
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { FC } from "react";
|
4 |
+
import { VisuallyHidden } from "@react-aria/visually-hidden";
|
5 |
+
import { SwitchProps, useSwitch } from "@nextui-org/switch";
|
6 |
+
import { useTheme } from "next-themes";
|
7 |
+
import { useIsSSR } from "@react-aria/ssr";
|
8 |
+
import clsx from "clsx";
|
9 |
+
import { MoonFilledIcon, SunFilledIcon } from "./Icons";
|
10 |
+
|
11 |
+
export interface ThemeSwitchProps {
|
12 |
+
className?: string;
|
13 |
+
classNames?: SwitchProps["classNames"];
|
14 |
+
}
|
15 |
+
|
16 |
+
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
17 |
+
className,
|
18 |
+
classNames,
|
19 |
+
}) => {
|
20 |
+
const { theme, setTheme } = useTheme();
|
21 |
+
const isSSR = useIsSSR();
|
22 |
+
|
23 |
+
const onChange = () => {
|
24 |
+
theme === "light" ? setTheme("dark") : setTheme("light");
|
25 |
+
};
|
26 |
+
|
27 |
+
const {
|
28 |
+
Component,
|
29 |
+
slots,
|
30 |
+
isSelected,
|
31 |
+
getBaseProps,
|
32 |
+
getInputProps,
|
33 |
+
getWrapperProps,
|
34 |
+
} = useSwitch({
|
35 |
+
isSelected: theme === "light" || isSSR,
|
36 |
+
"aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
|
37 |
+
onChange,
|
38 |
+
});
|
39 |
+
|
40 |
+
return (
|
41 |
+
<Component
|
42 |
+
{...getBaseProps({
|
43 |
+
className: clsx(
|
44 |
+
"px-px transition-opacity hover:opacity-80 cursor-pointer",
|
45 |
+
className,
|
46 |
+
classNames?.base
|
47 |
+
),
|
48 |
+
})}
|
49 |
+
>
|
50 |
+
<VisuallyHidden>
|
51 |
+
<input {...getInputProps()} />
|
52 |
+
</VisuallyHidden>
|
53 |
+
<div
|
54 |
+
{...getWrapperProps()}
|
55 |
+
className={slots.wrapper({
|
56 |
+
class: clsx(
|
57 |
+
[
|
58 |
+
"w-auto h-auto",
|
59 |
+
"bg-transparent",
|
60 |
+
"rounded-lg",
|
61 |
+
"flex items-center justify-center",
|
62 |
+
"group-data-[selected=true]:bg-transparent",
|
63 |
+
"!text-default-500",
|
64 |
+
"pt-px",
|
65 |
+
"px-0",
|
66 |
+
"mx-0",
|
67 |
+
],
|
68 |
+
classNames?.wrapper
|
69 |
+
),
|
70 |
+
})}
|
71 |
+
>
|
72 |
+
{!isSelected || isSSR ? (
|
73 |
+
<SunFilledIcon size={24} />
|
74 |
+
) : (
|
75 |
+
<MoonFilledIcon size={24} />
|
76 |
+
)}
|
77 |
+
</div>
|
78 |
+
</Component>
|
79 |
+
);
|
80 |
+
};
|
next.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('next').NextConfig} */
|
2 |
+
const nextConfig = {
|
3 |
+
output: 'standalone', // Enables standalone output for optimized server builds
|
4 |
+
};
|
5 |
+
|
6 |
+
module.exports = nextConfig;
|
package.json
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "next-app-template",
|
3 |
+
"version": "0.0.1",
|
4 |
+
"private": true,
|
5 |
+
"scripts": {
|
6 |
+
"dev": "node_modules/next/dist/bin/next dev",
|
7 |
+
"build": "next build",
|
8 |
+
"start": "next start",
|
9 |
+
"lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix"
|
10 |
+
},
|
11 |
+
"dependencies": {
|
12 |
+
"@ai-sdk/openai": "^0.0.34",
|
13 |
+
"@heygen/streaming-avatar": "^2.0.8",
|
14 |
+
"@nextui-org/button": "2.0.34",
|
15 |
+
"@nextui-org/chip": "^2.0.32",
|
16 |
+
"@nextui-org/code": "2.0.29",
|
17 |
+
"@nextui-org/input": "2.2.2",
|
18 |
+
"@nextui-org/kbd": "2.0.30",
|
19 |
+
"@nextui-org/link": "2.0.32",
|
20 |
+
"@nextui-org/listbox": "2.1.21",
|
21 |
+
"@nextui-org/navbar": "2.0.33",
|
22 |
+
"@nextui-org/react": "^2.4.2",
|
23 |
+
"@nextui-org/snippet": "2.0.38",
|
24 |
+
"@nextui-org/switch": "2.0.31",
|
25 |
+
"@nextui-org/system": "2.2.1",
|
26 |
+
"@nextui-org/theme": "2.2.5",
|
27 |
+
"@phosphor-icons/react": "^2.1.5",
|
28 |
+
"@react-aria/ssr": "3.9.4",
|
29 |
+
"@react-aria/visually-hidden": "3.8.12",
|
30 |
+
"@uiw/codemirror-extensions-langs": "^4.22.1",
|
31 |
+
"@uiw/react-codemirror": "^4.22.1",
|
32 |
+
"ahooks": "^3.8.1",
|
33 |
+
"ai": "^3.2.15",
|
34 |
+
"clsx": "2.1.1",
|
35 |
+
"framer-motion": "~11.1.1",
|
36 |
+
"intl-messageformat": "^10.5.0",
|
37 |
+
"next": "14.2.4",
|
38 |
+
"next-themes": "^0.2.1",
|
39 |
+
"openai": "^4.52.1",
|
40 |
+
"react": "18.3.1",
|
41 |
+
"react-dom": "18.3.1",
|
42 |
+
"zod": "^3.23.8"
|
43 |
+
},
|
44 |
+
"devDependencies": {
|
45 |
+
"@types/node": "20.5.7",
|
46 |
+
"@types/react": "18.3.3",
|
47 |
+
"@types/react-dom": "18.3.0",
|
48 |
+
"@typescript-eslint/eslint-plugin": "7.2.0",
|
49 |
+
"@typescript-eslint/parser": "7.2.0",
|
50 |
+
"autoprefixer": "10.4.19",
|
51 |
+
"eslint": "^8.57.0",
|
52 |
+
"eslint-config-next": "14.2.1",
|
53 |
+
"eslint-config-prettier": "^8.2.0",
|
54 |
+
"eslint-plugin-import": "^2.26.0",
|
55 |
+
"eslint-plugin-jsx-a11y": "^6.4.1",
|
56 |
+
"eslint-plugin-node": "^11.1.0",
|
57 |
+
"eslint-plugin-prettier": "^5.1.3",
|
58 |
+
"eslint-plugin-react": "^7.23.2",
|
59 |
+
"eslint-plugin-react-hooks": "^4.6.0",
|
60 |
+
"eslint-plugin-unused-imports": "^3.2.0",
|
61 |
+
"postcss": "8.4.38",
|
62 |
+
"tailwind-variants": "0.1.20",
|
63 |
+
"tailwindcss": "3.4.3",
|
64 |
+
"typescript": "5.0.4"
|
65 |
+
}
|
66 |
+
}
|
postcss.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
plugins: {
|
3 |
+
tailwindcss: {},
|
4 |
+
autoprefixer: {},
|
5 |
+
},
|
6 |
+
}
|
public/demo.png
ADDED
![]() |
Git LFS Details
|
public/heygen-logo.png
ADDED
![]() |
styles/globals.css
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
4 |
+
|
5 |
+
li {
|
6 |
+
list-style-type: square;
|
7 |
+
margin-left: 30px;
|
8 |
+
padding: 2px;
|
9 |
+
}
|
tailwind.config.js
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {nextui} from '@nextui-org/theme'
|
2 |
+
|
3 |
+
/** @type {import('tailwindcss').Config} */
|
4 |
+
module.exports = {
|
5 |
+
content: [
|
6 |
+
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
7 |
+
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
8 |
+
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
|
9 |
+
],
|
10 |
+
theme: {
|
11 |
+
extend: {
|
12 |
+
fontFamily: {
|
13 |
+
sans: ["var(--font-sans)"],
|
14 |
+
mono: ["var(--font-geist-mono)"],
|
15 |
+
},
|
16 |
+
},
|
17 |
+
},
|
18 |
+
darkMode: "class",
|
19 |
+
plugins: [nextui()],
|
20 |
+
}
|
tsconfig.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"target": "es5",
|
4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
5 |
+
"allowJs": true,
|
6 |
+
"skipLibCheck": true,
|
7 |
+
"strict": true,
|
8 |
+
"forceConsistentCasingInFileNames": true,
|
9 |
+
"noEmit": true,
|
10 |
+
"esModuleInterop": true,
|
11 |
+
"module": "esnext",
|
12 |
+
"moduleResolution": "node",
|
13 |
+
"resolveJsonModule": true,
|
14 |
+
"isolatedModules": true,
|
15 |
+
"jsx": "preserve",
|
16 |
+
"incremental": true,
|
17 |
+
"plugins": [
|
18 |
+
{
|
19 |
+
"name": "next"
|
20 |
+
}
|
21 |
+
],
|
22 |
+
"paths": {
|
23 |
+
"@/*": ["./*"]
|
24 |
+
}
|
25 |
+
},
|
26 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
27 |
+
"exclude": ["node_modules"]
|
28 |
+
}
|
yarn.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|