feat: fix timestamps
This commit is contained in:
@@ -77,7 +77,6 @@ export default ({
|
|||||||
return { fillWidth, handleLeft }
|
return { fillWidth, handleLeft }
|
||||||
}, [layoutWidth, sliderValue])
|
}, [layoutWidth, sliderValue])
|
||||||
|
|
||||||
console.log("using react native slider ")
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View style={styles.trackArea} onLayout={handleLayout}>
|
<View style={styles.trackArea} onLayout={handleLayout}>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Routes } from '../../navigation'
|
|||||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||||
import { gutters, Palette } from '../../styles'
|
import { gutters, Palette } from '../../styles'
|
||||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
|
import trimLeadingDuplicateSection from '../../utils/trimLeadingDuplicateSection'
|
||||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||||
|
|
||||||
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
|
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
|
||||||
@@ -64,6 +65,9 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const countdownActiveRef = useRef(false) // évite le déclenchement avant 1er tick
|
const countdownActiveRef = useRef(false) // évite le déclenchement avant 1er tick
|
||||||
const playbackStartedRef = useRef(false) // devient vrai lorsque l'audio progresse réellement
|
const playbackStartedRef = useRef(false) // devient vrai lorsque l'audio progresse réellement
|
||||||
const playbackStartedAtRef = useRef(0)
|
const playbackStartedAtRef = useRef(0)
|
||||||
|
const playbackProbeRef = useRef({ lastPos: 0, lastAt: 0 })
|
||||||
|
const playbackStartRequestAtRef = useRef(0)
|
||||||
|
const playbackStartWarnedRef = useRef(false)
|
||||||
const progressLogRef = useRef({ bucket: -1, lastPos: -1, lastDur: -1 })
|
const progressLogRef = useRef({ bucket: -1, lastPos: -1, lastDur: -1 })
|
||||||
const originalLoopingValueRef = useRef({ hasValue: false, value: false })
|
const originalLoopingValueRef = useRef({ hasValue: false, value: false })
|
||||||
const latestLoopingValueRef = useRef(isLooping ?? false)
|
const latestLoopingValueRef = useRef(isLooping ?? false)
|
||||||
@@ -132,9 +136,14 @@ const RecordPlayback = ({ route }) => {
|
|||||||
|
|
||||||
const alignedWords = useMemo(() => {
|
const alignedWords = useMemo(() => {
|
||||||
const ts = project?.musicTimestamps?.[musicIndex]
|
const ts = project?.musicTimestamps?.[musicIndex]
|
||||||
|
const rawAlignedWords = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
|
||||||
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
|
const trimmedAlignedWords = trimLeadingDuplicateSection(rawAlignedWords)
|
||||||
return arr.map((w) => ({
|
if (trimmedAlignedWords.length !== rawAlignedWords.length) {
|
||||||
|
log('Trimmed duplicated leading lyrics section', {
|
||||||
|
removedWords: rawAlignedWords.length - trimmedAlignedWords.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return trimmedAlignedWords.map((w) => ({
|
||||||
word: String(w?.word ?? ''),
|
word: String(w?.word ?? ''),
|
||||||
startS: Number(w?.startS ?? 0),
|
startS: Number(w?.startS ?? 0),
|
||||||
endS: Number(w?.endS ?? 0),
|
endS: Number(w?.endS ?? 0),
|
||||||
@@ -194,6 +203,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
playbackStartedAtRef.current = 0
|
playbackStartedAtRef.current = 0
|
||||||
|
playbackProbeRef.current = { lastPos: 0, lastAt: 0 }
|
||||||
|
playbackStartRequestAtRef.current = 0
|
||||||
|
playbackStartWarnedRef.current = false
|
||||||
|
setProgressInfo({ pos: 0, dur: 0 })
|
||||||
log('Song URL changed, reset counters', { songUrl })
|
log('Song URL changed, reset counters', { songUrl })
|
||||||
}, [songUrl])
|
}, [songUrl])
|
||||||
|
|
||||||
@@ -202,18 +215,43 @@ const RecordPlayback = ({ route }) => {
|
|||||||
if (!player) return
|
if (!player) return
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
|
const now = Date.now()
|
||||||
const dur = (player?.duration || 0) * 1000
|
const dur = (player?.duration || 0) * 1000
|
||||||
const pos = (player?.currentTime || 0) * 1000
|
const pos = (player?.currentTime || 0) * 1000
|
||||||
if (!playbackStartedRef.current && (player?.playing || pos > 0)) {
|
const lastProbe = playbackProbeRef.current
|
||||||
|
const advancing = pos > (lastProbe?.lastPos || 0) + 30
|
||||||
|
const playbackHasAdvanced = pos >= 80 || advancing
|
||||||
|
playbackProbeRef.current = { lastPos: pos, lastAt: now }
|
||||||
|
|
||||||
|
if (
|
||||||
|
playbackStartRequestAtRef.current &&
|
||||||
|
!playbackStartedRef.current &&
|
||||||
|
!playbackStartWarnedRef.current &&
|
||||||
|
now - playbackStartRequestAtRef.current > 1500
|
||||||
|
) {
|
||||||
|
playbackStartWarnedRef.current = true
|
||||||
|
log('Playback not started yet', {
|
||||||
|
playing: player?.playing,
|
||||||
|
pos,
|
||||||
|
dur,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playbackStartedRef.current && playbackHasAdvanced) {
|
||||||
playbackStartedRef.current = true
|
playbackStartedRef.current = true
|
||||||
const now = Date.now()
|
|
||||||
const estimatedStartAt = pos > 0 ? now - pos : now
|
const estimatedStartAt = pos > 0 ? now - pos : now
|
||||||
playbackStartedAtRef.current = estimatedStartAt
|
playbackStartedAtRef.current = estimatedStartAt
|
||||||
log('Playback detected', { pos, dur, startedAt: estimatedStartAt })
|
log('Playback detected', {
|
||||||
|
pos,
|
||||||
|
dur,
|
||||||
|
startedAt: estimatedStartAt,
|
||||||
|
reason: advancing ? 'advancing' : 'position',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setProgressInfo((prev) => {
|
setProgressInfo((prev) => {
|
||||||
if (prev.pos === pos && prev.dur === dur) return prev
|
const effectivePos = playbackStartedRef.current ? pos : 0
|
||||||
return { pos, dur }
|
if (prev.pos === effectivePos && prev.dur === dur) return prev
|
||||||
|
return { pos: effectivePos, dur }
|
||||||
})
|
})
|
||||||
if (dur <= 0) {
|
if (dur <= 0) {
|
||||||
if (progressLogRef.current.lastDur !== 0) {
|
if (progressLogRef.current.lastDur !== 0) {
|
||||||
@@ -317,6 +355,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
incrementDoneRef.current = false
|
incrementDoneRef.current = false
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
playbackStartedAtRef.current = 0
|
playbackStartedAtRef.current = 0
|
||||||
|
playbackProbeRef.current = { lastPos: 0, lastAt: 0 }
|
||||||
|
playbackStartRequestAtRef.current = 0
|
||||||
|
playbackStartWarnedRef.current = false
|
||||||
|
setProgressInfo({ pos: 0, dur: 0 })
|
||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
||||||
log('Session reset')
|
log('Session reset')
|
||||||
|
|
||||||
@@ -474,6 +516,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
await cameraRef.current?.resumePreview?.()
|
await cameraRef.current?.resumePreview?.()
|
||||||
} catch (_) { }
|
} catch (_) { }
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
|
playbackProbeRef.current = { lastPos: 0, lastAt: 0 }
|
||||||
|
playbackStartRequestAtRef.current = 0
|
||||||
|
playbackStartWarnedRef.current = false
|
||||||
|
setProgressInfo({ pos: 0, dur: 0 })
|
||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
||||||
log('startRecordingWithMusic', {
|
log('startRecordingWithMusic', {
|
||||||
hasPlayer: !!player,
|
hasPlayer: !!player,
|
||||||
@@ -556,6 +602,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
await new Promise(r => setTimeout(r, CAMERA_STARTUP_DELAY_MS))
|
await new Promise(r => setTimeout(r, CAMERA_STARTUP_DELAY_MS))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
playbackStartRequestAtRef.current = Date.now()
|
||||||
await player.play?.()
|
await player.play?.()
|
||||||
log('player.play invoked')
|
log('player.play invoked')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -605,7 +652,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}
|
}
|
||||||
const duration = (player?.duration || 0) * 1000
|
const duration = (player?.duration || 0) * 1000
|
||||||
const currentTime = (player?.currentTime || 0) * 1000
|
const currentTime = (player?.currentTime || 0) * 1000
|
||||||
if (!playbackStartedRef.current && (player?.playing || currentTime > 0)) {
|
if (!playbackStartedRef.current && player?.playing) {
|
||||||
playbackStartedRef.current = true
|
playbackStartedRef.current = true
|
||||||
}
|
}
|
||||||
const nearEnd = duration > 0 && currentTime >= duration - 600
|
const nearEnd = duration > 0 && currentTime >= duration - 600
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
|||||||
import { background } from '../../assets'
|
import { background } from '../../assets'
|
||||||
import RestartSpinnerIcon from '../../assets/UI/RestartSpinnerIcon'
|
import RestartSpinnerIcon from '../../assets/UI/RestartSpinnerIcon'
|
||||||
import { registerBlobUrl, releaseBlobUrl } from '../../utils/blobUrlCache'
|
import { registerBlobUrl, releaseBlobUrl } from '../../utils/blobUrlCache'
|
||||||
|
import trimLeadingDuplicateSection from '../../utils/trimLeadingDuplicateSection'
|
||||||
|
|
||||||
const TIME_BEFORE_INCREMENT_MS = 20000
|
const TIME_BEFORE_INCREMENT_MS = 20000
|
||||||
const COUNTDOWN_SECONDS = __DEV__ ? 1 : 10
|
const COUNTDOWN_SECONDS = __DEV__ ? 1 : 10
|
||||||
@@ -123,8 +124,9 @@ const RecordPlayback = ({ route }) => {
|
|||||||
// Lyrics
|
// Lyrics
|
||||||
const alignedWords = useMemo(() => {
|
const alignedWords = useMemo(() => {
|
||||||
const ts = project?.musicTimestamps?.[songIndex]
|
const ts = project?.musicTimestamps?.[songIndex]
|
||||||
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
|
const rawAlignedWords = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
|
||||||
return arr.map((w) => ({
|
const trimmedAlignedWords = trimLeadingDuplicateSection(rawAlignedWords)
|
||||||
|
return trimmedAlignedWords.map((w) => ({
|
||||||
word: String(w?.word ?? ''),
|
word: String(w?.word ?? ''),
|
||||||
startS: Number(w?.startS ?? 0),
|
startS: Number(w?.startS ?? 0),
|
||||||
endS: Number(w?.endS ?? 0),
|
endS: Number(w?.endS ?? 0),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { gutters, Palette } from '../../styles'
|
|||||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
const RecordedPlayback = ({ route }) => {
|
const RecordedPlayback = ({ route }) => {
|
||||||
const { videoUri, project, syncOffsetMs = 0 } = route.params || {}
|
const { videoUri, project, syncOffsetMs = 0 } = route.params || {}
|
||||||
|
|
||||||
const songUrl = project?.songUrl || null
|
const songUrl = project?.songUrl || null
|
||||||
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||||
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
|
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
|
||||||
@@ -127,7 +128,11 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
const resumeAfterSeek = useCallback(async () => {
|
const resumeAfterSeek = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (audioPlayer) await audioPlayer.play?.()
|
if (audioPlayer?.resume) {
|
||||||
|
await audioPlayer.resume?.()
|
||||||
|
} else if (audioPlayer) {
|
||||||
|
await audioPlayer.play?.()
|
||||||
|
}
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
try {
|
try {
|
||||||
if (videoPlayer) videoPlayer.play()
|
if (videoPlayer) videoPlayer.play()
|
||||||
|
|||||||
@@ -354,7 +354,6 @@ const SongOptionCard = ({
|
|||||||
|
|
||||||
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 })
|
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 })
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
const shouldResumeAfterSeekRef = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerPlayer(index, player)
|
registerPlayer(index, player)
|
||||||
@@ -402,27 +401,6 @@ const SongOptionCard = ({
|
|||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [player])
|
}, [player])
|
||||||
|
|
||||||
const hasCapturedSeekStateRef = useRef(false)
|
|
||||||
|
|
||||||
const resumePlaybackIfNeeded = useCallback(async () => {
|
|
||||||
if (!player) return
|
|
||||||
const shouldResume = shouldResumeAfterSeekRef.current
|
|
||||||
shouldResumeAfterSeekRef.current = false
|
|
||||||
hasCapturedSeekStateRef.current = false // Reset capture flag
|
|
||||||
|
|
||||||
if (!shouldResume) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (player.resume) {
|
|
||||||
await player.resume?.()
|
|
||||||
} else {
|
|
||||||
await player.play?.()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('SongReady resume after seek', error?.message)
|
|
||||||
}
|
|
||||||
}, [player])
|
|
||||||
|
|
||||||
const handleSeek = useCallback(
|
const handleSeek = useCallback(
|
||||||
async (targetMs) => {
|
async (targetMs) => {
|
||||||
if (!player || !progressInfo?.dur) {
|
if (!player || !progressInfo?.dur) {
|
||||||
@@ -443,26 +421,31 @@ const SongOptionCard = ({
|
|||||||
[player, progressInfo?.dur]
|
[player, progressInfo?.dur]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSeekStart = useCallback(async () => {
|
const handleSeekStart = useCallback(() => {
|
||||||
onSelect(index)
|
onSelect(index)
|
||||||
|
}, [index, onSelect])
|
||||||
|
|
||||||
|
const handlePause = useCallback(async () => {
|
||||||
if (!player) return
|
if (!player) return
|
||||||
|
|
||||||
const isCurrentlyPlaying = !!player.playing || isPlaying
|
|
||||||
|
|
||||||
// Only capture state if we haven't already for this drag interaction
|
|
||||||
if (!hasCapturedSeekStateRef.current) {
|
|
||||||
hasCapturedSeekStateRef.current = true
|
|
||||||
shouldResumeAfterSeekRef.current = isCurrentlyPlaying
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isCurrentlyPlaying) {
|
|
||||||
await player.pause?.()
|
await player.pause?.()
|
||||||
|
} catch (error) {
|
||||||
|
console.log('SongReady pause on seek', error?.message)
|
||||||
|
}
|
||||||
|
}, [player])
|
||||||
|
|
||||||
|
const handlePlay = useCallback(async () => {
|
||||||
|
if (!player) return
|
||||||
|
try {
|
||||||
|
if (player.resume) {
|
||||||
|
await player.resume?.()
|
||||||
|
} else {
|
||||||
|
await player.play?.()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('SongReady pause on seek start', error?.message)
|
console.log('SongReady resume on seek', error?.message)
|
||||||
}
|
}
|
||||||
}, [index, isPlaying, onSelect, player])
|
}, [player])
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
onSelect(index)
|
onSelect(index)
|
||||||
@@ -521,7 +504,8 @@ const SongOptionCard = ({
|
|||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
onSeekStart={handleSeekStart}
|
onSeekStart={handleSeekStart}
|
||||||
onSeek={handleSeek}
|
onSeek={handleSeek}
|
||||||
onSeekEnd={resumePlaybackIfNeeded}
|
onPause={handlePause}
|
||||||
|
onPlay={handlePlay}
|
||||||
disabled={!url}
|
disabled={!url}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
const SECTION_TAG_LEADING_REGEX = /^\s*\[([^\]]+)\]\s*/i
|
||||||
|
const TOKEN_CLEANUP_REGEX = /[.,!?;:()[\]{}"'`]+/g
|
||||||
|
|
||||||
|
const MAX_FIRST_SECTION_START_S = 3
|
||||||
|
const MIN_DUPLICATE_SECTION_START_S = 8
|
||||||
|
const MIN_DUPLICATE_GAP_S = 4
|
||||||
|
const MIN_DUPLICATE_PREFIX_TOKENS = 12
|
||||||
|
const MIN_DUPLICATE_PREFIX_RATIO = 0.9
|
||||||
|
|
||||||
|
const toSeconds = (value) => {
|
||||||
|
const n = Number(value)
|
||||||
|
return Number.isFinite(n) && n >= 0 ? n : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeTag = (value = '') =>
|
||||||
|
String(value || '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
const normalizeToken = (value = '') =>
|
||||||
|
String(value || '')
|
||||||
|
.replace(/\n/g, ' ')
|
||||||
|
.replace(TOKEN_CLEANUP_REGEX, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
const tokenizeWord = (value = '') => {
|
||||||
|
const normalized = normalizeToken(value)
|
||||||
|
return normalized ? normalized.split(' ') : []
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVerseLikeTag = (tag = '') => {
|
||||||
|
const normalized = normalizeTag(tag)
|
||||||
|
return normalized.includes('verse') || normalized.includes('couplet')
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildSegments = (alignedWords = []) => {
|
||||||
|
const segments = []
|
||||||
|
let current = null
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (current && current.tokens.length > 0) {
|
||||||
|
segments.push(current)
|
||||||
|
}
|
||||||
|
current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < alignedWords.length; index++) {
|
||||||
|
const entry = alignedWords[index] || {}
|
||||||
|
const rawWord = String(entry.word || '')
|
||||||
|
const tagMatch = rawWord.match(SECTION_TAG_LEADING_REGEX)
|
||||||
|
|
||||||
|
if (tagMatch) {
|
||||||
|
flush()
|
||||||
|
current = {
|
||||||
|
tag: normalizeTag(tagMatch[1]),
|
||||||
|
startIdx: index,
|
||||||
|
endIdx: index,
|
||||||
|
startS: toSeconds(entry.startS),
|
||||||
|
tokens: [],
|
||||||
|
}
|
||||||
|
} else if (!current) {
|
||||||
|
current = {
|
||||||
|
tag: '',
|
||||||
|
startIdx: index,
|
||||||
|
endIdx: index,
|
||||||
|
startS: toSeconds(entry.startS),
|
||||||
|
tokens: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noTagWord = tagMatch ? rawWord.replace(SECTION_TAG_LEADING_REGEX, '') : rawWord
|
||||||
|
const tokens = tokenizeWord(noTagWord)
|
||||||
|
if (tokens.length > 0) {
|
||||||
|
current.tokens.push(...tokens)
|
||||||
|
current.endIdx = index
|
||||||
|
} else if (current && current.tokens.length === 0) {
|
||||||
|
current.endIdx = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush()
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
|
||||||
|
const countCommonPrefixTokens = (tokensA = [], tokensB = []) => {
|
||||||
|
const max = Math.min(tokensA.length, tokensB.length)
|
||||||
|
let matched = 0
|
||||||
|
while (matched < max && tokensA[matched] === tokensB[matched]) {
|
||||||
|
matched += 1
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
export const trimLeadingDuplicateSection = (alignedWords = []) => {
|
||||||
|
if (!Array.isArray(alignedWords) || alignedWords.length === 0) return []
|
||||||
|
|
||||||
|
const segments = buildSegments(alignedWords)
|
||||||
|
if (segments.length < 2) return alignedWords
|
||||||
|
|
||||||
|
const first = segments[0]
|
||||||
|
const second = segments[1]
|
||||||
|
if (!isVerseLikeTag(first.tag) || !isVerseLikeTag(second.tag)) return alignedWords
|
||||||
|
if (first.tag !== second.tag) return alignedWords
|
||||||
|
|
||||||
|
const firstStartS = toSeconds(first.startS)
|
||||||
|
const secondStartS = toSeconds(second.startS)
|
||||||
|
|
||||||
|
if (firstStartS > MAX_FIRST_SECTION_START_S) return alignedWords
|
||||||
|
if (secondStartS < MIN_DUPLICATE_SECTION_START_S) return alignedWords
|
||||||
|
if (secondStartS - firstStartS < MIN_DUPLICATE_GAP_S) return alignedWords
|
||||||
|
|
||||||
|
const matchedPrefixTokens = countCommonPrefixTokens(first.tokens, second.tokens)
|
||||||
|
const minTokenCount = Math.min(first.tokens.length, second.tokens.length)
|
||||||
|
const matchedRatio = minTokenCount > 0 ? matchedPrefixTokens / minTokenCount : 0
|
||||||
|
|
||||||
|
if (matchedPrefixTokens < MIN_DUPLICATE_PREFIX_TOKENS) return alignedWords
|
||||||
|
if (matchedRatio < MIN_DUPLICATE_PREFIX_RATIO) return alignedWords
|
||||||
|
|
||||||
|
return [...alignedWords.slice(0, first.startIdx), ...alignedWords.slice(first.endIdx + 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default trimLeadingDuplicateSection
|
||||||
Reference in New Issue
Block a user