google/gemini-3.8-flash-lite-tts/text-to-speech0.03크레딧부터 /초2026년 9월 출시된 구글의 Gemini 3.8 Flash-Lite TTS는 3.8 Flash TTS보다 더 빠르고 저렴한 형제 모델입니다. 스크립트를 보내고 30가지 음성 중 하나를 선택하면 보통 몇 초 안에 WAV 파일을 받을 수 있습니다.
POST /jobs/createTask에서 모델 이름을 google/gemini-3.8-flash-lite-tts/text-to-speech(으)로 지정하면 호출할 수 있습니다. 0.03크레딧/초부터(US$0.0003). 새 계정에는 30크레딧이 지급되니 Playground에서 먼저 무료로 써 보세요.
- 텍스트 음성 변환
- 목소리 30종
google/gemini-3.8-flash-lite-tts/text-to-speechPOST https://api.popnow.ai/api/v1/jobs/createTask# 1. Create the task. Keep the Idempotency-Key if you retry after a network error.
export IDEMPOTENCY_KEY="$(uuidgen)"
curl -X POST https://api.popnow.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"model": "google/gemini-3.8-flash-lite-tts/text-to-speech",
"input": {
"text": "Welcome back. Your files finished rendering a minute ago and are ready to download.",
"voice_id": "Kore"
}
}'
# 2. Copy data.taskId from the response, then poll until state is succeeded, failed, canceled or expired.
TASK_ID="TASK_ID_FROM_RESPONSE"
curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $POPNOW_API_KEY"// Node 18+ (or any runtime with fetch). Keep the API key on your server.
const BASE = "https://api.popnow.ai/api/v1";
const headers = { Authorization: `Bearer ${process.env.POPNOW_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function call(path, init = {}) {
const res = await fetch(BASE + path, { ...init, headers: { ...headers, ...init.headers } });
const body = await res.json();
if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);
return body.data;
}
const { taskId } = await call("/jobs/createTask", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
"model": "google/gemini-3.8-flash-lite-tts/text-to-speech",
"input": {
"text": "Welcome back. Your files finished rendering a minute ago and are ready to download.",
"voice_id": "Kore"
}
}),
});
// Poll with backoff. A finished task may still be copying files: wait while an asset is pending.
let task;
for (let delay = 2000; ; delay = Math.min(delay * 1.5, 15000)) {
await sleep(delay);
task = await call(`/jobs/recordInfo?taskId=${taskId}`);
const copying = (task.output?.assets ?? []).some((a) => a.pending && !a.unavailable);
if (["succeeded", "failed", "canceled", "expired"].includes(task.state) && !copying) break;
}
if (task.state !== "succeeded") throw new Error(`${task.state}: ${task.errorMessage ?? "no detail"}`);
// Each asset has a signed url (no API key needed) that expires at expiresAt: save the file.
for (const asset of task.output.assets) console.log(asset.url);import os, time, uuid, requests
BASE = "https://api.popnow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['POPNOW_API_KEY']}"}
def call(method, path, **kwargs):
body = requests.request(method, BASE + path, headers={**HEADERS, **kwargs.pop("headers", {})}, timeout=30, **kwargs).json()
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]}')
return body["data"]
task_id = call(
"POST",
"/jobs/createTask",
headers={"Idempotency-Key": str(uuid.uuid4())}, # reuse it if you retry
json={
"model": "google/gemini-3.8-flash-lite-tts/text-to-speech",
"input": {
"text": "Welcome back. Your files finished rendering a minute ago and are ready to download.",
"voice_id": "Kore"
}
},
)["taskId"]
# Poll with backoff. A finished task may still be copying files: wait while an asset is pending.
delay = 2.0
while True:
time.sleep(delay)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
assets = (task.get("output") or {}).get("assets") or []
copying = any(a.get("pending") and not a.get("unavailable") for a in assets)
if task["state"] in ("succeeded", "failed", "canceled", "expired") and not copying:
break
delay = min(delay * 1.5, 15.0)
if task["state"] != "succeeded":
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
# Each asset has a signed url (no API key needed) that expires at expiresAt: save the file.
for asset in assets:
print(asset["url"])input은 아래 스키마로 만들었기 때문에 그대로 검증을 통과합니다. 프롬프트와 링크만 내 것으로 바꾸세요.
가격
| 옵션 | 크레딧 /초 | ≈ USD |
|---|---|---|
| 기본 | 0.03 | US$0.0003 |
1크레딧 = $0.01 · 모든 옵션과 가격: GET /models/google/gemini-3.8-flash-lite-tts/text-to-speech
제한
- 텍스트 길이(글자)
- 5,000
- 목소리
- 30
파라미터
이 필드들은 모든 요청을 검사할 때 쓰는 것과 같은 스키마에서 나옵니다. 목록에 없는 필드는 거부됩니다.
일부 필드는 서로 연결되어 있습니다(예: 참조 자료가 최소 하나 필요). 정확한 규칙은 아래 원본 스키마에 있습니다.
textstring경우에 따라 필수The words to be spoken, exactly as written. Spell out sounds such as sighs or laughter; bracketed cues may be read aloud.
dialoguearray<object>경우에 따라 필수A scripted conversation, used instead of text: each line names a speaker from speakers.
내부 필드
dialogue[].textstring필수The line, exactly as it should be spoken.
dialogue[].speakerstring필수A name from speakers.
speakersarray<object>경우에 따라 필수The cast for dialogue: one or two voices, each with a name the lines refer to.
내부 필드
speakers[].namestring필수Name the dialogue lines use for this speaker.
speakers[].voice_idstring필수Prebuilt voice for this speaker.
speakers[].pacestring선택Rhythm of this speaker's lines.
speakers[].stylestring선택Manner of this speaker's lines.
speakers[].accentstring선택English accent for this speaker.
speakers[].profilestring선택Short description of who is speaking; not read aloud.
voice_idstring선택사용 가능한 목소리 중에서 고릅니다.
stylestring선택미리 준비된 스타일이나 장르.
pacestring선택Rhythm of the reading. Omit it to keep the voice's own rhythm.
accentstring선택English accent used for the reading.
instructionsstring선택How to read the text: mood, distance from the microphone, who is listening. Shapes the delivery and is not spoken.
scenestring선택Where and to whom the words are spoken. Shapes the delivery and is not spoken.
temperaturenumber선택Variation between takes; higher is less predictable.
원본 JSON 스키마
{
"type": "object",
"allOf": [
{
"if": {
"required": [
"dialogue"
]
},
"then": {
"required": [
"speakers"
]
}
}
],
"anyOf": [
{
"required": [
"text"
]
},
{
"required": [
"dialogue"
]
}
],
"required": [],
"properties": {
"pace": {
"enum": [
"natural",
"rapid_fire",
"the_drift",
"staccato"
],
"type": "string",
"description": "Rhythm of the reading. Omit it to keep the voice's own rhythm."
},
"text": {
"type": "string",
"maxLength": 5000,
"minLength": 1,
"description": "The words to be spoken, exactly as written. Spell out sounds such as sighs or laughter; bracketed cues may be read aloud."
},
"scene": {
"type": "string",
"maxLength": 500,
"description": "Where and to whom the words are spoken. Shapes the delivery and is not spoken."
},
"style": {
"enum": [
"whisper",
"empathetic",
"vocal_smile",
"newscaster",
"promo_hype",
"deadpan"
],
"type": "string",
"description": "Overall manner of the reading. Omit it to use the voice's own default manner."
},
"accent": {
"enum": [
"neutral",
"american_general",
"american_valley",
"american_south",
"british_rp",
"british_brixton",
"transatlantic",
"australian"
],
"type": "string",
"description": "English accent used for the reading."
},
"dialogue": {
"type": "array",
"items": {
"type": "object",
"required": [
"speaker",
"text"
],
"properties": {
"text": {
"type": "string",
"maxLength": 250,
"minLength": 1,
"description": "The line, exactly as it should be spoken."
},
"speaker": {
"type": "string",
"maxLength": 40,
"minLength": 1,
"description": "A name from speakers."
}
},
"additionalProperties": false
},
"maxItems": 20,
"minItems": 1,
"description": "A scripted conversation, used instead of text: each line names a speaker from speakers."
},
"speakers": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"voice_id"
],
"properties": {
"name": {
"type": "string",
"maxLength": 40,
"minLength": 1,
"description": "Name the dialogue lines use for this speaker."
},
"pace": {
"enum": [
"natural",
"rapid_fire",
"the_drift",
"staccato"
],
"type": "string",
"description": "Rhythm of this speaker's lines."
},
"style": {
"enum": [
"whisper",
"empathetic",
"vocal_smile",
"newscaster",
"promo_hype",
"deadpan"
],
"type": "string",
"description": "Manner of this speaker's lines."
},
"accent": {
"enum": [
"neutral",
"american_general",
"american_valley",
"american_south",
"british_rp",
"british_brixton",
"transatlantic",
"australian"
],
"type": "string",
"description": "English accent for this speaker."
},
"profile": {
"type": "string",
"maxLength": 300,
"description": "Short description of who is speaking; not read aloud."
},
"voice_id": {
"enum": [
"Achernar",
"Achird",
"Algenib",
"Algieba",
"Alnilam",
"Aoede",
"Autonoe",
"Callirrhoe",
"Charon",
"Despina",
"Enceladus",
"Erinome",
"Fenrir",
"Gacrux",
"Iapetus",
"Kore",
"Laomedeia",
"Leda",
"Orus",
"Puck",
"Pulcherrima",
"Rasalgethi",
"Sadachbia",
"Sadaltager",
"Schedar",
"Sulafat",
"Umbriel",
"Vindemiatrix",
"Zephyr",
"Zubenelgenubi"
],
"type": "string",
"description": "Prebuilt voice for this speaker."
}
},
"additionalProperties": false
},
"maxItems": 2,
"minItems": 1,
"description": "The cast for dialogue: one or two voices, each with a name the lines refer to."
},
"voice_id": {
"enum": [
"Achernar",
"Achird",
"Algenib",
"Algieba",
"Alnilam",
"Aoede",
"Autonoe",
"Callirrhoe",
"Charon",
"Despina",
"Enceladus",
"Erinome",
"Fenrir",
"Gacrux",
"Iapetus",
"Kore",
"Laomedeia",
"Leda",
"Orus",
"Puck",
"Pulcherrima",
"Rasalgethi",
"Sadachbia",
"Sadaltager",
"Schedar",
"Sulafat",
"Umbriel",
"Vindemiatrix",
"Zephyr",
"Zubenelgenubi"
],
"type": "string",
"default": "Kore",
"description": "Prebuilt voice that reads the text. Each voice has a fixed character."
},
"temperature": {
"type": "number",
"maximum": 2,
"minimum": 0,
"description": "Variation between takes; higher is less predictable."
},
"instructions": {
"type": "string",
"maxLength": 1000,
"description": "How to read the text: mood, distance from the microphone, who is listening. Shapes the delivery and is not spoken."
}
},
"additionalProperties": false
}응답, 폴링, 콜백
모든 모델은 같은 방식으로 동작합니다. 작업을 만들고, 폴링하거나 콜백을 기다리세요.
먼저 견적 받기
curl -X POST https://api.popnow.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/gemini-3.8-flash-lite-tts/text-to-speech",
"input": {
"text": "Welcome back. Your files finished rendering a minute ago and are ready to download.",
"voice_id": "Kore"
}
}'본문은 createTask와 같습니다. 예상 청구액과 최대 청구액을 돌려주며, 아무것도 실행하지 않습니다.
접수됨
HTTP/1.1 202 Accepted
{
"code": 200,
"msg": "success",
"data": {
"taskId": "task_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued"
},
"request_id": "req_example_create"
}HTTP 202는 작업이 접수되었고 크레딧이 보류만 되었을 뿐 아직 차감되지 않았다는 뜻입니다. 최종 청구액은 보류액을 넘지 않습니다.
상태 확인
curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $POPNOW_API_KEY"간격을 조금씩 늘리며 몇 초마다 확인하고, state가 succeeded, failed, canceled, expired 중 하나가 되면 멈추세요.
완료된 작업
{
"code": 200,
"msg": "success",
"data": {
"taskId": "task_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "google/gemini-3.8-flash-lite-tts/text-to-speech",
"state": "succeeded",
"output": {
"assets": [
{
"url": "https://cdn.popnow.ai/tasks/task_01k3m8x9q2z4v7n5p6r8s0t1w3/output.mp3?signature=FROM_RESPONSE",
"expiresAt": "2026-10-17T12:20:00Z",
"mime": "audio/mpeg",
"durationSeconds": 30
}
]
},
"settled": true
},
"request_id": "req_example_record"
}expiresAt 전에 url에서 내려받으세요. 나중에 새 링크가 필요하면 POST /common/download-url을 호출하세요.
콜백 검증하기
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
// express.raw keeps the exact bytes that were signed; express.json would not match.
app.post("/hooks/popnow", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.header("X-Webhook-Timestamp");
const signature = req.header("X-Webhook-Signature") ?? "";
const body = JSON.parse(req.body.toString("utf8"));
const taskId = body.data?.taskId ?? body.task_id;
const digest = createHash("sha256").update(req.body).digest("hex");
const expected = createHmac("sha256", process.env.POPNOW_WEBHOOK_SECRET)
.update(`${taskId}.${timestamp}.${digest}`)
.digest("base64");
const ok = expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
// Check the signature first, then reject anything older than five minutes.
if (!ok || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end();
res.status(200).end(); // answer fast, then fetch the task and store its files
});callBackUrl을 설정하면 완료된 작업을 보내 드립니다. 믿기 전에 원본 요청 본문으로 서명을 확인하세요.
로컬 파일 보내기
curl -X POST https://api.popnow.ai/api/v1/common/upload-url \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contentType": "image/png", "bytes": 482113}'
# PUT the file bytes to data.uploadUrl with every header in data.headers, then commit it:
curl -X POST https://api.popnow.ai/api/v1/files/$FILE_ID/commit \
-H "Authorization: Bearer $POPNOW_API_KEY"
# The commit returns a popnow://f/... reference. Put it in any image, video or
# audio field of the task input instead of a public https URL.공개 https 링크는 그대로 쓸 수 있습니다. 로컬 파일은 먼저 upload-url과 commit을 거칩니다.

