Skip to content

Commit

Permalink
feat: file uploads and new sample app (#4)
Browse files Browse the repository at this point in the history
* fix: file upload logic

* feat: new example app

* chore: update changelog
  • Loading branch information
drochetti authored Nov 7, 2023
1 parent 39e5480 commit 74bf371
Show file tree
Hide file tree
Showing 14 changed files with 104 additions and 69 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 2 additions & 0 deletions example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) need access to your photo gallery so you can pick image patterns</string>
</dict>
</plist>
118 changes: 71 additions & 47 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -11,19 +12,20 @@ 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
Widget build(BuildContext context) {
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'),
Expand All @@ -33,73 +35,95 @@ 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
State<TextoToImageScreen> createState() => _TextoToImageScreenState();
}

class _TextoToImageScreenState extends State<TextoToImageScreen> {
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<String> 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;
});
}

@override
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: <Widget>[
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),
),
);
}
}
16 changes: 7 additions & 9 deletions example/lib/types.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
class TextToImageResult {
final List<ImageRef> 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<String, dynamic> json) {
return TextToImageResult(
images: (json['images'] as List<dynamic>)
.map((e) => ImageRef.fromMap(e as Map<String, dynamic>))
.toList(),
factory IllusionDiffusionResult.fromMap(Map<String, dynamic> json) {
return IllusionDiffusionResult(
image: ImageRef.fromMap(json['image'] as Map<String, dynamic>),
seed: (json['seed'] * 1).round(),
);
}
Expand All @@ -30,4 +28,4 @@ class ImageRef {
}
}

const textToImageId = '110602490-lora';
const textToImageId = '54285744-illusion-diffusion';
2 changes: 2 additions & 0 deletions example/macos/Runner/DebugProfile.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions example/macos/Runner/Release.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
2 changes: 1 addition & 1 deletion example/test/widget_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ abstract class Client {
});
}

/// The default implementation of the [Client] contract.
class FalClient implements Client {
final Config config;

Expand Down
4 changes: 2 additions & 2 deletions lib/src/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

Expand Down Expand Up @@ -66,7 +66,7 @@ Future<Map<String, dynamic>> 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(
Expand Down
2 changes: 1 addition & 1 deletion lib/src/runtime/version.dart
Original file line number Diff line number Diff line change
@@ -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';
6 changes: 1 addition & 5 deletions lib/src/storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 74bf371

Please sign in to comment.