บทสนทนาหลายผู้พูด
POST /v1/tts/dialogue พูดได้สูงสุด 100 บรรทัด แต่ละบรรทัดใช้เสียงของตัวเอง ออกมาเป็นไฟล์เสียงเดียวพร้อมช่วงเงียบระหว่างบรรทัด คิดค่าบริการครั้งเดียวทั้งสคริปต์ และเมื่อล้มเหลวจะคิดเฉพาะบรรทัดที่ส่งครบแล้ว
ส่งบรรทัดของบทสนทนาไปที่ POST /v1/tts/dialogue โดยกำหนดเสียงให้แต่ละบรรทัด แล้วรับไฟล์เสียงเดียวของบทสนทนาทั้งหมด แต่ละบรรทัดใช้กฎข้อความเดียวกับ POST /v1/tts และใช้แท็กที่เสียงนั้นอ่านได้ โดยไม่มีเพดานความยาวของตัวเอง คำขอหนึ่งมีได้สูงสุด 100 บรรทัด รวมกันไม่เกิน 20,000 ตัวอักษร
curl -X POST https://api.paxalabs.com/v1/tts/dialogue \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "paxa-tts-flash-v2",
"lines": [
{
"text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs",
"voice": "khanomkrok"
},
{
"text": "สวัสดีค่ะ วันนี้ให้ช่วยเรื่องอะไรดีคะ",
"voice": "nomyen"
}
]
}' \
--output dialogue.ndjsonimport { writeFile } from "node:fs/promises";
import { createInterface } from "node:readline";
import { Readable } from "node:stream";
const response = await fetch("https://api.paxalabs.com/v1/tts/dialogue", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"model": "paxa-tts-flash-v2",
"lines": [
{
"text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs",
"voice": "khanomkrok"
},
{
"text": "สวัสดีค่ะ วันนี้ให้ช่วยเรื่องอะไรดีคะ",
"voice": "nomyen"
}
]
}),
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const audio: Buffer[][] = [];
let failure;
for await (const line of createInterface({ input: Readable.fromWeb(response.body!) })) {
if (!line) continue;
const event = JSON.parse(line);
if (event.type === "error") failure = event;
if (event.type === "chunk" && event.audio) {
(audio[event.line] ??= []).push(Buffer.from(event.audio, "base64"));
}
}
const kept = failure ? audio.slice(0, failure.lines.length) : audio;
await writeFile("dialogue.mp3", Buffer.concat(kept.flat()));
if (failure) throw new Error(failure.error.title);import base64
import json
import os
import requests
response = requests.post(
"https://api.paxalabs.com/v1/tts/dialogue",
headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"},
json={
"model": "paxa-tts-flash-v2",
"lines": [
{
"text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs",
"voice": "khanomkrok",
},
{
"text": "สวัสดีค่ะ วันนี้ให้ช่วยเรื่องอะไรดีคะ",
"voice": "nomyen",
},
],
},
stream=True,
)
response.raise_for_status()
audio = {}
failure = None
for line in response.iter_lines():
if not line:
continue
event = json.loads(line)
if event["type"] == "error":
failure = event
if event["type"] == "chunk" and event.get("audio"):
audio.setdefault(event["line"], []).append(base64.b64decode(event["audio"]))
kept = len(failure["lines"]) if failure else len(audio)
with open("dialogue.mp3", "wb") as f:
for index in range(kept):
f.write(b"".join(audio.get(index, [])))
if failure:
raise RuntimeError(failure["error"]["title"])อ่านสตรีม#
คำตอบสตรีมเป็น NDJSON เสมอ อีเวนต์ chunk แต่ละตัวระบุ line และมีเสียงแบบ base64 เมื่อถอดรหัสทุก chunk ตามลำดับที่มาถึงจะได้ไฟล์เดียวใน format ที่ขอ บรรทัดต่าง ๆ สังเคราะห์พร้อมกันและส่งตามลำดับ จึงเริ่มเล่นได้ตั้งแต่ chunk แรก อีเวนต์ done ตัวสุดท้ายบอก start และ end ของแต่ละบรรทัดเป็นวินาที สำหรับทำคำบรรยายหรือแถบเลื่อนเวลา
{"type":"chunk","line":0,"audio":"//NkxAAaOAAAAAABLBQAAAAAAABVVVVV..."}
{"type":"chunk","line":1,"audio":"//NkxAAaOAAAAAABLBQAAAAAAABVVVVV..."}
{"type":"done","lines":[{"start":0,"end":2.35},{"start":2.65,"end":4.9}]}บทสนทนาแบบ WAV เปิดด้วยเฮดเดอร์เดียวที่ช่องขนาดเป็น 0xFFFFFFFF เพราะรู้ความยาวได้เมื่อจบเท่านั้น เครื่องเล่นจะอ่านไฟล์แบบนี้จนจบไฟล์
ช่วงเงียบระหว่างบรรทัด#
gap_ms กำหนดความเงียบระหว่างบรรทัดหนึ่งกับบรรทัดถัดไป ตั้งแต่ 0 ถึง 5,000 ms ค่าเริ่มต้น 300 ms และไม่เปลี่ยนค่าบริการ Opus ปัดช่วงเงียบเป็นทีละ 20 ms และ MP3 ปัดเป็นทีละหนึ่งเฟรม หากต้องการหยุดกลางบรรทัด ให้ใช้แท็กหยุด
ค่าบริการและความล้มเหลว#
- คิดค่าบริการครั้งเดียวทั้งคำขอ จากตัวอักษรของทุกบรรทัดรวมกันตามอัตราของโมเดล โดยมีขั้นต่ำ 0.1 เครดิตครั้งเดียว บรรทัดที่ใช้เสียงที่ออกแบบเองคิดตามอัตราของเสียงนั้น และ
timestampsมีผลกับทุกบรรทัด - ทุกบรรทัดถูกตรวจก่อนคิดค่าบริการ เนื้อหาปัญหาของคำขอที่ถูกปฏิเสธมี
lineคือลำดับของบรรทัดที่ผิด เช่น 400 tag_invalid จะบอกว่าต้องแก้บรรทัดไหน - ความล้มเหลวหลังคิดค่าบริการแต่ก่อนส่งเสียงใดๆ จะตอบปัญหาที่เนื้อหามี
lineและคืนค่าบริการทั้งหมด - ความล้มเหลวหลังเริ่มส่งเสียงแล้วจะจบสตรีมด้วยอีเวนต์
errorซึ่งlinesระบุบรรทัดที่ส่งครบแล้ว คือ chunk ที่lineน้อยกว่าจำนวนนั้น และcreditsคือค่าบริการของบรรทัดเหล่านั้นเมื่อคิดเป็นบทสนทนาของตัวเอง คำขอจะคิดค่าบริการเพียงเท่านั้นและคืนส่วนที่เหลือ ส่วนสตรีมที่อ่านช้ากว่าที่ระบบสร้างเสียงอาจใช้เวลาเกินงบเวลาของคำขอ สตรีมจะจบโดยไม่มีอีเวนต์doneและยังคิดค่าบริการ เหมือนกรณีที่ไคลเอนต์ตัดการเชื่อมต่อเอง - บทสนทนาหนึ่งนับเป็นหนึ่งคำขอในหน้าต่างอัตรา และถือหนึ่งช่องคำขอพร้อมกันฝั่งเสียงพูดระหว่างทำงาน Idempotency-Key ทำงานเหมือนใน POST /v1/tts