├── react-avatar-app
├── src
│ ├── vite-env.d.ts
│ ├── utils
│ │ └── cn.ts
│ ├── main.tsx
│ ├── components
│ │ ├── ui
│ │ │ ├── textarea.tsx
│ │ │ ├── input.tsx
│ │ │ └── button.tsx
│ │ └── ThemeProvider.tsx
│ ├── index.css
│ ├── assets
│ │ └── react.svg
│ ├── App.css
│ └── App.tsx
├── postcss.config.js
├── tsconfig.json
├── .gitignore
├── vite.config.ts
├── index.html
├── tsconfig.node.json
├── utils
│ └── logger.js
├── tsconfig.app.json
├── eslint.config.js
├── package.json
├── public
│ └── vite.svg
├── tailwind.config.js
└── README.md
├── server
├── favicon.ico
├── .env.sample
├── .gitignore
├── routes
│ ├── chatRouter.js
│ └── personaRoutes.js
├── .prettierrc
├── config
│ └── config.js
├── package.json
├── server.js
├── utils
│ ├── persona.js
│ └── logger.js
├── services
│ ├── heygenService.js
│ └── aiService.js
├── controllers
│ ├── chatController.js
│ ├── personaController.js
│ └── heygenController.js
├── soham_bot.js
├── README.md
└── package-lock.json
└── README.md
/react-avatar-app/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/server/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LiehSt/Streaming-Avatar/HEAD/server/favicon.ico
--------------------------------------------------------------------------------
/server/.env.sample:
--------------------------------------------------------------------------------
1 | HEYGEN_APIKEY=
2 | HEYGEN_SERVER_URL=https://api.heygen.com
3 | GEMINI_APIKEY=
4 | PORT=3000
--------------------------------------------------------------------------------
/react-avatar-app/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
--------------------------------------------------------------------------------
/react-avatar-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 | .vscode
4 | .env
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | dist/
9 | coverage/
10 | .DS_Store
11 | *.log
12 | *.pid
13 | *.seed
14 | *.pid.lock
--------------------------------------------------------------------------------
/server/routes/chatRouter.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import { AIChatResponse, InitializeBot } from '../controllers/chatController.js';
3 | const chatRouter = Router();
4 |
5 | chatRouter.post('/complete', AIChatResponse);
6 | chatRouter.post('/', InitializeBot);
7 | export default chatRouter;
8 |
--------------------------------------------------------------------------------
/react-avatar-app/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | /**
5 | * Combines multiple class names using clsx and optimizes them with tailwind-merge
6 | */
7 | export function cn(...inputs: ClassValue[]) {
8 | return twMerge(clsx(inputs));
9 | }
--------------------------------------------------------------------------------
/react-avatar-app/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/react-avatar-app/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import { ThemeProvider } from './components/ThemeProvider.tsx'
5 | import './index.css'
6 |
7 | ReactDOM.createRoot(document.getElementById('root')!).render(
8 |
9 |
10 |
11 |
12 | ,
13 | )
14 |
--------------------------------------------------------------------------------
/react-avatar-app/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | proxy: {
9 | '/persona': {
10 | target: 'http://localhost:3000',
11 | changeOrigin: true,
12 | },
13 | '/openai': {
14 | target: 'http://localhost:3000',
15 | changeOrigin: true,
16 | }
17 | }
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": false,
4 | "trailingComma": "all",
5 | "printWidth": 100,
6 | "useTabs": false,
7 | "tabWidth": 2,
8 | "semi": true,
9 | "quoteProps": "as-needed",
10 | "bracketSpacing": true,
11 | "bracketSameLine": false,
12 | "arrowParens": "always",
13 | "singleAttributePerLine": false,
14 | "overrides": [
15 | {
16 | "files": ".prettierrc",
17 | "options": { "parser": "json" }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/server/config/config.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config();
3 |
4 | export const config = {
5 | heygen: {
6 | apiKey: process.env.HEYGEN_APIKEY,
7 | serverUrl: process.env.HEYGEN_SERVER_URL,
8 | defaultQuality: 'low',
9 | },
10 | gemini: {
11 | apiKey: process.env.GEMINI_APIKEY,
12 | model: 'gemini-2.0-flash',
13 | config: {
14 | temperature: 0.9,
15 | topP: 0.1,
16 | topK: 16,
17 | maxOutputTokens: 2048,
18 | candidateCount: 1,
19 | stopSequences: [],
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "streamingavatar",
3 | "version": "1.0.0",
4 | "description": "## Pre-Requisites",
5 | "type": "module",
6 | "main": "server.js",
7 | "keywords": [],
8 | "author": "",
9 | "license": "ISC",
10 | "dependencies": {
11 | "cors": "^2.8.5",
12 | "dotenv": "^16.5.0",
13 | "express": "^4.21.2",
14 | "mime-types": "^3.0.1"
15 | },
16 | "scripts": {
17 | "start": "node server.js",
18 | "dev": "nodemon server.js",
19 | "test": "echo \"Error: no test specified\" && exit 1",
20 | "lint": "eslint .",
21 | "format": "prettier --write ."
22 | },
23 | "devDependencies": {
24 | "prettier": "^3.2.4"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/react-avatar-app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Streaming Avatar
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/react-avatar-app/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import path from 'path';
3 | import { fileURLToPath } from 'url';
4 | import { logger } from './utils/logger.js';
5 | import personaRouter from './routes/personaRoutes.js';
6 | import chatRouter from './routes/chatRouter.js';
7 | import dotenv from 'dotenv';
8 | import cors from 'cors';
9 | dotenv.config();
10 |
11 | const __filename = fileURLToPath(import.meta.url);
12 | const __dirname = path.dirname(__filename);
13 |
14 | const app = express();
15 | app.use(express.json());
16 | app.use(cors());
17 |
18 | app.use('/persona', personaRouter);
19 | app.use('/openai', chatRouter);
20 |
21 | app.listen(process.env.PORT, () => {
22 | logger.info('Server', `Server running at http://localhost:${process.env.PORT}`);
23 | });
24 |
--------------------------------------------------------------------------------
/react-avatar-app/utils/logger.js:
--------------------------------------------------------------------------------
1 | export const logger = {
2 | debug: (module, message, meta = {}) => {
3 | const timestamp = new Date().toISOString();
4 | console.debug(`[${timestamp}] [DEBUG] [${module}]`, message, meta);
5 | },
6 | info: (module, message, meta = {}) => {
7 | const timestamp = new Date().toISOString();
8 | console.info(`[${timestamp}] [INFO] [${module}]`, message, meta);
9 | },
10 | warn: (module, message, meta = {}) => {
11 | const timestamp = new Date().toISOString();
12 | console.warn(`[${timestamp}] [WARN] [${module}]`, message, meta);
13 | },
14 | error: (module, message, meta = {}) => {
15 | const timestamp = new Date().toISOString();
16 | console.error(`[${timestamp}] [ERROR] [${module}]`, message, meta);
17 | }
18 | };
--------------------------------------------------------------------------------
/react-avatar-app/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/react-avatar-app/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "../../utils/cn"
3 |
4 | export interface TextareaProps
5 | extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | }
20 | )
21 | Textarea.displayName = "Textarea"
22 |
23 | export { Textarea }
--------------------------------------------------------------------------------
/react-avatar-app/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/react-avatar-app/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "../../utils/cn"
3 |
4 | export interface InputProps
5 | extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | )
20 | }
21 | )
22 | Input.displayName = "Input"
23 |
24 | export { Input }
--------------------------------------------------------------------------------
/server/routes/personaRoutes.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import { personaDetails, updatePersonaDetails, getPersonaConfig, updatePersonaConfig } from '../controllers/personaController.js';
3 | import { initializeHeygenBot, createHeygenSession, startHeygenSession, sendHeygenText, stopHeygenSession, handleICECandidate } from '../controllers/heygenController.js';
4 |
5 | const personaRouter = Router();
6 |
7 | personaRouter.get('/', personaDetails);
8 | personaRouter.post('/', updatePersonaDetails);
9 | personaRouter.get('/config', getPersonaConfig);
10 | personaRouter.post('/update', updatePersonaConfig);
11 | personaRouter.post('/heygen/init', initializeHeygenBot);
12 | personaRouter.post('/heygen/session/create', createHeygenSession);
13 | personaRouter.post('/heygen/session/start', startHeygenSession);
14 | personaRouter.post('/heygen/ice', handleICECandidate);
15 | personaRouter.post('/heygen/text', sendHeygenText);
16 | personaRouter.post('/heygen/session/stop', stopHeygenSession);
17 |
18 | export default personaRouter;
19 |
--------------------------------------------------------------------------------
/react-avatar-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-avatar-app",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@tailwindcss/postcss": "^4.1.3",
14 | "class-variance-authority": "^0.7.1",
15 | "clsx": "^2.1.1",
16 | "react": "^19.0.0",
17 | "react-dom": "^19.0.0",
18 | "shadcn-ui": "^0.9.5",
19 | "tailwind-merge": "^3.2.0"
20 | },
21 | "devDependencies": {
22 | "@eslint/js": "^9.21.0",
23 | "@types/react": "^19.0.10",
24 | "@types/react-dom": "^19.0.4",
25 | "@vitejs/plugin-react": "^4.3.4",
26 | "autoprefixer": "^10.4.21",
27 | "eslint": "^9.21.0",
28 | "eslint-plugin-react-hooks": "^5.1.0",
29 | "eslint-plugin-react-refresh": "^0.4.19",
30 | "globals": "^15.15.0",
31 | "postcss": "^8.5.3",
32 | "tailwindcss": "^3.4.17",
33 | "typescript": "~5.7.2",
34 | "typescript-eslint": "^8.24.1",
35 | "vite": "^6.2.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/react-avatar-app/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/utils/persona.js:
--------------------------------------------------------------------------------
1 | export let persona = {
2 | "name": "Surya Ghosh",
3 | "title": "The Passionate Tech Explorer",
4 | "education": {
5 | "degree": "B.Tech in Electronics and Communication Engineering",
6 | "year": "3rd Year, 6th Semester",
7 | "institution": "Future Institute of Engineering and Management"
8 | },
9 | "traits": [
10 | "Curious",
11 | "passionate",
12 | "disciplined",
13 | "hardworking",
14 | "socially active"
15 | ],
16 | "technical": {
17 | "languages": [
18 | "Java",
19 | "C"
20 | ],
21 | "webStack": [
22 | "React",
23 | "Next.js",
24 | "Hono.js",
25 | "Drizzle ORM",
26 | "MongoDB"
27 | ],
28 | "projects": [
29 | "Women Safety App (gender classification + SMS alerts)",
30 | "CloneX – AI-powered digital human clone",
31 | "Obstacle Avoiding Robot",
32 | "Firefighting Robot with separate sensing unit",
33 | "ReelsPro – Media sharing Next.js app",
34 | "Astro.js based documentation site with login and backend",
35 | "Chat + Music Sync App"
36 | ]
37 | },
38 | "personality": {
39 | "style": "Goal-oriented, practical, and project-driven learner with a love for real-world applications",
40 | "interests": [
41 | "Artificial Intelligence & Deep Learning",
42 | "Robotics",
43 | "Full Stack Web Development",
44 | "Hackathons & Competitive Coding",
45 | "Building tech for social good"
46 | ],
47 | "goals": [
48 | "Revise and strengthen DSA, Java, and C fundamentals",
49 | "Build a successful hackathon project (April 12–13)",
50 | "Contribute daily to research work",
51 | "Maintain consistency despite distractions",
52 | "Balance academics, project work, and personal life"
53 | ]
54 | }
55 | };
--------------------------------------------------------------------------------
/server/utils/logger.js:
--------------------------------------------------------------------------------
1 | class Logger {
2 | constructor() {
3 | this.debugMode = true; // Set to false in production
4 | this.logHistory = [];
5 | }
6 |
7 | formatMessage(level, context, message, data = null) {
8 | const timestamp = new Date().toISOString();
9 | const logMessage = {
10 | timestamp,
11 | level,
12 | context,
13 | message,
14 | data: data ? JSON.stringify(data) : undefined,
15 | };
16 | this.logHistory.push(logMessage);
17 | return `[${timestamp}] [${level}] [${context}] ${message}${data ? '\nData: ' + JSON.stringify(data, null, 2) : ''}`;
18 | }
19 |
20 | log(level, context, message, data = null) {
21 | const formattedMessage = this.formatMessage(level, context, message, data);
22 | if (this.debugMode) {
23 | switch (level) {
24 | case 'ERROR':
25 | console.error(formattedMessage);
26 | break;
27 | case 'WARN':
28 | console.warn(formattedMessage);
29 | break;
30 | case 'INFO':
31 | console.info(formattedMessage);
32 | break;
33 | default:
34 | console.log(formattedMessage);
35 | }
36 | }
37 | return formattedMessage;
38 | }
39 |
40 | info(context, message, data = null) {
41 | return this.log('INFO', context, message, data);
42 | }
43 |
44 | warn(context, message, data = null) {
45 | return this.log('WARN', context, message, data);
46 | }
47 |
48 | error(context, message, data = null) {
49 | return this.log('ERROR', context, message, data);
50 | }
51 |
52 | debug(context, message, data = null) {
53 | return this.log('DEBUG', context, message, data);
54 | }
55 |
56 | getLogHistory() {
57 | return this.logHistory;
58 | }
59 |
60 | clearHistory() {
61 | this.logHistory = [];
62 | }
63 | }
64 |
65 | export const logger = new Logger();
66 |
--------------------------------------------------------------------------------
/react-avatar-app/src/components/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react";
2 |
3 | type Theme = "light" | "dark" | "system";
4 |
5 | type ThemeProviderProps = {
6 | children: React.ReactNode;
7 | defaultTheme?: Theme;
8 | storageKey?: string;
9 | };
10 |
11 | type ThemeProviderState = {
12 | theme: Theme;
13 | setTheme: (theme: Theme) => void;
14 | };
15 |
16 | const initialState: ThemeProviderState = {
17 | theme: "system",
18 | setTheme: () => null,
19 | };
20 |
21 | const ThemeProviderContext = createContext(initialState);
22 |
23 | export function ThemeProvider({
24 | children,
25 | defaultTheme = "system",
26 | storageKey = "vite-ui-theme",
27 | ...props
28 | }: ThemeProviderProps) {
29 | const [theme, setTheme] = useState(
30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31 | );
32 |
33 | useEffect(() => {
34 | const root = window.document.documentElement;
35 |
36 | root.classList.remove("light", "dark");
37 |
38 | if (theme === "system") {
39 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40 | .matches
41 | ? "dark"
42 | : "light";
43 |
44 | root.classList.add(systemTheme);
45 | return;
46 | }
47 |
48 | root.classList.add(theme);
49 | }, [theme]);
50 |
51 | const value = {
52 | theme,
53 | setTheme: (theme: Theme) => {
54 | localStorage.setItem(storageKey, theme);
55 | setTheme(theme);
56 | },
57 | };
58 |
59 | return (
60 |
61 | {children}
62 |
63 | );
64 | }
65 |
66 | export const useTheme = () => {
67 | const context = useContext(ThemeProviderContext);
68 |
69 | if (context === undefined)
70 | throw new Error("useTheme must be used within a ThemeProvider");
71 |
72 | return context;
73 | };
--------------------------------------------------------------------------------
/react-avatar-app/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 | import { cn } from "../../utils/cn"
4 |
5 | const buttonVariants = cva(
6 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
7 | {
8 | variants: {
9 | variant: {
10 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
11 | destructive:
12 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13 | outline:
14 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15 | secondary:
16 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
17 | ghost: "hover:bg-accent hover:text-accent-foreground",
18 | link: "text-primary underline-offset-4 hover:underline",
19 | },
20 | size: {
21 | default: "h-10 px-4 py-2",
22 | sm: "h-9 rounded-md px-3",
23 | lg: "h-11 rounded-md px-8",
24 | icon: "h-10 w-10",
25 | },
26 | },
27 | defaultVariants: {
28 | variant: "default",
29 | size: "default",
30 | },
31 | }
32 | )
33 |
34 | export interface ButtonProps
35 | extends React.ButtonHTMLAttributes,
36 | VariantProps {
37 | asChild?: boolean
38 | }
39 |
40 | const Button = React.forwardRef(
41 | ({ className, variant, size, asChild = false, ...props }, ref) => {
42 | const Comp = asChild ? React.Fragment : "button"
43 | return (
44 |
49 | )
50 | }
51 | )
52 | Button.displayName = "Button"
53 |
54 | export { Button, buttonVariants }
--------------------------------------------------------------------------------
/react-avatar-app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {
9 | colors: {
10 | background: "#ffffff",
11 | foreground: "#000000",
12 | border: "hsl(var(--border))",
13 | input: "hsl(var(--input))",
14 | ring: "hsl(var(--ring))",
15 | primary: {
16 | DEFAULT: "#3b82f6",
17 | foreground: "#ffffff",
18 | 50: "#eff6ff",
19 | 100: "#dbeafe",
20 | 200: "#bfdbfe",
21 | 300: "#93c5fd",
22 | 400: "#60a5fa",
23 | 500: "#3b82f6",
24 | 600: "#2563eb",
25 | 700: "#1d4ed8",
26 | 800: "#1e40af",
27 | 900: "#1e3a8a",
28 | 950: "#172554",
29 | },
30 | secondary: {
31 | DEFAULT: "#f3f4f6",
32 | foreground: "#111827",
33 | },
34 | destructive: {
35 | DEFAULT: "#ef4444",
36 | foreground: "#ffffff",
37 | },
38 | muted: {
39 | DEFAULT: "#f3f4f6",
40 | foreground: "#6b7280",
41 | },
42 | accent: {
43 | DEFAULT: "#f9fafb",
44 | foreground: "#111827",
45 | },
46 | popover: {
47 | DEFAULT: "#ffffff",
48 | foreground: "#020617",
49 | },
50 | card: {
51 | DEFAULT: "#ffffff",
52 | foreground: "#020617",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | fontFamily: {
61 | sans: ['Inter', 'sans-serif'],
62 | },
63 | boxShadow: {
64 | 'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
65 | },
66 | keyframes: {
67 | "accordion-down": {
68 | from: { height: 0 },
69 | to: { height: "var(--radix-accordion-content-height)" },
70 | },
71 | "accordion-up": {
72 | from: { height: "var(--radix-accordion-content-height)" },
73 | to: { height: 0 },
74 | },
75 | },
76 | animation: {
77 | "accordion-down": "accordion-down 0.2s ease-out",
78 | "accordion-up": "accordion-up 0.2s ease-out",
79 | },
80 | },
81 | },
82 | plugins: [],
83 | }
--------------------------------------------------------------------------------
/react-avatar-app/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --foreground-rgb: 0, 0, 0;
8 | --background-start-rgb: 250, 250, 250;
9 | --background-end-rgb: 240, 240, 240;
10 | }
11 |
12 | html {
13 | @apply scroll-smooth;
14 | }
15 |
16 | body {
17 | @apply text-gray-800 bg-gray-50 font-sans antialiased;
18 | }
19 |
20 | h1, h2, h3, h4, h5, h6 {
21 | @apply font-medium tracking-tight;
22 | }
23 | }
24 |
25 | @layer components {
26 | .main {
27 | @apply flex flex-col h-screen p-4 max-w-5xl mx-auto gap-4;
28 | }
29 |
30 | .configSection {
31 | @apply bg-white p-4 rounded-lg shadow;
32 | }
33 |
34 | .actionRowsWrap {
35 | @apply flex flex-col gap-2;
36 | }
37 |
38 | .actionRow {
39 | @apply flex items-center gap-2 flex-wrap;
40 | }
41 |
42 | .videoSectionWrap {
43 | @apply mt-4 flex flex-col;
44 | }
45 |
46 | .videoWrap {
47 | @apply w-full max-w-xl mx-auto overflow-hidden rounded-lg shadow-lg;
48 | }
49 |
50 | .videoEle {
51 | @apply w-full;
52 | }
53 |
54 | .show {
55 | @apply block;
56 | }
57 |
58 | .hide {
59 | @apply hidden;
60 | }
61 |
62 | .switchWrap {
63 | @apply flex items-center gap-2;
64 | }
65 |
66 | .btn {
67 | @apply inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none;
68 | }
69 |
70 | .btn-primary {
71 | @apply bg-blue-600 text-white hover:bg-blue-700 h-10 px-4 py-2;
72 | }
73 |
74 | .btn-secondary {
75 | @apply bg-white text-gray-900 border border-gray-200 hover:bg-gray-100 h-10 px-4 py-2;
76 | }
77 |
78 | .btn-danger {
79 | @apply bg-red-600 text-white hover:bg-red-700 h-10 px-4 py-2;
80 | }
81 |
82 | .input {
83 | @apply flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
84 | }
85 |
86 | .card {
87 | @apply rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm transition-all hover:shadow;
88 | }
89 |
90 | .formGroup {
91 | @apply mb-4;
92 | }
93 | }
94 |
95 | /* Custom scrollbar styling */
96 | @layer utilities {
97 | /* Width */
98 | ::-webkit-scrollbar {
99 | @apply w-2;
100 | }
101 |
102 | /* Track */
103 | ::-webkit-scrollbar-track {
104 | @apply bg-gray-100 rounded-full;
105 | }
106 |
107 | /* Handle */
108 | ::-webkit-scrollbar-thumb {
109 | @apply bg-gray-300 rounded-full;
110 | }
111 |
112 | /* Handle on hover */
113 | ::-webkit-scrollbar-thumb:hover {
114 | @apply bg-gray-400;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/server/services/heygenService.js:
--------------------------------------------------------------------------------
1 | import { config } from '../config/config.js';
2 |
3 | class HeygenService {
4 | constructor() {
5 | this.apiKey = config.heygen.apiKey;
6 | this.serverUrl = config.heygen.serverUrl;
7 | }
8 |
9 | async createSession(avatar_name, voice_id) {
10 | const response = await fetch(`${this.serverUrl}/v1/streaming.new`, {
11 | method: 'POST',
12 | headers: {
13 | 'Content-Type': 'application/json',
14 | 'X-Api-Key': this.apiKey,
15 | },
16 | body: JSON.stringify({
17 | quality: config.heygen.defaultQuality,
18 | avatar_name,
19 | voice: { voice_id },
20 | }),
21 | });
22 |
23 | const data = await response.json();
24 | if (response.status === 500 || data.code === 10013) {
25 | throw new Error(data.message || 'Failed to create session');
26 | }
27 | return data.data;
28 | }
29 |
30 | async startSession(session_id, sdp) {
31 | const response = await fetch(`${this.serverUrl}/v1/streaming.start`, {
32 | method: 'POST',
33 | headers: {
34 | 'Content-Type': 'application/json',
35 | 'X-Api-Key': this.apiKey,
36 | },
37 | body: JSON.stringify({ session_id, sdp }),
38 | });
39 |
40 | const data = await response.json();
41 | if (response.status === 500) {
42 | throw new Error('Failed to start session');
43 | }
44 | return data.data;
45 | }
46 |
47 | async handleICE(session_id, candidate) {
48 | const response = await fetch(`${this.serverUrl}/v1/streaming.ice`, {
49 | method: 'POST',
50 | headers: {
51 | 'Content-Type': 'application/json',
52 | 'X-Api-Key': this.apiKey,
53 | },
54 | body: JSON.stringify({ session_id, candidate }),
55 | });
56 |
57 | const data = await response.json();
58 | if (response.status === 500) {
59 | throw new Error('Failed to handle ICE candidate');
60 | }
61 | return data;
62 | }
63 |
64 | async sendText(session_id, text) {
65 | const response = await fetch(`${this.serverUrl}/v1/streaming.task`, {
66 | method: 'POST',
67 | headers: {
68 | 'Content-Type': 'application/json',
69 | 'X-Api-Key': this.apiKey,
70 | },
71 | body: JSON.stringify({ session_id, text }),
72 | });
73 |
74 | const data = await response.json();
75 | if (response.status === 500) {
76 | throw new Error('Failed to send text');
77 | }
78 | return data.data;
79 | }
80 |
81 | async stopSession(session_id) {
82 | const response = await fetch(`${this.serverUrl}/v1/streaming.stop`, {
83 | method: 'POST',
84 | headers: {
85 | 'Content-Type': 'application/json',
86 | 'X-Api-Key': this.apiKey,
87 | },
88 | body: JSON.stringify({ session_id }),
89 | });
90 |
91 | const data = await response.json();
92 | if (response.status === 500) {
93 | throw new Error('Failed to stop session');
94 | }
95 | return data.data;
96 | }
97 | }
98 |
99 | export const heygenService = new HeygenService();
100 |
--------------------------------------------------------------------------------
/server/services/aiService.js:
--------------------------------------------------------------------------------
1 | import { logger } from '../utils/logger.js';
2 | class AiService {
3 | constructor() {
4 | this.persona = '';
5 | this.personaConfig = null;
6 | logger.info('AiService', 'Service initialized');
7 | }
8 |
9 | async updatePersona() {
10 | logger.info('AiService', 'Updating persona configuration');
11 | try {
12 | const response = await fetch('http://localhost:3000/persona-config');
13 | if (!response.ok) {
14 | const error = 'Failed to fetch persona config';
15 | logger.error('AiService', error, { status: response.status });
16 | throw new Error(error);
17 | }
18 |
19 | this.personaConfig = await response.json();
20 | this.persona = this.generatePersonaPrompt(this.personaConfig);
21 | logger.info('AiService', 'Persona updated successfully', { name: this.personaConfig.name });
22 | return true;
23 | } catch (error) {
24 | logger.error('AiService', 'Failed to update persona', { error: error.message });
25 | return false;
26 | }
27 | }
28 |
29 | async initialize() {
30 | logger.info('AiService', 'Initializing AI service');
31 | try {
32 | await this.updatePersona();
33 | const response = await fetch('http://localhost:3000/openai/complete', {
34 | method: 'POST',
35 | headers: {
36 | 'Content-Type': 'application/json',
37 | },
38 | body: JSON.stringify({
39 | prompt: this.persona + `\nAct like ${this.personaConfig.name} always.`,
40 | }),
41 | });
42 |
43 | if (!response.ok) {
44 | const error = 'Failed to initialize AI';
45 | logger.error('AiService', error, { status: response.status });
46 | throw new Error(error);
47 | }
48 | logger.info('AiService', 'AI service initialized successfully');
49 | return true;
50 | } catch (error) {
51 | logger.error('AiService', 'AI initialization error', { error: error.message });
52 | return false;
53 | }
54 | }
55 |
56 | async getResponse(userInput) {
57 | logger.info('AiService', 'Getting AI response', { userInput });
58 | try {
59 | await this.updatePersona();
60 | const response = await fetch('http://localhost:3000/openai/complete', {
61 | method: 'POST',
62 | headers: {
63 | 'Content-Type': 'application/json',
64 | },
65 | body: JSON.stringify({
66 | prompt: `${this.persona}\nUser: ${userInput}\n${this.personaConfig.name}:`,
67 | }),
68 | });
69 |
70 | if (!response.ok) {
71 | const error = 'Failed to get AI response';
72 | logger.error('AiService', error, { status: response.status });
73 | throw new Error(error);
74 | }
75 |
76 | const data = await response.json();
77 | logger.info('AiService', 'Got AI response successfully', {
78 | responseLength: data.text.length,
79 | });
80 | return data.text;
81 | } catch (error) {
82 | logger.error('AiService', 'AI response error', { error: error.message });
83 | throw error;
84 | }
85 | }
86 | }
87 |
88 | export const aiService = new AiService();
89 |
--------------------------------------------------------------------------------
/server/controllers/chatController.js:
--------------------------------------------------------------------------------
1 | import { config } from '../config/config.js';
2 | import { logger } from '../utils/logger.js';
3 | import { persona } from '../utils/persona.js';
4 | import { GoogleGenerativeAI } from '@google/generative-ai';
5 |
6 | const genAI = new GoogleGenerativeAI(config.gemini_api_key);
7 | const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
8 |
9 | const getPersonaPrompt = () => {
10 | return `From today your identity is ${persona.name}.
11 | From now you are ${persona.name} and give all answers like ${persona.name}'s style.
12 | Don't use emojis in your answers.
13 | Behave like ${persona.name} in all chats.
14 | Always reply like ${persona.name}'s real talking style, attitude, tone & mood.
15 |
16 | Profile:
17 | Name: ${persona.name}
18 | Title: ${persona.title}
19 | Education: ${persona.education.degree} (${persona.education.year})
20 | Institution: ${persona.education.institution}
21 | Core Traits: ${persona.traits.join(', ')}
22 |
23 | Technical Identity:
24 | Languages Known: ${persona.technical.languages.join(', ')}
25 | Web Dev Stack: ${persona.technical.webStack.join(', ')}
26 | Projects:
27 | ${persona.technical.projects.join('\n')}
28 |
29 | Personality & Style:
30 | ${persona.personality.style}
31 | Interests: ${persona.personality.interests.join(', ')}
32 | Current Goals:
33 | ${persona.personality.goals.join('\n')}`;
34 | };
35 |
36 | const generateGeminiResponse = async (prompt) => {
37 | const personaPrompt = getPersonaPrompt();
38 | const chat = model.startChat({
39 | history: [
40 | {
41 | role: 'user',
42 | parts: [`Act like ${persona.name}, a formal corporate employee.`]
43 | },
44 | {
45 | role: 'model',
46 | parts: [`Understood. Responding as ${persona.name} professionally.`]
47 | },
48 | {
49 | role: 'user',
50 | parts: [personaPrompt]
51 | }
52 | ]
53 | });
54 | const result = await chat.sendMessage(prompt);
55 | return result.response.text();
56 | };
57 |
58 | export const InitializeBot = async (req, res) => {
59 | logger.info('Server', 'Initializing AI service');
60 | try {
61 | const result = await model.generateContent([
62 | { role: 'user', parts: ['You are now initialized. Be ready to chat.'] }
63 | ]);
64 | const text = result.response.text();
65 |
66 | logger.info('Server', 'AI service initialized successfully');
67 | res.json({
68 | success: true,
69 | message: 'AI service initialized successfully',
70 | text
71 | });
72 | } catch (error) {
73 | logger.error('Server', 'AI initialization error', { error: error.message });
74 | res.status(500).json({
75 | success: false,
76 | message: 'Error initializing AI service',
77 | error: error.message
78 | });
79 | }
80 | };
81 |
82 | export const AIChatResponse = async (req, res) => {
83 | logger.info('Server', 'Processing AI chat request');
84 | try {
85 | const userPrompt = req.body.prompt;
86 | const text = await generateGeminiResponse(userPrompt);
87 |
88 | logger.info('Server', 'AI response generated successfully');
89 | res.json({
90 | success: true,
91 | message: 'AI response generated successfully',
92 | text
93 | });
94 | } catch (error) {
95 | logger.error('Server', 'AI Error', { error: error.message });
96 | res.status(500).json({
97 | success: false,
98 | message: 'Error processing your request',
99 | error: error.message
100 | });
101 | }
102 | };
103 |
--------------------------------------------------------------------------------
/react-avatar-app/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/controllers/personaController.js:
--------------------------------------------------------------------------------
1 | import { persona } from '../utils/persona.js';
2 | import { logger } from '../utils/logger.js';
3 | import fs from 'fs';
4 | import path from 'path';
5 | import { fileURLToPath } from 'url';
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 | const personaFilePath = path.join(__dirname, '../utils/persona.js');
10 |
11 | export const personaDetails = (req, res) => {
12 | logger.info('PersonaController', 'Fetching persona details');
13 | res.json(persona);
14 | };
15 |
16 | export const updatePersonaDetails = (req, res) => {
17 | logger.info('PersonaController', 'Updating persona details', req.body);
18 |
19 | try {
20 | const personaChanges = req.body;
21 |
22 | // Update persona details
23 | if (personaChanges.name) persona.name = personaChanges.name;
24 | if (personaChanges.title) persona.title = personaChanges.title;
25 |
26 | // Education
27 | if (personaChanges.education) {
28 | if (personaChanges.education.degree) persona.education.degree = personaChanges.education.degree;
29 | if (personaChanges.education.year) persona.education.year = personaChanges.education.year;
30 | if (personaChanges.education.institution) persona.education.institution = personaChanges.education.institution;
31 | }
32 |
33 | res.json({
34 | success: true,
35 | message: 'Persona details updated successfully',
36 | persona
37 | });
38 | } catch (error) {
39 | logger.error('PersonaController', 'Error updating persona details', error);
40 | res.status(500).json({
41 | success: false,
42 | message: 'Error updating persona details',
43 | error: error.message
44 | });
45 | }
46 | };
47 |
48 | export const getPersonaConfig = (req, res) => {
49 | logger.info('PersonaController', 'Fetching persona configuration');
50 | res.json(persona);
51 | };
52 |
53 | export const updatePersonaConfig = async (req, res) => {
54 | try {
55 | logger.info('PersonaController', 'Updating persona configuration');
56 | const newConfig = req.body;
57 |
58 | if (!newConfig) {
59 | return res.status(400).json({
60 | success: false,
61 | message: 'No configuration data provided'
62 | });
63 | }
64 |
65 | // Update the persona object
66 | if (newConfig.name) persona.name = newConfig.name;
67 | if (newConfig.title) persona.title = newConfig.title;
68 |
69 | // Update education
70 | if (newConfig.education) {
71 | if (newConfig.education.degree) persona.education.degree = newConfig.education.degree;
72 | if (newConfig.education.year) persona.education.year = newConfig.education.year;
73 | if (newConfig.education.institution) persona.education.institution = newConfig.education.institution;
74 | }
75 |
76 | // Update technical details
77 | if (newConfig.technical) {
78 | if (newConfig.technical.languages) persona.technical.languages = newConfig.technical.languages;
79 | if (newConfig.technical.webStack) persona.technical.webStack = newConfig.technical.webStack;
80 | if (newConfig.technical.projects) persona.technical.projects = newConfig.technical.projects;
81 | }
82 |
83 | // Update personality
84 | if (newConfig.personality) {
85 | if (newConfig.personality.style) persona.personality.style = newConfig.personality.style;
86 | if (newConfig.personality.interests) persona.personality.interests = newConfig.personality.interests;
87 | if (newConfig.personality.goals) persona.personality.goals = newConfig.personality.goals;
88 | }
89 |
90 | // Update traits
91 | if (newConfig.traits) persona.traits = newConfig.traits;
92 |
93 | // Optionally, save to file (for persistance)
94 | const personaCode = `export let persona = ${JSON.stringify(persona, null, 2)};`;
95 | fs.writeFileSync(personaFilePath, personaCode);
96 |
97 | res.json({
98 | success: true,
99 | message: 'Persona configuration updated successfully',
100 | data: persona
101 | });
102 | } catch (error) {
103 | logger.error('PersonaController', 'Error updating persona configuration', { error: error.message });
104 | res.status(500).json({
105 | success: false,
106 | message: 'Error updating persona configuration',
107 | error: error.message
108 | });
109 | }
110 | };
111 |
--------------------------------------------------------------------------------
/react-avatar-app/README.md:
--------------------------------------------------------------------------------
1 | # Streaming Avatar React Application
2 |
3 | A modern React application built with Vite, TypeScript, and shadcn/ui that provides a user interface for interacting with the Streaming Avatar backend.
4 |
5 | ## 🚀 Features
6 |
7 | - **Persona Management**
8 | - View and update persona details
9 | - Configure persona settings
10 | - Manage persona's technical and personality traits
11 |
12 | - **HeyGen Integration**
13 | - Create and manage avatar streaming sessions
14 | - Real-time avatar interaction
15 | - WebRTC-based video streaming
16 | - Text-to-speech capabilities
17 |
18 | - **AI Chat Integration**
19 | - Interactive chat with persona-based responses
20 | - Gemini AI-powered conversations
21 | - Context-aware responses
22 |
23 | - **UI Features**
24 | - Modern, responsive design
25 | - Real-time video display
26 | - Session management controls
27 | - Background removal toggle
28 | - Dark/Light mode support
29 |
30 | ## 🛠️ Tech Stack
31 |
32 | ### Frontend
33 | - React 19 with TypeScript
34 | - Vite for build tooling
35 | - Tailwind CSS for styling
36 | - shadcn/ui for components
37 | - WebRTC for video streaming
38 |
39 | ### Backend Integration
40 | - Express.js server
41 | - HeyGen API for avatar streaming
42 | - Gemini AI for chat responses
43 |
44 | ## 📋 Prerequisites
45 |
46 | Before you begin, ensure you have:
47 | - Node.js (v18 or higher)
48 | - npm (v9 or higher)
49 | - HeyGen API credentials
50 | - Gemini AI API key
51 |
52 | ## 🛠️ Installation
53 |
54 | 1. Clone the repository:
55 | ```bash
56 | git clone
57 | cd react-avatar-app
58 | ```
59 |
60 | 2. Install dependencies:
61 | ```bash
62 | npm install
63 | ```
64 |
65 | 3. Set up environment variables:
66 | Create a `.env` file in the root directory with:
67 | ```env
68 | VITE_API_URL=http://localhost:3000
69 | VITE_GEMINI_API_KEY=your_gemini_api_key_here
70 | ```
71 |
72 | ## 🚀 Running the Application
73 |
74 | ### Development Mode
75 | ```bash
76 | npm run dev
77 | ```
78 | The application will be available at `http://localhost:5173`
79 |
80 | ### Production Build
81 | ```bash
82 | npm run build
83 | npm run preview
84 | ```
85 |
86 | ## 📝 Available Scripts
87 |
88 | - `npm run dev` - Start the Vite development server
89 | - `npm run build` - Build the application for production
90 | - `npm run preview` - Preview the production build
91 | - `npm run lint` - Run ESLint
92 | - `npm run format` - Format code with Prettier
93 |
94 | ## 🔌 API Integration
95 |
96 | The application integrates with the following backend endpoints:
97 |
98 | ### Persona Management
99 | - `GET /persona` - Get current persona details
100 | - `POST /persona` - Update persona details
101 | - `GET /persona/config` - Get persona configuration
102 | - `POST /persona/update` - Update persona configuration
103 |
104 | ### HeyGen Integration
105 | - `POST /persona/heygen/init` - Initialize HeyGen bot
106 | - `POST /persona/heygen/session/create` - Create streaming session
107 | - `POST /persona/heygen/session/start` - Start streaming session
108 | - `POST /persona/heygen/text` - Send text to avatar
109 | - `POST /persona/heygen/ice` - Handle WebRTC ICE candidate
110 | - `POST /persona/heygen/session/stop` - Stop streaming session
111 |
112 | ### Chat Integration
113 | - `POST /openai` - Initialize chat bot
114 | - `POST /openai/complete` - Get chat response
115 |
116 | ## 📁 Project Structure
117 |
118 | ```
119 | react-avatar-app/
120 | ├── src/
121 | │ ├── components/ # Reusable UI components
122 | │ ├── pages/ # Page components
123 | │ ├── services/ # API service functions
124 | │ ├── utils/ # Utility functions
125 | │ ├── hooks/ # Custom React hooks
126 | │ ├── types/ # TypeScript type definitions
127 | │ ├── styles/ # Global styles
128 | │ └── App.tsx # Main application component
129 | ├── public/ # Static assets
130 | └── package.json # Project dependencies
131 | ```
132 |
133 | ## 🤝 Contributing
134 |
135 | 1. Fork the repository
136 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
137 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
138 | 4. Push to the branch (`git push origin feature/AmazingFeature`)
139 | 5. Open a Pull Request
140 |
141 | ## 📄 License
142 |
143 | This project is licensed under the ISC License.
144 |
145 | ## 👥 Authors
146 |
147 | - Your Name - Initial work
148 |
149 | ## 🙏 Acknowledgments
150 |
151 | - Thanks to all contributors who have helped shape this project
152 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Streaming Avatar 🌟
2 |
3 | Welcome to the **Streaming Avatar** repository! This full-stack application enables real-time avatar streaming using HeyGen and Gemini AI. Built during a hackathon by Team "Bit by Bit," this project showcases the power of modern web technologies.
4 |
5 | [](https://github.com/LiehSt/Streaming-Avatar/releases)
6 |
7 | ## Table of Contents
8 |
9 | - [Features](#features)
10 | - [Technologies Used](#technologies-used)
11 | - [Getting Started](#getting-started)
12 | - [Usage](#usage)
13 | - [Contributing](#contributing)
14 | - [License](#license)
15 | - [Contact](#contact)
16 |
17 | ## Features
18 |
19 | - **Real-time Avatar Streaming**: Users can create and stream avatars in real-time.
20 | - **User-Friendly Interface**: Built with React and Tailwind CSS for a smooth user experience.
21 | - **TypeScript Support**: Strong typing for better code quality and maintainability.
22 | - **API Integration**: Seamless integration with HeyGen and Gemini AI APIs.
23 | - **Responsive Design**: Works on all devices, from desktops to mobile.
24 |
25 | ## Technologies Used
26 |
27 | This project utilizes a variety of technologies to deliver a robust application:
28 |
29 | - **Frontend**:
30 | - React
31 | - TypeScript
32 | - Tailwind CSS
33 |
34 | - **Backend**:
35 | - Node.js
36 | - Express
37 |
38 | - **APIs**:
39 | - HeyGen API
40 | - Gemini API
41 |
42 | ## Getting Started
43 |
44 | To get started with the Streaming Avatar application, follow these steps:
45 |
46 | 1. **Clone the Repository**:
47 | ```bash
48 | git clone https://github.com/LiehSt/Streaming-Avatar.git
49 | cd Streaming-Avatar
50 | ```
51 |
52 | 2. **Install Dependencies**:
53 | For the frontend:
54 | ```bash
55 | cd client
56 | npm install
57 | ```
58 |
59 | For the backend:
60 | ```bash
61 | cd server
62 | npm install
63 | ```
64 |
65 | 3. **Set Up Environment Variables**:
66 | Create a `.env` file in the `server` directory and add your API keys for HeyGen and Gemini.
67 |
68 | 4. **Run the Application**:
69 | Start the backend server:
70 | ```bash
71 | cd server
72 | npm start
73 | ```
74 |
75 | Then, in a new terminal, start the frontend:
76 | ```bash
77 | cd client
78 | npm start
79 | ```
80 |
81 | 5. **Access the Application**:
82 | Open your browser and go to `http://localhost:3000` to view the application.
83 |
84 | ## Usage
85 |
86 | Once the application is running, you can start creating avatars. Follow these steps:
87 |
88 | 1. **Create an Avatar**: Use the provided form to customize your avatar.
89 | 2. **Stream Your Avatar**: Click on the stream button to start broadcasting.
90 | 3. **Interact**: Engage with other users in real-time.
91 |
92 | ## Contributing
93 |
94 | We welcome contributions! To contribute to the Streaming Avatar project:
95 |
96 | 1. **Fork the Repository**: Click on the "Fork" button at the top right corner of this page.
97 | 2. **Create a Branch**:
98 | ```bash
99 | git checkout -b feature/YourFeature
100 | ```
101 | 3. **Make Your Changes**: Implement your feature or fix a bug.
102 | 4. **Commit Your Changes**:
103 | ```bash
104 | git commit -m "Add Your Feature"
105 | ```
106 | 5. **Push to Your Branch**:
107 | ```bash
108 | git push origin feature/YourFeature
109 | ```
110 | 6. **Create a Pull Request**: Go to the original repository and click on "New Pull Request."
111 |
112 | ## License
113 |
114 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
115 |
116 | ## Contact
117 |
118 | For any questions or feedback, feel free to reach out:
119 |
120 | - **Team Bit by Bit**: [Contact Us](mailto:teambitbybit@example.com)
121 |
122 | ## Additional Resources
123 |
124 | For more information on the technologies used, check out the following links:
125 |
126 | - [React Documentation](https://reactjs.org/docs/getting-started.html)
127 | - [TypeScript Documentation](https://www.typescriptlang.org/docs/)
128 | - [Tailwind CSS Documentation](https://tailwindcss.com/docs/installation)
129 | - [Node.js Documentation](https://nodejs.org/en/docs/)
130 | - [Express Documentation](https://expressjs.com/en/starter/installing.html)
131 |
132 | ## Releases
133 |
134 | You can find the latest releases of the Streaming Avatar application [here](https://github.com/LiehSt/Streaming-Avatar/releases). Download the latest version and execute it to experience the app.
135 |
136 | 
137 |
138 | ## Conclusion
139 |
140 | Thank you for checking out the Streaming Avatar project. We hope you find it useful and enjoyable. Feel free to contribute and help us improve the application. Happy streaming!
--------------------------------------------------------------------------------
/react-avatar-app/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
44 | .switch {
45 | position: relative;
46 | display: inline-block;
47 | width: 60px;
48 | height: 34px;
49 | }
50 |
51 | .switch input {
52 | opacity: 0;
53 | width: 0;
54 | height: 0;
55 | }
56 |
57 | .slider {
58 | position: absolute;
59 | cursor: pointer;
60 | top: 0;
61 | left: 0;
62 | right: 0;
63 | bottom: 0;
64 | background-color: #ccc;
65 | transition: .4s;
66 | }
67 |
68 | .slider:before {
69 | position: absolute;
70 | content: "";
71 | height: 26px;
72 | width: 26px;
73 | left: 4px;
74 | bottom: 4px;
75 | background-color: white;
76 | transition: .4s;
77 | }
78 |
79 | input:checked + .slider {
80 | background-color: #2196F3;
81 | }
82 |
83 | input:focus + .slider {
84 | box-shadow: 0 0 1px #2196F3;
85 | }
86 |
87 | input:checked + .slider:before {
88 | transform: translateX(26px);
89 | }
90 |
91 | .slider.round {
92 | border-radius: 34px;
93 | }
94 |
95 | .slider.round:before {
96 | border-radius: 50%;
97 | }
98 |
99 | .main {
100 | width: 100%;
101 | max-width: 1200px;
102 | margin: 0 auto;
103 | padding: 20px;
104 | font-family: Arial, sans-serif;
105 | }
106 |
107 | .configSection {
108 | margin-bottom: 20px;
109 | padding: 15px;
110 | border: 1px solid #ddd;
111 | border-radius: 5px;
112 | background-color: #f9f9f9;
113 | }
114 |
115 | .configSection h3 {
116 | margin-top: 0;
117 | margin-bottom: 15px;
118 | }
119 |
120 | .personaForm {
121 | margin-top: 15px;
122 | padding: 15px;
123 | border: 1px solid #eee;
124 | border-radius: 5px;
125 | background-color: #fff;
126 | }
127 |
128 | .formGroup {
129 | margin-bottom: 15px;
130 | }
131 |
132 | .formGroup h4 {
133 | margin-top: 5px;
134 | margin-bottom: 10px;
135 | }
136 |
137 | .formGroup label {
138 | display: block;
139 | margin-bottom: 8px;
140 | }
141 |
142 | .formGroup input[type="text"],
143 | .formGroup textarea {
144 | width: 100%;
145 | padding: 8px;
146 | border: 1px solid #ddd;
147 | border-radius: 4px;
148 | font-size: 14px;
149 | }
150 |
151 | .formGroup textarea {
152 | min-height: 80px;
153 | resize: vertical;
154 | }
155 |
156 | .actionRowsWrap {
157 | margin-bottom: 20px;
158 | }
159 |
160 | .actionRow {
161 | display: flex;
162 | flex-wrap: wrap;
163 | gap: 10px;
164 | align-items: center;
165 | margin-bottom: 10px;
166 | padding: 15px;
167 | border: 1px solid #ddd;
168 | border-radius: 5px;
169 | background-color: #f5f5f5;
170 | }
171 |
172 | .actionRow label {
173 | display: flex;
174 | flex-direction: column;
175 | flex: 1;
176 | gap: 5px;
177 | font-size: 14px;
178 | font-weight: bold;
179 | }
180 |
181 | .actionRow input {
182 | padding: 8px;
183 | border: 1px solid #ddd;
184 | border-radius: 4px;
185 | font-size: 14px;
186 | }
187 |
188 | button {
189 | padding: 8px 16px;
190 | background-color: #4285f4;
191 | color: white;
192 | border: none;
193 | border-radius: 4px;
194 | cursor: pointer;
195 | font-size: 14px;
196 | transition: background-color 0.2s;
197 | }
198 |
199 | button:hover {
200 | background-color: #3367d6;
201 | }
202 |
203 | button:disabled {
204 | background-color: #a0a0a0;
205 | cursor: not-allowed;
206 | }
207 |
208 | .statusWrap {
209 | margin-bottom: 20px;
210 | }
211 |
212 | .statusWrap h4 {
213 | margin-top: 0;
214 | margin-bottom: 5px;
215 | }
216 |
217 | .statusBox {
218 | height: 150px;
219 | overflow-y: auto;
220 | padding: 10px;
221 | border: 1px solid #ddd;
222 | border-radius: 5px;
223 | background-color: #f5f5f5;
224 | font-family: monospace;
225 | font-size: 14px;
226 | }
227 |
228 | .videoSectionWrap {
229 | display: flex;
230 | flex-direction: column;
231 | align-items: center;
232 | }
233 |
234 | .videoWrap {
235 | position: relative;
236 | width: 100%;
237 | max-width: 640px;
238 | border: 1px solid #ddd;
239 | border-radius: 5px;
240 | overflow: hidden;
241 | background-color: #000;
242 | }
243 |
244 | .videoEle, .canvasEle {
245 | display: block;
246 | width: 100%;
247 | height: auto;
248 | }
249 |
250 | .hide {
251 | display: none;
252 | }
253 |
254 | .show {
255 | display: block;
256 | }
257 |
258 | .bgControlsWrap {
259 | margin-top: 10px;
260 | padding: 10px;
261 | border: 1px solid #ddd;
262 | border-radius: 5px;
263 | background-color: #f9f9f9;
264 | }
265 |
266 | .bgControlsWrap.hide {
267 | display: none;
268 | }
269 |
270 | .checkbox-wrapper {
271 | display: flex;
272 | align-items: center;
273 | }
274 |
275 | .checkbox-wrapper label {
276 | display: flex;
277 | align-items: center;
278 | gap: 8px;
279 | cursor: pointer;
280 | }
281 |
282 | @media (max-width: 768px) {
283 | .actionRow {
284 | flex-direction: column;
285 | align-items: stretch;
286 | }
287 |
288 | .actionRow label {
289 | margin-bottom: 10px;
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/server/soham_bot.js:
--------------------------------------------------------------------------------
1 | import { persona } from "./utils/persona.js";
2 |
3 | /**
4 | * Initialize the chatbot with the persona of Surya
5 | * @returns {Promise} - Success status
6 | */
7 | async function initializeSuryaBot() {
8 | try {
9 | const response = await fetch('http://localhost:3000/persona/heygen/init', {
10 | method: 'POST',
11 | headers: {
12 | 'Content-Type': 'application/json',
13 | },
14 | body: JSON.stringify({
15 | persona_name: persona.name
16 | })
17 | });
18 |
19 | if (!response.ok) {
20 | throw new Error('Failed to initialize Surya bot');
21 | }
22 |
23 | console.log('Surya bot initialized successfully');
24 | return true;
25 | } catch (error) {
26 | console.error('Error initializing Surya bot:', error);
27 | return false;
28 | }
29 | }
30 |
31 | /**
32 | * Get AI response for the user input
33 | * @param {string} userInput - User message
34 | * @returns {Promise} - AI generated response
35 | */
36 | async function getSuryaResponse(userInput) {
37 | try {
38 | const response = await fetch('http://localhost:3000/openai/complete', {
39 | method: 'POST',
40 | headers: {
41 | 'Content-Type': 'application/json',
42 | },
43 | body: JSON.stringify({
44 | prompt: `User: ${userInput}\nAI:`,
45 | includePersona: true
46 | })
47 | });
48 |
49 | if (!response.ok) {
50 | throw new Error('Failed to get Surya response');
51 | }
52 |
53 | const data = await response.json();
54 | return data.text;
55 | } catch (error) {
56 | console.error('Error getting Surya response:', error);
57 | return 'Sorry, I had a glitch. Can you ask that again?';
58 | }
59 | }
60 |
61 | /**
62 | * Create a session with Heygen using the provided avatar ID
63 | * @param {string} avatarId - Heygen avatar ID
64 | * @param {string} voiceId - Heygen voice ID
65 | * @returns {Promise