[Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (5) - STT, TTS

2024. 8. 8. 14:47·Dev/flutter
728x90

 

이번 시간에 다룰 기능은 STT (Speech To Text)와 TTS (Text To Speech)이다. 사용자가 마이크 버튼을 누르고 말을 한 다음에 다시 버튼을 누르면 텍스트로 변환해 준다. 이어서 Speak을 누르면 화면에 작성된 글이 음성으로 변환되어 출력된다. 이 두 기능은 무료 오픈 소스를 사용하였다.

 

 

pubspec.yaml 파일에 두 라이브러리와 권한 요청을 위한 permission_handler를 추가해 준다. 

 

 

전 예제와 마찬가지로 ios > Runner > Info.plist 에 들어가서 권한 요청을 위해 해당 코드를 작성해 준다. permission handling이 제일 애먹었던 부분인데, 단순히 권한 설정을 Info.plist에 추가하고 permission_handler 라이브러리를 추가하는 것만으로도 시스템이 권한 요청이 필요한 순간에 알아서 팝업을 띄어 준다. (아이 편해) Android OS에서는 좀 더 추가적으로 작성을 해줘야 하는데, 프로젝트를 하이브리드로 전환할 때 필요한 설정들은 추후에 이곳에 적어보도록 하겠다. (안 할 수도)

 

 

에뮬레이터를 실행하면 마이크 버튼을 눌렀을 때 자동으로 필요한 권한을 요청한다. iOS는 앱 실행 시 한 번의 요청만을 보내고, 만약 추후에 다시 변경하고 싶으면 직접 설정 창에 들어가 수정해 주어야 한다.

 

라이브러리 및 기본 설정

import 'package:flutter/cupertino.dart'; // Cupertino 위젯을 사용하기 위해 Flutter Cupertino 패키지를 가져옵니다.
import 'package:speech_to_text/speech_to_text.dart' as stt; // 음성 인식을 위한 speech_to_text 패키지를 stt 별칭으로 가져옵니다.
import 'package:flutter_tts/flutter_tts.dart'; // 텍스트 음성 변환을 위한 flutter_tts 패키지를 가져옵니다.

void main() {
  runApp(const MyApp()); // 앱의 시작점을 정의합니다. MyApp 위젯을 실행합니다.
}

class MyApp extends StatelessWidget { // 앱의 루트 위젯인 MyApp 클래스를 정의합니다.
  const MyApp({super.key}); // 생성자입니다.

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp( // Cupertino 스타일의 앱을 반환합니다.
      title: 'Flutter STT & TTS', // 앱의 제목을 설정합니다.
      theme: CupertinoThemeData( // 앱의 테마를 설정합니다.
        primaryColor: CupertinoColors.activeBlue, // 기본 색상을 설정합니다.
      ),
      home: MyHomePage(title: 'Flutter STT & TTS'), // 기본 화면으로 MyHomePage 위젯을 설정합니다.
    );
  }
}

class MyHomePage extends StatefulWidget { // 상태를 갖는 위젯인 MyHomePage 클래스를 정의합니다.
  const MyHomePage({super.key, required this.title}); // 생성자입니다.
  final String title; // 제목을 저장하는 속성입니다.

  @override
  State<MyHomePage> createState() => _MyHomePageState(); // 상태를 생성합니다.
}

 

이전 예제와 비슷하다. 화면 상태가 바뀌므로 StatefulWidget으로 메인 페이지를 구성하였다.

 

변수 선언

class _MyHomePageState extends State<MyHomePage> { // MyHomePage의 상태 클래스입니다.
  final TextEditingController _controller = TextEditingController(); // TextField 컨트롤러를 추가합니다.

  bool _isListeningLoading = false; // 녹음 로딩 인디케이터 상태를 추가합니다.
  bool _isListening = false; // 녹음 상태를 추가합니다.
  final stt.SpeechToText _speechToText = stt.SpeechToText(); // SpeechToText 객체를 생성합니다.

  final FlutterTts _flutterTts = FlutterTts(); // FlutterTts 객체를 생성합니다.

