Skip to content

Commit 58b7da3

Browse files
authored
Merge pull request #161 from Runware/feature-webhookurl-support
feat: Add webhookURL support for all inference types
2 parents 97f26a3 + 7796c8b commit 58b7da3

6 files changed

Lines changed: 112 additions & 7 deletions

File tree

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,64 @@ async def main() -> None:
143143
- `cacheStopStep`: The step at which caching ends (default: total steps)
144144
- These parameters allow fine-grained control over when caching is active during the generation process.
145145

146+
### Asynchronous Processing with Webhooks
147+
148+
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.
149+
150+
#### How it works
151+
152+
1. Include `webhookURL` parameter in your request
153+
2. Receive immediate response with `taskType` and `taskUUID`
154+
3. Final result is POSTed to your webhook URL when ready
155+
156+
Supported operations:
157+
- Image Inference
158+
- Photo Maker
159+
- Image Caption
160+
- Image Background Removal
161+
- Image Upscale
162+
- Prompt Enhance
163+
- Video Inference
164+
165+
#### Example
166+
167+
```python
168+
from runware import Runware, IImageInference
169+
170+
async def main() -> None:
171+
runware = Runware(api_key=RUNWARE_API_KEY)
172+
await runware.connect()
173+
174+
request_image = IImageInference(
175+
positivePrompt="a beautiful mountain landscape",
176+
model="civitai:36520@76907",
177+
height=512,
178+
width=512,
179+
webhookURL="https://your-server.com/webhook/runware"
180+
)
181+
182+
# Returns immediately with task info
183+
response = await runware.imageInference(requestImage=request_image)
184+
print(f"Task Type: {response.taskType}")
185+
print(f"Task UUID: {response.taskUUID}")
186+
# Result will be sent to your webhook URL
187+
```
188+
189+
#### Webhook Payload Format
190+
Your webhook endpoint will receive a POST request with the same format as synchronous responses:
191+
```json{
192+
"data": [
193+
{
194+
"taskType": "imageInference",
195+
"taskUUID": "a770f077-f413-47de-9dac-be0b26a35da6",
196+
"imageUUID": "77da2d99-a6d3-44d9-b8c0-ae9fb06b6200",
197+
"imageURL": "https://im.runware.ai/image/...",
198+
"cost": 0.0013
199+
}
200+
]
201+
}
202+
```
203+
146204
### Enhancing Prompts
147205

148206
To enhance prompts using the Runware API, you can use the `promptEnhance` method of the `Runware` class. Here's an example:

runware/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
from .async_retry import *
77

88
__all__ = ["Runware"]
9-
__version__ = "0.4.24"
9+
__version__ = "0.4.25"

runware/base.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
IGoogleProviderSettings,
4040
IKlingAIProviderSettings,
4141
IFrameImage,
42+
IAsyncTaskResponse,
4243
)
4344
from .types import IImage, IError, SdkType, ListenerType
4445
from .utils import (
@@ -59,6 +60,7 @@
5960
LISTEN_TO_IMAGES_KEY,
6061
isLocalFile,
6162
process_image, delay,
63+
createAsyncTaskResponse,
6264
)
6365

6466
# Configure logging
@@ -199,6 +201,8 @@ async def photoMaker(self, requestPhotoMaker: IPhotoMaker):
199201
request_object["includeCost"] = requestPhotoMaker.includeCost
200202
if requestPhotoMaker.outputType:
201203
request_object["outputType"] = requestPhotoMaker.outputType
204+
if requestPhotoMaker.webhookURL:
205+
request_object["webhookURL"] = requestPhotoMaker.webhookURL
202206

203207
await self.send([request_object])
204208

