'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import BackButton from '@/app/components/BackButton'; type VideoTranscript = { id: string; speech_detected: boolean; language: string | null; audio_type: string | null; transcript: string | null; non_speech_summary: string | null; }; type Video = { aweme_id: string; desc: string; cover_url: string; duration_ms: number; author: { nickname: string; }; transcript: VideoTranscript | null; }; type ApiResponse = { videos: Video[]; total: number; }; export default function SttVideosPage() { const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [transcribing, setTranscribing] = useState>(new Set()); const [filter, setFilter] = useState<'all' | 'transcribed' | 'pending'>('all'); const [batchTranscribing, setBatchTranscribing] = useState(false); const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0 }); useEffect(() => { fetchVideos(); }, []); const fetchVideos = async () => { try { setLoading(true); const response = await fetch('/api/admin/stt/videos'); if (!response.ok) { 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'); } finally { setLoading(false); } }; // 简单的 sleep 工具 const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); type PollOptions = { intervalMs?: number; maxAttempts?: number; }; // 轮询某个视频的转写状态直至完成或超时 const pollVideoUntilTranscribed = async ( awemeId: string, { intervalMs = 2000, maxAttempts = 30 }: PollOptions = {} ): Promise => { for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const resp = await fetch('/api/admin/stt/videos'); if (resp.ok) { const data: ApiResponse = await resp.json(); setVideos(data.videos); const v = data.videos.find((x) => x.aweme_id === awemeId); if (v && v.transcript) { return true; } } } catch (e) { // 忽略网络错误,继续轮询 } await sleep(intervalMs); } return false; }; const handleTranscribe = async (awemeId: string) => { 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'); } // 轮询直到该视频转写完成或超时,避免后端异步导致立即刷新拿不到最新状态 const done = await pollVideoUntilTranscribed(awemeId); if (!done) { // 兜底刷新一次 await fetchVideos(); } } catch (err) { alert(err instanceof Error ? err.message : 'Transcription failed'); } finally { setTranscribing(prev => { const next = new Set(prev); next.delete(awemeId); return next; }); } }; const handleBatchTranscribe = async () => { const pendingVideos = videos.filter(v => v.transcript === null); if (pendingVideos.length === 0) { alert('没有待转写的视频'); return; } if (!confirm(`确定要转写 ${pendingVideos.length} 个视频吗?这可能需要较长时间。`)) { return; } setBatchTranscribing(true); setBatchProgress({ current: 0, total: pendingVideos.length }); const pollers: Promise[] = []; 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)); try { const response = await fetch(`/api/stt?awemeId=${video.aweme_id}`); if (!response.ok) { console.error(`Failed to transcribe video ${video.aweme_id}`); } // 为该视频启动后台轮询,不阻塞批处理的顺序执行 const p = pollVideoUntilTranscribed(video.aweme_id).finally(() => { setTranscribing(prev => { const next = new Set(prev); next.delete(video.aweme_id); return next; }); }); pollers.push(p); } catch (err) { console.error(`Error transcribing video ${video.aweme_id}:`, err); } } // 等待所有轮询结束(完成或超时),再进行最终刷新 if (pollers.length > 0) { await Promise.allSettled(pollers); } await fetchVideos(); setBatchTranscribing(false); setBatchProgress({ current: 0, total: 0 }); alert('批量转写完成!'); }; 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, }; if (loading) { return (

加载中...

); } if (error) { return (

错误: {error}

); } return (
{/* Header */}

视频转写管理

管理视频的语音转写状态

{/* Stats */}
总视频数
{stats.total}
已转写
{stats.transcribed}
待转写
{stats.pending}
{/* Filter */}
{batchTranscribing && (
正在转写: {batchProgress.current} / {batchProgress.total}
)}
{/* Video List */}
{filteredVideos.map((video) => ( ))}
视频 时长 转写状态 操作
{video.desc || '无描述'}

@{video.author.nickname}

{Math.floor(video.duration_ms / 1000)}s {video.transcript ? (
已转写 {video.transcript.language && ( {video.transcript.language} )}
{video.transcript.speech_detected ? (
{video.transcript.transcript}
) : (
{video.transcript.audio_type || '非语音'} {video.transcript.non_speech_summary && ( - {video.transcript.non_speech_summary} )}
)}
) : ( 待转写 )}
{video.transcript ? ( ) : ( )}
{filteredVideos.length === 0 && (
没有找到视频
)}
); }