diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f66c74 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.zip \ No newline at end of file diff --git a/constants/constants.js b/constants/constants.js index 6b1bd9e..1a68409 100644 --- a/constants/constants.js +++ b/constants/constants.js @@ -7,6 +7,7 @@ const CONTENT_COMMANDS = { CHANGE_PLAYBACK_RATE: 'change_playback_rate', GET_PLAYBACK_RATE: 'get_playback_rate', UPDATE_VALUE: 'update_value', + PLAY_SSML: 'play_ssml', }; const BACKGROUND_COMMANDS = { @@ -29,6 +30,7 @@ const WEBRICE_KEYS = { SUBSTITUTIONS: 'webrice_substitutions', VOLUME: 'webrice_volume', AWS_CREDS: 'webrice_aws_creds', + SSML_TEXT: 'webrice_ssml_text', }; const BACKENDS = { diff --git a/content/content.js b/content/content.js index d3cbb5b..230c12e 100644 --- a/content/content.js +++ b/content/content.js @@ -19,6 +19,7 @@ const settings = { pitch_default: true, subs: [], text: '', + ssml: '', }; // Sends messages to the background script @@ -60,15 +61,15 @@ const getText = () => { * Start playing audio. If needed setup and requesting of new audio is done. * @returns SUCCESS or an error message */ -const play = async () => { - const text = getText(); +const play = async ({ ssml = false } = {}) => { + const text = ssml ? settings.ssml : getText(); if (player.sameTextAndVoice(text, settings.voice)) { player.setPlaybackRate(settings.playbackRate); player.play(); return 'SUCCESS'; } - const result = getRequestHeaderAndContent(text, settings); + const result = getRequestHeaderAndContent(text, settings, ssml); if (result.requests.length == 0) { return 'Unable to formulate tts requests.'; @@ -168,6 +169,9 @@ const updateSetting = (setting, value) => { settings.volume = value; player.setVolume(value); break; + case WEBRICE_KEYS.SSML_TEXT: + settings.ssml = value; + break; default: break; } @@ -202,6 +206,8 @@ const commandHandler = async (message) => { const { setting, value } = message.settings; updateSetting(setting, value); break; + case CONTENT_COMMANDS.PLAY_SSML: + return await play({ ssml: false }); default: console.log(`WebRice extension: Unknown command -> ${message.command}`); break; diff --git a/content/fetch.js b/content/fetch.js index 82f7ef6..0a89d4c 100644 --- a/content/fetch.js +++ b/content/fetch.js @@ -53,14 +53,26 @@ const normalizeText = (text, specialTrim = false) => { * Normalizes the text and outputs an array of tts requests * @param {string} text text to normalize for tts * @param {object} settings eventual settings that might be used to change voice + * @param {boolean} ssml default false, turns on SSML settings for the request * @returns an array of requests, {url, content} */ -const getRequestHeaderAndContent = (text, settings) => { +const getRequestHeaderAndContent = (text, settings, ssml = false) => { const audioType = 'mp3'; const voiceName = settings?.voice ? settings.voice : DEFAULT_VOICE; + const awsVoice = AWS_VOICES.includes(settings?.voice); + + if (ssml) { + const ssml = `${text}`; + const request = awsVoice + ? awsRequest(ssml, audioType, voiceName, true) + : tiroRequest(ssml, audioType, voiceName, true); + return { + backend: awsVoice ? BACKENDS.POLLY : BACKENDS.TIRO, + requests: [request], + }; + } const specialTrim = SPECIAL_VOICES.includes(voiceName); const normalizedTexts = normalizeText(text, specialTrim); - const awsVoice = AWS_VOICES.includes(settings?.voice); const requests = normalizedTexts.map((text) => { const request = awsVoice @@ -72,7 +84,15 @@ const getRequestHeaderAndContent = (text, settings) => { return { backend: awsVoice ? BACKENDS.POLLY : BACKENDS.TIRO, requests }; }; -const tiroRequest = (text, audioType, voiceName) => { +/** + * Creates the Tiro tts request. + * @param {string} text The text that should be converted to TTS + * @param {string} audioType The wanted audio output type + * @param {string} voiceName The voice used for TTS + * @param {boolean} ssml If the request handles SSML or not + * @returns + */ +const tiroRequest = (text, audioType, voiceName, ssml = false) => { const url = 'https://tts.tiro.is/v0/speech'; return { @@ -92,13 +112,21 @@ const tiroRequest = (text, audioType, voiceName) => { SampleRate: '16000', SpeechMarkTypes: [], Text: text, - TextType: 'text', + TextType: ssml ? 'ssml' : 'text', VoiceId: voiceName, }), }, }; }; -const awsRequest = (text, audioType, voiceName) => { - return pollyParams(text, audioType, voiceName); +/** + * Creates the AWS polly request. + * @param {string} text The text that should be converted to TTS + * @param {string} audioType The wanted audio output type + * @param {string} voiceName The voice used for TTS + * @param {boolean} ssml If the request handles SSML or not + * @returns + */ +const awsRequest = (text, audioType, voiceName, ssml = false) => { + return pollyParams(text, audioType, voiceName, ssml); }; diff --git a/manifest.json b/manifest.json index 3b55212..412b122 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WebRICE", - "version": "1.0.3", + "version": "1.0.4", "description": "Text to speech service for icelandic", "homepage_url": "https://www.webrice.is", diff --git a/popup/popup.css b/popup/popup.css index e3d4606..d5aef01 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -260,6 +260,11 @@ details > summary { margin-bottom: 0.25rem; } +.small_margins { + margin-bottom: 0.25rem; + margin-top: 0.25rem; +} + .verification_item { border-radius: 1rem; background: white; diff --git a/popup/popup.html b/popup/popup.html index 0c34c16..7ae1973 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -171,6 +171,41 @@

Hljóðstyrkur

/> +
+
+ SSML +
+

+ Speech Synthesis Markup Language (SSML) gerir kleift að sérsníða + Text-To-Speech. +

+

Upplýsingar

+

+ Sjálfgefið bakendakerfi er veitt af Tiro og studd SSML merki og + dæmi má finna + hér. +

+

+ Fyrir Amazon Polly raddir (Karl, Dora) eru viðbótar SSML merki + studd. Dæmi og studd merki má finna + hér. +

+

+ Sláðu inn SSML í textareitinn fyrir neðan og smelltu á + spilunarhnappinn ofan til að prófa. +

+ <speak> + + </speak> +

+
+
+
TTS bakgrunnskerfi diff --git a/popup/popup.js b/popup/popup.js index def8278..7c68396 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -28,6 +28,9 @@ const resetAWSCredsButton = document.getElementById('aws_reset_button'); const resetAWSCredsDiv = document.getElementById('aws_reset'); const doraRadio = document.getElementById('dora_aws'); const karlRadio = document.getElementById('karl_aws'); +const helpAWS = document.getElementById('help_aws'); +const SSMLText = document.getElementById('webrice_ssml_text'); +const SSMLdetails = document.getElementById('ssml_details'); loadingIcon.style.display = 'none'; // start by hiding loading icon @@ -101,7 +104,12 @@ const toggleLoad = () => { */ const onPlayClicked = async () => { toggleLoad(); - const result = await sendToContent('play clicked', CONTENT_COMMANDS.PLAY); + // If SSML section is open and the more container is visible => request TTS with SSML + const command = + SSMLdetails.open && !isHidden(moreContainer) + ? CONTENT_COMMANDS.PLAY_SSML + : CONTENT_COMMANDS.PLAY; + const result = await sendToContent('play clicked', command); toggleLoad(); return result; }; @@ -168,6 +176,11 @@ const onRadioClicked = (e) => { updateValue(WEBRICE_KEYS.VOICE, voice); }; +/** + * Updates a value both in storage and in content + * @param {string} key The key of the value to update + * @param {any} value The value that should be stored. + */ const updateValue = (key, value) => { saveToStorage(key, value); updateContentValue(key, value); @@ -190,10 +203,22 @@ const onPitchDefaultChanged = (e) => { pitchSliderDiv.classList.remove('webrice_disabled'); }; +/** + * Updates values when volume slider changes + * @param {event} e + */ const onVolumeSliderChanged = (e) => { updateValue(WEBRICE_KEYS.VOLUME, e.target.valueAsNumber); }; +/** + * Keeps the SSML values updated in storage and content + * @param {event} e + */ +const onSSMLTextChanged = (e) => { + updateValue(WEBRICE_KEYS.SSML_TEXT, e.target.value); +}; + /**HTMLElement * On the AWS form submit update the stored values * @param {Event} e @@ -225,16 +250,27 @@ const onAWSFormSubmit = async (e) => { saveToStorage(WEBRICE_KEYS.AWS_CREDS, awsCreds); }; +/** + * Hides AWS credential input fields + */ const hideAWSCreds = () => { hide(awsForm); show(resetAWSCredsDiv); }; +/** + * Displays the AWS Form and hides the rest button + */ const onResetAWS = () => { show(awsForm); hide(resetAWSCredsDiv); }; +/** + * Used to initialize values from storage into the popup form + * @param {string} key + * @param {any} value + */ const initialize = (key, value) => { switch (key) { case WEBRICE_KEYS.PITCH: @@ -271,11 +307,17 @@ const initialize = (key, value) => { hideAWSCreds(); show(doraRadio); show(karlRadio); + show(helpAWS); break; } break; case WEBRICE_KEYS.VOICE: updateContentValue(key, value); + break; + case WEBRICE_KEYS.SSML_TEXT: + SSMLText.value = value; + updateContentValue(key, value); + break; default: break; } @@ -301,6 +343,7 @@ pitchSlider.onchange = onPitchSliderChanged; volumeSlider.oninput = onVolumeSliderChanged; awsForm.onsubmit = onAWSFormSubmit; resetAWSCredsButton.onclick = onResetAWS; +SSMLText.onkeyup = onSSMLTextChanged; let initialVoice = await getFromStorage(WEBRICE_KEYS.VOICE); if (!initialVoice) { diff --git a/utils/aws-helper.js b/utils/aws-helper.js index 12fc608..1f8a7e6 100644 --- a/utils/aws-helper.js +++ b/utils/aws-helper.js @@ -12,10 +12,11 @@ const testAws = async (region, accessKeyId, secretAccessKey) => { } }; -const pollyParams = (text, audioType, voice) => { +const pollyParams = (text, audioType, voice, ssml = false) => { params = { Engine: 'standard', Text: text, + TextType: ssml ? 'ssml' : 'text', OutputFormat: audioType, VoiceId: voice, };