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