From d82693b4fabd54f6d9c60a8b2a4d5b385e93f1a8 Mon Sep 17 00:00:00 2001 From: flym Date: Wed, 9 Sep 2026 00:43:56 +0800 Subject: [PATCH] feat: connect Phase 3 TTS workspace --- app/tts/page.tsx | 122 ++++++++++++++++++++++++++++++++++++---------- components/ui.tsx | 4 +- 2 files changed, 98 insertions(+), 28 deletions(-) diff --git a/app/tts/page.tsx b/app/tts/page.tsx index ab61b41..fa76f79 100644 --- a/app/tts/page.tsx +++ b/app/tts/page.tsx @@ -1,32 +1,102 @@ -import Link from "next/link"; -import { ArrowRight, WaveIcon } from "@/components/icons"; -import { DevPlaceholder, PreviewNotice, SectionHeading, StatusCard } from "@/components/ui"; +"use client"; -export const metadata = { title: "TTS 工作台" }; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { ArrowRight, WaveIcon } from "@/components/icons"; +import { PreviewNotice, SectionHeading, StatusCard } from "@/components/ui"; + +const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL ?? "/api/v1"; + +type Voice = { id: string; provider_voice_id: string; name: string; language?: string | null }; +type Usage = { available: number; used: number; reserved: number; plan: string }; +type Task = { id: string; status: string; text_length: number; voice_id: string; parameters: { format?: string; speed?: number }; error_code?: string | null; audio_available: boolean; created_at: string; finished_at?: string | null }; export default function TtsPage() { - return ( -
-
- - Phase 1 界面预览 -
-
当前不连接真实 TTS 服务,不保存文本,不显示虚假的生成成功状态。
+ const [csrf, setCsrf] = useState(""); + const [loggedIn, setLoggedIn] = useState(false); + const [voices, setVoices] = useState([]); + const [usage, setUsage] = useState(null); + const [tasks, setTasks] = useState([]); + const [text, setText] = useState(""); + const [voice, setVoice] = useState(""); + const [speed, setSpeed] = useState("1"); + const [format, setFormat] = useState("wav"); + const [activeTask, setActiveTask] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + async function loadWorkspace() { + const csrfResponse = await fetch(`${apiBase}/auth/csrf`, { credentials: "include" }); + const csrfBody = await csrfResponse.json(); + setCsrf(csrfBody.csrf_token ?? ""); + const meResponse = await fetch(`${apiBase}/auth/me`, { credentials: "include" }); + if (!meResponse.ok) { + setLoggedIn(false); + return; + } + setLoggedIn(true); + const [voiceResponse, usageResponse, taskResponse] = await Promise.all([ + fetch(`${apiBase}/tts/voices`, { credentials: "include" }), + fetch(`${apiBase}/account/usage`, { credentials: "include" }), + fetch(`${apiBase}/tts/tasks`, { credentials: "include" }), + ]); + const nextVoices = voiceResponse.ok ? await voiceResponse.json() : []; + setVoices(nextVoices); + setVoice((current) => current || nextVoices[0]?.provider_voice_id || ""); + if (usageResponse.ok) setUsage(await usageResponse.json()); + if (taskResponse.ok) setTasks(await taskResponse.json()); + } + + useEffect(() => { loadWorkspace().catch(() => setError("暂时无法连接 TTS 工作台")); }, []); + + async function pollTask(taskId: string) { + for (let attempt = 0; attempt < 180; attempt += 1) { + const response = await fetch(`${apiBase}/tts/tasks/${taskId}`, { credentials: "include" }); + if (!response.ok) throw new Error("无法读取任务状态"); + const nextTask: Task = await response.json(); + setActiveTask(nextTask); + if (["succeeded", "failed"].includes(nextTask.status)) return nextTask; + await new Promise((resolve) => window.setTimeout(resolve, 1000)); + } + throw new Error("任务等待超时"); + } + + async function createTask() { + setError(""); + if (!loggedIn) return; + if (!voice) return setError("当前没有可用音色,请先配置真实上游音色"); + if (!text.trim()) return setError("请输入需要转换的文本"); + setBusy(true); + try { + const response = await fetch(`${apiBase}/tts/tasks`, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf, "Idempotency-Key": crypto.randomUUID() }, + body: JSON.stringify({ text, voice_id: voice, parameters: { format, speed: Number(speed) } }), + }); + const body = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(body.error?.message ?? "任务创建失败"); + const result = await pollTask(body.id); + if (result.status === "failed") throw new Error(`任务失败:${result.error_code ?? "TASK_FAILED"}`); + await loadWorkspace(); + } catch (taskError) { + setError(taskError instanceof Error ? taskError.message : "任务失败"); + } finally { + setBusy(false); + } + } + + return
+
Phase 3
+
{loggedIn ? "当前使用测试数据库和真实任务 Worker。上游未配置时任务会明确失败并释放额度。" : "生成、历史、回放和下载需要登录。"}
+ {!loggedIn ?
前往登录
: <>
-
-

01 / Input

输入文本

0 / 待定字数
-