---
title: "Añade TTS con Clonación de Voz a Mastra con Fish Audio"
description: "Integra Fish Audio S2.1 Pro en Mastra: clona tu voz, crea una herramienta fish_tts, añade etiquetas de emoción y dale a tus agentes narración real para flujos de vídeo y contenido."
date: 2026-07-29
categories: ["ai"]
tags: ["mastra","fish-audio","tts"]
---

import Button from "@components/widgets/Button.astro";
import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Accordion from "@components/widgets/Accordion.astro";
import Tabs from "@components/widgets/Tabs.astro";
import Tab from "@components/widgets/Tab.astro";

Ya tengo un [asistente Mastra](/build-ai-agent-mastra/) que busca en la web, lee archivos, ejecuta comandos de shell y recuerda el contexto. También añadí un [agente de imágenes con Kie.ai](/es/agente-imagen-mastra-kie-ai/) para portadas y miniaturas.

Lo que aún faltaba para el vídeo era la **voz**. El TTS genérico suena como cualquier otra demo de IA. Quería narración con **mi** voz — clonada una vez, reutilizada para siempre — con control de emociones para que los tutoriales técnicos no suenen planos.

Así que integré [Fish Audio](https://go.bitdoze.com/fish-audio) en Mastra como una sola herramienta: `fish_tts`. El agente escribe el guion (con etiquetas `[emotion]`), llama a la herramienta y obtiene un `.wav` en disco. Ese archivo entra directamente en HyperFrames para YouTube y Shorts.

Esta guía cubre el recorrido completo: clonar una voz, crear la herramienta, registrarla en un agente y usar etiquetas de emoción en la narración. El mismo patrón funciona en el repositorio [mastra-assistant](https://github.com/bitdoze/mastra-assistant) o en cualquier app Mastra existente.

Si aún no has usado Fish Audio, empieza con la [reseña de Fish Audio](/es/resena-fish-audio/) o el [tutorial de clonación en 2 minutos](/es/fish-audio-clonar-voz/). ¿Comparando opciones? Mira [Fish Audio vs ElevenLabs](/es/fish-audio-vs-elevenlabs/) y [Fish Audio vs MiniMax](/es/fish-audio-vs-minimax/).

<Button text="Prueba Fish Audio Gratis" link="https://go.bitdoze.com/fish-audio" variant="solid" color="blue" size="md" icon="arrow-right" />
<Button text="Crea Primero un Agente Mastra" link="/build-ai-agent-mastra/" variant="outline" color="purple" size="md" icon="arrow-right" />

<Notice type="info" title="Divulgación de afiliados">
Algunos enlaces a Fish Audio en este artículo son enlaces de afiliado (`go.bitdoze.com/fish-audio`). Uso el producto para la locución en mi propio pipeline de vídeo con Mastra. Los precios y los límites del plan gratuito cambian; verifica siempre en el sitio oficial y en [docs.fish.audio](https://docs.fish.audio/).
</Notice>


<YouTubeEmbed
  url="https://www.youtube.com/embed/1qnuGqTKGL0"
  label="Clona Tu Voz con Fish Audio y Mastra"
/>

## Por qué Fish Audio para el TTS del agente

La mayoría de las APIs de TTS te dan voces predefinidas. Bien para prototipos. Mal cuando el producto es *tu* canal y los espectadores esperan *tu* voz.

| Característica | Qué obtienes |
| --- | --- |
| Clonación de voz | ~15 segundos de audio de muestra → un ID de modelo de voz reutilizable |
| Calidad S2.1 Pro | TTS de producción con prosodia natural |
| Control de emociones | `[happy]`, `[excited]`, `[calm]`, indicaciones libres como `[warm and friendly]` |
| API HTTP simple | `POST https://api.fish.audio/v1/tts` → bytes de audio sin procesar (sin trabajo de sondeo largo) |
| Formatos | `wav`, `mp3`, `opus` — `wav` funciona sin fricción con pipelines de vídeo |
| Modelo de desarrollo | `s2.1-pro-free` — misma calidad que el S2.1 Pro de pago para desarrollo (uso justo; revisa los términos actuales) |
| Idiomas | más de 80 con detección automática de idioma |

Por qué esto supera a Kokoro local / OpenAI TTS / ElevenLabs en mi configuración de agentes:

- **Clona una vez, reutiliza siempre** — `reference_id` apunta a tu modelo de voz
- **Respuesta síncrona** — sin crear tarea ni bucle de sondeo como muchas APIs de imágenes
- **La emoción va en el texto** — el agente escribe las etiquetas en el guion; no hay un campo de estilo aparte que olvidar
- **Una herramienta clara** — entradas obvias, la salida es un archivo binario en disco

Lo uso como la **única** vía de TTS para el agente de creación de vídeo. Las skills que por defecto usan HeyGen, ElevenLabs o Kokoro quedan anuladas: siempre `fish_tts`.

## Lo que vas a construir

<ListCheck>
<ul>
<li>Una cuenta de Fish Audio con una clave de API y un ID de modelo de voz clonada</li>
<li>Una herramienta Mastra <code>fish_tts</code> que envía texto y escribe audio en el workspace</li>
<li>Un agente (o fase de vídeo) que genera narración con etiquetas de emoción</li>
<li>Opcional: la misma herramienta en un pipeline de vídeo multifase</li>
</ul>
</ListCheck>

La herramienta puede:

- Generar voz a partir de texto usando tu voz clonada
- Aplicar ajustes de velocidad, volumen, temperatura y latencia
- Incrustar marcadores de emoción / pausa (`[confident]`, `[break]`, …)
- Guardar `wav` / `mp3` / `opus` en una ruta que elige el agente
- Devolver éxito, ruta y bytes escritos (o un error claro)

## Requisitos previos

- Un proyecto Mastra funcionando (ver [Crea tu propio agente IA con Mastra](/build-ai-agent-mastra/))
- Node.js 22+ o **Bun** (este repositorio usa Bun por defecto)
- Una clave de API de LLM para el cerebro del agente (OpenRouter, OpenCode Go, etc.)
- Una cuenta de [Fish Audio](https://go.bitdoze.com/fish-audio)

Opcional: HyperFrames (o cualquier stack de vídeo) si quieres pipelines completos de locución → transcripción → subtítulos. El TTS funciona sin vídeo — solo necesitas un lugar donde guardar el archivo.

## Arquitectura

```
User (Studio chat / workflow)
    │
    ▼
agent  (LLM + instructions)
    │
    └─ fish_tts  ──► POST api.fish.audio/v1/tts
                          │
                          ▼
                     workspace/.../narration.wav
                          │
                          ▼ (optional)
                     hyperframes transcribe → transcript.json
                     composition <audio class="clip">
```

El LLM nunca tiene la clave de API. Solo elige herramientas. La autenticación, la seguridad de rutas y la descarga binaria permanecen en TypeScript, donde puedes probarlas.

## Paso 1: Crea una clave de API y clona tu voz

1. Regístrate en [Fish Audio](https://go.bitdoze.com/fish-audio)
2. Crea una clave de API en el panel
3. Clona una voz: sube ~15 segundos de voz limpia (un solo hablante, poco ruido)
4. Copia el **voice model ID** resultante — este es el `reference_id` en la API

¿Necesitas capturas de pantalla y consejos de grabación? Sigue [Clona tu voz con Fish Audio en 2 minutos](/es/fish-audio-clonar-voz/).

Añade a `.env`:

```bash
FISH_API_KEY=your-fish-audio-key
FISH_VOICE_ID=your-cloned-voice-model-id
# TTS model default for fish_tts (override per call with model: still works)
FISH_TTS_MODEL=s2.1-pro-free
```

| Variable de entorno | Función |
| --- | --- |
| `FISH_API_KEY` | Token Bearer para `api.fish.audio` |
| `FISH_VOICE_ID` | ID del modelo de voz clonada por defecto (`reference_id`) |
| `FISH_TTS_MODEL` | Modelo TTS por defecto (`DEFAULT_MODEL` en la herramienta). Usa `s2.1-pro-free` durante el desarrollo; cambia a `s2.1-pro` para el SLA de producción |

Nunca pongas la clave en código frontend ni la subas a git.

### Consejos para clonar la voz

- Usa una habitación silenciosa y un ritmo natural
- Prefiere un monólogo continuo en lugar de clips cortos unidos
- Usa el idioma en el que narrarás con más frecuencia
- Vuelve a clonar si la fuente estaba comprimida o con ruido — un mal clon significa malas generaciones siempre

<Button text="Clona Tu Voz Gratis" link="https://go.bitdoze.com/fish-audio" variant="solid" color="blue" size="md" icon="arrow-right" />

## Paso 2: Fundamentos del cliente de Fish Audio

El TTS de Fish Audio es un único POST. El éxito devuelve **bytes de audio sin procesar**, no JSON.

### Autenticación y endpoint

```
POST https://api.fish.audio/v1/tts
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
model: s2.1-pro-free   # or s2.1-pro | s2-pro | s1
```

### Cuerpo de la petición (campos que usamos)

```json
{
  "text": "[confident] Docker makes deployment simple. [break] [excited] This tool makes it even easier.",
  "reference_id": "YOUR_VOICE_MODEL_ID",
  "format": "wav",
  "prosody": {
    "speed": 1.0,
    "volume": 0,
    "normalize_loudness": true
  },
  "temperature": 0.7,
  "top_p": 0.7,
  "chunk_length": 300,
  "normalize": true,
  "latency": "normal",
  "max_new_tokens": 1024,
  "repetition_penalty": 1.2,
  "condition_on_previous_chunks": true
}
```

| Campo | Función |
| --- | --- |
| `text` | Guion + marcadores `[emotion]` (S2 / S2.1). Máx. ~10k caracteres en nuestra herramienta. |
| `reference_id` | ID del modelo de voz clonada |
| `format` | `wav` \| `mp3` \| `opus` |
| `prosody.speed` | 0.5–2.0 (1.0 = natural para tutoriales) |
| `prosody.volume` | Desplazamiento en dB (−20…20) |
| `temperature` | Más alto = más variación expresiva |
| `latency` | `normal` (calidad) \| `balanced` \| `low` |

En caso de éxito: escribe `Buffer.from(await res.arrayBuffer())` en disco. En caso de error: analiza el `message` JSON cuando esté presente.

### Diccionarios de pronunciación

Un campo de petición más reciente que conviene conocer: `pronunciation_dictionary`. Si una palabra sale mal una y otra vez — el nombre de tu marca, "Mastra" mismo, siglas como SQL — antes tenías que editar etiquetas de fonemas en cada guion que escribe el agente. Ahora puedes publicar un diccionario una vez en la app web y pasar `{ "id": "...", "version": "..." }`, o enviar un diccionario en línea para un caso puntual:

```json
{
  "text": "Mastra agents call Kubernetes jobs.",
  "reference_id": "YOUR_VOICE_MODEL_ID",
  "pronunciation_dictionary": [
    {
      "items": [
        { "key": "Kubernetes", "value": "K UW2 B ER0 N EH1 T IY0 Z", "case_sensitive": false }
      ]
    }
  ]
}
```

Las entradas en inglés usan CMU Arpabet; el chino usa pinyin con números de tono; el japonés usa romaji con marcas de tono. Máximo 3 diccionarios por petición, 5.000 reglas cada uno. Referencia completa: [diccionarios de pronunciación](https://docs.fish.audio/developer-guide/core-features/fine-grained-control/pronunciation-dictionaries).

### Modelos

| ID del modelo | Notas |
| --- | --- |
| `s2.1-pro-free` | Misma calidad que S2.1 Pro para desarrollo / uso justo — buena opción por defecto mientras exploras |
| `s2.1-pro` | S2.1 Pro de producción con SLA |
| `s2-pro` | Generación S2 anterior; sigue usando emociones con `[corchetes]` |
| `s1` | Heredado; las emociones usan `(paréntesis)`, no corchetes |

Documentación de emociones: [docs.fish.audio — Emotion Control](https://docs.fish.audio/developer-guide/core-features/emotions).

## Paso 3: Herramienta `fish_tts` de Mastra

Crea `src/mastra/tools/fish-audio-tts.ts`. Las herramientas son `createTool` + Zod. El campo `description` es la documentación de la API que ve el modelo — escríbela con cuidado para que el agente incruste las etiquetas de emoción correctamente.

```typescript
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { resolve, isAbsolute, dirname } from "node:path";
import { mkdirSync, writeFileSync } from "node:fs";

const FISH_API_BASE = "https://api.fish.audio";
const DEFAULT_VOICE_ID = process.env.FISH_VOICE_ID ?? "";
// Default TTS model from env (s2.1-pro-free for dev; s2.1-pro for production SLA)
const DEFAULT_MODEL = process.env.FISH_TTS_MODEL ?? "s2.1-pro-free";
// Resolve relative output paths against your workspace root.
const WORKSPACE_PATH = process.env.WORKSPACE_PATH ?? `${process.cwd()}/workspace`;

function getApiKey(): string {
  const apiKey = process.env.FISH_API_KEY;
  if (!apiKey) {
    throw new Error(
      "FISH_API_KEY is not set. Add it to .env to enable Fish Audio TTS.",
    );
  }
  return apiKey;
}

export function createFishTts(basePath: string = WORKSPACE_PATH) {
  return createTool({
    id: "fish_tts",
    description: `Generate narration audio from text using Fish Audio S2.1 Pro TTS with a cloned voice.

EMOTION CONTROL (S2.1 Pro): Add emotion/style cues in [square brackets] directly in the text. Examples:
  [happy] What a great tool!
  [excited] And the best part? It's free!
  [calm] Let me walk you through the setup.
  [confident] This is the best option on the market.
  [curious] But what about performance?
  [sarcastic] Oh sure, because THAT always works.
  [whispering] Here's a secret most people miss.
  [laughing] Ha ha, yeah, I tried that too.
  [sighing] Another config file... sigh.

Combine emotions: [sad][whispering] for a quiet sad tone, [excited][laughing] for joyful energy.
Use [break] for a short pause, [long-break] for a longer pause.
Place emotion cues at the START of the sentence they should affect.
Natural language descriptions also work: [warm and friendly], [slightly annoyed], [very energetic].

For technical narration, [confident] or [calm] works best as the base tone. Use [excited] sparingly for highlights. Avoid [happy] for informational content — it sounds salesy.`,
    inputSchema: z.object({
      text: z
        .string()
        .max(10000)
        .describe(
          "Narration text. Supports [emotion] tags like [happy], [excited], [calm], [whispering], [break]. Place cues at sentence starts.",
        ),
      outputPath: z
        .string()
        .describe(
          "Path to save the audio. Relative paths resolve against the workspace. Extension must match format (.wav, .mp3, .opus).",
        ),
      speed: z
        .number()
        .min(0.5)
        .max(2)
        .optional()
        .describe("Speech speed. 1.0 = normal. Default 1.0."),
      volume: z
        .number()
        .min(-20)
        .max(20)
        .optional()
        .describe("Volume in dB. 0 = no change. Default 0."),
      voiceId: z
        .string()
        .optional()
        .describe("Fish Audio voice model ID. Defaults to FISH_VOICE_ID."),
      model: z
        .enum(["s2.1-pro-free", "s2.1-pro", "s2-pro", "s1"])
        .optional()
        .describe(
          "TTS model. Defaults to FISH_TTS_MODEL env (or s2.1-pro-free). Use s2.1-pro for production SLA.",
        ),
      format: z
        .enum(["wav", "mp3", "opus"])
        .optional()
        .describe("Output format. Default wav (good for video pipelines)."),
      temperature: z
        .number()
        .min(0)
        .max(1)
        .optional()
        .describe("Expressiveness. Higher = more varied. Default 0.7."),
      latency: z
        .enum(["normal", "balanced", "low"])
        .optional()
        .describe("normal = best quality (default). low = fastest."),
    }),
    outputSchema: z.object({
      success: z.boolean(),
      path: z.string().optional(),
      bytesWritten: z.number().optional(),
      format: z.string().optional(),
      error: z.string().optional(),
    }),
    execute: async (input) => {
      const apiKey = getApiKey();
      const format = input.format ?? "wav";
      const model = input.model ?? DEFAULT_MODEL;
      const voiceId = input.voiceId ?? DEFAULT_VOICE_ID;
      const speed = input.speed ?? 1.0;
      const volume = input.volume ?? 0;
      const temperature = input.temperature ?? 0.7;
      const latency = input.latency ?? "normal";

      if (!voiceId) {
        return {
          success: false,
          error:
            "No voice ID. Set FISH_VOICE_ID or pass voiceId. Clone a voice at Fish Audio first.",
        };
      }

      const absPath = isAbsolute(input.outputPath)
        ? input.outputPath
        : resolve(basePath, input.outputPath);

      try {
        mkdirSync(dirname(absPath), { recursive: true });
      } catch {
        // Directory may already exist
      }

      const body = {
        text: input.text,
        reference_id: voiceId,
        format,
        prosody: {
          speed,
          volume,
          normalize_loudness: true,
        },
        temperature,
        top_p: 0.7,
        chunk_length: 300,
        normalize: true,
        latency,
        max_new_tokens: 1024,
        repetition_penalty: 1.2,
        condition_on_previous_chunks: true,
      };

      try {
        const res = await fetch(`${FISH_API_BASE}/v1/tts`, {
          method: "POST",
          headers: {
            Authorization: `Bearer ${apiKey}`,
            "Content-Type": "application/json",
            model,
          },
          body: JSON.stringify(body),
        });

        if (!res.ok) {
          let errMsg = `Fish Audio TTS failed (HTTP ${res.status})`;
          try {
            const errData = await res.json();
            errMsg = errData?.message || errMsg;
          } catch {
            // not JSON
          }
          return { success: false, error: errMsg };
        }

        const audioBuffer = Buffer.from(await res.arrayBuffer());
        if (audioBuffer.length === 0) {
          return {
            success: false,
            error: "Fish Audio TTS returned empty audio data.",
          };
        }

        writeFileSync(absPath, audioBuffer);

        return {
          success: true,
          path: absPath,
          bytesWritten: audioBuffer.length,
          format,
        };
      } catch (error) {
        return {
          success: false,
          error: `Fish Audio TTS failed: ${
            error instanceof Error ? error.message : "Unknown error"
          }`,
        };
      }
    },
  });
}

// Default instance against workspace root
export const fishTts = createFishTts();
```

### Factory con una ruta base personalizada

Los proyectos de vídeo suelen vivir en un directorio dedicado (por ejemplo `workspace/hyperframes/`). Usa la factory para que las rutas relativas se resuelvan correctamente:

```typescript
import { createFishTts } from "../tools/fish-audio-tts";

// Paths like "my-video/assets/narration.wav" land under hyperframes/
const fishTts = createFishTts("/absolute/path/to/workspace/hyperframes");
```

Así es como está conectado el agente de creación de vídeo: una instancia de la herramienta con ámbito en el workspace de HyperFrames, no en todo el monorepo.

### O sáltate la herramienta: Fish Audio MCP

Si no quieres mantener una herramienta personalizada en absoluto, Fish Audio incluye un servidor MCP en `https://api.fish.audio/mcp` (HTTP streamable, inicio de sesión con OAuth). Conéctalo al toolset MCP de un agente Mastra — o a Claude Code, Cursor o Codex — y el agente puede buscar voces, ejecutar TTS, transcribir archivos y, desde la última actualización, generar y editar imágenes y vídeos.

Dos compromisos que debes conocer antes de cambiar `fish_tts` por él: MCP gasta los créditos de paquete de tu plan en lugar de los créditos de API de desarrollador (los trabajos de imagen y vídeo muestran un precio y piden confirmación al usuario), y devuelve URLs alojadas en lugar de escribir un `narration.wav` en tu workspace. Para un pipeline de vídeo programado sigo prefiriendo la herramienta determinista. Para un acceso rápido tipo "di esto con mi voz" desde cualquier cliente MCP, el servidor es la vía sin código. El mismo compromiso que traté en [Herramientas Mastra vs MCP](/es/herramientas-mastra-vs-mcp/).

## Paso 4: Define un agente con voz

Puedes adjuntar `fish_tts` a cualquier agente. Ejemplo mínimo — un agente de voz dedicado para pruebas:

```typescript
import { Agent } from "@mastra/core/agent";
import { fishTts } from "../tools/fish-audio-tts";

const AGENT_MODEL =
  process.env.AGENT_MODEL ?? "google/gemini-2.5-flash";

export const voiceAgent = new Agent({
  id: "voice-agent",
  name: "Voice Agent",
  instructions: () => {
    const iso = new Date().toISOString().split("T")[0];
    return `TODAY IS ${iso}.

You generate voiceover with the fish_tts tool using the user's cloned voice.

## Rules
- Always use fish_tts for audio. Never invent a local file path without calling the tool.
- Write narration WITH [emotion] tags. Base technical tone: [confident] or [calm].
- Highlights: [excited]. Problems: [frustrated]. Pauses: [break] or [long-break].
- Place emotion tags at the START of the sentence they affect. One primary emotion per sentence.
- Default speed 1.0. Default format wav.
- After success, report the local path and bytes written.
- If FISH_API_KEY or FISH_VOICE_ID is missing, say so clearly.
- TTS model defaults to FISH_TTS_MODEL (usually s2.1-pro-free); only pass model when you need a different one.`;
  },
  model: AGENT_MODEL,
  tools: {
    fishTts,
  },
  defaultOptions: { maxSteps: 10 },
});
```

### Instrucciones del agente de vídeo (el caso de uso real)

En un pipeline de vídeo completo, la locución es un paso entre investigación → diseño → guion → TTS → construcción → renderizado. El patrón de instrucciones importante:

```text
4b. VOICEOVER — Generate narration BEFORE building so the timeline can be sized to it.
- Finalize the narration script from research.
- Call fish_tts with text + output path like <project>/assets/narration.wav
- Keep speed at 1.0
- Embed emotion tags in the script, e.g.:
  "[confident] Docker makes deployment simple. [break] [excited] But this new tool? It makes Docker look complicated."
- After TTS: transcribe for word-level timestamps (e.g. hyperframes transcribe)
- Drop audio into the composition as <audio class="clip">
```

Y fuerza un único proveedor de TTS para que el modelo no se desvíe:

```text
TTS: Always use fish_tts (cloned user voice).
Do NOT use Kokoro, hyperframes tts, HeyGen, or ElevenLabs for narration.
```

### Registra el agente

En `src/mastra/index.ts`:

```typescript
import { Mastra } from "@mastra/core/mastra";
import { assistant } from "./agents/assistant";
import { voiceAgent } from "./agents/voice-agent";
// or: import { videoCreator } from "./agents/video-creator";

export const mastra = new Mastra({
  agents: {
    assistant,
    voiceAgent,
    // videoCreator,
  },
  // storage, logger, server — same as your existing app
});
```

Si usas flags de dominio (despliegues más ligeros), pon vídeo/voz detrás de una variable de entorno:

```typescript
// ENABLE_VIDEO=false to skip
if (process.env.ENABLE_VIDEO !== "false") {
  agents.videoCreator = videoCreator;
}
```

## Paso 5: Ejecuta y pruébalo

```bash
# .env must include:
# FISH_API_KEY=...
# FISH_VOICE_ID=...
# FISH_TTS_MODEL=s2.1-pro-free   # or s2.1-pro for production
# OPENROUTER_API_KEY=...   (or your LLM provider)
# AGENT_MODEL=google/gemini-2.5-flash

bun run dev
# or: npm run dev
```

Abre Studio en `http://localhost:4111`, selecciona **Voice Agent** (o **Video Creator**) y prueba:

```text
Generate a 20-second voiceover for a blog intro about Mastra + Fish Audio.
Save as voice/demo-narration.wav.
Use [confident] for the base tone and [excited] once when mentioning voice cloning.
```

Lo que deberías ver en las trazas:

1. El agente redacta un guion corto con etiquetas de emoción
2. `fish_tts` con `text`, `outputPath`, `speed: 1.0`
3. Éxito con una ruta local y `bytesWritten`
4. Archivo en disco dentro de tu workspace

### Ejemplo de narración con emociones

```text
[confident] If you already have a Mastra agent for research and coding, voice is the next unlock.
[break]
[calm] Clone your voice on Fish Audio in about fifteen seconds, then expose a single fish_tts tool.
[excited] From there, every video can sound like you — without sitting in front of a mic again.
```

### Después del TTS (pipeline de vídeo)

Para la temporización de subtítulos al estilo HyperFrames:

```bash
npx hyperframes transcribe assets/narration.wav -d .
# → transcript.json with word-level timestamps
```

Usa esas marcas de tiempo para los cortes de escena y los subtítulos sincronizados por palabra. No inventes tiempos.

Fish Audio tiene su propio ASR si quieres un solo proveedor de principio a fin: `POST /v1/asr` devuelve el texto más segmentos con marcas de tiempo. El modelo más reciente `transcribe-1-pro` (selecciónalo con la cabecera HTTP `model`) está pensado para grabaciones con varios hablantes — entrevistas, reuniones, podcasts — con marcadores en línea `<|speaker:N|>` y señales de emoción como `[laughter]` conservadas en la transcripción. Ambos modelos cuestan 0,36 $ por hora de audio.

## Chuleta de emociones (S2 / S2.1)

Corchetes. Colócalos al **inicio** de la frase. Las descripciones libres también funcionan (`[warm and friendly]`).

| Caso de uso | Etiquetas |
| --- | --- |
| Tono base técnico | `[confident]`, `[calm]` |
| Destacar una función | `[excited]` |
| Sorpresa / revelación | `[surprised]` |
| Punto de dolor | `[frustrated]` |
| Inciso / humor | `[sarcastic]`, `[laughing]` |
| Crear expectación | `[curious]` |
| Consejo en voz baja | `[whispering]` |
| Pausa | `[break]`, `[long-break]` |
| Combinadas | `[excited][laughing] We won!` |

<Tabs>
  <Tab name="Haz">
    <ul>
      <li>Una emoción principal por frase</li>
      <li>Ajusta la emoción al contenido (excited para logros, calm para pasos de configuración)</li>
      <li>Añade un pequeño texto "Ja ja" después de <code>[laughing]</code> cuando quieras una risa audible</li>
      <li>Prefiere <code>[confident]</code> sobre <code>[happy]</code> en tutoriales (happy suena a anuncio)</li>
    </ul>
  </Tab>
  <Tab name="No hagas">
    <ul>
      <li>No pongas una etiqueta cada tres palabras</li>
      <li>No mezcles emociones contradictorias en una misma frase</li>
      <li>No uses la sintaxis S1 <code>(paréntesis)</code> con modelos S2 (ni viceversa)</li>
    </ul>
  </Tab>
</Tabs>

Lista completa: [guía de emociones de Fish Audio](https://docs.fish.audio/developer-guide/core-features/emotions).

## Dónde encaja esto con los agentes de imagen e investigación

La misma app Mastra, herramientas especializadas:

| Agente | Tarea | Backend |
| --- | --- | --- |
| Asistente | Investigación, archivos, shell | TinyFish, workspace |
| Agente de imágenes | Portadas / miniaturas | [Kie.ai](/es/agente-imagen-mastra-kie-ai/) |
| Vídeo / voz | Narración + composiciones | **Fish Audio** + HyperFrames |

Patrón que funciona:

1. Investiga el tema (herramientas del asistente)
2. Genera la portada (agente de imágenes)
3. Guion + `fish_tts` + renderizado (agente de vídeo / pipeline)

Mantén las herramientas ligeras y la autenticación en el servidor. El LLM elige herramientas; TypeScript controla HTTP y archivos.

### Pipeline de vídeo multifase

Si un solo agente llega al límite de contexto, divide las fases (investigación → recursos → diseño → guion → **locución** → construcción → validación → renderizado). Solo la fase de locución necesita `fish_tts`. Las fases posteriores leen `assets/narration.wav` y `transcript.json` del disco — no hace falta regenerar el audio a menos que cambie el guion.

## Consejos para producción

1. **La calidad del clon lo es todo.** Muestra mala → mal resultado en cada vídeo. Vuelve a grabar si no suena bien.
2. **Velocidad por defecto 1.0.** A los agentes les encanta subir la velocidad; en tutoriales, el ritmo natural mantiene a la gente escuchando.
3. **Escribe las etiquetas de emoción dentro del guion.** No generes texto plano esperando que el modelo añada las etiquetas después.
4. **Persiste los archivos en tu workspace.** La herramienta ya escribe en disco — trata esa ruta como la fuente de verdad para el siguiente paso del pipeline.
5. **Un ID de voz por marca.** Guarda `FISH_VOICE_ID` en el entorno; permite sobrescribir `voiceId` solo cuando de verdad necesites varias voces.
6. **Define `FISH_TTS_MODEL` en el entorno** (`DEFAULT_MODEL` en el código). Desarrollo: `s2.1-pro-free`. SLA de producción: `s2.1-pro`. Los agentes aún pueden pasar `model` en cada llamada. Consulta [models overview](https://docs.fish.audio/developer-guide/models-pricing/models-overview) para la política actual del plan gratuito.
7. **Mantén los secretos en el servidor.** `FISH_API_KEY` solo en `.env` / secretos del host.
8. **Limita la longitud del texto.** Guiones largos: divídelos en secciones, genera varios archivos y concatena con ffmpeg si hace falta.
9. **Separa el agente del asistente de código.** Voz + vídeo consumen pasos. Un agente dedicado mantiene al asistente de investigación centrado.

## Solución de problemas

<Accordion label="401 / errores de autenticación" group="faq" expanded="true">
Falta o es incorrecta la cabecera <code>Authorization: Bearer ...</code>. Confirma que <code>FISH_API_KEY</code> está cargada en el proceso (Bun carga <code>.env</code> automáticamente; Node puede necesitar configuración extra).
</Accordion>

<Accordion label="Audio vacío / cero bytes" group="faq">
Es raro un cuerpo vacío — trátalo como fallo y reintenta. Revisa el panel / estado de Fish si sigue ocurriendo.
</Accordion>

<Accordion label="Voz incorrecta / voz genérica" group="faq">
<code>reference_id</code> incorrecto o vacío. Confirma que <code>FISH_VOICE_ID</code> coincide con el clon en el panel de Fish. Pasa <code>voiceId</code> explícitamente una vez para verificar.
</Accordion>

<Accordion label="Emociones ignoradas" group="faq">
Estás usando la sintaxis S1 <code>(happy)</code> con modelos S2.1 — cambia a <code>[happy]</code>. Etiquetas colocadas a mitad de frase, lejos de las palabras que deben matizar — muévelas al inicio de la frase. Exceso de etiquetas — simplifica a una indicación por frase.
</Accordion>

<Accordion label="Entrega poco natural" group="faq">
Baja <code>temperature</code> hacia 0.5 para más consistencia, o súbela ligeramente para más variedad. Mantén <code>speed</code> en 1.0. Espacia los cambios emocionales.
</Accordion>

<Accordion label="Ruta no encontrada / directorio incorrecto" group="faq">
Las rutas relativas se resuelven contra el <code>basePath</code> de la herramienta. Los agentes de vídeo deberían usar <code>createFishTts(HYPERFRAMES_PATH)</code> para que <code>project/assets/narration.wav</code> quede junto a la composición.
</Accordion>

<Accordion label="HTTP 429 / límites de tasa" group="faq">
Espera y reintenta. Procesa las locuciones por lotes con una pequeña pausa en los workflows.
</Accordion>

<Accordion label="El agente usa Kokoro / otro TTS" group="faq">
Las instrucciones no son lo bastante firmes. Prohíbe explícitamente las herramientas TTS alternativas e indica <code>fish_tts</code> como la única vía permitida para la locución.
</Accordion>

## Guías relacionadas de Fish Audio y Mastra

- [Reseña de Fish Audio 2026](/es/resena-fish-audio/) — precios, API gratuita, qué es bueno y qué no
- [Clona tu voz con Fish Audio](/es/fish-audio-clonar-voz/) — tutorial de 2 minutos con capturas
- [Fish Audio vs ElevenLabs](/es/fish-audio-vs-elevenlabs/) — calidad comparada, clonación y coste anual
- [Fish Audio vs MiniMax](/es/fish-audio-vs-minimax/) — calidad en inglés vs chino, precios, API gratuita
- [Crea un agente Mastra](/build-ai-agent-mastra/) — archivos, web, navegador, memoria
- [Herramientas Mastra vs MCP](/es/herramientas-mastra-vs-mcp/) — createTool nativo vs servidores MCP externos
- [Agente de imágenes Mastra con Kie.ai](/es/agente-imagen-mastra-kie-ai/) — portadas y miniaturas
- [Mastra vs Eve](/mastra-vs-eve-typescript-ai-agents/) — elección de framework para agentes TypeScript

Una vez que la voz funciona, el mismo patrón se extiende al vídeo totalmente autónomo: investigación → storyboard → `fish_tts` → composición HTML → renderizado. Lo mantengo como un agente de vídeo dedicado (o workflow multifase) para que los presupuestos de pasos y las instrucciones se mantengan limpios.

<Button text="Empieza con Fish Audio" link="https://go.bitdoze.com/fish-audio" variant="solid" color="blue" size="md" icon="arrow-right" />
<Button text="Guía del Asistente Mastra" link="/build-ai-agent-mastra/" variant="outline" color="purple" size="md" icon="arrow-right" />

<Notice type="success" title="Siguiente paso">
Añade <code>FISH_API_KEY</code>, <code>FISH_VOICE_ID</code> y opcionalmente <code>FISH_TTS_MODEL</code> (por defecto <code>s2.1-pro-free</code>), registra la herramienta en un agente, abre Studio y genera un <code>narration.wav</code> corto en el workspace. Si algo falla, revisa primero el mensaje de error de la herramienta — la mayoría de los problemas son una clave ausente, un ID de voz vacío o la sintaxis de emociones S1 vs S2.
</Notice>