├── .env.example ├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── actions ├── deleteTranslation.ts └── translate.ts ├── app ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx ├── transcribeAudio │ └── route.ts ├── translate │ └── page.tsx └── translationHistory │ └── route.ts ├── components.json ├── components ├── DeleteTranslationButton.tsx ├── Header.tsx ├── LanguageSelect.tsx ├── Recorder.tsx ├── SubmitButton.tsx ├── TimeAgoText.tsx ├── TranslationForm.tsx ├── TranslationHistory.tsx └── ui │ ├── button.tsx │ ├── select.tsx │ └── textarea.tsx ├── lib └── utils.ts ├── middleware.ts ├── mongodb ├── db.ts └── models │ └── User.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=put_key_here 2 | CLERK_SECRET_KEY=put_key_here 3 | AZURE_TEXT_TRANSLATION=put_key_here 4 | AZURE_TEXT_TRANSLATION_KEY=put_key_here 5 | AZURE_TEXT_LOCATION=put_key_here 6 | 7 | AZURE_API_KEY=put_key_here 8 | AZURE_ENDPOINT=put_key_here 9 | AZURE_DEPLOYMENT_NAME=put_key_here 10 | 11 | MONGO_DB_USERNAME=put_key_here 12 | MONGO_DB_PASSWORD=put_key_here -------------------------------------------------------------------------------- /.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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /actions/deleteTranslation.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { removeTranslation } from "@/mongodb/models/User"; 4 | import { auth } from "@clerk/nextjs/server"; 5 | import { revalidateTag } from "next/cache"; 6 | 7 | async function deleteTranslation(id: string) { 8 | auth().protect(); 9 | 10 | const { userId } = auth(); 11 | 12 | const user = await removeTranslation(userId!, id); 13 | 14 | revalidateTag("translationHistory"); 15 | 16 | return { 17 | translations: JSON.stringify(user.translations), 18 | }; 19 | } 20 | 21 | export default deleteTranslation; 22 | -------------------------------------------------------------------------------- /actions/translate.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { State } from "@/components/TranslationForm"; 4 | import connectDB from "@/mongodb/db"; 5 | import { addOrUpdateUser } from "@/mongodb/models/User"; 6 | import axios from "axios"; 7 | import { v4 } from "uuid"; 8 | import { auth } from "@clerk/nextjs/server"; 9 | import { revalidateTag } from "next/cache"; 10 | 11 | const key = process.env.AZURE_TEXT_TRANSLATION_KEY; 12 | const endpoint = process.env.AZURE_TEXT_TRANSLATION; 13 | const location = process.env.AZURE_TEXT_LOCATION; 14 | 15 | async function translate(prevState: State, formData: FormData) { 16 | auth().protect(); 17 | 18 | const { userId } = auth(); 19 | 20 | if (!userId) throw new Error("User not found"); 21 | 22 | const rawFormData = { 23 | input: formData.get("input") as string, 24 | inputLanguage: formData.get("inputLanguage") as string, 25 | output: formData.get("output") as string, 26 | outputLanguage: formData.get("outputLanguage") as string, 27 | }; 28 | 29 | const response = await axios({ 30 | baseURL: endpoint, 31 | url: "translate", 32 | method: "POST", 33 | headers: { 34 | "Ocp-Apim-Subscription-Key": key!, 35 | "Ocp-Apim-Subscription-Region": location!, 36 | "Content-type": "application/json", 37 | "X-ClientTraceId": v4().toString(), 38 | }, 39 | params: { 40 | "api-version": "3.0", 41 | from: 42 | rawFormData.inputLanguage === "auto" ? null : rawFormData.inputLanguage, 43 | to: rawFormData.outputLanguage, 44 | }, 45 | data: [ 46 | { 47 | text: rawFormData.input, 48 | }, 49 | ], 50 | responseType: "json", 51 | }); 52 | 53 | const data = response.data; 54 | 55 | if (data.error) { 56 | console.log(`Error ${data.error.code}: ${data.error.message}`); 57 | } 58 | 59 | // MongoDB 60 | await connectDB(); 61 | 62 | if (rawFormData.inputLanguage === "auto") { 63 | rawFormData.inputLanguage = data[0].detectedLanguage.language; 64 | } 65 | 66 | try { 67 | const translation = { 68 | to: rawFormData.outputLanguage, 69 | from: rawFormData.inputLanguage, 70 | fromText: rawFormData.input, 71 | toText: data[0].translations[0].text, 72 | }; 73 | 74 | addOrUpdateUser(userId, translation); 75 | } catch (err) { 76 | console.error(err); 77 | } 78 | 79 | revalidateTag("translationHistory"); 80 | 81 | return { 82 | ...prevState, 83 | output: data[0].translations[0].text, 84 | }; 85 | } 86 | 87 | export default translate; 88 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PapaReact/google-translate-2.0-clone-nextjs-14-ms-azure-clerk-openai-mongodb/9caf7001e6f2a2d51ff7e3d214970a2f9401419e/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import Header from "@/components/Header"; 5 | import { auth } from "@clerk/nextjs/server"; 6 | import { ClerkProvider } from "@clerk/nextjs"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Translate 2.0 Clone", 12 | description: "Generated by create next app", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 24 |
25 | 26 |
{children}
27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { SignInButton } from "@clerk/nextjs"; 3 | import { auth } from "@clerk/nextjs/server"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | 7 | export default async function Home() { 8 | const { userId } = auth(); 9 | 10 | return ( 11 |
12 |

13 | Understand your world and communicate across languages 14 |

{" "} 15 | logo 21 | {userId ? ( 22 | 26 | Translate Now 27 | 28 | ) : ( 29 | 34 | )} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/transcribeAudio/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | import { AzureKeyCredential, OpenAIClient } from "@azure/openai"; 4 | 5 | export async function POST(request: NextRequest) { 6 | const formData = await request.formData(); 7 | const file = formData.get("audio") as File; 8 | console.log(">>", file); 9 | 10 | if ( 11 | process.env.AZURE_API_KEY === undefined || 12 | process.env.AZURE_ENDPOINT === undefined || 13 | process.env.AZURE_DEPLOYMENT_NAME === undefined || 14 | process.env.AZURE_DEPLOYMENT_COMPLETIONS_NAME === undefined 15 | ) { 16 | console.error("Azure credentials not set"); 17 | return { 18 | sender: "", 19 | response: "Azure credentials not set", 20 | }; 21 | } 22 | 23 | if (file.size === 0) { 24 | return { 25 | sender: "", 26 | response: "No audio file provided", 27 | }; 28 | } 29 | 30 | console.log(">>", file); 31 | 32 | const arrayBuffer = await file.arrayBuffer(); 33 | const audio = new Uint8Array(arrayBuffer); 34 | 35 | // --- get audio transcription from Azure Whisper AI service ---- 36 | 37 | console.log("== Transcribe Audio Sample =="); 38 | 39 | const client = new OpenAIClient( 40 | process.env.AZURE_ENDPOINT, 41 | new AzureKeyCredential(process.env.AZURE_API_KEY) 42 | ); 43 | 44 | const result = await client.getAudioTranscription( 45 | process.env.AZURE_DEPLOYMENT_NAME, 46 | audio 47 | ); 48 | 49 | console.log(`Transcription: ${result.text}`); 50 | 51 | return NextResponse.json({ text: result.text }); 52 | } 53 | -------------------------------------------------------------------------------- /app/translate/page.tsx: -------------------------------------------------------------------------------- 1 | import TranslationForm from "@/components/TranslationForm"; 2 | import TranslationHistory from "@/components/TranslationHistory"; 3 | import { auth } from "@clerk/nextjs/server"; 4 | import Image from "next/image"; 5 | 6 | export type TranslationLanguages = { 7 | translation: { 8 | [key: string]: { 9 | name: string; 10 | nativeName: string; 11 | dir: "ltr" | "rtl"; 12 | }; 13 | }; 14 | }; 15 | 16 | async function TranslatePage() { 17 | auth().protect(); 18 | 19 | const { userId } = auth(); 20 | if (!userId) throw new Error("User not logged in"); 21 | 22 | const response = await fetch( 23 | "https://api.cognitive.microsofttranslator.com/languages?api-version=3.0", 24 | { 25 | next: { 26 | revalidate: 60 * 60 * 24, 27 | }, 28 | } 29 | ); 30 | 31 | const languages = (await response.json()) as TranslationLanguages; 32 | 33 | return ( 34 |
35 | 36 | 37 |
38 | ); 39 | } 40 | 41 | export default TranslatePage; 42 | -------------------------------------------------------------------------------- /app/translationHistory/route.ts: -------------------------------------------------------------------------------- 1 | import { getTranslations } from "@/mongodb/models/User"; 2 | 3 | import { NextRequest } from "next/server"; 4 | 5 | export async function GET(request: NextRequest) { 6 | const searchParams = request.nextUrl.searchParams; 7 | const userId = searchParams.get("userId"); 8 | 9 | const translations = await getTranslations(userId!); 10 | 11 | return Response.json({ translations }); 12 | } 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/DeleteTranslationButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { TrashIcon } from "lucide-react"; 4 | import { Button } from "./ui/button"; 5 | import deleteTranslation from "@/actions/deleteTranslation"; 6 | 7 | function DeleteTranslationButton({ id }: { id: string }) { 8 | const deleteTranslationAction = deleteTranslation.bind(null, id); 9 | 10 | return ( 11 |
12 | 20 |
21 | ); 22 | } 23 | 24 | export default DeleteTranslationButton; 25 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { SignInButton, UserButton } from "@clerk/nextjs"; 2 | import { auth } from "@clerk/nextjs/server"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | 6 | function Header() { 7 | const { userId } = auth(); 8 | 9 | return ( 10 |
11 |
12 | 13 | logo 20 | 21 |
22 | 23 | {userId ? ( 24 |
25 | 26 |
27 | ) : ( 28 | 29 | )} 30 |
31 | ); 32 | } 33 | 34 | export default Header; 35 | -------------------------------------------------------------------------------- /components/LanguageSelect.tsx: -------------------------------------------------------------------------------- 1 | import { TranslationLanguages } from "@/app/translate/page"; 2 | 3 | async function LanguageSelect({ 4 | name, 5 | defaultValue, 6 | }: { 7 | name: string; 8 | defaultValue: string; 9 | }) { 10 | const languages = await fetch( 11 | "https://api.cognitive.microsofttranslator.com/languages?api-version=3.0", 12 | { 13 | next: { 14 | revalidate: 60 * 60 * 24, 15 | }, 16 | } 17 | ).then((response) => response.json() as Promise); 18 | 19 | languages.translation["auto"] = { 20 | name: "Auto-Detect", 21 | nativeName: "Auto-Detect", 22 | dir: "ltr", 23 | }; 24 | return ( 25 | 32 | ); 33 | } 34 | 35 | export default LanguageSelect; 36 | -------------------------------------------------------------------------------- /components/Recorder.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MicIcon } from "lucide-react"; 4 | import { useEffect, useRef, useState } from "react"; 5 | import { useFormStatus } from "react-dom"; 6 | 7 | export const mimeType = "audio/webm"; 8 | 9 | function Recorder({ uploadAudio }: { uploadAudio: (blob: Blob) => void }) { 10 | const mediaRecorder = useRef(null); 11 | const { pending } = useFormStatus(); 12 | const [permission, setPermission] = useState(false); 13 | const [stream, setStream] = useState(null); 14 | const [recordingStatus, setRecordingStatus] = useState("inactive"); 15 | const [audioChunks, setAudioChunks] = useState([]); 16 | 17 | useEffect(() => { 18 | getMicrophonePermission(); 19 | }, []); 20 | 21 | const getMicrophonePermission = async () => { 22 | if ("MediaRecorder" in window) { 23 | try { 24 | const streamData = await navigator.mediaDevices.getUserMedia({ 25 | audio: true, 26 | video: false, 27 | }); 28 | setPermission(true); 29 | setStream(streamData); 30 | } catch (err: any) { 31 | alert(err.message); 32 | } 33 | } else { 34 | alert("Your browser does not support the MediaRecorder API"); 35 | } 36 | }; 37 | 38 | const startRecording = async () => { 39 | if (stream === null || pending) return; 40 | 41 | setRecordingStatus("recording"); 42 | 43 | // Create a new media recorder instance using the stream 44 | const media = new MediaRecorder(stream, { mimeType }); 45 | mediaRecorder.current = media; 46 | mediaRecorder.current.start(); 47 | 48 | let localAudioChunks: Blob[] = []; 49 | 50 | mediaRecorder.current.ondataavailable = (event) => { 51 | if (typeof event.data === "undefined") return; 52 | if (event.data.size === 0) return; 53 | 54 | localAudioChunks.push(event.data); 55 | }; 56 | 57 | setAudioChunks(localAudioChunks); 58 | }; 59 | 60 | const stopRecording = async () => { 61 | if (mediaRecorder.current === null || pending) return; 62 | 63 | setRecordingStatus("inactive"); 64 | mediaRecorder.current.stop(); 65 | mediaRecorder.current.onstop = () => { 66 | const audioBlob = new Blob(audioChunks, { type: mimeType }); 67 | uploadAudio(audioBlob); 68 | setAudioChunks([]); 69 | }; 70 | }; 71 | 72 | return ( 73 |
80 | 81 | 82 | {!permission && ( 83 | 84 | )} 85 | 86 | {pending && ( 87 |

88 | {recordingStatus === "recording" 89 | ? "Recording..." 90 | : "Stopping recording..."} 91 |

92 | )} 93 | 94 | {permission && recordingStatus === "inactive" && !pending && ( 95 | 101 | )} 102 | 103 | {recordingStatus === "recording" && ( 104 | 110 | )} 111 |
112 | ); 113 | } 114 | 115 | export default Recorder; 116 | -------------------------------------------------------------------------------- /components/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormStatus } from "react-dom"; 4 | import { Button } from "./ui/button"; 5 | 6 | function SubmitButton({ disabled }: { disabled: boolean }) { 7 | const { pending } = useFormStatus(); 8 | 9 | return ( 10 | 17 | ); 18 | } 19 | 20 | export default SubmitButton; 21 | -------------------------------------------------------------------------------- /components/TimeAgoText.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ReactTimeago from "react-timeago"; 4 | 5 | function TimeAgoText({ date }: { date: string }) { 6 | return ; 7 | } 8 | 9 | export default TimeAgoText; 10 | -------------------------------------------------------------------------------- /components/TranslationForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import translate from "@/actions/translate"; 4 | import { TranslationLanguages } from "@/app/translate/page"; 5 | import { useEffect, useRef, useState } from "react"; 6 | import { useFormState } from "react-dom"; 7 | import SubmitButton from "./SubmitButton"; 8 | import { Textarea } from "./ui/textarea"; 9 | import { 10 | Select, 11 | SelectContent, 12 | SelectGroup, 13 | SelectItem, 14 | SelectLabel, 15 | SelectTrigger, 16 | SelectValue, 17 | } from "@/components/ui/select"; 18 | import Recorder from "./Recorder"; 19 | import Image from "next/image"; 20 | import { MicIcon, SpeakerIcon, Volume2Icon } from "lucide-react"; 21 | import { Button } from "./ui/button"; 22 | 23 | const initialState = { 24 | inputLanguage: "auto", 25 | input: "", 26 | outputLanguage: "es", 27 | output: "", 28 | }; 29 | 30 | export type State = typeof initialState; 31 | 32 | function TranslationForm({ languages }: { languages: TranslationLanguages }) { 33 | const [state, formAction] = useFormState(translate, initialState); 34 | const [input, setInput] = useState(""); 35 | const [output, setOutput] = useState(""); 36 | const submitBtnRef = useRef(null); 37 | 38 | useEffect(() => { 39 | if (state.output) { 40 | setOutput(state.output); 41 | } 42 | }, [state]); 43 | 44 | useEffect(() => { 45 | if (!input?.trim()) return; 46 | 47 | const delayDebounceFn = setTimeout(() => { 48 | submitBtnRef.current?.click(); 49 | }, 500); 50 | 51 | return () => clearTimeout(delayDebounceFn); 52 | }, [input]); 53 | 54 | const uploadAudio = async (blob: Blob) => { 55 | const mimeType = "audio/webm"; 56 | 57 | const file = new File([blob], "audio.webm", { type: mimeType }); 58 | 59 | const formData = new FormData(); 60 | formData.append("audio", file); 61 | 62 | const response = await fetch("/transcribeAudio", { 63 | method: "POST", 64 | body: formData, 65 | }); 66 | 67 | const data = await response.json(); 68 | 69 | if (data.text) { 70 | setInput(data.text); 71 | } 72 | }; 73 | 74 | const playAudio = async () => { 75 | const synth = window.speechSynthesis; 76 | 77 | if (!output || !synth) return; 78 | 79 | const wordsToSay = new SpeechSynthesisUtterance(output); 80 | 81 | synth.speak(wordsToSay); 82 | }; 83 | 84 | return ( 85 | <> 86 |
87 |
88 | logo 94 | 95 | {/* style like a blue google button */} 96 |

97 | Text 98 |

99 |
100 | 101 | 102 |
103 | 104 |
105 |
106 |
107 | 130 | 131 |