godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.17k stars 98 forks source link

Add support for multipart form data in HTTPRequest #8520

Open tavurth opened 1 year ago

tavurth commented 1 year ago

Describe the project you are working on

A project that works with Image-to-Image tools such as Stable Diffusion

Describe the problem or limitation you are having in your project

The POST request must contain several body parameters. It must also contain one source image (named).

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Currently it's pretty hard to work with FormData requests in GDScript, even though most other programming languages feature a way to do exactly this.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Please see here for an exact reference:

https://platform.stability.ai/docs/api-reference#tag/v1generation/operation/imageToImage

In general I like pythonic style of:

response = requests.post(
    f"{api_host}/v1/generation/{engine_id}/image-to-image",
    headers={
        "Accept": "application/json",
        "Authorization": f"Bearer {api_key}"
    },
    files={
        "init_image": open("../someimage.png", "rb")
    },
    data={
        "someconfig": "somevalue"
    }
)

If this enhancement will not be used often, can it be worked around with a few lines of script?

This should be part of HTTPRequest

Is there a reason why this should be core and not an add-on in the asset library?

This is core GDScript

tavurth commented 1 year ago

Here's an example code of how it could be done

extends Node

func send_data_to_server():
    var http_request = HTTPRequest.new()
    add_child(http_request)

    # Create a FormData object
    var form_data = FormData.new()

    # Write the init image to the request
    var init_image_path = "../init_image_1024.png"
    var init_image_file = File.new()
    init_image_file.open(init_image_path, File.READ)
    form_data.add_file("init_image", init_image_path, init_image_file.get_buffer(init_image_file.get_len()))

    # Write other fields to the request
    form_data.add_field("init_image_mode", "IMAGE_STRENGTH")
    form_data.add_field("image_strength", "0.35")
    form_data.add_field("text_prompts[0][text]", "Galactic dog with a cape")
    form_data.add_field("cfg_scale", "7")
    form_data.add_field("samples", "1")
    form_data.add_field("steps", "30")

    # Set up the HTTPRequest
    var headers = [
         "Accept: image/png",
         "Authorization: Bearer %s" % config.api_key,
         "Content-Length: " + str(len(config.init_image)),
         "Content-Type: multipart/form-data; boundary=" + form_data.get_boundary())
    ]
    # Connect the signals to handle the response
    request.request_completed.connect(self._on_request_completed)

    # Set up the HTTPRequest
    request.request_raw(URL % config.model, headers, HTTPClient.METHOD_POST, form_data.get_data())

func _on_request_completed(result, response_code, headers, body):
    # Handle the response here
    print("Response Code:", response_code)
    print("Response Body:", body)

And here's the example FormData class reference

class_name FormData

extends Object

var boundary: String = "--WebKitFormBoundary7MA4YWxkTrZu0gW"

var files: Dictionary = {}
var fields: Dictionary = {}

func add_field(name: String, value: String) -> void:
    fields[name] = value

func add_file(name: String, filename: String, data: PackedByteArray) -> void:
    files[name] = {"filename": filename, "data": data}

func get_data() -> PackedByteArray:
    var data: String = ""

    for field_name in fields.keys():
        data += "--" + boundary + "\r\n"
        data += "Content-Disposition: form-data; name=\"" + field_name + "\"\r\n\r\n"
        data += fields[field_name] + "\r\n"

    for file_name in files.keys():
        data += "--" + boundary + "\r\n"
        data += "Content-Disposition: form-data; name=\"" + file_name + "\"; filename=\"" + files[file_name]["filename"] + "\"\r\n"
        data += "Content-Type: application/octet-stream\r\n\r\n"
        data += files[file_name]["data"] + "\r\n"

    data += "--" + boundary + "--\r\n"

    return data.to_utf8_buffer()

func get_boundary() -> String:
    return boundary
Goury commented 1 week ago

request.request_raw

@Calinou did you mean http_request ?

tavurth commented 1 week ago

For reference, I open sourced the project I was working on at the time:

https://github.com/tavurth/animation-styling/blob/master/utilities/dreamstudio_i2i.gd#L85-L136

https://github.com/tavurth/animation-styling/blob/master/utilities/FormData.gd