Credit: Duy Nguyen Ngoc
Lời mở đầu
Chào mừng các bạn đến với bài viết hướng dẫn lập trình Flutter qua những ứng dụng thú vị. Ở Part 1 chúng ta đã tìm hiểu về ChatGPT, OpenAI, hướng dẫn cách tạo tài khoản, lấy API Key và xây dựng một UI chat đơn giản. Trong bài viết này, chúng ta sẽ tích hợp OpenAI API để tạo ra một trợ lý ảo thông minh. Hãy cùng bắt đầu nào!
Sử dụng API OpenAI để tạo chatbot
Chúng ta đã có giao diện và API Key ở phần 1. Bây giờ để có thể gọi API chúng ta sẽ cài thư viện http bằng câu lệnh sau:
flutter pub add http
Sau khi chạy xong bạn sẽ thấy thư viện được cài nằm trong file pubspec.yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
http: ^0.13.5 # Thư viện vừa cài
Bước tiếp theo, để phần xử lý API này được tách biệt ra khỏi phần giao diện mình sẽ tạo một file chatgpt_api.dart và đặt tên cho class là ChatGPTApi.
Chúng ta sẽ sử dụng API POST tại đường dẫn https://api.openai.com/v1/completions, bạn sẽ cần gửi một yêu cầu HTTP với phần thân (request body) chứa các thông tin cần thiết để thực hiện yêu cầu hoàn thành (completion) văn bản.
Các thông tin có thể bao gồm:
- “model”: Tên mô hình mà bạn muốn sử dụng để hoàn thành văn bản.
- “prompt”: Văn bản mà bạn muốn hoàn thành.
- “max_tokens”: Số lượng tối đa của các ký tự (tokens) mà bạn muốn hoàn thành.
- “stop”: Ký tự (token) dừng của văn bản hoàn thành.
- “temperature”: Mức độ ngẫu nhiên của các kết quả hoàn thành.
- “top_p” là một tham số cho phép bạn đặt một ngưỡng xác suất để lọc các kết quả hoàn thành có xác suất cao hơn ngưỡng đó. Khi sử dụng tham số này, các kết quả hoàn thành có xác suất thấp hơn ngưỡng sẽ bị loại bỏ.
- “n” là số lượng kết quả hoàn thành mà bạn muốn nhận được.
- “stream” là một tham số cho phép bạn yêu cầu API trả về kết quả hoàn thành theo dạng số liệu liên tục (streaming).
- “logprobs” là một tham số cho phép bạn yêu cầu API trả về các xác suất của các kết quả hoàn thành dưới dạng logarithm (log).
Ví dụ, phần thân của một yêu cầu POST có thể như sau:
{
"model": "text-davinci-003",
"prompt": "Say this is a test",
"max_tokens": 7,
"temperature": 0,
"top_p": 1,
"n": 1,
"stream": false,
"logprobs": null,
"stop": "\n"
}
Phần response trả về:
{
"id": "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7",
"object": "text_completion",
"created": 1589478378,
"model": "text-davinci-003",
"choices": [
{
"text": "\n\nThis is indeed a test",
"index": 0,
"logprobs": null,
"finish_reason": "length"
}
],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 7,
"total_tokens": 12
}
}
Dữ liệu text
trong array choices
chính là kết quả mà chúng ta quan tâm và sử dụng.
Lưu ý rằng các thông tin trên chỉ là ví dụ và có thể khác nhau tùy vào yêu cầu của bạn. Hãy kiểm tra tài liệu của API để biết thêm thông tin về các tham số khác có thể sử dụng. (https://beta.openai.com/docs/api-reference/completions)
Code phần ChatGPT Api sẽ được mình triển khai như sau:
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
class Param {
String name;
dynamic value;
Param(this.name, this.value);
@override
String toString() {
return '{ $name, $value }';
}
}
class ChatGPTApi {
String apiKey;
ChatGPTApi({required this.apiKey});
Uri getUrl() {
return Uri.https("api.openai.com", "/v1/completions");
}
Future<String> complete(
String prompt, {
int maxTokens = 2000,
num temperature = 0,
num? topP,
num? frequencyPenalty,
num? presencePenalty,
int? n,
bool? stream,
String? stop,
int? logProbs,
bool? echo,
}) async {
String apiKey = this.apiKey;
List<Param> data = [];
data.add(Param('temperature', temperature));
data.add(Param('top_p', topP));
data.add(Param('frequency_penalty', frequencyPenalty));
data.add(Param('presence_penalty', presencePenalty));
data.add(Param('n', n));
data.add(Param('stream', stream));
data.add(Param('stop', stop));
data.add(Param('logprobs', logProbs));
data.add(Param('echo', echo));
Map mapNullable = {for (var e in data) e.name: e.value};
mapNullable.removeWhere((key, value) => key == null || value == null);
Map mapNotNull = {
"prompt": prompt,
'model': 'text-davinci-003',
"max_tokens": maxTokens,
};
Map reqData = {...mapNotNull, ...mapNullable};
var response = await http.post(
getUrl(),
headers: {
HttpHeaders.authorizationHeader: "Bearer $apiKey",
HttpHeaders.acceptHeader: "application/json",
HttpHeaders.contentTypeHeader: "application/json",
},
body: jsonEncode(reqData),
);
if (response.statusCode != 200) {
if (response.statusCode == 429) {
throw Exception('Rate limited');
} else {
throw Exception('Failed to send message');
}
} else if (_errorMessages.contains(response.body)) {
throw Exception('OpenAI returned an error');
}
Map<String, dynamic> newresponse = jsonDecode(
utf8.decode(response.bodyBytes),
);
if (newresponse['error'] != null) {
throw Exception(newresponse['error']['message']);
} else {
return newresponse['choices'][0]['text'];
}
}
}
const _errorMessages = [
"{\"detail\":\"Hmm...something seems to have gone wrong. Maybe try me again in a little bit.\"}",
];
Trong đó, class Param
là model chứa tên của param là name
và value
là một dữ liệu nullable. Mục đích là giúp chúng ta có nhiều option cho API và lọc những value có không null.
Class ChatGPTApi
là nơi ta sẽ gọi và xử lý với API. Đầu vào sẽ nhận một tham số bắt buộc là một chuỗi API Key. Future complete sẽ là nơi chúng ta gọi api, xử lý lỗi và trả về kết quả.
Sau khi tạo xong phần ChatGPT Api chúng ta tiến hành tích hợp nó với giao diện, tại _ChatPageState chúng ta thêm api vừa tạo và điền API Key vào:
class _ChatPageState extends State<ChatPage> {
final _textController = TextEditingController();
final _scrollController = ScrollController();
final List<ChatMessage> _messages = [];
late bool isLoading;
ChatGPTApi chatGPTApi = ChatGPTApi(apiKey: 'YOUR API KEY');
@override
void initState() {
super.initState();
isLoading = false;
}
Tiếp theo, trong widget _buildSubmit
tại hàm onPressed
bạn xử lý như sau:
onPressed: () async {
setState(
() {
_messages.add(
ChatMessage(
text: _textController.text,
chatMessageType: ChatMessageType.user,
),
);
isLoading = true;
},
);
final input = _textController.text;
_textController.clear();
Future.delayed(const Duration(milliseconds: 50))
.then((_) => _scrollDown());
chatGPTApi.complete(input).then((value) {
setState(() {
isLoading = false;
_messages.add(
ChatMessage(
text: value,
chatMessageType: ChatMessageType.bot,
),
);
});
}).catchError((error) {
setState(
() {
final snackBar = SnackBar(
content: Text(error.toString()),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
isLoading = false;
},
);
});
},
Bây giờ bạn hãy run và xem thành quả đạt được ❤️ :
Hoàn thiện trợ lý giọng nói thông minh
Vậy là chúng ta vừa hoàn thành một con chatbot thông minh chỉ với một vài bước cơ bản. Để trở thành một Smart Voice Assistant chúng ta sẽ phải xây dựng hai tính năng như sau cho ứng dụng:
- Lắng nghe giọng nói và chuyển đổi thành text để gửi cho chatbot
- Sau khi nhận phản hồi từ chatbot thì chuyển đổi nó thành giọng nói.
Việc đầu tiên sẽ là thiết kế giao diện cho 2 tính năng này.
Phần giao diện:
Thêm một nút voice để thu giọng nói ở phía bên phải nút gửi tin nhắn
Padding(
padding: const EdgeInsets.all(8.0),
child: Visibility(
visible: !isListening,
child: Row(
children: [
_buildInput(),
const SizedBox(width: 5),
_buildSubmit(),
const SizedBox(width: 5),
_buildVoice(),
],
),
),
),
],
),
),
);
}
Widget _buildVoice() {
return Visibility(
visible: !isLoading,
child: GestureDetector(
onTap: _listen,
child: const CircleAvatar(
backgroundColor: AppColors.botBackgroundColor,
radius: 25,
child: Icon(
Icons.mic_none,
color: Colors.white,
),
),
),
);
}
Tiếp đó tạo một nút voice với animation từ thư viện avatar_glow , giúp chúng ta nhận biết được trạng thái của voice. Tại hàm build
ta thêm floatingActionButtonLocation
và floatingActionButton
vào AppBar
. Cùng với đó là hiển thị danh sách ngôn ngữ bằng cách thêm một action
vào AppBar
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 100,
title: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"OpenAI's ChatGPT Flutter",
maxLines: 2,
textAlign: TextAlign.center,
),
),
backgroundColor: const Color(0xff10a37f),
actions: [
if (languages != null)
PopupMenuButton(
itemBuilder: (context) => (languages ?? [])
.map(
(value) => PopupMenuItem(
value: value,
child: Text(
value,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: langCode == value
? const Color(0xff10a37f)
: Colors.black,
),
),
onTap: () {
setState(() async {
langCode = value;
});
},
),
)
.toList(),
)
],
),
backgroundColor: Colors.white,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Visibility(
visible: isListening,
child: AvatarGlow(
animate: isListening,
glowColor: Theme.of(context).primaryColor,
endRadius: 75.0,
duration: const Duration(milliseconds: 2000),
repeatPauseDuration: const Duration(milliseconds: 100),
repeat: true,
child: GestureDetector(
onTap: _listen,
child: const CircleAvatar(
backgroundColor: Color(0xff10a37f),
radius: 40,
child: Icon(
Icons.close,
color: Colors.white,
),
),
),
),
),
body: _buildBody(context),
);
}
Sau đó, thêm đoạn sau vào hàm _buildList
để kiểm tra nếu đang speech sẽ hiển thị text speech thay cho danh sách tin nhắn:
Widget _buildList() {
if (isListening) {
return Center(
child: Text(
_textController.text,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.black,
),
),
);
}
Và cuối cùng là thêm biến isListening và hàm _listen:
class _ChatPageState extends State<ChatPage> {
final _textController = TextEditingController();
final _scrollController = ScrollController();
final List<ChatMessage> _messages = [];
late bool isLoading;
late bool isListening;
String langCode = "en-US";
List<String>? languages;
ChatGPTApi chatGPTApi = ChatGPTApi(apiKey: 'YOUR API KEY');
@override
void initState() {
super.initState();
isLoading = false;
isListening = true;
}
void _listen() {}
Chúng ta sẽ có giao diện như sau:
Phần Logic:
Ở phần này chúng ta cần thêm 2 thư viện sau để có thể xử lý các chức của một Smart Voice Assistant, đó là:
- speech_to_text : Thư viện này giúp chúng ta chuyển đổi phát biểu của chúng ta thành text
- flutter_tts: Thư viện này giúp chúng ta đọc những phản hồi từ API OpenAI
Sau khi thêm 2 thư viện trên cần phải config như sau:
- Đối với ios bạn thêm các dòng sau vào file Info.plist trong thư mục ios/Runner
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
......
<key>NSSpeechRecognitionUsageDescription</key> <!-- Thêm vào -->
<string>$(PRODUCT_NAME) uses Speech Recognition</string> <!-- Thêm vào -->
<key>NSMicrophoneUsageDescription</key> <!-- Thêm vào -->
<string>$(PRODUCT_NAME) uses Microphone</string> <!-- Thêm vào -->
</dict>
</plist>
- Đối với android bạn sẽ phải thêm các dòng sau vào file android/app/main/AndroidManifest.xml
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- Ngoài ra nếu bạn đang nhắm mục tiêu SDK Android,
tức là bạn đặt targetSDKVersion là dưới 30,
thì bạn sẽ không cần thêm phần ở dưới-->
<queries>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
</queries>
Và cuối cùng là thêm các dòng này vào file pubspec.yml
# To add assets to your application, add an assets section, like this:
assets:
- assets/sounds/speech_to_text_listening.m4r
- assets/sounds/speech_to_text_cancel.m4r
- assets/sounds/speech_to_text_stop.m4r
3 file này mình lấy từ https://github.com/Appli-chic/google-translate-flutter/tree/master/assets/sounds. Bạn cũng có thể thay đổi âm thanh theo sở thích của bạn, tuy nhiên nhớ lưu ý đặt đúng tên file và đường dẫn cho đúng. Chi tiết config bạn có thể xem tại dây
Như vậy đã xong phần config và giao diện. Bây giờ chúng ta bắt tay vào code phần chính cho chức năng thôi nào. 💪
import 'package:flutter_tts/flutter_tts.dart'; // thêm vào
import 'package:speech_to_text/speech_to_text.dart' as stt; // thêm vào
class _ChatPageState extends State<ChatPage> {
final _textController = TextEditingController();
final _scrollController = ScrollController();
final List<ChatMessage> _messages = [];
late bool isLoading;
late bool isListening;
final ChatGPTApi openAI = ChatGPTApi(apiKey: 'YOUR API KEY');
final speechToText = stt.SpeechToText(); // thêm vào
final flutterTts = FlutterTts(); // thêm vào
double volume = 1.0; // thêm vào
double pitch = 1.0; // thêm vào
double speechRate = 0.5; // thêm vào
List<String>? languages;
String langCode = "en-US";
@override
void initState() {
super.initState();
init();
}
init() async {
isLoading = false;
isListening = false;
final newlanguages = List<String>.from(await flutterTts.getLanguages);
setState(() async {
languages = newlanguages;
});
}
Trong đó:
speechToText
: api xử lý phát ngôn của bạnflutterTts
: api xử lý phản hồi từ chatbot ra âm thanhvolume
: Âm lượngpitch
: Cao độspeechRate
: Tốc độ nóilangCode
: Ngôn ngữ sẽ nóilanguages
: Danh sách ngôn ngữ hỗ trợ nói- hàm
flutterTts.getLanguages
là dùng để lấy danh sách ngôn ngữ mà api hỗ trợ.
Kế đến, chúng ta viết các hàm xử lý sau:
void _listen() async {
_stop();
if (!isListening) {
final avilable = await speechToText.initialize(
onStatus: (val) => print('onStatus: $val'),
onError: (val) => print('onError: $val'),
);
if (avilable) {
setState(() {
isListening = true;
speechToText.listen(onResult: (result) {
setState(() {
_textController.text = result.recognizedWords;
});
});
});
}
} else {
setState(() => isListening = false);
speechToText.stop();
}
}
void initSetting() async {
await flutterTts.setVolume(volume);
await flutterTts.setPitch(pitch);
await flutterTts.setSpeechRate(speechRate);
await flutterTts.setLanguage(langCode);
}
void _speak(text) async {
initSetting();
await flutterTts.speak(text);
}
void _stop() async {
await flutterTts.stop();
}
Trong đó:
- Hàm
_listen
là một toggle đóng vai trò trong việc xử lý lắng nghe phát ngôn từ người dùng, cập nhật trạng thái biếnisListening
và cập nhật text nhận diện được lên giao diện. - Hàm
initSetting
chịu trách nhiệm config âm lượng, ngôn ngữ, tốc độ nói và cao độ từ các biến. - Hàm
_speak
gọi hàminitSetting
và bắt đầu đọc các kí tự ở đầu vào của hàm. - Hàm
_stop
là dùng để dừng đọc.
Và cuối cùng là cập nhật hàm onPressed
của widget _buildSubmit
:
onPressed: () async {
setState(
() {
_messages.add(
ChatMessage(
text: _textController.text,
chatMessageType: ChatMessageType.user,
),
);
isLoading = true;
},
);
final input = _textController.text;
_textController.clear();
Future.delayed(const Duration(milliseconds: 50))
.then((_) => _scrollDown());
chatGPTApi.complete(input).then((value) {
setState(() {
isLoading = false;
_messages.add(
ChatMessage(
text: value,
chatMessageType: ChatMessageType.bot,
),
);
_speak(value); // Thêm vào
});
}).catchError((error) {
setState(
() {
final snackBar = SnackBar(
content: Text(error.toString()),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
isLoading = false;
},
);
});
},
Bậy giờ bạn hãy chạy lên và “enjoy the moment” 😍
Sản phẩm: link video
Tổng kết
Trong bài blog này, mình đã hướng dẫn tích hợp API của OpenAI vào dự án Flutter. Sử dụng Flutter đã giúp bạn xây dựng ứng dụng Smart Voice Assistant của chúng ta cho nhiều hệ điều hành khác nhau một cách dễ dàng và rất nhanh chóng.
Sử dụng OpenAI API cũng là phần quan trọng cho việc xây dựng trợ lý giọng nói thông minh. OpenAI API cung cấp nhiều tuỳ chọn và giúp việc xây dựng trợ lý giọng nói có thể trả lời câu hỏi của người dùng một cách chính xác và hiểu ý nghĩa của câu hỏi.
Một số ý tưởng cho các tính năng mà bạn có thể thử nghiệm vào dự án này:
- Vì API Key là bí mật mình khuyến khích các bạn nên xây dựng một server và gọi đến OpenAI thay vì client.
- Thêm tuỳ chọn tốc độ đọc, cao độ và âm lượng.
- Thêm màn hình và chức năng Generate ART work bằng việc sử dụng API https://api.openai.com/v1/images/generations
Hy vọng rằng sau series này, mọi người sẽ xây dựng được một trợ lý ảo thông minh ChatGPT cho riêng mình với những thông tin và gợi ý mà mình đã giới thiệu.