Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,64 @@ async def main() -> None:
- `cacheStopStep`: The step at which caching ends (default: total steps)
- These parameters allow fine-grained control over when caching is active during the generation process.

### Asynchronous Processing with Webhooks

The Runware SDK supports asynchronous processing via webhooks for long-running operations. When you provide a `webhookURL`, the API immediately returns a task response and sends the final result to your webhook endpoint when processing completes.

#### How it works

1. Include `webhookURL` parameter in your request
2. Receive immediate response with `taskType` and `taskUUID`
3. Final result is POSTed to your webhook URL when ready

Supported operations:
- Image Inference
- Photo Maker
- Image Caption
- Image Background Removal
- Image Upscale
- Prompt Enhance
- Video Inference

#### Example

```python
from runware import Runware, IImageInference

async def main() -> None:
runware = Runware(api_key=RUNWARE_API_KEY)
await runware.connect()

request_image = IImageInference(
positivePrompt="a beautiful mountain landscape",
model="civitai:36520@76907",
height=512,
width=512,
webhookURL="https://your-server.com/webhook/runware"
)

# Returns immediately with task info
response = await runware.imageInference(requestImage=request_image)
print(f"Task Type: {response.taskType}")
print(f"Task UUID: {response.taskUUID}")
# Result will be sent to your webhook URL
```

#### Webhook Payload Format
Your webhook endpoint will receive a POST request with the same format as synchronous responses:
```json{
"data": [
{
"taskType": "imageInference",
"taskUUID": "a770f077-f413-47de-9dac-be0b26a35da6",
"imageUUID": "77da2d99-a6d3-44d9-b8c0-ae9fb06b6200",
"imageURL": "https://im.runware.ai/image/...",
"cost": 0.0013
}
]
}
```

### Enhancing Prompts

To enhance prompts using the Runware API, you can use the `promptEnhance` method of the `Runware` class. Here's an example:
Expand Down
2 changes: 1 addition & 1 deletion runware/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
from .async_retry import *

__all__ = ["Runware"]
__version__ = "0.4.24"
__version__ = "0.4.25"
35 changes: 30 additions & 5 deletions runware/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
IGoogleProviderSettings,
IKlingAIProviderSettings,
IFrameImage,
IAsyncTaskResponse,
)
from .types import IImage, IError, SdkType, ListenerType
from .utils import (
Expand All @@ -59,6 +60,7 @@
LISTEN_TO_IMAGES_KEY,
isLocalFile,
process_image, delay,
createAsyncTaskResponse,
)

# Configure logging
Expand Down Expand Up @@ -199,6 +201,8 @@ async def photoMaker(self, requestPhotoMaker: IPhotoMaker):
request_object["includeCost"] = requestPhotoMaker.includeCost
if requestPhotoMaker.outputType:
request_object["outputType"] = requestPhotoMaker.outputType
if requestPhotoMaker.webhookURL:
request_object["webhookURL"] = requestPhotoMaker.webhookURL

await self.send([request_object])

