(Flutter) Chat GPT Client 만들어보기 with 음성인식 추가

 

지난 글에서 만들어 보았던 ChatGPT GUI Client App을 단순히 따라해 보기만 하고 끝내기는 아쉬워 음성인식 기능을 추가해 보도록 하겠습니다.

이 글에서 다루는 코드는 다음 Repository에서 확인할 수 있습니다.
ChatGPT_Flutter

음성 인식 처리

Flutter의 음성 인식 관련 패키지를 찾아 보니 flutter_speech 패키지를 발견 했습니다.
사용 방법도 단순하고 Android와 iOS, MacOS를 지원해서 단순 음성 인식 처리에는 적절한 패키지인 것 같습니다.

음성 인식 기능 추가에 필요한 정보는 총 5가지 입니다.

  1. 음성 인식 언어 코드 (영어권, 한국어권, 등)
  2. flutter_speech 패키지 클래스
  3. 음성 인식 사용 가능 여부
  4. 음성 인식 중 여부
  5. 음성 인식 결과 Text

해당 패키지를 추가 하고 ChatMessageViewModel ViewModel 부분에 음성 인식 기능에 필요한 추가 속성을 정의 합니다.

[src/viewmodels/chat_message_viewmodel.dart]

class ChatMessageViewModel {
  String? prompt;
  String message;
  bool isSent;
  bool isAwaiting;
  bool isError;
  bool canRemove;
  bool isEditing;
  ChatMessageViewModel? result;

  // 음성 인식 관련
  bool speechRecognitionAvailable;
  bool isListening;
  String transcription;
  bool speechRecognitionError;

  ChatMessageViewModel(
    this.prompt,
    this.message, {
    this.isSent = false,
    this.isAwaiting = false,
    this.isError = false,
    this.canRemove = false,
    this.isEditing = false,
    this.speechRecognitionAvailable = false,
    this.isListening = false,
    this.transcription = '',
    this.speechRecognitionError = false,
    this.result,
  });
}

그리고 ChatMessageCubit BLoC에 flutter_speech 패키지의 SpeechRecognition 인스턴스 초기화 처리 및 관련 이벤트 콜백 메서드를 구현 합니다.

[src/bloc/chat_message_cubit.dart]

class ChatMessageCubit extends Cubit<ChatMessageState> {
  ...[중간 생략]...
  
  // 음성 인식 관련
  // 영어권인 경우 'en_US'
  final String speechRecognition_locale = 'ko_KR';
  late SpeechRecognition _speech;
  late ChatMessageViewModel? _speechRecognitionchatTargetMessage;
  
  void activateSpeechRecognizer(ChatMessageViewModel chatMessage) {
    _speechRecognitionchatTargetMessage = chatMessage;

    //print('_MyAppState.activateSpeechRecognizer... ');
    _speech = SpeechRecognition();
    _speech.setAvailabilityHandler(_onSpeechAvailability);
    _speech.setRecognitionStartedHandler(_onRecognitionStarted);
    _speech.setRecognitionResultHandler(_onRecognitionResult);
    _speech.setRecognitionCompleteHandler(_onRecognitionComplete);
    _speech.setErrorHandler(_errorHandler);
    _speech.activate(speechRecognition_locale).then((res) {
      if (_speechRecognitionchatTargetMessage != null) {
        _speechRecognitionchatTargetMessage!.transcription = '';
        _speechRecognitionchatTargetMessage!.speechRecognitionError = false;
        _speechRecognitionchatTargetMessage!.speechRecognitionAvailable = res;
        emit(ChatMessageChangeState(_speechRecognitionchatTargetMessage!));
      }
    });
  }

  /// 음성 인식 유효성 검증
  void _onSpeechAvailability(bool result) {
    if (_speechRecognitionchatTargetMessage != null) {
      _speechRecognitionchatTargetMessage!.speechRecognitionAvailable = result;
      emit(ChatMessageChangeState(_speechRecognitionchatTargetMessage!));
    }
  }

  /// 음성 인식 시작
  void _onRecognitionStarted() {
    if (_speechRecognitionchatTargetMessage != null) {
      _speechRecognitionchatTargetMessage!.isListening = true;
      emit(ChatMessageChangeState(_speechRecognitionchatTargetMessage!));
    }
  }

  /// 음성 인식 결과
  void _onRecognitionResult(String text) {
    if (_speechRecognitionchatTargetMessage != null) {
      _speechRecognitionchatTargetMessage!.transcription = text;
      emit(ChatMessageChangeState(_speechRecognitionchatTargetMessage!));
    }
  }

  /// 음성 인식 완료
  void _onRecognitionComplete(String text) {
    if (_speechRecognitionchatTargetMessage != null) {
      _speechRecognitionchatTargetMessage!.transcription = text;
      _speechRecognitionchatTargetMessage!.isListening = false;
      _speechRecognitionchatTargetMessage!.speechRecognitionError = false;
      emit(ChatMessageChangeState(_speechRecognitionchatTargetMessage!));
    }
  }

  /// 음성 인식 오류 발생
  void _errorHandler() {
    if (_speechRecognitionchatTargetMessage != null) {
      _speechRecognitionchatTargetMessage!.transcription = '';
      _speechRecognitionchatTargetMessage!.isListening = false;
      _speechRecognitionchatTargetMessage!.speechRecognitionError = true;

      activateSpeechRecognizer(_speechRecognitionchatTargetMessage!);
    }
  }
  
  ...[중간 생략]...
}

