feat: srt sync and formatting

This commit is contained in:
CDN 2025-04-23 10:27:59 +08:00
parent 6bb9f06c52
commit ba2e477dc0
Signed by: CDN
GPG key ID: 0C656827F9F80080
4 changed files with 181 additions and 12 deletions

View file

@ -6,13 +6,18 @@ const Version = "0.4.0"
// Usage stores the general usage information
const Usage = `Usage: sub-cli [command] [options]
Commands:
sync Synchronize timeline of two lyrics files
convert Convert lyrics file to another format
fmt Format lyrics file
sync Synchronize timeline of two subtitle files
convert Convert subtitle file to another format
fmt Format subtitle file
help Show help`
// SyncUsage stores the usage information for the sync command
const SyncUsage = `Usage: sub-cli sync <source> <target>`
const SyncUsage = `Usage: sub-cli sync <source> <target>
Note:
Currently supports synchronizing between files of the same format:
- LRC to LRC
- SRT to SRT
If source and target have different numbers of entries, a warning will be shown.`
// ConvertUsage stores the usage information for the convert command
const ConvertUsage = `Usage: sub-cli convert <source> <target>

View file

@ -122,6 +122,23 @@ func formatSRTTimestamp(ts model.Timestamp) string {
ts.Milliseconds)
}
// Format standardizes and formats an SRT file
func Format(filePath string) error {
// Parse the file
entries, err := Parse(filePath)
if err != nil {
return fmt.Errorf("error parsing SRT file: %w", err)
}
// Standardize entry numbering and ensure consistent formatting
for i := range entries {
entries[i].Number = i + 1 // Ensure sequential numbering
}
// Write back the formatted content
return Generate(entries, filePath)
}
// ConvertToLyrics converts SRT entries to a Lyrics structure
func ConvertToLyrics(entries []model.SRTEntry) model.Lyrics {
lyrics := model.Lyrics{

View file

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

View file

@ -6,6 +6,7 @@ import (
"strings"
"sub-cli/internal/format/lrc"
"sub-cli/internal/format/srt"
"sub-cli/internal/model"
)
@ -14,11 +15,18 @@ func SyncLyrics(sourceFile, targetFile string) error {
sourceFmt := strings.TrimPrefix(filepath.Ext(sourceFile), ".")
targetFmt := strings.TrimPrefix(filepath.Ext(targetFile), ".")
// Currently only supports LRC files
if sourceFmt != "lrc" || targetFmt != "lrc" {
return fmt.Errorf("sync only supports LRC files currently")
// Check for supported format combinations
if sourceFmt == "lrc" && targetFmt == "lrc" {
return syncLRCFiles(sourceFile, targetFile)
} else if sourceFmt == "srt" && targetFmt == "srt" {
return syncSRTFiles(sourceFile, targetFile)
} else {
return fmt.Errorf("sync only supports files of the same format (lrc-to-lrc or srt-to-srt)")
}
}
// 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)
@ -29,15 +37,46 @@ func SyncLyrics(sourceFile, targetFile string) error {
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 := syncTimeline(source, target)
syncedLyrics := syncLRCTimeline(source, target)
// Write the synced lyrics to the target file
return lrc.Generate(syncedLyrics, targetFile)
}
// syncTimeline applies the timeline from the source lyrics to the target lyrics
func syncTimeline(source, target model.Lyrics) model.Lyrics {
// 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)
}
// 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,
@ -54,6 +93,64 @@ func syncTimeline(source, target model.Lyrics) model.Lyrics {
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 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
}
// 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 {
@ -77,3 +174,50 @@ func scaleTimeline(timeline []model.Timestamp, targetCount int) []model.Timestam
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 {
durationMillis = 3000 // Default 3 seconds if negative
}
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,
}
}