优化图文页
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import BackButton from '@/app/components/BackButton';
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import BackButton from "@/app/components/BackButton";
|
||||
|
||||
type VideoTranscript = {
|
||||
id: string;
|
||||
@@ -34,7 +34,9 @@ export default function SttVideosPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [transcribing, setTranscribing] = useState<Set<string>>(new Set());
|
||||
const [filter, setFilter] = useState<'all' | 'transcribed' | 'pending'>('all');
|
||||
const [filter, setFilter] = useState<"all" | "transcribed" | "pending">(
|
||||
"all",
|
||||
);
|
||||
const [batchTranscribing, setBatchTranscribing] = useState(false);
|
||||
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0 });
|
||||
|
||||
@@ -45,21 +47,22 @@ export default function SttVideosPage() {
|
||||
const fetchVideos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/admin/stt/videos');
|
||||
const response = await fetch("/api/admin/stt/videos");
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch videos');
|
||||
throw new Error("Failed to fetch videos");
|
||||
}
|
||||
const data: ApiResponse = await response.json();
|
||||
setVideos(data.videos);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
setError(err instanceof Error ? err.message : "Unknown error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 简单的 sleep 工具
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const sleep = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
type PollOptions = {
|
||||
intervalMs?: number;
|
||||
@@ -69,11 +72,11 @@ export default function SttVideosPage() {
|
||||
// 轮询某个视频的转写状态直至完成或超时
|
||||
const pollVideoUntilTranscribed = async (
|
||||
awemeId: string,
|
||||
{ intervalMs = 2000, maxAttempts = 30 }: PollOptions = {}
|
||||
{ intervalMs = 2000, maxAttempts = 30 }: PollOptions = {},
|
||||
): Promise<boolean> => {
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
const resp = await fetch('/api/admin/stt/videos');
|
||||
const resp = await fetch("/api/admin/stt/videos");
|
||||
if (resp.ok) {
|
||||
const data: ApiResponse = await resp.json();
|
||||
setVideos(data.videos);
|
||||
@@ -91,11 +94,11 @@ export default function SttVideosPage() {
|
||||
};
|
||||
|
||||
const handleTranscribe = async (awemeId: string) => {
|
||||
setTranscribing(prev => new Set(prev).add(awemeId));
|
||||
setTranscribing((prev) => new Set(prev).add(awemeId));
|
||||
try {
|
||||
const response = await fetch(`/api/stt?awemeId=${awemeId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to transcribe video');
|
||||
throw new Error("Failed to transcribe video");
|
||||
}
|
||||
// 轮询直到该视频转写完成或超时,避免后端异步导致立即刷新拿不到最新状态
|
||||
const done = await pollVideoUntilTranscribed(awemeId);
|
||||
@@ -104,9 +107,9 @@ export default function SttVideosPage() {
|
||||
await fetchVideos();
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Transcription failed');
|
||||
alert(err instanceof Error ? err.message : "Transcription failed");
|
||||
} finally {
|
||||
setTranscribing(prev => {
|
||||
setTranscribing((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(awemeId);
|
||||
return next;
|
||||
@@ -115,13 +118,17 @@ export default function SttVideosPage() {
|
||||
};
|
||||
|
||||
const handleBatchTranscribe = async () => {
|
||||
const pendingVideos = videos.filter(v => v.transcript === null);
|
||||
const pendingVideos = videos.filter((v) => v.transcript === null);
|
||||
if (pendingVideos.length === 0) {
|
||||
alert('没有待转写的视频');
|
||||
alert("没有待转写的视频");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`确定要转写 ${pendingVideos.length} 个视频吗?这可能需要较长时间。`)) {
|
||||
if (
|
||||
!confirm(
|
||||
`确定要转写 ${pendingVideos.length} 个视频吗?这可能需要较长时间。`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +139,7 @@ export default function SttVideosPage() {
|
||||
for (let i = 0; i < pendingVideos.length; i++) {
|
||||
const video = pendingVideos[i];
|
||||
setBatchProgress({ current: i + 1, total: pendingVideos.length });
|
||||
setTranscribing(prev => new Set(prev).add(video.aweme_id));
|
||||
setTranscribing((prev) => new Set(prev).add(video.aweme_id));
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/stt?awemeId=${video.aweme_id}`);
|
||||
@@ -141,7 +148,7 @@ export default function SttVideosPage() {
|
||||
}
|
||||
// 为该视频启动后台轮询,不阻塞批处理的顺序执行
|
||||
const p = pollVideoUntilTranscribed(video.aweme_id).finally(() => {
|
||||
setTranscribing(prev => {
|
||||
setTranscribing((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(video.aweme_id);
|
||||
return next;
|
||||
@@ -160,19 +167,19 @@ export default function SttVideosPage() {
|
||||
await fetchVideos();
|
||||
setBatchTranscribing(false);
|
||||
setBatchProgress({ current: 0, total: 0 });
|
||||
alert('批量转写完成!');
|
||||
alert("批量转写完成!");
|
||||
};
|
||||
|
||||
const filteredVideos = videos.filter(v => {
|
||||
if (filter === 'transcribed') return v.transcript !== null;
|
||||
if (filter === 'pending') return v.transcript === null;
|
||||
const filteredVideos = videos.filter((v) => {
|
||||
if (filter === "transcribed") return v.transcript !== null;
|
||||
if (filter === "pending") return v.transcript === null;
|
||||
return true;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: videos.length,
|
||||
transcribed: videos.filter(v => v.transcript !== null).length,
|
||||
pending: videos.filter(v => v.transcript === null).length,
|
||||
transcribed: videos.filter((v) => v.transcript !== null).length,
|
||||
pending: videos.filter((v) => v.transcript === null).length,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -212,7 +219,9 @@ export default function SttVideosPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<BackButton />
|
||||
<h1 className="text-3xl font-bold text-gray-900 mt-4">视频转写管理</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mt-4">
|
||||
视频转写管理
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">管理视频的语音转写状态</p>
|
||||
</div>
|
||||
|
||||
@@ -220,15 +229,21 @@ export default function SttVideosPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="text-sm text-gray-600">总视频数</div>
|
||||
<div className="text-3xl font-bold text-gray-900 mt-2">{stats.total}</div>
|
||||
<div className="text-3xl font-bold text-gray-900 mt-2">
|
||||
{stats.total}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="text-sm text-gray-600">已转写</div>
|
||||
<div className="text-3xl font-bold text-green-600 mt-2">{stats.transcribed}</div>
|
||||
<div className="text-3xl font-bold text-green-600 mt-2">
|
||||
{stats.transcribed}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="text-sm text-gray-600">待转写</div>
|
||||
<div className="text-3xl font-bold text-orange-600 mt-2">{stats.pending}</div>
|
||||
<div className="text-3xl font-bold text-orange-600 mt-2">
|
||||
{stats.pending}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -237,31 +252,31 @@ export default function SttVideosPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
onClick={() => setFilter("all")}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
filter === 'all'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
filter === "all"
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
全部 ({stats.total})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('transcribed')}
|
||||
onClick={() => setFilter("transcribed")}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
filter === 'transcribed'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
filter === "transcribed"
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
已转写 ({stats.transcribed})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('pending')}
|
||||
onClick={() => setFilter("pending")}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
filter === 'pending'
|
||||
? 'bg-orange-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
filter === "pending"
|
||||
? "bg-orange-600 text-white"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
待转写 ({stats.pending})
|
||||
@@ -284,7 +299,7 @@ export default function SttVideosPage() {
|
||||
批量转写中...
|
||||
</span>
|
||||
) : (
|
||||
'全部转写'
|
||||
"全部转写"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -326,7 +341,7 @@ export default function SttVideosPage() {
|
||||
href={`/aweme/${video.aweme_id}`}
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-800 line-clamp-2"
|
||||
>
|
||||
{video.desc || '无描述'}
|
||||
{video.desc || "无描述"}
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
@{video.author.nickname}
|
||||
@@ -357,7 +372,7 @@ export default function SttVideosPage() {
|
||||
) : (
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">
|
||||
{video.transcript.audio_type || '非语音'}
|
||||
{video.transcript.audio_type || "非语音"}
|
||||
</span>
|
||||
{video.transcript.non_speech_summary && (
|
||||
<span className="ml-1">
|
||||
@@ -386,7 +401,7 @@ export default function SttVideosPage() {
|
||||
重新转写...
|
||||
</span>
|
||||
) : (
|
||||
'重新转写'
|
||||
"重新转写"
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
@@ -401,7 +416,7 @@ export default function SttVideosPage() {
|
||||
转写中...
|
||||
</span>
|
||||
) : (
|
||||
'开始转写'
|
||||
"开始转写"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@@ -413,9 +428,7 @@ export default function SttVideosPage() {
|
||||
</div>
|
||||
|
||||
{filteredVideos.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
没有找到视频
|
||||
</div>
|
||||
<div className="text-center py-12 text-gray-500">没有找到视频</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user