top of page

Como integrar Realtime API Speeh To Speech da OpenAI no Flutter

Atualizado: 14 de out. de 2024

Integrando a Nova API em Tempo Real da OpenAI em Flutter

Recentemente, a OpenAI lançou sua nova API para interação em tempo real com os modelos LLM. O melhor de tudo é a nova API que nos permite realizar uma conexão via WebSockets, enviar áudios e receber respostas por texto ou chunks de áudios da fala dos modelos. Esta tecnologia permite o desenvolvimento de chatbots por conversa de maneira tão realista que podemos até interrompê-los enquanto respondem, além da capacidade do modelo detectar quando estamos falando novamente.


Quem se lembra da primeira versão de conversação do modelo da OpenAI? A questão era muito "limitada" em comparação com essa nova tecnologia. A interação era sempre a fala do humano, seguida da resposta do modelo, e assim por diante - algo muito programático. Com essa nova tecnologia, tudo fica mais interativo e dinâmico!


Sem mais delongas, vamos integrar isso ao nosso aplicativo em Flutter.


Instalação dos Pacotes

Primeiro, precisamos instalar os pacotes que vamos utilizar para fazer a integração. Adicione as seguintes dependências ao seu arquivo `pubspec.yaml`:


environment:
  sdk: '>=3.3.3 <4.0.0'
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  record: ^5.0.4
  path_provider: ^2.0.2
  permission_handler: ^11.0.1
  web_socket_channel: ^2.1.0
  audioplayers:

Captura de Áudio

Agora, vamos criar nosso fluxo de captura de áudio, de modo que seja possível enviar os pacotes de bytes de áudio para o servidor da OpenAI via WebSockets:


import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:record/record.dart';
class SpeechPage extends StatefulWidget {
  @override
  _SpeechPageState createState() => _SpeechPageState();
}
class _SpeechPageState extends State<SpeechPage> {
  final AudioRecorder _record = AudioRecorder();
  StreamSubscription<Uint8List>? _audioStreamSubscription;
  List<Uint8List> _audioChunks = [];
  @override
  void initState() {
    super.initState();
  }
  @override
  void dispose() {
    _audioStreamSubscription?.cancel();
    _record.dispose();
    super.dispose();
  }
  Future<void> _startRecording() async {
    bool hasPermission = await _record.hasPermission();
    if (hasPermission) {
      RecordConfig encoder = const RecordConfig(
        encoder: AudioEncoder.pcm16bits,
        sampleRate: 24000,
        numChannels: 1,
      );
      
      Stream<Uint8List> audioStream = await _record.startStream(encoder);
      
      _audioStreamSubscription = audioStream.listen((data) {
        String base64Audio = base64Encode(data);
        print(base64Audio.length);
      });
    }
  }
  Future<void> _stopRecording() async {
    await _record.stop();
    _audioStreamSubscription?.cancel();
  }
  @override
  Widget build(BuildContext context) {
    // Interface do usuário
  }
}

Integração com WebSockets

Agora, vamos criar uma classe para gerenciar todas as operações, desde o tratamento de resposta do servidor até a conexão e envio de novos dados:


class OpenAIRealtimeService {
  final String apiKey;
  IOWebSocketChannel? _channel;
  OpenAIRealtimeService(this.apiKey);
  List<Uint8List> _audioBuffer = [];
  void connect() {
    final uri = Uri.parse(
      'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01',
    );
    final headers = {
      'Authorization': 'Bearer $apiKey',
      'OpenAI-Beta': 'realtime=v1',
    };
    _channel = IOWebSocketChannel.connect(
      uri,
      headers: headers,
    );
    _channel?.stream.listen(
      (message) {
        print('Mensagem recebida: $message');
        _handleServerMessage(message);
      },
      onError: (error) {
        print('Erro na conexão WebSocket: $error');
      },
      onDone: () {
        print('Conexão WebSocket encerrada.');
      },
    );
    // Enviar mensagem inicial para configurar a resposta
    final initEvent = {
      "type": "response.create",
      "response": {
        "modalities": ["text", "audio"],
        "instructions": """Você é uma assistente...""",
        "voice": "alloy",
        "input_audio_format": "pcm16",
        "output_audio_format": "pcm16",
        "input_audio_transcription": {
          "enabled": true,
          "model": "whisper-1"
        },
        "turn_detection": null,
        "temperature": 0.8,
        "max_output_tokens": null
      },
    };
    sendMessage(jsonEncode(initEvent));
    print('Mensagem inicial enviada: $initEvent');
  }
  // Implementação dos métodos disconnect(), sendMessage(), _handleServerMessage(), etc.
}

