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,
12peut ê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.
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 |
|
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.
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
}
]
}