음성 인식 시작은 SpeechRecognition 클래스의 activate() 메서드로 음성 인식 활성화 후 listen() 메서드 호출로 시작할 수 있습니다.
listen() 호출시 별다른 오류발생이 없다면 RecognitionStartedHandler의 VoidCallback 콜백 메서드가 호출 됩니다.

[src/bloc/chat_message_cubit.dart]

...[중간 생략]...

void speechRecognizerStart(ChatMessageViewModel chatMessage) {
  _speechRecognitionchatTargetMessage = chatMessage;
  
  _speech.activate(speechRecognition_locale).then((_) {
    return _speech.listen().then((result) {
      _speechRecognitionchatTargetMessage!.isListening = result;
      activateSpeechRecognizer(_speechRecognitionchatTargetMessage!);
      emit(ChatMessageChangeState(_speechRecognitionchatTargetMessage!));
    });
  });
}

...[중간 생략]...

이제 음성 인식 기능 추가 준비가 되었습니다.
ChatItem 위젯에 음성 인식 버튼을 추가하고 ChatMessageChangeState 상태가 통보되면 음성 인식중 상태인지 체크 후 음성 인식 결과를 입력 메세지 Input TextField 컨트롤러인 _sendTextEditingController에 설정만 해주면 됩니다.
음성 인식 결과는 ChatMessageViewModel ViewModel의 transcription 속성으로 가져 올 수 있습니다.

ChatItem 위젯의 생명주기 initState 부분에서 SpeechRecognizer 초기화 및 활성화를 호출 합니다.

[src/components/chat_item.dart]

class _ChatItemState extends State<ChatItem> {
  ...[중간 생략]...

  @override
  void initState() {
    super.initState();
    // Android, iOS, macOS 플랫폼만 지원
    if (foundation.defaultTargetPlatform == foundation.TargetPlatform.iOS ||
        foundation.defaultTargetPlatform == foundation.TargetPlatform.android ||
        foundation.defaultTargetPlatform == foundation.TargetPlatform.macOS) {
      context.read<ChatMessageCubit>().activateSpeechRecognizer(widget.chatMessageViewModel);
    }
  
  ...[중간 생략]...
}

이렇게 채팅 이력이 추가 되면 SpeechRecognizer 기능이 활성화 되고 초기화 되어 집니다.
그리고 음성 인식 버튼을 눌렀을때 음성 인식 시작 호출 메서드를 다음과 같이 추가 합니다.

[src/components/chat_item.dart]

class _ChatItemState extends State<ChatItem> {
  ...[중간 생략]...

  /// 음성 인식 시작
  void _start() {
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text("Listening to your voice..."),
      duration: Duration(milliseconds: 2000),
    ));

    context
        .read<ChatMessageCubit>()
        .speechRecognizerStart(widget.chatMessageViewModel);
  }
  
  /// 음성 인식 버튼 위젯
  List<Widget> _displaySpeechRecognitionWidget(ChatMessageViewModel chatMessage) {
    // Android, iOS, macOS 플랫폼만 지원
    if (foundation.defaultTargetPlatform == foundation.TargetPlatform.iOS ||
      foundation.defaultTargetPlatform == foundation.TargetPlatform.android ||
      foundation.defaultTargetPlatform == foundation.TargetPlatform.macOS) {
        if (chatMessage.transcription.isNotEmpty) {
          _sendTextEditingController.text = chatMessage.transcription;
        }
        return [
          const SizedBox(width: 15,),

          // 음성 인식 버튼
          Container(
            height: 50,
            padding: const EdgeInsets.all(5),
            decoration: BoxDecoration(
              border: Border.all(
                color: const Color.fromARGB(255, 0, 0, 0), width: 1),
                borderRadius: BorderRadius.circular(5),
              ),
              child: IconButton(
                onPressed: () {
                  if (chatMessage.speechRecognitionAvailable && !chatMessage.isListening) {
                    _start();
                  }
                },
              icon: chatMessage.isListening ? const Icon(Icons.voice_chat) : const Icon(Icons.mic),
              tooltip: 'Voice recognition',
              hoverColor: Colors.transparent,
            )),
        ];
    }
    else {
      return [];
    }
  }
  
  ...[중간 생략]...
}

작성한 음성 인식 버튼 위젯 _displaySpeechRecognitionWidget 메서드는 기존에 전송 버튼 위젯을 감싸고 있는 Row 위젯의 children 부분에 추가 합니다.<br/

[src/components/chat_item.dart]

/// 메세지 입력 TextField 표시
Widget _displayMessageInputWidget(ChatMessageViewModel chatMessage) {
...[중간 생략]...

  // 전송 버튼
  Container(
          height: 50,
          padding: const EdgeInsets.all(5),
          decoration: BoxDecoration(
            border:
                Border.all(color: const Color.fromARGB(255, 0, 0, 0), width: 1),
            borderRadius: BorderRadius.circular(5),
          ),
          child: IconButton(
            onPressed: () => _sendChatMessage(),
            icon: const Icon(Icons.send),
            tooltip: 'Send',
            hoverColor: Colors.transparent,
          )),
  ..._displaySpeechRecognitionWidget(chatMessage),  // 음성 인식 버튼 추가

...[중간 생략]...
}

이렇게 음성 인식 기능 추가가 완성 되었습니다.


[결과 화면]

Flutter_ChatGPT3
KakaoTalk_20230314_145155855

flutter_speech 패키지 사용으로 간단하게 음성 인식 기능을 추가해 보았습니다.


위 코드는 다음 Repository에서 확인할 수 있습니다.

ChatGPT_Flutter