
# REPARACIÓN COMPLETA: Audio en el Portal (TTS + Grabaciones)
> **Última actualización:** 2026-06-22
> **Síntomas:** El player de audio se renderiza en el chat del portal pero NO suena (mudo), O los mensajes del agente con audio no muestran el player, solo texto.
> **Tags:** `#audio #tts #webchat #webm #mp3 #portal #chromium #stt`
---
## ⚠️ LEE ESTO AL INICIAR SESIÓN
Si llegás a esta sesión y hay problemas de audio en el portal, **revisá estos 4 puntos en orden** antes de tocar nada más:
1. **Verificá que los audios MP3 se sirvan correctamente** → `curl -s -I http://localhost:8888/uploads/algun-audio.mp3 | grep Content-Type` (debe ser `audio/mpeg`)
2. **Verificá que el file-server esté corriendo** → `ps aux | grep file-server`
3. **Verificá que los backups existan** → `ls sectores/sector\ portal/reparaciones/`
4. **Releé este documento completo** — cada sección tiene un problema específico con su solución exacta
---
## ÍNDICE DE PROBLEMAS
| # | Problema | Síntoma | Archivo |
|---|---|---|---|
| 1 | MIME type `.webm` incorrecto | Player aparece pero sin sonido | `file-server.js` |
| 2 | Playback falla en `<audio>` sin DOM | Player aparece pero sin sonido | `public_html/index.html` |
| 3 | WebM no suena en Chromium | Player aparece pero sin sonido | `file-server.js` + `public_html/index.html` |
| 4 | `new Audio()` suelto sin attached DOM | Error silencioso al hacer play | `public_html/index.html` |
| 5 | Audio del agente no tiene player | Solo texto, sin 🔊 player visibles | Formato de respuesta |
| 6 | Archivos TTS en directorio equivocado | Audio no se sirve (404) | Ubicación de archivos |
| 7 | Regresión tras actualización | Lo que funcionaba dejó de andar | Sin backup funcional |
| 8 | Autoplay del 🔊 no funciona | Audio aparece, no se reproduce solo | `public_html/index.html` → `renderAgentMessage()` |
| 9 | Agente responde en texto cuando STEREO1 manda audio | STEREO1 envía audio, agente responde texto | Comportamiento del agente |
---
## PROBLEMA 1: Chromium no reproduce WebM en `<audio>`
### Causa
Chromium (Chrome, Edge, Brave) NO reproduce archivos `.webm` servidos con `Content-Type: audio/webm` en elementos `<audio>`. WebM es un contenedor de video, y Chrome lo asocia con `video/webm`. Aunque el archivo solo tenga audio (Opus), el navegador rechaza el stream si el MIME es `audio/webm`.
**Esto ES intencional de Chromium** — no es un bug, es feature. Desde 2016 Chrome decidió no dar soporte a `audio/webm` en `<audio>` porque WebM está clasificado como formato de video.
### Solución
En `file-server.js`, el MIME map debe usar `video/webm` para archivos `.webm`:
```js
'.webm': 'video/webm', // ← NO 'audio/webm'
```
### Verificación
```bash
curl -s -I http://localhost:8888/uploads/archivo.webm | grep content-type
# Debe retornar: content-type: video/webm
```
### Edge cases
- **Chromium en Android (WebView)**: mismo problema, misma solución
- **Firefox/Safari**: aceptan `audio/webm` sin problema, pero `video/webm` también funciona
- **Si se agrega otro formato** (`.ogg`, `.m4a`): verificar que el MIME exista en el map
---
## PROBLEMA 2: `new Audio()` sin attached DOM no reproduce
### Causa
El código original usaba `new Audio(el.dataset.src)` para crear un elemento `<audio>` sin adjuntarlo al DOM. En ciertas versiones de Chromium (especialmente en Android WebView y headless), `audio.play()` sobre un elemento no adjunto al documento se rechaza silenciosamente. La Promise de `.play()` se rechaza pero el `.catch()` solo resetea el SVG del botón sin mostrar error.
### Solución
En lugar de `new Audio()`, crear el elemento con `document.createElement('audio')` y adjuntarlo al DOM oculto:
```js
// MAL ❌
const a = new Audio(url);
a.play().then(...).catch(...);
// BIEN ✅
const audioEl = document.createElement('audio');
audioEl.src = url;
audioEl.style.display = 'none';
contenedor.appendChild(audioEl); // ← attached al DOM
audioEl.play().then(...).catch(err => console.error(err));
```
Además:
- **Reutilizar** el elemento `<audio>` si ya existe dentro del contenedor
- Si el elemento tuvo error previo, forzar `audioEl.load()` antes de `play()`
- Mostrar texto de error visible en el player (ej: `timeEl.textContent = 'err'`)
### Archivo afectado
`public_html/index.html` → función `playAudio(btn)`
### Verificación
- Abrir consola del navegador (F12) → si hay error de play, se muestra `audio play error: ...`
- El tiempo muestra 'err' en lugar de '-:--' si falló
---
## PROBLEMA 3: WebM grabado no se reproduce en ningún formato
### Causa
El `MediaRecorder` de Chrome graba en formato `audio/webm` (Opus en contenedor WebM). Incluso con `video/webm` como MIME, algunos navegadores/chromium tienen problemas reproduciendo el Opus de Chrome sobre WebM. No es un problema del archivo (ffprobe confirma archivo válido con audio real) — es el decodificador de Opus en WebM del navegador.
### Solución
Convertir automáticamente los `.webm` subidos a `.mp3` en el servidor. **Async (no bloqueante)** con fallback al `.webm` original si la conversión falla.
En `file-server.js`, handler de `/__upload`:
```js
// Helper para responder después de la conversión async
function finishUpload(respFile) {
const isImage = ['.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext);
const isAudio = ['.mp3', '.wav', '.ogg', '.webm', '.m4a'].includes(ext);
const msg = {
id, from: 'human',
type: isImage ? 'image' : isAudio ? 'audio' : 'file',
file: respFile, name: origName,
size: fileBin.length, ts: new Date().toISOString(),
};
appendChat(msg);
notifyAgent(`[${msg.type}] ${msg.name}`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, id: msg.id, type: msg.type, file: respFile }));
}
// Convertir .webm a .mp3 (async, no bloquea el event loop)
if (ext === '.webm') {
const ff = '/home/node/.openclaw/workspace/node_modules/ffmpeg-static/ffmpeg';
const mp3Path = path.join(UPLOAD_DIR, id + '.mp3');
require('child_process').exec(
`${ff} -y -i '${filePath}' -acodec libmp3lame -ab 64k '${mp3Path}' 2>/dev/null`,
{ timeout: 15000 },
(err) => {
if (err || !fs.existsSync(mp3Path)) {
console.error('webm2mp3 fail, using .webm fallback:', err ? err.message : 'mp3 not found');
finishUpload(filename); // fallback al .webm original
} else {
finishUpload(id + '.mp3');
}
}
);
} else {
finishUpload(filename);
}
```
### Verificación
- Subir audio → ver que aparece en uploads/ el .webm Y el .mp3 del mismo timestamp
- Timeline: conversión ~0.3s para 40 segundos de audio (~160x speed), sin bloquear el servidor
### Edge cases
- **ffmpeg no encontrado**: la ruta hardcodeada es `/home/node/.openclaw/workspace/node_modules/ffmpeg-static/ffmpeg`. Si se reinstala, puede cambiar. Verificar con `which ffmpeg`. Si falla, se usa el .webm original como fallback
- **Timeout de conversión**: archivos muy largos (>10 min) pueden exceder el timeout de 15s. Aumentar si es necesario
- **Fallo con fallback automático**: si la conversión falla (ffmpeg no existe, timeout, codec error), el handler responde con el .webm original y el frontend lo reproduce sin problema
- **NUNCA usar execSync** en un servidor HTTP: bloquea el event loop de Node.js y puede causar crashes en cascada con conexiones SSE y WS abiertas
---
## PROBLEMA 4: Audio del Agente (TTS) no llega con player
### Causa
El `renderAgentMessage()` busca el patrón `🔊 /ruta/archivo.ext` en el texto del mensaje del agente para renderizar un player de audio. Si el mensaje usa otro formato (`MEDIA:`, `[audio]`, URL sola), no se detecta y se muestra como texto plano.
### Solución
**Siempre** incluir `🔊 /uploads/archivo.mp3` al inicio del mensaje cuando se envía TTS.
Pipeline correcto cuando generás TTS:
```bash
# 1. Generar audio (parámetros fijos de STEREO1)
node /app/node_modules/node-edge-tts/bin.js \
--voice "es-NI-FedericoNeural" \
--text "Texto a decir" \
--rate "+35%" --pitch "+2%" \
-f archivo.mp3
# 2. Copiar a uploads (para el portal)
cp archivo.mp3 public_html/uploads/archivo.mp3
# 3. Copiar a media/outbound (para WebChat normal)
cp archivo.mp3 /home/node/.openclaw/media/outbound/archivo.mp3
# 4. Responder con:
# 🔊 /uploads/archivo.mp3
# (texto opcional debajo)
```
### Verificación
- El texto del mensaje del agente debe EMPEZAR con `🔊 /uploads/...`
- El player debe aparecer con el tiempo de duración cargado
- Hacer clic en play debe reproducir el audio
### Edge cases
- **No poner texto ANTES del `🔊`**: el regex busca `🔊` seguido de espacio y URL. Si hay texto antes, el match puede fallar
- **Extensiones soportadas**: `mp3`, `webm`, `ogg`, `wav`. Aunque el player se renderiza, .webm puede no sonar (ver Problema 1 y 3)
- **Siempre preferir MP3** para TTS — no necesita conversión y funciona en todos los browsers
---
## PROBLEMA 5: Regresión — Los arreglos se pierden al reiniciar/actualizar
### Causa
No hay backup de los archivos modificados. Cuando se actualiza el portal o se reinicia el gateway, los cambios en `file-server.js` y `index.html` pueden revertirse. El agente no tiene un punto de referencia para restaurar.
### Solución
Siempre crear backups fechados después de cada reparación:
```bash
cp file-server.js file-server.js.bak.$(date +%Y-%m-%d)
cp public_html/index.html public_html/index.html.bak.$(date +%Y-%m-%d)
```
Además, mantener este documento actualizado para que el agente pueda re-aplicar los cambios.
### Verificación
```
ls sectores/sector portal/reparaciones/
# Debe mostrar: REPARACION-AUDIO-STTS.md (al menos)
```
---
## DIAGNÓSTICO RÁPIDO (para cuando no sabés qué pasa)
### Player se ve pero no suena
```
1. ¿El archivo es .webm?
→ Revisar Content-Type en respuesta HTTP (debe ser video/webm)
→ Verificar que existe el .mp3 convertido en uploads/
2. ¿El player muestra duración (0:00 / 1:17)?
→ Si NO: el archivo no cargó (404 o MIME incorrecto)
→ Si SÍ: el problema es de playback (DOM, codec, autoplay policy)
3. ¿La consola del navegador muestra errores?
→ "audio play error: NotAllowedError" → autoplay policy (solución: user gesture)
→ "audio play error: NotSupportedError" → codec no soportado
→ Sin errores → posible problema de volumen o elemento no attached
```
### Player no aparece
```
1. ¿El texto del mensaje del agente incluye 🔊 /uploads/archivo.ext?
→ Si NO: el renderAgentMessage no lo detectó
→ Si SÍ: revisar el regex: /🔊\s*(\/\S+\.(mp3|webm|ogg|wav))/i
2. ¿El archivo existe en public_html/uploads/?
→ curl -I http://localhost:8888/uploads/archivo.mp3
```
### El audio se grabó pero no se subió
```
1. ¿El MediaRecorder tiene soporte para audio/webm?
→ En algunos navegadores, MediaRecorder no soporta audio/webm
→ El código asume que sí (linea 532 de index.html)
2. ¿Hay espacio en disco?
→ df -h (los uploads pueden llenarse)
```
---
## ARCHIVOS CRÍTICOS (backupear estos)
| Archivo | Ruta | Propósito |
|---|---|---|
| `file-server.js` | `/home/node/.openclaw/workspace/file-server.js` | Sirve archivos, convierte webm a mp3, proxy SSE |
| `index.html` | `/home/node/.openclaw/workspace/public_html/index.html` | Portal webchat, render de mensajes y audio |
| `openclaw.json` | `/home/node/.openclaw/openclaw.json` | Config TTS (voice, rate, pitch) |
| `custom-mia.css` | `/home/node/.openclaw/workspace/custom-ui/custom-mia.css` | CSS del portal |
---
## COMANDOS DE VERIFICACIÓN
```bash
# Verificar que el file-server responde
curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/
# Verificar MIME de webm
curl -s -I http://localhost:8888/uploads/test.webm | grep content-type
# Verificar que el MP3 se sirve
curl -s -I http://localhost:8888/uploads/test.mp3 | grep content-type
# Verificar audio real (dura y volumen)
/home/node/.openclaw/workspace/node_modules/ffmpeg-static/ffmpeg -i archivo.webm -af volumedetect -f null - 2>&1 | grep "mean_volume\|max_volume"
# Verificar backup
ls -la /home/node/.openclaw/workspace/file-server.js.bak.*
ls -la /home/node/.openclaw/workspace/public_html/index.html.bak.*
```
---
## HISTORIAL DE REPARACIONES
| Fecha | Problema | Solución |
|---|---|---|
| 2026-06-21 | TTS del agente no llegaba al webchat | Config TTS en openclaw.json (auto=tagged, provider=microsoft, voice=FedericoNeural) |
| 2026-06-22 | WebM no se reproduce (mudo) | MIME `audio/webm` → `video/webm` en file-server.js |
| 2026-06-22 | Playback falla silenciosamente | `new Audio()` → `document.createElement('audio')` + attached DOM + error visible |
| 2026-06-22 | WebM grabado por micrófono no suena en ningún browser | Conversión automática webm→mp3 en upload handler |
| 2026-06-22 | Se pierden cambios al reiniciar | Backups fechados + este documento de reparación |
---
## PROBLEMA 8: 🔊 Autoplay — el audio no se reproduce solo
### Causa (solucionada)
El código `renderAgentMessage()` renderizaba el player de audio pero no llamaba a `play()` automáticamente. Había que hacer clic manual en el botón de play.
### Solución
En `renderAgentMessage()`, después de agregar el HTML del player, se agrega autoplay:
```js
// Autoplay: trigger play right after adding
const last = msgWrap.lastElementChild;
const ae = last?.querySelector('.audio-inline[data-src]');
if (ae) {
setTimeout(() => {
preloadAudio(ae);
var btn = ae.querySelector('.audio-play-btn');
if (btn) setTimeout(function(){ btn.click(); }, 100);
}, 50);
}
```
Esto busca el botón de play dentro del elemento recién agregado y lo activa después de 150ms, iniciando la reproducción automática.
### Edge cases
- **Autoplay policy del navegador**: si el usuario no ha interactuado con la página (<audio> sin user gesture), el navegador puede bloquear el autoplay. En la práctica, como el usuario está enviando mensajes (audios), la página ya tiene user gesture y funciona.
- **Múltiples audios seguidos**: el `playAudio()` pausa cualquier audio anterior antes de reproducir el nuevo, así que no hay superposición.
---
## PROBLEMA 9: Comportamiento del agente — responder con audio
### Regla
**Cuando STEREO1 envía un audio, el agente debe responder con audio (TTS).**
No es un bug, es el comportamiento esperado:
- STEREO1 manda audio → MIA responde con 🔊 /uploads/archivo.mp3
- STEREO1 manda texto → MIA responde con texto
- STEREO1 pide explícitamente un audio para alguien más → se genera con la voz que indique
### Pipeline
1. Transcribir el audio de STEREO1 con whisper (model tiny o base)
2. Procesar lo que dijo
3. Generar respuesta TTS con node-edge-tts: voz es-NI-FedericoNeural, rate +35%, pitch +2%
4. Copiar a public_html/uploads/
5. Responder con: 🔊 /uploads/archivo.mp3
---
## HISTORIAL DE REPARACIONES (continuación)
| Fecha | Problema | Solución |
|---|---|---|
| 2026-06-24 | Autoplay de 🔊 no funcionaba | Se agregó btn.click() automático en renderAgentMessage() |
| 2026-06-24 | Regla de respuesta audio documentada | Cuando STEREO1 envía audio, responder con audio |
---
## PROBLEMA 10: Versión estable — no hay persistencia tras reinicio
### Causa
No existía un mecanismo automático que iniciara el file-server al arrancar el gateway. Si el proceso moría o el servidor se reiniciaba, el portal quedaba caído.
### Solución — 2026-06-24
1. **Script de startup** en `startup.sh` — verifica que file-server esté corriendo, MIME types, ffmpeg, y uploads directory
2. **Cron de salud** cada 5 minutos — health-check via OpenClaw isolated cron que ejecuta el script
3. **Voz TTS actualizada** a `es-AR-ElenaNeural` (Argentina, femenina) en openclaw.json → messages.tts.providers.microsoft
### Verificación
```bash
bash startup.sh
# Debe mostrar ✅ en todos los checks
openclaw cron list | grep portal-file-server-healthcheck
# Debe mostrar el job habilitado
```
### Edge cases
- Si el gateway se reinicia, el health-check cron lo detecta y restaura el file-server en máximo 5 minutos
- Si no hay crontab disponible en el sistema, el cron de OpenClaw funciona como respaldo
# REPARACIONES DE DIÁLOGO
> Comportamiento del agente, reglas de respuesta, tono, y protocolos de conversación.
> Separado de `REPARACION-AUDIO-STTS.md` (que cubre lo técnico: TTS, STT, autoplay, etc.)
---
## HISTORIAL
| Fecha | Regla / Corrección | Origen |
|---|---|---|
| 2026-06-24 | Archivo creado para separar reglas de diálogo de las reparaciones técnicas de audio | STEREO1 |
| | | |
| | | |
---
## 🔴 FALLA CRÍTICA: Heartbeat repite último audio
### Problema
Cuando STEREO1 envía un audio y el agente está transcribiendo (whisper tarda ~10-20s), si un heartbeat se dispara durante ese tiempo, el agente re-envía el último audio que generó (la respuesta al audio ANTERIOR). Esto rompe el hilo de conversación: STEREO1 escucha una respuesta de una pregunta anterior y no sabe si es la nueva o la vieja.
### Causa
El heartbeat no distingue entre "acabo de interactuar" y "hace horas que no hablamos". Al enviar el mismo audio que ya se envió como respuesta, se duplica.
### Solución aplicada
1. **Regla:** Si el heartbeat se dispara justo después de enviar un audio de respuesta → responder con NO_REPLY. No repetir el mismo audio.
2. **Regla:** Si hay un audio de STEREO1 pendiente de transcripción → NO_REPLY hasta terminar.
3. **Regla:** Solo generar audio en heartbeat si pasó suficiente tiempo sin interacción (más de 5 minutos).
### Verificación
- Después de enviar un audio de respuesta, el siguiente heartbeat debe ser NO_REPLY
- No debe haber dos audios iguales seguidos en el chat
- La transcripción debe completarse antes de generar cualquier respuesta nueva
---
## 📢 Indicador de "Procesando" al recibir audio
### Problema
Cuando STEREO1 envía un audio, el agente se queda en silencio mientras transcribe (10-30s con whisper). STEREO1 no sabe si el agente recibió el mensaje o está procesando.
### Solución acordada
1. Al recibir un audio de STEREO1, responder INMEDIATAMENTE con texto: `🎤 Procesando...`
2. Luego transcribir el audio
3. Luego generar y enviar la respuesta en audio: `🔊 /uploads/...`
### Secuencia correcta
```
STEREO1 → [audio]
MIA → 🎤 Procesando...
MIA → 🔊 /uploads/respuesta.mp3 + texto contextual
```
### Regla
- El "procesando" es SIEMPRE texto, nunca audio
- No enviar "procesando" si el anterior sigue visible (no duplicar)
- Mantener la respuesta final en audio porque STEREO1 habla en audio
---
## 🎤 Eliminación del botón de micrófono
### Problema
A STEREO1 no le gusta el botón de micrófono en la interfaz del chat. No lo usa y prefiere no verlo.
### Solución
Comentado el elemento `<button id="btn-audio">` de `index.html` (línea ~414). El código JS asociado (`btnAudio.onclick`, `recording`, etc.) queda inactivo porque el elemento ya no existe en el DOM.
### Archivo
`public_html/index.html` — línea con `btn-audio` comentada.
---
## 🔊 Indicador "Procesando" con animación
### Implementación
Cuando el agente recibe un audio de STEREO1, envía inmediatamente:
```html
🎤 Procesando<span class="dots">.</span><span class="dots">.</span><span class="dots">.</span>
```
Tres dots animados con CSS (`dotPulse` keyframe): opacity 0→1 y translateY -2px, con delays de 0.2s entre cada uno, creando efecto de escritura/espera.
### CSS agregado en `index.html`
- `.msg-bubble.working .dots { animation: dotPulse 1.4s ease-in-out infinite }`
- `@keyframes dotPulse { 0%,100% { opacity:0.3; transform:translateY(0) } 50% { opacity:1; transform:translateY(-2px) } }`
- Cada dot tiene delay distinto: 0s, 0.2s, 0.4s
### Detección en JS
`renderAgentMessage()` detecta si el texto incluye "🎤 Procesando" y renderiza con la clase `.working` para dar el estilo visual.
### Comportamiento futuro
- El "procesando" es transitorio: desaparece cuando se renderiza la respuesta final (audio + texto)
- No se acumulan múltiples "procesando"
---
## ✅ REGLA FINAL: Audio solo, sin texto intermedio
### Regla
Cuando STEREO1 envía un audio, el agente no debe enviar NINGÚN texto intermedio:
- ❌ No "🎤 Procesando..."
- ❌ No texto de confirmación
- ❌ No indicadores de estado
- ✅ Solo el audio de respuesta directamente
### Motivo
STEREO1 ya sabe que el agente está trabajando cuando envía un audio. El "procesando" es redundante y molesto. En el portal, la comunicación será mayormente audio.
### Cuándo enviar texto
- Solo cuando STEREO1 envía TEXTO → responder con texto
- Si el agente necesita preguntar algo que no requiere audio → texto está bien
- Pero por defecto: audio cuando él envía audio, sin nada intermedio
---
## 🔴 CORREGIR: Heartbeat muestra texto o repite audio
### Problema original (ya documentado)
El heartbeat repetía el último audio cuando STEREO1 estaba enviando uno nuevo.
### Problema persistente
Aunque se documentó, el heartbeat seguía respondiendo con `HEARTBEAT_OK`, que aparece como TEXTO en el chat entre los audios de STEREO1 y la respuesta del agente.
### Solución definitiva
En sesión activa (últimos 5 minutos de interacción) el heartbeat responde con NO_REPLY. Eso significa:
- ❌ No "HEARTBEAT_OK"
- ❌ No audio repetido
- ❌ No texto de ningún tipo
- ✅ Silencio absoluto mientras se procesa el audio de STEREO1
### Causa raíz
El sistema de heartbeat no tiene contexto de "estoy procesando un audio ahora". La solución manual es: cuando el agente está en medio de transcripción o ha habido interacción reciente, responder NO_REPLY en lugar de cualquier contenido visible.
🎯 Galería de archivos
Este panel ahora es una galería visual.
🖼️ Imágenes
Se ven en miniatura. Tocás la imagen o el botón ⬇ Descargar y se baja.
📁 Carpetas
Tocás una carpeta y entrás a ver su contenido. Las imágenes aparecen como galería adentro. Usá el breadcrumb (ruta arriba) para volver.
📄 Otros archivos
ZIPs, PDFs, backups — se muestran con su icono y botón de descarga.
🗣️ El agente crea las carpetas
Decime "creá una carpeta logos" o "poné estas imágenes en una carpeta" y yo armo la estructura.
🎯 Editor Visual de Flyers
Seleccioná uno de los 4 slots de arriba para empezar a editar. Una vez adentro del slot, aparecen los controles de edición abajo.
🎮 Controles de edición (joystick)
Una vez dentro de un slot, seleccionás un elemento del flyer y usás estos botones:
• ◀ ▶ — mover horizontal
• ▲ ▼ — mover vertical
• − + — achicar/agrandar elemento
• A− A+ — reducir/agrandar texto
• 👁 — mostrar/ocultar bordes de selección
• 💾 — exportar a JSON
🗣️ Modo óptimo: pedile al agente
No hace falta mover nada manual. Decile al chat qué flyer querés y ella lo arma:
• Desde cero: "Diseñame un flyer para promocionar X. Fondo oscuro, logo arriba al centro, título grande"
• Con imágenes: "Usá esta foto como fondo, y poné esta otra imagen arriba a la derecha"
• Ajustes finos: "el logo está muy grande, reducirlo", "subí el título", "mové la imagen detrás del texto"
📐 Regla importante
Todo lo que ves en la previsualización se exporta idéntico. Los cambios se miden en porcentajes.
💡 Pro tip
Mandale las imágenes al chat, decile qué flyer querés, y ella arma la composición. Después afinás detalles.