Tous les produits
Search
Centre de documentation

Intelligent Speech Interaction:Présentation de la fonctionnalité d'horodatage de la synthèse vocale

Dernière mise à jour :Sep 10, 2026

Les horodatages de synthèse vocale indiquent le début et la fin de chaque caractère chinois ou mot anglais dans l'audio synthétisé. Utilisez-les pour synchroniser les sous-titres vidéo, la mise en surbrillance du texte et les mouvements labiaux des avatars virtuels. Activez les horodatages dans la requête de synthèse afin de recevoir les données de synchronisation en même temps que le flux audio.

Limitations

  • Les horodatages au niveau des mots sont disponibles uniquement pour les voix qui prennent en charge les limites de phonèmes au niveau des mots.

  • L'API RESTful de synthèse vocale pour textes courts ne renvoie pas d'horodatages. Utilisez l'API WebSocket ou un SDK correspondant pour les recevoir.

  • Le texte des sous-titres suit la prononciation et peut ne pas correspondre caractère par caractère au texte d'origine. Par exemple, dans une synthèse vocale en chinois, 12 peut être représenté par deux unités de sous-titrage : et . Affichez le texte d'origine et utilisez les données de synchronisation renvoyées pour déterminer les limites des phrases ou les moments de mise en surbrillance. Ne considérez pas les index des sous-titres comme des décalages de caractères dans la chaîne d'origine.

Exemple de code

Cet exemple Java utilise nls-sdk-tts 2.1.6. Il lit l'AppKey du projet et un jeton NLS valide à partir des variables d'environnement, affiche les informations de sous-titrage et enregistre l'audio dans tts_test.wav. Tout fichier existant portant ce nom sera écrasé.

Ajout des dépendances et configuration des identifiants

Ajoutez la dépendance suivante à la section dependencies du projet Maven :

<dependency>
    <groupId>com.alibaba.nls</groupId>
    <artifactId>nls-sdk-tts</artifactId>
    <version>2.1.6</version>
</dependency>

Définissez NLS_APP_KEY et NLS_TOKEN dans l'environnement d'exécution avec l'AppKey du projet et le jeton NLS. Le jeton doit être valide et appartenir au même compte que le projet. Ne substituez pas un jeton STS ou une clé API d'un autre produit.

Synthèse vocale et réception des horodatages

import com.alibaba.fastjson.JSONArray;
import com.alibaba.nls.client.protocol.NlsClient;
import com.alibaba.nls.client.protocol.OutputFormatEnum;
import com.alibaba.nls.client.protocol.SampleRateEnum;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizer;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizerListener;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizerResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

public class SpeechSynthesizerDemo {
    private static String requiredEnv(String name) {
        String value = System.getenv(name);
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException("Missing environment variable: " + name);
        }
        return value;
    }

    public static void main(String[] args) throws Exception {
        String appKey = requiredEnv("NLS_APP_KEY");
        String token = requiredEnv("NLS_TOKEN");
        String endpoint = "wss://nls-gateway-ap-southeast-1.aliyuncs.com/ws/v1";
        NlsClient client = new NlsClient(endpoint, token);

        try (OutputStream audio = Files.newOutputStream(Paths.get("tts_test.wav"))) {
            AtomicReference<String> failure = new AtomicReference<>();
            AtomicBoolean completed = new AtomicBoolean(false);
            AtomicBoolean firstAudio = new AtomicBoolean(true);
            final long start = System.nanoTime();
            SpeechSynthesizerListener listener = new SpeechSynthesizerListener() {
                @Override
                public void onMessage(ByteBuffer message) {
                    if (firstAudio.compareAndSet(true, false)) {
                        System.out.println("First audio latency (ms): "
                                + (System.nanoTime() - start) / 1_000_000);
                    }
                    byte[] bytes = new byte[message.remaining()];
                    message.get(bytes);
                    try {
                        audio.write(bytes);
                    } catch (IOException e) {
                        failure.compareAndSet(null, "Failed to write audio: " + e.getMessage());
                    }
                }

                @Override
                public void onMetaInfo(SpeechSynthesizerResponse response) {
                    JSONArray subtitles = (JSONArray) response.getObject("subtitles");
                    if (subtitles != null) {
                        System.out.println("MetaInfo: " + subtitles.toJSONString());
                    }
                }

                @Override
                public void onComplete(SpeechSynthesizerResponse response) {
                    completed.set(true);
                    System.out.println("SynthesisCompleted: " + response.getTaskId());
                }

                @Override
                public void onFail(SpeechSynthesizerResponse response) {
                    failure.set("task_id=" + response.getTaskId()
                            + ", status=" + response.getStatus()
                            + ", status_text=" + response.getStatusText());
                }
            };

            SpeechSynthesizer synthesizer = new SpeechSynthesizer(client, listener);
            try {
                synthesizer.setAppKey(appKey);
                synthesizer.setFormat(OutputFormatEnum.WAV);
                synthesizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_16K);
                synthesizer.setVoice("siyue");
                synthesizer.setPitchRate(100);
                synthesizer.setSpeechRate(100);
                synthesizer.setText("Hello world. I have 12 apples.");
                synthesizer.addCustomedParam("enable_subtitle", true);
                synthesizer.start();
                synthesizer.waitForComplete();
                if (failure.get() != null) {
                    throw new IllegalStateException(failure.get());
                }
                if (!completed.get()) {
                    throw new IllegalStateException("Synthesis did not complete");
                }
                System.out.println("Audio saved to tts_test.wav");
            } finally {
                synthesizer.close();
            }
        } finally {
            client.shutdown();
        }
    }
}

