groq_whisper_stt 0.1.2 copy "groq_whisper_stt: ^0.1.2" to clipboard
groq_whisper_stt: ^0.1.2 copied to clipboard

Real-time speech-to-text for Flutter using Groq's Whisper API. Supports Android and iOS with voice activity detection and streaming transcription results.

example/lib/main.dart

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:groq_whisper_stt/groq_whisper_stt.dart';

void main() => runApp(const SttDemoApp());

// -- Design Tokens --
class _Colors {
  static const primary = Color(0xFF2563EB);
  static const recording = Color(0xFFEF4444);
  static const processing = Color(0xFFF97316);
  static const surface = Color(0xFFF8FAFC);
  static const surfaceElevated = Color(0xFFFFFFFF);
  static const textPrimary = Color(0xFF1E293B);
  static const textSecondary = Color(0xFF64748B);
  static const textPlaceholder = Color(0xFF94A3B8);
  static const border = Color(0xFFE2E8F0);
  static const errorBg = Color(0xFFFEF2F2);
  static const errorBorder = Color(0xFFFECACA);
  static const errorText = Color(0xFF991B1B);
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Groq Whisper STT',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: _Colors.primary,
        useMaterial3: true,
        scaffoldBackgroundColor: _Colors.surface,
      ),
      home: const SttDemoScreen(),
    );
  }
}

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

  @override
  State<SttDemoScreen> createState() => _SttDemoScreenState();
}

