기록

[Flutter] speech_to_text로 음성 인식 구현하기

avocado8 2024. 9. 3. 10:40

 

AI 채팅봇과 대화할 수 있는 채팅 페이지를 만드는데, 타이핑뿐만 아니라 음성으로도 메시지를 입력할 수 있게 해야 한다.

speech_to_text 플러터 패키지를 사용해 구현할 수 있다.

 

https://pub.dev/packages/speech_to_text

 

speech_to_text | Flutter package

A Flutter plugin that exposes device specific speech to text recognition capability.

pub.dev

 

1. 설치

pubspec.yaml에 의존성 추가해주고 pub get

dependencies:
  speech_to_text: ^6.6.2

공식문서상에 따르면 가장 최신 버전은 7.0.0인 듯하나 나는 플러터 sdk 버전 문제로 인해 7버전은 설치할 수 없어서 조금 낮은 버전을 사용했다.

 

코드에서 사용시 아래 import를 추가해주면 사용 가능하다.

import 'package:speech_to_text/speech_to_text.dart';

 

2. 권한 설정, init

StatefulWidget에서 initState는 처음 한 번만 실행된다.

이때 initSpeech라는 함수를 불러 stt를 initialize해주자.

class _ChatScreenState extends State<ChatScreen> {
  final List<Map<String, String>> _messages = [
    {'sender': 'bot', 'text': '안녕! 오늘은 이 그림에 대해 이야기해볼까? 먼저 그림을 천천히 감상해보자!'}
  ]; // 발신자-메시지 저장
  final TextEditingController _controller = TextEditingController();
  final SpeechToText _speech = SpeechToText();
  bool _speechEnabled = false;
  String _ttsText = '';

  @override
  void initState() {
    super.initState();
    _initSpeech();
  }

  void _initSpeech() async {
    await Permission.microphone.status;
    _speechEnabled = await _speech.initialize(
      onStatus: (status) => print('Speech status: $status'),
      onError: (error) => print('Speech error: $error'),
    );
    print('Speech enabled: $_speechEnabled');
    setState(() {});
  }
  
  //...생략

리스트는 채팅 메시지를 담는 부분이라 stt와는 별 상관없다

stt를 사용하려면 먼저 SpeechTotext() 를 통해 SpeechToText 객체를 만들어주어야 한다. 나는 _speech라는 이름으로 만들었다.

 

initSpeech는 공식문서와 챗지피티를 참고했다

우선 마이크 기능을 사용하려면 디바이스에서 권한 허용을 해줘야 한다. 그 권한허용을 기다리기 위해 await을 넣었고, 이후에 initialize를 호출해 초기화해준다.

권한허용 관련 패키지는 퍼미션핸들러를 사용했다. 근데 이건 아직 테스트를 덜 해봐서 추가 확인이 필요할 듯...

https://pub.dev/packages/permission_handler

 

permission_handler | Flutter package

Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.

pub.dev

 

그리고 마이크권한 사용을 위해 프로젝트에서 추가적인 설정이 필요하다.

android/app/src/main/AndroidManifest.xml 에 레코드 오디오 권한을 설정해준다.

내폰이 갤럭시라 iOS는 아직 따로 설정을 안했다..

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>

 

또한 에뮬레이터에서는 음성인식이 똑바로 작동하지 않는 경우가 많아, 실제 디바이스를 연결해 돌렸다.

 

3. stt 시작하기/끝내기 함수

마이크 버튼을 누르면 듣기를 시작하고, 다시 누르면 듣기를 멈춘다.

들을 때는 listen, 멈출 때는 stop 메서드를 사용한다.

listen에서 들은 결과를 가지고 할 일은 onResult에, 리슨할 최대 길이는 listenFor, 리슨하고 기다리는 대기시간은 pauseFor에 지정해준다. 그리고 나는 한국어를 들을 것이기 때문에 localeId를 한국으로 설정해줘야 한국어로 인식한다.

void _startListening() async {
    await _speech.listen(
      onResult: _onSpeechResult,
      listenFor: const Duration(seconds: 30),
      pauseFor: const Duration(seconds: 5),
      localeId: 'ko_KR',
    );
    print('mic clicked');
    setState(() {});
  }

  void _stopListening() async {
    await _speech.stop();
    print('mic stopped');
    setState(() {});
  }

  void _onSpeechResult(SpeechRecognitionResult result) {
    print('speech result');
    print(result);
    setState(() {
      _ttsText = result.recognizedWords;
      _controller.text = _ttsText;
    });
  }

setState를 함수 끝에 호출해주는 이유는, _speech가 듣는 상태냐 아니냐에 따라서 마이크 아이콘의 모양을 바꿔주어야 하기 때문이다. StatefulWidget은 state가 바뀔 때마다 다시 렌더링하므로 이런 과정이 필요한 것 :/

 

리슨한 결과를 채팅 텍스트필드에 세팅해줄 것이므로, onSpeechResult 함수에는 들은 결과를 _ttsText에 저장하고, 텍스트필드 컨트롤러에 설정해준다.

 

이렇게 설정해주면 잘 작동한다.

마지막으로 위젯은 대충 아래처럼 생겼다.

원래는 마이크 모양이나 텍스트필드 placeholder를 isListening이라는 bool변수를 따로 만들어 그를 통해 관리하려 했는데, 이미 speechtotext에 isListening / isNotListening이 내장되어 있어서 굳이 필요없게 되었다.

//...생략
Container(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(
                      hintText:
                          _speech.isListening ? '음성 인식 중입니다...' : '메시지를 입력하세요',
                      border: OutlineInputBorder(
                        borderSide: BorderSide(
                          color: Theme.of(context).colorScheme.onPrimary,
                        ),
                        borderRadius: BorderRadius.circular(10),
                      ),
                      focusedBorder: OutlineInputBorder(
                        borderSide: BorderSide(
                          color: Theme.of(context).colorScheme.secondary,
                        ),
                      ),
                      contentPadding: const EdgeInsets.symmetric(
                        vertical: 10,
                        horizontal: 10,
                      ),
                    ),
                    style: const TextStyle(
                      fontSize: 16,
                    ),
                    cursorColor: Theme.of(context).colorScheme.onPrimary,
                  ),
                ),
                IconButton(
                  onPressed:
                      _speech.isNotListening ? _startListening : _stopListening,
                  icon: Icon(
                    _speech.isListening ? Icons.mic : Icons.mic_none,
                    color: Theme.of(context).colorScheme.secondary,
                    size: 30,
                  ),
                ),
                IconButton(
                  onPressed: _sendMessage,
                  icon: Icon(
                    Icons.send,
                    color: Theme.of(context).colorScheme.secondary,
                    size: 30,
                  ),
                ),
              ],
            ),
          ),
          //...생략

 

 

테스트 몇 번 해봤는데 꽤 잘 듣는다.

이제 AI 메시지에도 tts를... 구현해야 하는데 플러터 패키지에 text_to_speech가 있긴하다. 근데 이걸 쓸지 OpenAI 등에서 제공하는 걸 사용할지는 생각해봐야 할 듯