feat: 添加音频
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"useTranslateType": "custom",
|
||||
"id": "HmlGhw",
|
||||
"audioSrc": "/public/sound/article/nce2-1/1.mp3",
|
||||
"lrcPosition": [[14.6,19.15],[19.15,22.03],[22.03,24.59],[24.59,27.26],[27.26,31.65],[31.65,34.43],[34.43,36.98],[36.98,40.36],[40.7,42.47],[40.36,46.59],[46.59,50.65],[50.65,54.57],[55.03,56.84],[57.17,63],[62.98,68.85],[68.85,-1]]
|
||||
"lrcPosition": [[15.6,19.11],[19.15,22.03],[22.03,24.59],[24.59,27.26],[27.26,31.65],[31.65,34.43],[34.43,36.98],[36.98,40.36],[40.7,42.47],[42.47,46.59],[46.59,50.65],[50.65,54.57],[55.03,56.84],[57.17,63],[62.98,68.85],[68.85,72.54]]
|
||||
},
|
||||
{
|
||||
"title": "Breakfast or lunch?",
|
||||
@@ -22,7 +22,7 @@
|
||||
"useTranslateType": "custom",
|
||||
"newWords": [],
|
||||
"audioSrc": "/public/sound/article/nce2-1/2.mp3",
|
||||
"lrcPosition": [],
|
||||
"lrcPosition": [[15.9,17.48],[17.75,21.51],[21.54,26.13],[26.58,30.47],[30.86,33.34],[33.34,35.68],[35.68,39.41],[39.41,45.64],[45.64,48.45],[48.45,53.01],[53.01,55.3],[55.3,60.11],[60.11,63.68],[63.4,67.15],[67.3,70.19],[69.98,75.54]],
|
||||
"id": "1ao0Qx"
|
||||
},
|
||||
{
|
||||
@@ -34,6 +34,8 @@
|
||||
"textCustomTranslateIsFormat": true,
|
||||
"useTranslateType": "custom",
|
||||
"newWords": [],
|
||||
"audioSrc": "/public/sound/article/nce2-1/3.mp3",
|
||||
"lrcPosition": [[16.07,19.99],[19.99,24.13],[24.13,28.69],[28.53,33.01],[33.01,35.69],[35.77,41.15],[41.67,45.43],[45.48,51.95],[52.55,57.01],[57.01,62.48],[62.48,66.62],[66.32,-1]],
|
||||
"id": "x1VwGU"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import {Article, ArticleWord, DefaultArticleWord, DictType, Sentence, TranslateT
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import nlp from "compromise/one";
|
||||
import {split} from "sentence-splitter";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
|
||||
interface KeyboardMap {
|
||||
Period: string,
|
||||
@@ -242,7 +243,7 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
|
||||
|
||||
// console.log(sections)
|
||||
|
||||
text= sections.map(v => v.map(s => s.text.trim()).join('\n')).join('\n\n');
|
||||
text = sections.map(v => v.map(s => s.text.trim()).join('\n')).join('\n\n');
|
||||
// console.log('s',text)
|
||||
return {
|
||||
//s.text.trim()的trim()不能去掉,因为这个方法会重复执行,要保证句子后面只有一个\n,不trim() \n就会累加
|
||||
@@ -321,4 +322,33 @@ export function getTranslateText(article: Article) {
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function usePlaySentenceAudio() {
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
let timer = $ref(0)
|
||||
|
||||
function playSentenceAudio(sentence: Sentence, ref?: HTMLAudioElement, article?: Article) {
|
||||
if (sentence.audioPosition?.length && article.audioSrc && ref) {
|
||||
clearTimeout(timer)
|
||||
if (ref.played) {
|
||||
ref.pause()
|
||||
}
|
||||
let start = sentence.audioPosition[0];
|
||||
ref.currentTime = start
|
||||
ref.play()
|
||||
let end = sentence.audioPosition?.[1]
|
||||
if (end && end !== -1) {
|
||||
timer = setTimeout(() => {
|
||||
ref.pause()
|
||||
}, (end - start) * 1000)
|
||||
}
|
||||
} else {
|
||||
playWordAudio(sentence.text)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
playSentenceAudio
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {getSplitTranslateText} from "@/hooks/article.ts";
|
||||
import {cloneDeep, last} from "lodash-es";
|
||||
import {watch, ref} from "vue";
|
||||
import {watch, ref, nextTick} from "vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {UploadProps, UploadUserFile} from "element-plus";
|
||||
import {_copy, _parseLRC} from "@/utils";
|
||||
import {_copy, _nextTick, _parseLRC} from "@/utils";
|
||||
import * as Comparison from "string-comparison"
|
||||
import audio from '/public/sound/article/nce2-1/1.mp3'
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
@@ -274,6 +274,7 @@ const handleChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
|
||||
|
||||
let currentSentence = $ref<Sentence>({} as any)
|
||||
let editSentence = $ref<Sentence>({} as any)
|
||||
let preSentence = $ref<Sentence>({} as any)
|
||||
let showEditAudioDialog = $ref(false)
|
||||
let sentenceAudioRef = $ref<HTMLAudioElement>()
|
||||
let audioRef = $ref<HTMLAudioElement>()
|
||||
@@ -282,20 +283,23 @@ function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
|
||||
showEditAudioDialog = true
|
||||
currentSentence = val
|
||||
editSentence = cloneDeep(val)
|
||||
let pre = null
|
||||
preSentence = null
|
||||
if (j == 0) {
|
||||
if (i != 0) {
|
||||
pre = last(editArticle.sections[i - 1])
|
||||
preSentence = last(editArticle.sections[i - 1])
|
||||
}
|
||||
} else {
|
||||
pre = editArticle.sections[i][j - 1]
|
||||
preSentence = editArticle.sections[i][j - 1]
|
||||
}
|
||||
if (!editSentence.audioPosition?.length) {
|
||||
editSentence.audioPosition = [0, 0]
|
||||
if (pre) {
|
||||
editSentence.audioPosition = [pre.audioPosition[1] ?? 0, 0]
|
||||
if (preSentence) {
|
||||
editSentence.audioPosition = [preSentence.audioPosition[1] ?? 0, 0]
|
||||
}
|
||||
}
|
||||
_nextTick(() => {
|
||||
sentenceAudioRef.currentTime = editSentence.audioPosition[0]
|
||||
})
|
||||
}
|
||||
|
||||
function recordStart() {
|
||||
@@ -337,6 +341,16 @@ function saveLrcPosition() {
|
||||
currentSentence.audioPosition = cloneDeep(editSentence.audioPosition)
|
||||
editArticle.lrcPosition = editArticle.sections.map((v, i) => v.map((w, j) => (w.audioPosition ?? []))).flat()
|
||||
}
|
||||
|
||||
function jumpAudio(time: number) {
|
||||
sentenceAudioRef.currentTime = time
|
||||
}
|
||||
|
||||
function setPreEndTimeToCurrentStartTime() {
|
||||
if (preSentence) {
|
||||
editSentence.audioPosition[0] = preSentence.audioPosition[1]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -517,24 +531,44 @@ function saveLrcPosition() {
|
||||
@ok="saveLrcPosition"
|
||||
>
|
||||
<div class="p-4 pt-0 color-black w-150 flex flex-col gap-2">
|
||||
<div>
|
||||
句子:{{ editSentence.text }}
|
||||
<div class="">
|
||||
教程:点击音频播放按钮,当播放到句子开始时,点击开始时间的 <span class="color-red">记录</span>
|
||||
按钮;当播放到句子结束时,点击结束时间的 <span class="color-red">记录</span> 按钮,最后再试听是否正确
|
||||
</div>
|
||||
<audio ref="sentenceAudioRef" :src="editArticle.audioSrc" controls class="w-full"></audio>
|
||||
<div class="">
|
||||
使用方法:点击上方播放按钮,当音频播放到当前句子开始时,点击开始时间的 <span class="color-red">记录</span>
|
||||
按钮;当播放到句子结束时,点击结束时间的 <span class="color-red">记录</span> 按钮
|
||||
<div class="flex items-center gap-2 space-between mb-2" v-if="editSentence.audioPosition?.length">
|
||||
<div>{{ editSentence.text }}</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<div>
|
||||
<span>{{ editSentence.audioPosition?.[0] }}s</span>
|
||||
<span v-if="editSentence.audioPosition?.[1] !== -1"> - {{ editSentence.audioPosition?.[1] }}s</span>
|
||||
<span v-else> - 结束</span>
|
||||
</div>
|
||||
<BaseIcon icon="hugeicons:play"
|
||||
title="试听"
|
||||
@click="playSentenceAudio(editSentence,sentenceAudioRef)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>开始时间:</div>
|
||||
<div class="flex space-between flex-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input-number v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1">
|
||||
<template #suffix>
|
||||
<span>s</span>
|
||||
</template>
|
||||
</el-input-number>
|
||||
<BaseIcon
|
||||
@click="jumpAudio(editSentence.audioPosition[0])"
|
||||
title="跳转"
|
||||
icon="ic:sharp-my-location"
|
||||
/>
|
||||
<BaseIcon
|
||||
@click="setPreEndTimeToCurrentStartTime"
|
||||
title="使用前一句的结束时间"
|
||||
icon="twemoji:end-arrow"
|
||||
/>
|
||||
</div>
|
||||
<BaseButton @click="recordStart">记录</BaseButton>
|
||||
</div>
|
||||
@@ -542,7 +576,7 @@ function saveLrcPosition() {
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>结束时间:</div>
|
||||
<div class="flex space-between flex-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input-number v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1">
|
||||
<template #suffix>
|
||||
<span>s</span>
|
||||
@@ -555,14 +589,6 @@ function saveLrcPosition() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2" v-if="editSentence.audioPosition?.length">
|
||||
<div>
|
||||
lrc:<span>{{ editSentence.audioPosition?.[0] }}s</span>
|
||||
<span v-if="editSentence.audioPosition?.[1] !== -1"> - {{ editSentence.audioPosition?.[1] }}s</span>
|
||||
<span v-else> - 结束</span>
|
||||
</div>
|
||||
<BaseButton @click="playSentenceAudio(editSentence,audioRef)">试听</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onMounted, onUnmounted, watch} from "vue"
|
||||
import {Article, ArticleWord, DefaultArticle, Word} from "@/types.ts";
|
||||
import {Article, ArticleWord, DefaultArticle, Sentence, Word} from "@/types.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
@@ -36,6 +36,7 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
const emit = defineEmits<{
|
||||
ignore: [],
|
||||
wrong: [val: Word],
|
||||
play: [val: Sentence],
|
||||
nextWord: [val: ArticleWord],
|
||||
over: [],
|
||||
edit: [val: Article]
|
||||
@@ -174,9 +175,11 @@ function nextSentence() {
|
||||
console.log('打完了')
|
||||
isEnd = true
|
||||
emit('over')
|
||||
} else {
|
||||
emit('play', props.article.sections[sectionIndex][0])
|
||||
}
|
||||
} else {
|
||||
playWordAudio(currentSection[sentenceIndex].text)
|
||||
emit('play', currentSection[sentenceIndex])
|
||||
}
|
||||
lockNextSentence = false
|
||||
}
|
||||
@@ -217,6 +220,9 @@ function onTyping(e: KeyboardEvent) {
|
||||
}
|
||||
playKeyboardAudio()
|
||||
} else {
|
||||
if (sectionIndex === 0 && sentenceIndex === 0 && wordIndex === 0 && stringIndex === 0) {
|
||||
emit('play', currentSection[sentenceIndex])
|
||||
}
|
||||
let letter = e.key
|
||||
|
||||
let key = currentWord.word[stringIndex]
|
||||
@@ -261,20 +267,7 @@ function onTyping(e: KeyboardEvent) {
|
||||
|
||||
function play() {
|
||||
let currentSection = props.article.sections[sectionIndex]
|
||||
|
||||
return playWordAudio(currentSection[sentenceIndex].text)
|
||||
if (isPlay) {
|
||||
isPlay = false
|
||||
return window.speechSynthesis.pause();
|
||||
}
|
||||
let msg = new SpeechSynthesisUtterance();
|
||||
msg.text = 'article1'
|
||||
msg.rate = 0.5;
|
||||
msg.pitch = 1;
|
||||
msg.lang = 'en-US';
|
||||
// msg.lang = 'zh-HK';
|
||||
isPlay = true
|
||||
window.speechSynthesis.speak(msg);
|
||||
emit('play', currentSection[sentenceIndex])
|
||||
}
|
||||
|
||||
function del() {
|
||||
@@ -285,10 +278,6 @@ function del() {
|
||||
}
|
||||
}
|
||||
|
||||
function playWord(word: ArticleWord) {
|
||||
playWordAudio(word.word)
|
||||
}
|
||||
|
||||
function indexWord(word: ArticleWord) {
|
||||
return word.word.slice(input.length, input.length + 1)
|
||||
}
|
||||
@@ -305,7 +294,7 @@ function hideSentence() {
|
||||
hoverIndex = {sectionIndex: -1, sentenceIndex: -1}
|
||||
}
|
||||
|
||||
function onContextMenu(e: MouseEvent, text: string, i, j) {
|
||||
function onContextMenu(e: MouseEvent, sentence: Sentence, i, j) {
|
||||
//prevent the browser's default menu
|
||||
e.preventDefault();
|
||||
//show your menu
|
||||
@@ -318,19 +307,19 @@ function onContextMenu(e: MouseEvent, text: string, i, j) {
|
||||
onClick: () => {
|
||||
sectionIndex = i
|
||||
sentenceIndex = j
|
||||
playWordAudio(text)
|
||||
emit('play', sentence)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "播放",
|
||||
onClick: () => {
|
||||
playWordAudio(text)
|
||||
emit('play', sentence)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "复制",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(text).then(r => {
|
||||
navigator.clipboard.writeText(sentence.text).then(r => {
|
||||
$toast.success('已复制!', {position: 'top'});
|
||||
})
|
||||
}
|
||||
@@ -338,7 +327,7 @@ function onContextMenu(e: MouseEvent, text: string, i, j) {
|
||||
{
|
||||
label: "语法分析",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(text).then(r => {
|
||||
navigator.clipboard.writeText(sentence.text).then(r => {
|
||||
$toast.success('已复制!随后将打开语法分析网站!', {
|
||||
position: 'top',
|
||||
duration: 3000,
|
||||
@@ -388,7 +377,7 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
|
||||
hoverIndex.sectionIndex === indexI && hoverIndex.sentenceIndex === indexJ
|
||||
&&'hover-show'
|
||||
]"
|
||||
@contextmenu="e=>onContextMenu(e,sentence.text,indexI,indexJ)"
|
||||
@contextmenu="e=>onContextMenu(e,sentence,indexI,indexJ)"
|
||||
@mouseenter="settingStore.allowWordTip && showSentence(indexI,indexJ)"
|
||||
@mouseleave="hideSentence"
|
||||
v-for="(sentence,indexJ) in section">
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ArticleItem,
|
||||
ArticleWord,
|
||||
DefaultArticle,
|
||||
DisplayStatistics,
|
||||
DisplayStatistics, Sentence,
|
||||
ShortcutKey,
|
||||
TranslateType,
|
||||
Word
|
||||
@@ -31,6 +31,8 @@ import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
|
||||
import {useOnKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import VolumeSetting from "@/pages/pc/components/toolbar/VolumeSetting.vue";
|
||||
import TranslateSetting from "@/pages/pc/components/toolbar/TranslateSetting.vue";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const statisticsStore = usePracticeStore()
|
||||
@@ -332,6 +334,9 @@ onUnmounted(() => {
|
||||
timer && clearInterval(timer)
|
||||
})
|
||||
|
||||
let audioRef = $ref<HTMLAudioElement>()
|
||||
const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -347,6 +352,7 @@ onUnmounted(() => {
|
||||
@wrong="wrong"
|
||||
@over="skip"
|
||||
@nextWord="nextWord"
|
||||
@play="e => playSentenceAudio(e,audioRef,articleData.article)"
|
||||
:article="articleData.article"
|
||||
/>
|
||||
</div>
|
||||
@@ -460,8 +466,8 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<audio :src="articleData.article.audioSrc" controls></audio>
|
||||
<div class="flex flex-col items-center justify-center gap-1">
|
||||
<audio ref="audioRef" v-if="articleData.article.audioSrc" :src="articleData.article.audioSrc" controls></audio>
|
||||
<div class="flex gap-3 center">
|
||||
<Tooltip
|
||||
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
|
||||
@@ -254,7 +254,7 @@ export function _getStudyProgress(index: number, total: number) {
|
||||
return Number(((index / total) * 100).toFixed())
|
||||
}
|
||||
|
||||
export function _nextTick(cb: Function, time?: number) {
|
||||
export function _nextTick(cb: () => void, time?: number) {
|
||||
if (time) {
|
||||
nextTick(() => setTimeout(cb, time))
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user