tanny2109's picture
Upload 26 files
b5c24ef verified
Raw History Blame Contribute Delete
6.12 kB
"""Image editing tool using fal.ai API."""
import os
import json
import time
import tempfile
from typing import List, Union
from concurrent.futures import ThreadPoolExecutor, as_completed
import fal_client
import requests
from pydantic import BaseModel, Field, field_validator
from langchain_core.tools import tool
# Configure fal.ai
fal_client.api_key = os.getenv("FAL_KEY")
EDIT_MODEL = "fal-ai/bytedance/seedream/v4.5/edit"
class ImageEditInput(BaseModel):
"""Input schema for image editing."""
image_paths: Union[str, List[str]] = Field(
description="File path(s) of images to edit. Can be a single path, comma-separated paths, or a list of paths."
)
prompt: str = Field(
description="Text description of how to edit the image(s). Be specific about the changes you want."
)
@field_validator('image_paths', mode='before')
@classmethod
def normalize_paths(cls, v):
"""Convert list to comma-separated string for consistent handling."""
if isinstance(v, list):
return ", ".join(str(p) for p in v)
return v
def _upload_image_to_fal(image_path: str) -> str:
"""Upload a local image to fal.ai CDN and return the URL."""
with open(image_path, "rb") as f:
image_data = f.read()
# Determine content type
if image_path.lower().endswith(".png"):
content_type = "image/png"
elif image_path.lower().endswith((".jpg", ".jpeg")):
content_type = "image/jpeg"
elif image_path.lower().endswith(".webp"):
content_type = "image/webp"
else:
content_type = "image/png"
return fal_client.upload(image_data, content_type)
def _edit_single_image(image_path: str, prompt: str) -> str:
"""Edit a single image using fal.ai API."""
# Upload image to fal.ai CDN
image_url = _upload_image_to_fal(image_path)
# API expects image_urls as a list (matching smolagents implementation)
args = {
"image_urls": [image_url],
"prompt": prompt,
"width": 1024,
"height": 1024,
}
result = fal_client.submit(EDIT_MODEL, arguments=args).get()
if not result or "images" not in result or not result["images"]:
raise ValueError("No edited image returned by fal.ai")
output_url = result["images"][0]["url"]
# Download the edited image
response = requests.get(output_url, timeout=60)
response.raise_for_status()
temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
temp_file.write(response.content)
temp_file.close()
return temp_file.name
@tool(args_schema=ImageEditInput)
def edit_images(image_paths: str, prompt: str) -> str:
"""Edit existing images based on a text prompt.
Use this tool when the user wants to modify, transform, or change existing images.
The user must provide the file path(s) of images to edit.
Args:
image_paths: Comma-separated file paths of images to edit
prompt: Description of how to edit the image(s)
Returns:
String containing the file paths of edited images, separated by newlines
"""
# Parse paths
paths = [p.strip() for p in image_paths.split(",") if p.strip()]
# Debug logging
debug_info = {
"image_paths": image_paths,
"parsed_paths": paths,
"prompt": prompt[:100] + "..." if len(prompt) > 100 else prompt,
"num_images": len(paths),
}
print(f"\n{'='*80}")
print(f"DEBUG - Image Edit Tool Called:")
print(json.dumps(debug_info, indent=2))
print(f"{'='*80}\n")
if not paths:
print(f"DEBUG - ERROR: No valid image paths provided")
return "Error: No valid image paths provided"
# Validate paths exist
valid_paths = []
for path in paths:
if os.path.exists(path):
valid_paths.append(path)
print(f"DEBUG - Valid path: {path}")
else:
print(f"DEBUG - ERROR: File not found: {path}")
return f"Error: Image file not found: {path}"
print(f"DEBUG - {len(valid_paths)} valid paths to edit")
edited_paths = []
errors = []
st = time.time()
if len(valid_paths) == 1:
try:
print(f"DEBUG - Editing single image: {valid_paths[0]}")
edited_path = _edit_single_image(valid_paths[0], prompt)
edited_paths.append(edited_path)
print(f"DEBUG - Single image edited: {edited_path}")
except Exception as e:
print(f"DEBUG - Single image edit EXCEPTION: {type(e).__name__}: {e}")
errors.append(f"{valid_paths[0]}: {str(e)}")
else:
# Parallel editing for multiple images
print(f"DEBUG - Parallel editing {len(valid_paths)} images...")
with ThreadPoolExecutor(max_workers=len(valid_paths)) as executor:
futures = {
executor.submit(_edit_single_image, path, prompt): path
for path in valid_paths
}
for future in as_completed(futures):
original_path = futures[future]
try:
edited_path = future.result()
edited_paths.append(edited_path)
print(f"DEBUG - Edited: {original_path} -> {edited_path}")
except Exception as e:
print(f"DEBUG - Edit EXCEPTION for {original_path}: {type(e).__name__}: {e}")
errors.append(f"{original_path}: {str(e)}")
et = time.time()
print(f"DEBUG - Image editing completed in {et - st:.2f}s")
if not edited_paths:
print(f"DEBUG - All edits failed: {errors}")
return f"Failed to edit images: {'; '.join(errors)}"
print(f"DEBUG - Successfully edited {len(edited_paths)} image(s)")
for path in edited_paths:
print(f"DEBUG - Edited image path: {path}")
result = f"Successfully edited {len(edited_paths)} image(s):\n"
for path in edited_paths:
result += f"- {path}\n"
if errors:
result += f"\nSome edits failed: {'; '.join(errors)}"
print(f"DEBUG - Image edit result:\n{result}")
return result