mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
bug处理
This commit is contained in:
@@ -27,6 +27,7 @@ func srtToVTT(body string) string {
|
||||
func assToVTT(body string) string {
|
||||
out := strings.Builder{}
|
||||
out.WriteString("WEBVTT\n\n")
|
||||
seen := make(map[string]struct{})
|
||||
for i, line := range strings.Split(body, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "Dialogue:") {
|
||||
@@ -36,11 +37,22 @@ func assToVTT(body string) string {
|
||||
if len(parts) < 10 {
|
||||
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",
|
||||
i,
|
||||
normaliseTimecode(parts[1]),
|
||||
normaliseTimecode(parts[2]),
|
||||
stripASSTags(parts[9]),
|
||||
start,
|
||||
end,
|
||||
text,
|
||||
)
|
||||
}
|
||||
return out.String()
|
||||
@@ -86,5 +98,9 @@ func normaliseTimecode(t string) string {
|
||||
var assTag = regexp.MustCompile(`\{[^}]*\}`)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
|
||||
@@ -47,6 +47,14 @@ function parseWebVTTCues(body: string): SubtitleCue[] {
|
||||
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 = {
|
||||
media: Media | null
|
||||
/** 媒体元数据加载失败提示(非空时替代「加载中」展示)。 */
|
||||
@@ -255,6 +263,7 @@ export function PlayerVideoStage({
|
||||
const selectedTrack = subs[subtitleIndex]
|
||||
if (
|
||||
!video ||
|
||||
!tracksArmed ||
|
||||
subs.length === 0 ||
|
||||
subtitleIndex < 0 ||
|
||||
!selectedTrack ||
|
||||
@@ -267,9 +276,11 @@ export function PlayerVideoStage({
|
||||
const updateCue = () => {
|
||||
const absoluteTime = video.currentTime + (streamOffset ?? 0)
|
||||
if (subtitleTimeline?.path === selectedTrack.path) {
|
||||
const texts = subtitleTimeline.cues
|
||||
.filter((cue) => absoluteTime >= cue.startTime && absoluteTime <= cue.endTime)
|
||||
.map((cue) => cue.text)
|
||||
const texts = uniqueSubtitleTexts(
|
||||
subtitleTimeline.cues
|
||||
.filter((cue) => absoluteTime >= cue.startTime && absoluteTime <= cue.endTime)
|
||||
.map((cue) => cue.text),
|
||||
)
|
||||
setActiveCueText(texts.join('\n'))
|
||||
return
|
||||
}
|
||||
@@ -304,7 +315,7 @@ export function PlayerVideoStage({
|
||||
}
|
||||
}
|
||||
}
|
||||
setActiveCueText(texts.join('\n'))
|
||||
setActiveCueText(uniqueSubtitleTexts(texts).join('\n'))
|
||||
}
|
||||
|
||||
const apply = () => {
|
||||
@@ -359,7 +370,7 @@ export function PlayerVideoStage({
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [subtitleIndex, subs, videoRef, media, streamOffset, subtitleTimeline])
|
||||
}, [subtitleIndex, subs, videoRef, media, streamOffset, subtitleTimeline, tracksArmed])
|
||||
|
||||
// 根据视频画面宽高比与舞台宽高比,确定视频在哪个轴向撑满 100%
|
||||
const isWiderThanStage =
|
||||
@@ -412,7 +423,6 @@ export function PlayerVideoStage({
|
||||
src={subtitlesAPI.url(media.id, track.path)}
|
||||
srcLang={track.lang}
|
||||
label={track.label || track.lang}
|
||||
default={subtitleIndex === index}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user