import os os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") import spaces # MUST come before any torch/CUDA-touching import import torch import torchvision import torchvision.transforms.functional as _F import sys import types # Compatibility shim: torchvision >= 0.21 removed functional_tensor, which pytorchvideo still imports if not hasattr(torchvision.transforms, 'functional_tensor'): _mod = types.ModuleType('torchvision.transforms.functional_tensor') for _name in dir(_F): setattr(_mod, _name, getattr(_F, _name)) sys.modules['torchvision.transforms.functional_tensor'] = _mod torchvision.transforms.functional_tensor = _mod import gradio as gr import numpy as np import tempfile import time from pathlib import Path from huggingface_hub import snapshot_download, hf_hub_download from omegaconf import OmegaConf import copy import einops # Patch config module to use HF Hub paths before any model imports import config as _config_module # Download pretrained models from HF Hub and patch config paths _qwen3_06b_path = snapshot_download("Qwen/Qwen3-0.6B") _clip_path = snapshot_download("openai/clip-vit-base-patch16") _hubert_path = snapshot_download("TencentGameMate/chinese-hubert-base") _config_module.PATH_TO_LLM['Qwen3_0.6B'] = _qwen3_06b_path _config_module.PATH_TO_VISUAL['CLIP_VIT_BASE'] = _clip_path _config_module.PATH_TO_AUDIO['HUBERT_BASE'] = _hubert_path # Now import the model code (after config is patched) from my_affectgpt.common.registry import registry from my_affectgpt.models.affectgpt import AffectGPT from my_affectgpt.models.encoder import * # registers encoders from my_affectgpt.conversation.conversation_video import Chat from my_affectgpt.processors.video_processor import ( AlproVideoEvalProcessor, load_video, ) from my_affectgpt.processors.blip_processors import Blip2ImageEvalProcessor from my_affectgpt.models.ImageBind.data import load_audio, transform_audio # ---- Model config matching stage1-swdh-qwen3-0.6b/config.yaml ---- MODEL_CFG = OmegaConf.create({ "arch": "affectgpt", "model_type": "pretrain_vicuna", "frozen_video_proj": False, "frozen_video_Qformer": False, "frozen_audio_Qformer": False, "frozen_audio_proj": False, "frozen_multi_Qformer": False, "frozen_multi_llama_proj": False, "frozen_llm": False, "multi_fusion_type": "attention", "video_fusion_type": "attention", "audio_fusion_type": "attention", "image_fusion_type": "mean", "ckpt": "", "ckpt_2": "", "llama_model": "Qwen3_0.6B", "acoustic_encoder": "HUBERT_BASE", "visual_encoder": "CLIP_VIT_BASE", "num_audio_query_token": 1, "num_video_query_token": 1, "num_multi_query_token": 1, "num_image_query_token": 1, "max_length": 1024, "lora_r": 16, "vis_processor": { "train": { "name": "alpro_video_eval", "n_frms": 8, "image_size": 224, } }, }) # Download the Light-MER Stage 1 checkpoint ckpt_path = hf_hub_download( "kevin233333/Light-MER", "stage1-swdh-qwen3-0.6b/checkpoint_000060_loss_1.291.pth", ) MODEL_CFG.ckpt_3 = ckpt_path print("Loading Light-MER model...") model_cls = registry.get_model_class(MODEL_CFG.arch) model = model_cls.from_config(MODEL_CFG) model = model.to("cuda").eval() chat = Chat(model, MODEL_CFG, device="cuda") # Video processor for inference vis_processor = AlproVideoEvalProcessor( image_size=224, n_frms=8, mean=(0.48145466, 0.4578275, 0.40821073), std=(0.26862954, 0.26130258, 0.27577711), ) # Default prompt (matching the dataset's description question) DEFAULT_QUESTION = "Please infer the person's emotional state and provide your reasoning process." FACE_OR_FRAME = "multiface_audio_face_text" @spaces.GPU(duration=30) def recognize_emotion(video_path, subtitle, question, audio_path=None, max_new_tokens=512, temperature=1.0, top_p=0.9, do_sample=True, progress=gr.Progress(track_tqdm=True)): """Recognize emotions from video/audio using Light-MER. Args: video_path: Path to the input video file. audio_path: Path to the input audio file (optional if video has audio). subtitle: Subtitle text for the video (optional). question: The question to ask about the emotional state. max_new_tokens: Maximum number of tokens to generate. temperature: Sampling temperature. top_p: Nucleus sampling probability. do_sample: Whether to use sampling for generation. """ if video_path is None and (audio_path is None or audio_path == ""): return "Please provide a video or audio file." start_time = time.time() # Build sample_data like the inference script does sample_data = { 'frame': None, 'raw_frame': None, 'face': None, 'raw_face': None, 'audio': None, 'raw_audio': None, 'image': None, 'raw_image': None, } # Process video -> frames (and faces from frames) if video_path is not None: try: raw_frame, msg = load_video( video_path=video_path, n_frms=8, height=224, width=224, sampling="uniform", return_msg=True, ) frame = vis_processor.transform(raw_frame) sample_data['frame'] = frame sample_data['raw_frame'] = raw_frame # Use frames as "faces" (since we don't have OpenFace preprocessed faces) sample_data['face'] = frame sample_data['raw_face'] = raw_frame except Exception as e: print(f"Video loading error: {e}") # Process audio if audio_path is not None and audio_path != "": try: raw_audio = load_audio([audio_path], "cpu", clips_per_video=8)[0] audio = transform_audio(raw_audio, "cpu") sample_data['audio'] = audio sample_data['raw_audio'] = raw_audio except Exception as e: print(f"Audio loading error: {e}") # If we have video but no separate audio, try to extract audio from video if video_path is not None and sample_data['audio'] is None: try: raw_audio = load_audio([video_path], "cpu", clips_per_video=8)[0] audio = transform_audio(raw_audio, "cpu") sample_data['audio'] = audio sample_data['raw_audio'] = raw_audio except Exception as e: print(f"Audio extraction from video failed: {e}") if subtitle is None or subtitle.strip() == "": subtitle = "" if question is None or question.strip() == "": question = DEFAULT_QUESTION # Encode multimodal features audio_hiddens, audio_llms = chat.postprocess_audio(sample_data) frame_hiddens, frame_llms = chat.postprocess_frame(sample_data) face_hiddens, face_llms = chat.postprocess_face(sample_data) _, image_llms = chat.postprocess_image(sample_data) multi_llms = None if face_hiddens is not None and audio_hiddens is not None: _, multi_llms = chat.postprocess_multi(face_hiddens, audio_hiddens) img_list = { 'audio': audio_llms, 'frame': frame_llms, 'face': face_llms, 'image': image_llms, 'multi': multi_llms, } # Build prompt dynamically based on available features prompt_parts = ["###Human: "] if multi_llms is not None: prompt_parts.append("The audio and video merged info is: . ") if audio_llms is not None: prompt_parts.append("The audio content is as follows: . ") if face_llms is not None: prompt_parts.append("Meanwhile, we uniformly sample raw frames from the video and extract faces from these frames: . ") elif frame_llms is not None: prompt_parts.append("Meanwhile, we uniformly sample raw frames from the video: . ") # Always use the exact trained framing from the reference # implementation (base_dataset.get_prompt_for_multimodal). During training the # subtitle can be empty, in which case the reference simply passes an empty # string through the same template (). Any deviation from # this framing (e.g. injecting "there is no subtitle / do not invent" style # instructions) pushes the model off-distribution and produces garbled, # self-contradictory hedging. prompt_parts.append(f"The subtitle of this video is: {subtitle}. ") prompt_parts.append(f"Now, please answer my question based on all the provided information. {question} ###Assistant: ") prompt = "".join(prompt_parts) # Run inference response = chat.answer_sample( prompt=prompt, img_list=img_list, num_beams=1, temperature=temperature, do_sample=do_sample, top_p=top_p, max_new_tokens=max_new_tokens, max_length=2000, ) elapsed = time.time() - start_time result = f"{response}\n\n---\nInference time: {elapsed:.2f}s" return result # ---- Gradio UI ---- CSS = """ #col-container { max-width: 1100px; margin: 0 auto; } .dark .gradio-container { color: var(--body-text-color); } """ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo: gr.Markdown("# Light-MER: Lightweight Multimodal Emotion Recognition") gr.Markdown( "A sub-1B parameter multimodal emotion recognition model that processes " "video, audio, and text to recognize emotions. Upload a video or audio file " "and ask a question about the emotional state." ) gr.Markdown( "Paper: [Do We Really Need Multimodal Emotion Language Models Larger Than 1B Parameters?](https://arxiv.org/abs/2607.12787) | " "[GitHub](https://github.com/GAIR-Lab/Light-MER) | " "[Model](https://huggingface.co/kevin233333/Light-MER)" ) with gr.Row(): with gr.Column(scale=2): video_input = gr.Video(label="Video Input") audio_input = gr.Audio(label="Audio Input (optional)", type="filepath") subtitle_input = gr.Textbox( label="Subtitle (optional)", placeholder="Enter subtitle text if available...", lines=2, ) question_input = gr.Textbox( label="Question", value=DEFAULT_QUESTION, lines=2, ) run_btn = gr.Button("Recognize Emotion", variant="primary") with gr.Column(scale=3): output_text = gr.Textbox( label="Emotion Recognition Result", lines=15, show_copy_button=True, ) with gr.Accordion("Advanced Settings", open=False): max_tokens = gr.Slider( label="Max New Tokens", minimum=64, maximum=1024, value=512, step=64 ) temp = gr.Slider( label="Temperature", minimum=0.1, maximum=2.0, value=1.0, step=0.1 ) top_p_val = gr.Slider( label="Top P", minimum=0.1, maximum=1.0, value=0.9, step=0.05 ) do_sample_chk = gr.Checkbox(label="Do Sample", value=True) gr.Examples( examples=[ ["examples/man_laughing.mp4", "", "Please infer the person's emotional state and provide your reasoning process."], ["examples/man_sad.mp4", "", "Please infer the person's emotional state and provide your reasoning process."], ["examples/woman_laughing_studio.mp4", "", "Please infer the person's emotional state and provide your reasoning process."], ], inputs=[video_input, subtitle_input, question_input], outputs=output_text, fn=recognize_emotion, cache_examples=False, run_on_click=True, ) run_btn.click( fn=recognize_emotion, inputs=[video_input, subtitle_input, question_input, audio_input, max_tokens, temp, top_p_val, do_sample_chk], outputs=output_text, api_name="recognize_emotion", ) demo.launch()