Add speech as string prototype

This commit is contained in:
2023-08-30 22:08:23 +03:00
parent 4098ea6e50
commit db2434d4dc
4 changed files with 130 additions and 10 deletions

View File

@ -358,7 +358,7 @@ export class UserAPI {
input: string,
options: InputOptions = {},
id: number | string = ""
) => {
): Player => {
const zid = "z" + id.toString();
const key = id === "" ? this.generateCacheKey(input, options) : zid;
@ -1292,24 +1292,23 @@ export class UserAPI {
// Speech synthesis
// =============================================================
speak = (text: string, index: number, rate: number = 1, pitch: number = 1): void => {
speak = (text: string, voice: number, rate: number = 1, pitch: number = 1): void => {
/*
* Speaks the given text using the browser's speech synthesis API.
* @param text - The text to speak
* @param index - The index of the voice to use
* @param voice - The index of the voice to use
* @param rate - The rate at which to speak the text
* @param pitch - The pitch at which to speak the text
*
*/
const synth = window.speechSynthesis;
synth.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = speechSynthesis.getVoices()[index];
utterance.voice = speechSynthesis.getVoices()[voice];
utterance.rate = rate;
utterance.pitch = pitch;
synth.speak(utterance);
}
say = this.speak;
};
// =============================================================
// Trivial functions

View File

@ -1581,6 +1581,16 @@ mod(0.25) :: sound('sine')
Topos can also speak using [Web Speec API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API)!
Speech synthesis can be used in two ways:
- <icode>speak(text: string, voice: number, rate: number, pitch: number)</icode>: speak the given text.
Or by using string and chaining:
- <icode>"Hello".rate(1.5).pitch(0.5).speak()</icode>.
Examples:
${makeExample(
"Hello world!",
`
@ -1590,13 +1600,36 @@ mod(4) :: speak("Hello world!")
)}
${makeExample(
"Speak with a different voice",
"Different voices",
`
mod(2) :: speak("Topos!",irand(0,25))
`,
false
)}
${makeExample(
"Chaining string",
`
onbeat(1,3) :: "Foobaba".voice(irand(0,10)).speak()
`,
false
)}
${makeExample(
"Building string and chaining",
`
const subject = ["coder","user","loser"].pick()
const verb = ["is", "was", "isnt"].pick()
const object = ["happy","sad","tired"].pick()
const sentence = subject+" "+verb+" "+" "+object
mod(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).say()
`,
false
)}
`;

86
src/StringExtensions.ts Normal file
View File

@ -0,0 +1,86 @@
import { type UserAPI } from "./API";
import { Player } from "./classes/ZPlayer";
export {};
// Extend String prototype
declare global {
interface String {
z(): Player;
speak(): void;
rate(speed: number): string;
pitch(pitch: number): string;
volume(volume: number): string;
voice(voice: number): string;
options(): SpeechOptions;
}
}
const speechOptionsMap = new Map<string, SpeechOptions>();
export const makeStringExtensions = (api: UserAPI) => {
String.prototype.speak = function () {
const options = speechOptionsMap.get(this.valueOf()) || {};
new Speech({ ...options, text: this.valueOf() }).say();
};
String.prototype.rate = function (speed: number) {
const options = speechOptionsMap.get(this.valueOf()) || {};
speechOptionsMap.set(this.valueOf(), { ...options, rate: speed });
return this.valueOf();
};
String.prototype.pitch = function (pitch: number) {
const options = speechOptionsMap.get(this.valueOf()) || {};
speechOptionsMap.set(this.valueOf(), { ...options, pitch: pitch });
return this.valueOf();
};
String.prototype.volume = function (volume: number) {
const options = speechOptionsMap.get(this.valueOf()) || {};
speechOptionsMap.set(this.valueOf(), { ...options, volume: volume });
return this.valueOf();
};
String.prototype.voice = function (voice: number) {
const options = speechOptionsMap.get(this.valueOf()) || {};
speechOptionsMap.set(this.valueOf(), { ...options, voice: voice });
return this.valueOf();
};
String.prototype.options = function (): SpeechOptions {
return speechOptionsMap.get(this.valueOf()) || {};
};
String.prototype.z = function () {
return api.z(this.valueOf());
};
}
type SpeechOptions = {
text?: string;
rate?: number;
pitch?: number;
volume?: number;
voice?: number;
}
class Speech {
constructor(
public options: SpeechOptions
) {}
say = () => {
if (this.options.text) {
const synth = window.speechSynthesis;
synth.cancel();
const utterance = new SpeechSynthesisUtterance(this.options.text);
utterance.rate = this.options.rate || 1;
utterance.pitch = this.options.pitch || 1;
utterance.volume = this.options.volume || 1;
if (this.options.voice) {
utterance.voice = synth.getVoices()[this.options.voice];
}
synth.speak(utterance);
}
}
}

View File

@ -30,6 +30,7 @@ import { gzipSync, decompressSync, strFromU8 } from "fflate";
import showdown from "showdown";
showdown.setFlavor("github");
import showdownHighlight from "showdown-highlight";
import { makeStringExtensions } from "./StringExtensions";
const classMap = {
h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
@ -219,6 +220,7 @@ export class Editor {
this.api = new UserAPI(this);
makeArrayExtensions(this.api);
makeStringExtensions(this.api);
// ================================================================================
// CodeMirror Management