linguaielts-api / fronted /src /utils /segmentUtils.js
AnhviNguyen
shadowing
02f8344
Raw
History Blame Contribute Delete
2.7 kB
/**
* Shadowing segment helpers — scoring, tokenization, YouTube ID.
*/
const YT_PATTERNS = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
/^([a-zA-Z0-9_-]{11})$/,
]
export function extractYoutubeVideoId(url) {
const s = (url || '').trim()
for (const pat of YT_PATTERNS) {
const m = s.match(pat)
if (m) return m[1]
}
return null
}
/** YouTube thumbnail URL (mqdefault ≈ 320×180). */
export function youtubeThumbnail(videoId, quality = 'mqdefault') {
if (!videoId) return ''
return `https://i.ytimg.com/vi/${videoId}/${quality}.jpg`
}
export function tokenizeWords(text) {
return (text || '')
.trim()
.split(/\s+/)
.filter(Boolean)
}
function normalizeToken(w) {
return w.toLowerCase().replace(/[^\w']/g, '')
}
/**
* Dictation scoring — ignore case & punctuation.
* @returns {{ correct: string[], wrong: string[], missing: string[], score: number }}
*/
export function scoreAnswer(userInput, correct) {
const target = tokenizeWords(correct).map(normalizeToken).filter(Boolean)
const user = tokenizeWords(userInput).map(normalizeToken).filter(Boolean)
const correctWords = []
const wrong = []
const missing = []
const maxLen = Math.max(target.length, user.length)
for (let i = 0; i < maxLen; i++) {
const t = target[i]
const u = user[i]
if (!t && u) {
wrong.push(user[i] ?? u)
} else if (t && !u) {
missing.push(correct.split(/\s+/)[i] ?? t)
} else if (t && u) {
if (t === u) correctWords.push(correct.split(/\s+/)[i] ?? t)
else wrong.push(user[i] ?? u)
}
}
const score = target.length
? Math.round((correctWords.length / target.length) * 100)
: 0
return { correct: correctWords, wrong, missing, score }
}
/**
* Pronunciation scoring from Web Speech transcript vs target.
*/
export function scorePronunciation(spokenText, targetText) {
const target = tokenizeWords(targetText).map(normalizeToken).filter(Boolean)
const spoken = tokenizeWords(spokenText).map(normalizeToken).filter(Boolean)
const wordResults = target.map((t, i) => {
const s = spoken[i]
return {
word: tokenizeWords(targetText)[i] || t,
ok: s === t,
spoken: spoken[i] || null,
}
})
const extra = spoken.slice(target.length)
const correctCount = wordResults.filter((w) => w.ok).length
const score = target.length
? Math.round((correctCount / target.length) * 100)
: 0
return { wordResults, extra, score }
}
export function formatTimestamp(seconds) {
const s = Math.max(0, Number(seconds) || 0)
const m = Math.floor(s / 60)
const sec = (s % 60).toFixed(1)
return `${m}:${sec.padStart(4, '0')}`
}