Skip to content

Commit

Permalink
create deepgram & microphone helpers for the template
Browse files Browse the repository at this point in the history
  • Loading branch information
ansh committed Aug 30, 2024
1 parent 2cfcc1d commit 24003fd
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 0 deletions.
Binary file added bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"@ai-sdk/anthropic": "^0.0.48",
"@ai-sdk/openai": "^0.0.54",
"@deepgram/sdk": "^3.6.0",
"ai": "^3.3.20",
"firebase": "^10.13.0",
"lucide-react": "^0.436.0",
Expand Down
55 changes: 55 additions & 0 deletions src/app/api/deepgram/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { DeepgramError, createClient } from "@deepgram/sdk";
import { NextResponse, type NextRequest } from "next/server";

export const revalidate = 0;

export async function GET(request: NextRequest) {
// exit early so we don't request 70000000 keys while in devmode
if (process.env.DEEPGRAM_ENV === "development") {
return NextResponse.json({
key: process.env.DEEPGRAM_API_KEY ?? "",
});
}

// gotta use the request object to invalidate the cache every request :vomit:
const url = request.url;
const deepgram = createClient(process.env.DEEPGRAM_API_KEY ?? "");

let { result: projectsResult, error: projectsError } = await deepgram.manage.getProjects();

if (projectsError) {
return NextResponse.json(projectsError);
}

const project = projectsResult?.projects[0];

if (!project) {
return NextResponse.json(
new DeepgramError("Cannot find a Deepgram project. Please create a project first.")
);
}

let { result: newKeyResult, error: newKeyError } = await deepgram.manage.createProjectKey(
project.project_id,
{
comment: "Temporary API key",
scopes: ["usage:write"],
tags: ["next.js"],
time_to_live_in_seconds: 60,
}
);

if (newKeyError) {
return NextResponse.json(newKeyError);
}

const response = NextResponse.json({ ...newKeyResult, url });
response.headers.set("Surrogate-Control", "no-store");
response.headers.set(
"Cache-Control",
"s-maxage=0, no-store, no-cache, must-revalidate, proxy-revalidate"
);
response.headers.set("Expires", "0");

return response;
}
99 changes: 99 additions & 0 deletions src/contexts/DeepgramContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"use client";

import {
createClient,
LiveClient,
SOCKET_STATES,
LiveTranscriptionEvents,
type LiveSchema,
type LiveTranscriptionEvent,
} from "@deepgram/sdk";

import { createContext, useContext, useState, ReactNode, FunctionComponent } from "react";

interface DeepgramContextType {
connection: LiveClient | null;
connectToDeepgram: (options: LiveSchema, endpoint?: string) => Promise<void>;
disconnectFromDeepgram: () => void;
connectionState: SOCKET_STATES;
}

const DeepgramContext = createContext<DeepgramContextType | undefined>(undefined);

interface DeepgramContextProviderProps {
children: ReactNode;
}

const getApiKey = async (): Promise<string> => {
const response = await fetch("/api/authenticate", { cache: "no-store" });
const result = await response.json();
return result.key;
};

const DeepgramContextProvider: FunctionComponent<DeepgramContextProviderProps> = ({ children }) => {
const [connection, setConnection] = useState<LiveClient | null>(null);
const [connectionState, setConnectionState] = useState<SOCKET_STATES>(SOCKET_STATES.closed);

/**
* Connects to the Deepgram speech recognition service and sets up a live transcription session.
*
* @param options - The configuration options for the live transcription session.
* @param endpoint - The optional endpoint URL for the Deepgram service.
* @returns A Promise that resolves when the connection is established.
*/
const connectToDeepgram = async (options: LiveSchema, endpoint?: string) => {
const key = await getApiKey();
const deepgram = createClient(key);

const conn = deepgram.listen.live(options, endpoint);

conn.addListener(LiveTranscriptionEvents.Open, () => {
setConnectionState(SOCKET_STATES.open);
});

conn.addListener(LiveTranscriptionEvents.Close, () => {
setConnectionState(SOCKET_STATES.closed);
});

setConnection(conn);
};

const disconnectFromDeepgram = async () => {
if (connection) {
connection.finish();
setConnection(null);
}
};

return (
<DeepgramContext.Provider
value={{
connection,
connectToDeepgram,
disconnectFromDeepgram,
connectionState,
}}
>
{children}
</DeepgramContext.Provider>
);
};