Expand Down Expand Up @@ -466,8 +470,9 @@ async def _requestImageToText(
# Add optional parameters if they are provided
if requestImageToText.includeCost:
task_params["includeCost"] = requestImageToText.includeCost
if requestImageToText.webhookURL:
task_params["webhookURL"] = requestImageToText.webhookURL


await self.send([task_params])

lis = self.globalListener(
Expand Down Expand Up @@ -552,6 +557,8 @@ async def _removeImageBackground(
task_params["model"] = removeImageBackgroundPayload.model
if removeImageBackgroundPayload.outputQuality:
task_params["outputQuality"] = removeImageBackgroundPayload.outputQuality
if removeImageBackgroundPayload.webhookURL:
task_params["webhookURL"] = removeImageBackgroundPayload.webhookURL

# Handle settings if provided - convert dataclass to dictionary and add non-None values
if removeImageBackgroundPayload.settings:
Expand Down Expand Up @@ -635,6 +642,8 @@ async def _upscaleGan(self, upscaleGanPayload: IImageUpscale) -> List[IImage]:
task_params["outputFormat"] = upscaleGanPayload.outputFormat
if upscaleGanPayload.includeCost:
task_params["includeCost"] = upscaleGanPayload.includeCost
if upscaleGanPayload.webhookURL:
task_params["webhookURL"] = upscaleGanPayload.webhookURL

# Send the task with all applicable parameters
await self.send([task_params])
Expand Down Expand Up @@ -721,6 +730,10 @@ async def _enhancePrompt(
if promptEnhancer.includeCost:
task_params["includeCost"] = promptEnhancer.includeCost

has_webhook = promptEnhancer.webhookURL
if has_webhook:
task_params["webhookURL"] = promptEnhancer.webhookURL

# Send the task with all applicable parameters
await self.send([task_params])

Expand Down Expand Up @@ -1284,12 +1297,16 @@ async def videoInference(self, requestVideo: IVideoInference) -> List[IVideo]:
await self.ensureConnection()
return await asyncRetry(lambda: self._requestVideo(requestVideo))

async def _requestVideo(self, requestVideo: IVideoInference) -> List[IVideo]:
async def _requestVideo(self, requestVideo: IVideoInference) -> Union[List[IVideo], IAsyncTaskResponse]:
await self._processVideoImages(requestVideo)
requestVideo.taskUUID = requestVideo.taskUUID or getUUID()
request_object = self._buildVideoRequest(requestVideo)

if requestVideo.webhookURL:
request_object["webhookURL"] = requestVideo.webhookURL

await self.send([request_object])
return await self._handleInitialVideoResponse(requestVideo.taskUUID, requestVideo.numberResults)
return await self._handleInitialVideoResponse(requestVideo.taskUUID, requestVideo.numberResults, request_object.get("webhookURL"))

async def _processVideoImages(self, requestVideo: IVideoInference) -> None:
frame_tasks = []
Expand Down Expand Up @@ -1391,7 +1408,7 @@ def _addOptionalImageFields(self, request_object: Dict[str, Any], requestImage:
"outputType", "outputFormat", "outputQuality", "uploadEndpoint",
"includeCost", "checkNsfw", "negativePrompt", "seedImage", "maskImage",
"strength", "height", "width", "steps", "scheduler", "seed", "CFGScale",
"clipSkip", "promptWeighting", "maskMargin", "vae"
"clipSkip", "promptWeighting", "maskMargin", "vae", "webhookURL"
]

for field in optional_fields:
Expand Down Expand Up @@ -1499,7 +1516,7 @@ def _addProviderSettings(self, request_object: Dict[str, Any], requestVideo: IVi
if provider_dict:
request_object["providerSettings"] = provider_dict

async def _handleInitialVideoResponse(self, task_uuid: str, number_results: int) -> List[IVideo]:
async def _handleInitialVideoResponse(self, task_uuid: str, number_results: int, webhook_url: Optional[str] = None) -> Union[List[IVideo], IAsyncTaskResponse]:
lis = self.globalListener(taskUUID=task_uuid)

def check_initial_response(resolve: callable, reject: callable, *args: Any) -> bool:
Expand All @@ -1518,6 +1535,14 @@ def check_initial_response(resolve: callable, reject: callable, *args: Any) -> b
resolve([response])
return True

# Check if this is a webhook response (no imageUUID means async task accepted)
if not response.get("imageUUID") and webhook_url:
del self._globalMessages[task_uuid]
# Return async task response for webhook
async_response = createAsyncTaskResponse(response)
resolve([async_response])
return True

del self._globalMessages[task_uuid]
resolve("POLL_NEEDED")
return True
Expand Down
11 changes: 11 additions & 0 deletions runware/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ class File:
data: bytes


@dataclass
class IAsyncTaskResponse:
taskType: str
taskUUID: str


@dataclass
class RunwareBaseType:
apiKey: str
Expand Down Expand Up @@ -304,6 +310,7 @@ class IPhotoMaker:
outputFormat: Optional[IOutputFormat] = None
includeCost: Optional[bool] = None
taskUUID: Optional[str] = None
webhookURL: Optional[str] = None

def __post_init__(self):
# Validate `inputImages` to ensure it has a maximum of 4 elements
Expand Down Expand Up @@ -484,6 +491,7 @@ class IImageCaption:
model: Optional[str] = None # Optional: AIR ID (runware:150@1, runware:150@2) - backend handles default
includeCost: bool = False
template: Optional[str] = None
webhookURL: Optional[str] = None


@dataclass
Expand Down Expand Up @@ -548,6 +556,7 @@ class IPromptEnhance:
promptVersions: int
prompt: str
includeCost: bool = False
webhookURL: Optional[str] = None


@dataclass
Expand All @@ -565,6 +574,7 @@ class IImageUpscale:
outputType: Optional[IOutputType] = None
outputFormat: Optional[IOutputFormat] = None
includeCost: bool = False
webhookURL: Optional[str] = None


class ReconnectingWebsocketProps:
Expand Down Expand Up @@ -811,6 +821,7 @@ class IVideoInference:
numberResults: Optional[int] = 1
providerSettings: Optional[VideoProviderSettings] = None
speech: Optional[IPixverseSpeechSettings] = None
webhookURL: Optional[str] = None


@dataclass
Expand Down
11 changes: 11 additions & 0 deletions runware/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
IEnhancedPrompt,
IError,
UploadImageType,
IAsyncTaskResponse,
)
import logging

Expand Down Expand Up @@ -585,6 +586,16 @@ def process_single_prompt(prompt_data: dict) -> IEnhancedPrompt:
return [process_single_prompt(prompt) for prompt in response]


def createAsyncTaskResponse(response: dict) -> IAsyncTaskResponse:
processed_fields = {}

for field in fields(IAsyncTaskResponse):
if field.name in response:
processed_fields[field.name] = response[field.name]

return instantiateDataclass(IAsyncTaskResponse, processed_fields)


def createImageFromResponse(response: dict) -> IImage:
processed_fields = {}

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
setup(
name="runware",
license="MIT",
version="0.4.24",
version="0.4.25",
author="Runware Inc.",
author_email="python.sdk@runware.ai",
description="The Python Runware SDK is used to run image inference with the Runware API, powered by the Runware inference platform. It can be used to generate images with text-to-image and image-to-image. It also allows the use of an existing gallery of models or selecting any model or LoRA from the CivitAI gallery. The API also supports upscaling, background removal, inpainting and outpainting, and a series of other ControlNet models.",
Expand Down