chore: seperate large files
This commit is contained in:
parent
ebbf516689
commit
76e1298ded
44 changed files with 5745 additions and 4173 deletions
|
@ -1,15 +1,9 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"sub-cli/internal/format/ass"
|
||||
"sub-cli/internal/format/lrc"
|
||||
"sub-cli/internal/format/srt"
|
||||
"sub-cli/internal/format/vtt"
|
||||
"sub-cli/internal/model"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SyncLyrics synchronizes the timeline of a source lyrics file with a target lyrics file
|
||||
|
@ -30,438 +24,3 @@ func SyncLyrics(sourceFile, targetFile string) error {
|
|||
return fmt.Errorf("sync only supports files of the same format (lrc-to-lrc, srt-to-srt, vtt-to-vtt, or ass-to-ass)")
|
||||
}
|
||||
}
|
||||
|
||||
// syncLRCFiles synchronizes two LRC files
|
||||
func syncLRCFiles(sourceFile, targetFile string) error {
|
||||
source, err := lrc.Parse(sourceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing source file: %w", err)
|
||||
}
|
||||
|
||||
target, err := lrc.Parse(targetFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing target file: %w", err)
|
||||
}
|
||||
|
||||
// Check if line counts match
|
||||
if len(source.Timeline) != len(target.Content) {
|
||||
fmt.Printf("Warning: Source timeline (%d entries) and target content (%d entries) have different counts. Timeline will be scaled.\n",
|
||||
len(source.Timeline), len(target.Content))
|
||||
}
|
||||
|
||||
// Apply timeline from source to target
|
||||
syncedLyrics := syncLRCTimeline(source, target)
|
||||
|
||||
// Write the synced lyrics to the target file
|
||||
return lrc.Generate(syncedLyrics, targetFile)
|
||||
}
|
||||
|
||||
// syncSRTFiles synchronizes two SRT files
|
||||
func syncSRTFiles(sourceFile, targetFile string) error {
|
||||
sourceEntries, err := srt.Parse(sourceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing source SRT file: %w", err)
|
||||
}
|
||||
|
||||
targetEntries, err := srt.Parse(targetFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing target SRT file: %w", err)
|
||||
}
|
||||
|
||||
// Check if entry counts match
|
||||
if len(sourceEntries) != len(targetEntries) {
|
||||
fmt.Printf("Warning: Source (%d entries) and target (%d entries) have different entry counts. Timeline will be adjusted.\n",
|
||||
len(sourceEntries), len(targetEntries))
|
||||
}
|
||||
|
||||
// Sync the timelines
|
||||
syncedEntries := syncSRTTimeline(sourceEntries, targetEntries)
|
||||
|
||||
// Write the synced entries to the target file
|
||||
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)
|
||||
}
|
||||
|
||||
// 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{
|
||||
Metadata: target.Metadata,
|
||||
Content: target.Content,
|
||||
}
|
||||
|
||||
// Create timeline with same length as target content
|
||||
result.Timeline = make([]model.Timestamp, len(target.Content))
|
||||
|
||||
// Use source timeline if available and lengths match
|
||||
if len(source.Timeline) > 0 && len(source.Timeline) == len(target.Content) {
|
||||
copy(result.Timeline, source.Timeline)
|
||||
} else if len(source.Timeline) > 0 {
|
||||
// If lengths don't match, scale timeline using our improved scaleTimeline function
|
||||
result.Timeline = scaleTimeline(source.Timeline, len(target.Content))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// syncSRTTimeline applies the timing from source SRT entries to target SRT entries
|
||||
func syncSRTTimeline(sourceEntries, targetEntries []model.SRTEntry) []model.SRTEntry {
|
||||
result := make([]model.SRTEntry, len(targetEntries))
|
||||
|
||||
// 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 {
|
||||
result[i].StartTime = sourceEntries[i].StartTime
|
||||
result[i].EndTime = sourceEntries[i].EndTime
|
||||
}
|
||||
} else {
|
||||
// If entry counts differ, scale the timing
|
||||
for i := range result {
|
||||
// Calculate scaled index
|
||||
sourceIdx := 0
|
||||
if len(sourceEntries) > 1 {
|
||||
sourceIdx = i * (len(sourceEntries) - 1) / (len(targetEntries) - 1)
|
||||
}
|
||||
|
||||
// Ensure the index is within bounds
|
||||
if sourceIdx >= len(sourceEntries) {
|
||||
sourceIdx = len(sourceEntries) - 1
|
||||
}
|
||||
|
||||
// Apply the scaled timing
|
||||
result[i].StartTime = sourceEntries[sourceIdx].StartTime
|
||||
|
||||
// Calculate end time: if not the last entry, use duration from source
|
||||
if i < len(result)-1 {
|
||||
// If next source entry exists, calculate duration
|
||||
var duration model.Timestamp
|
||||
if sourceIdx+1 < len(sourceEntries) {
|
||||
duration = calculateDuration(sourceEntries[sourceIdx].StartTime, sourceEntries[sourceIdx+1].StartTime)
|
||||
} else {
|
||||
// If no next source entry, use the source's end time (usually a few seconds after start)
|
||||
duration = calculateDuration(sourceEntries[sourceIdx].StartTime, sourceEntries[sourceIdx].EndTime)
|
||||
}
|
||||
|
||||
// Apply duration to next start time
|
||||
result[i].EndTime = addDuration(result[i].StartTime, duration)
|
||||
} else {
|
||||
// For the last entry, add a fixed duration (e.g., 3 seconds)
|
||||
result[i].EndTime = sourceEntries[sourceIdx].EndTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure proper sequence numbering
|
||||
for i := range result {
|
||||
result[i].Number = i + 1
|
||||
}
|
||||
|
||||
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 len(source.Entries) == 0 || len(target.Entries) == 0 {
|
||||
// 确保索引编号正确
|
||||
for i := range result.Entries {
|
||||
result.Entries[i].Index = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return []model.Timestamp{}
|
||||
}
|
||||
|
||||
result := make([]model.Timestamp, targetCount)
|
||||
|
||||
if targetCount == 1 {
|
||||
result[0] = timeline[0]
|
||||
return result
|
||||
}
|
||||
|
||||
sourceLength := len(timeline)
|
||||
|
||||
// Handle simple case: same length
|
||||
if targetCount == sourceLength {
|
||||
copy(result, timeline)
|
||||
return result
|
||||
}
|
||||
|
||||
// Handle case where target is longer than source
|
||||
// We need to interpolate timestamps between source entries
|
||||
for i := 0; i < targetCount; i++ {
|
||||
if sourceLength == 1 {
|
||||
// If source has only one entry, use it for all target entries
|
||||
result[i] = timeline[0]
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate a floating-point position in the source timeline
|
||||
floatIndex := float64(i) * float64(sourceLength-1) / float64(targetCount-1)
|
||||
lowerIndex := int(floatIndex)
|
||||
upperIndex := lowerIndex + 1
|
||||
|
||||
// Handle boundary case
|
||||
if upperIndex >= sourceLength {
|
||||
upperIndex = sourceLength - 1
|
||||
lowerIndex = upperIndex - 1
|
||||
}
|
||||
|
||||
// If indices are the same, just use the source timestamp
|
||||
if lowerIndex == upperIndex || lowerIndex < 0 {
|
||||
result[i] = timeline[upperIndex]
|
||||
} else {
|
||||
// Calculate the fraction between the lower and upper indices
|
||||
fraction := floatIndex - float64(lowerIndex)
|
||||
|
||||
// Convert timestamps to milliseconds for interpolation
|
||||
lowerMS := timeline[lowerIndex].Hours*3600000 + timeline[lowerIndex].Minutes*60000 +
|
||||
timeline[lowerIndex].Seconds*1000 + timeline[lowerIndex].Milliseconds
|
||||
|
||||
upperMS := timeline[upperIndex].Hours*3600000 + timeline[upperIndex].Minutes*60000 +
|
||||
timeline[upperIndex].Seconds*1000 + timeline[upperIndex].Milliseconds
|
||||
|
||||
// Interpolate
|
||||
resultMS := int(float64(lowerMS) + fraction*float64(upperMS-lowerMS))
|
||||
|
||||
// Convert back to timestamp
|
||||
hours := resultMS / 3600000
|
||||
resultMS %= 3600000
|
||||
minutes := resultMS / 60000
|
||||
resultMS %= 60000
|
||||
seconds := resultMS / 1000
|
||||
milliseconds := resultMS % 1000
|
||||
|
||||
result[i] = model.Timestamp{
|
||||
Hours: hours,
|
||||
Minutes: minutes,
|
||||
Seconds: seconds,
|
||||
Milliseconds: milliseconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// calculateDuration calculates the time difference between two timestamps
|
||||
func calculateDuration(start, end model.Timestamp) model.Timestamp {
|
||||
startMillis := start.Hours*3600000 + start.Minutes*60000 + start.Seconds*1000 + start.Milliseconds
|
||||
endMillis := end.Hours*3600000 + end.Minutes*60000 + end.Seconds*1000 + end.Milliseconds
|
||||
|
||||
durationMillis := endMillis - startMillis
|
||||
if durationMillis < 0 {
|
||||
// Return zero duration if end is before start
|
||||
return model.Timestamp{
|
||||
Hours: 0,
|
||||
Minutes: 0,
|
||||
Seconds: 0,
|
||||
Milliseconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
hours := durationMillis / 3600000
|
||||
durationMillis %= 3600000
|
||||
minutes := durationMillis / 60000
|
||||
durationMillis %= 60000
|
||||
seconds := durationMillis / 1000
|
||||
milliseconds := durationMillis % 1000
|
||||
|
||||
return model.Timestamp{
|
||||
Hours: hours,
|
||||
Minutes: minutes,
|
||||
Seconds: seconds,
|
||||
Milliseconds: milliseconds,
|
||||
}
|
||||
}
|
||||
|
||||
// addDuration adds a duration to a timestamp
|
||||
func addDuration(start, duration model.Timestamp) model.Timestamp {
|
||||
startMillis := start.Hours*3600000 + start.Minutes*60000 + start.Seconds*1000 + start.Milliseconds
|
||||
durationMillis := duration.Hours*3600000 + duration.Minutes*60000 + duration.Seconds*1000 + duration.Milliseconds
|
||||
|
||||
totalMillis := startMillis + durationMillis
|
||||
|
||||
hours := totalMillis / 3600000
|
||||
totalMillis %= 3600000
|
||||
minutes := totalMillis / 60000
|
||||
totalMillis %= 60000
|
||||
seconds := totalMillis / 1000
|
||||
milliseconds := totalMillis % 1000
|
||||
|
||||
return model.Timestamp{
|
||||
Hours: hours,
|
||||
Minutes: minutes,
|
||||
Seconds: seconds,
|
||||
Milliseconds: milliseconds,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue