Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: file uploads and new sample app #4

Merged
merged 3 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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