이번 시간에 다룰 기능은 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'), // 버튼의 텍스트를 설정합니다.
),
],
),
),
],
),
),
);
}
}
'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 |