Add first pass of TTS read of steps in cook mode

This commit is contained in:
Daniel O'Connor 2025-02-01 04:13:35 +00:00
commit 6c370f15c8
2 changed files with 97 additions and 1 deletions

View file

@ -0,0 +1,93 @@
<template>
<span class="v-btn__content">
<i v-if="!playing.value" aria-hidden="true" class="v-icon notranslate mdi mdi-play theme--dark" @click.stop="play"></i>
<i v-if="playing.value" aria-hidden="true" class="v-icon notranslate mdi mdi-pause theme--dark" @click.stop="pause"></i>
</span>
</template>
<script lang="ts">
import { defineComponent, onUnmounted } from "@nuxtjs/composition-api";
import { RecipeStep } from "~/lib/api/types/recipe";
import { NoUndefinedField } from "~/lib/api/types/non-generated";
export default defineComponent({
props: {
step: {
type: Object as () => NoUndefinedField<RecipeStep>,
required: true
}
},
setup() {
onUnmounted(() => {
speechSynthesis.stop();
});
},
data() {
return {
playing: false,
played: false,
utterance: null,
currentIndex: {
type: Number,
default: 0
}
}
},
methods: {
play() {
const self = this;
if (this.utterance && this.playing) {
speechSynthesis.cancel();
}
this.utterance = new SpeechSynthesisUtterance(this.step.text);
this.utterance.onstart = (event) => {
self.playing = true;
// self.$emit("playing", true); // TODO: Consider if this event should bubble or a proxy of it should.
console.debug("Now playing: " + this.step.text);
};
this.utterance.onend = () => {
self.playing = false;
// self.$emit("playing", false);
console.debug("Playback complete");
};
this.utterance.onpause = () => {
self.playing = false;
};
this.utterance.onresume = () => {
self.playing = true;
};
this.utterance.onboundary = (event) => {
// Update the start of the current sentence.
if (event.name === "sentence") {
self.currentIndex = event.charIndex;
}
};
// TODO: Evaluate the usefulness of this event.
// this.utterance.onmark = (event) => {
// console.log("mark")
// console.log(event)
// };
this.utterance.onerror = (event) => {
console.error("Error in playback")
console.debug(event)
};
speechSynthesis.speak(this.utterance);
return true;
},
pause() {
console.log("Stop playing");
speechSynthesis.pause();
}
}
});
</script>
<style lang="css" scoped>
</style>

View file

@ -266,6 +266,7 @@
<v-divider v-if="isCookMode && step.ingredientReferences && step.ingredientReferences.length > 0 && $vuetify.breakpoint.smAndUp" vertical ></v-divider> <v-divider v-if="isCookMode && step.ingredientReferences && step.ingredientReferences.length > 0 && $vuetify.breakpoint.smAndUp" vertical ></v-divider>
<v-col> <v-col>
<SafeMarkdown class="markdown" :source="step.text" /> <SafeMarkdown class="markdown" :source="step.text" />
<RecipePageInstructionPlayer v-if="isCookMode" :step="step" />
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
@ -303,6 +304,7 @@ import { useExtractIngredientReferences } from "~/composables/recipe-page/use-ex
import { NoUndefinedField } from "~/lib/api/types/non-generated"; import { NoUndefinedField } from "~/lib/api/types/non-generated";
import DropZone from "~/components/global/DropZone.vue"; import DropZone from "~/components/global/DropZone.vue";
import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue"; import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue";
import RecipePageInstructionPlayer from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInstructionPlayer.vue";
interface MergerHistory { interface MergerHistory {
target: number; target: number;
source: number; source: number;
@ -315,7 +317,8 @@ export default defineComponent({
draggable, draggable,
RecipeIngredientHtml, RecipeIngredientHtml,
DropZone, DropZone,
RecipeIngredients RecipeIngredients,
RecipePageInstructionPlayer
}, },
props: { props: {
value: { value: {