AivisSpeech-engineをVPSで立ち上げてみた(3)

AivisSpeech-engineが無事に立ち上がったので、音声合成のデモアプリを作ってみました。

いつものようにFlutterで作成しています。こちらで作ったFlaskアプリに接続するようにします。

AivisSpeech-engineをVPSで立ち上げてみた(2)

以下の記事で、AivisSpeech-engineの立ち上げまでを実行しました。 サンプルコードでもわかるように、音声合成のためには、2回のAPIリクエストが必要になります。少々使…

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('発話'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

その後・・

ここまでできたので、「早速、チャットアプリを、、」と思って作ってみたのですが、合成に時間がかかりすぎて、あまり良い結果になりませんんでした。

使えないことはないのですが、ちょっともたつく感じです。

句読点や、感嘆符などで文章を区切りながら音声合成処理をしてみましたが、まだまだ改善の余地がありそうです。