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:

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:

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 AppBarCù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à:

Sau khi thêm 2 thư viện trên cần phải config như sau:

<?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>
<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 đó:

 

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 đó:

 

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:

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.