Skip to content
This repository has been archived by the owner on Apr 29, 2022. It is now read-only.

Add option to set voice (TTS) speed #363

Merged
merged 1 commit into from
Dec 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions main-src/libs/locales/de/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"allowTelemetry": "Erlaube der App anonyme Nutzungsdaten zu senden",
"share": "Teilen",
"extensions": "Erweiterungen",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"extensionDesc": "Wählen Sie den Text mit der Maus aus und übersetzen Sie ihn mit nur einem Klick.",
"privacyPolicy": "Datenschutzrichtlinien",
"voiceSpeed": "Sprachgeschwindigkeit",
"slowest": "Langsamste",
"normal": "Normal",
"fastest": "Schnellste"
}
6 changes: 5 additions & 1 deletion main-src/libs/locales/en/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,9 @@
"share": "Share",
"extensions": "Extensions",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"privacyPolicy": "Privacy Policy",
"voiceSpeed": "Voice speed",
"slowest": "Slowest",
"normal": "Normal",
"fastest": "Fastest"
}
8 changes: 6 additions & 2 deletions main-src/libs/locales/es/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"allowTelemetry": "Permitir que la aplicación envíe datos de uso anónimos",
"share": "Compartir",
"extensions": "Extensiones",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"extensionDesc": "Seleccione el texto con el ratón y traduzca con un solo clic.",
"privacyPolicy": "Política de Privacidad",
"voiceSpeed": "Velocidad de voz",
"slowest": "Muy lento",
"normal": "Normal",
"fastest": "Lo más rápido"
}
8 changes: 6 additions & 2 deletions main-src/libs/locales/fr/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"allowTelemetry": "Autoriser l'application à envoyer des données d'utilisation anonymes",
"share": "Partager",
"extensions": "Extensions",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"extensionDesc": "Sélectionnez le texte avec votre souris et traduisez en un seul clic.",
"privacyPolicy": "Politique de Confidentialité",
"voiceSpeed": "Vitesse de la voix",
"slowest": "Le plus lent",
"normal": "Normale",
"fastest": "Le plus rapide"
}
6 changes: 5 additions & 1 deletion main-src/libs/locales/it/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,9 @@
"share": "Condividi",
"extensions": "Estensioni",
"extensionDesc": "Seleziona il testo con il mouse e traduci con un solo clic.",
"privacyPolicy": "Privacy Policy"
"privacyPolicy": "Politica sulla Privacy",
"voiceSpeed": "Velocità vocale",
"slowest": "Più lenta",
"normal": "Normali",
"fastest": "Più Veloce"
}
8 changes: 6 additions & 2 deletions main-src/libs/locales/ja/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"allowTelemetry": "匿名の利用状況データの送信をアプリに許可する",
"share": "シェアする",
"extensions": "拡張機能",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"extensionDesc": "マウスでテキストを選択し、ワンクリックで翻訳します。",
"privacyPolicy": "プライバシーポリシー",
"voiceSpeed": "音声スピード",
"slowest": "最も低速",
"normal": "普通",
"fastest": "最速"
}
8 changes: 6 additions & 2 deletions main-src/libs/locales/ko/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"allowTelemetry": "앱이 익명의 사용 데이터를 보내도록 허용",
"share": "공유",
"extensions": "확장 프로그램",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"extensionDesc": "마우스로 텍스트를 선택하고 클릭 한 번으로 번역하세요.",
"privacyPolicy": "개인정보 취급방침",
"voiceSpeed": "음성 속도",
"slowest": "가장 느리게",
"normal": "보통",
"fastest": "가장 빠르게"
}
8 changes: 6 additions & 2 deletions main-src/libs/locales/pt-PT/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"allowTelemetry": "Permitir que a aplicação envie dados de utilização anónimos",
"share": "Compartilhar",
"extensions": "Extensões",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"extensionDesc": "Selecione texto com o seu mouse e traduza com apenas um clique.",
"privacyPolicy": "Política de Privacidade",
"voiceSpeed": "Velocidade de voz",
"slowest": "Mais lento",
"normal": "Normal",
"fastest": "Mais rápida"
}
8 changes: 6 additions & 2 deletions main-src/libs/locales/ru/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"allowTelemetry": "Разрешить приложению отправлять анонимные данные об использовании",
"share": "Поделиться",
"extensions": "Расширения",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"extensionDesc": "Выбирайте текст мышкой и переводите одним кликом.",
"privacyPolicy": "Политика приватности",
"voiceSpeed": "Скорость голоса",
"slowest": "Медленнейш",
"normal": "Норм",
"fastest": "Минимум"
}
6 changes: 5 additions & 1 deletion main-src/libs/locales/vi/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,9 @@
"share": "Chia sẻ",
"extensions": "Tiện ích mở rộng",
"extensionDesc": "Chọn văn bản bằng chuột và dịch chỉ với một cú nhấp chuột.",
"privacyPolicy": "Chính sách bảo mật"
"privacyPolicy": "Chính sách bảo mật",
"voiceSpeed": "Tốc độ giọng nói",
"slowest": "Chậm nhất",
"normal": "Bình thường",
"fastest": "Nhanh nhất"
}
8 changes: 6 additions & 2 deletions main-src/libs/locales/zh-CN/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"allowTelemetry": "允许该应用程序发送匿名使用情况数据",
"share": "分享",
"extensions": "扩展程序",
"extensionDesc": "Select text with your mouse and translate with just one click.",
"privacyPolicy": "Privacy Policy"
"extensionDesc": "用鼠标选择文本并只需点击一下即可翻译。",
"privacyPolicy": "隐私政策",
"voiceSpeed": "语音速度",
"slowest": "最慢",
"normal": "正常",
"fastest": "最快"
}
2 changes: 2 additions & 0 deletions main-src/libs/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const defaultPreferences = {
translateClipboardOnShortcut: false,
translateWhenPressingEnter: true,
useHardwareAcceleration: true,
// 10 => 1x (speed goes from 0.1x to 10x, we multiple by 10 to avoid using decimals)
voiceSpeed: 10,
};

