Integração da API de Reconhecimento de Voz Qwen3-ASR com Python e cURL

  1. Validação do Ambiente e Pré-requisitos

Antes de estabelecer a comunicação com o serviço de transcrição, é fundamental garantir que o daemon do Qwen3-ASR esteja operacional e acessível na rede. O modelo opera, por padrão, na porta 7860.

1.1 Verificação do Status do Serviço

Utilize os utilitários do sistema para confirmar que o processo está ativo e vinculando à porta correta:

# Verificar o estado do daemon via systemd
sudo systemctl status qwen3-asr

# Alternativamente, inspecionar os processos em execução
ps -ef | grep qwen-asr

# Confirmar a escuta na porta 7860
ss -tlnp | grep 7860

1.2 Preparação do Payload de Áudio

O motor de inferência do Qwen3-ASR otimiza o processamento para arquivos WAV com taxa de amostragem de 16kHz em mono. Caso seu arquivo de origem esteja em outro formato, utilize o ffmpeg para a conversão:

# Converter e normalizar o áudio para o formato exigido
ffmpeg -i midia_original.mp3 -ar 16000 -ac 1 -f wav amostra_teste.wav

  1. Implementação do Cliente em Python

Para integrações em aplicações backend ou pipelines de dados, a abordagem orientada a objetos utilizando a biblioteca requests oferece a melhor flexibilidade. O código abaixo encapsula a lógica de requisição, tratamento de parâmetros e resiliência de rede.

2.1 Cliente Base e Configurações Avançadas

import requests
import json
from typing import Dict, Optional

class QwenASRClient:
    def __init__(self, host: str = "http://127.0.0.1:7860"):
        self.endpoint = f"{host}/api/predict"
        self.session = requests.Session()

    def transcrever(self, media_path: str, idioma: str = "auto", detectar_voz: bool = True) -> Optional[Dict]:
        try:
            with open(media_path, "rb") as arquivo_audio:
                payload = {
                    "language": idioma,
                    "task": "transcribe",
                    "vad": str(detectar_voz).lower()
                }
                arquivos = {"audio": arquivo_audio}
                
                resposta = self.session.post(
                    self.endpoint, 
                    data=payload, 
                    files=arquivos, 
                    timeout=60
                )
                resposta.raise_for_status()
                return resposta.json()
                
        except requests.exceptions.RequestException as erro:
            print(f"Falha na requisicao: {erro}")
            return None
        except FileNotFoundError:
            print(f"Arquivo nao encontrado: {media_path}")
            return None

# Instanciacao e uso
if __name__ == "__main__":
    cliente = QwenASRClient()
    resultado = cliente.transcrever("amostra_teste.wav", idioma="zh")
    if resultado:
        print(f"Texto transcrito: {resultado.get('text')}")

2.2 Processamento Concorrente em Lote

Para processar múltiplos arquivos simultaneamente sem sobrecarregar o servidor, utilize um pool de threads com limite de trabalhadores:

import os
from concurrent.futures import ThreadPoolExecutor, as_completed

def processar_lote(diretorio_audio: str, max_trabalhadores: int = 3):
    cliente = QwenASRClient()
    arquivos_alvo = [
        os.path.join(diretorio_audio, f) 
        for f in os.listdir(diretorio_audio) if f.endswith('.wav')
    ]
    
    resultados = []
    with ThreadPoolExecutor(max_workers=max_trabalhadores) as executor:
        futuros = {executor.submit(cliente.transcrever, f): f for f in arquivos_alvo}
        
        for futuro in as_completed(futuros):
            arquivo = futuros[futuro]
            try:
                dados = futuro.result()
                resultados.append({"arquivo": arquivo, "dados": dados})
            except Exception as e:
                resultados.append({"arquivo": arquivo, "erro": str(e)})
                
    return resultados

  1. Automação e Testes via cURL

O cURL é ideal para testes rápidos de conectividade e para a construção de scripts de automação em ambientes Unix.

3.1 Requisições Diretas e Formatação

# Requisicao basica de transricao
curl -s -X POST http://127.0.0.1:7860/api/predict \
  -F "audio=@amostra_teste.wav" | jq '.text'

# Requisicao com parametros explicitos e salvamento em disco
curl -X POST http://127.0.0.1:7860/api/predict \
  -F "audio=@amostra_teste.wav" \
  -F "language=zh" \
  -F "vad=true" \
  -o transcricao_output.json

3.2 Script Bash para Processamento em Massa

O seguinte script varre um diretório, envia os arquivos para a API e armazena os metadados resultantes:

#!/usr/bin/env bash
set -euo pipefail

API_ENDPOINT="http://127.0.0.1:7860/api/predict"
PASTA_ENTRADA="./audios_brutos"
PASTA_SAIDA="./transcricoes_json"

