fix(blocks): Fix Image input on AIImageEditorBlock to accept relative path file media (#10210)

AIImageEditorBlock was not able to accept an image from AgentFileInput
or FileStore block.

### Changes 🏗️

* Add support for image loading for the image editor block:
<img width="1081" alt="Screenshot 2025-06-23 at 10 28 45 AM"
src="https://github.com/user-attachments/assets/ac3fea91-9503-4894-bbe3-2dc3c5649a39"
/>

* Avoid rendering a relative path image file.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [x] Run AiImageEditor block using AgentFileInput or FileStore block.
This commit is contained in:
Zamil Majdy
2025-06-24 13:36:02 -07:00
committed by GitHub
parent efa4b6d2a0
commit 1c6b829925
4 changed files with 42 additions and 16 deletions

View File

@@ -14,6 +14,12 @@ class FileStoreBlock(Block):
file_in: MediaFileType = SchemaField(
description="The file to store in the temporary directory, it can be a URL, data URI, or local path."
)
base_64: bool = SchemaField(
description="Whether produce an output in base64 format (not recommended, you can pass the string path just fine accross blocks).",
default=False,
advanced=True,
title="Produce Base64 Output",
)
class Output(BlockSchema):
file_out: MediaFileType = SchemaField(
@@ -37,12 +43,11 @@ class FileStoreBlock(Block):
graph_exec_id: str,
**kwargs,
) -> BlockOutput:
file_path = await store_media_file(
yield "file_out", await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.file_in,
return_content=False,
return_content=input_data.base_64,
)
yield "file_out", file_path
class StoreValueBlock(Block):

View File

@@ -13,7 +13,7 @@ from backend.data.model import (
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.file import MediaFileType
from backend.util.file import MediaFileType, store_media_file
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
@@ -108,7 +108,7 @@ class AIImageEditorBlock(Block):
output_schema=AIImageEditorBlock.Output,
test_input={
"prompt": "Add a hat to the cat",
"input_image": "https://example.com/cat.png",
"input_image": "data:image/png;base64,MQ==",
"aspect_ratio": AspectRatio.MATCH_INPUT_IMAGE,
"seed": None,
"model": FluxKontextModelName.PRO,
@@ -128,13 +128,22 @@ class AIImageEditorBlock(Block):
input_data: Input,
*,
credentials: APIKeyCredentials,
graph_exec_id: str,
**kwargs,
) -> BlockOutput:
result = await self.run_model(
api_key=credentials.api_key,
model_name=input_data.model.api_name,
prompt=input_data.prompt,
input_image=input_data.input_image,
input_image_b64=(
await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.input_image,
return_content=True,
)
if input_data.input_image
else None
),
aspect_ratio=input_data.aspect_ratio.value,
seed=input_data.seed,
)
@@ -145,14 +154,14 @@ class AIImageEditorBlock(Block):
api_key: SecretStr,
model_name: str,
prompt: str,
input_image: Optional[MediaFileType],
input_image_b64: Optional[str],
aspect_ratio: str,
seed: Optional[int],
) -> MediaFileType:
client = ReplicateClient(api_token=api_key.get_secret_value())
input_params = {
"prompt": prompt,
"input_image": input_image,
"input_image": input_image_b64,
"aspect_ratio": aspect_ratio,
**({"seed": seed} if seed is not None else {}),
}

View File

@@ -413,6 +413,12 @@ class AgentFileInputBlock(AgentInputBlock):
advanced=False,
title="Default Value",
)
base_64: bool = SchemaField(
description="Whether produce an output in base64 format (not recommended, you can pass the string path just fine accross blocks).",
default=False,
advanced=True,
title="Produce Base64 Output",
)
class Output(AgentInputBlock.Output):
result: str = SchemaField(description="File reference/path result.")
@@ -446,12 +452,11 @@ class AgentFileInputBlock(AgentInputBlock):
if not input_data.value:
return
file_path = await store_media_file(
yield "result", await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.value,
return_content=False,
return_content=input_data.base_64,
)
yield "result", file_path
class AgentDropdownInputBlock(AgentInputBlock):

View File

@@ -9,16 +9,24 @@ const getYouTubeVideoId = (url: string) => {
return match && match[7].length === 11 ? match[7] : null;
};
const isValidMediaUri = (url: string): boolean => {
if (url.startsWith("data:")) {
return true;
}
const validUrl = /^(https?:\/\/)(www\.)?/i;
const cleanedUrl = url.split("?")[0];
return validUrl.test(cleanedUrl);
};
const isValidVideoUrl = (url: string): boolean => {
if (url.startsWith("data:video")) {
return true;
}
const validUrl = /^(https?:\/\/)(www\.)?/i;
const videoExtensions = /\.(mp4|webm|ogg)$/i;
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/;
const cleanedUrl = url.split("?")[0];
return (
(validUrl.test(cleanedUrl) && videoExtensions.test(cleanedUrl)) ||
(isValidMediaUri(url) && videoExtensions.test(cleanedUrl)) ||
youtubeRegex.test(cleanedUrl)
);
};
@@ -29,17 +37,16 @@ const isValidImageUrl = (url: string): boolean => {
}
const imageExtensions = /\.(jpeg|jpg|gif|png|svg|webp)$/i;
const cleanedUrl = url.split("?")[0];
return imageExtensions.test(cleanedUrl);
return isValidMediaUri(url) && imageExtensions.test(cleanedUrl);
};
const isValidAudioUrl = (url: string): boolean => {
if (url.startsWith("data:audio")) {
return true;
}
const validUrl = /^(https?:\/\/)(www\.)?/i;
const audioExtensions = /\.(mp3|wav)$/i;
const cleanedUrl = url.split("?")[0];
return validUrl.test(cleanedUrl) && audioExtensions.test(cleanedUrl);
return isValidMediaUri(url) && audioExtensions.test(cleanedUrl);
};
const VideoRenderer: React.FC<{ videoUrl: string }> = ({ videoUrl }) => {