Building the audio recording interface
Whisper’s core interaction is a recording modal where users can capture audio directly in the browser:useAudioRecording hook, which handles all the browser audio recording logic.
Documentation Index
Fetch the complete documentation index at: /llms.txt
Use this file to discover all available pages before exploring further.
Learn how to build a real-time AI audio transcription app with Whisper, Next.js, and Together AI.
function RecordingModal({ onClose }: { onClose: () => void }) {
const { recording, audioBlob, startRecording, stopRecording } =
useAudioRecording();
const handleRecordingToggle = async () => {
if (recording) {
stopRecording();
} else {
await startRecording();
}
};
// Auto-process when we get an audio blob
useEffect(() => {
if (audioBlob) {
handleSaveRecording();
}
}, [audioBlob]);
return (
<Dialog open onOpenChange={onClose}>
<DialogContent>
<Button onClick={handleRecordingToggle}>
{recording ? "Stop Recording" : "Start Recording"}
</Button>
</DialogContent>
</Dialog>
);
}
useAudioRecording hook, which handles all the browser audio recording logic.
function useAudioRecording() {
const [recording, setRecording] = useState(false);
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
const startRecording = async () => {
try {
// Request microphone access
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Create MediaRecorder
const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder;
chunksRef.current = [];
// Collect audio data
mediaRecorder.ondataavailable = (e) => {
chunksRef.current.push(e.data);
};
// Create blob when recording stops
mediaRecorder.onstop = () => {
const blob = new Blob(chunksRef.current, { type: "audio/webm" });
setAudioBlob(blob);
// Stop all tracks to release microphone
stream.getTracks().forEach((track) => track.stop());
};
mediaRecorder.start();
setRecording(true);
} catch (err) {
console.error("Microphone access denied:", err);
}
};
const stopRecording = () => {
if (mediaRecorderRef.current && recording) {
mediaRecorderRef.current.stop();
setRecording(false);
}
};
return { recording, audioBlob, startRecording, stopRecording };
}
const handleSaveRecording = async () => {
if (!audioBlob) return;
try {
// Upload to S3
const file = new File([audioBlob], `recording-${Date.now()}.webm`, {
type: "audio/webm",
});
const { url } = await uploadToS3(file);
// Call our tRPC endpoint
const { id } = await transcribeMutation.mutateAsync({
audioUrl: url,
language: selectedLanguage,
durationSeconds: duration,
});
// Navigate to transcription page
router.push(`/whispers/${id}`);
} catch (err) {
toast.error("Failed to transcribe audio. Please try again.");
}
};
import { Together } from "together-ai";
import { createTogetherAI } from "@ai-sdk/togetherai";
import { generateText } from "ai";
export const whisperRouter = t.router({
transcribeFromS3: protectedProcedure
.input(
z.object({
audioUrl: z.string(),
language: z.string().optional(),
durationSeconds: z.number().min(1),
})
)
.mutation(async ({ input, ctx }) => {
// Call Together AI's Whisper model
const togetherClient = new Together({
apiKey: process.env.TOGETHER_API_KEY,
});
const res = await togetherClient.audio.transcriptions.create({
file: input.audioUrl,
model: "openai/whisper-large-v3",
language: input.language || "en",
});
const transcription = res.text as string;
// Generate a title using LLM
const togetherAI = createTogetherAI({
apiKey: process.env.TOGETHER_API_KEY,
});
const { text: title } = await generateText({
prompt: `Generate a title for the following transcription with max of 10 words: ${transcription}`,
model: togetherAI("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
maxTokens: 10,
});
// Save to database
const whisperId = uuidv4();
await prisma.whisper.create({
data: {
id: whisperId,
title: title.slice(0, 80),
userId: ctx.auth.userId,
fullTranscription: transcription,
audioTracks: {
create: [
{
fileUrl: input.audioUrl,
partialTranscription: transcription,
language: input.language,
},
],
},
},
});
return { id: whisperId };
}),
});
import Dropzone from "react-dropzone";
import { useS3Upload } from "next-s3-upload";
function UploadModal({ onClose }: { onClose: () => void }) {
const { uploadToS3 } = useS3Upload();
const handleDrop = useCallback(async (acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (!file) return;
try {
// Get audio duration and upload in parallel
const [duration, { url }] = await Promise.all([
getDuration(file),
uploadToS3(file),
]);
// Transcribe using the same endpoint
const { id } = await transcribeMutation.mutateAsync({
audioUrl: url,
language,
durationSeconds: Math.round(duration),
});
router.push(`/whispers/${id}`);
} catch (err) {
toast.error("Failed to transcribe audio. Please try again.");
}
}, []);
return (
<Dropzone
accept={{
"audio/mpeg3": [".mp3"],
"audio/wav": [".wav"],
"audio/mp4": [".m4a"],
}}
onDrop={handleDrop}
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drop audio files here or click to upload</p>
</div>
)}
</Dropzone>
);
}
import { createTogetherAI } from "@ai-sdk/togetherai";
import { generateText } from "ai";
const transformText = async (prompt: string, transcription: string) => {
const togetherAI = createTogetherAI({
apiKey: process.env.TOGETHER_API_KEY,
});
const { text } = await generateText({
prompt: `${prompt}\n\nTranscription: ${transcription}`,
model: togetherAI("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
});
return text;
};
const transcribeMutation = useMutation(
trpc.whisper.transcribeFromS3.mutationOptions()
);
// TypeScript knows the exact shape of the input and output
const result = await transcribeMutation.mutateAsync({
audioUrl: "...",
language: "en", // TypeScript validates this
durationSeconds: 120,
});
// result.id is properly typed
router.push(`/whispers/${result.id}`);
Was this page helpful?