diff --git a/docs/KIE_API_NANOBANANA_SPEC.md b/docs/KIE_API_NANOBANANA_SPEC.md index 7caee40..097ccec 100644 --- a/docs/KIE_API_NANOBANANA_SPEC.md +++ b/docs/KIE_API_NANOBANANA_SPEC.md @@ -218,4 +218,278 @@ Failure Callback Example }, "msg": "Playground task failed." } -. + + +KIE AI FILE UPLOAD Stream endpoint to use.. + +# File Stream Upload + +## OpenAPI + +````yaml file-upload-api/file-upload-api.json post /api/file-stream-upload +openapi: 3.0.0 +info: + title: File Upload API + description: >- + File Upload Service API Documentation - Supporting multiple file upload + methods, uploaded files are temporary and automatically deleted after 3 days + version: 1.0.0 + contact: + name: Technical Support + email: support@kie.ai +servers: + - url: https://kieai.redpandaai.co + description: API Server +security: + - BearerAuth: [] +paths: + /api/file-stream-upload: + post: + summary: File Stream Upload + operationId: upload-file-stream + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: File to upload (binary data) + uploadPath: + type: string + description: File upload path, without leading or trailing slashes + example: images/user-uploads + fileName: + type: string + description: >- + File name (optional), including file extension. If not + provided, a random file name will be generated. If the same + file name is uploaded again, the old file will be + overwritten, but changes may not take effect immediately due + to caching + example: my-image.jpg + required: + - file + - uploadPath + responses: + '200': + $ref: '#/components/responses/SuccessResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/ServerError' +components: + responses: + SuccessResponse: + description: File uploaded successfully + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: Whether the request was successful + code: + type: integer + enum: + - 200 + - 400 + - 401 + - 405 + - 500 + description: >- + Response Status Code + + + | Code | Description | + + |------|-------------| + + | 200 | Success - Request has been processed successfully | + + | 400 | Bad Request - Request parameters are incorrect or + missing required parameters | + + | 401 | Unauthorized - Authentication credentials are missing + or invalid | + + | 405 | Method Not Allowed - Request method is not supported | + + | 500 | Server Error - An unexpected error occurred while + processing the request | + msg: + type: string + description: Response message + example: File uploaded successfully + data: + $ref: '#/components/schemas/FileUploadResult' + required: + - success + - code + - msg + - data + example: + success: true + code: 200 + msg: File uploaded successfully + data: + fileName: uploaded-image.png + filePath: images/user-uploads/uploaded-image.png + downloadUrl: >- + https://tempfile.redpandaai.co/xxx/images/user-uploads/uploaded-image.png + fileSize: 154832 + mimeType: image/png + uploadedAt: '2025-01-01T12:00:00.000Z' + BadRequestError: + description: Request parameter error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + examples: + missing_parameter: + summary: Missing required parameter + value: + success: false + code: 400 + msg: 'Missing required parameter: uploadPath' + invalid_format: + summary: Format error + value: + success: false + code: 400 + msg: 'Base64 decoding failed: Invalid Base64 format' + UnauthorizedError: + description: Unauthorized access + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + example: + success: false + code: 401 + msg: 'Authentication failed: Invalid API Key' + ServerError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + example: + success: false + code: 500 + msg: Internal server error + schemas: + FileUploadResult: + type: object + properties: + fileName: + type: string + description: File name + example: uploaded-image.png + filePath: + type: string + description: Complete file path in storage + example: images/user-uploads/uploaded-image.png + downloadUrl: + type: string + format: uri + description: File download URL + example: >- + https://tempfile.redpandaai.co/xxx/images/user-uploads/uploaded-image.png + fileSize: + type: integer + description: File size in bytes + example: 154832 + mimeType: + type: string + description: File MIME type + example: image/png + uploadedAt: + type: string + format: date-time + description: Upload timestamp + example: '2025-01-01T12:00:00.000Z' + required: + - fileName + - filePath + - downloadUrl + - fileSize + - mimeType + - uploadedAt + ApiResponse: + type: object + properties: + success: + type: boolean + description: Whether the request was successful + code: + $ref: '#/components/schemas/StatusCode' + msg: + type: string + description: Response message + example: File uploaded successfully + required: + - success + - code + - msg + StatusCode: + type: integer + enum: + - 200 + - 400 + - 401 + - 405 + - 500 + description: >- + Response Status Code + + + | Code | Description | + + |------|-------------| + + | 200 | Success - Request has been processed successfully | + + | 400 | Bad Request - Request parameters are incorrect or missing + required parameters | + + | 401 | Unauthorized - Authentication credentials are missing or invalid + | + + | 405 | Method Not Allowed - Request method is not supported | + + | 500 | Server Error - An unexpected error occurred while processing the + request | + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: API Key + description: >- + All APIs require authentication via Bearer Token. + + + Get API Key: + + 1. Visit [API Key Management Page](https://kie.ai/api-key) to get your + API Key + + + Usage: + + Add to request header: + + Authorization: Bearer YOUR_API_KEY + +```` + +--- + +> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.kie.ai/llms.txt. diff --git a/nodes.py b/nodes.py index 44855a9..16b26d2 100644 --- a/nodes.py +++ b/nodes.py @@ -13,10 +13,12 @@ KIE_KEY_PATH = Path(__file__).resolve().parent / "config" / "kie_key.txt" API_URL = "https://api.kie.ai/api/v1/chat/credit" CREATE_TASK_URL = "https://api.kie.ai/api/v1/jobs/createTask" RECORD_INFO_URL = "https://api.kie.ai/api/v1/jobs/recordInfo" +UPLOAD_URL = "https://kieai.redpandaai.co/api/file-stream-upload" MODEL_NAME = "nano-banana-pro" ASPECT_RATIO_OPTIONS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "auto"] RESOLUTION_OPTIONS = ["1K", "2K", "4K"] OUTPUT_FORMAT_OPTIONS = ["png", "jpg"] +UPLOAD_PATH = "images/user-uploads" def _load_api_key() -> str: @@ -65,6 +67,12 @@ def _log(enabled: bool, msg: str) -> None: print(f"[KIE] {msg}") +def _truncate_url(url: str, max_length: int = 80) -> str: + if len(url) <= max_length: + return url + return url[:max_length] + "..." + + class TransientKieError(RuntimeError): def __init__(self, message: str, status_code: int | None = None): super().__init__(message) @@ -239,6 +247,32 @@ def _image_bytes_to_tensor(image_bytes: bytes) -> torch.Tensor: raise RuntimeError("Failed to decode result image.") from exc +def _image_tensor_to_png_bytes(image: torch.Tensor) -> bytes: + if image.dim() != 3 or image.shape[2] != 3: + raise RuntimeError("Image tensor must have shape [H, W, 3].") + if image.numel() == 0: + raise RuntimeError("Image tensor is empty.") + + if image.dtype != torch.uint8: + working = image.detach().cpu().clamp(0, 1) * 255.0 + working = working.round().to(torch.uint8) + else: + working = image.detach().cpu() + + working = working.contiguous() + height, width, _channels = working.shape + data_bytes = bytes(working.view(-1).tolist()) + + try: + pil_image = Image.frombytes("RGB", (width, height), data_bytes) + except Exception as exc: + raise RuntimeError("Failed to convert tensor to image.") from exc + + with BytesIO() as output: + pil_image.save(output, format="PNG") + return output.getvalue() + + def _should_retry_fail(fail_code: Any, fail_msg: Any, message: Any) -> bool: try: code_int = int(fail_code) @@ -260,6 +294,41 @@ def _should_retry_fail(fail_code: Any, fail_msg: Any, message: Any) -> bool: return False +def _upload_image(api_key: str, png_bytes: bytes) -> str: + try: + response = requests.post( + UPLOAD_URL, + headers={"Authorization": f"Bearer {api_key}"}, + files={"file": ("image.png", png_bytes, "image/png")}, + data={"uploadPath": UPLOAD_PATH}, + timeout=120, + ) + except requests.RequestException as exc: + raise RuntimeError(f"Failed to upload image: {exc}") from exc + + if response.status_code == 429 or response.status_code >= 500: + raise TransientKieError( + f"upload returned HTTP {response.status_code}: {response.text}", status_code=response.status_code + ) + + try: + payload_json: Any = response.json() + except json.JSONDecodeError as exc: + raise RuntimeError("Upload endpoint did not return valid JSON.") from exc + + success = payload_json.get("success") + code = payload_json.get("code") + if not success or code != 200: + raise RuntimeError(f"Upload failed (code={code}): {payload_json.get('msg')}") + + data = payload_json.get("data") or {} + url = data.get("downloadUrl") + if not url: + raise RuntimeError("Upload response missing downloadUrl.") + + return url + + class KIE_GetRemainingCredits: @classmethod def INPUT_TYPES(cls): @@ -284,6 +353,7 @@ class KIE_NanoBananaPro_Image: return { "required": {"prompt": ("STRING", {"multiline": True})}, "optional": { + "images": ("IMAGE",), "aspect_ratio": ("COMBO", {"options": ASPECT_RATIO_OPTIONS, "default": "auto"}), "resolution": ("COMBO", {"options": RESOLUTION_OPTIONS, "default": "1K"}), "output_format": ("COMBO", {"options": OUTPUT_FORMAT_OPTIONS, "default": "png"}), @@ -313,6 +383,7 @@ class KIE_NanoBananaPro_Image: retry_on_fail: bool = True, max_retries: int = 2, retry_backoff_s: float = 3.0, + images: torch.Tensor | None = None, ): _validate_prompt(prompt) @@ -330,6 +401,33 @@ class KIE_NanoBananaPro_Image: for attempt in range(1, attempts + 1): start_time = time.time() try: + api_key = _load_api_key() + + image_urls: list[str] = [] + if images is not None: + if not isinstance(images, torch.Tensor): + raise RuntimeError("images input must be a tensor batch.") + if images.dim() != 4 or images.shape[-1] != 3: + raise RuntimeError("images input must have shape [B, H, W, 3].") + + total_images = images.shape[0] + if total_images > 8 and log: + _log(log, f"More than 8 images provided ({total_images}); only first 8 will be used.") + + upload_count = min(total_images, 8) + if upload_count > 0: + _log(log, f"Uploading {upload_count} images...") + + for idx in range(upload_count): + try: + png_bytes = _image_tensor_to_png_bytes(images[idx]) + url = _upload_image(api_key, png_bytes) + image_urls.append(url) + _log(log, f"Image {idx + 1} upload success: {_truncate_url(url)}") + except Exception as exc: + _log(log, f"Image {idx + 1} upload failed: {exc}") + raise + payload = { "model": MODEL_NAME, "input": { @@ -337,11 +435,13 @@ class KIE_NanoBananaPro_Image: "aspect_ratio": aspect_ratio, "resolution": resolution, "output_format": output_format, + "image_input": image_urls, }, } + _log(log, f"Sending {len(image_urls)} image URLs to createTask") + _log(log, "Creating Nano Banana Pro task...") - api_key = _load_api_key() task_id, create_response_text = _create_nano_banana_task(api_key, payload) _log(log, f"createTask response (elapsed={time.time() - start_time:.1f}s): {create_response_text}") _log(log, f"Task created with ID {task_id}. Polling for completion...")