ลดความหน่วง
สิ่งที่ API ทำระหว่างรับคำขอเสียงจนส่งเสียงแรก ขั้นตอนใดที่ไคลเอนต์ควบคุมได้ และแต่ละทางเลือกมีต้นทุนอะไร
ระหว่างรับคำขอเสียงจนส่งเสียงแรก API ตรวจคีย์ คิดเครดิตของคำขอ เตรียมข้อความ และสังเคราะห์ช่วงต้นของข้อความ
curl -N -s -o /dev/null -X POST https://api.paxalabs.com/v1/tts \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "stream": true}' \
-w "connect %{time_connect}s, tls %{time_appconnect}s, first byte %{time_starttransfer}s\n"async function firstByteMs(text: string) {
const started = performance.now();
const response = await fetch("https://api.paxalabs.com/v1/tts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text,
voice: "khanomkrok",
model: "paxa-tts-flash-v2",
stream: true,
}),
});
if (!response.ok || !response.body) {
throw new Error(`TTS failed: ${response.status}`);
}
const reader = response.body.getReader();
await reader.read();
const elapsed = performance.now() - started;
// Read to the end. The client closes a connection with an unread body.
while (!(await reader.read()).done);
return elapsed;
}
// The first call includes the TCP and TLS handshakes.
// The second call reuses the connection.
console.log(await firstByteMs("สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs"));
console.log(await firstByteMs("สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs"));import os
import time
import requests
# One session keeps the connection open between requests.
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['PAXA_API_KEY']}"
def first_byte_ms(text):
started = time.perf_counter()
with session.post(
"https://api.paxalabs.com/v1/tts",
json={"text": text, "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "stream": True},
stream=True,
) as response:
response.raise_for_status()
chunks = response.iter_content(chunk_size=None)
next(chunks)
elapsed = (time.perf_counter() - started) * 1000
# Read to the end. The client closes a connection with an unread body.
for _ in chunks:
pass
return elapsed
# The first call includes the TCP and TLS handshakes.
# The second call reuses the connection.
print(first_byte_ms("สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs"))
print(first_byte_ms("สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs"))สิ่งที่ทำก่อนสังเคราะห์#
API แคชคีย์ที่ตรวจแล้วไว้ 5 นาที คำขอที่มาถึงหลังแคชหมดอายุจึงต้องค้นฐานข้อมูลเพิ่มหนึ่งครั้ง API ยังแคชยอดเครดิตของบัญชีไว้ 60 นาทีหลังคำขอที่คิดเครดิตแต่ละครั้ง เมื่อยอดเครดิตในแคชมีอย่างน้อย 4 เท่าของค่าบริการของคำขอ การสังเคราะห์จะเริ่มพร้อมกับการคิดเครดิต
การคิดเครดิตยังคงเสร็จก่อนที่ API จะส่งไบต์แรก และคำขอที่ยอดเครดิตไม่พอตอบ 402 insufficient_credits โดยไม่มีเสียง ในสี่กรณีต่อไปนี้ API คิดเครดิตให้เสร็จก่อนจึงเริ่มสังเคราะห์ เวลารอจึงเพิ่มขึ้นเท่ากับเวลาของการคิดเครดิต
- ยอดเครดิตในแคชต่ำกว่า 4 เท่าของค่าบริการของคำขอ
- บัญชีไม่มีคำขอที่คิดเครดิตในช่วง 60 นาทีที่ผ่านมา จึงไม่มียอดเครดิตในแคช
- คีย์มีวงเงินเครดิต ให้ใช้คีย์ที่ไม่มีวงเงินกับคำขอที่เสียงแรกสำคัญ และตั้งวงเงินไว้กับคีย์สำหรับงานแบตช์
- คำขอมี Idempotency-Key ให้ส่งคีย์กับคำขอที่จะส่งซ้ำเมื่อล้มเหลว และละคีย์ได้ในรอบสนทนาที่จะพูดใหม่โดยไม่ส่งซ้ำ
เตรียมแคชก่อนรอบสนทนาแรก#
GET /v1/me ทำให้ API แคชคีย์และเปิดการเชื่อมต่อ แต่คำขอนี้ไม่คิดเครดิต ยอดเครดิตจึงยังไม่ถูกแคช มีเพียงคำขอที่คิดเครดิตเท่านั้นที่ทำให้ยอดเครดิตถูกแคช หากต้องการให้แคชทั้งสองพร้อมก่อนคำตอบแรกของสาย ให้สังเคราะห์หนึ่งคำตอนเริ่มสาย คำขอแรกที่ข้ามขั้นตอนนี้ต้องรอการค้นคีย์ และรอการคิดเครดิตที่ต้องเสร็จก่อนเริ่มสังเคราะห์
แบ่งข้อความยาว#
การตอบกลับแบบสตรีมเริ่มจากช่วงต้นของข้อความระหว่างที่ส่วนที่เหลือยังสังเคราะห์อยู่ เสียงแรกจึงแทบไม่ขึ้นกับความยาว ยกเว้นขั้นตอนเตรียมข้อความ ซึ่งทำกับข้อความทั้งหมดก่อนเริ่มสังเคราะห์ และใช้เวลาเพิ่มตามสัดส่วนของความยาว เมื่อเสียงแรกสำคัญและข้อความยาว ให้ส่งย่อหน้าแรกเป็นคำขอแยก แล้วต่อคิวส่วนที่เหลือตามหลัง
อ่านสตรีมเร็วเท่าที่เครือข่ายส่งได้#
คำขอแบบสตรีมถือช่องคำขอพร้อมกันไว้จนการตอบกลับจบ และการสังเคราะห์เร็วกว่าการเล่นเสียงหลายเท่า ไคลเอนต์ที่อ่านด้วยความเร็วของการเล่นจึงถือช่องไว้นานเท่าความยาวของเสียง ให้อ่านเนื้อหาเร็วเท่าที่เครือข่ายส่งได้แล้วบัฟเฟอร์ไว้ฝั่งคุณ ช่องจะว่างสำหรับคำขอถัดไป
เลือกระหว่าง HTTP กับการเชื่อมต่อสด#
การเชื่อมต่อสดยืนยันตัวตนครั้งเดียวแล้วส่งเฉพาะเฟรม text แต่การคิดเครดิตของการสังเคราะห์แต่ละครั้งต้องเสร็จก่อนการสังเคราะห์ครั้งนั้นจะเริ่ม สำหรับประโยคที่ครบแล้วหนึ่งประโยค คำขอ HTTP แบบสตรีมบนการเชื่อมต่อที่เปิดอยู่ทำงานฝั่งเซิร์ฟเวอร์น้อยกว่า การเชื่อมต่อสดเร็วกว่าเมื่อข้อความทยอยมาจากโมเดลภาษา และเมื่อทางเลือกอีกทางคือการเปิดการเชื่อมต่อใหม่ทุกรอบสนทนา ให้เปิดตั้งแต่เริ่มบทสนทนา เมื่อผู้ใช้พูดแทรกให้ส่ง cancel แล้วคงการเชื่อมต่อไว้ เพราะการเชื่อมต่อที่ปิดไปต้องอัปเกรดใหม่ทั้งหมด
รูปแบบเสียงและไทม์สแตมป์#
wav และ pcm ไม่ผ่านตัวเข้ารหัส mp3 เพิ่มการเข้ารหัสช่วงสั้น และ opus เพิ่มการเข้ารหัสที่นานที่สุด ทุกแบบใช้เวลาน้อยเมื่อเทียบกับการสังเคราะห์ การตอบกลับ wav แบบสตรีมคือ PCM 16 บิต little-endian โมโนที่ 24 kHz ต่อจากเฮดเดอร์ 44 ไบต์ ซึ่งฟิลด์ขนาดทั้งสองเป็น 0xFFFFFFFF ส่วน pcm ส่งแซมเปิลชุดเดียวกันโดยไม่มีเฮดเดอร์ ไปป์ไลน์ที่รับ PCM ดิบส่งแซมเปิลต่อได้ทันที ไม่ต้องมีตัวถอดรหัสและไม่ต้องตัดอะไรทิ้ง
timestamps ไม่ทำให้เสียงแรกช้าลง แต่เปลี่ยนการตอบกลับแบบสตรีมเป็นอีเวนต์ NDJSON ที่บรรจุเสียงเป็น base64 ซึ่งใหญ่ขึ้นราวหนึ่งในสามและส่งโดยไม่บีบอัด ส่วนการตอบกลับแบบไฟล์เต็มที่มีไทม์สแตมป์จะถูกบีบอัดด้วย gzip เมื่อไคลเอนต์รับ gzip ซึ่งลดส่วนที่เพิ่มขึ้นนั้นได้เกือบทั้งหมด
การรับส่งข้อมูล#
โฮสต์ของ API รองรับ HTTP/2 และ HTTP/3 การเชื่อมต่อ HTTP/2 หนึ่งเส้นรับส่งคำขอพร้อมกันได้ถึงจำนวนคำขอพร้อมกันของแพ็กเกจ จึงไม่ต้องมี connection pool การทิ้งการตอบกลับก่อนอ่านจบจะปิดการเชื่อมต่อ HTTP/1.1 ส่วนบน HTTP/2 จะรีเซ็ตเฉพาะสตรีมของคำขอนั้น การตอบกลับ preflight มี access-control-max-age 24 ชั่วโมง
การส่งซ้ำ#
API ไม่เข้าคิวคำขอ คำขอที่เกินโควตารายนาทีหรือเกินจำนวนคำขอพร้อมกันตอบ 429 ทันที และคำขอที่ถูกปฏิเสธยังนับรวมในโควตา ไคลเอนต์ที่ส่งซ้ำต่อเนื่องขณะได้ 429 จึงยืดเวลาที่ตัวเองถูกจำกัด เมื่อได้ rate_limited ให้รอตาม retry-after เมื่อได้ concurrency_limited ให้รอจนคำขอของคุณเองจบหนึ่งคำขอ