Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stable diffusion mlx #474

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,5 @@ cython_debug/

**/*.xcodeproj/*
.aider*

exo/tinychat/images/*.png
1 change: 1 addition & 0 deletions build/lib/exo/__init__.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a bulld dir?

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from exo.helpers import DEBUG as DEBUG, DEBUG_DISCOVERY as DEBUG_DISCOVERY, VERSION as VERSION
1 change: 1 addition & 0 deletions build/lib/exo/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from exo.api.chatgpt_api import ChatGPTAPI as ChatGPTAPI
539 changes: 539 additions & 0 deletions build/lib/exo/api/chatgpt_api.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build/lib/exo/apputil/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from exo.apputil.anim import create_animation_mp4
161 changes: 161 additions & 0 deletions build/lib/exo/apputil/anim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import os
import numpy as np
import cv2

def draw_rounded_rectangle(draw, coords, radius, fill):
left, top, right, bottom = coords
diameter = radius * 2
draw.rectangle([left + radius, top, right - radius, bottom], fill=fill)
draw.rectangle([left, top + radius, right, bottom - radius], fill=fill)
draw.pieslice([left, top, left + diameter, top + diameter], 180, 270, fill=fill)
draw.pieslice([right - diameter, top, right, top + diameter], 270, 360, fill=fill)
draw.pieslice([left, bottom - diameter, left + diameter, bottom], 90, 180, fill=fill)
draw.pieslice([right - diameter, bottom - diameter, right, bottom], 0, 90, fill=fill)

def draw_centered_text_rounded(draw, text, font, rect_coords, radius=10, text_color="yellow", bg_color=(43,33,44)):
bbox = font.getbbox(text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
rect_left, rect_top, rect_right, rect_bottom = rect_coords
rect_width = rect_right - rect_left
rect_height = rect_bottom - rect_top
text_x = rect_left + (rect_width - text_width) // 2
text_y = rect_top + (rect_height - text_height) // 2
draw_rounded_rectangle(draw, rect_coords, radius, bg_color)
draw.text((text_x, text_y), text, fill=text_color, font=font)

def draw_left_aligned_text_rounded(draw, text, font, rect_coords, padding_left=20, radius=10, text_color="yellow", bg_color=(43,33,44)):
bbox = font.getbbox(text)
text_height = bbox[3] - bbox[1]
rect_left, rect_top, rect_right, rect_bottom = rect_coords
rect_height = rect_bottom - rect_top
text_y = rect_top + (rect_height - text_height) // 2
text_x = rect_left + padding_left
draw_rounded_rectangle(draw, rect_coords, radius, bg_color)
draw.text((text_x, text_y), text, fill=text_color, font=font)

def draw_right_text_dynamic_width_rounded(draw, text, font, base_coords, padding=20, radius=10, text_color="yellow", bg_color=(43,33,44)):
bbox = font.getbbox(text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
_, rect_top, rect_right, rect_bottom = base_coords
rect_height = rect_bottom - rect_top
new_rect_left = rect_right - (text_width + (padding * 2))
text_y = rect_top + (rect_height - text_height) // 2
text_x = new_rect_left + padding
draw_rounded_rectangle(draw, (new_rect_left, rect_top, rect_right, rect_bottom), radius, bg_color)
draw.text((text_x, text_y), text, fill=text_color, font=font)
return new_rect_left

def draw_progress_bar(draw, progress, coords, color="yellow", bg_color=(70, 70, 70)):
left, top, right, bottom = coords
total_width = right - left
draw.rectangle(coords, fill=bg_color)
progress_width = int(total_width * progress)
if progress_width > 0:
draw.rectangle((left, top, left + progress_width, bottom), fill=color)

def crop_image(image, top_crop=70):
width, height = image.size
return image.crop((0, top_crop, width, height))

def create_animation_mp4(
replacement_image_path,
output_path,
device_name,
prompt_text,
fps=30,
target_size=(512, 512),
target_position=(139, 755),
progress_coords=(139, 1285, 655, 1295),
device_coords=(1240, 370, 1640, 416),
prompt_coords=(332, 1702, 2662, 1745)
):
frames = []
try:
font = ImageFont.truetype("/System/Library/Fonts/SFNSMono.ttf", 20)
promptfont = ImageFont.truetype("/System/Library/Fonts/SFNSMono.ttf", 24)
except:
font = ImageFont.load_default()
promptfont = ImageFont.load_default()

# Process first frame
base_img = Image.open(os.path.join(os.path.dirname(__file__), "baseimages", "image1.png"))
draw = ImageDraw.Draw(base_img)
draw_centered_text_rounded(draw, device_name, font, device_coords)
frames.extend([crop_image(base_img)] * 30) # 1 second at 30fps

# Process second frame with typing animation
base_img2 = Image.open(os.path.join(os.path.dirname(__file__), "baseimages", "image2.png"))
for i in range(len(prompt_text) + 1):
current_frame = base_img2.copy()
draw = ImageDraw.Draw(current_frame)
draw_centered_text_rounded(draw, device_name, font, device_coords)
if i > 0: # Only draw if we have at least one character
draw_left_aligned_text_rounded(draw, prompt_text[:i], promptfont, prompt_coords)
frames.extend([crop_image(current_frame)] * 2) # 2 frames per character for smooth typing

# Hold the complete prompt for a moment
frames.extend([frames[-1]] * 30) # Hold for 1 second

# Create blur sequence
replacement_img = Image.open(replacement_image_path)
base_img = Image.open(os.path.join(os.path.dirname(__file__), "baseimages", "image3.png"))
blur_steps = [int(80 * (1 - i/8)) for i in range(9)]

for i, blur_amount in enumerate(blur_steps):
new_frame = base_img.copy()
draw = ImageDraw.Draw(new_frame)

replacement_copy = replacement_img.copy()
replacement_copy.thumbnail(target_size, Image.Resampling.LANCZOS)
if blur_amount > 0:
replacement_copy = replacement_copy.filter(ImageFilter.GaussianBlur(radius=blur_amount))

mask = replacement_copy.split()[-1] if replacement_copy.mode in ('RGBA', 'LA') else None
new_frame.paste(replacement_copy, target_position, mask)

draw_progress_bar(draw, (i + 1) / 9, progress_coords)
draw_centered_text_rounded(draw, device_name, font, device_coords)
draw_right_text_dynamic_width_rounded(draw, prompt_text, promptfont, (None, 590, 2850, 685), padding=30)

frames.extend([crop_image(new_frame)] * 15) # 0.5 seconds at 30fps

# Create and add final frame (image4)
final_base = Image.open(os.path.join(os.path.dirname(__file__), "baseimages", "image4.png"))
draw = ImageDraw.Draw(final_base)

draw_centered_text_rounded(draw, device_name, font, device_coords)
draw_right_text_dynamic_width_rounded(draw, prompt_text, promptfont, (None, 590, 2850, 685), padding=30)

replacement_copy = replacement_img.copy()
replacement_copy.thumbnail(target_size, Image.Resampling.LANCZOS)
mask = replacement_copy.split()[-1] if replacement_copy.mode in ('RGBA', 'LA') else None
final_base.paste(replacement_copy, target_position, mask)

frames.extend([crop_image(final_base)] * 30) # 1 second at 30fps

# Convert frames to video using H.264 codec
if frames:
first_frame = np.array(frames[0])
height, width = first_frame.shape[:2]
fourcc = cv2.VideoWriter_fourcc(*'avc1')
out = cv2.VideoWriter(
output_path,
fourcc,
fps,
(width, height),
isColor=True
)

if not out.isOpened():
print("Error: VideoWriter failed to open")
return

for frame in frames:
frame_array = cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR)
out.write(frame_array)

out.release()
print(f"Video saved successfully to {output_path}")
Empty file.
61 changes: 61 additions & 0 deletions build/lib/exo/download/download_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from typing import Dict, Callable, Coroutine, Any, Literal
from dataclasses import dataclass
from datetime import timedelta


@dataclass
class RepoFileProgressEvent:
repo_id: str
repo_revision: str
file_path: str
downloaded: int
downloaded_this_session: int
total: int
speed: int
eta: timedelta
status: Literal["not_started", "in_progress", "complete"]

def to_dict(self):
return {
"repo_id": self.repo_id, "repo_revision": self.repo_revision, "file_path": self.file_path, "downloaded": self.downloaded, "downloaded_this_session": self.downloaded_this_session,
"total": self.total, "speed": self.speed, "eta": self.eta.total_seconds(), "status": self.status
}

@classmethod
def from_dict(cls, data):
if 'eta' in data: data['eta'] = timedelta(seconds=data['eta'])
return cls(**data)


@dataclass
class RepoProgressEvent:
repo_id: str
repo_revision: str
completed_files: int
total_files: int
downloaded_bytes: int
downloaded_bytes_this_session: int
total_bytes: int
overall_speed: int
overall_eta: timedelta
file_progress: Dict[str, RepoFileProgressEvent]
status: Literal["not_started", "in_progress", "complete"]

def to_dict(self):
return {
"repo_id": self.repo_id, "repo_revision": self.repo_revision, "completed_files": self.completed_files, "total_files": self.total_files, "downloaded_bytes": self.downloaded_bytes,
"downloaded_bytes_this_session": self.downloaded_bytes_this_session, "total_bytes": self.total_bytes, "overall_speed": self.overall_speed, "overall_eta": self.overall_eta.total_seconds(),
"file_progress": {k: v.to_dict()
for k, v in self.file_progress.items()}, "status": self.status
}

@classmethod
def from_dict(cls, data):
if 'overall_eta' in data: data['overall_eta'] = timedelta(seconds=data['overall_eta'])
if 'file_progress' in data: data['file_progress'] = {k: RepoFileProgressEvent.from_dict(v) for k, v in data['file_progress'].items()}

return cls(**data)


RepoFileProgressCallback = Callable[[RepoFileProgressEvent], Coroutine[Any, Any, None]]
RepoProgressCallback = Callable[[RepoProgressEvent], Coroutine[Any, Any, None]]
Empty file.
Loading