Skip to content
Open
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
23 changes: 16 additions & 7 deletions bump_and_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
set -e
set -x

# Conditionally create virtual environment if it doesn't exist
if [ ! -d ".venv" ]; then
echo "Creating virtual environment..."
uv venv
fi

# Ensure dependencies are installed
echo "Syncing dependencies..."
uv sync --all-extras
uv sync --all-extras --dev

# Run tests with pytest
echo "Running tests with pytest..."
Expand All @@ -25,7 +31,7 @@ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
# Remove any rc suffix from PATCH if it exists
PATCH_NUM=$(echo $PATCH | sed 's/-rc[0-9]*//')

if [ "$BRANCH" = "master" ] || [ "$BRANCH" = "main" ]; then
if [ "$BRANCH" = "master" ]; then
# On main branch - bump patch version
if [[ $PATCH == *"-rc"* ]]; then
NEW_VERSION="$MAJOR.$MINOR.$PATCH_NUM"
Expand All @@ -49,8 +55,14 @@ else
fi
fi

# Update version in pyproject.toml
sed -i '' "s/^version = ".*"/version = \"$NEW_VERSION\"/" pyproject.toml
# Update version in pyproject.toml (platform-agnostic)
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS requires empty string after -i
sed -i '' "s/^version = ".*"/version = \"$NEW_VERSION\"/" pyproject.toml
else
# Linux and other Unix-like systems
sed -i "s/^version = ".*"/version = \"$NEW_VERSION\"/" pyproject.toml
fi

echo "Version bumped from $CURRENT_VERSION to $NEW_VERSION"

Expand Down Expand Up @@ -91,6 +103,3 @@ if [ "$1" = "release" ]; then
else
echo "Skipping release steps"
fi



