diff --git a/CHANGELOG.md b/CHANGELOG.md index be4680e..6cd7b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v0.2.2 + +### What's Changed +* feat: file uploads and new sample app by @drochetti in https://github.com/fal-ai/serverless-client-dart/pull/4 + +**Full Changelog**: https://github.com/fal-ai/serverless-client-dart/compare/v0.2.1...v0.2.2 + +--- + ## v0.2.1 ### What's Changed diff --git a/README.md b/README.md index 4c46e95..a216477 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ This client library is crafted as a lightweight layer atop platform standards li 3. Now use `fal.subcribe` to dispatch requests to the model API: ```dart - final result = await fal.subscribe('110602490-lora', + final result = await fal.subscribe('text-to-image', input: { 'prompt': 'a cute shih-tzu puppy', 'model_name': 'stabilityai/stable-diffusion-xl-base-1.0', diff --git a/example/README.md b/example/README.md index 491fa60..3afecd3 100644 --- a/example/README.md +++ b/example/README.md @@ -1,15 +1,16 @@ # fal_sample_app -A new Flutter project. +A sample app that shows how to use https://fal.ai model APIs with Flutter. ## Getting Started -This project is a starting point for a Flutter application. +This project is a starting point for a Flutter application with fal.ai. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +- [Get started with fal.ai](https://fal.ai/docs) For help getting started with Flutter development, view the [online documentation](https://docs.flutter.dev/), which offers tutorials, diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 931620b..d2b3739 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -45,5 +45,7 @@ UIApplicationSupportsIndirectInputEvents + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME) need access to your photo gallery so you can pick image patterns diff --git a/example/lib/main.dart b/example/lib/main.dart index c06252a..3442ddc 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:fal_client/fal_client.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import 'types.dart'; @@ -11,11 +12,11 @@ import 'types.dart'; final fal = FalClient.withCredentials('FAL_KEY_ID:FAL_KEY_SECRET'); void main() { - runApp(const MyApp()); + runApp(const FalSampleApp()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class FalSampleApp extends StatelessWidget { + const FalSampleApp({super.key}); // This widget is the root of your application. @override @@ -23,7 +24,8 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'fal.ai', theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.indigo, brightness: Brightness.dark), useMaterial3: true, ), home: const TextoToImageScreen(title: 'fal.ai'), @@ -33,16 +35,6 @@ class MyApp extends StatelessWidget { class TextoToImageScreen extends StatefulWidget { const TextoToImageScreen({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked 'final'. - final String title; @override @@ -50,26 +42,32 @@ class TextoToImageScreen extends StatefulWidget { } class _TextoToImageScreenState extends State { - bool _isLoading = false; - final _promptController = - TextEditingController(text: 'a cute shih-tzu puppy'); - ImageRef? _image; + final ImagePicker _picker = ImagePicker(); + XFile? _image; + final TextEditingController _promptController = TextEditingController(); + String? _generatedImageUrl; + bool _isProcessing = false; - void _generateImage() async { + Future generateImage(XFile image, String prompt) async { + final result = await fal.subscribe(textToImageId, input: { + 'prompt': prompt, + 'image_url': image, + }); + return result['image']['url'] as String; + } + + void _onGenerateImage() async { + if (_image == null || _promptController.text.isEmpty) { + // Handle error: either image not selected or prompt not entered + return; + } setState(() { - _isLoading = true; - _image = null; + _isProcessing = true; }); - final result = await fal.subscribe(textToImageId, - input: { - 'prompt': _promptController.text, - 'model_name': 'stabilityai/stable-diffusion-xl-base-1.0', - 'image_size': 'square_hd' - }, - onQueueUpdate: (update) => {print(update)}); + String imageUrl = await generateImage(_image!, _promptController.text); setState(() { - _isLoading = false; - _image = TextToImageResult.fromMap(result).images[0]; + _generatedImageUrl = imageUrl; + _isProcessing = false; }); } @@ -77,29 +75,55 @@ class _TextoToImageScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: const Text('Illusion Diffusion'), ), - body: Center( + body: Padding( + padding: const EdgeInsets.all(16.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - TextField(controller: _promptController), - if (_isLoading) const CircularProgressIndicator(), - if (!_isLoading && _image != null) - FittedBox( - fit: BoxFit.fill, - child: Image.network(_image!.url, - width: _image!.width.toDouble(), - height: _image!.height.toDouble()), - ), + ElevatedButton( + onPressed: () async { + final XFile? image = + await _picker.pickImage(source: ImageSource.gallery); + setState(() { + _image = image; + }); + }, + child: const Text('Pick Image'), + ), + // if (_image != null) + // Image, + TextFormField( + controller: _promptController, + decoration: const InputDecoration(labelText: 'Imagine...'), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _isProcessing ? null : _onGenerateImage, + child: _isProcessing + ? const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(), + ), + SizedBox(width: 8), + Text('Generating...'), + ], + ) + : const Text('Generate Image'), + ), + if (_generatedImageUrl != null) + Padding( + padding: const EdgeInsets.only(top: 20), + child: Image.network(_generatedImageUrl!)), ], ), ), - floatingActionButton: FloatingActionButton( - onPressed: _generateImage, - tooltip: 'Generate', - child: const Icon(Icons.play_arrow_rounded), - ), ); } } diff --git a/example/lib/types.dart b/example/lib/types.dart index 9e6e66d..f867b9c 100644 --- a/example/lib/types.dart +++ b/example/lib/types.dart @@ -1,14 +1,12 @@ -class TextToImageResult { - final List images; +class IllusionDiffusionResult { + final ImageRef image; final int seed; - TextToImageResult({required this.images, required this.seed}); + IllusionDiffusionResult({required this.image, required this.seed}); - factory TextToImageResult.fromMap(Map json) { - return TextToImageResult( - images: (json['images'] as List) - .map((e) => ImageRef.fromMap(e as Map)) - .toList(), + factory IllusionDiffusionResult.fromMap(Map json) { + return IllusionDiffusionResult( + image: ImageRef.fromMap(json['image'] as Map), seed: (json['seed'] * 1).round(), ); } @@ -30,4 +28,4 @@ class ImageRef { } } -const textToImageId = '110602490-lora'; +const textToImageId = '54285744-illusion-diffusion'; diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index 08c3ab1..8f69d6a 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,7 @@ com.apple.security.network.client + com.apple.security.files.user-selected.read-only + diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements index ee95ab7..e270453 100644 --- a/example/macos/Runner/Release.entitlements +++ b/example/macos/Runner/Release.entitlements @@ -6,5 +6,7 @@ com.apple.security.network.client + com.apple.security.files.user-selected.read-only + diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index ba2baad..5fc2b9f 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:fal_sample_app/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const FalSampleApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/lib/src/client.dart b/lib/src/client.dart index 7bbe879..006d903 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -63,6 +63,7 @@ abstract class Client { }); } +/// The default implementation of the [Client] contract. class FalClient implements Client { final Config config; diff --git a/lib/src/http.dart b/lib/src/http.dart index 712bf34..32c2ffd 100644 --- a/lib/src/http.dart +++ b/lib/src/http.dart @@ -33,7 +33,7 @@ String buildUrl( params != null && params.query.isNotEmpty ? '?${params.query}' : ''; return isValidUrl(id) - ? 'id$queryParams' + ? '$id$queryParams' : 'https://$id.${config.host}/$pathValue$queryParams'; } @@ -66,7 +66,7 @@ Future> sendRequest( headers['Authorization'] = 'Key ${config.credentials}'; } if (config.proxyUrl != null) { - headers['X-Fal-Target-Url'] = url; + headers['x-fal-target-url'] = url; } final request = http.Request( diff --git a/lib/src/runtime/version.dart b/lib/src/runtime/version.dart index 902964a..4c3bc87 100644 --- a/lib/src/runtime/version.dart +++ b/lib/src/runtime/version.dart @@ -1,3 +1,3 @@ // Note: temporary solution, should be replaced with something // else that resolves the package version at build-time or runtime. -const packageVersion = '0.2.1'; +const packageVersion = '0.2.2'; diff --git a/lib/src/storage.dart b/lib/src/storage.dart index 87c9c81..fbef7f9 100644 --- a/lib/src/storage.dart +++ b/lib/src/storage.dart @@ -4,10 +4,6 @@ import 'package:http/http.dart' as http; import './config.dart'; import './http.dart'; -bool isDataUri(String uri) { - return uri.startsWith("data:"); -} - /// This establishes the contract of the client with the file storage capabilities. /// Long running requests cannot keep files in memory, so models that require /// files/images as input need to upload them and submit their URLs instead. @@ -38,7 +34,7 @@ class StorageClient implements Storage { final contentType = file.mimeType ?? 'application/octet-stream'; final signedUpload = await sendRequest(url, input: { - 'filename': file.name, + 'file_name': file.name, 'content_type': contentType, }, config: config); diff --git a/pubspec.yaml b/pubspec.yaml index 91f12d4..699810f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: fal_client description: > The Dart client library for fal.ai model APIs. You can use it to call multiple AI models on your Dart and Flutter apps. -version: 0.2.1 +version: 0.2.2 homepage: https://fal.ai repository: https://github.com/fal-ai/serverless-client-dart issue_tracker: https://github.com/fal-ai/serverless-client-dart/issues?q=is%3Aissue+is%3Aopen