bug处理

This commit is contained in:
truewhile
2026-09-08 21:08:47 +08:00
parent bd02e194ff
commit 3d30f6f27c
3 changed files with 56 additions and 10 deletions
+20 -4
View File
@@ -27,6 +27,7 @@ func srtToVTT(body string) string {
func assToVTT(body string) string { func assToVTT(body string) string {
out := strings.Builder{} out := strings.Builder{}
out.WriteString("WEBVTT\n\n") out.WriteString("WEBVTT\n\n")
seen := make(map[string]struct{})
for i, line := range strings.Split(body, "\n") { for i, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "Dialogue:") { if !strings.HasPrefix(line, "Dialogue:") {
@@ -36,11 +37,22 @@ func assToVTT(body string) string {
if len(parts) < 10 { if len(parts) < 10 {
continue continue
} }
start := normaliseTimecode(parts[1])
end := normaliseTimecode(parts[2])
text := stripASSTags(parts[9])
if text == "" {
continue
}
key := start + "\x00" + end + "\x00" + text
if _, duplicate := seen[key]; duplicate {
continue
}
seen[key] = struct{}{}
fmt.Fprintf(&out, "%d\n%s --> %s\n%s\n\n", fmt.Fprintf(&out, "%d\n%s --> %s\n%s\n\n",
i, i,
normaliseTimecode(parts[1]), start,
normaliseTimecode(parts[2]), end,
stripASSTags(parts[9]), text,
) )
} }
return out.String() return out.String()
@@ -86,5 +98,9 @@ func normaliseTimecode(t string) string {
var assTag = regexp.MustCompile(`\{[^}]*\}`) var assTag = regexp.MustCompile(`\{[^}]*\}`)
func stripASSTags(s string) string { func stripASSTags(s string) string {
return assTag.ReplaceAllString(s, "") s = assTag.ReplaceAllString(s, "")
s = strings.ReplaceAll(s, `\N`, "\n")
s = strings.ReplaceAll(s, `\n`, "\n")
s = strings.ReplaceAll(s, `\h`, "\u00a0")
return strings.TrimSpace(s)
} }
+20
View File
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
@@ -84,6 +85,25 @@ func TestNormaliseTimecode(t *testing.T) {
} }
} }
func TestAssToVTTDeduplicatesDialogueAndNormalisesLineBreaks(t *testing.T) {
body := strings.Join([]string{
`Dialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,{\an2}第一行\N第二行`,
`Dialogue: 1,0:00:01.00,0:00:02.00,Copy,,0,0,0,,{\bord2}第一行\N第二行`,
`Dialogue: 0,0:00:03.00,0:00:04.00,Default,,0,0,0,,{\i1}`,
}, "\n")
got := assToVTT(body)
if strings.Count(got, "第一行\n第二行") != 1 {
t.Fatalf("duplicate ASS dialogue was not collapsed:\n%s", got)
}
if strings.Contains(got, `\N`) || strings.Contains(got, `\an2`) {
t.Fatalf("ASS control sequences leaked into WebVTT:\n%s", got)
}
if strings.Contains(got, "00:00:03.000 --> 00:00:04.000") {
t.Fatalf("empty styled dialogue should be omitted:\n%s", got)
}
}
func TestSubtitleServeRawWritesSourceBytes(t *testing.T) { func TestSubtitleServeRawWritesSourceBytes(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil { if err != nil {
+16 -6
View File
@@ -47,6 +47,14 @@ function parseWebVTTCues(body: string): SubtitleCue[] {
return cues return cues
} }
function uniqueSubtitleTexts(texts: string[]): string[] {
const unique = [...new Set(texts.map((text) => text.trim()).filter(Boolean))]
// ponytail: WebVTT cannot preserve ASS layers/positions. Keep at most two
// simultaneous blocks for bilingual subtitles; use libass if full ASS
// typesetting support is added later.
return unique.slice(-2)
}
type PlayerVideoStageProps = { type PlayerVideoStageProps = {
media: Media | null media: Media | null
/** 媒体元数据加载失败提示(非空时替代「加载中」展示)。 */ /** 媒体元数据加载失败提示(非空时替代「加载中」展示)。 */
@@ -255,6 +263,7 @@ export function PlayerVideoStage({
const selectedTrack = subs[subtitleIndex] const selectedTrack = subs[subtitleIndex]
if ( if (
!video || !video ||
!tracksArmed ||
subs.length === 0 || subs.length === 0 ||
subtitleIndex < 0 || subtitleIndex < 0 ||
!selectedTrack || !selectedTrack ||
@@ -267,9 +276,11 @@ export function PlayerVideoStage({
const updateCue = () => { const updateCue = () => {
const absoluteTime = video.currentTime + (streamOffset ?? 0) const absoluteTime = video.currentTime + (streamOffset ?? 0)
if (subtitleTimeline?.path === selectedTrack.path) { if (subtitleTimeline?.path === selectedTrack.path) {
const texts = subtitleTimeline.cues const texts = uniqueSubtitleTexts(
.filter((cue) => absoluteTime >= cue.startTime && absoluteTime <= cue.endTime) subtitleTimeline.cues
.map((cue) => cue.text) .filter((cue) => absoluteTime >= cue.startTime && absoluteTime <= cue.endTime)
.map((cue) => cue.text),
)
setActiveCueText(texts.join('\n')) setActiveCueText(texts.join('\n'))
return return
} }
@@ -304,7 +315,7 @@ export function PlayerVideoStage({
} }
} }
} }
setActiveCueText(texts.join('\n')) setActiveCueText(uniqueSubtitleTexts(texts).join('\n'))
} }
const apply = () => { const apply = () => {
@@ -359,7 +370,7 @@ export function PlayerVideoStage({
} }
} }
} }
}, [subtitleIndex, subs, videoRef, media, streamOffset, subtitleTimeline]) }, [subtitleIndex, subs, videoRef, media, streamOffset, subtitleTimeline, tracksArmed])
// 根据视频画面宽高比与舞台宽高比,确定视频在哪个轴向撑满 100% // 根据视频画面宽高比与舞台宽高比,确定视频在哪个轴向撑满 100%
const isWiderThanStage = const isWiderThanStage =
@@ -412,7 +423,6 @@ export function PlayerVideoStage({
src={subtitlesAPI.url(media.id, track.path)} src={subtitlesAPI.url(media.id, track.path)}
srcLang={track.lang} srcLang={track.lang}
label={track.label || track.lang} label={track.label || track.lang}
default={subtitleIndex === index}
/> />
), ),
)} )}