flutter_kitten_tts 0.0.4 copy "flutter_kitten_tts: ^0.0.4" to clipboard
flutter_kitten_tts: ^0.0.4 copied to clipboard

KittenTTS v0.8 - High-quality offline text-to-speech for Flutter using KittenML's ONNX model.

example/lib/main.dart

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_kitten_tts/flutter_kitten_tts.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(const KittenTTSDemo());
}

class KittenTTSDemo extends StatelessWidget {
  const KittenTTSDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KittenTTS Demo',
      theme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
        brightness: Brightness.dark,
      ),
      home: const TTSScreen(),
    );
  }
}

class TTSScreen extends StatefulWidget {
  const TTSScreen({super.key});

  @override
  State<TTSScreen> createState() => _TTSScreenState();
}

class _TTSScreenState extends State<TTSScreen> {
  final KittenTTS _tts = KittenTTS();
  final TextEditingController _textCtrl = TextEditingController(
    text:
        'Hello! This is KittenTTS, a high-quality offline text-to-speech engine. '
        'It runs entirely on your device, no internet required.',
  );

  String _selectedVoice = 'Jasper';
  double _speed = 1.0;
  double _downloadProgress = 0;
  String _status = 'Not initialized';
  bool _isGenerating = false;
  Float32List? _lastAudio;

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

  Future<void> _initTTS() async {
    try {
      await _tts.initialize(
        onProgress: (progress, status) {
          if (mounted) {
            setState(() {
              _downloadProgress = progress;
              _status = status;
            });
          }
        },
      );
      if (mounted) setState(() => _status = 'Ready');
    } catch (e) {
      if (mounted) setState(() => _status = 'Error: $e');
    }
  }

  Future<void> _generate() async {
    if (!_tts.isInitialized || _isGenerating) return;

    setState(() {
      _isGenerating = true;
      _status = 'Generating...';
    });

    try {
      final audio = await _tts.generate(
        _textCtrl.text,
        voice: _selectedVoice,
        speed: _speed,
      );

      setState(() {
        _lastAudio = audio;
        _status =
            'Generated ${audio.length} samples '
            '(${(audio.length / _tts.sampleRate).toStringAsFixed(1)}s)';
        _isGenerating = false;
      });
    } catch (e) {
      setState(() {
        _status = 'Error: $e';
        _isGenerating = false;
      });
    }
  }

  Future<void> _saveWav() async {
    if (_lastAudio == null) return;

    final dir = await getApplicationDocumentsDirectory();
    final path = '${dir.path}/kitten_tts_output.wav';

    final wavBytes = _encodeWav(_lastAudio!, _tts.sampleRate);
    await File(path).writeAsBytes(wavBytes);

    if (mounted) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Saved to $path')));
    }
  }

  Uint8List _encodeWav(Float32List samples, int sampleRate) {
    final numChannels = 1;
    final bitsPerSample = 16;
    final byteRate = sampleRate * numChannels * bitsPerSample ~/ 8;
    final blockAlign = numChannels * bitsPerSample ~/ 8;

    final pcm16 = Int16List(samples.length);
    for (var i = 0; i < samples.length; i++) {
      final s = (samples[i] * 32767).round().clamp(-32768, 32767);
      pcm16[i] = s;
    }

    final dataSize = pcm16.lengthInBytes;
    final fileSize = 36 + dataSize;
    final buffer = ByteData(44 + dataSize);

    void writeStr(int offset, String s) {
      for (var i = 0; i < s.length; i++) {
        buffer.setUint8(offset + i, s.codeUnitAt(i));
      }
    }

    writeStr(0, 'RIFF');
    buffer.setUint32(4, fileSize, Endian.little);
    writeStr(8, 'WAVE');
    writeStr(12, 'fmt ');
    buffer.setUint32(16, 16, Endian.little);
    buffer.setUint16(20, 1, Endian.little);
    buffer.setUint16(22, numChannels, Endian.little);
    buffer.setUint32(24, sampleRate, Endian.little);
    buffer.setUint32(28, byteRate, Endian.little);
    buffer.setUint16(32, blockAlign, Endian.little);
    buffer.setUint16(34, bitsPerSample, Endian.little);
    writeStr(36, 'data');
    buffer.setUint32(40, dataSize, Endian.little);

    final pcmBytes = pcm16.buffer.asUint8List();
    for (var i = 0; i < pcmBytes.length; i++) {
      buffer.setUint8(44 + i, pcmBytes[i]);
    }

    return buffer.buffer.asUint8List();
  }

  @override
  void dispose() {
    _tts.dispose();
    _textCtrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(title: const Text('KittenTTS Demo'), centerTitle: true),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Status card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Status',
                      style: Theme.of(context).textTheme.titleSmall,
                    ),
                    const SizedBox(height: 8),
                    if (!_tts.isInitialized && _downloadProgress < 1.0) ...[
                      LinearProgressIndicator(value: _downloadProgress),
                      const SizedBox(height: 8),
                    ],
                    Text(
                      _status,
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Text input
            TextField(
              controller: _textCtrl,
              maxLines: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'Text to speak',
                hintText: 'Enter text here...',
              ),
            ),
            const SizedBox(height: 16),

            // Voice selector
            Row(
              children: [
                Expanded(
                  child: DropdownButtonFormField<String>(
                    initialValue: _selectedVoice,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      labelText: 'Voice',
                    ),
                    items: kittenVoices
                        .map((v) => DropdownMenuItem(value: v, child: Text(v)))
                        .toList(),
                    onChanged: (v) {
                      if (v != null) setState(() => _selectedVoice = v);
                    },
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Speed: ${_speed.toStringAsFixed(1)}x'),
                      Slider(
                        value: _speed,
                        min: 0.5,
                        max: 2.0,
                        divisions: 6,
                        onChanged: (v) => setState(() => _speed = v),
                      ),
                    ],
                  ),
                ),
              ],
            ),
            const SizedBox(height: 24),

            // Generate button
            FilledButton.icon(
              onPressed: _tts.isInitialized && !_isGenerating
                  ? _generate
                  : null,
              icon: _isGenerating
                  ? const SizedBox(
                      width: 18,
                      height: 18,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        color: Colors.white,
                      ),
                    )
                  : const Icon(Icons.record_voice_over),
              label: Text(_isGenerating ? 'Generating...' : 'Generate Speech'),
              style: FilledButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
            ),

            if (_lastAudio != null) ...[
              const SizedBox(height: 12),
              OutlinedButton.icon(
                onPressed: _saveWav,
                icon: const Icon(Icons.save),
                label: const Text('Save as WAV'),
              ),
              const SizedBox(height: 8),
              Text(
                '${_lastAudio!.length} samples / '
                '${(_lastAudio!.length / _tts.sampleRate).toStringAsFixed(1)}s / '
                '${_tts.sampleRate} Hz',
                style: Theme.of(
                  context,
                ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
                textAlign: TextAlign.center,
              ),
            ],
          ],
        ),
      ),
    );
  }
}
3
likes
150
points
85
downloads

Publisher

verified publisherpiusikeoffiah.dev

Weekly Downloads

KittenTTS v0.8 - High-quality offline text-to-speech for Flutter using KittenML's ONNX model.

Repository (GitHub)
View/report issues

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

archive, ffi, flutter, flutter_onnxruntime, http, path, path_provider

More

Packages that depend on flutter_kitten_tts

Packages that implement flutter_kitten_tts