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