feat: basic ass processing (without style)

This commit is contained in:
CDN 2025-04-23 17:42:13 +08:00
parent 8897d7ae90
commit ebbf516689
Signed by: CDN
GPG key ID: 0C656827F9F80080
10 changed files with 2301 additions and 808 deletions

View file

@ -5,6 +5,7 @@ import (
"path/filepath"
"strings"
"sub-cli/internal/format/ass"
"sub-cli/internal/format/lrc"
"sub-cli/internal/format/srt"
"sub-cli/internal/format/vtt"
@ -23,8 +24,10 @@ func SyncLyrics(sourceFile, targetFile string) error {
return syncSRTFiles(sourceFile, targetFile)
} else if sourceFmt == "vtt" && targetFmt == "vtt" {
return syncVTTFiles(sourceFile, targetFile)
} else if sourceFmt == "ass" && targetFmt == "ass" {
return syncASSFiles(sourceFile, targetFile)
} else {
return fmt.Errorf("sync only supports files of the same format (lrc-to-lrc, srt-to-srt, or vtt-to-vtt)")
return fmt.Errorf("sync only supports files of the same format (lrc-to-lrc, srt-to-srt, vtt-to-vtt, or ass-to-ass)")
}
}
@ -103,6 +106,31 @@ func syncVTTFiles(sourceFile, targetFile string) error {
return vtt.Generate(syncedSubtitle, targetFile)
}
// syncASSFiles synchronizes two ASS files
func syncASSFiles(sourceFile, targetFile string) error {
sourceSubtitle, err := ass.Parse(sourceFile)
if err != nil {
return fmt.Errorf("error parsing source ASS file: %w", err)
}
targetSubtitle, err := ass.Parse(targetFile)
if err != nil {
return fmt.Errorf("error parsing target ASS file: %w", err)
}
// Check if entry counts match
if len(sourceSubtitle.Events) != len(targetSubtitle.Events) {
fmt.Printf("Warning: Source (%d events) and target (%d events) have different event counts. Timeline will be adjusted.\n",
len(sourceSubtitle.Events), len(targetSubtitle.Events))
}
// Sync the timelines
syncedSubtitle := syncASSTimeline(sourceSubtitle, targetSubtitle)
// Write the synced subtitle to the target file
return ass.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{
@ -131,6 +159,15 @@ func syncSRTTimeline(sourceEntries, targetEntries []model.SRTEntry) []model.SRTE
// Copy target entries
copy(result, targetEntries)
// If source is empty, just return the target entries as is
if len(sourceEntries) == 0 {
// Ensure proper sequence numbering
for i := range result {
result[i].Number = i + 1
}
return result
}
// If source and target have the same number of entries, directly apply timings
if len(sourceEntries) == len(targetEntries) {
for i := range result {
@ -253,6 +290,51 @@ func syncVTTTimeline(source, target model.Subtitle) model.Subtitle {
return result
}
// syncASSTimeline applies the timing from source ASS subtitle to target ASS subtitle
func syncASSTimeline(source, target model.ASSFile) model.ASSFile {
result := model.ASSFile{
ScriptInfo: target.ScriptInfo,
Styles: target.Styles,
Events: make([]model.ASSEvent, len(target.Events)),
}
// Copy target events to preserve content
copy(result.Events, target.Events)
// If there are no events in either source or target, return as is
if len(source.Events) == 0 || len(target.Events) == 0 {
return result
}
// Create a timeline of source start and end times
sourceStartTimes := make([]model.Timestamp, len(source.Events))
sourceEndTimes := make([]model.Timestamp, len(source.Events))
for i, event := range source.Events {
sourceStartTimes[i] = event.StartTime
sourceEndTimes[i] = event.EndTime
}
// Scale the timeline if source and target have different number of events
var scaledStartTimes, scaledEndTimes []model.Timestamp
if len(source.Events) != len(target.Events) {
scaledStartTimes = scaleTimeline(sourceStartTimes, len(target.Events))
scaledEndTimes = scaleTimeline(sourceEndTimes, len(target.Events))
} else {
scaledStartTimes = sourceStartTimes
scaledEndTimes = sourceEndTimes
}
// Apply scaled timeline to target events
for i := range result.Events {
result.Events[i].StartTime = scaledStartTimes[i]
result.Events[i].EndTime = scaledEndTimes[i]
}
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 {