@@ -466,8 +470,9 @@ async def _requestImageToText(
466470
# Add optional parameters if they are provided
467471
if requestImageToText.includeCost:
468472
task_params["includeCost"] = requestImageToText.includeCost
473+
if requestImageToText.webhookURL:
474+
task_params["webhookURL"] = requestImageToText.webhookURL
469475

470-
471476
await self.send([task_params])
472477

473478
lis = self.globalListener(
@@ -552,6 +557,8 @@ async def _removeImageBackground(
552557
task_params["model"] = removeImageBackgroundPayload.model
553558
if removeImageBackgroundPayload.outputQuality:
554559
task_params["outputQuality"] = removeImageBackgroundPayload.outputQuality
560+
if removeImageBackgroundPayload.webhookURL:
561+
task_params["webhookURL"] = removeImageBackgroundPayload.webhookURL
555562

556563
# Handle settings if provided - convert dataclass to dictionary and add non-None values
557564
if removeImageBackgroundPayload.settings:
@@ -647,6 +654,8 @@ async def _upscaleGan(self, upscaleGanPayload: IImageUpscale) -> List[IImage]:
647654
task_params["outputFormat"] = upscaleGanPayload.outputFormat
648655
if upscaleGanPayload.includeCost:
649656
task_params["includeCost"] = upscaleGanPayload.includeCost
657+
if upscaleGanPayload.webhookURL:
658+
task_params["webhookURL"] = upscaleGanPayload.webhookURL
650659

651660
# Send the task with all applicable parameters
652661

@@ -734,6 +743,10 @@ async def _enhancePrompt(
734743
if promptEnhancer.includeCost:
735744
task_params["includeCost"] = promptEnhancer.includeCost
736745

746+
has_webhook = promptEnhancer.webhookURL
747+
if has_webhook:
748+
task_params["webhookURL"] = promptEnhancer.webhookURL
749+
737750
# Send the task with all applicable parameters
738751
await self.send([task_params])
739752

@@ -1297,12 +1310,16 @@ async def videoInference(self, requestVideo: IVideoInference) -> List[IVideo]:
12971310
await self.ensureConnection()
12981311
return await asyncRetry(lambda: self._requestVideo(requestVideo))
12991312

1300-
async def _requestVideo(self, requestVideo: IVideoInference) -> List[IVideo]:
1313+
async def _requestVideo(self, requestVideo: IVideoInference) -> Union[List[IVideo], IAsyncTaskResponse]:
13011314
await self._processVideoImages(requestVideo)
13021315
requestVideo.taskUUID = requestVideo.taskUUID or getUUID()
13031316
request_object = self._buildVideoRequest(requestVideo)
1317+
1318+
if requestVideo.webhookURL:
1319+
request_object["webhookURL"] = requestVideo.webhookURL
1320+
13041321
await self.send([request_object])
1305-
return await self._handleInitialVideoResponse(requestVideo.taskUUID, requestVideo.numberResults)
1322+
return await self._handleInitialVideoResponse(requestVideo.taskUUID, requestVideo.numberResults, request_object.get("webhookURL"))
13061323

13071324
async def _processVideoImages(self, requestVideo: IVideoInference) -> None:
13081325
frame_tasks = []
@@ -1404,7 +1421,7 @@ def _addOptionalImageFields(self, request_object: Dict[str, Any], requestImage:
14041421
"outputType", "outputFormat", "outputQuality", "uploadEndpoint",
14051422
"includeCost", "checkNsfw", "negativePrompt", "seedImage", "maskImage",
14061423
"strength", "height", "width", "steps", "scheduler", "seed", "CFGScale",
1407-
"clipSkip", "promptWeighting", "maskMargin", "vae"
1424+
"clipSkip", "promptWeighting", "maskMargin", "vae", "webhookURL"
14081425
]
14091426

14101427
for field in optional_fields:
@@ -1512,7 +1529,7 @@ def _addProviderSettings(self, request_object: Dict[str, Any], requestVideo: IVi
15121529
if provider_dict:
15131530
request_object["providerSettings"] = provider_dict
15141531

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

15181535
def check_initial_response(resolve: callable, reject: callable, *args: Any) -> bool:
@@ -1531,6 +1548,14 @@ def check_initial_response(resolve: callable, reject: callable, *args: Any) -> b
15311548
resolve([response])
15321549
return True
15331550

1551+
# Check if this is a webhook response (no imageUUID means async task accepted)
1552+
if not response.get("imageUUID") and webhook_url:
1553+
del self._globalMessages[task_uuid]
1554+
# Return async task response for webhook
1555+
async_response = createAsyncTaskResponse(response)
1556+
resolve([async_response])
1557+
return True
1558+
15341559
del self._globalMessages[task_uuid]
15351560
resolve("POLL_NEEDED")
15361561
return True

runware/types.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ class File:
108108
data: bytes
109109

110110

111+
@dataclass
112+
class IAsyncTaskResponse:
113+
taskType: str
114+
taskUUID: str
115+
116+
111117
@dataclass
112118
class RunwareBaseType:
113119
apiKey: str
@@ -304,6 +310,7 @@ class IPhotoMaker:
304310
outputFormat: Optional[IOutputFormat] = None
305311
includeCost: Optional[bool] = None
306312
taskUUID: Optional[str] = None
313+
webhookURL: Optional[str] = None
307314

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

488496

489497
@dataclass
@@ -548,6 +556,7 @@ class IPromptEnhance:
548556
promptVersions: int
549557
prompt: str
550558
includeCost: bool = False
559+
webhookURL: Optional[str] = None
551560

552561

553562
@dataclass
@@ -589,6 +598,7 @@ class IImageUpscale:
589598
outputType: Optional[IOutputType] = None
590599
outputFormat: Optional[IOutputFormat] = None
591600
includeCost: bool = False
601+
webhookURL: Optional[str] = None
592602

593603

594604
class ReconnectingWebsocketProps:
@@ -835,6 +845,7 @@ class IVideoInference:
835845
numberResults: Optional[int] = 1
836846
providerSettings: Optional[VideoProviderSettings] = None
837847
speech: Optional[IPixverseSpeechSettings] = None
848+
webhookURL: Optional[str] = None
838849

839850

840851
@dataclass

runware/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
IEnhancedPrompt,
2828
IError,
2929
UploadImageType,
30+
IAsyncTaskResponse,
3031
)
3132
import logging
3233

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

587588

589+
def createAsyncTaskResponse(response: dict) -> IAsyncTaskResponse:
590+
processed_fields = {}
591+
592+
for field in fields(IAsyncTaskResponse):
593+
if field.name in response:
594+
processed_fields[field.name] = response[field.name]
595+
596+
return instantiateDataclass(IAsyncTaskResponse, processed_fields)
597+
598+
588599
def createImageFromResponse(response: dict) -> IImage:
589600
processed_fields = {}
590601

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
setup(
77
name="runware",
88
license="MIT",
9-
version="0.4.24",
9+
version="0.4.25",
1010
author="Runware Inc.",
1111
author_email="python.sdk@runware.ai",
1212
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.",

0 commit comments

Comments
 (0)