adds three-letter language picker

This commit is contained in:
Sara
2023-10-13 23:35:19 +02:00
parent 3555cc3a3c
commit 47fc52af11
8 changed files with 593 additions and 78 deletions

View File

@@ -0,0 +1,496 @@
import Script from "next/script";
// type Script = 'Latn' | 'Ethi' | 'Arab' | 'Beng' | 'Cyrl' | 'Taml' | 'Hant' | 'Hans' | 'Grek' | 'Gujr' | 'Hebr'| 'Deva'| 'Armn' | 'Jpan' | 'Knda' | 'Geor';
type LanguageOption = {
value: string | undefined;
name: string;
script?: string;
};
const supportedLanguages: LanguageOption[] = [
{ value: "afr", name: "Afrikaans", script: "Latn" },
{
value: "amh",
name: "Amharic",
script: "Ethi",
},
{
value: "arb",
name: "Modern Standard Arabic",
script: "Arab",
},
{
value: "ary",
name: "Moroccan Arabic",
script: "Arab",
},
{
value: "arz",
name: "Egyptian Arabic",
script: "Arab",
},
{
value: "asm",
name: "Assamese",
script: "Beng",
},
{
value: "azj",
name: "North Azerbaijani",
script: "Latn",
},
{
value: "bel",
name: "Belarusian",
script: "Cyrl",
},
{
value: "ben",
name: "Bengali",
script: "Beng",
},
{
value: "bos",
name: "Bosnian",
script: "Latn",
},
{
value: "bul",
name: "Bulgarian",
script: "Cyrl",
},
{
value: "cat",
name: "Catalan",
script: "Latn",
},
{
value: "ceb",
name: "Cebuano",
script: "Latn",
},
{
value: "ces",
name: "Czech",
script: "Latn",
},
{
value: "ckb",
name: "Central Kurdish",
script: "Arab",
},
{
value: "cmn",
name: "Mandarin Chinese",
script: "Hans",
},
{
value: "cmn_Ha",
name: "Mandarin Chinese",
script: "Hant",
},
{
value: "cym",
name: "Welsh",
script: "Latn",
},
{
value: "dan",
name: "Danish",
script: "Latn",
},
{
value: "deu",
name: "German",
script: "Latn",
},
{
value: "ell",
name: "Greek",
script: "Grek",
},
{
value: "eng",
name: "English",
script: "Latn",
},
{
value: "est",
name: "Estonian",
script: "Latn",
},
{
value: "eus",
name: "Basque",
script: "Latn",
},
{
value: "fin",
name: "Finnish",
script: "Latn",
},
{
value: "fra",
name: "French",
script: "Latn",
},
{
value: "gaz",
name: "West Central Oromo",
script: "Latn",
},
{
value: "gle",
name: "Irish",
script: "Latn",
},
{
value: "glg",
name: "Galician",
script: "Latn",
},
{
value: "guj",
name: "Gujarati",
script: "Gujr",
},
{
value: "heb",
name: "Hebrew",
script: "Hebr",
},
{
value: "hin",
name: "Hindi",
script: "Deva",
},
{
value: "hrv",
name: "Croatian",
script: "Latn",
},
{
value: "hun",
name: "Hungarian",
script: "Latn",
},
{
value: "hye",
name: "Armenian",
script: "Armn",
},
{
value: "ibo",
name: "Igbo",
script: "Latn",
},
{
value: "ind",
name: "Indonesian",
script: "Latn",
},
{
value: "isl",
name: "Icelandic",
script: "Latn",
},
{
value: "ita",
name: "Italian",
script: "Latn",
},
{
value: "jav",
name: "Javanese",
script: "Latn",
},
{
value: "jpn",
name: "Japanese",
script: "Jpan",
},
{
value: "kan",
name: "Kannada",
script: "Knda",
},
{
value: "kat",
name: "Georgian",
script: "Geor",
},
{
value: "kaz",
name: "Kazakh",
script: "Cyrl",
},
{
value: "khk",
name: "Halh Mongolian",
script: "Cyrl",
},
{
value: "khm",
name: "Khmer",
script: "Khmr",
},
{
value: "kir",
name: "Kyrgyz",
script: "Cyrl",
},
{
value: "kor",
name: "Korean",
script: "Kore",
},
{
value: "lao",
name: "Lao",
script: "Laoo",
},
{
value: "lit",
name: "Lithuanian",
script: "Latn",
},
{
value: "lug",
name: "Ganda",
script: "Latn",
},
{
value: "luo",
name: "Luo",
script: "Latn",
},
{
value: "lvs",
name: "Standard Latvian",
script: "Latn",
},
{
value: "mai",
name: "Maithili",
script: "Deva",
},
{
value: "mal",
name: "Malayalam",
script: "Mlym",
},
{
value: "mar",
name: "Marathi",
script: "Deva",
},
{
value: "mkd",
name: "Macedonian",
script: "Cyrl",
},
{
value: "mlt",
name: "Maltese",
script: "Latn",
},
{
value: "mni",
name: "Meitei",
script: "Beng",
},
{
value: "mya",
name: "Burmese",
script: "Mymr",
},
{
value: "nld",
name: "Dutch",
script: "Latn",
},
{
value: "nno",
name: "Norwegian Nynorsk",
script: "Latn",
},
{
value: "nob",
name: "Norwegian Bokmål",
script: "Latn",
},
{
value: "npi",
name: "Nepali",
script: "Deva",
},
{
value: "nya",
name: "Nyanja",
script: "Latn",
},
{
value: "ory",
name: "Odia",
script: "Orya",
},
{
value: "pan",
name: "Punjabi",
script: "Guru",
},
{
value: "pbt",
name: "Southern Pashto",
script: "Arab",
},
{
value: "pes",
name: "Western Persian",
script: "Arab",
},
{
value: "pol",
name: "Polish",
script: "Latn",
},
{
value: "por",
name: "Portuguese",
script: "Latn",
},
{
value: "ron",
name: "Romanian",
script: "Latn",
},
{
value: "rus",
name: "Russian",
script: "Cyrl",
},
{
value: "slk",
name: "Slovak",
script: "Latn",
},
{
value: "slv",
name: "Slovenian",
script: "Latn",
},
{
value: "sna",
name: "Shona",
script: "Latn",
},
{
value: "snd",
name: "Sindhi",
script: "Arab",
},
{
value: "som",
name: "Somali",
script: "Latn",
},
{
value: "spa",
name: "Spanish",
script: "Latn",
},
{
value: "srp",
name: "Serbian",
script: "Cyrl",
},
{
value: "swe",
name: "Swedish",
script: "Latn",
},
{
value: "swh",
name: "Swahili",
script: "Latn",
},
{
value: "tam",
name: "Tamil",
script: "Taml",
},
{
value: "tel",
name: "Telugu",
script: "Telu",
},
{
value: "tgk",
name: "Tajik",
script: "Cyrl",
},
{
value: "tgl",
name: "Tagalog",
script: "Latn",
},
{
value: "tha",
name: "Thai",
script: "Thai",
},
{
value: "tur",
name: "Turkish",
script: "Latn",
},
{
value: "ukr",
name: "Ukrainian",
script: "Cyrl",
},
{
value: "urd",
name: "Urdu",
script: "Arab",
},
{
value: "uzn",
name: "Northern Uzbek",
script: "Latn",
},
{
value: "vie",
name: "Vietnamese",
script: "Latn",
},
{
value: "yor",
name: "Yoruba",
script: "Latn",
},
{
value: "yue",
name: "Cantonese",
script: "Hant",
},
{
value: "zsm",
name: "Standard Malay",
script: "Latn",
},
{
value: "zul",
name: "Zulu",
script: "Latn",
},
];
const supportedLatinLanguages = supportedLanguages.filter(
(lan) => lan.script == "Latn",
);
supportedLatinLanguages.push({ value: undefined, name: "None" });
export { supportedLatinLanguages };
export default supportedLanguages;