  /*
  
  UI와 메서드를 이곳에 작성한다.
  
  */
}

 

필요한 변수 설정을 해주자.

 

- TextEditingController() : 이 컨트롤러를 통해 Textfield를 제어할 수 있다.

- _isListeningLoading, _isListening : 지금 음성 인식 중인지 아닌지를 판단하는 녀석들이다. 전체 코드를 살펴보면 이 bool 변수들은 true/false 변환이 항상 같이 일어나기 때문에 하나의 변수로 처리해도 되지만, LoadingIndicator의 구동과 음성 인식 상태를 좀 더 이해하기 쉽게 표현하기 위해 두 개를 모두 사용하였다. 하나로 줄여줘도 정상적으로 동작한다.

- stt.SpeechToText(), FlutterTts() : STT와 TTS 기능을 위해 필요한 라이브러리 속 객체들이다.

 

이어서 _MyHomePageState 클래스 내부에 UI와 인터랙션에 넣을 메서드를 구현하면 된다.

 

STT on

  void _startListening() async { // 녹음을 시작하는 함수입니다.
    bool available = await _speechToText.initialize(); // 음성 인식을 초기화하고 사용 가능한지 확인합니다.
    if (available) {
      setState(() {
        _isListening = true; // 녹음 상태를 true로 설정합니다.
        _isListeningLoading = true; // 로딩 인디케이터를 시작합니다.
      });
      _speechToText.listen(
        onResult: (result) { // 결과 콜백을 설정합니다.
          if (result.finalResult) { // 최종 결과일 때만 처리합니다.
            setState(() {
              _isListening = false; // 녹음 상태를 false로 설정합니다.
              _isListeningLoading = false; // 로딩 인디케이터를 중지합니다.
              _controller.text = result.recognizedWords; // 인식된 단어를 TextField에 설정합니다.
            });
          }
        },
        localeId: 'ko_KR', // 로케일을 한국어로 설정합니다.
      );
    } else {
      setState(() {
        _isListening = false;
        _isListeningLoading = false; // 사용 불가능할 때 로딩 인디케이터를 중지합니다.
      });
    }
  }

 

이 메서드에서 주목해야 할 부분은 _speechToText.listen()이다. result가 반환될 때 콜백으로 결과가 존재한다면 그때 녹음과 인디케이터의 상태를 false로 변경해 주고, controller에 결과를 표시해 준다. 그리고 한국어 인식을 위해 로케일을 한국어로 설정해 두었다.

 

STT off

  void _stopListening() { // 녹음을 중지하는 함수입니다.
    _speechToText.stop(); // 녹음을 중지합니다.
    setState(() {
      _isListening = false; // 녹음 상태를 false로 설정합니다.
      _isListeningLoading = false; // 로딩 인디케이터를 중지합니다.
    });
  }

 

다음으로 녹음을 중지하는 메서드이다. stop() 함수를 실행하고 상태를 변경해 준다.

 

TTS

Future<void> _speak() async { // 텍스트를 음성으로 변환하는 함수입니다.
  await _flutterTts.speak(_controller.text); // TextField의 텍스트를 음성으로 변환하여 말합니다.
}

 

마지막으로 TTS 메서드이다. 라이브러리 내장 함수를 그대로 사용하여 _controller에 표시되어 있는 text를 읽어준다.

 

UI Layout

