fix: 合并服务器未推送改动并保留手动视频标签
This commit is contained in:
+1
-135
@@ -1,135 +1 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import BackButton from "@/app/components/BackButton";
|
||||
|
||||
export default function SttAdminPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-4xl mx-auto p-8">
|
||||
<BackButton />
|
||||
|
||||
<div className="mt-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">STT 管理中心</h1>
|
||||
<p className="text-gray-600 mt-2">管理和配置视频语音转写功能</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-6">
|
||||
{/* 视频转写管理 */}
|
||||
<Link
|
||||
href="/admin/stt/videos"
|
||||
className="block bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow p-6 border border-gray-200 hover:border-blue-500"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-6 h-6 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
视频转写管理
|
||||
</h2>
|
||||
<p className="text-gray-600 mt-1">
|
||||
查看和管理所有视频的转写状态,对待转写的视频进行批量或单个转写
|
||||
</p>
|
||||
<div className="mt-4 text-blue-600 font-medium flex items-center gap-1">
|
||||
进入管理
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* 转写设置(预留) */}
|
||||
<div className="block bg-white rounded-lg shadow-md p-6 border border-gray-200 opacity-60">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
转写设置
|
||||
</h2>
|
||||
<p className="text-gray-600 mt-1">
|
||||
配置转写 API、模型参数和其他高级选项
|
||||
</p>
|
||||
<div className="mt-4 text-gray-400 font-medium">即将推出</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 转写统计(预留) */}
|
||||
<div className="block bg-white rounded-lg shadow-md p-6 border border-gray-200 opacity-60">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
转写统计
|
||||
</h2>
|
||||
<p className="text-gray-600 mt-1">
|
||||
查看转写使用情况、成功率、语言分布等统计数据
|
||||
</p>
|
||||
<div className="mt-4 text-gray-400 font-medium">即将推出</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export { default } from "../asr/page";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { VIDEO_CONTENT_TAGS } from "@/lib/video-tags";
|
||||
import BackButton from "@/app/components/BackButton";
|
||||
import {
|
||||
isActive,
|
||||
@@ -18,6 +19,7 @@ type Video = {
|
||||
duration_ms: number;
|
||||
author: { nickname: string };
|
||||
transcription: TranscriptionState;
|
||||
content_tags: string[];
|
||||
};
|
||||
const filters: ("all" | TranscriptionStatus)[] = [
|
||||
"all",
|
||||
@@ -36,6 +38,10 @@ export default function SttVideosPage() {
|
||||
const [submitting, setSubmitting] = useState<Set<string>>(new Set());
|
||||
const [batchSubmitting, setBatchSubmitting] = useState(false);
|
||||
const [filter, setFilter] = useState<"all" | TranscriptionStatus>("all");
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [tag, setTag] = useState<string>(VIDEO_CONTENT_TAGS[0]);
|
||||
const [tagFilter, setTagFilter] = useState("");
|
||||
const [savingTags, setSavingTags] = useState(false);
|
||||
const fetching = useRef(false);
|
||||
const fetchVideos = useCallback(async () => {
|
||||
if (fetching.current) return;
|
||||
@@ -104,6 +110,28 @@ export default function SttVideosPage() {
|
||||
setBatchSubmitting(false);
|
||||
}
|
||||
}
|
||||
async function saveTags(clear = false) {
|
||||
setSavingTags(true);
|
||||
try {
|
||||
const response = await fetch("/api/asr/tags", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
awemeIds: [...selected],
|
||||
contentTags: clear ? [] : [tag],
|
||||
mode: clear ? "set" : "add",
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error("保存手动标签失败");
|
||||
setNotice(`已更新 ${selected.size} 个视频的手动标签`);
|
||||
setSelected(new Set());
|
||||
await fetchVideos();
|
||||
} catch (err) {
|
||||
setNotice(err instanceof Error ? err.message : "保存失败");
|
||||
} finally {
|
||||
setSavingTags(false);
|
||||
}
|
||||
}
|
||||
const count = (status: TranscriptionStatus) =>
|
||||
videos.filter((v) => v.transcription.status === status).length;
|
||||
const eligible = videos.filter(
|
||||
@@ -111,7 +139,9 @@ export default function SttVideosPage() {
|
||||
v.transcription.transcript === null && !isActive(v.transcription.status),
|
||||
).length;
|
||||
const shown = videos.filter(
|
||||
(v) => filter === "all" || v.transcription.status === filter,
|
||||
(v) =>
|
||||
(filter === "all" || v.transcription.status === filter) &&
|
||||
(!tagFilter || v.content_tags.includes(tagFilter)),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -155,10 +185,70 @@ export default function SttVideosPage() {
|
||||
{batchSubmitting ? "正在加入队列…" : `批量补转 (${eligible})`}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-700">
|
||||
<select
|
||||
aria-label="筛选手动标签"
|
||||
value={tagFilter}
|
||||
onChange={(e) => {
|
||||
setTagFilter(e.target.value);
|
||||
setSelected(new Set());
|
||||
}}
|
||||
className="rounded border p-2"
|
||||
>
|
||||
<option value="">全部手动标签</option>
|
||||
{VIDEO_CONTENT_TAGS.map((value) => (
|
||||
<option key={value}>{value}</option>
|
||||
))}
|
||||
</select>
|
||||
<span>已选 {selected.size} 个(最多 200 个)</span>
|
||||
<select
|
||||
aria-label="批量添加的标签"
|
||||
value={tag}
|
||||
onChange={(e) => setTag(e.target.value)}
|
||||
className="rounded border p-2"
|
||||
>
|
||||
{VIDEO_CONTENT_TAGS.map((value) => (
|
||||
<option key={value}>{value}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!selected.size || savingTags}
|
||||
onClick={() => saveTags()}
|
||||
className="rounded border px-3 py-2 disabled:opacity-50"
|
||||
>
|
||||
添加手动标签
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!selected.size || savingTags}
|
||||
onClick={() => saveTags(true)}
|
||||
className="rounded border px-3 py-2 disabled:opacity-50"
|
||||
>
|
||||
清空手动标签
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded border border-gray-200 bg-white">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-gray-100 text-gray-600">
|
||||
<tr>
|
||||
<th className="p-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="选择当前列表前200个视频"
|
||||
checked={
|
||||
shown.length > 0 &&
|
||||
shown.slice(0, 200).every((v) => selected.has(v.aweme_id))
|
||||
}
|
||||
onChange={(e) =>
|
||||
setSelected(
|
||||
e.target.checked
|
||||
? new Set(shown.slice(0, 200).map((v) => v.aweme_id))
|
||||
: new Set(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</th>
|
||||
<th className="p-3">视频</th>
|
||||
<th className="p-3">时长</th>
|
||||
<th className="p-3">状态与文字</th>
|
||||
@@ -175,6 +265,24 @@ export default function SttVideosPage() {
|
||||
key={video.aweme_id}
|
||||
className="border-t border-gray-100 align-top"
|
||||
>
|
||||
<td className="p-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`选择视频 ${video.aweme_id}`}
|
||||
checked={selected.has(video.aweme_id)}
|
||||
disabled={
|
||||
!selected.has(video.aweme_id) && selected.size >= 200
|
||||
}
|
||||
onChange={(e) =>
|
||||
setSelected((previous) => {
|
||||
const next = new Set(previous);
|
||||
if (e.target.checked) next.add(video.aweme_id);
|
||||
else next.delete(video.aweme_id);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<div className="flex max-w-sm gap-3">
|
||||
{video.cover_url && (
|
||||
@@ -197,6 +305,9 @@ export default function SttVideosPage() {
|
||||
<p className="mt-1 text-gray-500">
|
||||
@{video.author.nickname}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-blue-700">
|
||||
{video.content_tags.join("、")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user