├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------