fix: finalize Phase 3 V1 minimum and worker recovery
This commit is contained in:
parent
9bf98522af
commit
d9a491ef43
@ -96,7 +96,7 @@ export default function TtsPage() {
|
||||
{!loggedIn ? <div className="mt-8"><StatusCard title="请先登录" description="登录后可使用真实任务、额度和历史功能。"><Link className="button-primary mt-5" href="/login">前往登录 <ArrowRight /></Link></StatusCard></div> : <>
|
||||
<div className="mt-8 grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
|
||||
<section className="rounded-2xl border border-line bg-panel/60 p-5 sm:p-7" aria-labelledby="tts-input-title"><div className="flex items-center justify-between gap-4"><div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle">01 / Input</p><h2 className="mt-2 text-xl font-semibold" id="tts-input-title">输入文本</h2></div><span className="text-xs text-subtle">{normalizeText(text).length} / 当前计划限制</span></div><textarea className="field-input mt-6 min-h-64 resize-y leading-7" onChange={(event) => setText(event.target.value)} placeholder="输入需要转换的文本。" value={text} /><div className="mt-5 flex flex-col gap-4 border-t border-line pt-5 sm:flex-row sm:items-center sm:justify-between"><p className="text-sm text-muted">可用额度:{usage?.available.toLocaleString() ?? "加载中"}</p><button className="button-primary" disabled={busy || !csrf} onClick={createTask} type="button">{busy ? "生成中…" : "生成语音"} <ArrowRight /></button></div></section>
|
||||
<aside className="space-y-6"><section className="rounded-2xl border border-line bg-panel/60 p-5 sm:p-7" aria-labelledby="tts-settings-title"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle">02 / Parameters</p><h2 className="mt-2 text-xl font-semibold" id="tts-settings-title">声音参数</h2><div className="mt-6 space-y-5"><div><label className="field-label" htmlFor="voice">音色</label><select className="field-input" id="voice" onChange={(event) => setVoice(event.target.value)} value={voice}><option value="">选择音色</option>{voices.map((item) => <option key={item.id} value={item.provider_voice_id}>{item.name}{item.language ? ` · ${item.language}` : ""}</option>)}</select></div><div><label className="field-label" htmlFor="speed">语速</label><select className="field-input" id="speed" onChange={(event) => setSpeed(event.target.value)} value={speed}><option value="0.5">0.5x</option><option value="0.8">0.8x</option><option value="1">1.0x</option><option value="1.2">1.2x</option><option value="1.5">1.5x</option><option value="2">2.0x</option></select></div><div><label className="field-label" htmlFor="format">格式</label><select className="field-input" id="format" onChange={(event) => setFormat(event.target.value)} value={format}><option value="wav">WAV</option><option value="mp3">MP3</option></select></div></div></section><StatusCard title="额度" description={`${usage?.plan?.toUpperCase() ?? ""} · 已用 ${usage?.used.toLocaleString() ?? "-"} · 冻结 ${usage?.reserved.toLocaleString() ?? "-"}`} tone="info" /></aside>
|
||||
<aside className="space-y-6"><section className="rounded-2xl border border-line bg-panel/60 p-5 sm:p-7" aria-labelledby="tts-settings-title"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle">02 / Parameters</p><h2 className="mt-2 text-xl font-semibold" id="tts-settings-title">声音参数</h2><div className="mt-6 space-y-5"><div><label className="field-label" htmlFor="voice">音色</label><select className="field-input" id="voice" onChange={(event) => setVoice(event.target.value)} value={voice}><option value="">选择音色</option>{voices.map((item) => <option key={item.id} value={item.provider_voice_id}>{item.name}{item.language ? ` · ${item.language}` : ""}</option>)}</select></div><div><span className="field-label">语速</span><div className="field-input text-muted">1.0x(V1 已验证范围)</div></div><div><span className="field-label">格式</span><div className="field-input text-muted">WAV(V1 已验证格式)</div></div></div></section><StatusCard title="额度" description={`${usage?.plan?.toUpperCase() ?? ""} · 已用 ${usage?.used.toLocaleString() ?? "-"} · 冻结 ${usage?.reserved.toLocaleString() ?? "-"}`} tone="info" /></aside>
|
||||
</div>
|
||||
{error ? <p className="mt-6 rounded-xl border border-danger/30 bg-danger/10 px-4 py-3 text-sm leading-6 text-danger" role="alert">{error}</p> : null}
|
||||
{activeTask?.status === "succeeded" ? <section className="mt-8 rounded-2xl border border-success/30 bg-success/5 p-5 sm:p-7"><div className="flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between"><div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-success">03 / Result</p><h2 className="mt-2 text-xl font-semibold">生成完成</h2><p className="mt-2 text-sm text-muted">任务 {activeTask.id}</p></div><a className="button-secondary" download href={`${apiBase}/tts/tasks/${activeTask.id}/download`}>下载 {format.toUpperCase()}</a></div><audio className="mt-6 w-full" controls src={`${apiBase}/tts/tasks/${activeTask.id}/audio`} /></section> : null}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
| 鉴权 | 配置有 API Key 时发送 `Authorization: Bearer <key>`;默认 Key 为空 | Key 只存旧工程服务端 SQLite,配置接口返回掩码 |
|
||||
| 输入文本字段 | `input` | 旧工程将业务文本映射为 `input` |
|
||||
| 音色 ID/语言 | `voice`;默认 `default`,逗号分隔配置;无真实语言清单 | 旧工程 `tts_config` 和前端 `/api/config` |
|
||||
| 参数范围 | `response_format`: `wav`/`mp3`;`speed`: `0.5` 至 `2.0`;文本 `1` 至 `5000` 字符 | `server.js` 服务端校验 |
|
||||
| 参数范围 | 旧客户端代码支持 `response_format`: `wav`/`mp3`、`speed`: `0.5` 至 `2.0`;文本 `1` 至 `5000` 字符 | 旧工程 `server.js` 校验;真实上游目前只实测 WAV、speed=1 |
|
||||
| 模型 | 默认 `qwen3-tts`,可由旧工程管理员配置 | `ReadMe.md`、`tts_config` |
|
||||
| 同步/异步 | 同步 | 单次 `fetch` 等待完整响应,无 task ID |
|
||||
| 结果格式 | 音频字节流;WAV `audio/wav`,MP3 `audio/mpeg` | 旧工程返回完整 `arrayBuffer` |
|
||||
@ -23,6 +23,16 @@
|
||||
|
||||
代码契约已核实;首次无鉴权请求返回 `401 Unauthorized`,配置服务端凭据后最小文本请求已返回 `200 audio/wav`。当前测试服务器没有 TTS 上游监听端口,业务 API 通过固定环境地址访问外部上游。
|
||||
|
||||
### 1.1 V1 已验证最小能力
|
||||
|
||||
- `provider_voice_id=default`。
|
||||
- 同步 `POST` 生成。
|
||||
- `response_format=wav`。
|
||||
- `speed=1`。
|
||||
- 最小中文文本成功返回音频;业务任务、私有保存、历史、回放、下载和额度消费已走通一次。
|
||||
|
||||
以下能力仍不能写成已通过:MP3、非 1.0 语速、其他音色/语言、provider task ID、异步查询、取消、上游幂等和安全重试。
|
||||
|
||||
旧工程自身的业务接口为:登录后 `GET /api/config` 获取模型/音色元数据,`POST /api/tts` 同步返回音频,`GET /api/history` 返回当前用户最多 100 条历史;管理员配置接口为 `/api/admin/config`。Phase 3 只复用上游边界,不复制旧工程用户体系或整套工程。
|
||||
|
||||
`www_site` 不复用旧工程的网页端管理员配置接口。上游主机、API Key、超时和相关秘密只由服务端环境配置注入;网站用户和管理员均不能通过网页修改上游连接配置。
|
||||
@ -46,7 +56,7 @@
|
||||
|
||||
### 2.2 创建任务请求
|
||||
|
||||
下面是脱敏的目标样例,仅用于说明业务层字段;字段名、音色和参数必须在上游联调后固定。
|
||||
下面是当前业务层请求样例;V1 最小实现只允许 `default`、WAV 和 `speed=1`。字段名和服务端规则已经固定,上游高级参数不在本版默认开放。
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@ -65,6 +65,15 @@
|
||||
- 部署失败或版本核对不一致时停止切换,不以 HTTP `200` 作为版本生效的唯一证据。
|
||||
- 当前 Phase 1 已用版本标识临时验证;统一归档部署脚本、原子切换和自动核对列为后续部署基础设施任务。
|
||||
|
||||
### D-08 旧 Web、推理服务和 Phase 3 存储边界
|
||||
|
||||
- `KT26-0903_Big-TTS` 是旧 Web 应用,不作为新站运行时依赖;当前测试服务器未发现其 SQLite、进程、容器或监听端口。
|
||||
- 新网站是用户、权限、额度、历史和 TTS 操作的唯一入口。旧工程不迁移用户、历史、音频或配置;当前测试环境未发现可迁移的旧运行数据。正式退役前仍需由旧系统负责人完成源端备份和替代验证确认。
|
||||
- 底层推理服务保持独立,由 `services/api` 通过服务端环境配置调用;旧 Web 退役不等于停止推理服务。
|
||||
- Phase 3 V1 最小契约固定为代码已验证的 `default` 音色、同步生成、WAV、服务端 Unicode 码点计量和 `speed=1`。旧客户端声明的 MP3、可变语速、其他音色不视为已验证能力。
|
||||
- 测试阶段音频使用 `/home/flym/kaotings-audio` 私有本地目录,不把它写成已部署对象存储;持久卷、容量、保留期、清理和备份恢复仍是上线阻断项。
|
||||
- 当前上游为公网 HTTP 地址,正式上线前必须改为 HTTPS 或受控加密通道,并限制来源;不修改只读授权的旧工程或上游服务器。
|
||||
|
||||
## 2. 尚未固定且不能虚构的事项
|
||||
|
||||
| 事项 | 当前处理 | 固定条件 |
|
||||
|
||||
@ -69,7 +69,7 @@ Phase 1 当前可在本地继续开发,也可在测试机仅部署官网骨架
|
||||
|
||||
进入条件:Phase 2 认证/归属/权益可用,且获得真实上游联调权限和测试额度。
|
||||
|
||||
当前状态:任务、Worker、额度失败释放、历史和受控音频接口已部署;真实成功联调被 TTS API Key 阻断,详见 `docs/phase-3-report.md` 和 `docs/TTS_UPSTREAM_REQUEST.md`。
|
||||
当前状态:任务、Worker、额度失败释放、历史、回放、下载和真实成功联调已完成;剩余工作集中于 Worker 异常矩阵、存储恢复、上游网络安全和旧 Web 退役前备份确认,详见 `docs/phase-3-report.md`。
|
||||
|
||||
- `P3-01` 依据 `API_CONTRACT.md` 建立音色、任务、音频模型和上游适配器。
|
||||
- `P3-02` 实现幂等、文本校验、限流、并发控制和额度冻结事务。
|
||||
|
||||
@ -44,6 +44,10 @@
|
||||
| 历史查询 | `1` 条记录 |
|
||||
| 额度 | `10000 -> 9998`,已用 `2`,冻结 `0` |
|
||||
|
||||
计量补测结果:服务端使用 NFC 规范化并将 `CRLF/CR` 统一为 `LF`,空白和标点计入;空白文本拒绝。隔离账户逐项提交结果为:中文 `2`、英文含空格 `3`、换行文本 `3`、emoji `2`、组合字符 `2`;空白文本返回 `422`。前端使用同一 NFC/换行口径展示计数。
|
||||
|
||||
并发和幂等补测:活动任务占用唯一并发槽时后续请求返回 `409 CONCURRENCY_LIMIT`;同用户同幂等键同请求返回同一任务;同键不同文本返回 `409`。
|
||||
|
||||
成功音频路径已通过最小文本验证;并发争用、Worker 重启、存储失败和上游超时矩阵仍需补测。
|
||||
|
||||
## 3. P3 工作包状态
|
||||
@ -64,3 +68,12 @@
|
||||
- 旧工程同步返回音频,没有 provider task ID、异步查询或幂等能力;业务 Worker 不能假设上游可安全重试。
|
||||
|
||||
凭据补齐后,必须重新执行成功生成、音频校验、私有回放/下载、重复请求、上游超时、存储失败、Worker 重启和额度争用测试。凭据只进入服务器 `/etc/kaotings/api.env`,不进入 Git、前端构建或日志。
|
||||
|
||||
## 5. 部署边界和上线阻断
|
||||
|
||||
- 当前 3000/443 只由新网站 Web、API 和 Caddy 服务提供;未发现旧 Big-TTS Web 进程、容器、SQLite 数据库或监听端口。
|
||||
- 当前未执行旧账户、历史、音频或配置迁移;测试机未发现旧运行数据。旧 Web 退役前仍需源端负责人提供备份证明和替代验收确认。
|
||||
- 底层推理服务继续由新 API 调用,旧 Web 退役不停止上游。
|
||||
- 测试音频目录为 `/home/flym/kaotings-audio`,是私有本地目录,不是已部署对象存储;容量限制、保留期、清理和备份恢复尚未完成。
|
||||
- 当前上游为公网 HTTP,正式上线前需要 HTTPS 或受控加密通道、来源限制和外部网络复测。
|
||||
- Worker 已增加原子领取、5 分钟租约、过期 running 释放、迟到结果拒绝、一次性结算和启动孤立文件清理;并发 Worker 仍固定为 systemd 单进程,数据库锁为跨进程安全边界。
|
||||
|
||||
@ -399,8 +399,8 @@ def create_tts_task(payload: TtsTaskRequest, request: Request, connection: Conne
|
||||
parameters = dict(payload.parameters)
|
||||
response_format = str(parameters.get("format", "wav")).lower()
|
||||
speed = float(parameters.get("speed", 1.0))
|
||||
if response_format not in {"wav", "mp3"} or not 0.5 <= speed <= 2:
|
||||
raise error("INVALID_PARAMETERS", "音频格式或语速不可用", 422)
|
||||
if response_format != "wav" or speed != 1:
|
||||
raise error("UNVERIFIED_PARAMETERS", "V1 当前只支持 WAV 和 1.0x 语速", 422)
|
||||
voice = connection.execute("SELECT * FROM tts_voices WHERE provider_voice_id = %s AND enabled = true", (payload.voice_id,)).fetchone()
|
||||
if not voice or plan not in (voice["allowed_plans"] or ["free", "vip"]):
|
||||
raise error("VOICE_NOT_ALLOWED", "音色不可用", 422)
|
||||
|
||||
3
services/api/migrations/004_v1_tts_minimum.sql
Normal file
3
services/api/migrations/004_v1_tts_minimum.sql
Normal file
@ -0,0 +1,3 @@
|
||||
UPDATE tts_voices
|
||||
SET supported_parameters = '{"format":["wav"],"speed":{"fixed":1}}'::jsonb
|
||||
WHERE provider_voice_id = 'default';
|
||||
Loading…
Reference in New Issue
Block a user