"""Hugging Face Spaces CPU-only image studio.""" import gc import os import random import tempfile from pathlib import Path os.environ['CUDA_VISIBLE_DEVICES'] = '' os.environ['HF_HUB_DISABLE_TELEMETRY'] = '1' import gradio as gr import numpy as np import torch from PIL import Image # CPU-compatible Diffusers backend. WeeLLM itself is GPU-oriented. MODELS = { 'Tiny Stable Diffusion (fast test, low quality)': 'diffusers/tiny-stable-diffusion-torch', 'Stable Diffusion 1.5 (slow, higher quality)': 'stable-diffusion-v1-5/stable-diffusion-v1-5', } pipe = None loaded_id = None def load_model(model_name, progress): global pipe, loaded_id model_id = MODELS[model_name] if pipe is not None and loaded_id == model_id: return pipe progress(0.03, desc='Loading model (first run downloads weights)') pipe = None loaded_id = None gc.collect() from diffusers import StableDiffusionPipeline new_pipe = StableDiffusionPipeline.from_pretrained( model_id, torch_dtype=torch.float32, safety_checker=None, requires_safety_checker=False, ) new_pipe = new_pipe.to('cpu') new_pipe.enable_attention_slicing() pipe = new_pipe loaded_id = model_id return pipe def render_image(model_name, prompt, negative, width, height, steps, guidance, seed, progress): if not prompt or not prompt.strip(): raise gr.Error('Please enter a prompt.') seed = int(seed) if seed is not None and int(seed) >= 0 else random.randint(0, 2**31 - 1) model = load_model(model_name, progress) progress(0.2, desc='Generating image on CPU; this may take several minutes') with torch.inference_mode(): result = model( prompt=prompt.strip(), negative_prompt=negative or '', width=int(width), height=int(height), num_inference_steps=int(steps), guidance_scale=float(guidance), generator=torch.Generator(device='cpu').manual_seed(seed), ) return result.images[0], seed def generate_image(model_name, prompt, negative, width, height, steps, guidance, seed, progress=gr.Progress()): try: img, used_seed = render_image(model_name, prompt, negative, width, height, steps, guidance, seed, progress) progress(1, desc='Done') return img, f'Image ready · seed {used_seed}' except Exception as exc: raise gr.Error(f'{type(exc).__name__}: {exc}') from exc def animate_image(img, seconds, fps, zoom): import imageio.v2 as imageio if img is None: raise gr.Error('Upload an image or generate one first.') img = Image.fromarray(np.asarray(img.convert('RGB'))) width, height = img.size frames = max(2, round(float(seconds) * int(fps))) output = Path(tempfile.mkdtemp(prefix='weellm_studio_')) / 'animation.mp4' try: writer = imageio.get_writer(str(output), fps=int(fps), codec='libx264', quality=7, macro_block_size=16) with writer: for index in range(frames): t = index / max(1, frames - 1) scale = 1 + (float(zoom) - 1) * t cw, ch = max(1, int(width / scale)), max(1, int(height / scale)) x0 = (width - cw) // 2 y0 = (height - ch) // 2 frame = img.crop((x0, y0, x0 + cw, y0 + ch)).resize((width, height), Image.Resampling.LANCZOS) writer.append_data(np.asarray(frame)) except Exception as exc: raise gr.Error(f'Video encoding failed: {exc}') from exc return str(output), 'MP4 created using camera motion (not AI-generated video).' def generate_video(model_name, prompt, negative, width, height, steps, guidance, seed, seconds, fps, zoom, progress=gr.Progress()): try: img, used_seed = render_image(model_name, prompt, negative, width, height, steps, guidance, seed, progress) progress(0.85, desc='Encoding MP4') path, status = animate_image(img, seconds, fps, zoom) progress(1, desc='Done') return img, path, f'{status} · seed {used_seed}' except Exception as exc: raise gr.Error(f'{type(exc).__name__}: {exc}') from exc CSS = ''' .gradio-container {max-width: 1220px !important; margin: auto !important;} .hero {padding: 18px 6px 24px;} .hero h1 {font-size: 2.4rem; font-weight: 800; letter-spacing: -.04em; margin-bottom: 6px;} .hero p {opacity: .78; font-size: 1.05rem;} ''' with gr.Blocks(title='WeeLLM CPU Studio', theme=gr.themes.Soft(primary_hue='indigo'), css=CSS) as demo: gr.HTML('

