feat: 添加音频

This commit is contained in:
zyronon
2025-05-19 03:48:34 +08:00
parent 8235edb84d
commit 2d84928eb0
6 changed files with 108 additions and 55 deletions

View File

@@ -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"
},
{

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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]})`"

View File

@@ -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 {