feat: add optional reference images to Nano Banana Pro node

This commit is contained in:
Steve Tietze
2025-12-17 19:27:43 +07:00
parent 2c77c91b1c
commit b4a2cf25a0
2 changed files with 376 additions and 2 deletions
+275 -1
View File
@@ -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.
+101 -1
View File
@@ -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...")