QwenCloudBRASIL
Tradução de apoio para consulta. Código e identificadores técnicos preservados. Consultar referência original ↗

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:

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

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:

  1. Modelo: Somente cosyvoice-v3-flash e cosyvoice-v3-plus suporte SSML.

  2. 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_ssml habilitado.

  3. Parâmetro: Definir enable_ssml para true na 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

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:

  1. Abra uma conexão WebSocket.

  2. Envie a instrução de execução da tarefa para iniciar uma tarefa.

  3. Aguarde o evento de tarefa iniciada antes de prosseguir.

  4. 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 text campo 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.

  5. Receba o fluxo de áudio através do binary canal.

  6. 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.

  7. Receba o evento de tarefa concluída do servidor.

  8. 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:

Prática recomendada:

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:

Dicas específicas da plataforma

##URL

wss://maas.qwencloudapi.com/api-ws/v1/inference

Erros comuns de URL:

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:

  1. Verifique o formato da chave API: Confirme os usos do cabeçalho de autorização bearer YOUR_API_KEY com um espaço entre bearer e a chave.
  2. 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.
  3. 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))
  4. 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

  1. Conecte-se a CosyVoice do seu backend (Node.js, Java ou Python), onde você pode definir o cabeçalho Authorization.
  2. Faça com que o frontend se conecte ao seu backend via WebSocket, que encaminha mensagens para CosyVoice.
  3. 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.

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:

  1. Enviar executar tarefa
  2. Enviar continuar tarefa
    • Envia texto para sintetizar.
    • Enviar somente após receber tarefa iniciada.
  3. 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.

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:

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.

  1. Envie uma nova tarefa de execução somente após receber tarefa concluída.
  2. Use task_ids diferentes para tarefas diferentes na mesma conexão.
  3. Acionador de tarefas com falha tarefa com falha e feche a conexão (não é possível reutilizar).
  4. 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:

Solucionar problemas de conexões lentas (>30 segundos):

  1. Latência de rede: Verifique a qualidade da conexão internacional ou o desempenho do ISP.
  2. DNS lento: Experimente o DNS público (8.8.8.8) ou configure um arquivo de hosts locais para maas.qwencloudapi.com.
  3. Handshake TLS: Atualize para TLS 1.2 ou posterior.
  4. Proxy/firewall: Redes corporativas podem bloquear ou retardar conexões WebSocket.

Ferramentas de solução de problemas:

Velocidade de geraçã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:

  1. Conectar: Chame a função de conexão da sua biblioteca WebSocket com Headers e URL.

  2. 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.
  3. Enviar instruções: Em um thread separado, envie instruções ao servidor.

  4. 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()
```