> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rumik.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# silk mulberry 1.5

> our faster, description-driven text-to-speech voice.

export const VoiceSample = ({tone, label, note, description, text, src}) => {
  const TONE_PILL = {
    happy: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-300",
    excited: "bg-orange-100 text-orange-700 dark:bg-orange-500/10 dark:text-orange-300",
    sad: "bg-sky-100 text-sky-700 dark:bg-sky-500/10 dark:text-sky-300",
    whisper: "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/10 dark:text-indigo-300",
    angry: "bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-300",
    neutral: "bg-gray-100 text-gray-600 dark:bg-white/10 dark:text-gray-300"
  };
  const PLAY_EVENT = "voicesample:play";
  const fmtTime = s => {
    if (!s || !isFinite(s)) return "0:00";
    const m = Math.floor(s / 60);
    const sec = Math.floor(s % 60).toString().padStart(2, "0");
    return m + ":" + sec;
  };
  const audioRef = React.useRef(null);
  const idRef = React.useRef(null);
  if (idRef.current === null) idRef.current = Math.random();
  const [playing, setPlaying] = React.useState(false);
  const [cur, setCur] = React.useState(0);
  const [dur, setDur] = React.useState(0);
  const [copied, setCopied] = React.useState(false);
  React.useEffect(() => {
    const onOther = e => {
      if (e.detail !== idRef.current && audioRef.current) {
        audioRef.current.pause();
        setPlaying(false);
      }
    };
    window.addEventListener(PLAY_EVENT, onOther);
    return () => window.removeEventListener(PLAY_EVENT, onOther);
  }, []);
  const toggle = () => {
    const a = audioRef.current;
    if (!a) return;
    if (playing) {
      a.pause();
      setPlaying(false);
    } else {
      window.dispatchEvent(new CustomEvent(PLAY_EVENT, {
        detail: idRef.current
      }));
      a.play();
      setPlaying(true);
    }
  };
  const copyValue = description ? "description: " + description + "\ntext: " + text : text;
  const copy = () => {
    if (navigator.clipboard) navigator.clipboard.writeText(copyValue);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  };
  const seekTo = (clientX, el) => {
    const a = audioRef.current;
    if (!a || !dur) return;
    const rect = el.getBoundingClientRect();
    const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
    a.currentTime = ratio * dur;
    setCur(a.currentTime);
  };
  const pct = dur ? cur / dur * 100 : 0;
  const pill = label ? TONE_PILL.neutral : TONE_PILL[tone] || TONE_PILL.neutral;
  const pillLabel = label || "[" + (tone || "neutral") + "]";
  const lbl = "text-gray-400 dark:text-gray-500";
  return <div className="not-prose my-3 flex flex-col gap-3 rounded-2xl border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/[0.03]">
      <div className="flex items-start justify-between gap-3">
        <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
          <span className={"rounded-md px-2 py-0.5 font-mono text-xs font-semibold " + pill}>
            {pillLabel}
          </span>
          {note && <span className="text-xs text-gray-500 dark:text-gray-400">{note}</span>}
        </div>
        <button type="button" onClick={copy} aria-label={copied ? "Copied" : "Copy prompt"} className="flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-xs text-gray-500 transition hover:bg-gray-100 hover:text-gray-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200">
          {copied ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <path d="M20 6 9 17l-5-5" />
            </svg> : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <rect x="9" y="9" width="13" height="13" rx="2" />
              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
            </svg>}
          {copied ? "copied" : "copy"}
        </button>
      </div>

      <div className="overflow-hidden rounded-lg bg-gray-50 px-3 py-2.5 font-mono text-xs leading-relaxed break-words whitespace-pre-wrap text-gray-700 dark:bg-white/5 dark:text-gray-300">
        {description ? <>
            <span className={lbl}>description:</span> {description}
            {"\n"}
            <span className={lbl}>text:</span> {text}
          </> : text}
      </div>

      <audio ref={audioRef} src={src} preload="metadata" onLoadedMetadata={e => setDur(e.currentTarget.duration)} onTimeUpdate={e => setCur(e.currentTarget.currentTime)} onEnded={() => {
    setPlaying(false);
    setCur(0);
    if (audioRef.current) audioRef.current.currentTime = 0;
  }} />

      <div className="flex items-center gap-3">
        <button type="button" onClick={toggle} aria-label={playing ? "Pause" : "Play " + (label || tone || "voice") + " sample"} className="flex size-9 shrink-0 items-center justify-center rounded-full bg-gray-900 text-white transition hover:bg-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 focus-visible:ring-offset-2 dark:bg-white dark:text-gray-900 dark:focus-visible:ring-white dark:focus-visible:ring-offset-gray-900">
          {playing ? <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
              <rect x="6" y="5" width="4" height="14" rx="1" />
              <rect x="14" y="5" width="4" height="14" rx="1" />
            </svg> : <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
              <path d="M8 5v14l11-7z" />
            </svg>}
        </button>

        <div role="slider" aria-label="Seek" aria-valuemin={0} aria-valuemax={Math.round(dur) || 0} aria-valuenow={Math.round(cur)} tabIndex={0} onClick={e => seekTo(e.clientX, e.currentTarget)} onKeyDown={e => {
    const a = audioRef.current;
    if (!a || !dur) return;
    if (e.key === "ArrowRight") {
      a.currentTime = Math.min(dur, a.currentTime + 2);
      setCur(a.currentTime);
    } else if (e.key === "ArrowLeft") {
      a.currentTime = Math.max(0, a.currentTime - 2);
      setCur(a.currentTime);
    }
  }} className="relative h-1.5 flex-1 cursor-pointer rounded-full bg-gray-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 dark:bg-white/15">
          <div className="absolute inset-y-0 left-0 rounded-full bg-gray-900 dark:bg-white" style={{
    width: pct + "%"
  }} />
        </div>

        <span className="shrink-0 font-mono text-xs tabular-nums text-gray-500 dark:text-gray-400">
          {fmtTime(cur)} / {fmtTime(dur)}
        </span>
      </div>
    </div>;
};

