@@ -37,7 +37,7 @@ class BFLClient:
3737 def __init__ (
3838 self ,
3939 api_key : str ,
40- base_url : str = "https://api.us1. bfl.ai" ,
40+ base_url : str = "https://api.bfl.ai" ,
4141 timeout : int = 30 ,
4242 ):
4343 """
@@ -57,6 +57,50 @@ def __init__(
5757 'Content-Type' : 'application/json' ,
5858 'Accept' : 'application/json' ,
5959 })
60+ # Map to store task_id -> (polling_url, timestamp)
61+ self ._task_polling_urls = {}
62+
63+ def _cleanup_expired_polling_urls (self ) -> None :
64+ """
65+ Remove polling URL entries that are older than 30 minutes.
66+ """
67+ current_time = time .time ()
68+ expired_keys = []
69+
70+ for task_id , (polling_url , timestamp ) in self ._task_polling_urls .items ():
71+ # Remove entries older than 30 minutes (1800 seconds)
72+ if current_time - timestamp > 1800 :
73+ expired_keys .append (task_id )
74+
75+ for key in expired_keys :
76+ del self ._task_polling_urls [key ]
77+
78+ def clear_polling_urls (self ) -> None :
79+ """
80+ Manually clear all stored polling URLs.
81+ """
82+ self ._task_polling_urls .clear ()
83+
84+ def _get_polling_endpoint (self , task_id : str ) -> str :
85+ """
86+ Get the appropriate polling endpoint for a task.
87+
88+ Args:
89+ task_id: The task ID to get the endpoint for
90+
91+ Returns:
92+ The endpoint (relative path or full URL) to use for polling
93+ """
94+ # Clean up expired entries
95+ self ._cleanup_expired_polling_urls ()
96+
97+ if task_id in self ._task_polling_urls :
98+ polling_url , _ = self ._task_polling_urls [task_id ] # Extract URL from tuple
99+ # Return the full polling URL as-is since _request() now handles full URLs
100+ return polling_url
101+ else :
102+ # Fallback to constructed URL
103+ return f"/v1/get_result?id={ task_id } "
60104
61105 def _request (
62106 self ,
@@ -71,7 +115,7 @@ def _request(
71115
72116 Args:
73117 method: HTTP method
74- endpoint: API endpoint
118+ endpoint: API endpoint (can be a relative path or full URL)
75119 params: URL parameters
76120 data: Form data
77121 json: JSON data
@@ -82,7 +126,11 @@ def _request(
82126 Raises:
83127 BFLError: If the API request fails
84128 """
85- url = urljoin (self .base_url , endpoint )
129+ # If endpoint is already a full URL, use it directly
130+ if endpoint .startswith (('http://' , 'https://' )):
131+ url = endpoint
132+ else :
133+ url = urljoin (self .base_url , endpoint )
86134
87135 try :
88136 response = self .session .request (
@@ -112,6 +160,38 @@ def _encode_image(self, image_path: str) -> str:
112160 with open (image_path , 'rb' ) as image_file :
113161 return base64 .b64encode (image_file .read ()).decode ('utf-8' )
114162
163+ def _is_file_path (self , value : str ) -> bool :
164+ """Check if a string is a file path (not base64 or URL)."""
165+ if value .startswith (('http://' , 'https://' )):
166+ return False
167+ # Check if it looks like base64 (no path separators, mostly alphanumeric with +/=)
168+ if '/' not in value and '\\ ' not in value and len (value ) > 100 :
169+ try :
170+ base64 .b64decode (value )
171+ return False # It's valid base64
172+ except Exception :
173+ pass
174+ # Check if file exists
175+ return os .path .exists (value )
176+
177+ def _process_kontext_inputs (self , inputs : Dict [str , Any ]) -> Dict [str , Any ]:
178+ """Process flux-kontext-pro inputs to automatically encode image paths."""
179+ processed_inputs = inputs .copy ()
180+
181+ # Image fields that might need encoding
182+ image_fields = ['input_image' , 'input_image_2' , 'input_image_3' , 'input_image_4' ]
183+
184+ for field in image_fields :
185+ if field in processed_inputs and processed_inputs [field ]:
186+ value = processed_inputs [field ]
187+ if isinstance (value , str ) and self ._is_file_path (value ):
188+ try :
189+ processed_inputs [field ] = self ._encode_image (value )
190+ except Exception as e :
191+ raise BFLError (f"Error encoding image file '{ value } ' for field '{ field } ': { str (e )} " )
192+
193+ return processed_inputs
194+
115195 def _process_folder (self , folder_path : str ) -> List [str ]:
116196 """Process all images in a folder and return list of base64 encoded images."""
117197 folder = Path (folder_path )
@@ -198,8 +278,15 @@ def process_image(
198278 # Make the API request
199279 response = self ._request ("POST" , endpoint , json = payload )
200280
281+ # Store the polling URL for this task if available
282+ task_id = response .get ("id" )
283+ if task_id and "polling_url" in response :
284+ # Clean up expired entries before adding new one
285+ self ._cleanup_expired_polling_urls ()
286+ self ._task_polling_urls [task_id ] = (response ["polling_url" ], time .time ())
287+
201288 return ImageProcessingResponse (
202- task_id = response . get ( "id" ) ,
289+ task_id = task_id ,
203290 status = "submitted" ,
204291 result = response
205292 )
@@ -214,7 +301,8 @@ def get_task_status(self, task_id: str) -> ImageProcessingResponse:
214301 Returns:
215302 ImageProcessingResponse containing current status and result if available
216303 """
217- response = self ._request ("GET" , f"/v1/get_result?id={ task_id } " )
304+ endpoint = self ._get_polling_endpoint (task_id )
305+ response = self ._request ("GET" , endpoint )
218306
219307 return ImageProcessingResponse (
220308 task_id = task_id ,
@@ -250,13 +338,15 @@ def get_polling_result(
250338 while attempts < config .max_retries :
251339 print (f"Polling task { task_id } for result. \
252340 Attempt { attempts + 1 } of { config .max_retries } " )
253- response = self ._request ("GET" , f"/v1/get_result?id={ task_id } " )
341+
342+ endpoint = self ._get_polling_endpoint (task_id )
343+ response = self ._request ("GET" , endpoint )
254344
255345 # Check if the task is complete
256346 if response .get ("status" ) in ["Ready" , "completed" , "failed" ]:
257347 if response .get ("status" ) == "failed" :
258- raise BFLError ( f"Task failed: { response .get ('error' , \
259- 'Unknown error' ) } " )
348+ error_msg = response .get ('error' , 'Unknown error' )
349+ raise BFLError ( f"Task failed: { error_msg } " )
260350 return response .get ("result" , {})
261351
262352 # Check for timeout
@@ -319,30 +409,42 @@ def generate(
319409 raise BFLError (f"Model { model } not supported. \
320410 Supported models: { list (self .model_input_registry .keys ())} " )
321411
412+ # Process inputs for automatic image encoding if using flux-kontext-pro
413+ processed_inputs = inputs
414+ if model == "flux-kontext-pro" :
415+ processed_inputs = self ._process_kontext_inputs (inputs )
416+
322417 # Convert the inputs to the appropriate type
323- typed_inputs = input_cls (** inputs )
418+ typed_inputs = input_cls (** processed_inputs )
324419
325420 response = self ._request ("POST" , f"{ self .api_version } /{ model } " ,
326421 json = typed_inputs .model_dump (exclude_none = True ))
327422
423+ # Store the polling URL for this task
424+ task_id = response ["id" ]
425+ if "polling_url" in response :
426+ # Clean up expired entries before adding new one
427+ self ._cleanup_expired_polling_urls ()
428+ self ._task_polling_urls [task_id ] = (response ["polling_url" ], time .time ())
429+
328430 # Track usage if requested
329431 if track_usage :
330432 self .track_usage_via_api (model , 1 )
331433
332434 # If sync is True, poll for results
333435 if config .sync :
334436 try :
335- result = self .get_polling_result (response [ "id" ] , config )
437+ result = self .get_polling_result (task_id , config )
336438 return SyncResponse (
337- id = response [ "id" ] ,
439+ id = task_id ,
338440 result = result
339441 )
340442 except Exception as e :
341443 raise BFLError (f"Error getting synchronous result: { str (e )} " )
342444
343445 else :
344446 return AsyncResponse (
345- id = response [ "id" ] ,
447+ id = task_id ,
346448 polling_url = response ["polling_url" ]
347449 )
348450
@@ -383,5 +485,4 @@ def track_usage_via_api(self, name: str, n: int = 1) -> None:
383485 self ._request ("POST" , endpoint , json = payload )
384486 print (f"Successfully tracked usage for { name } with { n } generations" )
385487 except BFLError as e :
386- raise BFLError (f"Failed to track usage for { name } : { str (e )} " )
387- q
488+ raise BFLError (f"Failed to track usage for { name } : { str (e )} " )
0 commit comments