Cómo hacer un editor de síntesis de voz

 

 

 

  • ¡Registro!
  • SmashingConf UX y diseño, Amberes 2024

  • Índice
    1. ¿Qué es SSML?
    2. El editor de texto portátil
      1. Instalación de cordura
    3. Cómo configurar esquemas en Sanity Studio
      1. Creando los archivos del editor
    4. Agregar SSML al editor
      1. Énfasis
      2. Alias
      3. Prosodia
      4. Saya s
    5. Personalizando la apariencia
    6. Agregar un botón de vista previa usando Texto a voz de Google
      1. Envolviendo el editor en un componente de React
      2. Convertir texto portátil a SSML
    7. Adding A Preview Button That Speaks Back To You
    8. You’ve Created A Speech Synthesis Editor, And Now What?

    Los asistentes de voz están de camino a los hogares, las muñecas y los bolsillos de las personas. Eso significa que parte de nuestro contenido se pronunciará en voz alta con la ayuda de la síntesis de voz digital. En este tutorial, aprenderá cómo crear un editor Lo que obtienes es lo que escuchas (WYGIWYH) para síntesis de voz utilizando el editor de texto portátil de Sanity.io .

     

    Cuando Steve Jobs presentó el Macintosh en 1984, nos dijo “Hola” desde el escenario. Incluso en ese momento, la síntesis de voz no era realmente una tecnología nueva: Bell Labs desarrolló el vocoder ya a finales de los años 30, y el concepto de una computadora asistente de voz se hizo popular cuando Stanley Kubrick convirtió el vocoder en la voz de HAL9000 en 2001: Una odisea en el espacio (1968).

    No fue antes de la introducción de Siri, Amazon Echo y Google Assistant de Apple a mediados de la década de 2015 que las interfaces de voz llegaron a los hogares, las muñecas y los bolsillos de un público más amplio. Todavía estamos en una fase de adopción, pero parece que estos asistentes de voz llegaron para quedarse.

    En otras palabras, la Web ya no es sólo texto pasivo en una pantalla . Los editores web y los diseñadores de UX deben acostumbrarse a crear contenidos y servicios de los que se debe hablar en voz alta.

    Ya estamos avanzando rápidamente hacia el uso de sistemas de gestión de contenido que nos permitan trabajar con nuestro contenido sin cabeza y a través de API. La pieza final es crear interfaces editoriales que faciliten la adaptación del contenido a la voz. ¡Así que hagamos justamente eso!

    ¿Qué es SSML?

    Mientras que los navegadores web utilizan la especificación W3C para el lenguaje de marcado de hipertexto (HTML) para representar visualmente documentos, la mayoría de los asistentes de voz utilizan el lenguaje de marcado de síntesis de voz (SSML) al generar voz.

    Un ejemplo mínimo usando el elemento raíz speaky las etiquetas de párrafo ( p) y oración ( s):

    speak p sThis is the first sentence of the paragraph./s sHere’s another sentence./s /p/speak

    Donde SSML comienza a existir es cuando introducimos etiquetas para emphasisy prosody(pitch):

    speak p sPut some emphasis strength="strong"extra weight on these words/emphasis/s sAnd say prosody pitch="high" rate="fast"this a bit higher and faster/prosody!/s /p/speak

    SSML tiene más funciones, pero esto es suficiente para tener una idea de lo básico. Ahora, echemos un vistazo más de cerca al editor que usaremos para crear la interfaz de edición de síntesis de voz.

    El editor de texto portátil

    Para crear este editor, usaremos el editor de texto portátil que aparece en Sanity.io. Portable Text es una especificación JSON para la edición de texto enriquecido, que se puede serializar en cualquier lenguaje de marcado, como SSML. Esto significa que puede utilizar fácilmente el mismo fragmento de texto en varios lugares utilizando diferentes lenguajes de marcado.

    Editor predeterminado de Sanity.io para texto portátil ( vista previa grande )

     

    Instalación de cordura

    Sanity.io es una plataforma para contenido estructurado que viene con un entorno de edición de código abierto creado con React.js. Se necesitan dos minutos para que todo esté en funcionamiento.

    Escribe npm i -g @sanity/cli sanity initen tu terminal y sigue las instrucciones. Elija "vacío" cuando se le solicite una plantilla de proyecto.

    Si no desea seguir este tutorial y crear este editor desde cero, también puede clonar el código de este tutorial y seguir las instrucciones en README.md.

    Cuando se descarga el editor, lo ejecuta sanity starten la carpeta del proyecto para iniciarlo. Iniciará un servidor de desarrollo que utiliza Hot Module Reloading para actualizar los cambios a medida que edita sus archivos.

    Cómo configurar esquemas en Sanity Studio

    Creando los archivos del editor

    Comenzaremos creando una carpeta llamada ssml-editor en la carpeta /schemas . En esa carpeta, pondremos algunos archivos vacíos:

    /ssml-tutorial/schemas/ssml-editor ├── alias.js ├── emphasis.js ├── annotations.js ├── preview.js ├── prosody.js ├── sayAs.js ├── blocksToSSML.js ├── speech.js ├── SSMLeditor.css └── SSMLeditor.js

    Ahora podemos agregar esquemas de contenido en estos archivos. Los esquemas de contenido son los que definen la estructura de datos para el texto enriquecido y lo que Sanity Studio utiliza para generar la interfaz editorial. Son objetos JavaScript simples que en su mayoría requieren solo a namey a type.

    También podemos agregar a titley a descriptionpara hacerlo un poco más agradable para los editores. Por ejemplo, este es un esquema para un campo de texto simple para title:

    export default { name: 'title', type: 'string', title: 'Title', description: 'Titles should be short and descriptive'}

    El estudio con nuestro campo de título y el editor predeterminado ( vista previa grande )

    Portable Text se basa en la idea de texto enriquecido como datos. Esto es poderoso porque le permite consultar su texto enriquecido y convertirlo en prácticamente cualquier marcado que desee.

    Es una serie de objetos llamados "bloques" que puedes considerar como "párrafos". En un bloque, hay una serie de tramos secundarios. Cada bloque puede tener un estilo y un conjunto de definiciones de marcas, que describen estructuras de datos distribuidas en los tramos secundarios.

    Sanity.io viene con un editor que puede leer y escribir en texto portátil y se activa colocando el blocktipo dentro de un arraycampo, como este:

    // speech.jsexport default { name: 'speech', type: 'array', title: 'SSML Editor', of: [ { type: 'block' } ]}

    Una matriz puede ser de varios tipos. Para un editor SSML, estos podrían ser bloques para archivos de audio, pero eso queda fuera del alcance de este tutorial.

     

    Lo último que queremos hacer es agregar un tipo de contenido donde se pueda usar este editor. La mayoría de los asistentes utilizan un modelo de contenido simple de "intenciones" y "cumplimientos":

    • Intenciones
      Generalmente, una lista de cadenas utilizadas por el modelo de IA para delinear lo que el usuario quiere hacer.
    • Cumplimientos
      Esto sucede cuando se identifica una “intención”. Una satisfacción a menudo viene, o al menos, viene con algún tipo de respuesta.

    Entonces, creemos un tipo de contenido simple llamado fulfillmentque use el editor de síntesis de voz. Cree un nuevo archivo llamado cumplimiento.js y guárdelo en la carpeta /schema :

    // fulfillment.jsexport default { name: 'fulfillment', type: 'document', title: 'Fulfillment', of: [ { name: 'title', type: 'string', title: 'Title', description: 'Titles should be short and descriptive' }, { name: 'response', type: 'speech' } ]}

    Guarde el archivo y abra esquema.js . Agrégalo a tu estudio así:

    // schema.jsimport createSchema from 'part:@sanity/base/schema-creator'import schemaTypes from 'all:part:@sanity/base/schema-type'import fullfillment from './fullfillment'import speech from './speech'export default createSchema({ name: 'default', types: schemaTypes.concat([ fullfillment, speech, ])})

    Si ahora ejecuta sanity startsu interfaz de línea de comandos dentro de la carpeta raíz del proyecto, el estudio se iniciará localmente y podrá agregar entradas para los cumplimientos. Puedes mantener el estudio en funcionamiento mientras continuamos, ya que se recargará automáticamente con nuevos cambios cuando guardes los archivos.

    Agregar SSML al editor

    De forma predeterminada, el blocktipo le brindará un editor estándar para texto enriquecido orientado visualmente con estilos de encabezado, estilos de decorador para énfasis y fuerza, anotaciones para enlaces y listas. Ahora queremos anular aquellos con los conceptos auditivos que se encuentran en SSML.

    Comenzamos definiendo las diferentes estructuras de contenido, con descripciones útiles para los editores, que agregaremos a blockSSMLeditorSchema.js como configuraciones para annotations. Esos son "énfasis", "alias", "prosodia" y "decir como".

    Énfasis

    Comenzamos con "énfasis", que controla cuánto peso se le da al texto marcado. Lo definimos como una cadena con una lista de valores predefinidos entre los que el usuario puede elegir:

    // emphasis.jsexport default { name: 'emphasis', type: 'object', title: 'Emphasis', description: 'The strength of the emphasis put on the contained text', fields: [ { name: 'level', type: 'string', options: { list: [ { value: 'strong', title: 'Strong' }, { value: 'moderate', title: 'Moderate' }, { value: 'none', title: 'None' }, { value: 'reduced', title: 'Reduced' } ] } } ]}

    Alias

    A veces el término escrito y hablado difieren. Por ejemplo, desea utilizar la abreviatura de una frase en un texto escrito, pero hacer que se lea la frase completa en voz alta. Por ejemplo:

     

    sThis is a sub alias="Speech Synthesis Markup Language"SSML/sub tutorial/s

    El campo de entrada para el alias es una cadena simple:

    // alias.jsexport default { name: 'alias', type: 'object', title: 'Alias (sub)', description: 'Replaces the contained text for pronunciation. This allows a document to contain both a spoken and written form.', fields: [ { name: 'text', type: 'string', title: 'Replacement text', } ]}

    Prosodia

    Con la propiedad de la prosodia podemos controlar diferentes aspectos de cómo se debe pronunciar el texto, como el tono, la velocidad y el volumen. El marcado para esto puede verse así:

    sSay this with an prosody pitch="x-low"extra low pitch/prosody, and this prosody rate="fast" volume="loud"loudly with a fast rate/prosody/s

    Esta entrada tendrá tres campos con opciones de cadena predefinidas:

    // prosody.jsexport default { name: 'prosody', type: 'object', title: 'Prosody', description: 'Control of the pitch, speaking rate, and volume', fields: [ { name: 'pitch', type: 'string', title: 'Pitch', description: 'The baseline pitch for the contained text', options: { list: [ { value: 'x-low', title: 'Extra low' }, { value: 'low', title: 'Low' }, { value: 'medium', title: 'Medium' }, { value: 'high', title: 'High' }, { value: 'x-high', title: 'Extra high' }, { value: 'default', title: 'Default' } ] } }, { name: 'rate', type: 'string', title: 'Rate', description: 'A change in the speaking rate for the contained text', options: { list: [ { value: 'x-slow', title: 'Extra slow' }, { value: 'slow', title: 'Slow' }, { value: 'medium', title: 'Medium' }, { value: 'fast', title: 'Fast' }, { value: 'x-fast', title: 'Extra fast' }, { value: 'default', title: 'Default' } ] } }, { name: 'volume', type: 'string', title: 'Volume', description: 'The volume for the contained text.', options: { list: [ { value: 'silent', title: 'Silent' }, { value: 'x-soft', title: 'Extra soft' }, { value: 'medium', title: 'Medium' }, { value: 'loud', title: 'Loud' }, { value: 'x-loud', title: 'Extra loud' }, { value: 'default', title: 'Default' } ] } } ]}

    Saya s

    El último que queremos incluir es say-as. Esta etiqueta nos permite ejercer un poco más de control sobre cómo se pronuncia cierta información. Incluso podemos usarlo para silenciar palabras si necesita redactar algo en las interfaces de voz. ¡Eso es @!%© útil!

    sDo I have to say-as interpret-as="expletive"frakking/say-as say-as interpret-as="verbatim"spell/say-as it out for you!?/s

    Letras en Graffiti Gratis | Descubre Todos los Estilos

     

    // sayAs.jsexport default { name: 'sayAs', type: 'object', title: 'Say as...', description: 'Lets you indicate information about the type of text construct that is contained within the element. It also helps specify the level of detail for rendering the contained text.', fields: [ { name: 'interpretAs', type: 'string', title: 'Interpret as...', options: { list: [ { value: 'cardinal', title: 'Cardinal numbers' }, { value: 'ordinal', title: 'Ordinal numbers (1st, 2nd, 3th...)' }, { value: 'characters', title: 'Spell out characters' }, { value: 'fraction', title: 'Say numbers as fractions' }, { value: 'expletive', title: 'Blip out this word' }, { value: 'unit', title: 'Adapt unit to singular or plural' }, { value: 'verbatim', title: 'Spell out letter by letter (verbatim)' }, { value: 'date', title: 'Say as a date' }, { value: 'telephone', title: 'Say as a telephone number' } ] } }, { name: 'date', type: 'object', title: 'Date', fields: [ { name: 'format', type: 'string', description: 'The format attribute is a sequence of date field character codes. Supported field character codes in format are {y, m, d} for year, month, and day (of the month) respectively. If the field code appears once for year, month, or day then the number of digits expected are 4, 2, and 2 respectively. If the field code is repeated then the number of expected digits is the number of times the code is repeated. Fields in the date text may be separated by punctuation and/or spaces.' }, { name: 'detail', type: 'number', validation: Rule = Rule.required() .min(0) .max(2), description: 'The detail attribute controls the spoken form of the date. For detail='1' only the day fields and one of month or year fields are required, although both may be supplied' } ] } ]}

    Ahora podemos importarlos en un archivo annotations.js , lo que hace que todo esté un poco más ordenado.

    // annotations.jsexport {default as alias} from './alias'export {default as emphasis} from './emphasis'export {default as prosody} from './prosody'export {default as sayAs} from './sayAs'

    Ahora podemos importar estos tipos de anotaciones a nuestros esquemas principales:

    // schema.jsimport createSchema from "part:@sanity/base/schema-creator"import schemaTypes from "all:part:@sanity/base/schema-type"import fulfillment from './fulfillment'import speech from './ssml-editor/speech'import { alias, emphasis, prosody, sayAs} from './annotations'export default createSchema({ name: "default", types: schemaTypes.concat([ fulfillment, speech, alias, emphasis, prosody, sayAs ])})

    Finalmente, ahora podemos agregarlos al editor de esta manera:

    // speech.jsexport default { name: 'speech', type: 'array', title: 'SSML Editor', of: [ { type: 'block', styles: [], lists: [], marks: { decorators: [], annotations: [ {type: 'alias'}, {type: 'emphasis'}, {type: 'prosody'}, {type: 'sayAs'} ] } } ]}

    Observe que también agregamos matrices vacías a stylesy decorators. Esto desactiva los estilos y decoradores predeterminados (como negrita y énfasis), ya que no tienen mucho sentido en este caso específico.

     

    Personalizando la apariencia

    Ahora tenemos la funcionalidad implementada, pero como no hemos especificado ningún ícono, cada anotación usará el ícono predeterminado, lo que hace que el editor sea difícil de usar para los autores. ¡Así que arreglemos eso!

    Con el editor de Texto Portátil es posible inyectar componentes de React tanto para los íconos como para cómo se debe representar el texto marcado. Aquí, simplemente dejaremos que algunos emoji hagan el trabajo por nosotros, pero obviamente puedes llegar lejos con esto, haciéndolos dinámicos, etc. Pues prosodyincluso haremos que el icono cambie dependiendo del volumen seleccionado. Tenga en cuenta que omití los campos en estos fragmentos por motivos de brevedad; no debe eliminarlos de sus archivos locales.

    // alias.jsimport React from 'react'export default { name: 'alias', type: 'object', title: 'Alias (sub)', description: 'Replaces the contained text for pronunciation. This allows a document to contain both a spoken and written form.', fields: [ /* all the fields */ ], blockEditor: { icon: () = ' ', render: ({ children }) = span{children} /span, },};
    // emphasis.jsimport React from 'react'export default { name: 'emphasis', type: 'object', title: 'Emphasis', description: 'The strength of the emphasis put on the contained text', fields: [ /* all the fields */ ], blockEditor: { icon: () = ' ', render: ({ children }) = span{children} /span, },};
    // prosody.jsimport React from 'react'export default { name: 'prosody', type: 'object', title: 'Prosody', description: 'Control of the pitch, speaking rate, and volume', fields: [ /* all the fields */ ], blockEditor: { icon: () = ' ', render: ({ children, volume }) = ( span {children} {['x-loud', 'loud'].includes(volume) ? ' ' : ' '} /span ), },};
    // sayAs.jsimport React from 'react'export default { name: 'sayAs', type: 'object', title: 'Say as...', description: 'Lets you indicate information about the type of text construct that is contained within the element. It also helps specify the level of detail for rendering the contained text.', fields: [ /* all the fields */ ], blockEditor: { icon: () = ' ', render: props = span{props.children} /span, },};

    El editor con nuestras marcas SSML personalizadas ( vista previa grande )

    Ahora tiene un editor para editar texto que pueden utilizar los asistentes de voz. ¿Pero no sería útil si los editores también pudieran obtener una vista previa de cómo sonará realmente el texto?

    Agregar un botón de vista previa usando Texto a voz de Google

    La compatibilidad con la síntesis de voz nativa está en camino para los navegadores. Pero en este tutorial usaremos la API Text-to-Speech de Google que admite SSML. La creación de esta funcionalidad de vista previa también será una demostración de cómo serializar texto portátil en SSML en cualquier servicio para el que desee utilizarlo.

     

    Envolviendo el editor en un componente de React

    Comenzamos abriendo el archivo SSMLeditor.js y agregamos el siguiente código:

    // SSMLeditor.jsimport React, { Fragment } from 'react';import { BlockEditor } from 'part:@sanity/form-builder';export default function SSMLeditor(props) { return ( Fragment BlockEditor {...props} / /Fragment );}

    Ahora hemos incluido el editor en nuestro propio componente React. Todos los accesorios que necesita, incluidos los datos que contiene, se transmiten en tiempo real. Para utilizar realmente este componente, debe importarlo a su speech.jsarchivo:

    // speech.jsimport React from 'react'import SSMLeditor from './SSMLeditor.js'export default { name: 'speech', type: 'array', title: 'SSML Editor', inputComponent: SSMLeditor, of: [ { type: 'block', styles: [], lists: [], marks: { decorators: [], annotations: [ { type: 'alias' }, { type: 'emphasis' }, { type: 'prosody' }, { type: 'sayAs' }, ], }, }, ],}

    Cuando guardas esto y el estudio se recarga, debería verse prácticamente igual, pero eso se debe a que aún no hemos comenzado a modificar el editor.

    Convertir texto portátil a SSML

    The editor will save the content as Portable Text, an array of objects in JSON that makes it easy to convert rich text into whatever format you need it to be. When you convert Portable Text into another syntax or format, we call that “serialization”. Hence, “serializers” are the recipes for how the rich text should be converted. In this section, we will add serializers for speech synthesis.

    You have already made the blocksToSSML.js file. Now we’ll need to add our first dependency. Begin by running the terminal command npm init -y inside the ssml-editor folder. This will add a package.json where the editor’s dependencies will be listed.

    Once that’s done, you can run npm install @sanity/block-content-to-html to get a library that makes it easier to serialize Portable Text. We’re using the HTML-library because SSML has the same XML syntax with tags and attributes.

    This is a bunch of code, so do feel free to copy-paste it. I’ll explain the pattern right below the snippet:

    // blocksToSSML.jsimport blocksToHTML, { h } from '@sanity/block-content-to-html'const serializers = { marks: { prosody: ({ children, mark: { rate, pitch, volume } }) = h('prosody', { attrs: { rate, pitch, volume } }, children), alias: ({ children, mark: { text } }) = h('sub', { attrs: { alias: text } }, children), sayAs: ({ children, mark: { interpretAs } }) = h('say-as', { attrs: { 'interpret-as': interpretAs } }, children), break: ({ children, mark: { time, strength } }) = h('break', { attrs: { time: '${time}ms', strength } }, children), emphasis: ({ children, mark: { level } }) = h('emphasis', { attrs: { level } }, children) }}export const blocksToSSML = blocks = blocksToHTML({ blocks, serializers })

    This code will export a function that takes the array of blocks and loop through them. Whenever a block contains a mark, it will look for a serializer for the type. If you have marked some text to have emphasis, it this function from the serializers object:

     

    emphasis: ({ children, mark: { level } }) = h('emphasis', { attrs: { level } }, children)

    Maybe you recognize the parameter from where we defined the schema? The h() function lets us defined an HTML element, that is, here we “cheat” and makes it return an SSML element called emphasis. We also give it the attribute level if that is defined, and place the children elements within it — which in most cases will be the text you have marked up with emphasis.

    { "_type": "block", "_key": "f2c4cf1ab4e0", "style": "normal", "markDefs": [ { "_type": "emphasis", "_key": "99b28ed3fa58", "level": "strong" } ], "children": [ { "_type": "span", "_key": "f2c4cf1ab4e01", "text": "Say this strongly!", "marks": [ "99b28ed3fa58" ] } ]}

    That is how the above structure in Portable Text gets serialized to this SSML:

    emphasis level="strong"Say this strongly/emphasis

    If you want support for more SSML tags, you can add more annotations in the schema, and add the annotation types to the marks section in the serializers.

    Now we have a function that returns SSML markup from our marked up rich text. The last part is to make a button that lets us send this markup to a text-to-speech service.

    Adding A Preview Button That Speaks Back To You

    Ideally, we should have used the browser’s speech synthesis capabilities in the Web API. That way, we would have gotten away with less code and dependencies.

    As of early 2019, however, native browser support for speech synthesis is still in its early stages. It looks like support for SSML is on the way, and there is proof of concepts of client-side JavaScript implementations for it.

    Chances are that you are going to use this content with a voice assistant anyways. Both Google Assistant and Amazon Echo (Alexa) support SSML as responses in a fulfillment. In this tutorial, we will use Google’s text-to-speech API, which also sounds good and support several languages.

    Start by obtaining an API key by signing up for Google Cloud Platform (it will be free for the first 1 million characters you process). Once you’re signed up, you can make a new API key on this page.

    Now you can open your PreviewButton.js file, and add this code to it:

    // PreviewButton.jsimport React from 'react'import Button from 'part:@sanity/components/buttons/default'import { blocksToSSML } from './blocksToSSML'// You should be careful with sharing this key// I put it here to keep the code simpleconst API_KEY = 'yourAPIkey'const GOOGLE_TEXT_TO_SPEECH_URL = 'https://texttospeech.googleapis.com/v1beta1/text:synthesize?key=' + API_KEYconst speak = async blocks = { // Serialize blocks to SSML const ssml = blocksToSSML(blocks) // Prepare the Google Text-to-Speech configuration const body = JSON.stringify({ input: { ssml }, // Select the language code and voice name (A-F) voice: { languageCode: 'en-US', name: 'en-US-Wavenet-A' }, // Use MP3 in order to play in browser audioConfig: { audioEncoding: 'MP3' } }) // Send the SSML string to the API const res = await fetch(GOOGLE_TEXT_TO_SPEECH_URL, { method: 'POST', body }).then(res = res.json()) // Play the returned audio with the Browser’s Audo API const audio = new Audio('data:audio/wav;base64,' + res.audioContent) audio.play()}export default function PreviewButton (props) { return Button style={{ marginTop: '1em' }} onClick={() = speak(props.blocks)}Speak text/Button}

    I’ve kept this preview button code to a minimal to make it easier to follow this tutorial. Of course, you could build it out by adding state to show if the preview is processing or make it possible to preview with the different voices that Google’s API supports.

    Add the button to SSMLeditor.js:

    // SSMLeditor.jsimport React, { Fragment } from 'react';import { BlockEditor } from 'part:@sanity/form-builder';import PreviewButton from './PreviewButton';export default function SSMLeditor(props) { return ( Fragment BlockEditor {...props} / PreviewButton blocks={props.value} / /Fragment );}

    Now you should be able to mark up your text with the different annotations, and hear the result when pushing “Speak text”. Cool, isn’t it?

    You’ve Created A Speech Synthesis Editor, And Now What?

    If you have followed this tutorial, you have been through how you can use the editor for Portable Text in Sanity Studio to make custom annotations and customize the editor. You can use these skills for all sorts of things, not only to make a speech synthesis editor. You have also been through how to serialize Portable Text into the syntax you need. Obviously, this is also handy if you’re building frontends in React or Vue. You can even use these skills to generate Markdown from Portable Text.

    We haven’t covered how you actually use this together with a voice assistant. If you want to try, you can use much of the same logic as with the preview button in a serverless function, and set it as the API endpoint for a fulfillment using webhooks, e.g. with Dialogflow.

    If you’d like me to write a tutorial on how to use the speech synthesis editor with a voice assistant, feel free to give me a hint on Twitter or share in the comments section below.

    Further Reading on SmashingMag:






    Tal vez te puede interesar:

    1. ¿Deberían abrirse los enlaces en ventanas nuevas?
    2. 24 excelentes tutoriales de AJAX
    3. 70 técnicas nuevas y útiles de AJAX y JavaScript
    4. Más de 45 excelentes recursos y repositorios de fragmentos de código

    Cómo hacer un editor de síntesis de voz

    Cómo hacer un editor de síntesis de voz

    ¡Registro! SmashingConf UX y diseño, Amberes 2024 Índice ¿Qué es SSML?

    programar

    es

    https://aprendeprogramando.es/static/images/programar-como-hacer-un-editor-de-sintesis-de-voz-975-0.jpg

    2024-05-20

     

    Cómo hacer un editor de síntesis de voz
    Cómo hacer un editor de síntesis de voz

     

     

    Top 20