En cas de succès, le programme affiche les tableaux de sous-titres MetaInfo, SynthesisCompleted et une confirmation du fichier audio. La latence du premier audio est mesurée depuis le moment précédant le début de la synthèse jusqu'à l'arrivée du premier fragment audio. Elle diffère du temps nécessaire pour recevoir tout l'audio.

Remarque

L'exemple enregistre l'audio dans un fichier. Pour une lecture sensible à la latence, lisez les fragments audio dès leur arrivée dans onMessage au lieu d'attendre la fin de la synthèse.

Horodatages au niveau des mots

Paramètres de requête

Avant de démarrer la synthèse, définissez enable_subtitle sur true. Les horodatages sont désactivés par défaut. Dans le SDK Java, utilisez :

synthesizer.addCustomedParam("enable_subtitle", true);

Réponse du serveur

Le service renvoie les horodatages dans les événements MetaInfo. Chaque événement contient un tableau de sous-titres dans payload.subtitles. Le SDK Java transmet ces événements via le rappel onMetaInfo.

Champs de chaque élément des sous-titres

Champ

Type

Description

text

String

Une unité de texte basée sur la prononciation, telle qu'un caractère chinois ou un mot anglais.

begin_time

Integer

L'heure de début de l'unité de texte dans l'audio synthétisé, en millisecondes.

end_time

Integer

L'heure de fin de l'unité de texte dans l'audio synthétisé, en millisecondes.

phoneme

String

Lorsque seuls les horodatages au niveau des mots sont activés, ce champ contient la chaîne "null", et non la valeur JSON null.

begin_index

Integer

L'index de début (base zéro) de l'unité de texte. Il ne s'agit pas d'un décalage de caractère dans la chaîne d'origine.

end_index

Integer

L'index de fin exclusif de l'unité de texte.

Par exemple, les deux mots de Hello world! occupent les plages d'index [0, 1) et [1, 2). La ponctuation peut occuper une position d'index : dans l'entrée chinoise 你好,世界。, le caractère a un index de début de 3.

Remarque

Une requête de synthèse peut générer plusieurs événements MetaInfo. Les tableaux de sous-titres peuvent se répéter ou inclure des entrées déjà renvoyées. Mettez à jour les données existantes par index et horodatage au lieu d'ajouter chaque tableau entier.

Exemple de réponse

L'extrait de payload suivant est renvoyé pour Hello world!. Les valeurs de synchronisation varient selon la voix, la vitesse d'élocution et le texte.

{
  "subtitles": [
    {
      "text": "Hello",
      "phoneme": "null",
      "begin_index": 0,
      "end_index": 1,
      "begin_time": 0,
      "end_time": 328
    },
    {
      "text": "world",
      "phoneme": "null",
      "begin_index": 1,
      "end_index": 2,
      "begin_time": 328,
      "end_time": 775
    }
  ]
}