// Use the useDeepgram hook to access the deepgram context and use the deepgram in any component.
// This allows you to connect to the deepgram and disconnect from the deepgram via a socket.
// Make sure to wrap your application in a DeepgramContextProvider to use the deepgram.
function useDeepgram(): DeepgramContextType {
const context = useContext(DeepgramContext);
if (context === undefined) {
throw new Error("useDeepgram must be used within a DeepgramContextProvider");
}
return context;
}

export {
DeepgramContextProvider,
useDeepgram,
SOCKET_STATES,
LiveTranscriptionEvents,
type LiveTranscriptionEvent,
};
113 changes: 113 additions & 0 deletions src/contexts/MicrophoneContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use client";

import { createContext, useCallback, useContext, useState, ReactNode } from "react";

interface MicrophoneContextType {
microphone: MediaRecorder | null;
startMicrophone: () => void;
stopMicrophone: () => void;
setupMicrophone: () => void;
microphoneState: MicrophoneState | null;
}

export enum MicrophoneEvents {
DataAvailable = "dataavailable",
Error = "error",
Pause = "pause",
Resume = "resume",
Start = "start",
Stop = "stop",
}

export enum MicrophoneState {
NotSetup = -1,
SettingUp = 0,
Ready = 1,
Opening = 2,
Open = 3,
Error = 4,
Pausing = 5,
Paused = 6,
}

const MicrophoneContext = createContext<MicrophoneContextType | undefined>(undefined);

interface MicrophoneContextProviderProps {
children: ReactNode;
}

const MicrophoneContextProvider: React.FC<MicrophoneContextProviderProps> = ({ children }) => {
const [microphoneState, setMicrophoneState] = useState<MicrophoneState>(MicrophoneState.NotSetup);
const [microphone, setMicrophone] = useState<MediaRecorder | null>(null);

const setupMicrophone = async () => {
setMicrophoneState(MicrophoneState.SettingUp);

try {
const userMedia = await navigator.mediaDevices.getUserMedia({
audio: {
noiseSuppression: true,
echoCancellation: true,
},
});

const microphone = new MediaRecorder(userMedia);

setMicrophoneState(MicrophoneState.Ready);
setMicrophone(microphone);
} catch (err: any) {
console.error(err);

throw err;
}
};

const stopMicrophone = useCallback(() => {
setMicrophoneState(MicrophoneState.Pausing);

if (microphone?.state === "recording") {
microphone.pause();
setMicrophoneState(MicrophoneState.Paused);
}
}, [microphone]);

const startMicrophone = useCallback(() => {
setMicrophoneState(MicrophoneState.Opening);

if (microphone?.state === "paused") {
microphone.resume();
} else {
microphone?.start(250);
}

setMicrophoneState(MicrophoneState.Open);
}, [microphone]);

return (
<MicrophoneContext.Provider
value={{
microphone,
startMicrophone,
stopMicrophone,
setupMicrophone,
microphoneState,
}}
>
{children}
</MicrophoneContext.Provider>
);
};

// Use the useMicrophone hook to access the microphone context and use the microphone in any component.
// Make sure to wrap your application in a MicrophoneContextProvider to use the microphone.
function useMicrophone(): MicrophoneContextType {
const context = useContext(MicrophoneContext);

if (context === undefined) {
throw new Error("useMicrophone must be used within a MicrophoneContextProvider");
}

return context;
}

export { MicrophoneContextProvider, useMicrophone };

0 comments on commit 24003fd

Please sign in to comment.