const initCachedPreferences = () => {
Expand Down
73 changes: 38 additions & 35 deletions src/components/pages/preferences/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { open as openDialogShortcut } from '../../../state/pages/preferences/sho
import { open as openDialogOpenSourceNotices } from '../../../state/root/dialog-open-source-notices/actions';

import DialogShortcut from './dialog-shortcut';
import ListItemSliders from './list-item-sliders';

import {
requestOpenInBrowser,
Expand Down Expand Up @@ -274,44 +275,11 @@ const Preferences = (props) => {
/>
</ListItemSecondaryAction>
</ListItem>
<Divider />
<ListItemSliders />
</List>
</Paper>

{/* extensions are not supported on Linux because it doesn't support protocol translatium:// */}
{window.process.platform !== 'linux' && (
<>
<Typography variant="body2" className={classes.paperTitle}>
{getLocale('extensions')}
</Typography>
<Paper elevation={0} className={classes.paper}>
<List dense disablePadding>
<ListItem
button
onClick={() => window.remote.shell.openExternal('https://webcatalog.io/translatium/extensions/')}
>
<div className={classes.extensionBlock}>
<div className={classes.extensionIcons}>
<img src={popclipIconPng} alt="PopClip" className={classes.extensionIcon} />
<img src={chromeIconPng} alt="Google Chrome" className={classes.extensionIcon} />
<img src={edgeIconPng} alt="Microsoft Edge" className={classes.extensionIcon} />
<img src={safariIconPng} alt="Safari" className={classes.extensionIcon} />
<img src={firefoxIconPng} alt="Firefox" className={classes.extensionIcon} />
<img src={operaIconPng} alt="Opera" className={classes.extensionIcon} />
<img src={braveIconPng} alt="Brave" className={classes.extensionIcon} />
<img src={vivaldiIconPng} alt="Vivaldi" className={classes.extensionIcon} />
</div>
<ListItemText
primary={getLocale('extensions')}
secondary={getLocale('extensionDesc')}
/>
</div>
<ChevronRightIcon color="action" />
</ListItem>
</List>
</Paper>
</>
)}

<Typography variant="body2" className={classes.paperTitle}>
{getLocale('menubar')}
</Typography>
Expand Down Expand Up @@ -384,6 +352,41 @@ const Preferences = (props) => {
</List>
</Paper>

{/* extensions are not supported on Linux because it doesn't support protocol translatium:// */}
{window.process.platform !== 'linux' && (
<>
<Typography variant="body2" className={classes.paperTitle}>
{getLocale('extensions')}
</Typography>
<Paper elevation={0} className={classes.paper}>
<List dense disablePadding>
<ListItem
button
onClick={() => window.remote.shell.openExternal('https://webcatalog.io/translatium/extensions/')}
>
<div className={classes.extensionBlock}>
<div className={classes.extensionIcons}>
<img src={popclipIconPng} alt="PopClip" className={classes.extensionIcon} />
<img src={chromeIconPng} alt="Google Chrome" className={classes.extensionIcon} />
<img src={edgeIconPng} alt="Microsoft Edge" className={classes.extensionIcon} />
<img src={safariIconPng} alt="Safari" className={classes.extensionIcon} />
<img src={firefoxIconPng} alt="Firefox" className={classes.extensionIcon} />
<img src={operaIconPng} alt="Opera" className={classes.extensionIcon} />
<img src={braveIconPng} alt="Brave" className={classes.extensionIcon} />
<img src={vivaldiIconPng} alt="Vivaldi" className={classes.extensionIcon} />
</div>
<ListItemText
primary={getLocale('extensions')}
secondary={getLocale('extensionDesc')}
/>
</div>
<ChevronRightIcon color="action" />
</ListItem>
</List>
</Paper>
</>
)}

<Typography variant="body2" className={classes.paperTitle}>
{getLocale('advanced')}
</Typography>
Expand Down
112 changes: 112 additions & 0 deletions src/components/pages/preferences/list-item-sliders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import PropTypes from 'prop-types';

import Grid from '@material-ui/core/Grid';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Slider from '@material-ui/core/Slider';
import Typography from '@material-ui/core/Typography';

import connectComponent from '../../../helpers/connect-component';
import getLocale from '../../../helpers/get-locale';

import {
requestSetPreference,
} from '../../../senders';

const styles = (theme) => ({
sliderContainer: {
paddingTop: theme.spacing(2),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(5),
},
sliderTitleContainer: {
paddingTop: `${theme.spacing(1.5)}px !important`,
width: 160,
},
sliderMarkLabel: {
fontSize: '0.75rem',
},
});

// Well, math
// we use different scaling system for the slider to make it look nice
const convertSliderValToPreferenceVal = (val) => {
if (val >= 0) return val + 10; // 0 -> 10, 30 -> 40
return 10 + (val * 2) / 10; // -25 => 5
};

const convertPreferenceValToSliderVal = (val) => {
if (val >= 10) return val - 10; // 10 -> 0, 40 -> 30
return (val - 10) * 5; // 5 -> -25
};

const ListItemSliders = ({
classes,
voiceSpeed,
}) => (
<ListItem>
<ListItemText className={classes.sliderContainer}>
<Grid container spacing={2}>
<Grid classes={{ item: classes.sliderTitleContainer }} item>
<Typography id="brightness-slider" variant="body2" gutterBottom={false}>
{getLocale('voiceSpeed')}
</Typography>
</Grid>
<Grid item xs>
<Slider
classes={{ markLabel: classes.sliderMarkLabel }}
value={convertPreferenceValToSliderVal(voiceSpeed)}
aria-labelledby="voice-speed-slider"
valueLabelDisplay="auto"
step={5}
// for look, the slider uses different scale
// from what we're actually storing in JSON
valueLabelFormat={(val) => {
if (val < 0) return `${1 + (val * 2) / 100}x`;
return `${(val + 10) / 10}x`;
}}
marks={[
{
value: -25,
label: getLocale('slowest'),
},
{
value: 0,
label: getLocale('normal'),
},
{
value: 40,
label: getLocale('fastest'),
},
]}
min={-25}
max={40}
onChange={(e, value) => {
requestSetPreference('voiceSpeed', convertSliderValToPreferenceVal(value));
}}
/>
</Grid>
</Grid>
</ListItemText>
</ListItem>
);

ListItemSliders.propTypes = {
classes: PropTypes.object.isRequired,
voiceSpeed: PropTypes.number.isRequired,
};

const mapStateToProps = (state) => ({
voiceSpeed: state.preferences.voiceSpeed,
});

export default connectComponent(
ListItemSliders,
mapStateToProps,
null,
styles,
);
8 changes: 6 additions & 2 deletions src/state/pages/home/text-to-speech/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const preloadGoogleTTSAsync = (lang, text, idx, total) => () => {
};

// max length 100 characters
const playGoogleTTSAsync = (lang, text, idx, total) => () => {
const playGoogleTTSAsync = (lang, text, idx, total) => (_, getState) => {
const uri = encodeURI(`https://translate.google.com/translate_tts?ie=UTF-8&q=${text}&tl=${lang}&total=${total}&idx=${idx}&textlen=8&client=dict-chrome-ex&prev=input`);

const opts = {
Expand All @@ -56,6 +56,8 @@ const playGoogleTTSAsync = (lang, text, idx, total) => () => {
const blob = new window.Blob([buffer], { type: 'audio/mp3' });
const url = window.URL.createObjectURL(blob);
player = new window.Audio(url);
const { voiceSpeed } = getState().preferences;
player.playbackRate = voiceSpeed / 10;
return new Promise((resolve, reject) => {
player.play();
player.onended = () => resolve();
Expand All @@ -67,7 +69,7 @@ const playGoogleTTSAsync = (lang, text, idx, total) => () => {
});
};

export const startTextToSpeech = (textToSpeechLang, textToSpeechText) => ((dispatch) => {
export const startTextToSpeech = (textToSpeechLang, textToSpeechText) => ((dispatch, getState) => {
if (textToSpeechText.length < 1) return;

currentTaskId = Date.now();
Expand All @@ -89,6 +91,8 @@ export const startTextToSpeech = (textToSpeechLang, textToSpeechText) => ((dispa

const utterThis = new window.SpeechSynthesisUtterance(textToSpeechText);
utterThis.voice = voice;
const { voiceSpeed } = getState().preferences;
utterThis.rate = voiceSpeed / 10;
utterThis.onend = () => {
dispatch({ type: END_TEXT_TO_SPEECH });
};
Expand Down