feat: support vtt in sync and fmt

This commit is contained in:
CDN 2025-04-23 15:29:27 +08:00
parent a6284897c8
commit 2fa12dbcde
Signed by: CDN
GPG key ID: 0C656827F9F80080
4 changed files with 122 additions and 1 deletions

View file

@ -7,6 +7,7 @@ import (
"sub-cli/internal/format/lrc"
"sub-cli/internal/format/srt"
"sub-cli/internal/format/vtt"
)
// Format formats a subtitle file to ensure consistent formatting
@ -18,6 +19,8 @@ func Format(filePath string) error {
return lrc.Format(filePath)
case "srt":
return srt.Format(filePath)
case "vtt":
return vtt.Format(filePath)
default:
return fmt.Errorf("unsupported format for formatting: %s", ext)
}

View file

@ -7,6 +7,7 @@ import (
"sub-cli/internal/format/lrc"
"sub-cli/internal/format/srt"
"sub-cli/internal/format/vtt"
"sub-cli/internal/model"
)
@ -20,8 +21,10 @@ func SyncLyrics(sourceFile, targetFile string) error {
return syncLRCFiles(sourceFile, targetFile)
} else if sourceFmt == "srt" && targetFmt == "srt" {
return syncSRTFiles(sourceFile, targetFile)
} else if sourceFmt == "vtt" && targetFmt == "vtt" {
return syncVTTFiles(sourceFile, targetFile)
} else {
return fmt.Errorf("sync only supports files of the same format (lrc-to-lrc or srt-to-srt)")
return fmt.Errorf("sync only supports files of the same format (lrc-to-lrc, srt-to-srt, or vtt-to-vtt)")
}
}
@ -75,6 +78,31 @@ func syncSRTFiles(sourceFile, targetFile string) error {
return srt.Generate(syncedEntries, targetFile)
}
// syncVTTFiles synchronizes two VTT files
func syncVTTFiles(sourceFile, targetFile string) error {
sourceSubtitle, err := vtt.Parse(sourceFile)
if err != nil {
return fmt.Errorf("error parsing source VTT file: %w", err)
}
targetSubtitle, err := vtt.Parse(targetFile)
if err != nil {
return fmt.Errorf("error parsing target VTT file: %w", err)
}
// Check if entry counts match
if len(sourceSubtitle.Entries) != len(targetSubtitle.Entries) {
fmt.Printf("Warning: Source (%d entries) and target (%d entries) have different entry counts. Timeline will be adjusted.\n",
len(sourceSubtitle.Entries), len(targetSubtitle.Entries))
}
// Sync the timelines
syncedSubtitle := syncVTTTimeline(sourceSubtitle, targetSubtitle)
// Write the synced subtitle to the target file
return vtt.Generate(syncedSubtitle, targetFile)
}
// syncLRCTimeline applies the timeline from the source lyrics to the target lyrics
func syncLRCTimeline(source, target model.Lyrics) model.Lyrics {
result := model.Lyrics{
@ -151,6 +179,68 @@ func syncSRTTimeline(sourceEntries, targetEntries []model.SRTEntry) []model.SRTE
return result
}
// syncVTTTimeline applies the timing from source VTT subtitle to target VTT subtitle
func syncVTTTimeline(source, target model.Subtitle) model.Subtitle {
result := model.NewSubtitle()
result.Format = "vtt"
result.Title = target.Title
result.Metadata = target.Metadata
result.Styles = target.Styles
// Create entries array with same length as target
result.Entries = make([]model.SubtitleEntry, len(target.Entries))
// Copy target entries
copy(result.Entries, target.Entries)
// If source and target have the same number of entries, directly apply timings
if len(source.Entries) == len(target.Entries) {
for i := range result.Entries {
result.Entries[i].StartTime = source.Entries[i].StartTime
result.Entries[i].EndTime = source.Entries[i].EndTime
}
} else {
// If entry counts differ, scale the timing similar to SRT sync
for i := range result.Entries {
// Calculate scaled index
sourceIdx := 0
if len(source.Entries) > 1 {
sourceIdx = i * (len(source.Entries) - 1) / (len(target.Entries) - 1)
}
// Ensure the index is within bounds
if sourceIdx >= len(source.Entries) {
sourceIdx = len(source.Entries) - 1
}
// Apply the scaled timing
result.Entries[i].StartTime = source.Entries[sourceIdx].StartTime
// Calculate end time: if not the last entry, use duration from source
if i < len(result.Entries)-1 {
// If next source entry exists, calculate duration
var duration model.Timestamp
if sourceIdx+1 < len(source.Entries) {
duration = calculateDuration(source.Entries[sourceIdx].StartTime, source.Entries[sourceIdx+1].StartTime)
} else {
duration = calculateDuration(source.Entries[sourceIdx].StartTime, source.Entries[sourceIdx].EndTime)
}
result.Entries[i].EndTime = addDuration(result.Entries[i].StartTime, duration)
} else {
// For the last entry, use the end time from source
result.Entries[i].EndTime = source.Entries[sourceIdx].EndTime
}
}
}
// Ensure proper index numbering
for i := range result.Entries {
result.Entries[i].Index = i + 1
}
return result
}
// scaleTimeline scales a timeline to match a different number of entries
func scaleTimeline(timeline []model.Timestamp, targetCount int) []model.Timestamp {
if targetCount <= 0 || len(timeline) == 0 {