xpunge 0.1.4
xpunge: ^0.1.4 copied to clipboard
On-device NSFW/explicit content detection SDK for Android and iOS. No images leave the device.
example/lib/main.dart
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:xpunge/xpunge.dart';
void main() {
runApp(const XPungeExampleApp());
}
class XPungeExampleApp extends StatelessWidget {
const XPungeExampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'xPunge Example',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _initialized = false;
bool _loading = false;
String _status = 'Not initialized';
Uint8List? _imageBytes;
int _imageWidth = 0;
int _imageHeight = 0;
List<Detection> _detections = [];
// Replace with a real key from https://xpunge.com/dashboard
static const _testApiKey =
'xp1.eyJ2IjoxLCJ0aWVyIjoiaW5kaWUiLCJwa2ciOiIiLCJleHAiOjB9.mmZOQZwsYXzsoyau8mW2hw';
@override
void initState() {
super.initState();
_initSdk();
}
Future<void> _initSdk() async {
setState(() => _status = 'Initializing…');
try {
await XPunge.initialize(_testApiKey);
setState(() {
_initialized = true;
_status = 'Ready — tier: ${XPunge.tier.name}';
});
} on XPungeException catch (e) {
setState(() => _status = 'Init failed: $e');
}
}
Future<void> _pickAndAnalyze() async {
if (!_initialized) return;
final picker = ImagePicker();
final picked = await picker.pickImage(source: ImageSource.gallery);
if (picked == null) return;
final bytes = await picked.readAsBytes();
final codec = await ui.instantiateImageCodec(bytes);
final frame = await codec.getNextFrame();
final imgW = frame.image.width;
final imgH = frame.image.height;
frame.image.dispose();
setState(() {
_loading = true;
_imageBytes = bytes;
_imageWidth = imgW;
_imageHeight = imgH;
_detections = [];
_status = 'Analyzing…';
});
try {
final detections = await XPunge.analyzeXFile(picked);
setState(() {
_detections = detections;
_status = detections.isEmpty
? 'No explicit content detected'
: '${detections.length} detection(s)';
});
} on XPungeException catch (e) {
setState(() => _status = 'Detection failed: $e');
} finally {
setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('xPunge Demo')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(_status, style: Theme.of(context).textTheme.titleMedium),
),
if (_imageBytes != null)
Expanded(
child: Stack(
fit: StackFit.expand,
children: [
Image.memory(_imageBytes!, fit: BoxFit.contain),
if (XPunge.tier == XPungeTier.paid)
CustomPaint(
painter: _BoundingBoxPainter(
_detections,
_imageWidth,
_imageHeight,
),
),
],
),
),
if (_detections.isNotEmpty)
SizedBox(
height: 160,
child: ListView.builder(
itemCount: _detections.length,
itemBuilder: (context, i) {
final d = _detections[i];
return ListTile(
title: Text(d.label),
subtitle: Text(
'confidence: ${(d.confidence * 100).toStringAsFixed(1)}%'
'${XPunge.tier == XPungeTier.paid ? ' box: ${d.boundingBox}' : ' (upgrade for bounding boxes)'}',
),
);
},
),
),
const SizedBox(height: 16),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _loading ? null : _pickAndAnalyze,
label: const Text('Pick Image'),
icon: const Icon(Icons.photo_library),
),
);
}
}
class _BoundingBoxPainter extends CustomPainter {
final List<Detection> detections;
final int imageWidth;
final int imageHeight;
_BoundingBoxPainter(this.detections, this.imageWidth, this.imageHeight);
@override
void paint(Canvas canvas, Size size) {
if (imageWidth == 0 || imageHeight == 0) return;
// Compute the rect occupied by the image inside the widget (BoxFit.contain).
final scale = (size.width / imageWidth).clamp(0.0, size.height / imageHeight);
final renderedW = imageWidth * scale;
final renderedH = imageHeight * scale;
final offsetX = (size.width - renderedW) / 2;
final offsetY = (size.height - renderedH) / 2;
final paint = Paint()
..color = const Color(0xFFFF3B30)
..style = PaintingStyle.stroke
..strokeWidth = 2;
for (final d in detections) {
final box = d.boundingBox;
final rect = Rect.fromLTWH(
offsetX + box.left * renderedW,
offsetY + box.top * renderedH,
box.width * renderedW,
box.height * renderedH,
);
canvas.drawRect(rect, paint);
final tp = TextPainter(
text: TextSpan(
text: '${d.label} ${(d.confidence * 100).toStringAsFixed(0)}%',
style: const TextStyle(color: Color(0xFFFF3B30), fontSize: 12),
),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(canvas, Offset(rect.left, rect.top - 14));
}
}
@override
bool shouldRepaint(_BoundingBoxPainter old) =>
old.detections != detections ||
old.imageWidth != imageWidth ||
old.imageHeight != imageHeight;
}