Skip to content

Commit

Permalink
Implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Valentin1918 committed Apr 11, 2024
1 parent bf5ddfd commit c79596c
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 35 deletions.
8 changes: 2 additions & 6 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
.currently-reading {

}

.currently-reading .currently-reading-text {
.currently-reading .current-sentence {
font-size: 24px;
}

.currently-reading .currently-reading-text .currentword {
.currently-reading .current-sentence .current-word {
font-size: 24px;
color: red;
}
40 changes: 34 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
import './App.css';

import {useEffect, useState} from "react";
import { Controls } from './components/Controls';
import { CurrentlyReading } from './components/CurrentlyReading';
import {fetchSentences} from "./api/fetchContent";
import useSpeech from "./lib/useSpeech";


function App() {
// const [sentences, setSentences] = useState<Array<string>>([]);
// const { currentWord, currentSentence, controls } = useSpeech(sentences);
const [sentences, setSentences] = useState<Array<string>>([]);
const {
currentSentenceIdx,
currentSentenceWordIdx,
playbackState,
play,
pause,
} = useSpeech(sentences);

const loadNewContent = () => {
fetchSentences().then((sentences) => {
setSentences(sentences);
});
}

useEffect(() => {
loadNewContent();
}, []);

return (
<div className="App">
<h1>Text to speech</h1>
<div>
<CurrentlyReading/>
<CurrentlyReading
currentSentenceIdx={currentSentenceIdx}
currentSentenceWordIdx={currentSentenceWordIdx}
sentences={sentences}
playbackState={playbackState}
/>
</div>
<div>
<Controls/>
<Controls
play={play}
pause={pause}
playbackState={playbackState}
loadNewContent={loadNewContent}
/>
</div>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions src/api/fetchContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {parseContent} from "../utils/parseContent";

export const fetchSentences = async () => {
const apiHost = location.href.replace(location.port, '5174');
const reply= await fetch(`${apiHost}content`);
const {content} = await reply.json();
return parseContent(content);
}
23 changes: 21 additions & 2 deletions src/components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,31 @@ import { PlayingState } from '../lib/speech';
export const Controls = ({
play,
pause,
stop,
loadNewContent,
playbackState,
}: {
play: () => void;
pause: () => void;
stop: () => void;
loadNewContent: () => void;
state: PlayingState;
playbackState: PlayingState;
}) => {
return <div></div>;
return (
<div>
{(playbackState === 'playing') ? (
<button onClick={pause}>
Pause
</button>
) : (
<button onClick={play}>
Play
</button>
)}

<button onClick={loadNewContent}>
Load new content
</button>
</div>
);
};
33 changes: 30 additions & 3 deletions src/components/CurrentlyReading.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {PlayingState} from "../lib/speech";
import '../App.css';

/**
* Implement the CurrentlyReading component here
* This component should have the following,
Expand All @@ -8,13 +11,37 @@
* See example.gif for an example of how the component should look like, feel free to style it however you want as long as the testID exists
*/
export const CurrentlyReading = ({
currentWordRange,
currentSentenceIdx,
currentSentenceWordIdx,
sentences,
playbackState,
}: {
currentWordRange: [number, number];
currentSentenceIdx: number;
currentSentenceWordIdx: number;
sentences: string[];
playbackState: PlayingState;
}) => {
return <div data-testid="currently-reading"></div>;
if (!sentences.length) return null;

return (
<div data-testid="currently-reading" className={playbackState === 'playing' ? 'currently-reading' : ''}>
{sentences.map((sentence, sentenceIndex) => (
<p
data-testid={sentenceIndex === currentSentenceIdx ? 'current-sentence' : ''}
className={sentenceIndex === currentSentenceIdx ? 'current-sentence' : ''}
key={`sentence-${sentenceIndex}`}
>
{sentence.split(' ').map((word, wordIndex) => (
<span
data-testid={wordIndex === currentSentenceWordIdx ? 'current-word' : ''}
className={wordIndex === currentSentenceWordIdx ? 'current-word' : ''}
key={`sentence-${sentenceIndex}_word${wordIndex}`}
>
{`${word} `}
</span>
))}
</p>
))}
</div>
);
};
26 changes: 20 additions & 6 deletions src/lib/speech.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type PlayingState = "initialized" | "playing" | "paused" | "ended";

export type SpeechEngineState = {
utterance: SpeechSynthesisUtterance | null;
playbackState: PlayingState;
config: {
rate: number;
volume: number;
Expand All @@ -25,13 +26,19 @@ export type SpeechEngine = ReturnType<typeof createSpeechEngine>;
const createSpeechEngine = (options: SpeechEngineOptions) => {
const state: SpeechEngineState = {
utterance: null,
playbackState: 'ended',
config: {
rate: 1,
volume: 1,
voice: window.speechSynthesis.getVoices()[0],
},
};

const onStateUpdate = (playbackState: PlayingState) => {
options.onStateUpdate(playbackState);
state.playbackState = playbackState;
}

window.speechSynthesis.onvoiceschanged = (e) => {
state.config.voice = speechSynthesis.getVoices()[0];
};
Expand All @@ -44,7 +51,7 @@ const createSpeechEngine = (options: SpeechEngineOptions) => {
// set up listeners
utterance.onboundary = (e) => options.onBoundary(e);
utterance.onend = (e) => {
options.onStateUpdate("ended");
onStateUpdate("ended");
options.onEnd(e);
};

Expand All @@ -56,18 +63,25 @@ const createSpeechEngine = (options: SpeechEngineOptions) => {
if (!state.utterance) throw new Error("No active utterance found to play");
state.utterance.onstart = () => {
console.log('waiting for onstart')
options.onStateUpdate("playing");
onStateUpdate("playing");
};
window.speechSynthesis.cancel();
window.speechSynthesis.speak(state.utterance);

if (state.playbackState === 'paused') {
onStateUpdate('playing');
window.speechSynthesis.resume();
} else {
window.speechSynthesis.cancel();
window.speechSynthesis.speak(state.utterance);
}
};

const pause = () => {
options.onStateUpdate("paused");
onStateUpdate("paused");
window.speechSynthesis.pause();
};

const cancel = () => {
options.onStateUpdate("initialized");
onStateUpdate("initialized");
window.speechSynthesis.cancel();
};

Expand Down
48 changes: 36 additions & 12 deletions src/lib/useSpeech.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
import { useState } from 'react';

import { PlayingState } from './speech';
import {useEffect, useState} from 'react';
import { PlayingState, createSpeechEngine } from './speech';
import {makeRanges} from "../utils/makeRanges";

/*
@description
Implement a custom useSpeech hook that uses a speech engine defined in 'speech.ts'
to play the sentences that have been fetched and parsed previously.
This hook should return react friendly controls for playing, and pausing audio as well as provide information about
the currently read word and sentence
*/

const useSpeech = (sentences: Array<string>) => {
const [currentSentenceIdx, setCurrentSentenceIdx] = useState(0);
const [currentWordRange, setCurrentWordRange] = useState([0, 0]);
const [currentSentenceWordIdx, setCurrentSentenceWordIdx] = useState(0);
const [controls, setControls] = useState<{ play: () => void; pause: () => void; }>();
const [playbackState, setPlaybackState] = useState<PlayingState>('paused');
const sentencesRanges = makeRanges(sentences);

useEffect(() => {

const {
play,
pause,
load
} = createSpeechEngine({
onBoundary: ({ charIndex }) => {
const sentenceIndex = sentencesRanges.findIndex(({ from, to }) => charIndex >= from && charIndex <= to);
setCurrentSentenceIdx(sentenceIndex);

const charactersLeft = sentencesRanges[sentenceIndex].from;
const wordsRanges = makeRanges(sentences[sentenceIndex].split(' '));
const charIndexInWord = charIndex - charactersLeft;
const wordIndex = wordsRanges.findIndex(({ from, to }) => charIndexInWord >= from && charIndexInWord <= to)
setCurrentSentenceWordIdx(wordIndex);
},
onEnd: () => {},
onStateUpdate: (state: PlayingState) => setPlaybackState(state),
})

const [playbackState, setPlaybackState] = useState<PlayingState>("paused");
setControls({ play, pause });
load(sentences.join(' '));

const play = () => {};
const pause = () => {};
}, [sentences]);

return {
currentSentenceIdx,
currentWordRange,
currentSentenceWordIdx,
playbackState,
play,
pause,
...controls,
};
};

export { useSpeech };
export default useSpeech;
20 changes: 20 additions & 0 deletions src/utils/makeRanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const makeRanges = (sentences: Array<string>) =>
sentences.reduce((acc: Array<{ from: number, to: number }>, v: string, i) => {

let content = {
from: 0,
to: v.length,
}

if (i) {
const spaceLength = 1;
const from = acc[i - 1].to + spaceLength;
content = {
from,
to: from + v.length
}
}

acc.push(content);
return acc;
}, []);
10 changes: 10 additions & 0 deletions src/utils/parseContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const parseContent = (content: string) =>
content.replace('</speak>', '').replace('<speak>', '').split('</s>').reduce((acc: Array<string>, v: string) => {
if (v.match('^<s>[\\s\\S]+')) {
acc.push(v.replace('<s>', ''));
} else if (v.includes('<s>')) {
const placement = v.indexOf('<s>');
acc.push(v.slice(placement).replace('<s>', ''));
}
return acc;
}, []);

0 comments on commit c79596c

Please sign in to comment.