**silk mulberry 1.5** is our faster voice. you describe how it should sound in one
natural sentence, then give it your text. it streams, so it's a good fit for
low-latency voice agents.

<VoiceSample label="podcast host" description="a female 30s hindi voice, normal pitch, smooth timbre, conversational pacing, energetic, casual register, like a podcast host." text="आज का episode थोड़ा अलग है। एक minute के लिए सीधा बैठ जाओ।" src="/audio/mulberry/podcast-host.mp3" />

## at a glance

| field    | value                                                                    |
| -------- | ------------------------------------------------------------------------ |
| model id | `mulberry`                                                               |
| language | hindi in devanagari, english in latin (code-mixed)                       |
| best for | low-latency narration, voice agents, and creative voices                 |
| steering | a natural-language `description`, optionally pinned to a named `speaker` |
| length   | up to 2000 characters per request                                        |

## quickstart

one call, one `.wav` file:

```bash theme={null}
curl -X POST https://silk-api.rumik.ai/v1/tts \
  -H "Authorization: Bearer rk_live_•••••••••" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mulberry",
    "text": "आज का episode थोड़ा अलग है।",
    "description": "a female 30s hindi voice, smooth timbre, conversational pacing, casual register, like a podcast host"
  }' \
  --output mulberry.wav
```

put the voice in `description` and your spoken text in `text`. the full request
schema is in the [API reference](/api-reference).

## preset voices

`description` is what builds the voice, and it's always required. leave `speaker`
out and mulberry generates a voice to match the description you wrote. add
`speaker` to pin one of twelve named voices instead:

| voice                                                   | gender |
| ------------------------------------------------------- | ------ |
| `emma` `mia` `sophia` `ava` `ira` `siya` `aisha` `zoya` | female |
| `lucas` `noah` `theo` `adam`                            | male   |

send `description` alongside it, exactly as you would without a speaker:

```bash theme={null}
curl -X POST https://silk-api.rumik.ai/v1/tts \
  -H "Authorization: Bearer rk_live_•••••••••" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mulberry",
    "text": "आज का episode थोड़ा अलग है।",
    "description": "a female 30s hindi voice, smooth timbre, conversational pacing, casual register, like a podcast host",
    "speaker": "siya"
  }' \
  --output siya.wav
```

names are case-insensitive. send a name we don't know and you get a voice built
from your `description`, not an error, so a typo sounds like the wrong voice
rather than failing loudly.

<Note>
  **deprecating soon.** the old numbered preset values still work for now and map
  to `ira`, `siya`, `aisha` and `zoya`, in that order. they're going away, so move
  to the names above.
</Note>

## parameters

| field                | default | notes                                                                          |
| -------------------- | ------- | ------------------------------------------------------------------------------ |
| `text`               | n/a     | required. up to 2000 characters.                                               |
| `description`        | n/a     | required. natural-language voice description.                                  |
| `speaker`            | n/a     | optional. a named voice, e.g. `siya`. still send `description`.                |
| `temperature`        | `0.6`   | sampling temperature.                                                          |
| `top_p`              | `0.95`  | nucleus sampling.                                                              |
| `top_k`              | `50`    | top-k sampling.                                                                |
| `repetition_penalty` | `1.2`   | penalise repeated tokens.                                                      |
| `max_new_tokens`     | `2048`  | output length cap. if a long line comes back cut off, raise it (up to `8192`). |

<Note>
  **getting truncated audio?** if the returned speech stops before the end of your
  text, the generation hit the token cap. raise `max_new_tokens` above the default
  `2048`, up to a maximum of `8192`.
</Note>

→ to write good descriptions, read the [prompting guide](/prompting-mulberry).
