leumas-private-shared
Version:
Private React JSX Package For Leumas Shared Components, Headers, Footers, Asides, Login Pages, API Key Manager and much more. Styles and everything reusable to avoid DRY code across all of our subdomains
226 lines (205 loc) • 9.32 kB
JSX
import React, { useState, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import debounce from 'lodash.debounce';
import { Helmet } from 'react-helmet';
import translateText from './translateText';
import languagesMap from './languagesMap';
import {
Container,
Typography,
Select,
MenuItem,
Button,
Card,
CardContent,
Grid,
IconButton,
TextField,
Paper,
CircularProgress,
Snackbar
} from '@mui/material';
import VolumeUpIcon from '@mui/icons-material/VolumeUp';
import StopIcon from '@mui/icons-material/Stop';
import InfinityBackgroundComponent from '../Components/Backgrounds/Infinity';
import MuiAlert from '@mui/material/Alert';
const Alert = React.forwardRef((props, ref) => <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />);
const VoiceToTextParrot = () => {
const { language } = useParams();
const [text, setText] = useState('');
const [translations, setTranslations] = useState([]);
const [isListening, setIsListening] = useState(false);
const [mode, setMode] = useState('press-to-speak'); // 'always-listening', 'press-to-speak', or 'type-to-translate'
const [loading, setLoading] = useState(false);
const [openSnackbar, setOpenSnackbar] = useState(false);
const recognitionRef = useRef(null);
const utteranceRef = useRef(null);
useEffect(() => {
recognitionRef.current = new window.webkitSpeechRecognition();
recognitionRef.current.lang = 'en-US';
recognitionRef.current.interimResults = true;
recognitionRef.current.continuous = mode === 'always-listening';
recognitionRef.current.onresult = (event) => {
const current = event.resultIndex;
const transcript = event.results[current][0].transcript;
setText(transcript);
debouncedTranslateText(transcript);
};
recognitionRef.current.onerror = (event) => {
console.error('Speech recognition error', event.error);
setOpenSnackbar(true);
};
if (mode === 'always-listening') {
recognitionRef.current.start();
}
return () => {
recognitionRef.current.stop();
};
}, [mode]);
const handlePressToSpeak = () => {
if (!isListening) {
recognitionRef.current.start();
setIsListening(true);
} else {
recognitionRef.current.stop();
setIsListening(false);
}
};
const handleTranslation = async (text) => {
setLoading(true);
const translatedText = await translateText(text, language);
setLoading(false);
if (translatedText) {
setTranslations(prev => [...prev, { original: text, translated: translatedText }]);
handleSpeak(translatedText);
}
};
const debouncedTranslateText = useRef(debounce((text) => {
handleTranslation(text);
}, 1000)).current;
const handleSpeak = (text) => {
if (utteranceRef.current) {
window.speechSynthesis.cancel();
}
utteranceRef.current = new SpeechSynthesisUtterance(text);
utteranceRef.current.lang = languagesMap[language.toLowerCase()];
window.speechSynthesis.speak(utteranceRef.current);
};
const handleStopSpeak = () => {
if (utteranceRef.current) {
window.speechSynthesis.cancel();
utteranceRef.current = null;
}
};
const handleTextChange = (e) => {
const typedText = e.target.value;
setText(typedText);
debouncedTranslateText(typedText);
};
const handleCloseSnackbar = () => {
setOpenSnackbar(false);
};
const languageDisplayName = language.charAt(0).toUpperCase() + language.slice(1);
const metaDescription = `Translate text to ${languageDisplayName} dynamically using our voice-to-text translation tool.`;
return (
<>
<Helmet>
<title>{`Voice to Text and Translate to ${languageDisplayName} | Leumas Tech`}</title>
<meta name="description" content={metaDescription} />
<meta name="keywords" content={`translate, voice to text, ${languageDisplayName} translation, real-time translation, speech recognition`} />
<meta name="language" content={languagesMap[language.toLowerCase()]} />
<link rel="canonical" href={`https://yourwebsite.com/speaker/${language}`} />
<meta property="og:title" content={`Voice to Text and Translate to ${languageDisplayName} | Leumas Tech`} />
<meta property="og:description" content={metaDescription} />
<meta property="og:image" content="https://res.cloudinary.com/dx25lltre/image/upload/v1707175639/Leumas/LEUMAS_Tech_Logo_500x500_bvg8wu.png" />
<meta property="og:url" content={`https://yourwebsite.com/speaker/${language}`} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Leumas Tech" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={`Voice to Text and Translate to ${languageDisplayName} | Leumas Tech`} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:image" content="https://res.cloudinary.com/dx25lltre/image/upload/v1707175639/Leumas/LEUMAS_Tech_Logo_500x500_bvg8wu.png" />
<meta name="twitter:site" content="@LeumasTech" />
<link rel="icon" href="https://res.cloudinary.com/dx25lltre/image/upload/v1707175639/Leumas/LEUMAS_Tech_Logo_500x500_bvg8wu.png" />
</Helmet>
<InfinityBackgroundComponent>
<Container maxWidth="md" style={{ paddingTop: '20px', paddingBottom: '20px' }}>
<Paper elevation={3} style={{ padding: '20px' }}>
<Typography variant="h4" gutterBottom style={{ color: '#3f51b5', fontWeight: 'bold' }}>Voice to Text and Translate</Typography>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} sm={4}>
<Typography variant="h6" style={{ color: '#3f51b5' }}>Select Mode:</Typography>
<Select
fullWidth
value={mode}
onChange={(e) => setMode(e.target.value)}
>
<MenuItem value="press-to-speak">Press to Speak</MenuItem>
<MenuItem value="always-listening">Always Listening</MenuItem>
<MenuItem value="type-to-translate">Type to Translate</MenuItem>
</Select>
</Grid>
{mode === 'press-to-speak' && (
<Grid item xs={12} sm={4}>
<Button
variant="contained"
color={isListening ? 'secondary' : 'primary'}
onClick={handlePressToSpeak}
style={{ marginTop: '24px' }}
>
{isListening ? 'Stop Listening' : 'Press to Speak'}
</Button>
</Grid>
)}
{mode === 'type-to-translate' && (
<Grid item xs={12} sm={4}>
<TextField
fullWidth
label="Type text to translate"
variant="outlined"
value={text}
onChange={handleTextChange}
style={{ marginTop: '16px' }}
/>
</Grid>
)}
<Grid item xs={12} sm={4}>
<Button
variant="contained"
color="secondary"
onClick={handleStopSpeak}
style={{ marginTop: '24px' }}
>
<StopIcon /> Stop Speaking
</Button>
</Grid>
</Grid>
{loading && <CircularProgress style={{ display: 'block', margin: '20px auto' }} />}
<Typography variant="h6" gutterBottom style={{ marginTop: '20px', color: '#3f51b5' }}>Translations</Typography>
<div style={{ maxHeight: '400px', overflowY: 'auto', border: '1px solid #3f51b5', borderRadius: '4px', padding: '10px', backgroundColor: '#f5f5f5' }}>
{translations.map((translation, index) => (
<Card key={index} style={{ marginBottom: '10px' }}>
<CardContent>
<Typography variant="body1"><strong>Original:</strong> {translation.original}</Typography>
<Typography variant="body1">
<strong>Translation ({languageDisplayName}):</strong> {translation.translated}
<IconButton onClick={() => handleSpeak(translation.translated)} aria-label="speak">
<VolumeUpIcon />
</IconButton>
</Typography>
</CardContent>
</Card>
))}
</div>
</Paper>
</Container>
<Snackbar open={openSnackbar} autoHideDuration={6000} onClose={handleCloseSnackbar}>
<Alert onClose={handleCloseSnackbar} severity="error">
Speech recognition error occurred.
</Alert>
</Snackbar>
</InfinityBackgroundComponent>
</>
);
};
export default VoiceToTextParrot;