├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── api │ ├── deepgram │ │ └── route.ts │ ├── groq │ │ └── route.ts │ └── neets │ │ └── route.ts ├── dg.svg ├── favicon.ico ├── globals.css ├── layout.tsx ├── microphone.tsx ├── page.tsx └── recording.svg ├── deepgram.toml ├── media └── groq.webm ├── next.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── click.png ├── deepgram.svg └── speak.png ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | certificates -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Groq-Powered Real-Time Voice Assistant with Next.Js neets deepgram 2 | 3 | Voice-driven interactions with groq, a real-time voice assistant that seamlessly blends Next.js for web functionality, Groq for LLM, Deepgram for live transcription TTS with Neets for TTS. [Next.js](https://nextjs.org/) [Deepgram](https://deepgram.com/) [Groq](https://groq.com/) [Neets](https://neets.ai/). 4 | 5 | ## Example Video 6 | [Example Video](https://github.com/serkandyck/realtime-voice-assistant-groq/assets/12444059/f2823fcb-d42f-4901-be17-f61d6e38e1c0) 7 | 8 | ## Quickstart 9 | 10 | ### Manual 11 | 12 | Follow these steps to get started with this application. 13 | 14 | #### Clone the repository 15 | 16 | Go to GitHub and [clone the repository](https://github.com/serkandyck/realtime-voice-assistant-groq). 17 | 18 | #### Install dependencies 19 | 20 | Install the project dependencies. 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | #### Edit the config file 27 | 28 | Copy the code from `sample.env.local` and create a new file called `.env.local`. Paste in the code and enter your API key. 29 | 30 | ```bash 31 | DEEPGRAM_API_KEY="apikey" 32 | NEETS_API_KEY="apikey" 33 | GROQ_API_KEY="apikey" 34 | ``` 35 | 36 | #### Run the application 37 | 38 | Once running, you can [access the application in your browser](http://localhost:3000). 39 | 40 | ```bash 41 | npm run dev 42 | ``` 43 | 44 | ## Author 45 | 46 | [SERKAN DAYICIK](https://www.linkedin.com/in/serkandyck/) 47 | -------------------------------------------------------------------------------- /app/api/deepgram/route.ts: -------------------------------------------------------------------------------- 1 | import { DeepgramError, createClient } from "@deepgram/sdk"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(request: Request) { 5 | // gotta use the request object to invalidate the cache every request :vomit: 6 | const url = request.url; 7 | const deepgram = createClient(process.env.NEXT_PUBLIC_DEEPGRAM_API_KEY ?? ""); 8 | 9 | let { result: projectsResult, error: projectsError } = 10 | await deepgram.manage.getProjects(); 11 | console.log(projectsResult); 12 | if (projectsError) { 13 | return NextResponse.json(projectsError); 14 | } 15 | console.log(projectsResult); 16 | 17 | return NextResponse.json({ ...projectsResult, url, key: process.env.NEXT_PUBLIC_DEEPGRAM_API_KEY }); 18 | } 19 | -------------------------------------------------------------------------------- /app/api/groq/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from "next/server"; 2 | 3 | export const GET = async () => { 4 | const apiKey = process.env.NEXT_PUBLIC_GROQ_API_KEY; 5 | return NextResponse.json({ apiKey: apiKey }, { status: 200 }); 6 | }; 7 | -------------------------------------------------------------------------------- /app/api/neets/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from 'next/server'; 2 | 3 | export const GET = async () => { 4 | const apiKey = process.env.NEXT_PUBLIC_NEETS_API_KEY 5 | return NextResponse.json({ apiKey: apiKey }, { status: 200 }); 6 | }; -------------------------------------------------------------------------------- /app/dg.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serkandyck/realtime-voice-assistant-groq/652531fc452a77843aeac453568e9c546ee2977f/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Live transcription by Deepgram in Next.js", 9 | description: 10 | "Generated by create next app, live transcription by Deepgram in Next.js", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/microphone.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | CreateProjectKeyResponse, 5 | LiveClient, 6 | LiveTranscriptionEvents, 7 | createClient, 8 | } from "@deepgram/sdk"; 9 | import { useState, useEffect, useCallback, } from "react"; 10 | import { useQueue } from "@uidotdev/usehooks"; 11 | import Recording from "./recording.svg"; 12 | import axios from "axios"; 13 | import Siriwave from 'react-siriwave'; 14 | 15 | import ChatGroq from "groq-sdk"; 16 | 17 | 18 | export default function Microphone() { 19 | const { add, remove, first, size, queue } = useQueue([]); 20 | const [apiKey, setApiKey] = useState(); 21 | const [neetsApiKey, setNeetsApiKey] = useState(); 22 | const [groqClient, setGroqClient] = useState(); 23 | const [connection, setConnection] = useState(); 24 | const [isListening, setListening] = useState(false); 25 | const [isLoadingKey, setLoadingKey] = useState(true); 26 | const [isLoading, setLoading] = useState(true); 27 | const [isProcessing, setProcessing] = useState(false); 28 | const [micOpen, setMicOpen] = useState(false); 29 | const [microphone, setMicrophone] = useState(); 30 | const [userMedia, setUserMedia] = useState(); 31 | const [caption, setCaption] = useState(); 32 | const [audio, setAudio] = useState(); 33 | 34 | const toggleMicrophone = useCallback(async () => { 35 | if (microphone && userMedia) { 36 | setUserMedia(null); 37 | setMicrophone(null); 38 | 39 | microphone.stop(); 40 | } else { 41 | const userMedia = await navigator.mediaDevices.getUserMedia({ 42 | audio: true, 43 | }); 44 | 45 | const microphone = new MediaRecorder(userMedia); 46 | microphone.start(500); 47 | 48 | microphone.onstart = () => { 49 | setMicOpen(true); 50 | }; 51 | 52 | microphone.onstop = () => { 53 | setMicOpen(false); 54 | }; 55 | 56 | microphone.ondataavailable = (e) => { 57 | add(e.data); 58 | }; 59 | 60 | setUserMedia(userMedia); 61 | setMicrophone(microphone); 62 | } 63 | }, [add, microphone, userMedia]); 64 | 65 | useEffect(() => { 66 | if (!groqClient) { 67 | console.log("getting a new groqClient"); 68 | const groq = new ChatGroq({ apiKey: process.env.NEXT_PUBLIC_GROQ_API_KEY, dangerouslyAllowBrowser: true }); 69 | setGroqClient(groq); 70 | setLoadingKey(false); 71 | } 72 | }, [groqClient]); 73 | 74 | useEffect(() => { 75 | if (!neetsApiKey) { 76 | console.log("getting a new neets api key"); 77 | setNeetsApiKey(process.env.NEXT_PUBLIC_NEETS_API_KEY); 78 | setLoadingKey(false); 79 | } 80 | }, [neetsApiKey]); 81 | 82 | useEffect(() => { 83 | if (!apiKey) { 84 | console.log("getting a new api key"); 85 | fetch("/api/deepgram", { cache: "no-store" }) 86 | .then((res) => res.json()) 87 | .then((object) => { 88 | if (!("key" in object)) throw new Error("No api key returned"); 89 | console.log(object) 90 | setApiKey(object); 91 | setLoadingKey(false); 92 | }) 93 | .catch((e) => { 94 | console.error(e); 95 | }); 96 | } 97 | }, [apiKey]); 98 | 99 | useEffect(() => { 100 | 101 | if (apiKey && "key" in apiKey) { 102 | console.log("connecting to deepgram"); 103 | const deepgram = createClient(apiKey?.key ?? ""); 104 | const connection = deepgram.listen.live({ 105 | model: "nova", 106 | interim_results: false, 107 | language: "en-US", 108 | smart_format: true, 109 | }); 110 | 111 | connection.on(LiveTranscriptionEvents.Open, () => { 112 | console.log("connection established"); 113 | setListening(true); 114 | }); 115 | 116 | connection.on(LiveTranscriptionEvents.Close, () => { 117 | console.log("connection closed"); 118 | setListening(false); 119 | setApiKey(null); 120 | setConnection(null); 121 | }); 122 | 123 | connection.on(LiveTranscriptionEvents.Transcript, (data) => { 124 | const words = data.channel.alternatives[0].words; 125 | const caption = words 126 | .map((word: any) => word.punctuated_word ?? word.word) 127 | .join(" "); 128 | if (caption !== "") { 129 | setCaption(caption); 130 | if (data.is_final) { 131 | if (groqClient) { 132 | const completion = groqClient.chat.completions 133 | .create({ 134 | messages: [ 135 | { 136 | role: "assistant", 137 | content: "You are communicating with the user on a phone, so your answers should not be too long and go directly to the essence of the sentences.", 138 | }, 139 | { 140 | role: "user", 141 | content: caption, 142 | } 143 | ], 144 | model: "mixtral-8x7b-32768", 145 | }) 146 | .then((chatCompletion) => { 147 | if (neetsApiKey) { 148 | setCaption(chatCompletion.choices[0]?.message?.content || ""); 149 | axios.post("https://api.neets.ai/v1/tts", { 150 | text: chatCompletion.choices[0]?.message?.content || "", 151 | voice_id: 'us-female-2', 152 | params: { 153 | model: 'style-diff-500' 154 | } 155 | }, 156 | { 157 | headers: { 158 | 'Content-Type': 'application/json', 159 | 'X-API-Key': neetsApiKey 160 | }, 161 | responseType: 'arraybuffer' 162 | } 163 | ).then((response) => { 164 | const blob = new Blob([response.data], { type: 'audio/mp3' }); 165 | const url = URL.createObjectURL(blob); 166 | 167 | const audio = new Audio(url); 168 | setAudio(audio); 169 | console.log('Playing audio.'); 170 | 171 | audio.play(); 172 | }) 173 | .catch((error) => { 174 | console.error(error); 175 | }); 176 | } 177 | }); 178 | 179 | } 180 | } 181 | } 182 | }); 183 | 184 | setConnection(connection); 185 | setLoading(false); 186 | } 187 | }, [apiKey]); 188 | 189 | useEffect(() => { 190 | const processQueue = async () => { 191 | if (size > 0 && !isProcessing) { 192 | setProcessing(true); 193 | 194 | if (isListening) { 195 | const blob = first; 196 | connection?.send(blob); 197 | remove(); 198 | } 199 | 200 | const waiting = setTimeout(() => { 201 | clearTimeout(waiting); 202 | setProcessing(false); 203 | }, 250); 204 | } 205 | }; 206 | 207 | processQueue(); 208 | }, [connection, queue, remove, first, size, isProcessing, isListening]); 209 | 210 | function handleAudio() { 211 | return audio && audio.currentTime > 0 && !audio.paused && !audio.ended && audio.readyState > 2; 212 | } 213 | 214 | if (isLoadingKey) 215 | return ( 216 | Loading temporary API key... 217 | ); 218 | if (isLoading) 219 | return Loading the app...; 220 | 221 | return ( 222 |
223 |
224 | 228 |
229 |
230 | 241 |
242 | {caption} 243 |
244 |
245 | 246 |
247 | ); 248 | } 249 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Image from "next/image"; 3 | import Microphone from "./microphone"; 4 | import { FaGithub } from "react-icons/fa"; 5 | import { CiLinkedin } from "react-icons/ci"; 6 | import Siriwave from 'react-siriwave'; 7 | 8 | 9 | export default async function Home() { 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/recording.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /deepgram.toml: -------------------------------------------------------------------------------- 1 | [meta] 2 | title = "Live audio Next.js Starter" 3 | description = "Basic demo for using Deepgram to transcribe microphone audio in Next.js" 4 | author = "Deepgram DX Team (https://developers.deepgram.com)" 5 | useCase = "Live" 6 | language = "JavaScript" 7 | framework = "Next.js" 8 | 9 | [build] 10 | command = "npm install" 11 | 12 | [config] 13 | sample = "sample.env.local" 14 | output = ".env.local" 15 | 16 | [post-build] 17 | message = "Run `npm run dev` to get up and running locally." 18 | -------------------------------------------------------------------------------- /media/groq.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serkandyck/realtime-voice-assistant-groq/652531fc452a77843aeac453568e9c546ee2977f/media/groq.webm -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | webpack(config) { 4 | config.module.rules.push({ 5 | test: /\.svg$/, 6 | use: ["@svgr/webpack"], 7 | }); 8 | 9 | return config; 10 | }, 11 | reactStrictMode: false, 12 | }; 13 | 14 | module.exports = nextConfig; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "live-nextjs-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@deepgram/sdk": "^3.0.0-beta.2", 13 | "@langchain/groq": "^0.0.1", 14 | "@svgr/webpack": "^8.1.0", 15 | "@uidotdev/usehooks": "^2.4.1", 16 | "axios": "^1.6.7", 17 | "fs": "0.0.1-security", 18 | "groq-sdk": "^0.3.0", 19 | "next": "14.0.1", 20 | "react": "^18", 21 | "react-dom": "^18", 22 | "react-icons": "^5.0.1", 23 | "react-siriwave": "^3.1.0", 24 | "swr": "^2.2.4" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20", 28 | "@types/react": "^18", 29 | "@types/react-dom": "^18", 30 | "autoprefixer": "^10.0.1", 31 | "eslint": "^8", 32 | "eslint-config-next": "14.0.1", 33 | "postcss": "^8", 34 | "tailwindcss": "^3.3.0", 35 | "typescript": "^5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serkandyck/realtime-voice-assistant-groq/652531fc452a77843aeac453568e9c546ee2977f/public/click.png -------------------------------------------------------------------------------- /public/deepgram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/speak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serkandyck/realtime-voice-assistant-groq/652531fc452a77843aeac453568e9c546ee2977f/public/speak.png -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | dropShadow: { 12 | glowBlue: [ 13 | "0px 0px 2px #000", 14 | "0px 0px 4px #000", 15 | "0px 0px 30px #0141ff", 16 | "0px 0px 100px #0141ff80", 17 | ], 18 | glowRed: [ 19 | "0px 0px 2px #f00", 20 | "0px 0px 4px #000", 21 | "0px 0px 15px #ff000040", 22 | "0px 0px 30px #f00", 23 | "0px 0px 100px #ff000080", 24 | ], 25 | }, 26 | backgroundImage: { 27 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 28 | "gradient-conic": 29 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 30 | }, 31 | }, 32 | }, 33 | plugins: [], 34 | }; 35 | export default config; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------