✦ WeeLLM CPU Studio

Image creation and animated video · Hugging Face CPU Spaces · No GPU required

') gr.Markdown('**Backend:** CPU-compatible Diffusers. This deployment does **not** run WeeLLM inference, which may require CUDA. The video feature animates a still image rather than generating motion with a video diffusion model.') with gr.Tabs(): with gr.Tab('✦ Text to image'): with gr.Row(): with gr.Column(scale=2): model = gr.Dropdown(list(MODELS), value=list(MODELS)[0], label='Image model') prompt = gr.Textbox(lines=4, label='Prompt', placeholder='A cozy cabin in a snowy forest, cinematic lighting...') negative = gr.Textbox(label='Negative prompt', value='blurry, distorted, low quality') with gr.Accordion('Settings', open=True): with gr.Row(): width = gr.Dropdown([256, 384, 512], value=256, label='Width') height = gr.Dropdown([256, 384, 512], value=256, label='Height') steps = gr.Slider(1, 30, value=8, step=1, label='Inference steps') guidance = gr.Slider(0, 12, value=7.5, step=0.5, label='Guidance scale') seed = gr.Number(value=-1, precision=0, label='Seed (-1 = random)') image_button = gr.Button('Generate image', variant='primary', size='lg') with gr.Column(scale=3): image_output = gr.Image(label='Generated image', type='pil', interactive=False) image_status = gr.Textbox(label='Status', interactive=False) image_button.click(generate_image, [model, prompt, negative, width, height, steps, guidance, seed], [image_output, image_status]) with gr.Tab('▶ Text to animated video'): with gr.Row(): with gr.Column(scale=2): video_model = gr.Dropdown(list(MODELS), value=list(MODELS)[0], label='Image model') video_prompt = gr.Textbox(lines=4, label='Prompt') video_negative = gr.Textbox(label='Negative prompt', value='blurry, distorted, low quality') with gr.Row(): video_width = gr.Dropdown([256, 384, 512], value=256, label='Width') video_height = gr.Dropdown([256, 384, 512], value=256, label='Height') video_steps = gr.Slider(1, 30, value=8, step=1, label='Steps') video_guidance = gr.Slider(0, 12, value=7.5, step=0.5, label='Guidance') video_seed = gr.Number(value=-1, precision=0, label='Seed (-1 = random)') with gr.Row(): seconds = gr.Slider(1, 6, value=3, step=1, label='Duration (seconds)') fps = gr.Slider(4, 16, value=8, step=1, label='FPS') zoom = gr.Slider(1.0, 1.35, value=1.12, step=0.01, label='Ending zoom') video_button = gr.Button('Generate animated MP4', variant='primary', size='lg') with gr.Column(scale=3): video_still = gr.Image(label='Source image', type='pil') video_output = gr.Video(label='Animated MP4') video_status = gr.Textbox(label='Status', interactive=False) video_button.click(generate_video, [video_model, video_prompt, video_negative, video_width, video_height, video_steps, video_guidance, video_seed, seconds, fps, zoom], [video_still, video_output, video_status]) with gr.Tab('↗ Animate an uploaded image'): upload = gr.Image(label='Upload image', type='pil') with gr.Row(): upload_seconds = gr.Slider(1, 6, value=3, step=1, label='Duration (seconds)') upload_fps = gr.Slider(4, 16, value=8, step=1, label='FPS') upload_zoom = gr.Slider(1.0, 1.35, value=1.12, step=0.01, label='Ending zoom') animate_button = gr.Button('Animate image', variant='primary') upload_video = gr.Video(label='MP4') upload_status = gr.Textbox(label='Status', interactive=False) animate_button.click(animate_image, [upload, upload_seconds, upload_fps, upload_zoom], [upload_video, upload_status]) gr.Markdown('**CPU tip:** Start with Tiny Stable Diffusion at 256×256 and 8 steps. Stable Diffusion 1.5 is significantly slower and may exceed the memory available on a free CPU Space.') if __name__ == '__main__': demo.queue(default_concurrency_limit=1, max_size=5).launch(server_name='0.0.0.0', server_port=int(os.environ.get('PORT', '7860')))