1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies = [
"openai>=1.75.0",
"perplexityai>=0.22.0",
"pillow>=12.1.0",
"prefect>=3.6.15",
"pyairtable>=3.1.1",
"pytest-asyncio>=1.1.0",
"pytest>=8.3.5",
Expand Down
190 changes: 124 additions & 66 deletions src/universal_mcp/applications/google_gemini/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,18 +273,17 @@ async def generate_video(
"""
client = await self.get_genai_client()

config = {
"aspectRatio": aspect_ratio,
"resolution": resolution,
"durationSeconds": duration_seconds,
}
if negative_prompt:
config["negativePrompt"] = negative_prompt
config = types.GenerateVideosConfig(
aspect_ratio=aspect_ratio,
resolution=resolution,
duration_seconds=duration_seconds,
negative_prompt=negative_prompt if negative_prompt else None
)

response = client.models.generate_videos(
model=model,
prompt=prompt,
config=types.GenerateVideosConfig(**config),
config=config,
)

return {
Expand Down Expand Up @@ -337,28 +336,44 @@ async def generate_video_from_image(
"""
client = await self.get_genai_client()

# Load the image
# Load and encode the image
if image_url.startswith(("http://", "https://")):
import requests
response = requests.get(image_url)
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
image_bytes = response.content
else:
with open(image_url, "rb") as f:
image_bytes = f.read()

# Determine MIME type
if image_url.lower().endswith(".png"):
mime_type = "image/png"
elif image_url.lower().endswith((".jpg", ".jpeg")):
mime_type = "image/jpeg"
elif image_url.lower().endswith(".webp"):
mime_type = "image/webp"
else:
image = Image.open(image_url)
mime_type = "image/png" # default

config = {
"aspectRatio": aspect_ratio,
"resolution": resolution,
"durationSeconds": duration_seconds,
}
if negative_prompt:
config["negativePrompt"] = negative_prompt
# Create types.Image with base64 encoding
image = types.Image(
image_bytes=image_bytes,
mime_type=mime_type
)

config = types.GenerateVideosConfig(
aspect_ratio=aspect_ratio,
resolution=resolution,
duration_seconds=duration_seconds,
negative_prompt=negative_prompt if negative_prompt else None
)

response = client.models.generate_videos(
model=model,
prompt=prompt,
image=image,
config=types.GenerateVideosConfig(**config),
config=config,
)

return {
Expand Down Expand Up @@ -413,38 +428,48 @@ async def generate_video_with_frames(
"""
client = await self.get_genai_client()

# Load first frame
if first_frame_url.startswith(("http://", "https://")):
import requests
response = requests.get(first_frame_url)
response.raise_for_status()
first_frame = Image.open(io.BytesIO(response.content))
else:
first_frame = Image.open(first_frame_url)
# Helper to load and convert image
def load_image_as_types_image(image_url: str) -> types.Image:
if image_url.startswith(("http://", "https://")):
import requests
response = requests.get(image_url)
response.raise_for_status()
image_bytes = response.content
else:
with open(image_url, "rb") as f:
image_bytes = f.read()

# Determine MIME type
if image_url.lower().endswith(".png"):
mime_type = "image/png"
elif image_url.lower().endswith((".jpg", ".jpeg")):
mime_type = "image/jpeg"
elif image_url.lower().endswith(".webp"):
mime_type = "image/webp"
else:
mime_type = "image/png"

# Load last frame
if last_frame_url.startswith(("http://", "https://")):
import requests
response = requests.get(last_frame_url)
response.raise_for_status()
last_frame = Image.open(io.BytesIO(response.content))
else:
last_frame = Image.open(last_frame_url)
return types.Image(
image_bytes=image_bytes,
mime_type=mime_type
)

config = {
"aspectRatio": aspect_ratio,
"resolution": resolution,
"durationSeconds": duration_seconds,
"lastFrame": last_frame,
}
if negative_prompt:
config["negativePrompt"] = negative_prompt
# Load both frames as types.Image
first_frame = load_image_as_types_image(first_frame_url)
last_frame = load_image_as_types_image(last_frame_url)

# Note: When using last_frame for interpolation, other config parameters
# (aspect_ratio, resolution, duration_seconds) are not supported
config = types.GenerateVideosConfig(
last_frame=last_frame,
negative_prompt=negative_prompt if negative_prompt else None
)

response = client.models.generate_videos(
model=model,
prompt=prompt,
image=first_frame,
config=types.GenerateVideosConfig(**config),
config=config,
)

return {
Expand Down Expand Up @@ -502,31 +527,49 @@ async def generate_video_with_reference_images(

client = await self.get_genai_client()

# Load reference images
reference_images = []
for image_url in reference_image_urls:
# Helper function to load image and convert to types.Image
def load_and_encode_image(image_url: str) -> types.Image:
if image_url.startswith(("http://", "https://")):
import requests
response = requests.get(image_url)
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
image_bytes = response.content
else:
image = Image.open(image_url)
reference_images.append(image)

config = {
"aspectRatio": aspect_ratio,
"resolution": resolution,
"durationSeconds": duration_seconds,
"referenceImages": reference_images,
}
if negative_prompt:
config["negativePrompt"] = negative_prompt
with open(image_url, "rb") as f:
image_bytes = f.read()

# Determine MIME type
if image_url.lower().endswith(".png"):
mime_type = "image/png"
elif image_url.lower().endswith((".jpg", ".jpeg")):
mime_type = "image/jpeg"
elif image_url.lower().endswith(".webp"):
mime_type = "image/webp"
else:
mime_type = "image/png" # default

return types.Image(
image_bytes=image_bytes,
mime_type=mime_type
)

# Load reference images
reference_images = []
for image_url in reference_image_urls:
reference_images.append(load_and_encode_image(image_url))

config = types.GenerateVideosConfig(
aspect_ratio=aspect_ratio,
resolution=resolution,
duration_seconds=duration_seconds,
reference_images=reference_images,
negative_prompt=negative_prompt if negative_prompt else None
)

response = client.models.generate_videos(
model=model,
prompt=prompt,
config=types.GenerateVideosConfig(**config),
config=config,
)

return {
Expand Down Expand Up @@ -592,14 +635,15 @@ async def extend_video(
# Create video object
video = types.Video(data=video_data)

config = {"video": video}
if negative_prompt:
config["negativePrompt"] = negative_prompt
config = types.GenerateVideosConfig(
video=video,
negative_prompt=negative_prompt if negative_prompt else None
)

response = client.models.generate_videos(
model=model,
prompt=prompt,
config=types.GenerateVideosConfig(**config),
config=config,
)

return {
Expand Down Expand Up @@ -681,6 +725,17 @@ async def check_video_operation(
# Handle the actual response format from the API
if "generateVideoResponse" in response_data:
generate_video_response = response_data["generateVideoResponse"]

# Check if content was filtered
if "raiMediaFilteredCount" in generate_video_response and generate_video_response["raiMediaFilteredCount"] > 0:
filter_reasons = generate_video_response.get("raiMediaFilteredReasons", ["Content filtered"])
return {
"done": True,
"status": "FAILED",
"error": "; ".join(filter_reasons),
"message": f"Video generation blocked by content filter: {'; '.join(filter_reasons)}",
}

if "generatedSamples" in generate_video_response and len(generate_video_response["generatedSamples"]) > 0:
video_info = generate_video_response["generatedSamples"][0]
video_data_obj = video_info.get("video", {})
Expand Down Expand Up @@ -714,11 +769,14 @@ async def check_video_operation(
"message": "Video URI not found in response.",
}

# Unexpected state
# Unexpected state - log the actual response for debugging
import json
response_json = json.dumps(operation_data, indent=2)
return {
"done": True,
"status": "UNKNOWN",
"message": "Operation completed but response format was unexpected.",
"message": f"Operation completed but response format was unexpected. Response: {response_json[:500]}...",
"raw_response": operation_data
}

def list_tools(self):
Expand Down
Loading