mkdir -p "$PASTA_SAIDA"

for midia in "$PASTA_ENTRADA"/*.wav; do
    [ -e "$midia" ] || continue
    nome_base=$(basename "$midia" .wav)
    destino="$PASTA_SAIDA/${nome_base}.json"
    
    echo "Processando $midia..."
    
    http_code=$(curl -s -w "%{http_code}" -X POST "$API_ENDPOINT" \
        -F "audio=@$midia" \
        -o "$destino")
        
    if [ "$http_code" -eq 200 ]; then
        echo "Sucesso: $destino"
    else
        echo "Erro $http_code ao processar $midia"
    fi
done

  1. Diagnóstico de Falhas Comuns

Durante a integração, alguns erros podem surgir devido a configurações de ambiente ou formatação de mídia.

4.1 Recusa de Conexão (Connection Refused)

Se o cURL retornar o erro (7) Failed to connect, verifique se o serviço não está vinculado apenas ao localhost ou se há regras de firewall bloqueando a porta 7860. Utilize sudo ufw status ou iptables -L para inspeção.

4.2 Limitação de Tamanho e Timeout

Arquivos longos podem exceder o tempo limite do proxy ou do próprio servidor. A estratégia recomendada é segmentar o áudio antes do envio:

# Segmentar audio em blocos de 3 minutos (180 segundos)
ffmpeg -i audio_longo.wav -f segment -segment_time 180 -c copy bloco_%03d.wav

  1. Aplicações Práticas: Interface Web com Flask

Para fornecer uma interface amigável para usuários finais, é possível expor um gateway utilizando o framework Flask. O código abaixo gerencia o upload temporário, encaminha para o Qwen3-ASR e retorna o JSON processado.

from flask import Flask, request, jsonify, render_template_string
from werkzeug.utils import secure_filename
import os
import requests

app = Flask(__name__)
ASR_URL = "http://127.0.0.1:7860/api/predict"
PASTA_TEMP = "/tmp/asr_uploads"
os.makedirs(PASTA_TEMP, exist_ok=True)

@app.route('/processar', methods=['POST'])
def roteador_transcricao():
    if 'arquivo_voz' not in request.files:
        return jsonify({"erro": "Nenhum arquivo enviado"}), 400
        
    arquivo = request.files['arquivo_voz']
    nome_seguro = secure_filename(arquivo.filename)
    caminho_local = os.path.join(PASTA_TEMP, nome_seguro)
    arquivo.save(caminho_local)
    
    try:
        with open(caminho_local, 'rb') as f:
            resp = requests.post(ASR_URL, files={'audio': f}, timeout=120)
            return jsonify(resp.json())
    except Exception as e:
        return jsonify({"erro": str(e)}), 500
    finally:
        if os.path.exists(caminho_local):
            os.remove(caminho_local)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

O frontend pode ser servido diretamente pelo Flask utilizando um template HTML mínimo com suporte a Drag-and-Drop:


<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <title>Transcritor Qwen3</title>
    <style>
        .zona-drop { border: 2px dashed #888; padding: 40px; text-align: center; cursor: pointer; }
        .zona-drop.ativo { border-color: #000; background: #f0f0f0; }
        #saida { margin-top: 20px; padding: 15px; background: #e9ecef; display: none; }
    </style>
</head>
<body>
    <h2>Envio de Audio para Transcricao</h2>
    <div class="zona-drop" id="areaDrop">
        Arraste o arquivo de audio aqui ou clique para selecionar
        <input type="file" id="seletorArquivo" accept="audio/*" hidden>
    </div>
    <div id="saida"><strong>Resultado:</strong> <p id="textoFinal"></p></div>

    <script>
        const area = document.getElementById('areaDrop');
        const input = document.getElementById('seletorArquivo');
        
        area.onclick = () => input.click();
        input.onchange = (e) => enviarArquivo(e.target.files[0]);
        
        area.ondragover = (e) => { e.preventDefault(); area.classList.add('ativo'); };
        area.ondragleave = () => area.classList.remove('ativo');
        area.ondrop = (e) => {
            e.preventDefault();
            area.classList.remove('ativo');
            enviarArquivo(e.dataTransfer.files[0]);
        };

        function enviarArquivo(file) {
            const formData = new FormData();
            formData.append('arquivo_voz', file);
            
            fetch('/processar', { method: 'POST', body: formData })
                .then(res => res.json())
                .then(data => {
                    document.getElementById('saida').style.display = 'block';
                    document.getElementById('textoFinal').innerText = data.text || data.erro;
                });
        }
    </script>
</body>
</html>

Tags: Qwen3-ASR Reconhecimento de Voz Python cURL Flask

Publicado em 6-16 21:20 por Thomas