55
66namespace ImageGenCli ;
77
8+ /// <summary>
9+ /// Image generation client for Black Forest Labs FLUX API.
10+ /// Supports flux-2-pro, flux-2-flex, and flux-2-max models.
11+ /// </summary>
812public class BflImageClient : IImageGenerationClient
913{
10- private readonly HttpClient _http ;
14+ private static readonly HttpClient Http = CreateHttpClient ( ) ;
1115 private readonly string _apiKey ;
1216 private readonly string _model ;
1317 private const string BaseUrl = "https://api.bfl.ml" ;
1418 private static readonly TimeSpan PollInterval = TimeSpan . FromSeconds ( 1 ) ;
1519 private static readonly TimeSpan MaxWaitTime = TimeSpan . FromMinutes ( 5 ) ;
1620
21+ private const int FlexDefaultSteps = 50 ;
22+ private const double FlexDefaultGuidance = 4.5 ;
23+ private const double MaxMegapixels = 4.0 ;
24+ private const int DimensionMultiple = 16 ;
25+ private const int MinDimension = 64 ;
26+ private const int MaxReferenceImages = 8 ;
27+
1728 private static readonly JsonSerializerOptions JsonOptions = new ( )
1829 {
1930 PropertyNamingPolicy = JsonNamingPolicy . SnakeCaseLower ,
2031 DefaultIgnoreCondition = JsonIgnoreCondition . WhenWritingNull
2132 } ;
2233
34+ private static HttpClient CreateHttpClient ( )
35+ {
36+ return new HttpClient ( ) ;
37+ }
38+
39+ /// <summary>
40+ /// Creates a new BFL FLUX image client.
41+ /// </summary>
42+ /// <param name="apiKey">The BFL API key.</param>
43+ /// <param name="model">The model to use (default: flux-2-pro).</param>
2344 public BflImageClient ( string apiKey , string model = "flux-2-pro" )
2445 {
2546 _apiKey = apiKey ;
2647 _model = model ;
27- _http = new HttpClient ( ) ;
28- _http . DefaultRequestHeaders . Add ( "x-key" , _apiKey ) ;
2948 }
3049
50+ /// <inheritdoc />
3151 public async Task < GenerationResult > GenerateImagesAsync ( GenerationRequest request , CancellationToken ct = default )
3252 {
3353 var endpoint = GetEndpoint ( ) ;
@@ -43,13 +63,13 @@ public async Task<GenerationResult> GenerateImagesAsync(GenerationRequest reques
4363 [ "output_format" ] = "png"
4464 } ;
4565
46- // Add reference images if provided (up to 8 )
47- for ( int i = 0 ; i < Math . Min ( request . ReferenceImages . Length , 8 ) ; i ++ )
66+ // Add reference images if provided (up to max limit )
67+ for ( int i = 0 ; i < Math . Min ( request . ReferenceImages . Length , MaxReferenceImages ) ; i ++ )
4868 {
4969 var imagePath = request . ReferenceImages [ i ] ;
5070 var bytes = await File . ReadAllBytesAsync ( imagePath , ct ) ;
5171 var base64 = Convert . ToBase64String ( bytes ) ;
52- var mimeType = GetMimeType ( imagePath ) ;
72+ var mimeType = MimeTypeHelper . GetMimeType ( imagePath ) ;
5373 var dataUri = $ "data:{ mimeType } ;base64,{ base64 } ";
5474
5575 var key = i == 0 ? "input_image" : $ "input_image_{ i + 1 } ";
@@ -59,8 +79,8 @@ public async Task<GenerationResult> GenerateImagesAsync(GenerationRequest reques
5979 // Flex-specific parameters
6080 if ( _model . Contains ( "flex" , StringComparison . OrdinalIgnoreCase ) )
6181 {
62- body [ "steps" ] = 50 ;
63- body [ "guidance" ] = 4.5 ;
82+ body [ "steps" ] = FlexDefaultSteps ;
83+ body [ "guidance" ] = FlexDefaultGuidance ;
6484 }
6585
6686 var images = new List < GeneratedImage > ( ) ;
@@ -74,7 +94,11 @@ public async Task<GenerationResult> GenerateImagesAsync(GenerationRequest reques
7494 body [ "seed" ] = Random . Shared . Next ( ) ;
7595 }
7696
77- var response = await _http . PostAsJsonAsync ( url , body , JsonOptions , ct ) ;
97+ using var requestMessage = new HttpRequestMessage ( HttpMethod . Post , url ) ;
98+ requestMessage . Headers . Add ( "x-key" , _apiKey ) ;
99+ requestMessage . Content = JsonContent . Create ( body , options : JsonOptions ) ;
100+
101+ var response = await Http . SendAsync ( requestMessage , ct ) ;
78102 var content = await response . Content . ReadAsStringAsync ( ct ) ;
79103
80104 if ( ! response . IsSuccessStatusCode )
@@ -108,7 +132,7 @@ public async Task<GenerationResult> GenerateImagesAsync(GenerationRequest reques
108132 {
109133 ct . ThrowIfCancellationRequested ( ) ;
110134
111- var response = await _http . GetAsync ( pollUrl , ct ) ;
135+ var response = await Http . GetAsync ( pollUrl , ct ) ;
112136 var content = await response . Content . ReadAsStringAsync ( ct ) ;
113137
114138 if ( ! response . IsSuccessStatusCode )
@@ -163,9 +187,9 @@ public async Task<GenerationResult> GenerateImagesAsync(GenerationRequest reques
163187 throw new ImageGenerationException ( $ "BFL generation timed out after { MaxWaitTime . TotalMinutes } minutes") ;
164188 }
165189
166- private async Task < GeneratedImage > DownloadImageAsync ( string imageUrl , CancellationToken ct )
190+ private static async Task < GeneratedImage > DownloadImageAsync ( string imageUrl , CancellationToken ct )
167191 {
168- var response = await _http . GetAsync ( imageUrl , ct ) ;
192+ var response = await Http . GetAsync ( imageUrl , ct ) ;
169193 if ( ! response . IsSuccessStatusCode )
170194 {
171195 throw new ImageGenerationException ( $ "Failed to download generated image: { ( int ) response . StatusCode } ") ;
@@ -202,22 +226,22 @@ private static (int width, int height) MapAspectRatioToSize(string aspectRatio,
202226 _ => 1.0 // 1K default
203227 } ;
204228
205- // Calculate dimensions based on aspect ratio, constrained to 4MP max
206- megapixels = Math . Min ( megapixels , 4.0 ) ;
229+ // Constrain to max megapixels
230+ megapixels = Math . Min ( megapixels , MaxMegapixels ) ;
207231 var totalPixels = megapixels * 1_000_000 ;
208232
209233 var ( ratioW , ratioH ) = ParseAspectRatio ( aspectRatio ) ;
210234 var scale = Math . Sqrt ( totalPixels / ( ratioW * ratioH ) ) ;
211235 var width = ( int ) ( ratioW * scale ) ;
212236 var height = ( int ) ( ratioH * scale ) ;
213237
214- // Round to nearest multiple of 16 (BFL requirement)
215- width = ( width / 16 ) * 16 ;
216- height = ( height / 16 ) * 16 ;
238+ // Round to nearest multiple (BFL requirement)
239+ width = ( width / DimensionMultiple ) * DimensionMultiple ;
240+ height = ( height / DimensionMultiple ) * DimensionMultiple ;
217241
218- // Ensure minimum of 64
219- width = Math . Max ( 64 , width ) ;
220- height = Math . Max ( 64 , height ) ;
242+ // Ensure minimum dimension
243+ width = Math . Max ( MinDimension , width ) ;
244+ height = Math . Max ( MinDimension , height ) ;
221245
222246 return ( width , height ) ;
223247 }
@@ -234,20 +258,6 @@ private static (double w, double h) ParseAspectRatio(string aspectRatio)
234258 return ( 1 , 1 ) ; // Default to square
235259 }
236260
237- private static string GetMimeType ( string path )
238- {
239- var ext = Path . GetExtension ( path ) . ToLowerInvariant ( ) ;
240- return ext switch
241- {
242- ".png" => "image/png" ,
243- ".jpg" or ".jpeg" => "image/jpeg" ,
244- ".gif" => "image/gif" ,
245- ".webp" => "image/webp" ,
246- ".bmp" => "image/bmp" ,
247- _ => "application/octet-stream"
248- } ;
249- }
250-
251261 private class AsyncResponse
252262 {
253263 public string ? Id { get ; set ; }
0 commit comments