Añade TTS con Clonación de Voz a Mastra con Fish Audio
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.
46 min de lectura

Ya tengo un asistente 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 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 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 o en cualquier app Mastra existente.
Si aún no has usado Fish Audio, empieza con la reseña de Fish Audio o el tutorial de clonación en 2 minutos. ¿Comparando opciones? Mira Fish Audio vs ElevenLabs y Fish Audio vs MiniMax.
Prueba Fish Audio Gratis Crea Primero un Agente MastraDivulgació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.
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_idapunta 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
- Una cuenta de Fish Audio con una clave de API y un ID de modelo de voz clonada
- Una herramienta Mastra
fish_ttsque envía texto y escribe audio en el workspace - Un agente (o fase de vídeo) que genera narración con etiquetas de emoción
- Opcional: la misma herramienta en un pipeline de vídeo multifase
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/opusen 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)
- 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
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
- Regístrate en Fish Audio
- Crea una clave de API en el panel
- Clona una voz: sube ~15 segundos de voz limpia (un solo hablante, poco ruido)
- Copia el voice model ID resultante — este es el
reference_iden la API
¿Necesitas capturas de pantalla y consejos de grabación? Sigue Clona tu voz con Fish Audio en 2 minutos.
Añade a .env:
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
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 | s1Cuerpo de la petición (campos que usamos)
{
"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:
{
"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.
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.
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.
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:
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.
Paso 4: Define un agente con voz
Puedes adjuntar fish_tts a cualquier agente. Ejemplo mínimo — un agente de voz dedicado para pruebas:
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:
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:
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:
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:
// ENABLE_VIDEO=false to skip
if (process.env.ENABLE_VIDEO !== "false") {
agents.videoCreator = videoCreator;
}Paso 5: Ejecuta y pruébalo
# .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 devAbre Studio en http://localhost:4111, selecciona Voice Agent (o Video Creator) y prueba:
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:
- El agente redacta un guion corto con etiquetas de emoción
fish_ttscontext,outputPath,speed: 1.0- Éxito con una ruta local y
bytesWritten - Archivo en disco dentro de tu workspace
Ejemplo de narración con emociones
[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:
npx hyperframes transcribe assets/narration.wav -d .
# → transcript.json with word-level timestampsUsa 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! |
Haz
- Una emoción principal por frase
- Ajusta la emoción al contenido (excited para logros, calm para pasos de configuración)
- Añade un pequeño texto “Ja ja” después de
[laughing]cuando quieras una risa audible - Prefiere
[confident]sobre[happy]en tutoriales (happy suena a anuncio)
No hagas
- No pongas una etiqueta cada tres palabras
- No mezcles emociones contradictorias en una misma frase
- No uses la sintaxis S1
(paréntesis)con modelos S2 (ni viceversa)
Lista completa: guía de emociones de Fish Audio.
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 |
| Vídeo / voz | Narración + composiciones | Fish Audio + HyperFrames |
Patrón que funciona:
- Investiga el tema (herramientas del asistente)
- Genera la portada (agente de imágenes)
- 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
- La calidad del clon lo es todo. Muestra mala → mal resultado en cada vídeo. Vuelve a grabar si no suena bien.
- Velocidad por defecto 1.0. A los agentes les encanta subir la velocidad; en tutoriales, el ritmo natural mantiene a la gente escuchando.
- Escribe las etiquetas de emoción dentro del guion. No generes texto plano esperando que el modelo añada las etiquetas después.
- 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.
- Un ID de voz por marca. Guarda
FISH_VOICE_IDen el entorno; permite sobrescribirvoiceIdsolo cuando de verdad necesites varias voces. - Define
FISH_TTS_MODELen el entorno (DEFAULT_MODELen el código). Desarrollo:s2.1-pro-free. SLA de producción:s2.1-pro. Los agentes aún pueden pasarmodelen cada llamada. Consulta models overview para la política actual del plan gratuito. - Mantén los secretos en el servidor.
FISH_API_KEYsolo en.env/ secretos del host. - Limita la longitud del texto. Guiones largos: divídelos en secciones, genera varios archivos y concatena con ffmpeg si hace falta.
- 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
401 / errores de autenticación
Falta o es incorrecta la cabecera Authorization: Bearer …. Confirma que FISH_API_KEY está cargada en el proceso (Bun carga .env automáticamente; Node puede necesitar configuración extra).
Audio vacío / cero bytes
Es raro un cuerpo vacío — trátalo como fallo y reintenta. Revisa el panel / estado de Fish si sigue ocurriendo.
Voz incorrecta / voz genérica
reference_id incorrecto o vacío. Confirma que FISH_VOICE_ID coincide con el clon en el panel de Fish. Pasa voiceId explícitamente una vez para verificar.
Emociones ignoradas
Estás usando la sintaxis S1 (happy) con modelos S2.1 — cambia a [happy]. 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.
Entrega poco natural
Baja temperature hacia 0.5 para más consistencia, o súbela ligeramente para más variedad. Mantén speed en 1.0. Espacia los cambios emocionales.
Ruta no encontrada / directorio incorrecto
Las rutas relativas se resuelven contra el basePath de la herramienta. Los agentes de vídeo deberían usar createFishTts(HYPERFRAMES_PATH) para que project/assets/narration.wav quede junto a la composición.
HTTP 429 / límites de tasa
Espera y reintenta. Procesa las locuciones por lotes con una pequeña pausa en los workflows.
El agente usa Kokoro / otro TTS
Las instrucciones no son lo bastante firmes. Prohíbe explícitamente las herramientas TTS alternativas e indica fish_tts como la única vía permitida para la locución.
Guías relacionadas de Fish Audio y Mastra
- Reseña de Fish Audio 2026 — precios, API gratuita, qué es bueno y qué no
- Clona tu voz con Fish Audio — tutorial de 2 minutos con capturas
- Fish Audio vs ElevenLabs — calidad comparada, clonación y coste anual
- Fish Audio vs MiniMax — calidad en inglés vs chino, precios, API gratuita
- Crea un agente Mastra — archivos, web, navegador, memoria
- Herramientas Mastra vs MCP — createTool nativo vs servidores MCP externos
- Agente de imágenes Mastra con Kie.ai — portadas y miniaturas
- Mastra vs Eve — 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.
Siguiente paso
Añade FISH_API_KEY, FISH_VOICE_ID y opcionalmente FISH_TTS_MODEL (por defecto s2.1-pro-free), registra la herramienta en un agente, abre Studio y genera un narration.wav 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.


