ApisVoice: Dictado por Voz en Cualquier Web con 10 Idiomas
Extension de Chrome MV3 que convierte voz en texto en Gmail, ChatGPT, LinkedIn, Notion y cualquier web. Web Speech API del navegador, sin servidores propios, sin cuenta, sin telemetria. 10 idiomas.

Galería del Proyecto





Tecnologías Utilizadas
Sobre el Proyecto
Que es ApisVoice
ApisVoice es una extension de Google Chrome Manifest V3 que convierte la voz en texto dentro de cualquier pagina web. Inyecta un icono flotante en cada pestana, escucha al usuario, transcribe con la Web Speech API del navegador y escribe el resultado directamente en el campo donde tienes el cursor (input, textarea, contenteditable). En Google Docs el texto se copia automaticamente al portapapeles.
La pieza clave del proyecto es la privacidad por diseno: ApisDom (el responsable) NO esta en la cadena del audio. El audio lo procesa la Web Speech API del propio navegador. ApisVoice no tiene cuenta, no tiene servidores propios, no tiene telemetria y no toca el contenido de las paginas que visitas. Lo unico que se guarda es chrome.storage.local con tus preferencias: idioma, posicion del icono por dominio, dominios donde lo has ocultado y los timers de pausa que tu eliges.
| Pieza | Valor |
|---|---|
| Manifest version | 3 |
| Version publicada | 1.0.0 |
| Chrome minimo | 100 |
| Locale por defecto | es (con traduccion completa a en) |
| Atajo de teclado | Alt+Shift+V (configurable en chrome://extensions/shortcuts) |
| Idiomas de dictado | 10 verificados |
| Permisos | activeTab, storage, scripting |
| Host permissions | <all_urls> (solo para inyectar el icono, NO se lee contenido) |
| Pais y autor | ApisDom (apisdom.com) |
| Pagina oficial | https://app.apisdom.com/apisvoice/ |
| Politica de privacidad | https://app.apisdom.com/privacidad/apisvoice/ |
| Preguntas frecuentes | https://app.apisdom.com/apisvoice/preguntas/ |
Los 10 idiomas de dictado
Unica fuente de verdad en src/config/languages.js. Cada idioma tiene nombre en espanol y en ingles para mostrarse correctamente segun el locale del navegador.
| Codigo BCP-47 | Idioma |
|---|---|
en-US | Ingles (Estados Unidos) |
en-GB | Ingles (Reino Unido) |
es-ES | Espanol (Espana) (idioma por defecto) |
es-MX | Espanol (Mexico) |
fr-FR | Frances (Francia) |
de-DE | Aleman (Alemania) |
it-IT | Italiano (Italia) |
pt-BR | Portugues (Brasil) |
ja-JP | Japones |
ko-KR | Coreano |
Tres piezas del producto
| Pieza | Rol | Tecnologia |
|---|---|---|
Popup (popup.html) | UI con 3 pestanas: Privacidad, Como funciona, Configuracion. Test de microfono, sliders de pausas, gestion de dominios ocultos | HTML + CSS + JS modulo ESM |
Service worker (background.js) | Atajo de teclado, apertura de bienvenida en onInstalled, reinyeccion de content script en pestanas ya abiertas tras update | Chrome chrome.runtime, chrome.commands, chrome.scripting |
Content script (content.js) | Icono flotante en Shadow DOM closed sobre cada pestana, reconocimiento de voz, insercion de texto en el campo activo, drag, persistencia por dominio | Shadow DOM, Web Speech API, SVG inline |
Stack tecnico real
| Pieza | Version / Detalle |
|---|---|
| Chrome Extension Manifest | V3 |
| JavaScript | ESM nativo (type: "module" en background y popup) |
| TypeScript (verificadores) | 5.6 |
| tsx (runner de verificadores) | 4.19 |
| Web Speech API | webkitSpeechRecognition con fallback a SpeechRecognition |
| Chrome APIs | runtime, commands, tabs, scripting, storage.local |
| Audio (test de microfono) | getUserMedia + AudioContext + AnalyserNode (RMS pico) |
| Insercion de texto | setRangeText, execCommand('insertText'), Clipboard API |
| Aislamiento UI | Shadow DOM mode closed en el icono flotante |
| Iconografia | SVG inline sobre viewBox 0..400, overlays propios (aro, X, botones de armado, minidisplay) |
| Landing local de desarrollo | Node + Express 4.21 + open 11 (server.js) |
| Verificadores automaticos | 9 scripts bloqueantes en npm run verify |
Información Técnica
apisvoice-dictado-por-voz-en-cualquier-web-con-10-idiomas
rJTsMwwadHrArMXt73AW
Documentación Específica del Proyecto
Tres procesos, tres responsabilidades
ApisVoice respeta el modelo de proceso de Manifest V3: cada pieza tiene una unica responsabilidad y los limites estan definidos en manifest.json.
| Proceso | Archivo | Responsabilidad | Limitaciones MV3 que se respetan |
|---|---|---|---|
| Service worker | background.js | Atajo de teclado, apertura de welcome.html en primera instalacion, reinyeccion de content script en pestanas ya abiertas tras update, mantener vivo el port para que los content scripts huerfanos detecten la recarga | Sin DOM, sin window, sin alert. Solo imports ESM estaticos (los dinamicos estan prohibidos en service workers) |
| Content script | content.js (1.529 lineas) | Inyectar el icono flotante en cada pagina (<all_urls>), reconocer voz con webkitSpeechRecognition, insertar el texto en el campo activo, persistir posicion y estado por dominio | Aislamiento total con Shadow DOM mode: "closed". Guard idempotente con window.__apisvoiceLoaded. Limpieza de huerfanos con marcador data-apisvoice-host |
| Popup | popup.html + popup.js | UI con 3 pestanas (Privacidad, Como funciona, Configuracion). Live status bar, sliders de pausas con debounce de 300 ms, test de microfono, gestion de dominios ocultos | Modulo ESM. NO se ejecuta dictado aqui, solo configuracion |
Pipeline del dictado (de la voz al campo de texto)
| Paso | Que ocurre | Codigo |
|---|---|---|
| 1. Trigger | Usuario pulsa el icono flotante O el atajo Alt+Shift+V | chrome.commands.onCommand envia TOGGLE_DICTATION al content script de la pestana activa |
| 2. Guard de dominio | Si el dominio esta en hiddenDomains o en domainArmed = false, no se hace nada | src/storage/preferences.js |
| 3. Permiso de microfono | Chrome muestra dialogo nativo. Si denegado, popup detecta y ofrece abrir chrome://settings/content/microphone | Web Speech API + handler not-allowed |
| 4. Reconocimiento | webkitSpeechRecognition con continuous: true, interimResults: true, idioma del usuario (default es-ES) | src/utils/recognition.js |
| 5. Manejo de errores | 4 errores fatales (not-allowed, service-not-allowed, audio-capture, language-not-supported) detienen sin reintentar. network reintenta 3 veces con backoff 1s/2s/3s | FATAL_ERRORS + MAX_RETRIES_NETWORK = 3 |
| 6. Puntuacion automatica | Si delta entre resultado anterior y actual supera pauseCommaMs (default 800 ms) o pausePeriodMs (default 1500 ms), se inserta , o . | src/utils/punctuation.js |
| 7. Capitalizacion | Si el texto previo termina en . ! ? (o esta vacio), la primera letra del nuevo fragmento va en mayuscula | src/utils/capitalization.js |
| 8. Insercion | Prioridad: caso especial Google Docs (clipboard), <input>/<textarea> con foco (setRangeText), [contenteditable] (execCommand('insertText') con fallback a InputEvent y a clipboard) | src/utils/insertText.js |
Caso especial Google Docs
Google Docs renderiza el documento en canvas, no en DOM. setRangeText y execCommand no funcionan. ApisVoice detecta el hostname docs.google.com y conmuta directamente a Clipboard API. El usuario pega con Ctrl+V. Toast info.copied lo avisa.
Persistencia: 8 claves en chrome.storage.local
Toda la persistencia pasa por una unica capa (src/storage/preferences.js, regla ARCH1 del proyecto). Cualquier otro archivo que necesite leer o escribir DEBE importar desde aqui.
| Clave | Valor |
|---|---|
apisvoice.hiddenDomains | string[] con los dominios donde el usuario oculto el icono con la X |
apisvoice.iconPosition | { [domain]: { x, y } } posicion del icono por dominio |
apisvoice.domainArmed | { [domain]: boolean } toggle armado/apagado por dominio (default armado) |
apisvoice.language | Codigo BCP-47 del idioma de dictado elegido |
apisvoice.pauseCommaMs | Timer personalizado para insertar coma (rango 500 a 1500 ms, step 50) |
apisvoice.pausePeriodMs | Timer personalizado para insertar punto (rango 1000 a 3000 ms, step 50) |
apisvoice.installDate | Timestamp ms de la primera instalacion (lo escribe background.js en onInstalled) |
apisvoice.reviewPromptShown | true cuando ya se mostro el modal de pedir resena (no vuelve a salir en esa instalacion) |
Por que un Shadow DOM closed y no un DOM normal
La extension inyecta su UI en cualquier pagina web (host_permissions: <all_urls>). Sin aislamiento, los estilos y scripts de la pagina podrian romper el icono o, peor, leerlo. Solucion: attachShadow({ mode: 'closed' }).
| Decision | Razon |
|---|---|
Shadow DOM closed | La pagina huesped no puede acceder al arbol del icono ni leer sus listeners. Aislamiento total de CSS y JS |
| Logo intacto, overlays propios | El SVG del logo no se modifica. Aro perimetral, X de cerrar, botones verde/rojo de armado y minidisplay se construyen como overlays SVG sobre el mismo viewBox 0..400 |
z-index: 2147483647 | Limite maximo de int32. No garantiza estar siempre por encima (un stacking context superior puede taparlo) pero minimiza el riesgo |
Marcador data-apisvoice-host="true" | Permite detectar y limpiar iconos huerfanos de una version anterior tras una recarga de la extension |
Guard window.__apisvoiceLoaded | Evita duplicar el icono cuando el bootstrap se dispara dos veces (manifest content_scripts + reinyeccion via chrome.scripting.executeScript) |
Drag con umbral de 5 px
Para distinguir un click intencional de un drag, ApisVoice usa el umbral DRAG_THRESHOLD_PX = 5. Si el movimiento del raton es menor de 5 px se trata como click (activa o detiene dictado). A partir de 5 px, modo drag: el usuario reposiciona el icono y la nueva posicion se guarda en chrome.storage.local bajo apisvoice.iconPosition[domain].
Colision con widgets de soporte conocidos
Si la pagina huesped ya tiene un widget de chat de soporte en la esquina inferior derecha, ApisVoice se reposiciona en bottom-left por defecto para no taparlo. Lista intencionalmente corta y conservadora.
| Ecosistema detectado | Selectores comprobados al cargar |
|---|---|
| Intercom | #intercom-container, iframe[name="intercom-frame"], .intercom-launcher |
| HubSpot | #hubspot-messages-iframe-container |
| Zendesk | #zendesk-launcher, iframe#launcher |
| Drift | #drift-frame-controller, #drift-widget |
| Crisp | #crisp-chatbox, .crisp-client |
Estados visuales del aro perimetral
| Estado | Color del aro | Significado |
|---|---|---|
READY | Verde | Listo para grabar |
RECORDING | Rojo | Grabando audio |
PAUSED | Naranja | Reconocimiento pausado |
ERROR | Rojo intenso | Error de Web Speech API (network, permiso denegado, etc.) |
DISARMED | Apagado (toggle del mango) | Icono visible pero la Web Speech API no se invoca aunque el usuario haga click |
Tamano y posicion inicial
| Constante | Valor |
|---|---|
FLOATING_ICON_SIZE_PX | 112 (aprox. 2.1 cm a 96 dpi, confirmado por el usuario el 2026-05-04) |
FLOATING_ICON_MARGIN_PX | 24 (margen desde la esquina cuando el dominio aun no tiene posicion guardada) |
TOAST_DURATION_MS | 2.000 (duracion del toast efimero al cambiar de estado armado/apagado) |
Test de microfono (sin transcribir, sin red)
El boton "Probar microfono" del popup ejecuta runMicTest() durante MIC_TEST_DURATION_MS = 3000 ms. Mide el RMS pico del audio capturado con AnalyserNode.getByteTimeDomainData() sobre fftSize: 512. Si el RMS pico supera MIC_TEST_RMS_THRESHOLD = 0.02 devuelve "Te oigo bien"; si no, avisa que no detecta sonido. NO transcribe. NO envia audio por red. NO afirma precision cientifica (umbral empirico documentado).
Cada cambio en el codigo pasa por 9 verificadores antes de publicar una nueva version en Chrome Web Store. El comando npm run verify (orquestador scripts/verify-all.ts) los ejecuta en secuencia y bloquea la publicacion si alguno falla con codigo de salida distinto de 0.
| # | Verificador | Que comprueba |
|---|---|---|
| 1 | verify-mv3-compliance.ts | Conformidad con Manifest V3 (sin APIs deprecadas, sin chrome.extension.*, sin eval, sin Function() dinamico, service worker correcto) |
| 2 | verify-csp.ts | Content Security Policy: sin scripts inline, sin unsafe-eval, sin recursos remotos en HTML, sin string-as-handler en addEventListener |
| 3 | verify-architecture.ts | Reglas ARCH del proyecto: capa unica para chrome.storage (ARCH1), unica fuente de verdad de idiomas (ARCH7) y de constantes (src/config/constants.js) |
| 4 | verify-no-hardcoded.ts | Sin URLs, sin emails y sin literales user-facing hardcodeados en HTML o JS (todo desde constants.js o src/i18n/*.json) |
| 5 | verify-error-handling.ts | Sin catch vacios, errores logueados, tratamiento de "Extension context invalidated" |
| 6 | verify-runtime-safety.ts | Accesos defensivos a arrays, instanceof antes de tocar el DOM, target.isConnected antes de insertar texto |
| 7 | verify-i18n.ts | Cobertura completa de las claves en _locales/es/messages.json y _locales/en/messages.json, sin claves huerfanas, sin claves usadas sin definir |
| 8 | audit-colors.ts | Sin colores hardcodeados fuera del tema (cada color con justificacion semantica) |
| 9 | verify-code-quality.ts | Funciones por debajo del umbral de complejidad, sin codigo muerto, sin ?? '' que enmascaren bugs en adapters |
Cualquier fallo en uno solo bloquea el avance. La filosofia es: si un verificador no pasa, el codigo no se publica.
Privacidad por diseno: lo que ApisVoice NO hace
| Lo que la extension NO hace | Verificable en |
|---|---|
| No tiene servidores propios | No hay fetch ni XMLHttpRequest saliente en el codigo del producto. La unica red la maneja la Web Speech API del navegador, no la extension |
| No tiene cuenta | No hay flujo de login, no hay tokens, no hay chrome.identity |
| No tiene telemetria | No hay chrome.metricsPrivate, no hay analytics, no hay pings |
| No lee el contenido de las paginas | El content script solo inyecta el icono y escucha eventos de su propio Shadow DOM. No recorre el DOM huesped salvo para detectar colision con widgets conocidos (selectores publicos) |
| No envia el audio a ApisDom | El audio lo procesa webkitSpeechRecognition del navegador. En Chrome ese motor envia el audio a servidores de Google (no on-device en la mayoria de plataformas). ApisDom NO esta en esa cadena. Documentado literalmente en el popup |
| No guarda audio en disco | Las preferencias son texto plano en chrome.storage.local (idioma, posicion del icono, timers, dominios). Ningun blob de audio |
Justificacion de cada permiso
En la pestana "Privacidad" del popup, ApisVoice explica al usuario por que pide cada permiso. Texto literal de src/i18n/es.json:
| Permiso | Justificacion mostrada al usuario |
|---|---|
activeTab | "Inyectar el icono en la pestana activa cuando lo pides" |
scripting | "Colocar el codigo del icono dentro de la pagina" |
storage | "Guardar tus preferencias en tu navegador" |
host_permissions: <all_urls> | "Que el icono pueda aparecer en cualquier web. NO leemos el contenido de las paginas" |
Otras decisiones de calidad verificadas en el codigo
| Decision | Constante / Codigo |
|---|---|
| Reseña no insistente | Modal de "Dejar resena" aparece UNA sola vez por instalacion, tras DAYS_BEFORE_REVIEW_PROMPT = 15 dias. Si el usuario lo descarta, no vuelve a salir |
| Service worker resistente | Listener vacio en chrome.runtime.onConnect para que los content scripts no detecten falsa muerte de la extension |
| Reinyeccion controlada | Tras update, background.js reinyecta content.js SOLO en pestanas con protocolo http(s):// o file:// (las internas chrome://, edge://, about:, view-source: se ignoran sin error) |
| Limpieza de huerfanos | Antes de inyectar el icono nuevo, el bootstrap elimina cualquier [data-apisvoice-host="true"] que haya quedado de una version anterior |
| Manejo de "Extension context invalidated" | Helper isContextInvalidatedError en preferences.js con match case-insensitive sobre todas las variaciones posibles del mensaje, para no spammear la consola del usuario tras un update |
Compartir este Conocimiento
Si este proyecto aporta valor o podría inspirar a otros, considera difundirlo en tu red profesional.
