sate_ai 0.7.1 copy "sate_ai: ^0.7.1" to clipboard
sate_ai: ^0.7.1 copied to clipboard

A fault injection framework for testing on-device AI models in Flutter. Simulate memory pressure, malformed inputs, and degradation to catch failures before deployment.

example/lib/main.dart

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

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

enum AdapterType { mock, onnx, tflite }

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SATE AI Stress Test Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6C63FF),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const StressDashboard(),
    );
  }
}

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

  @override
  State<StressDashboard> createState() => _StressDashboardState();
}

class _StressDashboardState extends State<StressDashboard>
    with TickerProviderStateMixin {
  AdapterType _selectedAdapter = AdapterType.mock;
  StressReport? _report;
  bool _running = false;
  String _status = 'Ready';
  final List<String> _log = [];
  late AnimationController _pulseCtrl;

  // Selected injectors
  bool _useMemoryPressure = true;
  bool _useMalformedInput = true;
  bool _useThermalThrottle = true;
  bool _useQuantizationDrift = true;
  bool _useConfidenceValidation = true;

  @override
  void initState() {
    super.initState();
    _pulseCtrl = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 900),
    )..repeat(reverse: true);
  }

  @override
  void dispose() {
    _pulseCtrl.dispose();
    super.dispose();
  }

  Future<AIModelAdapter> _createAdapter() async {
    switch (_selectedAdapter) {
      case AdapterType.mock:
        return MockAdapter(
          modelId: 'mock-llm-v1',
          inferenceDelay: const Duration(milliseconds: 150),
        );

      case AdapterType.onnx:
        _appendLog('๐Ÿ“ฆ Initializing OnnxAdapter (assets/models/mobilenet.onnx)...');
        try {
          final bytes = await rootBundle.load('assets/models/mobilenet.onnx');
          return OnnxAdapter(
            modelId: 'onnx-mobilenet-v2',
            modelBytes: bytes.buffer.asUint8List(),
          );
        } catch (e) {
          _appendLog('โš ๏ธ ONNX init fallback to Mock: $e');
          return MockAdapter(modelId: 'onnx-fallback-mock');
        }

      case AdapterType.tflite:
        _appendLog('๐Ÿ“ฆ Initializing TFLiteAdapter (assets/models/mobilenet.tflite)...');
        try {
          return await TFLiteAdapter.fromAsset(
            'assets/models/mobilenet.tflite',
            modelId: 'tflite-mobilenet-v1',
          );
        } catch (e) {
          _appendLog('โš ๏ธ TFLite init fallback to Mock: $e');
          return MockAdapter(modelId: 'tflite-fallback-mock');
        }
    }
  }

  Future<void> _runStressTest() async {
    setState(() {
      _running = true;
      _report = null;
      _log.clear();
      _status = 'Initialising selected model adapter...';
    });

    _appendLog('๐Ÿค– Selected Adapter: ${_selectedAdapter.name.toUpperCase()}');
    final model = await _createAdapter();
    _appendLog('โœ… Model initialized: ${model.modelId}');

    final List<FaultInjector> injectors = [];

    if (_useMemoryPressure) {
      injectors.add(MemoryPressureInjector(model: model, limitMb: 120));
      _appendLog('โš™๏ธ Configured MemoryPressureInjector (120 MB)');
    }
    if (_useMalformedInput) {
      injectors.add(const MalformedInputInjector());
      _appendLog('โš™๏ธ Configured MalformedInputInjector');
    }
    if (_useThermalThrottle) {
      injectors.add(
        ThermalThrottleInjector(
          model: model,
          temperatureStep: 15,
          maxTemperature: 80,
        ),
      );
      _appendLog('โš™๏ธ Configured ThermalThrottleInjector (Max 80ยฐC)');
    }
    if (_useQuantizationDrift) {
      injectors.add(
        QuantizationDriftInjector(
          model: model,
          driftFactor: 0.1,
          degradationThreshold: 0.3,
        ),
      );
      _appendLog('โš™๏ธ Configured QuantizationDriftInjector');
    }
    if (_useConfidenceValidation) {
      injectors.add(ConfidenceThresholdInjector(model: model, threshold: 0.6));
      _appendLog('โš™๏ธ Configured ConfidenceThresholdInjector (0.6)');
    }

    if (injectors.isEmpty) {
      _appendLog('โš ๏ธ No injectors selected. Adding default MalformedInputInjector.');
      injectors.add(const MalformedInputInjector());
    }

    setState(() => _status = 'Running SateAI.stress() runner...');
    _appendLog('๐Ÿงช Starting stress runner evaluation (${injectors.length} injectors)...');

    final report = await SateAI.stress(
      model: model,
      injectors: injectors,
      timeout: const Duration(seconds: 30),
    );

    for (final result in report.results) {
      final icon = result.passed ? 'โœ…' : 'โŒ';
      final timeMs = result.inferenceTime?.inMilliseconds ?? 0;
      _appendLog(
        '$icon ${result.injectorType.displayName}: ${timeMs}ms '
        '${result.memoryUsageMB != null ? "(${result.memoryUsageMB}MB)" : ""}',
      );
    }

    setState(() {
      _running = false;
      _report = report;
      _status = report.passed
          ? 'โœ… Stress Suite Passed (${report.passCount}/${report.totalTests})'
          : 'โŒ Failures Detected (${report.failureCount} failed)';
    });
  }

  void _appendLog(String line) => setState(() => _log.add(line));

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

    return Scaffold(
      backgroundColor: const Color(0xFF0F0F1A),
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        title: Row(
          children: [
            Container(
              padding: const EdgeInsets.all(6),
              decoration: BoxDecoration(
                gradient: const LinearGradient(
                  colors: [Color(0xFF6C63FF), Color(0xFF48CAE4)],
                ),
                borderRadius: BorderRadius.circular(8),
              ),
              child: const Icon(Icons.science, color: Colors.white, size: 18),
            ),
            const SizedBox(width: 10),
            const Text(
              'SATE AI Stress Test Demo',
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.w700,
                fontSize: 16,
              ),
            ),
          ],
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Row 1: Compact Horizontal Adapter Selector
            _buildAdapterSelectorRow(),
            const SizedBox(height: 8),
            
            // Row 2: Horizontal Sliding Injectors Row (Left-to-Right Scrollable)
            _buildInjectorScrollRow(),
            const SizedBox(height: 8),

            // Compact Status Banner + Run Button Row
            Row(
              children: [
                Expanded(child: _buildStatusCard(cs)),
                const SizedBox(width: 8),
                _buildRunButton(cs),
              ],
            ),
            const SizedBox(height: 8),

            if (_report != null) ...[
              _buildSummaryCard(_report!),
              const SizedBox(height: 8),
            ],

            // Maximized Execution Log Screen (Fills remaining height)
            Expanded(child: _buildLogCard()),
          ],
        ),
      ),
    );
  }

  // Row layout for Select Model Adapter
  Widget _buildAdapterSelectorRow() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
      decoration: BoxDecoration(
        color: const Color(0xFF1A1A2E),
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: Colors.white12),
      ),
      child: Row(
        children: [
          const Text(
            'ADAPTER:',
            style: TextStyle(
              color: Colors.white70,
              fontSize: 10,
              fontWeight: FontWeight.w700,
              letterSpacing: 0.8,
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: Row(
              children: AdapterType.values.map((type) {
                final isSelected = _selectedAdapter == type;
                return Expanded(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 3),
                    child: ChoiceChip(
                      padding: EdgeInsets.zero,
                      labelPadding: const EdgeInsets.symmetric(horizontal: 4),
                      visualDensity: VisualDensity.compact,
                      label: Center(
                        child: Text(
                          type.name.toUpperCase(),
                          style: TextStyle(
                            color: isSelected ? Colors.white : Colors.white60,
                            fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                            fontSize: 11,
                          ),
                        ),
                      ),
                      selected: isSelected,
                      selectedColor: const Color(0xFF6C63FF),
                      backgroundColor: const Color(0xFF0F0F1A),
                      onSelected: (val) {
                        if (val) setState(() => _selectedAdapter = type);
                      },
                    ),
                  ),
                );
              }).toList(),
            ),
          ),
        ],
      ),
    );
  }

  // Horizontal Left-to-Right Sliding Injectors Row
  Widget _buildInjectorScrollRow() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
      decoration: BoxDecoration(
        color: const Color(0xFF1A1A2E),
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: Colors.white12),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'INJECTORS (SWIPE LEFT/RIGHT):',
            style: TextStyle(
              color: Colors.white70,
              fontSize: 10,
              fontWeight: FontWeight.w700,
              letterSpacing: 0.8,
            ),
          ),
          const SizedBox(height: 4),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            physics: const BouncingScrollPhysics(),
            child: Row(
              children: [
                _buildInjectorChip('Memory Pressure', _useMemoryPressure, (v) => setState(() => _useMemoryPressure = v)),
                const SizedBox(width: 6),
                _buildInjectorChip('Malformed Input', _useMalformedInput, (v) => setState(() => _useMalformedInput = v)),
                const SizedBox(width: 6),
                _buildInjectorChip('Thermal Throttle', _useThermalThrottle, (v) => setState(() => _useThermalThrottle = v)),
                const SizedBox(width: 6),
                _buildInjectorChip('Quantization Drift', _useQuantizationDrift, (v) => setState(() => _useQuantizationDrift = v)),
                const SizedBox(width: 6),
                _buildInjectorChip('Confidence Validator', _useConfidenceValidation, (v) => setState(() => _useConfidenceValidation = v)),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildInjectorChip(String label, bool selected, ValueChanged<bool> onSelected) {
    return FilterChip(
      visualDensity: VisualDensity.compact,
      labelPadding: const EdgeInsets.symmetric(horizontal: 4),
      label: Text(label, style: const TextStyle(fontSize: 11)),
      selected: selected,
      selectedColor: const Color(0xFF48CAE4).withAlpha(180),
      checkmarkColor: Colors.white,
      onSelected: onSelected,
    );
  }

  Widget _buildStatusCard(ColorScheme cs) {
    final color = _running
        ? const Color(0xFF48CAE4)
        : (_report?.passed ?? true)
            ? const Color(0xFF06D6A0)
            : const Color(0xFFEF476F);

    return AnimatedBuilder(
      animation: _pulseCtrl,
      builder: (context, child) {
        return Container(
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
          decoration: BoxDecoration(
            color: const Color(0xFF1A1A2E),
            borderRadius: BorderRadius.circular(10),
            border: Border.all(
              color: _running
                  ? color.withAlpha(((_pulseCtrl.value * 180 + 75).round()))
                  : color.withAlpha(80),
              width: 1.5,
            ),
          ),
          child: Row(
            children: [
              _running
                  ? SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        color: color,
                      ),
                    )
                  : Icon(
                      _report == null
                          ? Icons.pending_outlined
                          : (_report!.passed ? Icons.check_circle : Icons.error),
                      color: color,
                      size: 18,
                    ),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  _status,
                  style: TextStyle(
                    color: color,
                    fontWeight: FontWeight.w600,
                    fontSize: 12,
                  ),
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  Widget _buildRunButton(ColorScheme cs) {
    return GestureDetector(
      onTap: _running ? null : _runStressTest,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
        decoration: BoxDecoration(
          gradient: _running
              ? const LinearGradient(
                  colors: [Color(0xFF3A3A5C), Color(0xFF2A2A4A)],
                )
              : const LinearGradient(
                  colors: [Color(0xFF6C63FF), Color(0xFF48CAE4)],
                ),
          borderRadius: BorderRadius.circular(10),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              _running ? Icons.hourglass_top : Icons.play_arrow_rounded,
              color: Colors.white,
              size: 18,
            ),
            const SizedBox(width: 6),
            Text(
              _running ? 'Running...' : 'Run Suite',
              style: const TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.w700,
                fontSize: 13,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSummaryCard(StressReport report) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
      decoration: BoxDecoration(
        color: const Color(0xFF1A1A2E),
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: Colors.white12),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          _StatChip(label: 'Total', value: '${report.totalTests}', color: const Color(0xFF48CAE4)),
          _StatChip(label: 'Passed', value: '${report.passCount}', color: const Color(0xFF06D6A0)),
          _StatChip(label: 'Failed', value: '${report.failureCount}', color: const Color(0xFFEF476F)),
          _StatChip(label: 'Duration', value: '${report.totalDuration.inMilliseconds}ms', color: const Color(0xFFFFD166)),
        ],
      ),
    );
  }

  // Maximized Execution Log Card
  Widget _buildLogCard() {
    return Container(
      decoration: BoxDecoration(
        color: const Color(0xFF0D0D1A),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: Colors.white10),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Padding(
            padding: EdgeInsets.fromLTRB(12, 8, 12, 4),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  'EXECUTION LOG (MAXIMIZED SCREEN)',
                  style: TextStyle(
                    color: Colors.white54,
                    fontSize: 10,
                    fontWeight: FontWeight.w700,
                    letterSpacing: 1.2,
                  ),
                ),
                Icon(Icons.terminal, color: Colors.white38, size: 14),
              ],
            ),
          ),
          const Divider(color: Colors.white10, height: 1),
          Expanded(
            child: _log.isEmpty
                ? const Center(
                    child: Text(
                      'Select Model Adapter & Injectors, then tap "Run Suite"',
                      style: TextStyle(color: Colors.white24, fontSize: 12),
                    ),
                  )
                : ListView.builder(
                    padding: const EdgeInsets.all(10),
                    itemCount: _log.length,
                    itemBuilder: (ctx, i) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        _log[i],
                        style: const TextStyle(
                          color: Color(0xFFB0B8D8),
                          fontFamily: 'monospace',
                          fontSize: 12,
                        ),
                      ),
                    ),
                  ),
          ),
        ],
      ),
    );
  }
}

class _StatChip extends StatelessWidget {
  const _StatChip({
    required this.label,
    required this.value,
    required this.color,
  });

  final String label;
  final String value;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          value,
          style: TextStyle(
            color: color,
            fontSize: 15,
            fontWeight: FontWeight.w800,
          ),
        ),
        Text(
          label,
          style: const TextStyle(
            color: Colors.white38,
            fontSize: 9,
            fontWeight: FontWeight.w600,
          ),
        ),
      ],
    );
  }
}
0
likes
160
points
468
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A fault injection framework for testing on-device AI models in Flutter. Simulate memory pressure, malformed inputs, and degradation to catch failures before deployment.

Repository (GitHub)
View/report issues
Contributing

Topics

#testing #ai #on-device #fault-injection #research

License

MIT (license)

Dependencies

args, flutter, onnxruntime, tflite_flutter

More

Packages that depend on sate_ai