Reprodução de Áudio

Vamos adicionar métodos para reproduzir o áudio recebido do modelo:


import 'package:audioplayers/audioplayers.dart';

import 'package:path_provider/path_provider.dart';


class OpenAIRealtimeService {
  // código anterior ......
  final AudioPlayer _audioPlayer = AudioPlayer();
  Future<String> saveAudioToFile(Uint8List audioData) async {
    final tempDir = await getTemporaryDirectory();
    final tempFile = File('${tempDir.path}/temp_audio.wav');
    await tempFile.writeAsBytes(audioData, flush: true);
    return tempFile.path;
  }
  Uint8List addWavHeader(Uint8List pcmData, int sampleRate, int channels) {
    // Implementação do método addWavHeader
 }
  Uint8List _intToBytes(int value, int bytes) {
    // Implementação do método _intToBytes
  }
  void _playAudio(Uint8List audioData) async {
    try {
      Uint8List wavData = addWavHeader(audioData, 24000, 1);
      String filePath = await saveAudioToFile(wavData);
      await _audioPlayer.play(DeviceFileSource(filePath));
    } catch (e) {
      print('Erro ao reproduzir áudio: $e');
    }
  }
}

Integração na Interface do Usuário

Finalmente, vamos integrar todos esses processos na nossa interface para começar a utilização do nosso aplicativo chatbot Speech-to-Speech:


class SpeechPage extends StatefulWidget {
  @override
  _SpeechPageState createState() => _SpeechPageState();
}
class _SpeechPageState extends State<SpeechPage> {
  final AudioRecorder _record = AudioRecorder();
  StreamSubscription<Uint8List>? _audioStreamSubscription;
  List<Uint8List> _audioChunks = [];
  late OpenAIRealtimeService _openAIService;
  @override
  void initState() {
    super.initState();
    _openAIService = OpenAIRealtimeService('<Sua Chave de API>');
  }
  @override
  void dispose() {
    _audioStreamSubscription?.cancel();
    _record.dispose();
    super.dispose();
  }
  Future<void> _startRecording() async {
    // Implementação do método _startRecording
  }
  Future<void> _stopRecording() async {
    // Implementação do método _stopRecording
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Captura de Áudio"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            ElevatedButton(
              onPressed: _startRecording,
              child: Text("Iniciar Gravação"),
            ),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: _stopRecording,
              child: Text("Parar Gravação"),
            ),
            SizedBox(height: 20),
            Text(
              "Chunks de Áudio:",
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            Expanded(
              child: ListView.builder(
                itemCount: _audioChunks.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text("Chunk ${index + 1}"),
                    subtitle: Text(
                        "Tamanho: ${_audioChunks[index].length} bytes"),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Pronto! Agora temos uma integração completa da nova API em tempo real da OpenAI em nosso aplicativo Flutter. Esta implementação permite a captura de áudio, o envio para a API da OpenAI, e a reprodução das respostas geradas pelo modelo.


Código completo:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/status.dart' as status;
import 'package:flutter/material.dart';
import 'package:record/record.dart';
import 'package:audioplayers/audioplayers.dart';

class SpeechPage extends StatefulWidget {
  @override
  _SpeechPageState createState() => _SpeechPageState();
}

class _SpeechPageState extends State<SpeechPage> {
  final AudioRecorder _record = AudioRecorder();
  StreamSubscription<Uint8List>? _audioStreamSubscription;
  List<Uint8List> _audioChunks = [];
  late OpenAIRealtimeService _openAIService;

  @override
  void initState() {
    super.initState();
    _openAIService = OpenAIRealtimeService('xx-xx-XXXXX...');
  }

  @override
  void dispose() {
    _audioStreamSubscription?.cancel();
    _record.dispose();
    super.dispose();
  }

  Future<void> _startRecording() async {
    _openAIService.connect();
    bool hasPermission = await _record.hasPermission();
    if (hasPermission) {
      RecordConfig encoder = const RecordConfig(
        encoder: AudioEncoder.pcm16bits,
        sampleRate: 24000,
        numChannels: 1,
      );

      Stream<Uint8List> audioStream = await _record.startStream(encoder);

      _audioStreamSubscription = audioStream.listen((data) {
        String base64Audio = base64Encode(data);

        // Crie o evento de append
        final event = {
          "type": "input_audio_buffer.append",
          "audio": base64Audio,
        };

        // Envie o evento para a API
        _openAIService.sendMessage(jsonEncode(event));

        final initEvent = {
          "type": "response.create",
          "item": {
            "type": "message",
            "role": "user",
            "content": [{
              "type": "audio",
              "audio": base64Audio
            }]
          }
        };
        _openAIService.sendMessage(jsonEncode(initEvent));
        print('Mensagem inicial enviada: $initEvent');

        print('Chunk de áudio enviado: tamanho ${data.length} bytes');
      });

    }
  }

  Future<void> _stopRecording() async {
    await _record.stop();
    _audioStreamSubscription?.cancel();

    // Envie o evento de commit para processar o buffer de áudio
    final commitEvent = {"type": "input_audio_buffer.commit"};
    _openAIService.sendMessage(jsonEncode(commitEvent));
    print('Evento de commit enviado: $commitEvent');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Captura de Áudio"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            ElevatedButton(
              onPressed: _startRecording,
              child: Text("Iniciar Gravação"),
            ),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: _stopRecording,
              child: Text("Parar Gravação"),
            ),
            SizedBox(height: 20),
            Text(
              "Chunks de Áudio:",
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            Expanded(
              child: ListView.builder(
                itemCount: _audioChunks.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text("Chunk ${index + 1}"),
                    subtitle: Text(
                        "Tamanho: ${_audioChunks[index].length} bytes"),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class OpenAIRealtimeService {
  final String apiKey;
  IOWebSocketChannel? _channel;

  OpenAIRealtimeService(this.apiKey);

  List<Uint8List> _audioBuffer = [];

  void connect() {
    final uri = Uri.parse(
      'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01',
    );

    final headers = {
      'Authorization': 'Bearer $apiKey',
      'OpenAI-Beta': 'realtime=v1',
    };

    _channel = IOWebSocketChannel.connect(
      uri,
      headers: headers,
    );

    _channel?.stream.listen(
          (message) {
        print('Mensagem recebida: $message');
        _handleServerMessage(message);
      },
      onError: (error) {
        print('Erro na conexão WebSocket: $error');
      },
      onDone: () {
        print('Conexão WebSocket encerrada.');
      },
    );
    // Enviar mensagem inicial para configurar a resposta
    final initEvent = {
      "type": "response.create",
      "response": {
        "modalities": ["text", "audio"],
        "instructions": """Você é uma assistente de IA especializado chamado João e gosta de contar piadas...""",
        "voice": "alloy",
        "input_audio_format": "pcm16",
        "output_audio_format": "pcm16",
        "input_audio_transcription": {
          "enabled": true,
          "model": "whisper-1"
        },
        "turn_detection": null,
        "temperature": 0.8,
        "max_output_tokens": null
      },
    };
    sendMessage(jsonEncode(initEvent));
    print('Mensagem inicial enviada: $initEvent');
  }

  void disconnect() {
    _channel?.sink.close(status.goingAway);
  }

  void sendMessage(String message) {
    if (_channel != null) {
      print('Enviando mensagem: $message');
      _channel?.sink.add(message);
    }
  }

  void _handleServerMessage(dynamic message) {
    final event = jsonDecode(message);
    print('Evento recebido: ${event['type']}');

    switch (event['type']) {
      case 'conversation.item.input_audio_transcription.completed':
        break;
      case 'response.audio.delta':
      // Processa cada pedaço de áudio recebido
        String base64Audio = event['delta'];

        Uint8List audioData = base64Decode(base64Audio);
        _audioBuffer.add(audioData);
        break;

      case 'response.done':
      // Reproduz o áudio completo ao receber a confirmação do término
        Uint8List fullAudioData = Uint8List.fromList(_audioBuffer.expand((x) => x).toList());
        _playAudio(fullAudioData);
        _audioBuffer.clear();

        String transcript = event['response']['output'][0]['content'][0]["transcript"];
        print('Transcrição recebida: $transcript');

        final userMessageEvent = {
          "type": "conversation.item.create",
          "item": {
            "type": "message",
            "role": "assistant",
            "content": [
              {
                "type": "text",
                "text": transcript,
              }
            ]
          }
        };
        sendMessage(jsonEncode(userMessageEvent));
        print('Mensagem do usuário adicionada ao histórico.');
        break;

      case 'error':
      // Tratar erros
        print('Erro recebido: ${event['error']}');
        break;

      default:
        print('Evento não tratado: ${event['type']}');
    }
  }

  final AudioPlayer _audioPlayer = AudioPlayer();

  Future<String> saveAudioToFile(Uint8List audioData) async {
    final tempDir = await getTemporaryDirectory();
    final tempFile = File('${tempDir.path}/temp_audio.wav');
    await tempFile.writeAsBytes(audioData, flush: true);
    return tempFile.path;
  }

  Uint8List addWavHeader(Uint8List pcmData, int sampleRate, int channels) {
    int byteRate = sampleRate * channels * 2; // 16 bits = 2 bytes
    int blockAlign = channels * 2;
    int dataLength = pcmData.length;

    BytesBuilder builder = BytesBuilder();
    builder.add(ascii.encode('RIFF'));
    builder.add(_intToBytes(36 + dataLength, 4)); // ChunkSize
    builder.add(ascii.encode('WAVE'));
    builder.add(ascii.encode('fmt '));
    builder.add(_intToBytes(16, 4)); // Subchunk1Size
    builder.add(_intToBytes(1, 2)); // AudioFormat (PCM)
    builder.add(_intToBytes(channels, 2));
    builder.add(_intToBytes(sampleRate, 4));
    builder.add(_intToBytes(byteRate, 4));
    builder.add(_intToBytes(blockAlign, 2));
    builder.add(_intToBytes(16, 2)); // BitsPerSample
    builder.add(ascii.encode('data'));
    builder.add(_intToBytes(dataLength, 4));
    builder.add(pcmData);

    return builder.toBytes();
  }

  Uint8List _intToBytes(int value, int bytes) {
    final ByteData byteData = ByteData(bytes);
    for (int i = 0; i < bytes; i++) {
      byteData.setUint8(i, (value >> (8 * i)) & 0xFF);
    }
    return byteData.buffer.asUint8List();
  }


  void _playAudio(Uint8List audioData) async {
    try {
      Uint8List wavData = addWavHeader(audioData, 24000, 1);

      // Salvar o áudio em um arquivo temporário
      String filePath = await saveAudioToFile(wavData);

      // Reproduzir o áudio a partir do arquivo
      await _audioPlayer.play(DeviceFileSource(filePath));

    } catch (e) {
      print('Erro ao reproduzir áudio: $e');
    }
  }
}

É hora de testar essa nova tecnologia que a OpenAI disponibilizou para nós! Obrigado e até logo!

Comentarios


bottom of page