feat: add optional reference images to Nano Banana Pro node
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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...")
|
||||
|
||||
Reference in New Issue
Block a user