feat: support vtt in sync and fmt
This commit is contained in:
parent
a6284897c8
commit
2fa12dbcde
4 changed files with 122 additions and 1 deletions
|
@ -107,6 +107,7 @@ sub-cli sync <source> <target>
|
||||||
Currently, synchronization only works between files of the same format:
|
Currently, synchronization only works between files of the same format:
|
||||||
- SRT to SRT
|
- SRT to SRT
|
||||||
- LRC to LRC
|
- LRC to LRC
|
||||||
|
- VTT to VTT
|
||||||
|
|
||||||
### Behavior Details
|
### Behavior Details
|
||||||
|
|
||||||
|
@ -127,6 +128,16 @@ Currently, synchronization only works between files of the same format:
|
||||||
- **Preserved from target**: All subtitle text content.
|
- **Preserved from target**: All subtitle text content.
|
||||||
- **Modified in target**: Timestamps are updated and entry numbers are standardized (sequential from 1).
|
- **Modified in target**: Timestamps are updated and entry numbers are standardized (sequential from 1).
|
||||||
|
|
||||||
|
#### For VTT Files:
|
||||||
|
|
||||||
|
- **When entry counts match**: Both start and end times from the source are directly applied to the target entries.
|
||||||
|
- **When entry counts differ**: A scaled approach is used, similar to SRT synchronization:
|
||||||
|
- Start times are taken from proportionally matched source entries
|
||||||
|
- End times are calculated based on source entry durations
|
||||||
|
- The timing relationship between entries is preserved
|
||||||
|
- **Preserved from target**: All subtitle text content, formatting, cue settings, and styling.
|
||||||
|
- **Modified in target**: Timestamps are updated and cue identifiers are standardized (sequential from 1).
|
||||||
|
|
||||||
### Edge Cases
|
### Edge Cases
|
||||||
|
|
||||||
- If the source file has no timing information, the target remains unchanged.
|
- If the source file has no timing information, the target remains unchanged.
|
||||||
|
@ -142,6 +153,9 @@ sub-cli sync reference.srt target.srt
|
||||||
|
|
||||||
# Synchronize an LRC file using another LRC file as reference
|
# Synchronize an LRC file using another LRC file as reference
|
||||||
sub-cli sync reference.lrc target.lrc
|
sub-cli sync reference.lrc target.lrc
|
||||||
|
|
||||||
|
# Synchronize a VTT file using another VTT file as reference
|
||||||
|
sub-cli sync reference.vtt target.vtt
|
||||||
```
|
```
|
||||||
|
|
||||||
## fmt
|
## fmt
|
||||||
|
|
|
@ -107,6 +107,7 @@ sub-cli sync <源文件> <目标文件>
|
||||||
目前,同步仅适用于相同格式的文件之间:
|
目前,同步仅适用于相同格式的文件之间:
|
||||||
- SRT到SRT
|
- SRT到SRT
|
||||||
- LRC到LRC
|
- LRC到LRC
|
||||||
|
- VTT到VTT
|
||||||
|
|
||||||
### 行为详情
|
### 行为详情
|
||||||
|
|
||||||
|
@ -127,6 +128,16 @@ sub-cli sync <源文件> <目标文件>
|
||||||
- **从目标保留**: 所有字幕文本内容。
|
- **从目标保留**: 所有字幕文本内容。
|
||||||
- **在目标中修改**: 更新时间戳并标准化条目编号(从1开始顺序编号)。
|
- **在目标中修改**: 更新时间戳并标准化条目编号(从1开始顺序编号)。
|
||||||
|
|
||||||
|
#### 对于VTT文件:
|
||||||
|
|
||||||
|
- **当条目数匹配时**: 源的开始和结束时间直接应用于目标条目。
|
||||||
|
- **当条目数不同时**: 使用缩放方法:
|
||||||
|
- 开始时间取自按比例匹配的源条目
|
||||||
|
- 结束时间根据源条目时长计算
|
||||||
|
- 保持条目之间的时间关系
|
||||||
|
- **从目标保留**: 所有字幕文本内容和样式信息。
|
||||||
|
- **在目标中修改**: 更新时间戳并标准化提示标识符。
|
||||||
|
|
||||||
### 边缘情况
|
### 边缘情况
|
||||||
|
|
||||||
- 如果源文件没有时间信息,目标保持不变。
|
- 如果源文件没有时间信息,目标保持不变。
|
||||||
|
@ -142,6 +153,9 @@ sub-cli sync reference.srt target.srt
|
||||||
|
|
||||||
# 使用另一个LRC文件作为参考来同步LRC文件
|
# 使用另一个LRC文件作为参考来同步LRC文件
|
||||||
sub-cli sync reference.lrc target.lrc
|
sub-cli sync reference.lrc target.lrc
|
||||||
|
|
||||||
|
# 使用另一个VTT文件作为参考来同步VTT文件
|
||||||
|
sub-cli sync reference.vtt target.vtt
|
||||||
```
|
```
|
||||||
|
|
||||||
## fmt
|
## fmt
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"sub-cli/internal/format/lrc"
|
"sub-cli/internal/format/lrc"
|
||||||
"sub-cli/internal/format/srt"
|
"sub-cli/internal/format/srt"
|
||||||
|
"sub-cli/internal/format/vtt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Format formats a subtitle file to ensure consistent formatting
|
// Format formats a subtitle file to ensure consistent formatting
|
||||||
|
@ -18,6 +19,8 @@ func Format(filePath string) error {
|
||||||
return lrc.Format(filePath)
|
return lrc.Format(filePath)
|
||||||
case "srt":
|
case "srt":
|
||||||
return srt.Format(filePath)
|
return srt.Format(filePath)
|
||||||
|
case "vtt":
|
||||||
|
return vtt.Format(filePath)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported format for formatting: %s", ext)
|
return fmt.Errorf("unsupported format for formatting: %s", ext)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"sub-cli/internal/format/lrc"
|
"sub-cli/internal/format/lrc"
|
||||||
"sub-cli/internal/format/srt"
|
"sub-cli/internal/format/srt"
|
||||||
|
"sub-cli/internal/format/vtt"
|
||||||
"sub-cli/internal/model"
|
"sub-cli/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,8 +21,10 @@ func SyncLyrics(sourceFile, targetFile string) error {
|
||||||
return syncLRCFiles(sourceFile, targetFile)
|
return syncLRCFiles(sourceFile, targetFile)
|
||||||
} else if sourceFmt == "srt" && targetFmt == "srt" {
|
} else if sourceFmt == "srt" && targetFmt == "srt" {
|
||||||
return syncSRTFiles(sourceFile, targetFile)
|
return syncSRTFiles(sourceFile, targetFile)
|
||||||
|
} else if sourceFmt == "vtt" && targetFmt == "vtt" {
|
||||||
|
return syncVTTFiles(sourceFile, targetFile)
|
||||||
} else {
|
} 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)
|
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
|
// syncLRCTimeline applies the timeline from the source lyrics to the target lyrics
|
||||||
func syncLRCTimeline(source, target model.Lyrics) model.Lyrics {
|
func syncLRCTimeline(source, target model.Lyrics) model.Lyrics {
|
||||||
result := model.Lyrics{
|
result := model.Lyrics{
|
||||||
|
@ -151,6 +179,68 @@ func syncSRTTimeline(sourceEntries, targetEntries []model.SRTEntry) []model.SRTE
|
||||||
return result
|
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
|
// scaleTimeline scales a timeline to match a different number of entries
|
||||||
func scaleTimeline(timeline []model.Timestamp, targetCount int) []model.Timestamp {
|
func scaleTimeline(timeline []model.Timestamp, targetCount int) []model.Timestamp {
|
||||||
if targetCount <= 0 || len(timeline) == 0 {
|
if targetCount <= 0 || len(timeline) == 0 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue