Spaces:
Paused
Paused
| # app_wan.py | |
| import os | |
| # PyTorch 2.8 (temporary hack) — manter conforme app original | |
| #os.system('pip install --upgrade --pre --extra-index-url https://download.pytorch.org/whl/nightly/cu126 "torch<2.9" spaces') | |
| import gradio as gr | |
| import tempfile | |
| import numpy as np | |
| from PIL import Image | |
| from gradio_client import Client, handle_file | |
| # === Constantes (espelhando o app original) === | |
| MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers" | |
| # Dimensões | |
| MAX_DIMENSION = 832 | |
| MIN_DIMENSION = 480 | |
| DIMENSION_MULTIPLE = 16 | |
| SQUARE_SIZE = 480 | |
| # Geração | |
| MAX_SEED = np.iinfo(np.int32).max | |
| FIXED_FPS = 16 | |
| MIN_FRAMES_MODEL = 8 | |
| MAX_FRAMES_MODEL = 81 | |
| MIN_DURATION = round(MIN_FRAMES_MODEL / FIXED_FPS, 1) | |
| MAX_DURATION = round(MAX_FRAMES_MODEL / FIXED_FPS, 1) | |
| default_negative_prompt = ( | |
| "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量," | |
| "JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体," | |
| "手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走,过曝," | |
| ) | |
| # === Importa o serviço de geração (manager) === | |
| from aduc_framework.managers.wan_manager import WanManager | |
| wan_manager = WanManager() | |
| # === Utilidades de UI === | |
| def switch_to_upload_tab(): | |
| # Atualiza o Tabs existente para a aba "Upload" | |
| return gr.Tabs.update(selected="upload_tab") | |
| def generate_end_frame(start_img, gen_prompt, progress=gr.Progress(track_tqdm=True)): | |
| """ | |
| Chama uma API Gradio externa para gerar uma imagem (end frame). | |
| """ | |
| if start_img is None: | |
| raise gr.Error("Please provide a Start Frame first.") | |
| hf_token = os.getenv("HF_TOKEN") | |
| if not hf_token: | |
| raise gr.Error("HF_TOKEN not found in environment variables. Please set it in your Space secrets.") | |
| with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile: | |
| start_img.save(tmpfile.name) | |
| tmp_path = tmpfile.name | |
| progress(0.1, desc="Connecting to image generation API...") | |
| client = Client("multimodalart/nano-banana") | |
| progress(0.5, desc=f"Generating with prompt: '{gen_prompt}'...") | |
| try: | |
| result = client.predict( | |
| prompt=gen_prompt, | |
| images=[{"image": handle_file(tmp_path)}], | |
| manual_token=hf_token, | |
| api_name="/unified_image_generator", | |
| ) | |
| finally: | |
| try: | |
| os.remove(tmp_path) | |
| except: | |
| pass | |
| progress(1.0, desc="Done!") | |
| return result | |
| # Wrapper: a UI monta images_condition_items e delega ao serviço | |
| def ui_generate_video( | |
| start_image_pil, | |
| start_frame_text, | |
| handle_image_pil, | |
| handle_frame_text, | |
| handle_peso, | |
| end_image_pil, | |
| end_frame_text, | |
| end_peso, | |
| prompt, | |
| negative_prompt=default_negative_prompt, | |
| duration_seconds=2.1, | |
| steps=8, | |
| guidance_scale=1.0, | |
| guidance_scale_2=1.0, | |
| seed=42, | |
| randomize_seed=False, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| # Conversões simples | |
| def to_int_safe(v, default=0): | |
| try: | |
| return int(v) | |
| except: | |
| return default | |
| def to_float_safe(v, default=1.0): | |
| try: | |
| return float(v) | |
| except: | |
| return default | |
| # Peso da imagem inicial é fixo 1.0 | |
| start_item = [start_image_pil, to_int_safe(start_frame_text, 0), 1.0] | |
| # Handle é opcional: se não houver imagem, não enviar item | |
| items = [start_item] | |
| if handle_image_pil is not None: | |
| items.append([handle_image_pil, to_int_safe(handle_frame_text, 4), to_float_safe(handle_peso, 1.0)]) | |
| # Último item (end) sempre presente | |
| items.append([end_image_pil, to_int_safe(end_frame_text, MAX_FRAMES_MODEL - 1), to_float_safe(end_peso, 1.0)]) | |
| return wan_manager.generate_video_from_conditions( | |
| images_condition_items=items, | |
| prompt=prompt, | |
| negative_prompt=negative_prompt, | |
| duration_seconds=float(duration_seconds), | |
| steps=int(steps), | |
| guidance_scale=float(guidance_scale), | |
| guidance_scale_2=float(guidance_scale_2), | |
| seed=int(seed), | |
| randomize_seed=bool(randomize_seed), | |
| ) | |
| # === UI Gradio === | |
| css = ''' | |
| .fillable{max-width: 1100px !important} | |
| .dark .progress-text {color: white} | |
| #general_items{margin-top: 2em} | |
| #group_all{overflow:visible} | |
| #group_all .styler{overflow:visible} | |
| #group_tabs .tabitem{padding: 0} | |
| .tab-wrapper{margin-top: -33px;z-index: 999;position: absolute;width: 100%;background-color: var(--block-background-fill);padding: 0;} | |
| #component-9-button{width: 50%;justify-content: center} | |
| #component-11-button{width: 50%;justify-content: center} | |
| #or_item{text-align: center; padding-top: 1em; padding-bottom: 1em; font-size: 1.1em;margin-left: .5em;margin-right: .5em;width: calc(100% - 1em)} | |
| #fivesec{margin-top: 5em;margin-left: .5em;margin-right: .5em;width: calc(100% - 1em)} | |
| ''' | |
| with gr.Blocks(theme=gr.themes.Citrus(), css=css) as app: | |
| gr.Markdown("# Wan 2.2 Aduca-sdr") | |
| with gr.Row(elem_id="general_items"): | |
| with gr.Column(): | |
| with gr.Group(elem_id="group_all"): | |
| with gr.Row(): | |
| # Coluna: Start (peso fixo 1.0) | |
| with gr.Column(): | |
| start_image = gr.Image(type="pil", label="Start Frame", sources=["upload", "clipboard"]) | |
| start_frame_tb = gr.Textbox(label="Start Frame (int)", value="0") | |
| # Coluna: Handle (opcional) | |
| with gr.Column(): | |
| handle_image = gr.Image(type="pil", label="Handle Image", sources=["upload", "clipboard"]) | |
| handle_frame_tb = gr.Textbox(label="Handle Frame (int)", value="4") | |
| handle_peso_sl = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, value=1.0, label="Handle Peso") | |
| # Coluna: End (Upload/Generate) | |
| with gr.Tabs(elem_id="group_tabs") as tabs: | |
| with gr.TabItem("Upload", id="upload_tab"): | |
| with gr.Column(): | |
| end_image = gr.Image(type="pil", label="End Frame", sources=["upload", "clipboard"]) | |
| end_frame_tb = gr.Textbox(label="End Frame (int)", value=str(MAX_FRAMES_MODEL - 1)) | |
| end_peso_sl = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, value=1.0, label="End Peso") | |
| with gr.TabItem("Generate", id="generate_tab"): | |
| generate_5seconds = gr.Button("Generate scene 5 seconds in the future", elem_id="fivesec") | |
| gr.Markdown( | |
| "Generate a custom end-frame with an edit model like Nano Banana or Qwen Image Edit", | |
| elem_id="or_item", | |
| ) | |
| prompt = gr.Textbox(label="Prompt", info="Describe the transition between the two images") | |
| with gr.Accordion("Advanced Settings", open=False): | |
| duration_seconds_input = gr.Slider( | |
| minimum=MIN_DURATION, | |
| maximum=MAX_DURATION, | |
| step=0.1, | |
| value=2.1, | |
| label="Video Duration (seconds)", | |
| info=f"Clamped to model's {MIN_FRAMES_MODEL}-{MAX_FRAMES_MODEL} frames at {FIXED_FPS}fps.", | |
| ) | |
| negative_prompt_input = gr.Textbox(label="Negative Prompt", value=default_negative_prompt, lines=3) | |
| steps_slider = gr.Slider(minimum=1, maximum=30, step=1, value=8, label="Inference Steps") | |
| guidance_scale_input = gr.Slider( | |
| minimum=0.0, maximum=10.0, step=0.5, value=1.0, label="Guidance Scale - high noise" | |
| ) | |
| guidance_scale_2_input = gr.Slider( | |
| minimum=0.0, maximum=10.0, step=0.5, value=1.0, label="Guidance Scale - low noise" | |
| ) | |
| with gr.Row(): | |
| seed_input = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42) | |
| randomize_seed_checkbox = gr.Checkbox(label="Randomize seed", value=True) | |
| generate_button = gr.Button("Generate Video", variant="primary") | |
| with gr.Column(): | |
| output_video = gr.Video(label="Generated Video", autoplay=True) | |
| # Inputs/outputs para o wrapper | |
| ui_inputs = [ | |
| start_image, start_frame_tb, | |
| handle_image, handle_frame_tb, handle_peso_sl, | |
| end_image, end_frame_tb, end_peso_sl, | |
| prompt, negative_prompt_input, duration_seconds_input, | |
| steps_slider, guidance_scale_input, guidance_scale_2_input, | |
| seed_input, randomize_seed_checkbox, | |
| ] | |
| ui_outputs = [output_video, seed_input] | |
| generate_button.click(fn=ui_generate_video, inputs=ui_inputs, outputs=ui_outputs) | |
| # Cadeia “5 seconds”: alterna aba, gera end frame e dispara render | |
| generate_5seconds.click( | |
| fn=switch_to_upload_tab, | |
| inputs=None, | |
| outputs=[tabs] | |
| ).then( | |
| fn=lambda img: generate_end_frame( | |
| img, | |
| "this image is a still frame from a movie. generate a new frame with what happens on this scene 5 seconds in the future" | |
| ), | |
| inputs=[start_image], | |
| outputs=[end_image] | |
| ).success( | |
| fn=ui_generate_video, | |
| inputs=ui_inputs, | |
| outputs=ui_outputs | |
| ) | |
| if __name__ == "__main__": | |
| os.makedirs("examples", exist_ok=True) | |
| try: | |
| Image.new('RGB', (832, 480), color=(73, 109, 137)).save("examples/frame_1.png") | |
| Image.new('RGB', (832, 480), color=(173, 109, 237)).save("examples/frame_2.png") | |
| Image.new('RGB', (832, 480), color=(255, 255, 0)).save("examples/frame_3.png") | |
| except: | |
| pass | |
| app.launch(server_name="0.0.0.0", server_port=7860, show_error=True) | |