class _SttDemoScreenState extends State<SttDemoScreen>
    with TickerProviderStateMixin {
  late final GroqWhisperStt _stt;
  String _transcript = '';
  SttState _state = SttState.idle;
  bool _isInitialized = false;
  String? _error;

  late final AnimationController _pulseController;
  late final AnimationController _waveController;
  late final Animation<double> _pulseAnimation;

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

    _pulseController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1500),
    );
    _pulseAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
      CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
    );

    _waveController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 2000),
    );

    _stt = GroqWhisperStt(
      apiKey: const String.fromEnvironment('GROQ_API_KEY'),
      config: const SttConfig(
        model: WhisperModel.largev3Turbo,
        language: 'en',
        chunkDuration: Duration(seconds: 3),
      ),
    );
    _init();
  }

  Future<void> _init() async {
    await _stt.initialize();
    setState(() => _isInitialized = true);

    _stt.transcriptionStream.listen((result) {
      setState(() {
        _transcript = result.sessionText;
        _error = null;
      });
    });

    _stt.stateStream.listen((state) {
      setState(() => _state = state);
      _updateAnimations(state);
    });

    _stt.errorStream.listen((error) {
      setState(() => _error = error.message);
    });
  }

  void _updateAnimations(SttState state) {
    if (state == SttState.recording) {
      _pulseController.repeat(reverse: true);
      _waveController.repeat();
    } else if (state == SttState.processing) {
      _pulseController.stop();
      _waveController.repeat();
    } else {
      _pulseController.stop();
      _pulseController.reset();
      _waveController.stop();
      _waveController.reset();
    }
  }

  @override
  void dispose() {
    _pulseController.dispose();
    _waveController.dispose();
    _stt.dispose();
    super.dispose();
  }

  bool get _isActive =>
      _state == SttState.listening ||
      _state == SttState.recording ||
      _state == SttState.processing;

  @override
  Widget build(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: SystemUiOverlayStyle.dark,
      child: Scaffold(
        body: SafeArea(
          child: Column(
            children: [
              _buildHeader(),
              if (_error != null) _buildErrorBanner(),
              Expanded(child: _buildTranscriptArea()),
              _buildBottomControls(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildHeader() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
      child: Row(
        children: [
          const Icon(Icons.graphic_eq_rounded,
              color: _Colors.primary, size: 28),
          const SizedBox(width: 10),
          const Expanded(
            child: Text(
              'Groq Whisper',
              style: TextStyle(
                fontSize: 22,
                fontWeight: FontWeight.w700,
                color: _Colors.textPrimary,
                letterSpacing: -0.5,
              ),
            ),
          ),
          _buildStateBadge(),
        ],
      ),
    );
  }

  Widget _buildStateBadge() {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 250),
      curve: Curves.easeOut,
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: _stateColor.withValues(alpha: 0.1),
        borderRadius: BorderRadius.circular(20),
        border: Border.all(
          color: _stateColor.withValues(alpha: 0.3),
          width: 1,
        ),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (_state == SttState.recording || _state == SttState.processing)
            Container(
              width: 8,
              height: 8,
              margin: const EdgeInsets.only(right: 6),
              decoration: BoxDecoration(
                color: _stateColor,
                shape: BoxShape.circle,
              ),
            ),
          Text(
            _stateLabel,
            style: TextStyle(
              fontSize: 13,
              fontWeight: FontWeight.w600,
              color: _stateColor,
              letterSpacing: 0.3,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildErrorBanner() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(24, 12, 24, 0),
      child: Container(
        width: double.infinity,
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: _Colors.errorBg,
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: _Colors.errorBorder),
        ),
        child: Row(
          children: [
            const Icon(Icons.error_outline_rounded,
                color: _Colors.errorText, size: 20),
            const SizedBox(width: 8),
            Expanded(
              child: Text(
                _error!,
                style: const TextStyle(
                    color: _Colors.errorText, fontSize: 14, height: 1.4),
              ),
            ),
            GestureDetector(
              onTap: () => setState(() => _error = null),
              child: const Icon(Icons.close_rounded,
                  color: _Colors.errorText, size: 18),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildTranscriptArea() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
      child: Container(
        width: double.infinity,
        decoration: BoxDecoration(
          color: _Colors.surfaceElevated,
          borderRadius: BorderRadius.circular(16),
          border: Border.all(
            color: _isActive
                ? _Colors.primary.withValues(alpha: 0.3)
                : _Colors.border,
          ),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withValues(alpha: 0.04),
              blurRadius: 12,
              offset: const Offset(0, 2),
            ),
          ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Transcript header
            Padding(
              padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
              child: Row(
                children: [
                  Text(
                    _isActive ? 'Listening...' : 'Transcript',
                    style: TextStyle(
                      fontSize: 13,
                      fontWeight: FontWeight.w600,
                      color: _isActive
                          ? _Colors.primary
                          : _Colors.textSecondary,
                      letterSpacing: 0.3,
                    ),
                  ),
                  const Spacer(),
                  if (_transcript.isNotEmpty)
                    GestureDetector(
                      onTap: () {
                        Clipboard.setData(ClipboardData(text: _transcript));
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: const Text('Copied to clipboard'),
                            behavior: SnackBarBehavior.floating,
                            shape: RoundedRectangleBorder(
                                borderRadius: BorderRadius.circular(10)),
                            duration: const Duration(seconds: 2),
                          ),
                        );
                      },
                      child: const Icon(Icons.copy_rounded,
                          size: 18, color: _Colors.textSecondary),
                    ),
                ],
              ),
            ),
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 20),
              child: Divider(height: 24, color: _Colors.border),
            ),
            // Transcript body
            Expanded(
              child: SingleChildScrollView(
                padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
                child: _transcript.isEmpty
                    ? _buildEmptyState()
                    : Text(
                        _transcript,
                        style: const TextStyle(
                          fontSize: 17,
                          height: 1.65,
                          color: _Colors.textPrimary,
                          letterSpacing: -0.2,
                        ),
                      ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildEmptyState() {
    return Padding(
      padding: const EdgeInsets.only(top: 48),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              Icons.mic_none_rounded,
              size: 48,
              color: _Colors.textPlaceholder.withValues(alpha: 0.5),
            ),
            const SizedBox(height: 12),
            const Text(
              'Tap the microphone to start',
              style: TextStyle(
                fontSize: 16,
                color: _Colors.textPlaceholder,
                fontWeight: FontWeight.w500,
              ),
            ),
            const SizedBox(height: 4),
            const Text(
              'Your transcription will appear here',
              style: TextStyle(
                fontSize: 14,
                color: _Colors.textPlaceholder,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBottomControls() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(24, 20, 24, 24),
      child: Column(
        children: [
          // Waveform indicator
          AnimatedBuilder(
            animation: _waveController,
            builder: (context, child) {
              return SizedBox(
                height: 32,
                child: _isActive
                    ? _buildWaveform()
                    : const SizedBox.shrink(),
              );
            },
          ),
          const SizedBox(height: 16),
          // Mic button
          _buildMicButton(),
        ],
      ),
    );
  }

  Widget _buildWaveform() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: List.generate(24, (index) {
        final phase = _waveController.value * 2 * pi + index * 0.4;
        final height = _state == SttState.recording
            ? 8.0 + 16.0 * ((sin(phase) + 1) / 2)
            : 4.0 + 6.0 * ((sin(phase) + 1) / 2);
        return AnimatedContainer(
          duration: const Duration(milliseconds: 100),
          width: 3,
          height: height,
          margin: const EdgeInsets.symmetric(horizontal: 1.5),
          decoration: BoxDecoration(
            color: _state == SttState.recording
                ? _Colors.recording.withValues(alpha: 0.7)
                : _Colors.processing.withValues(alpha: 0.6),
            borderRadius: BorderRadius.circular(2),
          ),
        );
      }),
    );
  }

  Widget _buildMicButton() {
    final isIdle = _state == SttState.idle;
    final buttonColor = isIdle ? _Colors.primary : _Colors.recording;

    return GestureDetector(
      onTap: _isInitialized ? _toggleRecording : null,
      child: AnimatedBuilder(
        animation: _pulseAnimation,
        builder: (context, child) {
          final scale =
              _state == SttState.recording ? _pulseAnimation.value : 1.0;
          return Transform.scale(
            scale: scale,
            child: Container(
              width: 72,
              height: 72,
              decoration: BoxDecoration(
                color: buttonColor,
                shape: BoxShape.circle,
                boxShadow: [
                  BoxShadow(
                    color: buttonColor.withValues(alpha: 0.3),
                    blurRadius: _state == SttState.recording ? 24 : 12,
                    spreadRadius: _state == SttState.recording ? 4 : 0,
                  ),
                ],
              ),
              child: AnimatedSwitcher(
                duration: const Duration(milliseconds: 200),
                child: Icon(
                  isIdle ? Icons.mic_rounded : Icons.stop_rounded,
                  key: ValueKey(isIdle),
                  size: 32,
                  color: Colors.white,
                ),
              ),
            ),
          );
        },
      ),
    );
  }

  Future<void> _toggleRecording() async {
    HapticFeedback.mediumImpact();
    if (_state == SttState.idle) {
      await _stt.start();
    } else {
      await _stt.stop();
    }
  }

  String get _stateLabel => switch (_state) {
        SttState.idle => 'Ready',
        SttState.initializing => 'Starting',
        SttState.listening => 'Listening',
        SttState.recording => 'Recording',
        SttState.processing => 'Processing',
        SttState.paused => 'Paused',
        SttState.error => 'Error',
      };

  Color get _stateColor => switch (_state) {
        SttState.idle => _Colors.textSecondary,
        SttState.initializing => _Colors.primary,
        SttState.listening => _Colors.primary,
        SttState.recording => _Colors.recording,
        SttState.processing => _Colors.processing,
        SttState.paused => const Color(0xFFD97706),
        SttState.error => _Colors.recording,
      };
}
2
likes
160
points
121
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Real-time speech-to-text for Flutter using Groq's Whisper API. Supports Android and iOS with voice activity detection and streaming transcription results.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, http, http_parser

More

Packages that depend on groq_whisper_stt

Packages that implement groq_whisper_stt