Skip to content

BFL

BFL 属于任务型接口:创建任务 → 查询结果。

Base URL / 鉴权

  • Base URL:https://api.uniapi.io/bfl
  • Header:x-key: <your-api-key>

你原文示例也体现了这一点:client headers 使用 x-key

主要接口

  • POST /v1/{model}
  • GET /v1/get_result?id={task_id}

具体字段与返回结构以 API 开发文档为准:https://api-docs.uniapi.ai/

示例

python
import time
import requests
import json
import os
from typing import Dict, Any, List, Union, Optional

class BFLAPIError(Exception):
    """BFL API Error"""

    def __init__(self, status_code: int, message: str):
        self.status_code = status_code
        self.message = message
        super().__init__(f"API Error [{status_code}]: {message}")

    @classmethod
    def from_response(cls, response: requests.Response):
        """Create exception from response object"""
        try:
            data = response.json()
            if 'detail' in data and data['detail']:
                message = data['detail']
            elif 'details' in data and data['details']:
                message = json.dumps(data['details'], ensure_ascii=False)
            else:
                message = response.text or "Unknown Error"
        except Exception:
            message = response.text or "Unknown Error"

        return cls(
            status_code=response.status_code,
            message=message
        )

class BFLOutput:
    """Handle BFL API Output"""

    def __init__(self, result: Dict[str, Any], task_id: str):
        self._result = result
        self._task_id = task_id
        self._sample_url = result.get('sample')

    def url(self) -> Optional[str]:
        """Return the sample URL"""
        return self._sample_url

    def save(self, path: str) -> str:
        if not self._sample_url:
            raise ValueError("No sample URL available to save")

        os.makedirs(path, exist_ok=True)

        if '.png' in self._sample_url.lower():
            ext = '.png'
        elif '.jpg' in self._sample_url.lower() or '.jpeg' in self._sample_url.lower():
            ext = '.jpg'
        else:
            ext = '.jpg'

        filename = f"{self._task_id}{ext}"
        filepath = os.path.join(path, filename)

        response = requests.get(self._sample_url, stream=True)
        response.raise_for_status()

        with open(filepath, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

        return filepath

class BFLClient:
    """BFL API Client"""

    def __init__(self, api_key: str, base_url: str = "https://api.uniapi.io/bfl"):
        self.api_key = api_key
        self.base_url = base_url.rstrip('/')
        self.session = requests.Session()
        self.session.headers.update({
            'x-key': api_key,
            'Content-Type': 'application/json'
        })

    def run(
        self,
        model: str,
        input: Dict[str, Any],
        poll_interval: float = 1.0,
        timeout: Optional[float] = None
    ) -> BFLOutput:
        task_id = self._create_task(model, input)
        print(f"Task created: {task_id}")

        result_data = self._poll_result(task_id, poll_interval, timeout)

        return BFLOutput(result_data['result'], task_id)

    def _create_task(self, model: str, input: Dict[str, Any]) -> str:
        url = f"{self.base_url}/v1/{model}"

        response = self.session.post(url, json=input)

        if response.status_code != 200:
            raise BFLAPIError.from_response(response)

        data = response.json()
        return data['id']

    def _poll_result(
        self,
        task_id: str,
        poll_interval: float,
        timeout: Optional[float]
    ) -> Dict[str, Any]:
        url = f"{self.base_url}/v1/get_result"
        params = {'id': task_id}

        start_time = time.time()

        while True:
            if timeout and (time.time() - start_time) > timeout:
                raise TimeoutError(f"Task {task_id} timed out")

            response = self.session.get(url, params=params)

            # Handle 404 by ignoring and continuing
            if response.status_code == 404:
                time.sleep(poll_interval)
                continue

            if response.status_code != 200:
                raise BFLAPIError.from_response(response)

            data = response.json()
            status = data.get('status')

            if status == 'Ready':
                result = data.get('result')
                if not result or 'sample' not in result:
                    raise RuntimeError("Task Ready but result or sample missing")
                return data

            elif status in ['Request Moderated', 'Content Moderated', 'Error']:
                details = data.get('details')
                if not details:
                    raise RuntimeError(
                        f"Task failed with status {status}. Details: {data.get('detail') or 'No details provided'}"
                    )

                if isinstance(details, (dict, list)):
                    details_str = json.dumps(details, ensure_ascii=False)
                else:
                    details_str = str(details)

                raise RuntimeError(f"Task failed with status {status}: {details_str}")

            elif status == 'Pending':
                time.sleep(poll_interval)

            elif status == 'Task not found':
                print(f"Status: {status}, waiting...")
                time.sleep(poll_interval)

            else:
                print(f"Unknown status: {status}, waiting...")
                time.sleep(poll_interval)


if __name__ == "__main__":
    client = BFLClient(
        api_key="sk-xxxxxx",
        base_url="https://api.uniapi.io/bfl" 
    )

    input_data = {
        "prompt": "A futuristic city with flying cars, neon lights, cyberpunk style",
        "width": 1024,
        "height": 768
    }

    try:
        output = client.run(
            "flux-pro-1.1",
            input=input_data
        )

        print("Output URL:")
        print(output.url())

        saved_file = output.save("./output")
        print(f"\nSaved file: {saved_file}")

    except Exception as e:
        print(f"An error occurred: {e}")