AivisSpeech-engineをVPSで立ち上げてみた(3)
AivisSpeech-engineが無事に立ち上がったので、音声合成のデモアプリを作ってみました。
いつものようにFlutterで作成しています。こちらで作ったFlaskアプリに接続するようにします。
Flutter Web サンプルコード
以下のコードは、入力したテキストを発話せさるサンプルプログラムです。
エラー処理も、分割処理もしていないので、あまり長い文章を送るとおかしなことになるかもしれません。

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:html' as html;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Text to Speech',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const TextToSpeechPage(),
);
}
}
class TextToSpeechPage extends StatefulWidget {
const TextToSpeechPage({super.key});
@override
State<TextToSpeechPage> createState() => _TextToSpeechPageState();
}
class _TextToSpeechPageState extends State<TextToSpeechPage> {
final TextEditingController _textController = TextEditingController();
final TextEditingController _speakerController = TextEditingController();
bool _isLoading = false;
html.AudioElement? _audioElement;
Future<void> _generateAndPlay() async {
if (_textController.text.isEmpty) return;
setState(() {
_isLoading = true;
});
try {
print('Sending request with http...');
var uri = Uri.parse('http://<Aivis-Server>/generate');
var request = http.MultipartRequest('POST', uri);
request.fields['text'] = _textController.text;
if (_speakerController.text.isNotEmpty) {
request.fields['speaker_id'] = _speakerController.text;
}
var response = await request.send();
if (response.statusCode == 200) {
var bytes = await response.stream.toBytes();
print('Response received, length: ${bytes.length}');
final base64 = Uri.dataFromBytes(
Uint8List.fromList(bytes),
mimeType: 'audio/wav',
).toString();
_audioElement?.remove();
_audioElement = html.AudioElement()
..src = base64
..autoplay = true;
html.document.body?.append(_audioElement!);
print('Audio element appended');
} else {
print('Error: HTTP ${response.statusCode}');
}
} catch (e, stackTrace) {
print('Error: $e');
print('Stack trace: $stackTrace');
_showError('エラーが発生しました: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}
void _showError(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
void dispose() {
_textController.dispose();
_speakerController.dispose();
_audioElement?.remove();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Aivis-Speech Sample'),
),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _textController,
decoration: const InputDecoration(
labelText: '発話するテキスト',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
TextField(
controller: _speakerController,
decoration: const InputDecoration(
labelText: '話者ID(任意)',
border: OutlineInputBorder(),
hintText: 'デフォルト: 888753760',
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _generateAndPlay,
style: ElevatedButton.styleFrom(
minimumSize: const Size(200, 50),
),
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('発話'),
),
],
),
),
),
),
);
}
}
その後・・
ここまでできたので、「早速、チャットアプリを、、」と思って作ってみたのですが、合成に時間がかかりすぎて、あまり良い結果になりませんんでした。
使えないことはないのですが、ちょっともたつく感じです。
句読点や、感嘆符などで文章を区切りながら音声合成処理をしてみましたが、まだまだ改善の余地がありそうです。