View File

@@ -41,14 +41,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
const webRTC = useWebRTC(stream, details.params.transcriptId, api); const webRTC = useWebRTC(stream, details.params.transcriptId, api);
const webSockets = useWebSockets(details.params.transcriptId); const webSockets = useWebSockets(details.params.transcriptId);
const { const { audioDevices, getAudioStream } = useAudioDevice();
loading,
permissionOk,
permissionDenied,
audioDevices,
requestPermission,
getAudioStream,
} = useAudioDevice();
const [hasRecorded, setHasRecorded] = useState(false); const [hasRecorded, setHasRecorded] = useState(false);
const [transcriptStarted, setTranscriptStarted] = useState(false); const [transcriptStarted, setTranscriptStarted] = useState(false);
@@ -115,7 +108,10 @@ const TranscriptRecord = (details: TranscriptDetails) => {
you start recording. you start recording.
</div> </div>
) : ( ) : (
<LiveTrancription text={webSockets.transcriptText} /> <LiveTrancription
text={webSockets.transcriptText}
translateText={webSockets.translateText}
/>
)} )}
</div> </div>
</div> </div>

View File

@@ -25,7 +25,7 @@ const useCreateTranscript = (): CreateTranscript => {
const requestParameters: V1TranscriptsCreateRequest = { const requestParameters: V1TranscriptsCreateRequest = {
createTranscript: { createTranscript: {
name: params.name || "Weekly All-Hands", // Default name: params.name || "Weekly All-Hands", // Default
targetLanguage: params.targetLanguage || "en", // Default targetLanguage: params.targetLanguage || "eng", // Default
}, },
}; };

View File

@@ -1,14 +1,19 @@
type LiveTranscriptionProps = { type LiveTranscriptionProps = {
text: string; text: string;
translateText: string;
}; };
export default function LiveTrancription(props: LiveTranscriptionProps) { export default function LiveTrancription(props: LiveTranscriptionProps) {
return ( return (
<div className="text-center p-4"> <div className="text-center p-4">
<p className="text-lg md:text-xl font-bold line-clamp-4"> <p className="text-lg md:text-xl font-bold line-clamp-4">
{/* Nous allons prendre quelques appels téléphoniques et répondre à quelques questions */} {props.translateText ? props.translateText : props.text}
</p>
{props.translateText && (
<p className="text-base md:textlg font-bold line-clamp-4">
{props.text} {props.text}
</p> </p>
)}
</div> </div>
); );
} }

View File

@@ -1,20 +1,15 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import useWebRTC from "../useWebRTC";
import useTranscript from "../useTranscript";
import { useWebSockets } from "../useWebSockets";
import useAudioDevice from "../useAudioDevice"; import useAudioDevice from "../useAudioDevice";
import "../../styles/button.css"; import "../../styles/button.css";
import { Topic } from "../webSocketTypes";
import getApi from "../../lib/getApi"; import getApi from "../../lib/getApi";
import About from "../../(aboutAndPrivacy)/about"; import About from "../../(aboutAndPrivacy)/about";
import Privacy from "../../(aboutAndPrivacy)/privacy"; import Privacy from "../../(aboutAndPrivacy)/privacy";
import { lockWakeState, releaseWakeState } from "../../lib/wakeLock";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import createTranscript from "../createTranscript";
import { GetTranscript } from "../../api";
import { Router } from "next/router";
import useCreateTranscript from "../createTranscript"; import useCreateTranscript from "../createTranscript";
import SelectSearch from "react-select-search";
import { supportedLatinLanguages } from "../../supportedLanguages";
import "react-select-search/style.css";
const TranscriptCreate = () => { const TranscriptCreate = () => {
// const transcript = useTranscript(stream, api); // const transcript = useTranscript(stream, api);
@@ -27,6 +22,10 @@ const TranscriptCreate = () => {
}; };
const [targetLanguage, setTargetLanguage] = useState<string>(); const [targetLanguage, setTargetLanguage] = useState<string>();
const onLanguageChange = (newval) => {
typeof newval === "string" && setTargetLanguage(newval);
};
const createTranscript = useCreateTranscript(); const createTranscript = useCreateTranscript();
const send = () => { const send = () => {
@@ -38,54 +37,61 @@ const TranscriptCreate = () => {
router.push(`/transcripts/${createTranscript.response.id}/record`); router.push(`/transcripts/${createTranscript.response.id}/record`);
}, [createTranscript.response]); }, [createTranscript.response]);
const { const { loading, permissionOk, permissionDenied, requestPermission } =
loading, useAudioDevice();
permissionOk,
permissionDenied,
audioDevices,
requestPermission,
getAudioStream,
} = useAudioDevice();
return ( return (
<> <>
<div></div> <div></div>
<div className="max-h-full overflow-auto"> <div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-mobile-inner lg:grid-rows-1 gap-2 lg:gap-4 h-full">
<section className="flex flex-col w-full h-full items-center justify-evenly p-4 md:px-6 md:py-8"> <section className="flex flex-col w-full h-full items-center justify-evenly p-4 md:px-6 md:py-8">
<div>
<div className="flex flex-col max-w-xl items-center justify-center"> <div className="flex flex-col max-w-xl items-center justify-center">
<h1 className="text-2xl font-bold mb-2"> <h1 className="text-2xl font-bold mb-2">
Welcome to reflector.media Welcome to reflector.media
</h1> </h1>
<p> <p>
Reflector is a transcription and summarization pipeline that Reflector is a transcription and summarization pipeline that
transforms audio into knowledge. The output is meeting minutes transforms audio into knowledge. The output is meeting minutes and
and topic summaries enabling topic-specific analyses stored in topic summaries enabling topic-specific analyses stored in your
your systems of record. This is accomplished on your systems of record. This is accomplished on your infrastructure
infrastructure without 3rd parties keeping your data without 3rd parties keeping your data private, secure, and
private, secure, and organized. organized.
</p> </p>
<About buttonText="Learn more" /> <About buttonText="Learn more" />
</div>
</section>
<section className="rounded-xl md:bg-blue-200 flex flex-col justify-start p-6">
<h2 className="text-2xl font-bold mt-4 mb-2"> Try Reflector</h2>
<label className="mb-3">
<p>What is this meeting about ?</p>
<input type="text" onChange={nameChange} /> <input type="text" onChange={nameChange} />
<button onClick={() => setTargetLanguage("fr")}>Language</button> </label>
<h2 className="text-2xl font-bold mt-4 mb-2">
Audio Permissions <label className="mb-3">
</h2> <p>Do you need live translation ?</p>
<SelectSearch
search
options={supportedLatinLanguages}
value={targetLanguage}
onChange={onLanguageChange}
placeholder="Choose your language"
/>
</label>
{loading ? ( {loading ? (
<p className="text-center">Checking permission...</p> <p className="">Checking permission...</p>
) : permissionOk ? ( ) : permissionOk ? (
<> Microphone permission granted </> <> Microphone permission granted </>
) : ( ) : (
<> <>
<p className="text-center"> <p className="">
In order to use Reflector, we kindly request permission to In order to use Reflector, we kindly request permission to
access your microphone during meetings and events. access your microphone during meetings and events.
<br /> <br />
<Privacy buttonText="Privacy policy" /> <Privacy buttonText="Privacy policy" />
<br /> <br />
{permissionDenied {permissionDenied &&
? "Permission to use your microphone was denied, please change the permission setting in your browser and refresh this page." "Permission to use your microphone was denied, please change the permission setting in your browser and refresh this page."}
: "Please grant permission to continue."}
</p> </p>
<button <button
className="mt-4 bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white font-bold py-2 px-4 rounded m-auto" className="mt-4 bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white font-bold py-2 px-4 rounded m-auto"
@@ -96,11 +102,9 @@ const TranscriptCreate = () => {
</button> </button>
</> </>
)} )}
</div>
<button onClick={send} disabled={!permissionOk}> <button onClick={send} disabled={!permissionOk}>
{createTranscript.loading ? "loading" : "Send"} {createTranscript.loading ? "loading" : "Send"}
</button> </button>
</div>
</section> </section>
</div> </div>
</> </>

View File

@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
type UseWebSockets = { type UseWebSockets = {
transcriptText: string; transcriptText: string;
translateText: string;
topics: Topic[]; topics: Topic[];
finalSummary: FinalSummary; finalSummary: FinalSummary;
status: Status; status: Status;
@@ -12,7 +13,9 @@ type UseWebSockets = {
export const useWebSockets = (transcriptId: string | null): UseWebSockets => { export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
const [transcriptText, setTranscriptText] = useState<string>(""); const [transcriptText, setTranscriptText] = useState<string>("");
const [translateText, setTranslateText] = useState<string>("");
const [textQueue, setTextQueue] = useState<string[]>([]); const [textQueue, setTextQueue] = useState<string[]>([]);
const [translationQueue, setTranslationQueue] = useState<string[]>([]);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [topics, setTopics] = useState<Topic[]>([]); const [topics, setTopics] = useState<Topic[]>([]);
const [finalSummary, setFinalSummary] = useState<FinalSummary>({ const [finalSummary, setFinalSummary] = useState<FinalSummary>({
@@ -30,6 +33,8 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
setIsProcessing(true); setIsProcessing(true);
const text = textQueue[0]; const text = textQueue[0];
setTranscriptText(text); setTranscriptText(text);
setTranslateText(translationQueue[0]);
console.log("displaying " + translateText);
const WPM_READING = 200 + textQueue.length * 10; // words per minute to read const WPM_READING = 200 + textQueue.length * 10; // words per minute to read
const wordCount = text.split(/\s+/).length; const wordCount = text.split(/\s+/).length;
@@ -38,6 +43,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
setTimeout(() => { setTimeout(() => {
setIsProcessing(false); setIsProcessing(false);
setTextQueue((prevQueue) => prevQueue.slice(1)); setTextQueue((prevQueue) => prevQueue.slice(1));
setTranslationQueue((prevQueue) => prevQueue.slice(1));
}, delay); }, delay);
}, [textQueue, isProcessing]); }, [textQueue, isProcessing]);
@@ -158,11 +164,13 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
switch (message.event) { switch (message.event) {
case "TRANSCRIPT": case "TRANSCRIPT":
const newText = (message.data.text ?? "").trim(); const newText = (message.data.text ?? "").trim();
const newTranslation = (message.data.translation ?? "").trim();
if (!newText) break; if (!newText) break;
console.debug("TRANSCRIPT event:", newText); console.debug("TRANSCRIPT event:", newText);
setTextQueue((prevQueue) => [...prevQueue, newText]); setTextQueue((prevQueue) => [...prevQueue, newText]);
setTranslationQueue((prevQueue) => [...prevQueue, newTranslation]);
break; break;
case "TOPIC": case "TOPIC":
@@ -233,5 +241,5 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
}; };
}, [transcriptId]); }, [transcriptId]);
return { transcriptText, topics, finalSummary, status }; return { transcriptText, translateText, topics, finalSummary, status };
}; };

View File

@@ -27,6 +27,7 @@
"react-dropdown": "^1.11.0", "react-dropdown": "^1.11.0",
"react-markdown": "^9.0.0", "react-markdown": "^9.0.0",
"react-qr-code": "^2.0.12", "react-qr-code": "^2.0.12",
"react-select-search": "^4.1.7",
"sass": "^1.63.6", "sass": "^1.63.6",
"simple-peer": "^9.11.1", "simple-peer": "^9.11.1",
"superagent": "^8.0.9", "superagent": "^8.0.9",

View File

@@ -2088,6 +2088,11 @@ react-qr-code@^2.0.12:
prop-types "^15.8.1" prop-types "^15.8.1"
qr.js "0.0.0" qr.js "0.0.0"
react-select-search@^4.1.7:
version "4.1.7"
resolved "https://registry.yarnpkg.com/react-select-search/-/react-select-search-4.1.7.tgz#5662729b9052282bde52e1352006d495d9c5ed6e"
integrity sha512-pU7ONAdK+bmz2tbhBWYQv9m5mnXOn8yImuiy+5UhimIG80d5iKv3nSYJIjJWjDbdrrdoXiCRwQm8xbA8llTjmQ==
react@^18.2.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"