Talk to your agent: speech in, speech out

Build a voice assistant on one Flux key: record a question, transcribe it, stream an answer from flux-auto, and speak it back sentence by sentence, with the cost of each turn.

A voice assistant is a loop of three calls on the same Flux key:

  1. Listen. Record a question and send it to speech to text.
  2. Think. Send the text to flux-auto and stream the answer.
  3. Speak. Send the answer to text to speech, a sentence at a time, and play each one as it arrives.
StepStatus
TranscribeLive
SpeakRolling out

Both speech steps need a paid plan. See Plans.

With your agent

If your agent already has the Flux tools server, it can run the loop on files without any code. Tools server status: transcribe Shipping, speak Shipping.

Built into Murage, which decides which abilities it uses. Nothing to set up here. See murage.ai.

For the whole loop, ask something like:

Transcribe question.m4a with the Flux transcribe tool, answer the question in two or three short sentences, then use the Flux speak tool to say your answer in the nova voice and save the audio as answer.mp3.

The speak tool returns the audio as base64, so the request has to say where to save it. It takes shorter text per call than the API; the agent can split a long answer and speak each part.

In your own app

The full loop below records from the microphone, transcribes, streams the answer from flux-auto, and speaks each sentence as soon as it is complete, so the first words play while the rest of the answer is still being written. It prints the cost of each turn from the receipt headers.

Python

Install the client and the audio libraries first: pip install openai sounddevice soundfile pyaudio.

python
import io
import re

import pyaudio
import sounddevice as sd
import soundfile as sf
from openai import OpenAI

client = OpenAI(api_key="sk-YOUR-FLUX-KEY", base_url="https://api.fluxrouter.ai/v1")
speaker = pyaudio.PyAudio().open(format=pyaudio.paInt16, channels=1, rate=24000, output=True)

RATE = 16000
SENTENCE_END = re.compile(r"(?<=[.!?])\s+")
history = [{"role": "system", "content": "You are a voice assistant. Answer in short, plain sentences."}]


def record(seconds: float = 5.0) -> io.BytesIO:
    input(f"Press Enter, then speak for {seconds:.0f} seconds...")
    audio = sd.rec(int(seconds * RATE), samplerate=RATE, channels=1, dtype="int16")
    sd.wait()
    buf = io.BytesIO()
    sf.write(buf, audio, RATE, format="WAV")
    buf.seek(0)
    buf.name = "question.wav"
    return buf


def speak(sentence: str) -> float:
    with client.audio.speech.with_streaming_response.create(
        model="flux-voice-speak",
        voice="nova",
        input=sentence,
        response_format="pcm",
    ) as response:
        cost = float(response.headers.get("x-flux-cost-usd", 0))
        for chunk in response.iter_bytes(chunk_size=4096):
            speaker.write(chunk)
    return cost


while True:
    # 1. Listen.
    raw = client.audio.transcriptions.with_raw_response.create(model="flux-voice", file=record())
    question = raw.parse().text
    turn_cost = float(raw.headers.get("x-flux-cost-usd", 0))
    print("you:", question)
    history.append({"role": "user", "content": question})

    # 2. Think, streamed. 3. Speak each sentence as soon as it is complete.
    answer, pending = "", ""
    stream = client.chat.completions.create(model="flux-auto", messages=history, stream=True)
    for chunk in stream:
        if not chunk.choices:
            continue
        pending += chunk.choices[0].delta.content or ""
        *done, pending = SENTENCE_END.split(pending)
        for sentence in done:
            answer += sentence + " "
            turn_cost += speak(sentence)
    if pending.strip():
        answer += pending
        turn_cost += speak(pending)

    history.append({"role": "assistant", "content": answer.strip()})
    print("agent:", answer.strip())
    print(f"speech cost this turn: ${turn_cost:.4f} (chat is on your usage page)")

TypeScript (Node)

This version reads the question from a file and plays the answer through ffplay, which comes with ffmpeg. Install the client with npm install openai.

ts
import { spawn } from "node:child_process";
import fs from "node:fs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: "sk-YOUR-FLUX-KEY",
  baseURL: "https://api.fluxrouter.ai/v1",
});

const SENTENCE_END = /(?<=[.!?])\s+/;

async function speak(sentence: string): Promise<number> {
  const res = await client.audio.speech.create({
    model: "flux-voice-speak",
    voice: "nova",
    input: sentence,
    response_format: "pcm",
  });
  const player = spawn(
    "ffplay",
    ["-f", "s16le", "-ar", "24000", "-ch_layout", "mono", "-nodisp", "-autoexit", "-loglevel", "quiet", "-"],
    { stdio: ["pipe", "ignore", "ignore"] },
  );
  const done = new Promise((resolve) => player.on("close", resolve));
  await pipeline(Readable.fromWeb(res.body as any), player.stdin);
  await done;
  return Number(res.headers.get("x-flux-cost-usd") ?? 0);
}

// 1. Listen.
const { data: heard, response: raw } = await client.audio.transcriptions
  .create({ model: "flux-voice", file: fs.createReadStream("question.m4a") })
  .withResponse();
let turnCost = Number(raw.headers.get("x-flux-cost-usd") ?? 0);
console.log("you:", heard.text);

// 2. Think, streamed. 3. Speak each sentence as soon as it is complete.
const stream = await client.chat.completions.create({
  model: "flux-auto",
  stream: true,
  messages: [
    { role: "system", content: "You are a voice assistant. Answer in short, plain sentences." },
    { role: "user", content: heard.text },
  ],
});

let pending = "";
for await (const chunk of stream) {
  pending += chunk.choices[0]?.delta?.content ?? "";
  const parts = pending.split(SENTENCE_END);
  pending = parts.pop() ?? "";
  for (const sentence of parts) turnCost += await speak(sentence);
}
if (pending.trim()) turnCost += await speak(pending);

console.log(`speech cost this turn: $${turnCost.toFixed(4)} (chat is on your usage page)`);

Keep your Flux key on the server. In a browser app, record in the browser, send the audio to your own server, and run these calls there.

Latency tips

  • Speak sentence by sentence. Waiting for the whole answer before speaking adds its full writing time to every turn. Splitting at sentence ends lets the first sentence play while the model is still writing the rest.
  • Ask for short sentences. A system prompt such as "Answer in short, plain sentences" gives the first sentence sooner, and short answers suit being heard.
  • Use pcm for playback. It is raw audio with no container, so each chunk can go straight to the speaker with nothing to decode.
  • Send short, compressed recordings. Speech to text bills a minimum of 10 seconds a file, and small files upload faster. Mono Opus or webm from a browser recorder is a good choice.
  • Leave flux-voice on automatic. It already favours speed for longer clips. Pin flux-voice-fast only if every clip is long.

What a turn costs

Each turn has three parts, each billed on its own:

StepBilled byWhere to read it
TranscribeSeconds of audio, at least 10 secondsx-flux-billed-seconds and x-flux-cost-usd on the response
ThinkTokens, at the flux-auto rateYour usage page; streamed chat responses do not carry a cost header. See Receipts.
SpeakCharacters of text spokenx-flux-billed-chars and x-flux-cost-usd on each speech response

Current prices: transcription $0.10 per minute heard; speech from $26 per 1M characters. Long answers cost more to speak, so asking for short answers keeps turns cheap as well as quick.

Speech and transcription share one hourly audio budget per account, 36,000 seconds of audio an hour. A busy voice app can reach it; see Speech to text problems and Text to speech problems for 429.

Next