@override
Widget build(BuildContext context) {
  return CupertinoPageScaffold( // 페이지의 기본 구조를 정의합니다.
    navigationBar: CupertinoNavigationBar( // 상단 네비게이션 바를 설정합니다.
      middle: Text(widget.title), // 네비게이션 바의 중앙에 제목을 설정합니다.
    ),
    child: SafeArea(
      child: Column(
        children: [
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: CupertinoTextField( // 텍스트 입력 필드를 설정합니다.
                controller: _controller,
                placeholder: '여기에 메시지를 입력하거나 말하세요.', // 플레이스홀더를 설정합니다.
                maxLines: null, // 여러 줄 입력을 허용합니다.
              ),
            ),
          ),
          if (_isListeningLoading) // 녹음 로딩 상태일 때만 표시됩니다.
            const Padding(
              padding: EdgeInsets.all(8.0),
              child: CupertinoActivityIndicator(), // 로딩 인디케이터를 표시합니다.
            ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: [
                CupertinoButton(
                  onPressed: _isListening ? _stopListening : _startListening, // 녹음 상태에 따라 버튼 동작을 설정합니다.
                  child: Icon(_isListening ? CupertinoIcons.mic_off : CupertinoIcons.mic), // 녹음 상태에 따라 아이콘을 변경합니다.
                ),
                const SizedBox(width: 8), // 간격을 추가합니다.
                CupertinoButton(
                  onPressed: _speak, // 텍스트를 말하는 동작을 설정합니다.
                  child: const Text('Speak'), // 버튼의 텍스트를 설정합니다.
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

 

마지막으로 UI 레이아웃 구성이다. NavigationBar 아래에 SafeArea, 그 안에 Column을 두고 STT, TTS 버튼을 아래에 먼저 배치하고 나머지 Textfield를 Expanded 위젯으로 꽉 차게 배치하였다.

 

전체 코드 보기

더보기
import 'package:flutter/cupertino.dart'; // Cupertino 위젯을 사용하기 위해 Flutter Cupertino 패키지를 가져옵니다.
import 'package:speech_to_text/speech_to_text.dart' as stt; // 음성 인식을 위한 speech_to_text 패키지를 stt 별칭으로 가져옵니다.
import 'package:flutter_tts/flutter_tts.dart'; // 텍스트 음성 변환을 위한 flutter_tts 패키지를 가져옵니다.

void main() {
  runApp(const MyApp()); // 앱의 시작점을 정의합니다. MyApp 위젯을 실행합니다.
}

class MyApp extends StatelessWidget { // 앱의 루트 위젯인 MyApp 클래스를 정의합니다.
  const MyApp({super.key}); // 생성자입니다.

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp( // Cupertino 스타일의 앱을 반환합니다.
      title: 'Flutter STT & TTS', // 앱의 제목을 설정합니다.
      theme: CupertinoThemeData( // 앱의 테마를 설정합니다.
        primaryColor: CupertinoColors.activeBlue, // 기본 색상을 설정합니다.
      ),
      home: MyHomePage(title: 'Flutter STT & TTS'), // 기본 화면으로 MyHomePage 위젯을 설정합니다.
    );
  }
}

class MyHomePage extends StatefulWidget { // 상태를 갖는 위젯인 MyHomePage 클래스를 정의합니다.
  const MyHomePage({super.key, required this.title}); // 생성자입니다.
  final String title; // 제목을 저장하는 속성입니다.

  @override
  State<MyHomePage> createState() => _MyHomePageState(); // 상태를 생성합니다.
}

class _MyHomePageState extends State<MyHomePage> { // MyHomePage의 상태 클래스입니다.
  final TextEditingController _controller = TextEditingController(); // TextField 컨트롤러를 추가합니다.

  bool _isListeningLoading = false; // 녹음 로딩 인디케이터 상태를 추가합니다.
  bool _isListening = false; // 녹음 상태를 추가합니다.
  final stt.SpeechToText _speechToText = stt.SpeechToText(); // SpeechToText 객체를 생성합니다.

  final FlutterTts _flutterTts = FlutterTts(); // FlutterTts 객체를 생성합니다.

  void _startListening() async { // 녹음을 시작하는 함수입니다.
    bool available = await _speechToText.initialize(); // 음성 인식을 초기화하고 사용 가능한지 확인합니다.
    if (available) {
      setState(() {
        _isListening = true; // 녹음 상태를 true로 설정합니다.
        _isListeningLoading = true; // 로딩 인디케이터를 시작합니다.
      });
      _speechToText.listen(
        onResult: (result) { // 결과 콜백을 설정합니다.
          if (result.finalResult) { // 최종 결과일 때만 처리합니다.
            setState(() {
              _isListening = false; // 녹음 상태를 false로 설정합니다.
              _isListeningLoading = false; // 로딩 인디케이터를 중지합니다.
              _controller.text = result.recognizedWords; // 인식된 단어를 TextField에 설정합니다.
            });
          }
        },
        localeId: 'ko_KR', // 로케일을 한국어로 설정합니다.
      );
    } else {
      setState(() {
        _isListening = false;
        _isListeningLoading = false; // 사용 불가능할 때 로딩 인디케이터를 중지합니다.
      });
    }
  }

  void _stopListening() { // 녹음을 중지하는 함수입니다.
    _speechToText.stop(); // 녹음을 중지합니다.
    setState(() {
      _isListening = false; // 녹음 상태를 false로 설정합니다.
      _isListeningLoading = false; // 로딩 인디케이터를 중지합니다.
    });
  }

  Future<void> _speak() async { // 텍스트를 음성으로 변환하는 함수입니다.
    await _flutterTts.speak(_controller.text); // TextField의 텍스트를 음성으로 변환하여 말합니다.
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold( // 페이지의 기본 구조를 정의합니다.
      navigationBar: CupertinoNavigationBar( // 상단 네비게이션 바를 설정합니다.
        middle: Text(widget.title), // 네비게이션 바의 중앙에 제목을 설정합니다.
      ),
      child: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: CupertinoTextField( // 텍스트 입력 필드를 설정합니다.
                  controller: _controller,
                  placeholder: '여기에 메시지를 입력하거나 말하세요.', // 플레이스홀더를 설정합니다.
                  maxLines: null, // 여러 줄 입력을 허용합니다.
                ),
              ),
            ),
            if (_isListeningLoading) // 녹음 로딩 상태일 때만 표시됩니다.
              const Padding(
                padding: EdgeInsets.all(8.0),
                child: CupertinoActivityIndicator(), // 로딩 인디케이터를 표시합니다.
              ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Row(
                children: [
                  CupertinoButton(
                    onPressed: _isListening ? _stopListening : _startListening, // 녹음 상태에 따라 버튼 동작을 설정합니다.
                    child: Icon(_isListening ? CupertinoIcons.mic_off : CupertinoIcons.mic), // 녹음 상태에 따라 아이콘을 변경합니다.
                  ),
                  const SizedBox(width: 8), // 간격을 추가합니다.
                  CupertinoButton(
                    onPressed: _speak, // 텍스트를 말하는 동작을 설정합니다.
                    child: const Text('Speak'), // 버튼의 텍스트를 설정합니다.
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
728x90
저작자표시 비영리 변경금지 (새창열림)

'Dev > flutter' 카테고리의 다른 글

[Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (7) - 화면 전환  (2) 2024.08.09
[Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (6) - PageView  (2) 2024.08.09
[Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (4) - 서버에 이미지 업로드  (2) 2024.08.06
[Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (3) - Camera, Gallery  (2) 2024.08.05
[Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (2) - Image  (0) 2024.08.05
'Dev/flutter' 카테고리의 다른 글
  • [Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (7) - 화면 전환
  • [Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (6) - PageView
  • [Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (4) - 서버에 이미지 업로드
  • [Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (3) - Camera, Gallery
100두산
100두산
출발하게 만드는 힘이 동기라면, 계속 나아가게 만드는 힘은 습관이다.
  • 100두산
    정상에서 보자 ✈️
    100두산
  • 전체
    오늘
    어제
    • 분류 전체보기 (126)
      • Life (6)
        • living (1)
      • Research (6)
      • AI (20)
      • Dev (45)
        • iOS (28)
        • Web (4)
        • flutter (9)
        • etc (4)
      • PS (Problem Solving) (23)
      • Computer Science and Engine.. (21)
        • Data Structures and Algorit.. (13)
        • OOP (Object Oriented Progra.. (8)
      • etc (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    xcode
    Challenger
    자료구조
    오블완
    c++
    TIP
    티스토리챌린지
    SKT
    swift
    AI
    PS
    파이썬
    ios
    백트래킹
    D3
    SKTelecom
    BOJ
    백준
    Python
    알고리즘
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
100두산
[Dev, flutter] 플러터로 iOS 앱 개발 시작하기 (5) - STT, TTS
상단으로

티스토리툴바