Fix intro/outro detection

This commit is contained in:
Zoe Roux 2026-04-15 22:24:19 +02:00
parent b71d6ffec3
commit df0c2ced13
No known key found for this signature in database
6 changed files with 47 additions and 23 deletions

View File

@ -194,8 +194,9 @@ export const videosMetadata = new Elysia({
)
.get(
":id/prepare",
async ({ params: { id }, headers: { authorization } }) => {
await prepareVideo(id, authorization!);
async ({ params: { id }, headers: { authorization }, status }) => {
const ret = await prepareVideo(id, authorization!);
if (ret) return status(ret.status, ret);
},
{
detail: { description: "Prepare a video for playback" },
@ -219,7 +220,6 @@ export const videosMetadata = new Elysia({
);
export const prepareVideo = async (slug: string, auth: string) => {
logger.info("Preparing next video {slug}", { slug });
const [vid] = await db
.select({ path: videos.path, show: entries.showPk, order: entries.order })
.from(videos)
@ -228,6 +228,13 @@ export const prepareVideo = async (slug: string, auth: string) => {
.where(eq(entryVideoJoin.slug, slug))
.limit(1);
if (!vid) {
return {
status: 404,
message: `No video found with slug ${slug}`,
} as const;
}
const related = vid.show
? await db
.select({ order: entries.order, path: videos.path })
@ -238,6 +245,14 @@ export const prepareVideo = async (slug: string, auth: string) => {
.orderBy(entries.order)
: [];
const idx = related.findIndex((x) => x.order === vid.order);
const near = [related[idx - 1], related[idx + 1]]
.filter((x) => x)
.map((x) => x.path);
logger.info("Preparing next video {slug} (near episodes: {near})", {
slug,
near,
});
const path = Buffer.from(vid.path, "utf8").toString("base64url");
await fetch(
@ -252,9 +267,7 @@ export const prepareVideo = async (slug: string, auth: string) => {
},
method: "POST",
body: JSON.stringify({
nearEpisodes: [related[idx - 1], related[idx + 1]]
.filter((x) => x)
.map((x) => x.path),
nearEpisodes: near,
}),
},
);

View File

@ -97,7 +97,7 @@ export const Controls = ({
<SkipChapterButton
player={player}
chapters={chapters}
isVisible={hover && controlsVisible}
isVisible={controlsVisible}
/>
</View>
);

View File

@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { useEvent, type VideoPlayer } from "react-native-video";
import type { Chapter } from "~/models";
import { Button } from "~/primitives";
import { cn } from "~/utils";
export const SkipChapterButton = ({
player,
@ -32,7 +33,10 @@ export const SkipChapterButton = ({
<Button
text={t(`player.skip-${chapter.type}`)}
onPress={() => player.seekTo(chapter.endTime)}
className="pointer-events-box-none absolute top-safe right-safe z-20 border-slate-200 bg-slate-900/70 p-4 px-4 py-2"
className={cn(
"absolute right-safe bottom-2/10 m-8",
"z-20 bg-slate-900/70 px-4 py-2",
)}
/>
);
};

View File

@ -155,11 +155,11 @@ func (s *MetadataService) matchByOverlap(
slog.WarnContext(ctx, "failed to store fingerprint", "path", otherInfo.Path, "err", err)
}
intros, err := FpFindOverlap(fingerprint.Start, otherPrint.Start)
intros, err := FpFindOverlap(ctx, fingerprint.Start, otherPrint.Start)
if err != nil {
return nil, fmt.Errorf("failed to find intro overlaps: %w", err)
}
credits, err := FpFindOverlap(fingerprint.End, otherPrint.End)
credits, err := FpFindOverlap(ctx, fingerprint.End, otherPrint.End)
if err != nil {
return nil, fmt.Errorf("failed to find credit overlaps: %w", err)
}
@ -178,6 +178,7 @@ func (s *MetadataService) matchByOverlap(
continue
}
slog.InfoContext(ctx, "Identified intro", "start", intro.StartFirst, "duration", intro.Duration)
candidates = append(candidates, Chapter{
Id: info.Id,
StartTime: float32(intro.StartFirst),
@ -189,9 +190,8 @@ func (s *MetadataService) matchByOverlap(
})
}
endOffset := max(info.Duration-FpEndDuration, 0)
for _, ov := range credits {
segData, err := ExtractSegment(fingerprint.End, ov.StartFirst, ov.StartFirst+ov.Duration)
for _, cred := range credits {
segData, err := ExtractSegment(fingerprint.End, cred.StartFirst, cred.StartFirst+cred.Duration)
if err != nil {
slog.WarnContext(ctx, "failed to extract credits segment", "err", err)
continue
@ -203,14 +203,16 @@ func (s *MetadataService) matchByOverlap(
continue
}
endOffset := info.Duration - samplesToSec(len(fingerprint.End))
slog.InfoContext(ctx, "Identified credits", "start", endOffset+cred.StartFirst, "duration", cred.Duration, "end_offset", endOffset)
candidates = append(candidates, Chapter{
Id: info.Id,
StartTime: float32(endOffset + ov.StartFirst),
EndTime: float32(endOffset + ov.StartFirst + ov.Duration),
StartTime: float32(endOffset + cred.StartFirst),
EndTime: float32(endOffset + cred.StartFirst + cred.Duration),
Name: "",
Type: Credits,
FingerprintId: &fpId,
MatchAccuracy: new(int32(ov.Accuracy)),
MatchAccuracy: new(int32(cred.Accuracy)),
})
}

View File

@ -24,9 +24,9 @@ type Fingerprint struct {
}
func (s *MetadataService) ComputeFingerprint(ctx context.Context, info *MediaInfo) (*Fingerprint, error) {
getRunning, set := s.fingerprintLock.Start(info.Path)
if getRunning != nil {
return getRunning()
get_running, set := s.fingerprintLock.Start(info.Sha)
if get_running != nil {
return get_running()
}
var startData string
@ -67,7 +67,7 @@ func (s *MetadataService) ComputeFingerprint(ctx context.Context, info *MediaInf
endFingerprint, err := computeChromaprint(
ctx,
info.Path,
max(info.Duration-5*60, 0),
max(info.Duration-FpEndDuration, 0),
-1,
)
if err != nil {

View File

@ -1,6 +1,8 @@
package src
import (
"context"
"log/slog"
"math/bits"
)
@ -174,12 +176,14 @@ func findMatchingRuns(fp1, fp2 []uint32, start1, start2 int) []Overlap {
// Handle a run that extends to the last block.
nblocks++
blockCorr = append(blockCorr, MatchThreshold)
blockCorr = append(blockCorr, 0)
for b := range nblocks {
if blockCorr[b] >= MatchThreshold {
if !inRun {
runStart = b
}
inRun = true
runStart = min(runStart, b)
continue
}
if !inRun {
@ -214,9 +218,10 @@ func findMatchingRuns(fp1, fp2 []uint32, start1, start2 int) []Overlap {
// per block using the AcoustID scoring formula.
// 4. Find contiguous runs of high-correlation blocks that are at least
// MinOverlapDuration long.
func FpFindOverlap(fp1 []uint32, fp2 []uint32) ([]Overlap, error) {
func FpFindOverlap(ctx context.Context, fp1 []uint32, fp2 []uint32) ([]Overlap, error) {
offset := findBestOffset(fp1, fp2)
if offset == nil {
slog.InfoContext(ctx, "no good offset found")
return nil, nil
}