CosyVoice API WebSocket
CosyVoice API WebSocket
Parâmetros e protocolo para CosyVoice conversão de texto em fala via WebSocket. O DashScope SDK suporta apenas Java e Python – use WebSocket para outras linguagens.
Guia do usuário: Para obter visões gerais do modelo e seleção de voz, consulte Síntese de fala.
WebSocket permite comunicação full-duplex. O cliente e o servidor estabelecem uma conexão persistente com um único handshake e, em seguida, enviam dados entre si em tempo real.
Bibliotecas WebSocket comuns:
-
Vá:
gorilla/websocket-PHP:Ratchet-Node.js:wsCosyVoice os modelos suportam apenas WebSocket - não APIs HTTP REST. Solicitações HTTP (POST, GET) retornam InvalidParameter ou erros de URL.
Limites de texto e formato
Limites de comprimento de texto
Envie até 20.000 caracteres por instrução de continuação da tarefa. O total de todas as instruções de continuação da tarefa não deve exceder 200.000 caracteres.
Regras de contagem de caracteres
- Os caracteres chineses (simplificado, tradicional, Kanji japonês, Hanja coreano) contam como dois caracteres. Todos os outros (pontuação, letras, números, Kana/Hangul) contam como um.
- As tags SSML são excluídas da contagem de caracteres.
- Exemplos:
"你好"→ 2 + 2 = 4 caracteres"中A文123"→ 2 + 1 + 2 + 1 + 1 + 1 = 8 caracteres"中文。"→ 2 + 2 + 1 = 5 caracteres"中 文。"→ 2 + 1 + 2 + 1 = 6 caracteres"<speak>你好</speak>"→ 2 + 2 = 4 caracteres
Formato de codificação
Use codificação UTF-8.
Suporte a expressões matemáticas
A análise de expressões matemáticas está disponível para cosyvoice-v3-flash e cosyvoice-v3-plus. Abrange matemática comum do ensino fundamental e médio, incluindo operações básicas, álgebra e geometria.
Este recurso oferece suporte apenas ao chinês.
Consulte Converter fórmulas LaTeX em fala (somente chinês).
SSML suporte
SSML requer todos os seguintes:
-
Modelo: Somente cosyvoice-v3-flash e cosyvoice-v3-plus suporte SSML.
-
Voz: Use uma voz habilitada para SSML:
-
Todas as vozes clonadas (criadas através da API Voice Cloning).
-
Vozes do sistema marcadas como habilitadas para SSML na lista de vozes.
As vozes do sistema sem suporte SSML (como algumas vozes básicas) retornam o erro "O texto SSML não é suportado no momento!" mesmo com
enable_ssmlhabilitado.
-
-
Parâmetro: Definir
enable_ssmlparatruena instrução de execução da tarefa.
Em seguida, envie o texto formatado em SSML por meio da instrução continue-task. Para obter um exemplo completo, consulte Introdução.
Fluxo de interação
As mensagens cliente-servidor são instruções. Mensagens de servidor para cliente são JSON eventos ou fluxos de áudio binários.
A sequência:
-
Abra uma conexão WebSocket.
-
Envie a instrução de execução da tarefa para iniciar uma tarefa.
-
Aguarde o evento de tarefa iniciada antes de prosseguir.
-
Envie texto:
Envie uma ou mais instruções para continuar a tarefa em ordem. Após receber uma frase completa, o servidor retorna um evento gerado pelo resultado e o fluxo de áudio. Para restrições de comprimento de texto, consulte o
textcampo na instrução de continuação da tarefa.Envie várias instruções para continuar a tarefa para enviar fragmentos de texto em ordem. O servidor segmenta o texto em frases automaticamente:
- Frases completas são sintetizadas imediatamente.
- Frases incompletas são armazenadas em buffer até serem concluídas. Nenhum áudio é retornado para frases incompletas.
Depois de receber as instruções de conclusão da tarefa, o servidor sintetiza à força todo o conteúdo armazenado em buffer.
-
Receba o fluxo de áudio através do
binarycanal. -
Após enviar todo o texto, envie as instruções de conclusão da tarefa. Continue recebendo o fluxo de áudio. Não pule esta etapa ou a parte final do áudio poderá ser perdida.
-
Receba o evento de tarefa concluída do servidor.
-
Feche a conexão WebSocket.
Reutilize uma conexão WebSocket para diversas tarefas em vez de criar uma nova conexão a cada vez. Consulte Sobrecarga de conexão e reutilização.
Mantenha o task_id consistente: run-task, todas as instruções continue-task e finish-task em uma única tarefa devem usar o mesmo task_id.
Tarefa_ids incompatíveis causa:
- Entrega de áudio desordenada.
- Conteúdo de fala desalinhado.
- Estado anormal da tarefa, possivelmente impedindo o recebimento do evento de conclusão da tarefa.
- Falhas de faturação ou estatísticas de utilização imprecisas.
Prática recomendada:
- Gere um task_id exclusivo (por exemplo, UUID) ao enviar run-task.
- Armazene o task_id em uma variável.
- Use este task_id para todas as instruções subsequentes de tarefa contínua e tarefa final.
- Após receber a tarefa concluída, gere um novo task_id para a próxima tarefa.
Dicas de implementação do cliente
O servidor entrega áudio como vários quadros binários em ordem. Concatene todos os frames para produzir o arquivo de áudio completo:
# Python: Concatenate audio chunks
with open("output.mp3", "ab") as f: # Append mode
f.write(audio_chunk) # audio_chunk is each received binary audio chunk
// JavaScript: Concatenate audio chunks
const audioChunks = [];
ws.onmessage = (event) => {
if (event.data instanceof Blob) {
audioChunks.push(event.data); // Collect all audio chunks
}
};
// Merge audio after task completes
const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' });
Erros comuns:
-
Fechar a conexão antes que todos os pedaços de áudio cheguem, resultando em áudio incompleto.
-
Esquecendo de enviar terminar tarefa, deixando o texto em buffer e sem processamento.
-
Em pipelines ASR para LLM para TTS, streaming de texto caractere por caractere em vez de enviar frases completas via continue-task.
Aplicativos móveis e de navegador: o sistema operacional pode pausar as conexões WebSocket quando o aplicativo entra em segundo plano. Use uma tarefa ou serviço em segundo plano para manter a conexão ativa ou reinicialize-a ao retornar ao primeiro plano. Em navegadores, use
beforeunloadpara fechar a conexão antes que a página seja descarregada.
Dicas específicas da plataforma
- Flutter: Feche a conexão no
disposemétodo para evitar vazamentos de memória ao usarweb_socket_channel. Lidar com eventos do ciclo de vida do aplicativo (comoAppLifecycleState.paused) para transições em segundo plano. - Web (navegador): Alguns navegadores limitam as conexões WebSocket. Reutilize uma única conexão para diversas tarefas. Usar
beforeunloadpara fechar a conexão antes que a página feche. - Celular (nativo para iOS/Android): O sistema operacional pode pausar ou encerrar conexões de rede quando o aplicativo entra em segundo plano. Use uma tarefa em segundo plano ou serviço em primeiro plano para manter o WebSocket ativo ou reinicialize a tarefa ao retornar em primeiro plano.
##URL
wss://maas.qwencloudapi.com/api-ws/v1/inference
Erros comuns de URL:
- Protocolo errado: Usar
wss://, nãohttp://ouhttps://. - Auth na string de consulta: Não coloque Autorização na URL (como
?Authorization=bearer YOUR_API_KEY). Defina-o nos cabeçalhos de handshake HTTP. Consulte Cabeçalhos. - Segmentos de caminho extras: não anexe nomes de modelos ou outros parâmetros ao URL. Especifique o modelo em
payload.modelna instrução de execução da tarefa.
Cabeçalhos
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| Autorização | corda | Sim | Token de autenticação. Formato: Bearer $DASHSCOPE_API_KEY. |
| agente do usuário | corda | Não | Identificador do cliente para rastreamento de origem. |
| X-DashScope-WorkSpace | corda | Não | Seu Qwen ID do espaço de trabalho do código. |
| X-DashScope-DataInspection | corda | Não | Inspeção de conformidade de dados. Padrão: enable. Não defina a menos que seja necessário. |
Tempo de autenticação
A autenticação ocorre durante o handshake do WebSocket, não ao enviar run-task. Se o cabeçalho Authorization estiver ausente ou for inválido, o servidor rejeitará o handshake com um erro HTTP 401 ou 403. As bibliotecas cliente normalmente relatam isso como uma exceção WebSocketBadStatus.
Solucionar problemas de falhas de autenticação
Se a conexão WebSocket falhar:
- Verifique o formato da chave API: Confirme os usos do cabeçalho de autorização
bearer YOUR_API_KEYcom um espaço entrebearere a chave. - Verifique a validade da chave de API: Verifique sua página de chaves de API para confirmar que a chave está ativa e autorizada para CosyVoice modelos.
- Verifique o posicionamento do cabeçalho: Defina o cabeçalho de autorização durante o handshake do WebSocket. Exemplos por idioma:
-Python (websockets):
extra_headers={"Authorization": f"bearer {api_key}"}- JavaScript: a API WebSocket do navegador não oferece suporte a cabeçalhos personalizados. Use um proxy do lado do servidor ou outra biblioteca como ws.
- Vá (gorila/websocket):
header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
- Teste a conectividade de rede: Use curl ou Postman para verificar a chave de API chamando outras APIs DashScope suportadas por HTTP.
Usando WebSocket em navegadores
O navegador new WebSocket(url) A API não oferece suporte a cabeçalhos de solicitação personalizados (incluindo autorização) durante o handshake. Você não pode autenticar diretamente do código frontend.
Solução: use um proxy de back-end
- Conecte-se a CosyVoice do seu backend (Node.js, Java ou Python), onde você pode definir o cabeçalho Authorization.
- Faça com que o frontend se conecte ao seu backend via WebSocket, que encaminha mensagens para CosyVoice.
- Isso mantém a chave de API oculta e permite adicionar autenticação, registro ou limitação de taxa.
Nunca codifique sua chave de API no código de front-end. Uma chave de API vazada pode levar ao comprometimento da conta, cobranças inesperadas ou violações de dados.
Código de exemplo:
Para outras linguagens, implemente a mesma lógica ou use ferramentas de IA para converter esses exemplos.
- Frontend (web nativo) + Backend (Node.js Express): cosyvoiceNodeJs_en.zip
- Frontend (web nativa) + Backend (Python Flask): cosyvoiceFlask_en.zip
Instruções (cliente para servidor)
As instruções são mensagens JSON enviadas como quadros de texto WebSocket. Eles controlam o ciclo de vida da tarefa.
Envie instruções nesta ordem:
- Enviar executar tarefa
- Inicia a tarefa.
- Use o mesmo
task_idem todas as continue-task e tarefa final instruções.
- Enviar continuar tarefa
- Envia texto para sintetizar.
- Enviar somente após receber tarefa iniciada.
- Enviar concluir tarefa
- Termina a tarefa.
- Enviar depois que todas as instruções de continuação da tarefa forem enviadas.
1. instrução run-task: iniciar uma tarefa
Inicia uma tarefa de conversão de texto em fala. Configure voz, taxa de amostragem e outros parâmetros aqui.
- Tempo: Enviar após a conexão WebSocket ser estabelecida.
- Não envie texto aqui. Envie texto usando continue-task em vez disso.
- O campo de entrada é obrigatório mas deve ser
{}. Omiti-lo causa o erro "a tarefa não pode ser nula".
Exemplo:
{
"header": {
"action": "run-task",
"task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
"streaming": "duplex"
},
"payload": {
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "cosyvoice-v3-flash",
"parameters": {
"text_type": "PlainText",
"voice": "longanyang",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1
},
"input": {}
}
}
header parâmetros:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| cabeçalho.ação | corda | Sim | Valor fixo: "run-task". |
| cabeçalho.task_id | corda | Sim | Um UUID de 32 caracteres. Hífens são opcionais (como "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx" ou "2bf83b9abaeb4fda8d9axxxxxxxxxxxx"). A maioria das linguagens fornece APIs UUID integradas. |
| cabeçalho.streaming | corda | Sim | Valor fixo: “duplex”. |
Exemplo Python para gerar um ID de tarefa:
import uuid
def generateTaskId(self):
# Generate random UUID
return uuid.uuid4().hex
Use o mesmo task_id para todas as continue-task e tarefa final instruções.
payload parâmetros:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| carga útil.task_group | corda | Sim | Valor fixo: "áudio". |
| carga útil.tarefa | corda | Sim | Valor fixo: "tts". |
| carga útil.função | corda | Sim | Valor fixo: "SpeechSynthesizer". |
| carga útil.model | corda | Sim | O modelo de texto para fala. Consulte Lista de vozes. |
| carga útil.input | objeto | Sim | Obrigatório, mas deve estar vazio ({}) em run-task. Envie texto usando continue-task. |
Erro comum: Omitir o campo de entrada ou adicionar campos inesperados (como modo ou conteúdo) causa "InvalidParameter: a tarefa não pode ser nula" ou fechamento da conexão (código WebSocket 1007).
parâmetros de carga útil:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| texto_tipo | corda | Sim | Valor fixo: "PlainText". |
| voz | corda | Sim | Voz para síntese. Consulte Lista de vozes para vozes do sistema disponíveis. |
| formato | corda | Não | Formato de áudio. Suporta pcm, wav, mp3 (padrão) e opus. Para opus, ajuste a taxa de bits com bit_rate. |
| amostra_taxa | inteiro | Não | Taxa de amostragem em Hz. Padrão: 22050. Valores válidos: 8000, 16000, 22050, 24000, 44100, 48000. |
| volume | inteiro | Não | Volume. Padrão: 50. Faixa: [0, 100]. Escala linearmente. 0 é silencioso, 100 é o máximo. |
| taxa | flutuar | Não | Taxa de fala. Padrão: 1.0. Faixa: [0,5, 2,0]. Abaixo de 1,0 retarda a fala; acima de 1,0 acelera. |
| arremesso | flutuar | Não | Multiplicador de passo. Padrão: 1.0. Faixa: [0,5, 2,0]. A relação com o tom percebido não é estritamente linear. Teste para encontrar um valor adequado. |
| ativar_ssml | booleano | Não | Habilite SSML. Quando true, apenas uma instrução de continuação da tarefa é permitido. |
| bit_taxa | interno | Não | Taxa de bits de áudio em kbps (para formato Opus). Padrão: 32. Faixa: [6, 510]. |
| palavra_timestamp_enabled | booleano | Não | Habilite carimbos de data/hora em nível de palavra. Padrão: falso. Disponível para vozes do sistema marcadas como suportadas na lista de vozes. |
| semente | interno | Não | Semente aleatória para geração. A mesma semente com parâmetros idênticos reproduz a mesma saída. Padrão: 0. Faixa: [0, 65535]. |
| idioma_dicas | matriz[string] | Não | Linguagem alvo para síntese. Valores válidos: zh, en, fr, de, ja, ko, ru, pt, th, id, vi. Este é um array, mas apenas o primeiro elemento é processado. |
| instrução | corda | Não | Controla os efeitos de síntese, como dialeto, emoção ou estilo de fala. Disponível para vozes do sistema marcadas como suporte ao Instruct na lista de vozes. Comprimento máximo: 100 caracteres. |
| ativar_aigc_tag | booleano | Não | Adicione um identificador AIGC invisível ao áudio gerado. Quando verdadeiro, um identificador é incorporado nos formatos WAV, MP3 e Opus. Padrão: falso. Apoiado por cosyvoice-v3-flash e cosyvoice-v3-plus. |
| aigc_propagador | corda | Não | Define o ContentPropagator campo no identificador AIGC. Só entra em vigor quando enable_aigc_tag é true. Padrão: UID. Apoiado por cosyvoice-v3-flash e cosyvoice-v3-plus. |
| aigc_propagate_id | corda | Não | Define o PropagateID campo no identificador AIGC. Só entra em vigor quando enable_aigc_tag é true. Padrão: o ID da solicitação atual. Apoiado por cosyvoice-v3-flash e cosyvoice-v3-plus. |
| quente_fix | objeto | Não | Configuração de hotpatching de texto. Personalize a pronúncia ou substitua o texto antes da síntese. Disponível apenas para cosyvoice-v3-flash. |
| ativar_markdown_filtro | booleano | Não | Habilite a filtragem Markdown. Remove símbolos Markdown do texto de entrada antes da síntese. Padrão: falso. Disponível apenas para cosyvoice-v3-flash. |
Quando word_timestamp_enabled está ativado, os carimbos de data e hora aparecem no evento gerado pelo resultado:
{
"header": {
"task_id": "3f39be22-efbd-4844-91d5-xxxxxxxxxxxx",
"event": "result-generated",
"attributes": {}
},
"payload": {
"output": {
"sentence": {
"index": 0,
"words": [
{
"text": "bed",
"begin_index": 0,
"end_index": 1,
"begin_time": 280,
"end_time": 640
}
]
}
}
}
}
Exemplos de instruções para cosyvoice-v3-flash com vozes clonadas:
Please speak in Cantonese. (Supported dialects: Cantonese, Northeastern, Gansu, Guizhou, Henan, Hubei, Jiangxi, Minnan, Ningxia, Shanxi, Shaanxi, Shandong, Shanghainese, Sichuan, Tianjin, Yunnan.)
Please say a sentence as loudly as possible.
Please say a sentence as slowly as possible.
Please say a sentence as quickly as possible.
Please say a sentence very softly.
Can you speak a little slower?
Can you speak very quickly?
Can you speak very slowly?
Can you speak a little faster?
Please say a sentence very angrily.
Please say a sentence very happily.
Please say a sentence very fearfully.
Please say a sentence very sadly.
Please say a sentence very surprisedly.
Please try to sound as firm as possible.
Please try to sound as angry as possible.
Please try an approachable tone.
Please speak in a cold tone.
Please speak in a majestic tone.
I want to experience a natural tone.
I want to see how you express a threat.
I want to see how you express wisdom.
I want to see how you express seduction.
I want to hear you speak in a lively way.
I want to hear you speak with passion.
I want to hear you speak in a steady manner.
I want to hear you speak with confidence.
Can you talk to me with excitement?
Can you show an arrogant emotion?
Can you show an elegant emotion?
Can you answer the question happily?
Can you give a gentle emotional demonstration?
Can you talk to me in a calm tone?
Can you answer me in a deep way?
Can you talk to me with a gruff attitude?
Tell me the answer in a sinister voice.
Tell me the answer in a resilient voice.
Narrate in a natural and friendly chat style.
Speak in the tone of a radio drama podcaster.
Para cosyvoice-v3-flash com vozes do sistema, a instrução deve usar um formato fixo. Veja a lista de vozes.
exemplo hot_fix:
"hot_fix": {
"pronunciation": [
{"weather": "tian1 qi4"}
],
"replace": [
{"today": "jin1 tian1"}
]
}
2. instruções para continuar a tarefa
Envia texto para sintetizar. Envie todo o texto em uma instrução ou divida-o em várias instruções em ordem.
Quando enviar: Depois de receber tarefa iniciada.
Não espere mais de 23 segundos entre fragmentos de texto ou ocorrerá um erro de "tempo limite da solicitação após 23 segundos". Se não sobrar mais texto, envie finish-task para finalizar a tarefa. O tempo limite de 23 segundos é imposto pelo servidor e não pode ser modificado.
Exemplo:
{
"header": {
"action": "continue-task",
"task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
"streaming": "duplex"
},
"payload": {
"input": {
"text": "Before my bed, moonlight shines bright, I suspect it's frost upon the ground."
}
}
}
header parâmetros:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| cabeçalho.ação | corda | Sim | Valor fixo: "continue-task". |
| cabeçalho.task_id | corda | Sim | Deve corresponder ao task_id de run-task. |
| cabeçalho.streaming | corda | Sim | Valor fixo: “duplex”. |
payload parâmetros:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| entrada.texto | corda | Sim | Texto para sintetizar. |
3. instrução de conclusão da tarefa: Finalizar tarefa
Termina a tarefa. Sempre envie esta instrução. Caso contrário:
-
Áudio incompleto: O servidor não forçará a síntese de frases em cache, causando falta de finais.
-
Tempo limite de conexão: Esperando mais de 23 segundos após a última tarefa contínua aciona um tempo limite.
-
Problemas de faturamento: As informações de uso podem estar imprecisas.
Quando enviar: Envie imediatamente depois de tudo continue-task instruções. Não espere o áudio terminar – isso pode causar tempos limite.
Exemplo:
{
"header": {
"action": "finish-task",
"task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
"streaming": "duplex"
},
"payload": {
"input": {}
}
}
header parâmetros:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| cabeçalho.ação | corda | Sim | Valor fixo: "tarefa final". |
| cabeçalho.task_id | corda | Sim | Deve corresponder ao task_id de run-task. |
| cabeçalho.streaming | corda | Sim | Valor fixo: “duplex”. |
payload parâmetros:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| carga útil.input | objeto | Sim | Valor fixo: {}. |
Eventos (servidor para cliente)
Eventos são mensagens JSON do servidor. Cada um marca um estágio no ciclo de vida da tarefa.
O áudio binário é enviado separadamente – não incluído em nenhum evento.
1. evento de tarefa iniciada: tarefa iniciada
Confirma que a tarefa foi iniciada. Enviar continuar tarefa ou tarefa final somente após receber este evento. Caso contrário, a tarefa falhará.
O task-started evento payload está vazio.
Exemplo:
{
"header": {
"task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
"event": "task-started",
"attributes": {}
},
"payload": {}
}
header parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
| cabeçalho.evento | corda | Valor fixo: "tarefa iniciada". |
| cabeçalho.task_id | corda | ID da tarefa gerado pelo cliente. |
2. evento gerado por resultado
Enquanto você envia continue-task e tarefa final instruções, o servidor retorna result-generated eventos e quadros de áudio binários.
Cada result-generated O evento contém o índice da frase atual. Os dados de áudio chegam como quadros binários entre eventos. Uma frase produz vários quadros de áudio binários. Receba quadros em ordem e anexe ao mesmo arquivo.
Exemplo:
{
"header": {
"task_id": "3f2d5c86-0550-45c0-801f-xxxxxxxxxx",
"event": "result-generated",
"attributes": {}
},
"payload": {
"output": {
"sentence": {
"index": 0,
"words": []
}
},
"usage": {
"characters": 11
}
}
}
header parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
| cabeçalho.evento | corda | Valor fixo: “gerado por resultado”. |
| cabeçalho.task_id | corda | ID da tarefa gerado pelo cliente. |
| cabeçalho.atributos | objeto | Atributos adicionais – geralmente vazios. |
payload parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
| carga útil.output.sentence.index | inteiro | Número da frase, começando em 0. |
| carga útil.output.sentence.words | matriz | Matriz de informações de palavras. |
| carga útil.saída.sentença.palavras.texto | corda | Texto de palavra. |
| carga útil.output.sentence.words.begin_index | inteiro | Posição inicial da palavra na frase, contando a partir de 0. |
| carga útil.output.sentence.words.end_index | inteiro | Posição final da palavra na frase, contando a partir de 1. |
| carga útil.output.sentence.words.begin_time | inteiro | Carimbo de data e hora de início do áudio da palavra, em milissegundos. |
| carga útil.output.sentence.words.end_time | inteiro | Carimbo de data/hora final do áudio da palavra, em milissegundos. |
| carga útil.usage.characters | inteiro | Caracteres cobrados cumulativamente até o momento. O usage campo aparece em alguns result-generated eventos. Use a última ocorrência. |
3. evento de tarefa concluída: tarefa concluída
Marca o fim da tarefa.
Após o término da tarefa, feche a conexão WebSocket ou reutilize-a para enviar uma nova instrução de execução da tarefa (consulte Sobrecarga de conexão e reutilização).
Exemplo:
{
"header": {
"task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
"event": "task-finished",
"attributes": {}
},
"payload": {
"output": {}
}
}
header parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
| cabeçalho.evento | corda | Valor fixo: "tarefa concluída". |
| cabeçalho.task_id | corda | ID da tarefa gerado pelo cliente. |
4. evento com falha na tarefa: falha na tarefa
Indica que a tarefa falhou. Feche a conexão WebSocket e revise a mensagem de erro.
Exemplo:
{
"header": {
"task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
"event": "task-failed",
"error_code": "InvalidParameter",
"error_message": "[tts:]Engine return error code: 418",
"attributes": {}
},
"payload": {}
}
header parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
| cabeçalho.evento | corda | Valor fixo: "falha na tarefa". |
| cabeçalho.task_id | corda | ID da tarefa gerado pelo cliente. |
| cabeçalho.error_code | corda | Tipo de erro. |
| header.error_message | corda | Motivo detalhado do erro. |
Interrupção de tarefa
Durante a síntese de streaming, você pode interromper a tarefa atual antecipadamente (por exemplo, se o usuário cancelar a reprodução) usando um destes métodos:
| Método de interrupção | Comportamento do servidor | Caso de uso |
|---|---|---|
| Feche a conexão | Interrompe a síntese imediatamente. Descarta áudio não enviado. Nenhum evento de tarefa concluída. A conexão não pode ser reutilizada. | Parada imediata: o usuário cancela a reprodução, troca de conteúdo ou sai do aplicativo. |
| Enviar tarefa final | Força a síntese do texto armazenado em cache. Retorna o áudio restante e o evento de tarefa concluída. A conexão permanece reutilizável. | Final elegante: Pare de enviar texto, mas receba todo o áudio armazenado em cache. |
Sobrecarga de conexão e reutilização
O serviço WebSocket oferece suporte à reutilização de conexão.
Enviar tarefa de execução para iniciar uma tarefa e finish-task para acabar com isso. Depois de tarefa concluída, reutilize a mesma conexão enviando uma nova instrução run-task.
- Envie uma nova tarefa de execução somente após receber tarefa concluída.
- Use task_ids diferentes para tarefas diferentes na mesma conexão.
- Acionador de tarefas com falha tarefa com falha e feche a conexão (não é possível reutilizar).
- Tempo limite das conexões após 60 segundos de inatividade.
Desempenho e simultaneidade
Limites de simultaneidade
Consulte Limites de taxa.
Para aumentar sua cota de simultaneidade, entre em contato com o suporte ao cliente. Os ajustes de cota exigem revisão e normalmente levam de 1 a 3 dias úteis.
Prática recomendada: Reutilize uma conexão WebSocket para diversas tarefas. Consulte Sobrecarga de conexão e reutilização.
Latência de conexão
Tempo de conexão típico:
- Conexões transfronteiriças: 1 a 3 segundos. Em casos raros, 10 a 30 segundos.
Solucionar problemas de conexões lentas (>30 segundos):
- Latência de rede: Verifique a qualidade da conexão internacional ou o desempenho do ISP.
- DNS lento: Experimente o DNS público (8.8.8.8) ou configure um arquivo de hosts locais para maas.qwencloudapi.com.
- Handshake TLS: Atualize para TLS 1.2 ou posterior.
- Proxy/firewall: Redes corporativas podem bloquear ou retardar conexões WebSocket.
Ferramentas de solução de problemas:
- Use Wireshark ou tcpdump para analisar o handshake TCP, o handshake TLS e o tempo de atualização do WebSocket.
- Teste a latência HTTP com curl:
curl -w "@curl-format.txt" -o /dev/null -s https://maas.qwencloudapi.com
Velocidade de geração de áudio
- Fator de tempo real (RTF): 0,1 a 0,5x em tempo real (1 segundo de áudio leva de 0,1 a 0,5 segundos para ser gerado). A velocidade real varia de acordo com o modelo, comprimento do texto e carga do servidor.
- Latência do primeiro pacote: 200 a 800 ms desde o envio da tarefa contínua até o recebimento do primeiro pedaço de áudio.
Código de exemplo
Exemplo de conectividade básica. Implemente lógica pronta para produção para seu caso de uso. Use programação assíncrona para enviar e receber simultaneamente:
-
Conectar: Chame a função de conexão da sua biblioteca WebSocket com Headers e URL.
-
Ouvir mensagens: O servidor envia áudio binário e events:
Eventos:
- tarefa iniciada: Tarefa iniciada. Enviar continuar tarefa ou tarefa final somente depois disso.
- gerado por resultado: Retornado continuamente após o envio de continue-task ou tarefa final.
- tarefa concluída: Tarefa concluída. Fechar conexão.
- falha na tarefa: Falha na tarefa. Feche a conexão e verifique o erro.
Áudio binário:
- Para streaming de MP3/Opus: Use um reprodutor de streaming (FFmpeg, PyAudio, AudioFormat, MediaSource). Não jogue quadro a quadro.
- Para salvar o áudio completo: Grave quadros no mesmo arquivo no modo anexar.
- Para WAV/MP3: Somente o primeiro quadro possui informações de cabeçalho; os quadros subsequentes são apenas dados de áudio.
-
Enviar instruções: Em um thread separado, envie instruções ao servidor.
-
Fechar conexão: Fechar quando terminar, em caso de erro ou após tarefa concluída/falha na tarefa.
Go
```go
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
const (
wsURL = "wss://maas.qwencloudapi.com/api-ws/v1/inference/"
outputFile = "output.mp3"
)
func main() {
// If no environment variable is set, replace next line with: apiKey := "YOUR_API_KEY"
apiKey := os.Getenv("DASHSCOPE_API_KEY")
// Clear output file
os.Remove(outputFile)
os.Create(outputFile)
// Connect WebSocket
header := make(http.Header)
header.Add("X-DashScope-DataInspection", "enable")
header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header)
if err != nil {
if resp != nil {
fmt.Printf("Connection failed HTTP status code: %d\n", resp.StatusCode)
}
fmt.Println("Connection failed:", err)
return
}
defer conn.Close()
// Generate task ID
taskID := uuid.New().String()
fmt.Printf("Generated task ID: %s\n", taskID)
// Send run-task instruction
runTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "run-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "cosyvoice-v3-flash",
"parameters": map[string]interface{}{
"text_type": "PlainText",
"voice": "longanyang",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
// If enable_ssml is true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
"enable_ssml": false,
},
"input": map[string]interface{}{},
},
}
runTaskJSON, _ := json.Marshal(runTaskCmd)
fmt.Printf("Sent run-task instruction: %s\n", string(runTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
if err != nil {
fmt.Println("Failed to send run-task:", err)
return
}
textSent := false
// Process messages
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("Failed to read message:", err)
break
}
// Process binary message
if messageType == websocket.BinaryMessage {
fmt.Printf("Received binary message, length: %d\n", len(message))
file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
file.Write(message)
file.Close()
continue
}
// Process text message
messageStr := string(message)
fmt.Printf("Received text message: %s\n", strings.ReplaceAll(messageStr, "\n", ""))
// Simple JSON parse to get event type
var msgMap map[string]interface{}
if json.Unmarshal(message, &msgMap) == nil {
if header, ok := msgMap["header"].(map[string]interface{}); ok {
if event, ok := header["event"].(string); ok {
fmt.Printf("Event type: %s\n", event)
switch event {
case "task-started":
fmt.Println("=== Received task-started event ===")
if !textSent {
// Send continue-task instruction
texts := []string{"Before my bed, moonlight shines bright, I suspect it's frost upon the ground.", "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home."}
for _, text := range texts {
continueTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "continue-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{
"text": text,
},
},
}
continueTaskJSON, _ := json.Marshal(continueTaskCmd)
fmt.Printf("Sent continue-task instruction: %s\n", string(continueTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
if err != nil {
fmt.Println("Failed to send continue-task:", err)
return
}
}
textSent = true
// Delay before sending finish-task
time.Sleep(500 * time.Millisecond)
// Send finish-task instruction
finishTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "finish-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{},
},
}
finishTaskJSON, _ := json.Marshal(finishTaskCmd)
fmt.Printf("Sent finish-task instruction: %s\n", string(finishTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
if err != nil {
fmt.Println("Failed to send finish-task:", err)
return
}
}
case "task-finished":
fmt.Println("=== Task completed ===")
return
case "task-failed":
fmt.Println("=== Task failed ===")
if header["error_message"] != nil {
fmt.Printf("Error message: %s\n", header["error_message"])
}
return
case "result-generated":
fmt.Println("Received result-generated event")
}
}
}
}
}
}
```
C#
```csharp
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
class Program {
// If no environment variable is set, replace next line with: private static readonly string ApiKey = "YOUR_API_KEY"
private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");
private const string WebSocketUrl = "wss://maas.qwencloudapi.com/api-ws/v1/inference/";
// Output file path
private const string OutputFilePath = "output.mp3";
// WebSocket client
private static ClientWebSocket _webSocket = new ClientWebSocket();
// Cancellation token source
private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
// Task ID
private static string? _taskId;
// Task started flag
private static TaskCompletionSource<bool> _taskStartedTcs = new TaskCompletionSource<bool>();
static async Task Main(string[] args) {
try {
// Clear output file
ClearOutputFile(OutputFilePath);
// Connect to WebSocket service
await ConnectToWebSocketAsync(WebSocketUrl);
// Start receiving messages
Task receiveTask = ReceiveMessagesAsync();
// Send run-task instruction
_taskId = GenerateTaskId();
await SendRunTaskCommandAsync(_taskId);
// Wait for task-started event
await _taskStartedTcs.Task;
// Send continue-task instructions
string[] texts = {
"Before my bed, moonlight shines bright",
"I suspect it's frost upon the ground",
"I raise my eyes to gaze at the bright moon",
"then bow my head, thinking of home"
};
foreach (string text in texts) {
await SendContinueTaskCommandAsync(text);
}
// Send finish-task instruction
await SendFinishTaskCommandAsync(_taskId);
// Wait for receive task to complete
await receiveTask;
Console.WriteLine("Task completed. Connection closed.");
} catch (OperationCanceledException) {
Console.WriteLine("Task canceled.");
} catch (Exception ex) {
Console.WriteLine($"Error: {ex.Message}");
} finally {
_cancellationTokenSource.Cancel();
_webSocket.Dispose();
}
}
private static void ClearOutputFile(string filePath) {
if (File.Exists(filePath)) {
File.WriteAllText(filePath, string.Empty);
Console.WriteLine("Output file cleared.");
} else {
Console.WriteLine("Output file does not exist. No action needed.");
}
}
private static async Task ConnectToWebSocketAsync(string url) {
var uri = new Uri(url);
if (_webSocket.State == WebSocketState.Connecting || _webSocket.State == WebSocketState.Open) {
return;
}
// Set WebSocket request headers
_webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
_webSocket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");
try {
await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
Console.WriteLine("Successfully connected to WebSocket service.");
} catch (OperationCanceledException) {
Console.WriteLine("WebSocket connection canceled.");
} catch (Exception ex) {
Console.WriteLine($"WebSocket connection failed: {ex.Message}");
throw;
}
}
private static async Task SendRunTaskCommandAsync(string taskId) {
var command = CreateCommand("run-task", taskId, "duplex", new {
task_group = "audio",
task = "tts",
function = "SpeechSynthesizer",
model = "cosyvoice-v3-flash",
parameters = new
{
text_type = "PlainText",
voice = "longanyang",
format = "mp3",
sample_rate = 22050,
volume = 50,
rate = 1,
pitch = 1,
// If enable_ssml is true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
enable_ssml = false
},
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("Sent run-task instruction.");
}
private static async Task SendContinueTaskCommandAsync(string text) {
if (_taskId == null) {
throw new InvalidOperationException("Task ID not initialized.");
}
var command = CreateCommand("continue-task", _taskId, "duplex", new {
input = new {
text
}
});
await SendJsonMessageAsync(command);
Console.WriteLine("Sent continue-task instruction.");
}
private static async Task SendFinishTaskCommandAsync(string taskId) {
var command = CreateCommand("finish-task", taskId, "duplex", new {
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("Sent finish-task instruction.");
}
private static async Task SendJsonMessageAsync(string message) {
var buffer = Encoding.UTF8.GetBytes(message);
try {
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
} catch (OperationCanceledException) {
Console.WriteLine("Message send canceled.");
}
}
private static async Task ReceiveMessagesAsync() {
while (_webSocket.State == WebSocketState.Open) {
var response = await ReceiveMessageAsync();
if (response != null) {
var eventStr = response.RootElement.GetProperty("header").GetProperty("event").GetString();
switch (eventStr) {
case "task-started":
Console.WriteLine("Task started.");
_taskStartedTcs.TrySetResult(true);
break;
case "task-finished":
Console.WriteLine("Task completed.");
_cancellationTokenSource.Cancel();
break;
case "task-failed":
Console.WriteLine("Task failed: " + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
_cancellationTokenSource.Cancel();
break;
default:
// Handle result-generated here
break;
}
}
}
}
private static async Task<JsonDocument?> ReceiveMessageAsync() {
var buffer = new byte[1024 * 4];
var segment = new ArraySegment<byte>(buffer);
try {
WebSocketReceiveResult result = await _webSocket.ReceiveAsync(segment, _cancellationTokenSource.Token);
if (result.MessageType == WebSocketMessageType.Close) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
return null;
}
if (result.MessageType == WebSocketMessageType.Binary) {
// Process binary data
Console.WriteLine("Received binary data...");
// Save binary data to file
using (var fileStream = new FileStream(OutputFilePath, FileMode.Append)) {
fileStream.Write(buffer, 0, result.Count);
}
return null;
}
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
return JsonDocument.Parse(message);
} catch (OperationCanceledException) {
Console.WriteLine("Message receive canceled.");
return null;
}
}
private static string GenerateTaskId() {
return Guid.NewGuid().ToString("N").Substring(0, 32);
}
private static string CreateCommand(string action, string taskId, string streaming, object payload) {
var command = new {
header = new {
action,
task_id = taskId,
streaming
},
payload
};
return JsonSerializer.Serialize(command);
}
}
```
PHP
**composer.json:**
```json
{
"require": {
"react/event-loop": "^1.3",
"react/socket": "^1.11",
"react/stream": "^1.2",
"react/http": "^1.1",
"ratchet/pawl": "^0.4"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
```
**Código:**
```php
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
// If no environment variable is set, replace next line with: $api_key = "YOUR_API_KEY"
$api_key = getenv("DASHSCOPE_API_KEY");
$websocket_url = 'wss://maas.qwencloudapi.com/api-ws/v1/inference/'; // WebSocket server address
$output_file = 'output.mp3'; // Output file path
$loop = Loop::get();
if (file_exists($output_file)) {
// Clear file content
file_put_contents($output_file, '');
}
// Create custom connector
$socketConnector = new SocketConnector($loop, [
'tcp' => [
'bindto' => '0.0.0.0:0',
],
'tls' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$connector = new Connector($loop, $socketConnector);
$headers = [
'Authorization' => 'bearer ' . $api_key,
'X-DashScope-DataInspection' => 'enable'
];
$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $output_file) {
echo "Connected to WebSocket server\n";
// Generate task ID
$taskId = generateTaskId();
// Send run-task instruction
sendRunTaskMessage($conn, $taskId);
// Define function to send continue-task instruction
$sendContinueTask = function() use ($conn, $loop, $taskId) {
// Text to send
$texts = ["Before my bed, moonlight shines bright", "I suspect it's frost upon the ground", "I raise my eyes to gaze at the bright moon", "then bow my head, thinking of home"];
$continueTaskCount = 0;
foreach ($texts as $text) {
$continueTaskMessage = json_encode([
"header" => [
"action" => "continue-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => [
"text" => $text
]
]
]);
echo "Preparing to send continue-task instruction: " . $continueTaskMessage . "\n";
$conn->send($continueTaskMessage);
$continueTaskCount++;
}
echo "Number of continue-task instructions sent: " . $continueTaskCount . "\n";
// Send finish-task instruction
sendFinishTaskMessage($conn, $taskId);
};
// Flag for task-started event
$taskStarted = false;
// Listen for messages
$conn->on('message', function($msg) use ($conn, $sendContinueTask, $loop, &$taskStarted, $taskId, $output_file) {
if ($msg->isBinary()) {
// Write binary data to local file
file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
} else {
// Process non-binary message
$response = json_decode($msg, true);
if (isset($response['header']['event'])) {
handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
} else {
echo "Unknown message format\n";
}
}
});
// Listen for connection close
$conn->on('close', function($code = null, $reason = null) {
echo "Connection closed\n";
if ($code !== null) {
echo "Close code: " . $code . "\n";
}
if ($reason !== null) {
echo "Close reason: " . $reason . "\n";
}
});
}, function ($e) {
echo "Cannot connect: {$e->getMessage()}\n";
});
$loop->run();
/**
* Generate task ID
* @return string
*/
function generateTaskId(): string {
return bin2hex(random_bytes(16));
}
/**
* Send run-task instruction
* @param $conn
* @param $taskId
*/
function sendRunTaskMessage($conn, $taskId) {
$runTaskMessage = json_encode([
"header" => [
"action" => "run-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"task_group" => "audio",
"task" => "tts",
"function" => "SpeechSynthesizer",
"model" => "cosyvoice-v3-flash",
"parameters" => [
"text_type" => "PlainText",
"voice" => "longanyang",
"format" => "mp3",
"sample_rate" => 22050,
"volume" => 50,
"rate" => 1,
"pitch" => 1,
// If enable_ssml is true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
"enable_ssml" => false
],
"input" => (object) []
]
]);
echo "Preparing to send run-task instruction: " . $runTaskMessage . "\n";
$conn->send($runTaskMessage);
echo "run-task instruction sent\n";
}
/**
* Read audio file
* @param string $filePath
* @return bool|string
*/
function readAudioFile(string $filePath) {
$voiceData = file_get_contents($filePath);
if ($voiceData === false) {
echo "Cannot read audio file\n";
}
return $voiceData;
}
/**
* Split audio data
* @param string $data
* @param int $chunkSize
* @return array
*/
function splitAudioData(string $data, int $chunkSize): array {
return str_split($data, $chunkSize);
}
/**
* Send finish-task instruction
* @param $conn
* @param $taskId
*/
function sendFinishTaskMessage($conn, $taskId) {
$finishTaskMessage = json_encode([
"header" => [
"action" => "finish-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => (object) []
]
]);
echo "Preparing to send finish-task instruction: " . $finishTaskMessage . "\n";
$conn->send($finishTaskMessage);
echo "finish-task instruction sent\n";
}
/**
* Handle event
* @param $conn
* @param $response
* @param $sendContinueTask
* @param $loop
* @param $taskId
* @param $taskStarted
*/
function handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, &$taskStarted) {
switch ($response['header']['event']) {
case 'task-started':
echo "Task started. Sending continue-task instructions...\n";
$taskStarted = true;
// Send continue-task instruction
$sendContinueTask();
break;
case 'result-generated':
// Received result-generated event
break;
case 'task-finished':
echo "Task completed\n";
$conn->close();
break;
case 'task-failed':
echo "Task failed\n";
echo "Error code: " . $response['header']['error_code'] . "\n";
echo "Error message: " . $response['header']['error_message'] . "\n";
$conn->close();
break;
case 'error':
echo "Error: " . $response['payload']['message'] . "\n";
break;
default:
echo "Unknown event: " . $response['header']['event'] . "\n";
break;
}
// Close connection if task completed
if ($response['header']['event'] == 'task-finished') {
// Wait 1 second to ensure all data is transmitted
$loop->addTimer(1, function() use ($conn) {
$conn->close();
echo "Client closed connection\n";
});
}
// Close connection if no task-started event received
if (!$taskStarted && in_array($response['header']['event'], ['task-failed', 'error'])) {
$conn->close();
}
}
```
Node.js
Instale dependências:
```sh
npm install ws
npm install uuid
```
```javascript
const WebSocket = require('ws');
const fs = require('fs');
const uuid = require('uuid').v4;
// If no environment variable is set, replace next line with: const apiKey = "YOUR_API_KEY"
const apiKey = process.env.DASHSCOPE_API_KEY;
const url = 'wss://maas.qwencloudapi.com/api-ws/v1/inference/';
// Output file path
const outputFilePath = 'output.mp3';
// Clear output file
fs.writeFileSync(outputFilePath, '');
// Create WebSocket client
const ws = new WebSocket(url, {
headers: {
Authorization: `bearer ${apiKey}`,
'X-DashScope-DataInspection': 'enable'
}
});
let taskStarted = false;
let taskId = uuid();
ws.on('open', () => {
console.log('Connected to WebSocket server');
// Send run-task instruction
const runTaskMessage = JSON.stringify({
header: {
action: 'run-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
task_group: 'audio',
task: 'tts',
function: 'SpeechSynthesizer',
model: 'cosyvoice-v3-flash',
parameters: {
text_type: 'PlainText',
voice: 'longanyang', // Voice
format: 'mp3', // Audio format
sample_rate: 22050, // Sample rate
volume: 50, // Volume
rate: 1, // Speech rate
pitch: 1, // Pitch
enable_ssml: false // Enable SSML. If true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
},
input: {}
}
});
ws.send(runTaskMessage);
console.log('Sent run-task message');
});
const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
ws.on('message', (data, isBinary) => {
if (isBinary) {
// Write binary data to file
fileStream.write(data);
} else {
const message = JSON.parse(data);
switch (message.header.event) {
case 'task-started':
taskStarted = true;
console.log('Task started');
// Send continue-task instruction
sendContinueTasks(ws);
break;
case 'task-finished':
console.log('Task completed');
ws.close();
fileStream.end(() => {
console.log('File stream closed');
});
break;
case 'task-failed':
console.error('Task failed: ', message.header.error_message);
ws.close();
fileStream.end(() => {
console.log('File stream closed');
});
break;
default:
// Handle result-generated here
break;
}
}
});
function sendContinueTasks(ws) {
const texts = [
'Before my bed, moonlight shines bright,',
'I suspect it\'s frost upon the ground.',
'I raise my eyes to gaze at the bright moon,',
'then bow my head, thinking of home.'
];
texts.forEach((text, index) => {
setTimeout(() => {
if (taskStarted) {
const continueTaskMessage = JSON.stringify({
header: {
action: 'continue-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {
text: text
}
}
});
ws.send(continueTaskMessage);
console.log(`Sent continue-task, text: ${text}`);
}
}, index * 1000); // Send every second
});
// Send finish-task instruction
setTimeout(() => {
if (taskStarted) {
const finishTaskMessage = JSON.stringify({
header: {
action: 'finish-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {}
}
});
ws.send(finishTaskMessage);
console.log('Sent finish-task');
}
}, texts.length * 1000 + 1000); // Send 1 second after last continue-task
}
ws.on('close', () => {
console.log('Disconnected from WebSocket server');
});
```
Java
**pom.xml:**
```xml
<dependencies>
<!-- WebSocket Client -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>
```
**construir.gradle:**
```gradle
// Omit other code
dependencies {
// WebSocket Client
implementation 'org.java-websocket:Java-WebSocket:1.5.3'
// JSON Processing
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// Omit other code
```
**Código:**
```java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.*;
public class TTSWebSocketClient extends WebSocketClient {
private final String taskId = UUID.randomUUID().toString();
private final String outputFile = "output_" + System.currentTimeMillis() + ".mp3";
private boolean taskFinished = false;
public TTSWebSocketClient(URI serverUri, Map<String, String> headers) {
super(serverUri, headers);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("Connection successful");
// Send run-task instruction
// If enable_ssml is true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
String runTaskCommand = "{ \"header\": { \"action\": \"run-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"task_group\": \"audio\", \"task\": \"tts\", \"function\": \"SpeechSynthesizer\", \"model\": \"cosyvoice-v3-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanyang\", \"format\": \"mp3\", \"sample_rate\": 22050, \"volume\": 50, \"rate\": 1, \"pitch\": 1, \"enable_ssml\": false }, \"input\": {} }}";
send(runTaskCommand);
}
@Override
public void onMessage(String message) {
System.out.println("Received server message: " + message);
try {
// Parse JSON message
Map<String, Object> messageMap = new ObjectMapper().readValue(message, Map.class);
if (messageMap.containsKey("header")) {
Map<String, Object> header = (Map<String, Object>) messageMap.get("header");
if (header.containsKey("event")) {
String event = (String) header.get("event");
if ("task-started".equals(event)) {
System.out.println("Received task-started event");
List<String> texts = Arrays.asList(
"Before my bed, moonlight shines bright, I suspect it's frost upon the ground",
"I raise my eyes to gaze at the bright moon, then bow my head, thinking of home"
);
for (String text : texts) {
// Send continue-task instruction
sendContinueTask(text);
}
// Send finish-task instruction
sendFinishTask();
} else if ("task-finished".equals(event)) {
System.out.println("Received task-finished event");
taskFinished = true;
closeConnection();
} else if ("task-failed".equals(event)) {
System.out.println("Task failed: " + message);
closeConnection();
}
}
}
} catch (Exception e) {
System.err.println("Exception occurred: " + e.getMessage());
}
}
@Override
public void onMessage(ByteBuffer message) {
System.out.println("Received binary audio data size: " + message.remaining());
try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
byte[] buffer = new byte[message.remaining()];
message.get(buffer);
fos.write(buffer);
System.out.println("Audio data written to local file " + outputFile);
} catch (IOException e) {
System.err.println("Failed to write audio data to local file: " + e.getMessage());
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Connection closed: " + reason + " (" + code + ")");
}
@Override
public void onError(Exception ex) {
System.err.println("Error: " + ex.getMessage());
ex.printStackTrace();
}
private void sendContinueTask(String text) {
String command = "{ \"header\": { \"action\": \"continue-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": { \"text\": \"" + text + "\" } }}";
send(command);
}
private void sendFinishTask() {
String command = "{ \"header\": { \"action\": \"finish-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": {} }}";
send(command);
}
private void closeConnection() {
if (!isClosed()) {
close();
}
}
public static void main(String[] args) {
try {
// If no environment variable is set, replace next line with: String apiKey = "YOUR_API_KEY"
String apiKey = System.getenv("DASHSCOPE_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("Set DASHSCOPE_API_KEY environment variable");
return;
}
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "bearer " + apiKey);
TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://maas.qwencloudapi.com/api-ws/v1/inference/"), headers);
client.connect();
while (!client.isClosed() && !client.taskFinished) {
Thread.sleep(1000);
}
} catch (Exception e) {
System.err.println("Failed to connect to WebSocket service: " + e.getMessage());
e.printStackTrace();
}
}
}
```
Python
Instale dependências:
```bash
pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
```
```python
import websocket
import json
import uuid
import os
import time
class TTSClient:
def __init__(self, api_key, uri):
"""
Initialize TTSClient instance
Parameters:
api_key (str): API key for authentication
uri (str): WebSocket service address
"""
self.api_key = api_key # Replace with your API key
self.uri = uri # Replace with your WebSocket address
self.task_id = str(uuid.uuid4()) # Generate unique task ID
self.output_file = f"output_{int(time.time())}.mp3" # Output audio file path
self.ws = None # WebSocketApp instance
self.task_started = False # Whether task-started was received
self.task_finished = False # Whether task-finished or task-failed was received
def on_open(self, ws):
"""
Callback when WebSocket connection opens
Send run-task instruction to start speech synthesis
"""
print("WebSocket connected")
# Build run-task instruction
run_task_cmd = {
"header": {
"action": "run-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "cosyvoice-v3-flash",
"parameters": {
"text_type": "PlainText",
"voice": "longanyang",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
# If enable_ssml is True, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
"enable_ssml": False
},
"input": {}
}
}
# Send run-task instruction
ws.send(json.dumps(run_task_cmd))
print("Sent run-task instruction")
def on_message(self, ws, message):
"""
Callback when message is received
Handle text and binary messages separately
"""
if isinstance(message, str):
# Handle JSON text message
try:
msg_json = json.loads(message)
print(f"Received JSON message: {msg_json}")
if "header" in msg_json:
header = msg_json["header"]
if "event" in header:
event = header["event"]
if event == "task-started":
print("Task started")
self.task_started = True
# Send continue-task instruction
texts = [
"Before my bed, moonlight shines bright, I suspect it's frost upon the ground",
"I raise my eyes to gaze at the bright moon, then bow my head, thinking of home"
]
for text in texts:
self.send_continue_task(text)
# Send finish-task after all continue-task instructions
self.send_finish_task()
elif event == "task-finished":
print("Task completed")
self.task_finished = True
self.close(ws)
elif event == "task-failed":
error_msg = msg_json.get("error_message", "Unknown error")
print(f"Task failed: {error_msg}")
self.task_finished = True
self.close(ws)
except json.JSONDecodeError as e:
print(f"JSON parsing failed: {e}")
else:
# Handle binary message (audio data)
print(f"Received binary message, size: {len(message)} bytes")
with open(self.output_file, "ab") as f:
f.write(message)
print(f"Audio data written to local file {self.output_file}")
def on_error(self, ws, error):
"""Callback on error"""
print(f"WebSocket error: {error}")
def on_close(self, ws, close_status_code, close_msg):
"""Callback on close"""
print(f"WebSocket closed: {close_msg} ({close_status_code})")
def send_continue_task(self, text):
"""Send continue-task instruction with text to synthesize"""
cmd = {
"header": {
"action": "continue-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {
"text": text
}
}
}
self.ws.send(json.dumps(cmd))
print(f"Sent continue-task instruction, text: {text}")
def send_finish_task(self):
"""Send finish-task instruction to end speech synthesis"""
cmd = {
"header": {
"action": "finish-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {}
}
}
self.ws.send(json.dumps(cmd))
print("Sent finish-task instruction")
def close(self, ws):
"""Close connection manually"""
if ws and ws.sock and ws.sock.connected:
ws.close()
print("Manually closed connection")
def run(self):
"""Start WebSocket client"""
# Set request headers (authentication)
header = {
"Authorization": f"bearer {self.api_key}",
"X-DashScope-DataInspection": "enable"
}
# Create WebSocketApp instance
self.ws = websocket.WebSocketApp(
self.uri,
header=header,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
print("Listening for WebSocket messages...")
self.ws.run_forever() # Start long-lived connection
# Example usage
if __name__ == "__main__":
# If no environment variable is set, replace next line with: API_KEY = "YOUR_API_KEY"
API_KEY = os.environ.get("DASHSCOPE_API_KEY")
SERVER_URI = "wss://maas.qwencloudapi.com/api-ws/v1/inference/" # Replace with your WebSocket address
client = TTSClient(API_KEY, SERVER_URI)
client.run()
```