feat: add manual video tagging

This commit is contained in:
2026-04-29 21:20:21 +08:00
parent 02d37f3953
commit eac6ad2343
17 changed files with 1066 additions and 418 deletions
+68 -55
View File
@@ -1,9 +1,10 @@
import { prisma } from "@/lib/prisma";
import BackButton from "@/app/components/BackButton";
import AwemeDetailClient from "./Client";
import type { Metadata } from "next";
import BackButton from "@/app/components/BackButton";
import { resolveEffectiveContentTags } from "@/lib/asr-tags";
import { getFileUrl } from "@/lib/minio";
import { AwemeData, VideoTranscript } from "./types";
import { prisma } from "@/lib/prisma";
import AwemeDetailClient from "./Client";
import type { AwemeData, VideoTranscript } from "./types";
export async function generateMetadata({
params,
@@ -53,7 +54,7 @@ export default async function AwemeDetail({
const [video, post] = await Promise.all([
prisma.video.findUnique({
where: { aweme_id: id },
include: { author: true },
include: { author: true, transcript: true },
}),
prisma.imagePost.findUnique({
where: { aweme_id: id },
@@ -63,65 +64,77 @@ export default async function AwemeDetail({
if (!video && !post) return <main className="p-8">找不到该作品</main>;
const isVideo = !!video;
const isVideo = Boolean(video);
// 获取评论总数
const commentsCount = await prisma.comment.count({
where: isVideo ? { videoId: id } : { imagePostId: id },
});
const aweme = isVideo ? video : post;
const data: AwemeData = {
aweme_id: aweme!.aweme_id,
desc: aweme!.desc,
created_at: aweme!.created_at,
likesCount: Number(aweme!.digg_count),
commentsCount,
author: {
nickname: aweme!.author.nickname,
avatar_url: getFileUrl(aweme!.author.avatar_url || "default-avatar.png"),
sec_uid: aweme!.author.sec_uid,
},
...(() => {
if (isVideo) {
const aweme = video!;
return {
type: "video" as const,
cover_url: getFileUrl(aweme!.cover_url ?? "default-cover.png"),
cover_size: { h: aweme.height ?? 0, w: aweme.width ?? 0 },
duration_ms: aweme!.duration_ms,
video_url: getFileUrl(aweme!.video_url),
width: aweme!.width ?? null,
height: aweme!.height ?? null,
};
} else {
const aweme = post!;
return {
type: "image" as const,
cover_url: getFileUrl(aweme!.images[0].url ?? "default-cover.png"),
cover_size: {
h: aweme!.images[0].height ?? 0,
w: aweme!.images[0].width ?? 0,
},
images: aweme!.images.map((img) => ({
...img,
url: getFileUrl(img.url),
animated: img.animated ? getFileUrl(img.animated) : null,
})),
music_url: getFileUrl(aweme!.music_url || "default-music.mp3"),
};
}
})(),
};
let data: AwemeData;
let currentCreatedAt: Date;
let transcript: VideoTranscript | null = null;
const transcript: VideoTranscript | null = isVideo
? await prisma.videoTranscript.findUnique({
where: { videoId: id },
})
: null;
if (video) {
transcript = video.transcript ?? null;
currentCreatedAt = video.created_at;
data = {
type: "video",
aweme_id: video.aweme_id,
desc: video.desc,
created_at: video.created_at,
likesCount: Number(video.digg_count),
commentsCount,
author: {
nickname: video.author.nickname,
avatar_url: getFileUrl(video.author.avatar_url || "default-avatar.png"),
sec_uid: video.author.sec_uid,
},
cover_url: getFileUrl(video.cover_url ?? "default-cover.png"),
cover_size: { h: video.height ?? 0, w: video.width ?? 0 },
duration_ms: video.duration_ms,
video_url: getFileUrl(video.video_url),
width: video.width ?? null,
height: video.height ?? null,
content_tags: resolveEffectiveContentTags(
video.manual_content_tags,
transcript?.content_tags,
),
manual_content_tags: video.manual_content_tags,
asr_content_tags: transcript?.content_tags ?? [],
};
} else if (post) {
const coverImage = post.images[0];
currentCreatedAt = post.created_at;
data = {
type: "image",
aweme_id: post.aweme_id,
desc: post.desc,
created_at: post.created_at,
likesCount: Number(post.digg_count),
commentsCount,
author: {
nickname: post.author.nickname,
avatar_url: getFileUrl(post.author.avatar_url || "default-avatar.png"),
sec_uid: post.author.sec_uid,
},
cover_url: getFileUrl(coverImage?.url ?? "default-cover.png"),
cover_size: {
h: coverImage?.height ?? 0,
w: coverImage?.width ?? 0,
},
images: post.images.map((img) => ({
...img,
url: getFileUrl(img.url),
animated: img.animated ? getFileUrl(img.animated) : null,
})),
music_url: getFileUrl(post.music_url || "default-music.mp3"),
};
} else {
return <main className="p-8">找不到该作品</main>;
}
// Compute prev/next neighbors by created_at across videos and image posts
const currentCreatedAt = isVideo ? video!.created_at : post!.created_at;
const [newerVideo, newerPost, olderVideo, olderPost] = await Promise.all([
prisma.video.findFirst({
where: { created_at: { gt: currentCreatedAt } },