├── .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 | [](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 | } colorScheme="red">
82 | Leave
83 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------