diff --git a/app/tts/page.tsx b/app/tts/page.tsx index abdd6c2..009adf6 100644 --- a/app/tts/page.tsx +++ b/app/tts/page.tsx @@ -27,8 +27,9 @@ export default function TtsPage() { const [speed, setSpeed] = useState("1"); const [format, setFormat] = useState("wav"); const [activeTask, setActiveTask] = useState(null); - const [nameDrafts, setNameDrafts] = useState>({}); const [renaming, setRenaming] = useState(false); + const [renameOpen, setRenameOpen] = useState(false); + const [renameDraft, setRenameDraft] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); @@ -51,7 +52,11 @@ export default function TtsPage() { setVoices(nextVoices); setVoice((current) => current || nextVoices[0]?.provider_voice_id || ""); if (usageResponse.ok) setUsage(await usageResponse.json()); - if (taskResponse.ok) setTasks(await taskResponse.json()); + if (taskResponse.ok) { + const taskData: Task[] = await taskResponse.json(); + setTasks(taskData); + if (!activeTask && taskData.length > 0 && taskData[0].status === "succeeded") setActiveTask(taskData[0]); + } } useEffect(() => { loadWorkspace().catch(() => setError("暂时无法连接 TTS 工作台")); }, []); @@ -93,9 +98,9 @@ export default function TtsPage() { } } - async function renameTask(taskId: string) { - const draft = (nameDrafts[taskId] ?? "").trim(); - if (!draft || renaming) return; + async function renameTask(taskId: string, rawName: string) { + const draft = rawName.trim(); + if (renaming) return; setRenaming(true); setError(""); try { @@ -107,7 +112,7 @@ export default function TtsPage() { }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(body.error?.message ?? "重命名失败"); - setNameDrafts((current) => ({ ...current, [taskId]: body.file_name })); + setRenameOpen(false); await loadWorkspace(); } catch (e) { setError(e instanceof Error ? e.message : "重命名失败"); @@ -125,8 +130,29 @@ export default function TtsPage() { {error ?

{error}

: null} - {activeTask?.status === "succeeded" ?

03 / Result

生成完成

任务 {activeTask.id}

下载 {format.toUpperCase()}

自定义文件名

setNameDrafts((current) => ({ ...current, [activeTask.id]: event.target.value }))} />
: null} -

04 / History

生成历史

{tasks.length} 条记录
{tasks.length ?
{tasks.map((task) =>

{task.status === "succeeded" ? "生成完成" : task.status === "failed" ? "生成失败" : "处理中"}

{task.voice_id} · {task.text_length} 字 · {new Date(task.created_at).toLocaleString()}{task.file_name ? ` · 文件名:${task.file_name}` : ""}

{task.audio_available ? : null}
)}
:

还没有生成记录

提交第一条任务后,状态和音频访问会出现在这里。

}
+ {activeTask?.status === "succeeded" ?

03 / Result

生成完成

{activeTask.file_name ?? "音频已就绪"}

下载 {format.toUpperCase()}
: null} +

04 / History

生成历史

{tasks.length} 条记录
{tasks.length ?
{tasks.map((task) =>

{task.status === "succeeded" ? "生成完成" : task.status === "failed" ? "生成失败" : "处理中"}

{task.status === "succeeded" ? (task.file_name ?? "音频已就绪") : `${task.voice_id} · ${task.text_length} 字`}

{new Date(task.created_at).toLocaleString()}

{task.audio_available ? : null}
)}
:

还没有生成记录

提交第一条任务后,状态和音频访问会出现在这里。

}
} + {renameOpen && activeTask ? ( +
+
+

重命名文件

+

确认后将使用新文件名下载;留空恢复自动命名。

+ setRenameDraft(event.target.value)} + onKeyDown={(event) => { if (event.key === "Enter") void renameTask(activeTask.id, renameDraft); }} + /> +
+ + +
+
+
+ ) : null} ; } diff --git a/services/api/app/main.py b/services/api/app/main.py index 42ecbfd..7cdd4f0 100644 --- a/services/api/app/main.py +++ b/services/api/app/main.py @@ -232,12 +232,17 @@ def normalize_tts_text(value: str) -> str: def task_view(connection: Connection, task: dict) -> dict[str, Any]: - audio = connection.execute("SELECT expires_at, status FROM audio_files WHERE task_id = %s", (task["id"],)).fetchone() + audio = connection.execute("SELECT expires_at, status, mime_type FROM audio_files WHERE task_id = %s", (task["id"],)).fetchone() + available = bool(audio and audio["status"] == "available") + file_name = task["file_name"] + if not file_name and available: + voice = connection.execute("SELECT name FROM tts_voices WHERE provider_voice_id = %s", (task["provider_voice_id"],)).fetchone() + file_name = tts_download_name(task, voice["name"] if voice else None, audio["mime_type"]) return { "id": task["id"], "status": task["status"], "text_length": task["text_length"], "voice_id": task["provider_voice_id"], "parameters": task["parameters"], - "error_code": task["error_code"], "audio_available": bool(audio and audio["status"] == "available"), - "audio_expires_at": audio["expires_at"] if audio else None, "file_name": task["file_name"], "created_at": task["created_at"], + "error_code": task["error_code"], "audio_available": available, + "audio_expires_at": audio["expires_at"] if audio else None, "file_name": file_name, "created_at": task["created_at"], "started_at": task["started_at"], "finished_at": task["finished_at"], } @@ -756,7 +761,7 @@ def tts_download_name(task: dict, voice_name: str | None, mime_type: str) -> str raw = re.sub(r'[\\/:*?"<>|]', "", raw) summary = summarize_text(raw) or "tts" voice = re.sub(r'[\\/:*?"<>|]', "", voice_name or "")[:16] or "voice" - date = task["created_at"].strftime("%Y%m%d") + date = task["created_at"].strftime("%Y%m%d_%H%M%S") return f"{summary}_{voice}_{date}.{ext}" @@ -799,11 +804,9 @@ def sanitize_file_name(value: str) -> str: def rename_tts_task(task_id: UUID, payload: TtsFileNameRequest, user: dict = Depends(current_user), connection: Connection = Depends(get_connection)): task = owned_task(task_id, user, connection) name = sanitize_file_name(payload.file_name) - if not name: - raise error("INVALID_FILE_NAME", "文件名不能为空或只包含非法字符", 422) - connection.execute("UPDATE tts_tasks SET file_name = %s WHERE id = %s", (name, task["id"])) + connection.execute("UPDATE tts_tasks SET file_name = %s WHERE id = %s", (name or None, task["id"])) connection.commit() - return {"file_name": name} + return {"file_name": name or None} USER_COLUMNS = "id, username, email, phone, role, plan, status, email_verified, phone_verified, created_at, last_login_at, updated_at" diff --git a/services/api/app/schemas.py b/services/api/app/schemas.py index 56188f1..720791f 100644 --- a/services/api/app/schemas.py +++ b/services/api/app/schemas.py @@ -190,7 +190,7 @@ class TtsSettingsTest(BaseModel): class TtsFileNameRequest(BaseModel): - file_name: str = Field(min_length=1, max_length=60) + file_name: str = Field(default="", max_length=60) class AdminTaskFilter(BaseModel):