├── .eslintrc ├── .gitignore ├── .prettierrc ├── README.md ├── components ├── DisclaimerModal.js ├── Meeting.js ├── NewMeetingModal.js ├── RecordBtns.js └── TranscriptBox.js ├── constants ├── agora.js ├── app.js ├── firebase.js ├── storage.js └── supportedLanguages.js ├── firebaseServiceAccount.json ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ ├── generate-token.js │ ├── only-translate.js │ ├── test.js │ ├── transcript.js │ └── translate.js ├── index.js └── meeting │ └── [meetingId].js ├── public ├── favicon.ico └── vercel.svg ├── service-account-key.json ├── services ├── auth.js ├── meeting.js ├── record.js ├── speak.js ├── storage.js ├── subtitle-translate.js └── translate.js └── utils ├── convert-langauge-array-to-options.js ├── firebase-admin.js └── firebase.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["eslint:recommended", "next", "next/core-web-vitals", "prettier"], 8 | "plugins": ["prettier"], 9 | "rules": { 10 | "prettier/prettier": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TalkEasy 2 | 3 | > Development env will not work as we have deleted the Firebase project and Cloud translation projects. However, the deployed demo will still work at [https://talk-easy.vercel.app](https://talk-easy.vercel.app) 4 | 5 | > To run it locally, follow the instructions in the *Development* section 6 | 7 | ### Watch the demo 8 | 9 | [![TalkEasy Demo](https://img.youtube.com/vi/AiFNjv_QTgI/0.jpg)](https://www.youtube.com/watch?v=AiFNjv_QTgI) 10 | 11 | 12 | ### Try it yourself 13 | [https://talk-easy.vercel.app](https://talk-easy.vercel.app) 14 | 15 | ### Why? 16 | People face language barriers when it comes to the conference meetings around the globe. TalkEasy allows them to communicate in their own languages. 17 | 18 | Currently in the market, there are a few apps like Zoom and Skype that do allow translation. Zoom allows translation in the mode of a physical translator joining the users meeting. Skype shows translated subtitles in the language the user prefers but only for 11 langauges but it does not support speech synthesis on the receiver's end. 19 | 20 | TalkEasy provides the following features - 21 | - 70 languages with different accents including Indian languages 22 | - Realtime subtitles just like you are watching a movie 23 | - Users can speak in multi-languages in a call which gets translated too. 24 | - After the speaker completes a sentence, the receiver receives the translated audio in the selected language. 25 | - Once the meeting has ended, the user receives an option to download the entire meeting’s transcript in one of the languages that the meeting was in 26 | 27 | ## Development 28 | 29 | You will need the following env variables in a file called `.env.local` 30 | 31 | - We use Agora for WebRTC 32 | - Firebase for Firestore, Auth, and Realtime Database 33 | 34 | ``` 35 | NEXT_PUBLIC_AGORA_APPID 36 | NEXT_PUBLIC_AGORA_APPC 37 | 38 | NEXT_AGORA_CUSTOMER_ID 39 | NEXT_AGORA_CUSTOMER_SECRET 40 | 41 | NEXT_PUBLIC_FIREBASE_API_KEY 42 | NEXT_PUBLIC_FIERBASE_AUTH_DOMAIN 43 | NEXT_PUBLIC_PROJECT_ID 44 | NEXT_PUBLIC_STORAGE_BUCKET 45 | NEXT_PUBLIC_MESSAGING_ID 46 | NEXT_PUBLIC_APP_ID 47 | NEXT_PUBLIC_FIREBASE_DB 48 | NEXT_PUBLIC_CLIENT_LOCATION=http://localhost:3000 49 | ``` 50 | 51 | Add the `firebase-admin` service account config in the root directory in a file called `firebaseServiceAccount.json` 52 | 53 | For realtime subtitles to work, you will need to create a service account on Google Cloud which has access to Cloud Translation API. Create a file called `service-account-key.json` in the root directory 54 | 55 | Finally, run the development server: 56 | 57 | ```bash 58 | npm run dev 59 | # or 60 | yarn dev 61 | ``` 62 | 63 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the website. 64 | 65 | ## License 66 | 67 | MIT 68 | -------------------------------------------------------------------------------- /components/DisclaimerModal.js: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalOverlay, 4 | ModalContent, 5 | ModalHeader, 6 | ModalFooter, 7 | ModalBody, 8 | Button, 9 | Text, 10 | } from "@chakra-ui/react"; 11 | 12 | const DisclaimerModal = ({ isOpen, onClose }) => { 13 | return ( 14 | 15 | 16 | 17 | Disclaimer 18 | 19 | This website is for demo purpose only. Expect bugs! 20 | Subtitle feature is disabled on the version deployed to Vercel 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default DisclaimerModal; 34 | -------------------------------------------------------------------------------- /components/Meeting.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect, useCallback } from "react"; 2 | import firebase from "firebase/app"; 3 | import "firebase/firestore"; 4 | import "firebase/database"; 5 | import { useRouter } from "next/router"; 6 | import { agoraPublicKeys } from "constants/agora"; 7 | import { Flex, Grid, Box } from "@chakra-ui/react"; 8 | 9 | import AgoraRTC from "agora-rtc-sdk"; 10 | import { getUserId, getUserLanguage, setMeetingDetails } from "services/storage"; 11 | import { init as initSpeaking, speak } from "services/speak"; 12 | import TranscriptBox from "./TranscriptBox"; 13 | import RecordBtns from "./RecordBtns"; 14 | import throttle from "lodash.throttle"; 15 | import { fetchSubtitle } from "services/subtitle-translate"; 16 | import { listenToMeetingChanges, listenToMessageChanges } from "services/meeting"; 17 | 18 | const Meeting = () => { 19 | const router = useRouter(); 20 | const { meetingId } = router.query; 21 | 22 | const [joined, setJoined] = useState(false); 23 | const [messages, setMessages] = useState([]); 24 | const [subtitle, setSubtitle] = useState(""); 25 | 26 | const localStreamRef = useRef(null); 27 | const remoteStreamRef = useRef(null); 28 | 29 | const throtlledGoogleTranslate = useRef( 30 | throttle(async (text, from, to) => { 31 | const result = await fetchSubtitle(text, from, to); 32 | setSubtitle(result.text); 33 | }, 1000), 34 | ).current; 35 | 36 | const rtcRef = useRef({ 37 | client: null, 38 | joined: false, 39 | published: false, 40 | localStream: null, 41 | remoteStreams: [], 42 | params: {}, 43 | }); 44 | 45 | const handleFail = () => {}; 46 | 47 | /** 48 | * Handle user pressing the leave button or 49 | * the other user pressed the leave button 50 | */ 51 | const handleLeave = () => { 52 | const rtc = rtcRef.current; 53 | 54 | // cleanup WebRTC streams 55 | if (rtc && rtc.client) { 56 | rtc.client.unpublish(rtc.localStream); 57 | // stop playing local stream 58 | rtc.localStream.stop(); 59 | 60 | // close local stream 61 | if (rtc.localStream) rtc.localStream.close(); 62 | 63 | rtc.client.leave(() => { 64 | // while (rtc.remoteStreams.length > 0) { 65 | // const stream = rtc.remoteStreams.shift(); 66 | // stream.stop(); 67 | // } 68 | // console.log("client leaves channel success"); 69 | 70 | setJoined(false); 71 | rtcRef.current = null; 72 | 73 | // send to dashboard 74 | }, handleFail); 75 | } 76 | }; 77 | 78 | useEffect(() => { 79 | initSpeaking(); 80 | const userId = getUserId(); 81 | const userLanguage = getUserLanguage().split("-")[0]; 82 | 83 | const userLanguageForTranslation = getUserLanguage().split("-")[0]; 84 | 85 | const unsubscribeToMeetingChanges = listenToMeetingChanges(meetingId, (snapshot) => { 86 | setMeetingDetails({ id: meetingId, ...snapshot.data() }); 87 | }); 88 | 89 | const unsubscribeMessageChangeListener = listenToMessageChanges(meetingId, (docs) => { 90 | const newMessages = []; 91 | docs.forEach((doc) => { 92 | newMessages.push({ 93 | id: doc.id, 94 | ...doc.data(), 95 | }); 96 | }); 97 | 98 | if (newMessages.length) { 99 | const message = newMessages[newMessages.length - 1]; 100 | if (message.userId !== userId) { 101 | const { text } = message.texts.find((t) => t.lang === userLanguage); 102 | speak(text); 103 | } 104 | } 105 | 106 | setMessages(newMessages); 107 | setSubtitle(""); 108 | }); 109 | 110 | const subtitleRef = firebase.database().ref("meetings/" + meetingId); 111 | subtitleRef.on("value", (snapshot) => { 112 | const data = snapshot.val(); 113 | if (data?.userId !== userId) { 114 | if (data) throtlledGoogleTranslate(data.text, "", userLanguageForTranslation); 115 | } else { 116 | setSubtitle(data.text); 117 | } 118 | }); 119 | 120 | return () => { 121 | unsubscribeToMeetingChanges(); 122 | unsubscribeMessageChangeListener(); 123 | }; 124 | }, [meetingId, throtlledGoogleTranslate]); 125 | 126 | const addParticipantToMeeting = useCallback(async () => { 127 | const userId = getUserId(); 128 | const lang = getUserLanguage().split("-")[0]; 129 | 130 | const db = firebase.firestore(); 131 | 132 | await db 133 | .collection("meetings") 134 | .doc(meetingId) 135 | .update({ 136 | participants: firebase.firestore.FieldValue.arrayUnion(userId), 137 | languages: firebase.firestore.FieldValue.arrayUnion(lang), 138 | }); 139 | }, [meetingId]); 140 | 141 | /** 142 | * Attemp to join Agora call 143 | */ 144 | const joinAgoraCall = useCallback(async () => { 145 | const rtc = rtcRef.current; 146 | 147 | if (!rtc || !meetingId) { 148 | return; 149 | } 150 | 151 | // generate uuid using math random 152 | // Agora only supports numbers as uid 153 | // generate a int id and convert to number 154 | let uid = Math.floor(Math.random() * 100 + 1); 155 | 156 | const channelName = meetingId; // use meeting id as channel id 157 | 158 | // generate a token before initialising client 159 | const response = await fetch("/api/generate-token", { 160 | body: JSON.stringify({ 161 | uid, 162 | channelName, 163 | }), 164 | headers: { 165 | "Content-Type": "application/json", 166 | }, 167 | method: "POST", 168 | }).then((response) => response.json()); 169 | 170 | const token = response.token; 171 | // console.log(response.token, agoraPublicKeys); 172 | 173 | rtc.client = AgoraRTC.createClient({ mode: "rtc", codec: "h264" }); 174 | rtc.client.init( 175 | agoraPublicKeys.appId, 176 | () => { 177 | // client init 178 | 179 | rtc.client.join( 180 | token, // token generated 181 | channelName, // name of channel to join 182 | uid, 183 | (uid) => { 184 | // client join success 185 | rtc.params.uid = uid; 186 | 187 | // create a stream 188 | rtc.localStream = AgoraRTC.createStream({ 189 | streamID: rtc.params.uid, 190 | audio: false, 191 | video: true, 192 | screen: false, 193 | }); 194 | rtc.localStream.setVideoProfile("480p_4"); 195 | 196 | // initialise the local stream 197 | rtc.localStream.init(async () => { 198 | // local stream initialised 199 | rtc.localStream.play("local-stream", { fit: "contain" }); 200 | 201 | // publish the local stream 202 | rtc.client.publish(rtc.localStream, handleFail); 203 | 204 | // setJoined 205 | setJoined(true); 206 | }, handleFail); 207 | }, 208 | handleFail, 209 | ); 210 | 211 | // subscribe to remote stream when it is added 212 | rtc.client.on("stream-added", async (evt) => { 213 | const remoteStream = evt.stream; 214 | const id = remoteStream.getId(); 215 | if (id !== rtc.params.uid) { 216 | const repeated = rtc.remoteStreams.some((item) => item.getId() === id); 217 | if (repeated) { 218 | return; 219 | } 220 | rtc.remoteStreams.push(remoteStream); 221 | 222 | rtc.client.subscribe(remoteStream, handleFail); 223 | } 224 | }); 225 | 226 | // play remote stream after subscribing to it 227 | rtc.client.on("stream-subscribed", (evt) => { 228 | const remoteStream = evt.stream; 229 | // const id = remoteStream.getId(); 230 | 231 | remoteStream.play("remote-stream", { fit: "contain" }); 232 | }); 233 | 234 | // handle remote leave 235 | rtc.client.on("stream-removed", async (evt) => { 236 | const remoteStream = evt.stream; 237 | // const id = remoteStream.getId(); 238 | remoteStream.close(); 239 | // remoteStream.stop("remote-stream"); 240 | }); 241 | 242 | // called when remote calls leave or drops connection 243 | rtc.client.on("peer-leave", async (evt) => { 244 | const remoteStream = evt.stream; 245 | // let id = null; 246 | if (remoteStream) { 247 | // id = remoteStream.getId(); 248 | } 249 | 250 | // peer has left the meeting 251 | // remove from remote streams 252 | // stop meeting timer 253 | rtc.remoteStreams.shift(); 254 | 255 | if (remoteStream) { 256 | // remoteStream.stop("remote-stream"); 257 | remoteStream.close(); 258 | } 259 | }); 260 | }, 261 | handleFail, 262 | ); 263 | }, [rtcRef, meetingId]); 264 | 265 | // Call handleLeave when this component is unmounted 266 | // Clean artifacts from Agora video call and exit gracefully 267 | useEffect(() => { 268 | const f = async () => { 269 | await Promise.all([joinAgoraCall(), addParticipantToMeeting()]); 270 | }; 271 | 272 | f(); 273 | }, [joinAgoraCall, addParticipantToMeeting]); 274 | 275 | if (typeof window === "undefined") { 276 | return ""; 277 | } 278 | 279 | return ( 280 | 281 | 282 | {/* Video call */} 283 | 284 | {rtcRef.current && ( 285 | 286 |
292 | 293 |
298 | 299 | 307 | {subtitle} 308 | 309 | 310 | )} 311 | 312 | 313 | {/* Audio recording */} 314 | 315 | 316 | 317 | 318 | 319 | {/* Transcription */} 320 | 321 | 322 | 323 | 324 | ); 325 | }; 326 | 327 | export default Meeting; 328 | -------------------------------------------------------------------------------- /components/NewMeetingModal.js: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalOverlay, 4 | ModalContent, 5 | ModalHeader, 6 | ModalFooter, 7 | ModalBody, 8 | ModalCloseButton, 9 | Button, 10 | Link, 11 | Text, 12 | useClipboard, 13 | Flex, 14 | Input, 15 | } from "@chakra-ui/react"; 16 | import { appConfig } from "constants/app"; 17 | 18 | const NewMeetingModal = ({ isOpen, onClose, meetingId }) => { 19 | const meetingLink = `${appConfig.clientLocation}/meeting/${meetingId}`; 20 | 21 | const { hasCopied, onCopy } = useClipboard(meetingLink); 22 | 23 | return ( 24 | 25 | 26 | 27 | Join new meeting 28 | 29 | 30 | Share this link to join 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default NewMeetingModal; 52 | -------------------------------------------------------------------------------- /components/RecordBtns.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import firebase from "firebase/app"; 3 | import "firebase/database"; 4 | import { init as initRecording, startRecording, stopRecording } from "services/record"; 5 | import { Button, Flex } from "@chakra-ui/react"; 6 | import { FaMicrophone, FaMicrophoneSlash } from "react-icons/fa"; 7 | import { BiExit } from "react-icons/bi"; 8 | import { getMeetingDetails, getUserId, getUserLanguage } from "services/storage"; 9 | import { useRouter } from "next/router"; 10 | import { throttle } from "lodash"; 11 | 12 | export default function RecordBtns({ meetingId, handleLeave }) { 13 | const router = useRouter(); 14 | const [recording, setRecording] = useState(false); 15 | 16 | const throttledAddMessage = useRef( 17 | throttle(async (rawText, data) => { 18 | await fetch("/api/translate", { 19 | method: "POST", 20 | body: JSON.stringify({ 21 | rawText, 22 | meetingId: data.id, 23 | userId: getUserId(), 24 | languages: data.languages, 25 | userLanguage: getUserLanguage().split("-")[0], 26 | }), 27 | headers: { 28 | "Content-Type": "application/json", 29 | }, 30 | }); 31 | }, 500), 32 | ).current; 33 | 34 | useEffect(() => { 35 | const userId = getUserId(); 36 | const onResult = async (event) => { 37 | // console.log(event.results); 38 | try { 39 | if (event.results[0].isFinal) { 40 | const rawText = event.results[0][0].transcript; 41 | const data = getMeetingDetails(); 42 | 43 | throttledAddMessage(rawText, data); 44 | } else { 45 | let interim_transcript = ""; 46 | for (let i = event.resultIndex; i < event.results.length; ++i) 47 | interim_transcript += event.results[i][0].transcript; 48 | 49 | const database = firebase.database(); 50 | 51 | await database.ref("meetings/" + meetingId).set({ 52 | text: interim_transcript, 53 | userId: userId, 54 | }); 55 | } 56 | } catch (e) { 57 | console.error(e); 58 | } 59 | }; 60 | initRecording(onResult); 61 | return () => {}; 62 | }, [meetingId]); 63 | 64 | const handleRecordClick = () => { 65 | if (!recording) startRecording(); 66 | else stopRecording(); 67 | 68 | setRecording((r) => !r); 69 | }; 70 | 71 | const handleLeaveStream = () => { 72 | handleLeave(); 73 | router.replace("/"); 74 | }; 75 | 76 | return ( 77 | 78 | 81 | 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /components/TranscriptBox.js: -------------------------------------------------------------------------------- 1 | import { Box, Text } from "@chakra-ui/layout"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import { getUserId, getUserLanguage } from "services/storage"; 4 | 5 | const TranscriptBox = ({ messages }) => { 6 | const [transcript, setTranscript] = useState([]); 7 | const scrollRef = useRef(null); 8 | 9 | useEffect(() => { 10 | const userLanguage = getUserLanguage().split("-")[0]; 11 | const userId = getUserId(); 12 | 13 | const trans = []; 14 | messages.forEach((message) => { 15 | const text = message.texts.find((t) => t.lang === userLanguage); 16 | if (text) { 17 | trans.push({ 18 | id: message.id, 19 | content: text.text, 20 | userName: userId === message.userId ? "You" : "Peer", 21 | }); 22 | } 23 | }); 24 | 25 | setTranscript(trans); 26 | scrollRef.current.scrollIntoView({ 27 | behavior: "smooth", 28 | block: "start", 29 | }); 30 | }, [messages]); 31 | 32 | return ( 33 | 34 | {transcript.map((message) => { 35 | return ( 36 | 37 | 38 | {message.userName}:{" "} 39 | 40 | {message.content} 41 | 42 | ); 43 | })} 44 |
45 | 46 | ); 47 | }; 48 | 49 | export default TranscriptBox; 50 | -------------------------------------------------------------------------------- /constants/agora.js: -------------------------------------------------------------------------------- 1 | export const agoraPublicKeys = { 2 | appId: process.env.NEXT_PUBLIC_AGORA_APPID, 3 | appCertificate: process.env.NEXT_PUBLIC_AGORA_APPC, 4 | }; 5 | 6 | export const agoraSecretKeys = { 7 | customerId: process.env.NEXT_AGORA_CUSTOMER_ID, 8 | customerSecret: process.env.NEXT_AGORA_CUSTOMER_SECRET, 9 | }; 10 | -------------------------------------------------------------------------------- /constants/app.js: -------------------------------------------------------------------------------- 1 | export const appConfig = { 2 | clientLocation: process.env.NEXT_PUBLIC_CLIENT_LOCATION, 3 | }; 4 | -------------------------------------------------------------------------------- /constants/firebase.js: -------------------------------------------------------------------------------- 1 | export const firebaseConfig = { 2 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 3 | authDomain: process.env.NEXT_PUBLIC_FIERBASE_AUTH_DOMAIN, 4 | projectId: process.env.NEXT_PUBLIC_PROJECT_ID, 5 | storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET, 6 | messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_ID, 7 | appId: process.env.NEXT_PUBLIC_APP_ID, 8 | databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DB, 9 | }; 10 | -------------------------------------------------------------------------------- /constants/storage.js: -------------------------------------------------------------------------------- 1 | export const storage = { 2 | userId: "userId", 3 | userLanguage: "userLanguage", 4 | meetingDetails: "meetingDetails", 5 | }; 6 | -------------------------------------------------------------------------------- /constants/supportedLanguages.js: -------------------------------------------------------------------------------- 1 | export const supportedLanguages = [ 2 | ["Afrikaans", ["af-ZA"]], 3 | ["አማርኛ", ["am-ET"]], 4 | ["Azərbaycanca", ["az-AZ"]], 5 | ["বাংলা", ["bn-BD", "বাংলাদেশ"], ["bn-IN", "ভারত"]], 6 | ["Bahasa Indonesia", ["id-ID"]], 7 | ["Bahasa Melayu", ["ms-MY"]], 8 | ["Català", ["ca-ES"]], 9 | ["Čeština", ["cs-CZ"]], 10 | ["Dansk", ["da-DK"]], 11 | ["Deutsch", ["de-DE"]], 12 | [ 13 | "English", 14 | ["en-AU", "Australia"], 15 | ["en-CA", "Canada"], 16 | ["en-IN", "India"], 17 | ["en-KE", "Kenya"], 18 | ["en-TZ", "Tanzania"], 19 | ["en-GH", "Ghana"], 20 | ["en-NZ", "New Zealand"], 21 | ["en-NG", "Nigeria"], 22 | ["en-ZA", "South Africa"], 23 | ["en-PH", "Philippines"], 24 | ["en-GB", "United Kingdom"], 25 | ["en-US", "United States"], 26 | ], 27 | [ 28 | "Español", 29 | ["es-AR", "Argentina"], 30 | ["es-BO", "Bolivia"], 31 | ["es-CL", "Chile"], 32 | ["es-CO", "Colombia"], 33 | ["es-CR", "Costa Rica"], 34 | ["es-EC", "Ecuador"], 35 | ["es-SV", "El Salvador"], 36 | ["es-ES", "España"], 37 | ["es-US", "Estados Unidos"], 38 | ["es-GT", "Guatemala"], 39 | ["es-HN", "Honduras"], 40 | ["es-MX", "México"], 41 | ["es-NI", "Nicaragua"], 42 | ["es-PA", "Panamá"], 43 | ["es-PY", "Paraguay"], 44 | ["es-PE", "Perú"], 45 | ["es-PR", "Puerto Rico"], 46 | ["es-DO", "República Dominicana"], 47 | ["es-UY", "Uruguay"], 48 | ["es-VE", "Venezuela"], 49 | ], 50 | ["Euskara", ["eu-ES"]], 51 | ["Filipino", ["fil-PH"]], 52 | ["Français", ["fr-FR"]], 53 | ["Basa Jawa", ["jv-ID"]], 54 | ["Galego", ["gl-ES"]], 55 | ["ગુજરાતી", ["gu-IN"]], 56 | ["Hrvatski", ["hr-HR"]], 57 | ["IsiZulu", ["zu-ZA"]], 58 | ["Íslenska", ["is-IS"]], 59 | ["Italiano", ["it-IT", "Italia"], ["it-CH", "Svizzera"]], 60 | ["ಕನ್ನಡ", ["kn-IN"]], 61 | ["ភាសាខ្មែរ", ["km-KH"]], 62 | ["Latviešu", ["lv-LV"]], 63 | ["Lietuvių", ["lt-LT"]], 64 | ["മലയാളം", ["ml-IN"]], 65 | ["मराठी", ["mr-IN"]], 66 | ["Magyar", ["hu-HU"]], 67 | ["ລາວ", ["lo-LA"]], 68 | ["Nederlands", ["nl-NL"]], 69 | ["नेपाली भाषा", ["ne-NP"]], 70 | ["Norsk bokmål", ["nb-NO"]], 71 | ["Polski", ["pl-PL"]], 72 | ["Português", ["pt-BR", "Brasil"], ["pt-PT", "Portugal"]], 73 | ["Română", ["ro-RO"]], 74 | ["සිංහල", ["si-LK"]], 75 | ["Slovenščina", ["sl-SI"]], 76 | ["Basa Sunda", ["su-ID"]], 77 | ["Slovenčina", ["sk-SK"]], 78 | ["Suomi", ["fi-FI"]], 79 | ["Svenska", ["sv-SE"]], 80 | ["Kiswahili", ["sw-TZ", "Tanzania"], ["sw-KE", "Kenya"]], 81 | ["ქართული", ["ka-GE"]], 82 | ["Հայերեն", ["hy-AM"]], 83 | [ 84 | "தமிழ்", 85 | ["ta-IN", "இந்தியா"], 86 | ["ta-SG", "சிங்கப்பூர்"], 87 | ["ta-LK", "இலங்கை"], 88 | ["ta-MY", "மலேசியா"], 89 | ], 90 | ["తెలుగు", ["te-IN"]], 91 | ["Tiếng Việt", ["vi-VN"]], 92 | ["Türkçe", ["tr-TR"]], 93 | ["اُردُو", ["ur-PK", "پاکستان"], ["ur-IN", "بھارت"]], 94 | ["Ελληνικά", ["el-GR"]], 95 | ["български", ["bg-BG"]], 96 | ["Pусский", ["ru-RU"]], 97 | ["Српски", ["sr-RS"]], 98 | ["Українська", ["uk-UA"]], 99 | ["한국어", ["ko-KR"]], 100 | [ 101 | "中文", 102 | ["cmn-Hans-CN", "普通话 (中国大陆)"], 103 | ["cmn-Hans-HK", "普通话 (香港)"], 104 | ["cmn-Hant-TW", "中文 (台灣)"], 105 | ["yue-Hant-HK", "粵語 (香港)"], 106 | ], 107 | ["日本語", ["ja-JP"]], 108 | ["हिन्दी", ["hi-IN"]], 109 | ["ภาษาไทย", ["th-TH"]], 110 | ]; 111 | 112 | export const langaugeOptions = [ 113 | { label: "Afrikaans af-ZA", value: "af-ZA" }, 114 | { label: "አማርኛ am-ET", value: "am-ET" }, 115 | { label: "Azərbaycanca az-AZ", value: "az-AZ" }, 116 | { label: "বাংলা bn-BD", value: "bn-BD" }, 117 | { label: "বাংলা bn-IN", value: "bn-IN" }, 118 | { label: "Bahasa Indonesia id-ID", value: "id-ID" }, 119 | { label: "Bahasa Melayu ms-MY", value: "ms-MY" }, 120 | { label: "Català ca-ES", value: "ca-ES" }, 121 | { label: "Čeština cs-CZ", value: "cs-CZ" }, 122 | { label: "Dansk da-DK", value: "da-DK" }, 123 | { label: "Deutsch de-DE", value: "de-DE" }, 124 | { label: "English en-AU", value: "en-AU" }, 125 | { label: "English en-CA", value: "en-CA" }, 126 | { label: "English en-IN", value: "en-IN" }, 127 | { label: "English en-KE", value: "en-KE" }, 128 | { label: "English en-TZ", value: "en-TZ" }, 129 | { label: "English en-GH", value: "en-GH" }, 130 | { label: "English en-NZ", value: "en-NZ" }, 131 | { label: "English en-NG", value: "en-NG" }, 132 | { label: "English en-ZA", value: "en-ZA" }, 133 | { label: "English en-PH", value: "en-PH" }, 134 | { label: "English en-GB", value: "en-GB" }, 135 | { label: "English en-US", value: "en-US" }, 136 | { label: "Español es-AR", value: "es-AR" }, 137 | { label: "Español es-BO", value: "es-BO" }, 138 | { label: "Español es-CL", value: "es-CL" }, 139 | { label: "Español es-CO", value: "es-CO" }, 140 | { label: "Español es-CR", value: "es-CR" }, 141 | { label: "Español es-EC", value: "es-EC" }, 142 | { label: "Español es-SV", value: "es-SV" }, 143 | { label: "Español es-ES", value: "es-ES" }, 144 | { label: "Español es-US", value: "es-US" }, 145 | { label: "Español es-GT", value: "es-GT" }, 146 | { label: "Español es-HN", value: "es-HN" }, 147 | { label: "Español es-MX", value: "es-MX" }, 148 | { label: "Español es-NI", value: "es-NI" }, 149 | { label: "Español es-PA", value: "es-PA" }, 150 | { label: "Español es-PY", value: "es-PY" }, 151 | { label: "Español es-PE", value: "es-PE" }, 152 | { label: "Español es-PR", value: "es-PR" }, 153 | { label: "Español es-DO", value: "es-DO" }, 154 | { label: "Español es-UY", value: "es-UY" }, 155 | { label: "Español es-VE", value: "es-VE" }, 156 | { label: "Euskara eu-ES", value: "eu-ES" }, 157 | { label: "Filipino fil-PH", value: "fil-PH" }, 158 | { label: "Français fr-FR", value: "fr-FR" }, 159 | { label: "Basa Jawa jv-ID", value: "jv-ID" }, 160 | { label: "Galego gl-ES", value: "gl-ES" }, 161 | { label: "ગુજરાતી gu-IN", value: "gu-IN" }, 162 | { label: "Hrvatski hr-HR", value: "hr-HR" }, 163 | { label: "IsiZulu zu-ZA", value: "zu-ZA" }, 164 | { label: "Íslenska is-IS", value: "is-IS" }, 165 | { label: "Italiano it-IT", value: "it-IT" }, 166 | { label: "Italiano it-CH", value: "it-CH" }, 167 | { label: "ಕನ್ನಡ kn-IN", value: "kn-IN" }, 168 | { label: "ភាសាខ្មែរ km-KH", value: "km-KH" }, 169 | { label: "Latviešu lv-LV", value: "lv-LV" }, 170 | { label: "Lietuvių lt-LT", value: "lt-LT" }, 171 | { label: "മലയാളം ml-IN", value: "ml-IN" }, 172 | { label: "मराठी mr-IN", value: "mr-IN" }, 173 | { label: "Magyar hu-HU", value: "hu-HU" }, 174 | { label: "ລາວ lo-LA", value: "lo-LA" }, 175 | { label: "Nederlands nl-NL", value: "nl-NL" }, 176 | { label: "नेपाली भाषा ne-NP", value: "ne-NP" }, 177 | { label: "Norsk bokmål nb-NO", value: "nb-NO" }, 178 | { label: "Polski pl-PL", value: "pl-PL" }, 179 | { label: "Português pt-BR", value: "pt-BR" }, 180 | { label: "Português pt-PT", value: "pt-PT" }, 181 | { label: "Română ro-RO", value: "ro-RO" }, 182 | { label: "සිංහල si-LK", value: "si-LK" }, 183 | { label: "Slovenščina sl-SI", value: "sl-SI" }, 184 | { label: "Basa Sunda su-ID", value: "su-ID" }, 185 | { label: "Slovenčina sk-SK", value: "sk-SK" }, 186 | { label: "Suomi fi-FI", value: "fi-FI" }, 187 | { label: "Svenska sv-SE", value: "sv-SE" }, 188 | { label: "Kiswahili sw-TZ", value: "sw-TZ" }, 189 | { label: "Kiswahili sw-KE", value: "sw-KE" }, 190 | { label: "ქართული ka-GE", value: "ka-GE" }, 191 | { label: "Հայերեն hy-AM", value: "hy-AM" }, 192 | { label: "தமிழ் ta-IN", value: "ta-IN" }, 193 | { label: "தமிழ் ta-SG", value: "ta-SG" }, 194 | { label: "தமிழ் ta-LK", value: "ta-LK" }, 195 | { label: "தமிழ் ta-MY", value: "ta-MY" }, 196 | { label: "తెలుగు te-IN", value: "te-IN" }, 197 | { label: "Tiếng Việt vi-VN", value: "vi-VN" }, 198 | { label: "Türkçe tr-TR", value: "tr-TR" }, 199 | { label: "اُردُو ur-PK", value: "ur-PK" }, 200 | { label: "اُردُو ur-IN", value: "ur-IN" }, 201 | { label: "Ελληνικά el-GR", value: "el-GR" }, 202 | { label: "български bg-BG", value: "bg-BG" }, 203 | { label: "Pусский ru-RU", value: "ru-RU" }, 204 | { label: "Српски sr-RS", value: "sr-RS" }, 205 | { label: "Українська uk-UA", value: "uk-UA" }, 206 | { label: "한국어 ko-KR", value: "ko-KR" }, 207 | { label: "中文 cmn-Hans-CN", value: "cmn-Hans-CN" }, 208 | { label: "中文 cmn-Hans-HK", value: "cmn-Hans-HK" }, 209 | { label: "中文 cmn-Hant-TW", value: "cmn-Hant-TW" }, 210 | { label: "中文 yue-Hant-HK", value: "yue-Hant-HK" }, 211 | { label: "日本語 ja-JP", value: "ja-JP" }, 212 | { label: "हिन्दी hi-IN", value: "hi-IN" }, 213 | { label: "ภาษาไทย th-TH", value: "th-TH" }, 214 | ]; 215 | -------------------------------------------------------------------------------- /firebaseServiceAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "talk-easy-d2267", 4 | "private_key_id": "27b05e1b059632288832d99bec9ee0da5811ca30", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC8JjN2ZXfmhBX4\n7iX1jGb1pTU/+zhi0arragOJlGg2HTsInJr1gMnPO+jBVPLT/G3OECxhTRqpJyN/\nhugTs3jMMbmMP9eCRQq+t+7Oxbt0j5xg0fqChySmzlZTc7bgbbClFqw4BDnKuksE\nv20s9BGU7ULbOL9esCDZht+2/xJDIZYM5mlgCKrUZm6d1lLyjggchtXCC2R93XzT\n8O6XRmvF0TG7YoCBkl/IO4ltAvU3NrWs9m2qmjg8W7clCb1UzTCdVeedi6+dpXVI\nCslhfd5ZGfFNCkvcBNBDQpN46UmoBqjG+WTd+KvC1GJSUcwVQipFm10lO9zVqYR8\n4lMLSeUhAgMBAAECggEAEHgFjCkAkEBijfoGf5JNFtZhrVqHinrQy69pZNsFLitr\n1eqjU7b0szuKuZV+ddjEIcPfppqxqTnATTLZUJQmkDUQCTszWXdCpAQElrvPFzpU\n1VK76z36EtG/06kuykE/s4ujAno4NssMsvswir1IZrFH51l1wsuG7JN2NJXqGs8I\nd/z60yyNvaJ6+D/GFSCC6ANSlpMh2QcbDKYQt2VuZEprzWh3KEk416EdFKxd41Ur\n5TKSAPIQAR8UII4vN+wLfEXePvpRlJO7yMF5NgAKw08ijDcmQYQbsHJ893RxIq+c\n2/4rZHnosoKgZ3CeSQIogD/jE3KUdE0IH9AjCmPgQwKBgQD9zP8jvhD2hKBWqV/l\n517g6fauYrMJb1eKmRSLtEWtKYjvqDT9Le6avTDOXfjci9iDRRPbqDAyhyDKzRAZ\n6cZiC6TWywmROYJ76Rxkg91FuTBQqlYVVq05vfTKbeOc5A0tBjK+QVbmNhUEfjj5\nLwPkp8s8nvlGagYztv2AJkI87wKBgQC9x5H/w1TSRRkjjC6P/Z0RFYBDInyvZMnR\n14/QLcSeq5j2BEK1Kc7Ir9PJPLeOkFJWoEWtRoiX/EWZpQ21Q5Cj2OwPpYWY1CKZ\n2SbZGL8FDSmx+smzsNvrMnJiBnlumFdPAp8BWnWsRcEtfrm29SDzr7Dfu/BYZRnN\nYI7EoqEe7wKBgQCWUfRLlyc02xicO3UxFfh7/ha88nhX/jo7PK+Ojxc1mIQibd30\nll/cBnIByGa9OZbjKOa6EsN5Kc+iThJbRrrZF0xqa5cfDJDcExVd8zv7L9QN8tVJ\njizLJlb2Dl/hbLDhGeq0BL8TWrTYFGpqLA6CP1+AaCf8LI+/0YIThJV2wQKBgQCc\nLtUBxxBUaBdzQNfFGrQbrjU7ivNQKUNK1ft+GVx6NMCSnxkDHSAX21QRhk2OH0oU\nDpypKKYbZrsk4kgwyCUOIuTLT65uAw9iy+qDujDiiF2rIrjCkCe9HWwzLh7bnLYl\nyQNwyrCTEWkU9vkCECSJSCrpRjNbnACrG+8C9tBgswKBgQDv4DrG2o2fMIMXKhBf\nbhr8Mw3pSPDpYMgzBNZHW2Eg9hM9v17Y43cudHOwjq31nmsu5jIKDPhbC/n1H+iI\nJbjsPpVchUmCdg3n1QjH4XBR2ydOn/EgjY+hdep7/o3JqNohIAinYFMdIPRov4m+\ny0VJwo0DK4SwVcseIdIEJKOMkg==\n-----END PRIVATE KEY-----\n", 6 | "client_email": "firebase-adminsdk-60o90@talk-easy-d2267.iam.gserviceaccount.com", 7 | "client_id": "109608527863106371109", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://oauth2.googleapis.com/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-60o90%40talk-easy-d2267.iam.gserviceaccount.com" 12 | } 13 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@components": ["./components"], 6 | "@constants": ["./constants"], 7 | "@config": ["./config"], 8 | "@page-sections": ["./page-sections"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "talk-easy", 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 | "@chakra-ui/react": "^1.6.3", 13 | "@emotion/react": "^11.4.0", 14 | "@emotion/styled": "^11.3.0", 15 | "@google-cloud/translate": "^6.2.2", 16 | "agora-access-token": "^2.0.4", 17 | "agora-rtc-sdk": "^3.6.0", 18 | "firebase": "^8.6.8", 19 | "firebase-admin": "^9.9.0", 20 | "framer-motion": "^4.1.17", 21 | "lodash.debounce": "^4.0.8", 22 | "lodash.throttle": "^4.1.1", 23 | "next": "11.0.0", 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2", 26 | "react-icons": "^4.2.0", 27 | "translation-google": "^0.2.1" 28 | }, 29 | "devDependencies": { 30 | "eslint": "7.28.0", 31 | "eslint-config-next": "11.0.0", 32 | "eslint-config-prettier": "^8.3.0", 33 | "eslint-plugin-prettier": "^3.4.0", 34 | "prettier": "^2.3.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | import { ChakraProvider } from "@chakra-ui/react"; 4 | import { firebaseInit } from "utils/firebase"; 5 | import { getUserLanguage, setUserLanguage } from "services/storage"; 6 | import { signInAnonymously } from "services/auth"; 7 | 8 | firebaseInit(); 9 | 10 | function App({ Component, pageProps }) { 11 | const setDefaultLanguage = () => { 12 | const langauge = getUserLanguage(); 13 | if (!langauge) { 14 | setUserLanguage("en-IN"); 15 | } 16 | }; 17 | 18 | useEffect(() => { 19 | setDefaultLanguage(); 20 | signInAnonymously(); 21 | }, []); 22 | 23 | return ( 24 | 25 | 26 | 27 | ); 28 | } 29 | export default App; 30 | -------------------------------------------------------------------------------- /pages/api/generate-token.js: -------------------------------------------------------------------------------- 1 | import { RtcTokenBuilder, RtcRole } from "agora-access-token"; 2 | import { agoraPublicKeys } from "constants/agora"; 3 | 4 | /** 5 | * Generate token to be used by Agora 6 | */ 7 | export default function handler(req, res) { 8 | if (req.method === "POST") { 9 | try { 10 | const { channelName, uid } = req.body; 11 | 12 | console.log("channelName", channelName, uid); 13 | 14 | const role = RtcRole.PUBLISHER; 15 | 16 | // token expiry time 17 | const expirationTimeInSeconds = 7200; 18 | 19 | const currentTimestamp = Math.floor(Date.now() / 1000); 20 | 21 | const privilegeExpiredTs = currentTimestamp + expirationTimeInSeconds; 22 | 23 | // Build token with uid 24 | const token = RtcTokenBuilder.buildTokenWithUid( 25 | agoraPublicKeys.appId, 26 | agoraPublicKeys.appCertificate, 27 | channelName, 28 | uid, 29 | role, 30 | privilegeExpiredTs, 31 | ); 32 | 33 | res.status(200).json({ token }); 34 | } catch (error) { 35 | console.log("[api]", "generate-token", error.message); 36 | } 37 | } else { 38 | res.status(405).json({ 39 | error: "Method not allowed", 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pages/api/only-translate.js: -------------------------------------------------------------------------------- 1 | import { subtitleGoogleTranslate } from "services/subtitle-translate"; 2 | 3 | export default async function handler(req, res) { 4 | if (req.method === "POST") { 5 | const { text, from, to } = req.body; 6 | 7 | const destText = await subtitleGoogleTranslate(text, from, to); 8 | 9 | res.status(200).json({ ok: true, text: destText }); 10 | } else { 11 | res.status(405).json({ 12 | error: "Method not allowed", 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/api/test.js: -------------------------------------------------------------------------------- 1 | import { db } from "utils/firebase-admin"; 2 | 3 | export default function test(req, res) { 4 | db.collection("test2").add({ lol: "chala" }); 5 | res.status(200).json({ 6 | error: "Method not allowed", 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /pages/api/transcript.js: -------------------------------------------------------------------------------- 1 | import { db, storage } from "utils/firebase-admin"; 2 | 3 | const path = require("path"); 4 | const os = require("os"); 5 | const fs = require("fs"); 6 | 7 | const tmpdir = os.tmpdir(); 8 | const bucket = storage.bucket(); 9 | 10 | export default async function handler(req, res) { 11 | const { userLanguage, meetingId, userId } = req.body; 12 | const lang = userLanguage.split("-")[0]; 13 | const data = []; 14 | 15 | try { 16 | const { docs } = await db 17 | .collection("meetings") 18 | .doc(meetingId) 19 | .collection("messages") 20 | .orderBy("createdAt", "asc") 21 | .get(); 22 | for (const doc of docs) { 23 | try { 24 | const { texts, createdAt, userId: messageUserId } = doc.data(); 25 | const { text } = texts.filter((item) => item.lang === lang)[0]; 26 | const date = createdAt.toDate().toLocaleString(); 27 | data.push(`[${date}] ${messageUserId === userId ? "You" : "Peer"} : ${text}`); 28 | } catch (e) { 29 | console.error(e); 30 | } 31 | } 32 | 33 | const filename = `${meetingId}-${userId}`; 34 | const filepath = path.join(tmpdir, filename); 35 | const destFilePath = `talkeasy/${filename}`; 36 | 37 | fs.writeFileSync(filepath, data.join("\n"), { 38 | encoding: "utf-8", 39 | }); 40 | await bucket.upload(filepath, { destination: destFilePath }); 41 | const file = bucket.file(destFilePath); 42 | const [url] = await file.getSignedUrl({ action: "read", expires: "01-01-2500" }); 43 | 44 | return res.status(200).json({ url }); 45 | } catch (e) { 46 | console.error(e); 47 | } 48 | 49 | return res.status(200).json({ error: true }); 50 | } 51 | -------------------------------------------------------------------------------- /pages/api/translate.js: -------------------------------------------------------------------------------- 1 | import { db, firestore } from "utils/firebase-admin"; 2 | import { googleTranslate } from "services/translate"; 3 | 4 | export default async function handler(req, res) { 5 | // console.log(req.body); 6 | if (req.method === "POST") { 7 | const { languages, meetingId, rawText, userLanguage, userId } = req.body; 8 | 9 | const docKeLie = { 10 | userId, 11 | userLanguage, 12 | createdAt: firestore.FieldValue.serverTimestamp(), 13 | texts: [], 14 | }; 15 | 16 | for (const destLang of languages) { 17 | if (destLang !== userLanguage) { 18 | const destText = await googleTranslate(rawText, userLanguage, destLang); 19 | docKeLie.texts.push({ lang: destLang, text: destText }); 20 | } 21 | } 22 | docKeLie.texts.push({ lang: userLanguage, text: rawText }); 23 | await db.collection("meetings").doc(meetingId).collection("messages").add(docKeLie); 24 | 25 | res.status(200).json({ ok: true }); 26 | } else { 27 | res.status(405).json({ 28 | error: "Method not allowed", 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Head from "next/head"; 3 | import { useRouter } from "next/router"; 4 | import firebase from "firebase/app"; 5 | import "firebase/firestore"; 6 | import { 7 | Container, 8 | Heading, 9 | Flex, 10 | Button, 11 | Input, 12 | HStack, 13 | Text, 14 | useDisclosure, 15 | Select, 16 | Box, 17 | IconButton, 18 | CircularProgress, 19 | Center, 20 | } from "@chakra-ui/react"; 21 | import NewMeetingModal from "components/NewMeetingModal"; 22 | import { appConfig } from "constants/app"; 23 | import { langaugeOptions } from "constants/supportedLanguages"; 24 | import { getUserId, getUserLanguage, setUserLanguage } from "services/storage"; 25 | import { FaFileDownload } from "react-icons/fa"; 26 | import DisclaimerModal from "components/DisclaimerModal"; 27 | 28 | export default function Home() { 29 | const [link, setLink] = useState(""); 30 | const [newMeetingId, setNewMeetingId] = useState(""); 31 | const [loading, setLoading] = useState(false); 32 | const [selectedLanguage, setSelectedLanguage] = useState(""); 33 | const [history, setHistory] = useState([]); 34 | const [loadingHistory, setLoadingHistory] = useState(false); 35 | const [transcriptionLoading, setTranscriptionLoading] = useState(false); 36 | 37 | const { isOpen, onOpen, onClose } = useDisclosure(); 38 | const { 39 | isOpen: isDisclaimerOpen, 40 | onOpen: onDisclaimerOpen, 41 | onClose: onDisclaimerClose, 42 | } = useDisclosure(); 43 | 44 | const router = useRouter(); 45 | const handleLinkChange = (e) => { 46 | setLink(e.target.value); 47 | }; 48 | 49 | const handleLanguageChange = (e) => { 50 | const lang = e.target.value; 51 | setSelectedLanguage(lang); 52 | setUserLanguage(lang); 53 | }; 54 | 55 | const handleDonePressed = () => { 56 | if (link.startsWith("http")) { 57 | router.push(link); 58 | } else { 59 | router.push(`${appConfig.clientLocation}/meeting/${link}`); 60 | } 61 | }; 62 | 63 | const handleTranscriptionDownload = async (meetingId) => { 64 | setTranscriptionLoading(meetingId); 65 | 66 | const userId = getUserId(); 67 | const userLanguage = getUserLanguage(); 68 | 69 | const response = await fetch("/api/transcript", { 70 | body: JSON.stringify({ 71 | meetingId, 72 | userId, 73 | userLanguage, 74 | }), 75 | headers: { 76 | "Content-Type": "application/json", 77 | }, 78 | method: "POST", 79 | }).then((response) => response.json()); 80 | 81 | console.log(response); 82 | 83 | const url = response.url; 84 | 85 | const a = document.createElement("a"); 86 | a.setAttribute("download", "true"); 87 | a.setAttribute("href", url); 88 | a.setAttribute("target", "_blank"); 89 | 90 | a.click(); 91 | 92 | setTranscriptionLoading(false); 93 | }; 94 | 95 | const handleNewMeetingPressed = async () => { 96 | const db = firebase.firestore(); 97 | 98 | setLoading(true); 99 | 100 | const docRef = db.collection("meetings").doc(); 101 | await docRef.set({ 102 | createdAt: firebase.firestore.FieldValue.serverTimestamp(), 103 | }); 104 | 105 | setNewMeetingId(docRef.id); 106 | onOpen(); 107 | setLoading(false); 108 | }; 109 | 110 | const fetchHistory = async () => { 111 | const userId = getUserId(); 112 | 113 | if (userId) { 114 | setLoadingHistory(true); 115 | const db = firebase.firestore(); 116 | 117 | const querySnapshot = await db 118 | .collection("meetings") 119 | .where("participants", "array-contains", userId) 120 | .orderBy("createdAt", "desc") 121 | .get(); 122 | 123 | const newHistory = []; 124 | 125 | querySnapshot.docs.forEach((doc) => { 126 | newHistory.push({ 127 | id: doc.id, 128 | ...doc.data(), 129 | }); 130 | }); 131 | 132 | setHistory(newHistory); 133 | setLoadingHistory(false); 134 | } 135 | }; 136 | 137 | useEffect(() => { 138 | setSelectedLanguage(getUserLanguage() || "en-IN"); 139 | onDisclaimerOpen(); 140 | 141 | const f = async () => { 142 | await fetchHistory(); 143 | }; 144 | 145 | f(); 146 | }, [onDisclaimerOpen]); 147 | 148 | return ( 149 |
150 | 151 | TalkEasy 152 | 153 | 154 | 155 | 156 | 157 | 158 | TalkEasy 159 | 160 | 161 | Talk easily even when you speak in different languages 162 | 163 | 164 | 165 | 166 | 167 | Select langauge 168 | 169 | 184 | 185 | 186 | 187 | 188 | 191 | 197 | 205 | 206 | 207 | 208 | History 209 | 210 | 211 |
212 | {loadingHistory && } 213 |
214 | 215 | 216 | {history.map((item) => { 217 | return ( 218 | 228 | 229 | Meeting at {item.createdAt.toDate().toLocaleTimeString()}{" "} 230 | {item.createdAt.toDate().toLocaleDateString()} 231 | 232 | handleTranscriptionDownload(item.id)} 235 | colorScheme="green" 236 | aria-label="Download" 237 | icon={} 238 | /> 239 | 240 | ); 241 | })} 242 | 243 |
244 | 245 | 246 | 247 |
248 |
249 | ); 250 | } 251 | -------------------------------------------------------------------------------- /pages/meeting/[meetingId].js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import dynamic from "next/dynamic"; 3 | 4 | const Meeting = dynamic(() => import("../../components/Meeting"), { 5 | ssr: false, 6 | }); 7 | 8 | export default function MeetingPage() { 9 | const [show, setShow] = useState(false); 10 | 11 | useEffect(() => { 12 | setShow(true); 13 | }, []); 14 | 15 | return
{show && typeof window !== "undefined" && }
; 16 | } 17 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burhanuday/talk-easy/5d8afd7421f55bbf518b41f6b945f62aa9b2cabb/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /service-account-key.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "talk-easy-d2267", 4 | "private_key_id": "ed4a1d8df8c324eb7782c69fb55e7f94afcb0839", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUOz3dKR25Q2L8\n50RI0SqE8xdnoGtj9l235ILpTE8jTc90WxWEA0vuTuBuKTm5u5REnLXy86Y72WMs\nFZHPfzDivUfmxktL9S6Xx3+MvJpJ+9xbt2rcO/lefAUJUaz6Vpjeq1rVvdE/hUSU\n3oRwPIuskQP7C39nOy17gTFuQFMz/TrxgGudN/KSDsI9O7tGm3QGPK6fNALn+WRP\n3WIbPRJMyJRVXNCdcZfsguYR46JARC8tN8SG1Ppd8+AH0pajDlJLmuZHWyCkXRFe\nup8/vdTf3CuT4LTq1+B52MeFuusoVdlEmooVzfVdcKRfwd8M4cp8ZbtbLulobpKf\nMkTuQfPDAgMBAAECggEAElUPsfPkQmWEvm3HozaWIqfQ5L2hqR/V1pWhjT9vMpB9\nFALuJ9oVC/+6OSnMd7OxSf/zY4oSWOcdPuiaEL9e3KcW9+M3C9eg77Fm3iRIe42K\n6k08qMtxbezujnCErZcfW1SX4xCja4Y6d1WFQSCpu/1Gx7+lgKhG9H0jZO+fgET+\nHpU2Q6CpBJwD6qjCO1pcbw2JVVpl+DD07M0MTMu9DK+JE4RzgsxZ1Qop1VBXUk4z\nB30Nrs0SAI4AEretnXQ06qBJgqxZLLY4lnbzLNdk/5ltz9lzgAmg2bDmY+eqrlrV\nxREEn7fcPGPEW7pVXn1u++smxzkAXy08JKZl9OwQGQKBgQD5kYrsroP4TnP4vWjH\nyWRoc4LTANhdTa7KAECXFdpJMgB4vl2BrnQunfjscpBBCY0J/hLO1iqsk8cWWCf9\nZ/HITT8WC++HuuC5tksLhdHhOPNq7Cn29GQiyzZbGvF3svvDU3JhqCAi6030/gdi\n4fHaBTEt3Ade9yMHuXa2J90LdQKBgQDZs2DEwxFJNX50XJDXiX0O2wluO3hkrtNN\nfYOCcoELvgFqF6EQpumqi0WbCs9NvTn02zWMYBdjMlIFqbVYpfp4aO/c8PJR0iIo\nJOgnhSrcxSSnF6ApHbLgIGxs1Tx37zN18759I/2vhfzYjmvipedqjFzneI/itQ4S\nYwR5DdfzVwKBgGX9u9e0DkzIyw5JYevb+wPQyRMwUjv3RkpZRgw4qwekvpqZuZ8I\ny4RNnPAMdbWOkKwXwFn1HmV+0yrnhhhChYYFQ5Xf9Nj0X2il/g9MdhRj8N6uewvo\nno+1mpYq8amoZMlTbfhI/DEpFqfbtOsNSYh+/LUwKXb+6rr6aXBR5D0NAoGBAIwy\nTaZJLS+lSIttNUXo3+WaP4eCuvSz9ZYYt4FhdiN2uHh3QR11MFPJHwlKu9gHfXRn\nWyPMmiMiu9mzwfqV4Sh8A8SYUqVImwCZS/xvcPv95a3JtDXmT1Sw7MJlzGw8Wjqi\nvtDeRbgspHldtrKePtrKC+ZxKNBJ4wcKR04iESk/AoGALBElrlA8yAn5PA//zYts\nYSOqqk864Vy3Ql2goawxxaXI2LtlkyQCDETaUlUHPsUnt9PuNllWNyXPKIlvW2ck\n5eH65qoDaLi9FnakufHK14EF6ku4IoVycrj2GjmU5kj8pyG95/ENUbDh0n6HT0j9\nC8Sm2e0lMzCA+bufZCABFoA=\n-----END PRIVATE KEY-----\n", 6 | "client_email": "google-translate-service@talk-easy-d2267.iam.gserviceaccount.com", 7 | "client_id": "116147647512138978098", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://oauth2.googleapis.com/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-translate-service%40talk-easy-d2267.iam.gserviceaccount.com" 12 | } 13 | -------------------------------------------------------------------------------- /services/auth.js: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/app"; 2 | import "firebase/auth"; 3 | import "firebase/firestore"; 4 | 5 | import { storage } from "constants/storage"; 6 | 7 | export const signInAnonymously = async () => { 8 | try { 9 | const uid = localStorage.getItem(storage.userId); 10 | if (uid) return uid; 11 | 12 | const { user } = await firebase.auth().signInAnonymously(); 13 | localStorage.setItem(storage.userId, user.uid); 14 | return user.uid; 15 | } catch (e) { 16 | console.error("SIGN_IN_ERROR::", e); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /services/meeting.js: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/app"; 2 | import "firebase/firestore"; 3 | 4 | /** 5 | * Set a real time listener to change in messages of meeting 6 | * @param {string} meetingId meeting id to listen message changes of 7 | * @param {Function} cb callback on snapshot change 8 | * @returns function to unsubscribe to changes 9 | */ 10 | export const listenToMessageChanges = async (meetingId, cb) => { 11 | const db = firebase.firestore(); 12 | 13 | return db 14 | .collection("meetings") 15 | .doc(meetingId) 16 | .collection("messages") 17 | .orderBy("createdAt", "asc") 18 | .onSnapshot((docs) => cb(docs)); 19 | }; 20 | 21 | /** 22 | * real time listener to listen to changes in meeting doc 23 | * @param {string} meetingId meeting id 24 | * @param {Function} cb cb fired when change occurs 25 | * @returns function to unsub from changes 26 | */ 27 | export const listenToMeetingChanges = async (meetingId, cb) => { 28 | const db = firebase.firestore(); 29 | 30 | return db 31 | .collection("meetings") 32 | .doc(meetingId) 33 | .onSnapshot((snapshot) => cb(snapshot)); 34 | }; 35 | -------------------------------------------------------------------------------- /services/record.js: -------------------------------------------------------------------------------- 1 | import { getUserLanguage } from "services/storage"; 2 | 3 | let recognition; 4 | 5 | export const startRecording = () => { 6 | if (recognition) recognition.start(); 7 | else { 8 | console.error("no audio api"); 9 | return; 10 | } 11 | }; 12 | 13 | export const stopRecording = () => { 14 | if (recognition) recognition.stop(); 15 | else { 16 | console.error("no audio api"); 17 | return; 18 | } 19 | }; 20 | 21 | export const init = (onResult) => { 22 | if (!("SpeechRecognition" in window || "webkitSpeechRecognition" in window)) { 23 | console.error("Speech Recognition Not Available"); 24 | return; 25 | } 26 | 27 | const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; 28 | // console.log(SpeechRecognition); 29 | recognition = new SpeechRecognition(); 30 | recognition.continuous = true; 31 | recognition.interimResults = true; 32 | recognition.lang = getUserLanguage(); 33 | 34 | // recognition.onstart = () => { 35 | // console.log("started"); 36 | // }; 37 | 38 | // recognition.onend = (e) => { 39 | // console.log("ended", e); 40 | // }; 41 | 42 | recognition.onresult = onResult; 43 | }; 44 | -------------------------------------------------------------------------------- /services/speak.js: -------------------------------------------------------------------------------- 1 | import { getUserLanguage } from "./storage"; 2 | 3 | let speechSynthesis; 4 | let voices; 5 | 6 | export const init = () => { 7 | if (!("speechSynthesis" in window)) { 8 | console.error("Speech Synthesis Not Available"); 9 | return; 10 | } 11 | 12 | speechSynthesis = window.speechSynthesis; 13 | setTimeout(() => { 14 | voices = speechSynthesis.getVoices(); 15 | console.log("VOICES after Timeout", voices); 16 | }, 3000); 17 | }; 18 | 19 | export const speak = (text) => { 20 | const lang = getUserLanguage(); 21 | const msg = new SpeechSynthesisUtterance(); 22 | if (!voices?.length) voices = speechSynthesis.getVoices(); 23 | 24 | if (voices) { 25 | msg.text = text; 26 | 27 | let voice = voices.filter((voice) => voice.lang === lang); 28 | if (!voice?.length) voice = voices.filter((voice) => voice.lang === "hi-IN"); 29 | 30 | msg.voice = voice[0]; 31 | speechSynthesis.speak(msg); 32 | } else { 33 | console.error("VOICES NAHI HAI"); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /services/storage.js: -------------------------------------------------------------------------------- 1 | const { storage } = require("constants/storage"); 2 | 3 | export const getUserId = () => localStorage.getItem(storage.userId); 4 | 5 | export const setUserLanguage = (userLanguage) => 6 | localStorage.setItem(storage.userLanguage, userLanguage); 7 | 8 | export const getUserLanguage = () => localStorage.getItem(storage.userLanguage); 9 | 10 | export const setMeetingDetails = (details) => 11 | localStorage.setItem(storage.meetingDetails, JSON.stringify(details)); 12 | export const getMeetingDetails = () => JSON.parse(localStorage.getItem(storage.meetingDetails)); 13 | -------------------------------------------------------------------------------- /services/subtitle-translate.js: -------------------------------------------------------------------------------- 1 | // import translate from "translation-google"; 2 | const { Translate } = require("@google-cloud/translate").v2; 3 | 4 | // Creates a client 5 | const translate = new Translate({ 6 | projectId: "talk-easy-d2267", 7 | keyFilename: "../service-account-key.json", 8 | }); 9 | 10 | /** 11 | * Translate a sentence from initial language to target language 12 | * @param {string} text text to be translated 13 | * @param {string} from 2 char character code of initial language 14 | * @param {string} to 2 char code of target language 15 | * @returns translated text 16 | */ 17 | export const subtitleGoogleTranslate = async (text, from, to) => { 18 | try { 19 | let [translations] = await translate.translate(text, to); 20 | translations = Array.isArray(translations) ? translations : [translations]; 21 | console.log("Translations:"); 22 | const data = translations[0]; 23 | if (data) { 24 | return data; 25 | } else { 26 | return ""; 27 | } 28 | } catch (error) { 29 | console.log(error.message); 30 | return ""; 31 | } 32 | }; 33 | 34 | /** 35 | * Fetch subtitle for a intermediary text 36 | * @param {string} text text to fetch subtitle for 37 | * @param {string} from initial language 38 | * @param {string} to target language 39 | * @returns translated subtitle 40 | */ 41 | export const fetchSubtitle = async (text, from, to) => { 42 | try { 43 | let result = await fetch("/api/only-translate", { 44 | body: JSON.stringify({ 45 | text, 46 | from, 47 | to, 48 | }), 49 | headers: { 50 | "Content-Type": "application/json", 51 | }, 52 | method: "POST", 53 | }).then((response) => response.json()); 54 | 55 | result = await result.json(); 56 | 57 | return result; 58 | } catch (error) { 59 | console.error(error); 60 | return "Error while fetching subtitles"; 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /services/translate.js: -------------------------------------------------------------------------------- 1 | import translate from "translation-google"; 2 | 3 | /** 4 | * Translate a sentence from initial language to target language 5 | * @param {string} text text to be translated 6 | * @param {string} from 2 char character code of initial language 7 | * @param {string} to 2 char code of target language 8 | * @returns translated text 9 | */ 10 | export const googleTranslate = async (text, from, to) => { 11 | console.log("data in translate", text, from, to); 12 | try { 13 | const res = await translate(text, { from: "auto", to }); 14 | return res.text; 15 | } catch (error) { 16 | console.log("error while translating", error); 17 | return ""; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /utils/convert-langauge-array-to-options.js: -------------------------------------------------------------------------------- 1 | const supportedLanguages = [ 2 | ["Afrikaans", ["af-ZA"]], 3 | ["አማርኛ", ["am-ET"]], 4 | ["Azərbaycanca", ["az-AZ"]], 5 | ["বাংলা", ["bn-BD", "বাংলাদেশ"], ["bn-IN", "ভারত"]], 6 | ["Bahasa Indonesia", ["id-ID"]], 7 | ["Bahasa Melayu", ["ms-MY"]], 8 | ["Català", ["ca-ES"]], 9 | ["Čeština", ["cs-CZ"]], 10 | ["Dansk", ["da-DK"]], 11 | ["Deutsch", ["de-DE"]], 12 | [ 13 | "English", 14 | ["en-AU", "Australia"], 15 | ["en-CA", "Canada"], 16 | ["en-IN", "India"], 17 | ["en-KE", "Kenya"], 18 | ["en-TZ", "Tanzania"], 19 | ["en-GH", "Ghana"], 20 | ["en-NZ", "New Zealand"], 21 | ["en-NG", "Nigeria"], 22 | ["en-ZA", "South Africa"], 23 | ["en-PH", "Philippines"], 24 | ["en-GB", "United Kingdom"], 25 | ["en-US", "United States"], 26 | ], 27 | [ 28 | "Español", 29 | ["es-AR", "Argentina"], 30 | ["es-BO", "Bolivia"], 31 | ["es-CL", "Chile"], 32 | ["es-CO", "Colombia"], 33 | ["es-CR", "Costa Rica"], 34 | ["es-EC", "Ecuador"], 35 | ["es-SV", "El Salvador"], 36 | ["es-ES", "España"], 37 | ["es-US", "Estados Unidos"], 38 | ["es-GT", "Guatemala"], 39 | ["es-HN", "Honduras"], 40 | ["es-MX", "México"], 41 | ["es-NI", "Nicaragua"], 42 | ["es-PA", "Panamá"], 43 | ["es-PY", "Paraguay"], 44 | ["es-PE", "Perú"], 45 | ["es-PR", "Puerto Rico"], 46 | ["es-DO", "República Dominicana"], 47 | ["es-UY", "Uruguay"], 48 | ["es-VE", "Venezuela"], 49 | ], 50 | ["Euskara", ["eu-ES"]], 51 | ["Filipino", ["fil-PH"]], 52 | ["Français", ["fr-FR"]], 53 | ["Basa Jawa", ["jv-ID"]], 54 | ["Galego", ["gl-ES"]], 55 | ["ગુજરાતી", ["gu-IN"]], 56 | ["Hrvatski", ["hr-HR"]], 57 | ["IsiZulu", ["zu-ZA"]], 58 | ["Íslenska", ["is-IS"]], 59 | ["Italiano", ["it-IT", "Italia"], ["it-CH", "Svizzera"]], 60 | ["ಕನ್ನಡ", ["kn-IN"]], 61 | ["ភាសាខ្មែរ", ["km-KH"]], 62 | ["Latviešu", ["lv-LV"]], 63 | ["Lietuvių", ["lt-LT"]], 64 | ["മലയാളം", ["ml-IN"]], 65 | ["मराठी", ["mr-IN"]], 66 | ["Magyar", ["hu-HU"]], 67 | ["ລາວ", ["lo-LA"]], 68 | ["Nederlands", ["nl-NL"]], 69 | ["नेपाली भाषा", ["ne-NP"]], 70 | ["Norsk bokmål", ["nb-NO"]], 71 | ["Polski", ["pl-PL"]], 72 | ["Português", ["pt-BR", "Brasil"], ["pt-PT", "Portugal"]], 73 | ["Română", ["ro-RO"]], 74 | ["සිංහල", ["si-LK"]], 75 | ["Slovenščina", ["sl-SI"]], 76 | ["Basa Sunda", ["su-ID"]], 77 | ["Slovenčina", ["sk-SK"]], 78 | ["Suomi", ["fi-FI"]], 79 | ["Svenska", ["sv-SE"]], 80 | ["Kiswahili", ["sw-TZ", "Tanzania"], ["sw-KE", "Kenya"]], 81 | ["ქართული", ["ka-GE"]], 82 | ["Հայերեն", ["hy-AM"]], 83 | [ 84 | "தமிழ்", 85 | ["ta-IN", "இந்தியா"], 86 | ["ta-SG", "சிங்கப்பூர்"], 87 | ["ta-LK", "இலங்கை"], 88 | ["ta-MY", "மலேசியா"], 89 | ], 90 | ["తెలుగు", ["te-IN"]], 91 | ["Tiếng Việt", ["vi-VN"]], 92 | ["Türkçe", ["tr-TR"]], 93 | ["اُردُو", ["ur-PK", "پاکستان"], ["ur-IN", "بھارت"]], 94 | ["Ελληνικά", ["el-GR"]], 95 | ["български", ["bg-BG"]], 96 | ["Pусский", ["ru-RU"]], 97 | ["Српски", ["sr-RS"]], 98 | ["Українська", ["uk-UA"]], 99 | ["한국어", ["ko-KR"]], 100 | [ 101 | "中文", 102 | ["cmn-Hans-CN", "普通话 (中国大陆)"], 103 | ["cmn-Hans-HK", "普通话 (香港)"], 104 | ["cmn-Hant-TW", "中文 (台灣)"], 105 | ["yue-Hant-HK", "粵語 (香港)"], 106 | ], 107 | ["日本語", ["ja-JP"]], 108 | ["हिन्दी", ["hi-IN"]], 109 | ["ภาษาไทย", ["th-TH"]], 110 | ]; 111 | 112 | const languages = []; 113 | 114 | supportedLanguages.forEach((langGroup) => { 115 | const langName = langGroup.splice(0, 1); 116 | langGroup.forEach((langCode) => { 117 | const code = langCode[0]; 118 | 119 | languages.push({ 120 | label: langName + " " + code, 121 | value: code, 122 | }); 123 | }); 124 | }); 125 | 126 | console.log(JSON.stringify(languages)); 127 | -------------------------------------------------------------------------------- /utils/firebase-admin.js: -------------------------------------------------------------------------------- 1 | import * as admin from "firebase-admin"; 2 | import serviceAccount from "../firebaseServiceAccount.json"; 3 | 4 | if (!admin.apps.length) 5 | admin.initializeApp({ 6 | credential: admin.credential.cert(serviceAccount), 7 | storageBucket: "talk-easy-d2267.appspot.com", 8 | }); 9 | 10 | export const db = admin.firestore(); 11 | export const firestore = admin.firestore; 12 | export const storage = admin.storage(); 13 | -------------------------------------------------------------------------------- /utils/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/app"; 2 | import "firebase/auth"; 3 | import "firebase/firestore"; 4 | 5 | import { firebaseConfig } from "constants/firebase"; 6 | 7 | export const firebaseInit = () => { 8 | if (!firebase.apps.length) firebase.initializeApp(firebaseConfig); 9 | }; 10 | --------------------------------------------------------------------------------