Initial Commit

This commit is contained in:
OmniLottie
2026-03-01 21:36:54 +08:00
commit a386c803e1
199 changed files with 42253 additions and 0 deletions
+383
View File
@@ -0,0 +1,383 @@
<!-- <div align= "center">
<h1> Official repo for OmniSVG</h1>
</div> -->
<h3 align="center"><strong>OmniLottie: Generating Vector Animations via Parameterized Lottie Tokens
</strong></h3>
<div align="center">
<a href='https://arxiv.org/abs/2504.06263'><img src='https://img.shields.io/badge/arXiv-2504.06263-b31b1b.svg'></a> &nbsp;&nbsp;&nbsp;&nbsp;
<a href='https://openvglab.github.io/OmniLottie/'><img src='https://img.shields.io/badge/Project-Page-Green'></a> &nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://huggingface.co/OmniLottie/OmniLottie"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Weights-HF-orange"></a> &nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://huggingface.co/datasets/OmniLottie/MMLottie-2M"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Dataset%20-HF-orange"></a> &nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://huggingface.co/datasets/OmniLottie/MMLottieBench"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Bench-HF-orange"></a> &nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://huggingface.co/spaces/OmniLottie/OmniLottie"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Demo%20-HF-orange"></a> &nbsp;&nbsp;&nbsp;&nbsp;
<a href='https://github.com/OpenVGLab/OmniLottie'><img src='https://img.shields.io/badge/Training-Code-blue?logo=github'></a>
</div>
## 🔥🔥🔥 News !!
- [2026/03/02] 👋 Upload paper and init project. [Read](https://arxiv.org/abs/2504.06263)
- [2026/03/02] 👋 We have released the inference code and model weight. 🤗[Weight](https://huggingface.co/OmniLottie/OmniLottie).
- [2026/03/02] We have released **MMLottieBench** benchmark! Check out [MMLottieBench](https://huggingface.co/datasets/OmniLottie/MMLottieBench).
- [2026/03/02] 👋 Release MMLottie-2M Dataser 🤗[MMLottie-2M Dataset](https://huggingface.co/datasets/OmniLottie/MMLottie-2M).
- [2026/03/02] 👋 We have released the Huggingface Demo. 🤗[Demo](https://huggingface.co/spaces/OmniLottie/OmniLottie).
- [2026/02/21] OmniLottie is accepted to **CVPR 2026**🔥! See you in Denver!
<p align="center">
<img src="assets/OmniLottie-main-demo.gif" alt="Demo GIF" width="720px" />
</p>
## 📑 Open-source Plan
- [x] Project Page & Technical Report
- [x] MMLottie-2M Dataset Release
- [x] Inference Code & Model Weight
- [x] Online Demo (Gradio deployed on Huggingface)
- [x] MMLottieBench Benchmark
- [ ] Training Code
## 1. Introduction
**OmniLottie** is the first family of end-to-end multimodal Lottie generators that leverage pre-trained Vision-Language Models (VLMs), capable of generating complex and detailed Lottie animations from multi-modal instructions including texts, images, and videos. We also introduce MMLottie-2M, a multimodal dataset with two million richly annotated Lottie animations, along with a standardized evaluation protocol for multi-modal vector animation generation tasks.
## 2. Models Downloading
| Model | Download link | Size | Update date |
|-----------------------------|-------------------------------|------------|------|
| OmniLottie(4B) | [Huggingface](https://huggingface.co/OmniLottie/OmniLottie) | 8.46 GB | 2026-03-02 |
## 3. Dependencies and Installation
The dependencies configured according to the following instructions provide an environment equipped for inference
### 3.1 Clone the Repository
```bash
git clone https://github.com/OpenVGLab/OmniLottie
cd OmniLottie
```
### 3.2 Create Conda Environment
Create and activate a new conda environment with Python 3.10:
```bash
conda create -n omnilottie python=3.10
conda activate omnilottie
```
### 3.3 Install Dependencies
#### Python Dependencies
We have tested our environment with CUDA 12.1. You can install CUDA 12.1 by following the [CUDA Toolkit installation guide](https://developer.nvidia.com/cuda-12-1-0-download-archive).
Install PyTorch with CUDA 12.1 support:
```bash
pip install torch==2.3.0+cu121 torchvision==0.18.0+cu121 --index-url https://download.pytorch.org/whl/cu121
```
Install remaining dependencies:
```bash
pip install -r requirements.txt
```
## 4. Inference
| | GPU Memory Usage | Time per 256/512/1024/2048/4096 tokens |
| ------------------------------------------------ | ---------------- | ----------------- |
| OmniLottie | 15.2G | 8.34/16.68/33.38/66.74/133.49 seconds |
<font color="red">**Note: The inference time shown here is measured per OmniLottie Lottie tokens, while the inference time reported in our paper is measured per JSON code tokens for fair comparison with baseline methods.**</font>
### Quick Start
**Download Model Weights**
First, install the Hugging Face CLI tool:
```bash
pip install huggingface-hub
```
**Download the model from Hugging Face:**
```bash
# Download OmniLottie model
huggingface-cli download OmniLottie/OmniLottie --local-dir /PATH/TO/OmniLottie
```
**Try with Example Data**
We provide example prompts, images, and videos in the `example/` directory:
- `example/demo.txt` - 37 text prompts
- `example/demo_images/` - 26 images (with corresponding text descriptions)
- `example/demo_video/` - 30 videos
```bash
# Test with example text prompts
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--batch_text_file example/demo.txt \
--output_dir ./output_demo_text
# Test with example images
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--single_image example/demo_images/00de75e2c031cb3fc3f472e356aba5b6.png \
--output_dir ./output_demo_image
# Test with example videos
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--single_video example/demo_video/02b8ce2014690a9e30dc25da846e8afb.mp4 \
--output_dir ./output_demo_video
```
### Text-to-Lottie Generation
Generate Lottie animations from text descriptions:
**Single prompt:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--single_text "A red ball appearing, bouncing up and down, then fading out, repeating seamlessly" \
--output_dir ./output_text
```
**Batch generation from file:**
```bash
# Create a prompts.txt file with one prompt per line
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--batch_text_file example/demo.txt \
--output_dir ./output_text
```
**Custom generation parameters:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--single_text "a blue bird appearing, pulsing while sliding downward, lingers briefly, then growing back while sliding upward to reset with clear phase changes, repeating seamlessly" \
--use_sampling \
--temperature 0.8 \
--top_p 0.25 \
--top_k 5 \
--repetition_penalty 1.01 \
--output_dir ./output
```
**Generate with Best-of-N selection:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--single_text "a light blue piggy bank with a darker blue outline, with a single light blue coin with a dark blue yen symbol (£) appears above the piggy bank, then starts descending towards the piggy bank's opening" \
--num_candidates 8 \
--output_dir ./output
```
### Text-Image-to-Lottie Generation
Generate Lottie animations from an image:
**Single image:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--single_image /path/to/image.png \
--output_dir ./output_image
```
### Video-to-Lottie Generation
Convert video to Lottie animation:
**Single video:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--single_video /path/to/video.mp4 \
--output_dir ./output_video
```
### Advanced Options
**Specify tokenizer path:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--tokenizer_name /PATH/TO/Qwen2.5-VL-3B-Instruct \
--single_text "Your prompt here" \
--output_dir ./output
```
**Adjust token length:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--maxlen 6072 \
--text_len 512 \
--single_text "Your prompt here" \
--output_dir ./output
```
**Filter by task type (when using MMLottieBench dataset):**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--mmlottie_bench_dir /PATH/TO/mmlottie_bench \
--split real \
--task_filter text \
--output_dir ./output
```
**Process limited samples with shuffling:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--mmlottie_bench_dir /PATH/TO/mmlottie_bench \
--split real \
--max_samples 10 \
--shuffle \
--output_dir ./output
```
### Interactive Demo
We provide an interactive generation interface using Gradio:
- **Local Deployment**
```bash
python app.py
```
- **Online Demo**
Try our live demo on [Hugging Face Spaces](https://huggingface.co/spaces/OmniLottie/OmniLottie)
## 5. Benchmark & Evaluation
We provide **MMLottieBench** for standardized evaluation of Lottie generation models.
### Download MMLottieBench
**Option 1: Using download script:**
```bash
python download_mmlottie_bench.py --output_dir /PATH/TO/mmlottie_bench
```
**Option 2: Using Hugging Face CLI:**
```bash
huggingface-cli download OmniLottie/MMLottieBench --repo-type dataset --local-dir /PATH/TO/mmlottie_bench
```
**Option 3: Automatic download (in code):**
```python
from datasets import load_dataset
dataset = load_dataset("OmniLottie/MMLottieBench")
```
### Benchmark Overview
MMLottieBench contains **900 samples** split into:
- **Real split**: 450 real-world Lottie animations
- **Synthetic split**: 450 synthetically generated samples
Each split contains **3 task types** (150 samples each):
- **Text-to-Lottie**: Generate from text descriptions
- **Text-Image-to-Lottie**: Generate from image + text guidance
- **Video-to-Lottie**: Convert video to Lottie animation
### Run Benchmark Inference
MMLottieBench provides two splits that can be switched using `--split`:
- `--split real` - Test on 450 real-world Lottie animations
- `--split synthetic` - Test on 450 synthetically generated samples
**Test on real split (all tasks):**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--mmlottie_bench_dir /PATH/TO/mmlottie_bench \
--split real \
--output_dir ./benchmark_results_real
```
**Test on synthetic split (all tasks):**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--mmlottie_bench_dir /PATH/TO/mmlottie_bench \
--split synthetic \
--output_dir ./benchmark_results_synthetic
```
**Test specific task type on real split:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--mmlottie_bench_dir /PATH/TO/mmlottie_bench \
--split real \
--mmlottie_task text2lottie \
--output_dir ./benchmark_results
```
**Available task types:**
- `text2lottie` - Text-to-Lottie generation (150 samples per split)
- `text_image2lottie` - Text-Image-to-Lottie generation (150 samples per split)
- `video2lottie` - Video-to-Lottie generation (150 samples per split)
**Process limited samples with filtering:**
```bash
python inference.py \
--sketch_weight /PATH/TO/OmniLottie \
--mmlottie_bench_dir /PATH/TO/mmlottie_bench \
--split real \
--max_samples 50 \
--shuffle \
--output_dir ./benchmark_results
```
For detailed usage, see:
- [MMLottieBench Usage Guide](https://huggingface.co/datasets/OmniLottie/MMLottieBench/blob/main/README.md)
## 6. License
OmniLottie is licensed under the [**Apache License 2.0**](https://www.apache.org/licenses/LICENSE-2.0), while MMLottie-2M dataset is under [**Creative Commons Attribution Non Commercial Share Alike 4.0 License**](https://spdx.org/licenses/CC-BY-NC-SA-4.0). You can find the license files in the respective github and HuggingFace repositories.
## Citation
```bibtex
@article{yang2025omnilottie,
title={OmniLottie: Generating Vector Animations via Parameterized Lottie Tokens},
author={Yiying Yang and Wei Cheng and Sijin Chen and Xianfang Zeng and Jiaxu Zhang and Liao Wang and Gang Yu and Xinjun Ma and Yu-Gang Jiang},
journal={arXiv preprint arxiv:2504.06263},
year={2025}
}
```
## Acknowledgments
We thank the following excellent open-source works:
[IconShop](https://icon-shop.github.io/): is the first advanced work that leverages LLMs to generate monochrome, icon-level SVGs. We referred to its parametric implementation.
Here is the list of highly related concurrent works:
[LLM4SVG](https://arxiv.org/abs/2412.11102): treats SVG coordinates as number strings and predicts decimal part for higher spatial accuracy.
[StarVector](https://starvector.github.io/): equips LLM with an image encoder for Image-to-SVG generation.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=OpenVGLab/OmniLottie&type=Date)](https://www.star-history.com/#OpenVGLab/OmniLottie&Date)
+976
View File
@@ -0,0 +1,976 @@
import gradio as gr
import json
import torch
import os
import numpy as np
import random
import re
import tempfile
import base64
import threading
import time
from PIL import Image as PILImage
from decord import VideoReader, cpu
from decoder import LottieDecoder
from transformers import AutoProcessor
from qwen_vl_utils import process_vision_info
from lottie.objects.lottie_tokenize import LottieTensor
from lottie.objects.lottie_param import (
from_sequence, ShapeLayer, NullLayer, PreCompLayer, TextLayer,
SolidColorLayer, Font, Chars,
shape_layer_to_json, null_layer_to_json, precomp_layer_to_json,
text_layer_to_json, solid_layer_to_json, font_to_json, char_to_json
)
SYSTEM_PROMPT = "You are a Lottie animation expert."
VIDEO_PROMPT = "Turn this video into Lottie code."
LOTTIE_BOS = 192398
LOTTIE_EOS = 192399
PAD_TOKEN = 151643
model = None
processor = None
device = None
generation_lock = threading.Lock()
def load_model_once():
global model, processor, device
if model is not None:
return model, processor, device
checkpoint_path = "/PATH/TO/OmniLottie"
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Loading model from {checkpoint_path}...")
model = LottieDecoder(pix_len=4560, text_len=1500)
model_file = os.path.join(checkpoint_path, 'pytorch_model.bin')
if os.path.exists(model_file):
model.load_state_dict(torch.load(model_file, map_location='cpu'))
else:
raise FileNotFoundError(f"Model file not found: {model_file}")
model = model.to(device).eval()
processor = AutoProcessor.from_pretrained(
"Qwen/Qwen2.5-VL-3B-Instruct",
padding_side="left"
)
print(f"✅ Model loaded on {device}")
return model, processor, device
def simplify_to_animation_description(text):
if not text or not isinstance(text, str):
return text
prefixes = [
r'^The video features?\s+', r'^The scene shows?\s+',
r'^An animation of\s+', r'^There is\s+', r'^It shows?\s+'
]
for pattern in prefixes:
text = re.sub(pattern, '', text, flags=re.IGNORECASE)
if text:
text = text[0].upper() + text[1:]
return text.strip()
def add_random_background(img):
if img.mode != 'RGBA':
return img.convert('RGB')
light_colors = [(255, 255, 255), (245, 245, 245), (250, 250, 250)]
bg_color = random.choice(light_colors)
background = PILImage.new('RGB', img.size, bg_color)
background.paste(img, (0, 0), img)
return background
def load_frames_from_video(video_path, num_frames=8, max_size=336):
import os
ext = os.path.splitext(video_path)[1].lower()
frames = []
if ext in ('.gif', '.webp'):
try:
img = PILImage.open(video_path)
total_frames = getattr(img, 'n_frames', 1)
if total_frames < 1:
raise ValueError(f"No frames in {ext.upper()}: {video_path}")
indices = np.linspace(0, total_frames - 1, min(num_frames, total_frames)).astype(int)
for idx in indices:
img.seek(idx)
frame = img.convert('RGB')
if max(frame.size) > max_size:
frame.thumbnail((max_size, max_size), PILImage.LANCZOS)
frames.append(frame)
img.close()
except Exception as e:
raise ValueError(f"Failed to load {ext.upper()}: {str(e)}")
else:
try:
vr = VideoReader(video_path, ctx=cpu(0))
total_frames = len(vr)
if total_frames < 1:
raise ValueError(f"Video has no frames: {video_path}")
indices = np.linspace(0, total_frames - 1, num_frames).astype(int)
frames_np = vr.get_batch(indices).asnumpy()
for f in frames_np:
img = PILImage.fromarray(f)
if max(img.size) > max_size:
img.thumbnail((max_size, max_size), PILImage.LANCZOS)
frames.append(img)
except Exception as e:
raise ValueError(f"Failed to load video: {str(e)}")
while len(frames) < num_frames:
frames.append(frames[-1].copy())
return frames
def build_messages(task_type, text_prompt=None, image=None, video_frames=None):
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
if task_type == "text":
text = simplify_to_animation_description(text_prompt)
messages.append({
"role": "user",
"content": [{"type": "text", "text": f"Generate Lottie code: {text}"}]
})
elif task_type == "image":
text = simplify_to_animation_description(text_prompt)
messages.append({
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": f"Animate this image: {text}"}
]
})
elif task_type == "video":
messages.append({
"role": "user",
"content": [
{"type": "video", "video": video_frames, "fps": 8.0},
{"type": "text", "text": VIDEO_PROMPT}
]
})
return messages
def prepare_inference_input(processor, messages, device):
text_input = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
image_inputs, video_inputs = process_vision_info(messages)
inputs = processor(
text=[text_input],
images=image_inputs if image_inputs else None,
videos=video_inputs if video_inputs else None,
padding=False,
return_tensors="pt"
)
input_ids = inputs['input_ids']
attention_mask = inputs['attention_mask']
target_len = 1500
if input_ids.shape[1] < target_len:
pad_len = target_len - input_ids.shape[1]
input_ids = torch.cat([
torch.full((1, pad_len), PAD_TOKEN, dtype=torch.long),
input_ids
], dim=1)
attention_mask = torch.cat([
torch.zeros((1, pad_len), dtype=torch.long),
attention_mask
], dim=1)
result = {
'input_ids': input_ids.to(device),
'attention_mask': attention_mask.to(device),
'pixel_values': inputs.get('pixel_values').to(device) if inputs.get('pixel_values') is not None else None,
'image_grid_thw': inputs.get('image_grid_thw').to(device) if inputs.get('image_grid_thw') is not None else None,
'pixel_values_videos': inputs.get('pixel_values_videos').to(device) if inputs.get('pixel_values_videos') is not None else None,
'video_grid_thw': inputs.get('video_grid_thw').to(device) if inputs.get('video_grid_thw') is not None else None,
}
return result
def generate_lottie(model, inputs, max_tokens, device, use_sampling=False, temperature=0.95, top_p=0.25, top_k=5):
"""生成 Lottie tokens"""
model.transformer.rope_deltas = None
position_ids, _ = model.transformer.get_rope_index(
input_ids=inputs['input_ids'],
attention_mask=inputs['attention_mask'],
image_grid_thw=inputs.get('image_grid_thw'),
video_grid_thw=inputs.get('video_grid_thw'),
)
position_ids = position_ids * inputs['attention_mask'][None, ]
kwargs = {
'input_ids': inputs['input_ids'],
'attention_mask': inputs['attention_mask'],
'pixel_values': inputs.get('pixel_values'),
'image_grid_thw': inputs.get('image_grid_thw'),
'pixel_values_videos': inputs.get('pixel_values_videos'),
'video_grid_thw': inputs.get('video_grid_thw'),
'position_ids': position_ids,
'max_new_tokens': max_tokens,
'eos_token_id': LOTTIE_EOS,
'pad_token_id': PAD_TOKEN,
'use_cache': True,
}
if use_sampling:
kwargs.update({'do_sample': True, 'temperature': temperature, 'top_p': top_p, 'top_k': top_k})
else:
kwargs.update({'do_sample': False, 'num_beams': 1})
with torch.no_grad():
outputs = model.transformer.generate(**kwargs)
input_len = inputs['input_ids'].shape[1]
generated_ids = outputs[0][input_len:].tolist()
del outputs, kwargs, position_ids
if generated_ids and generated_ids[0] == LOTTIE_BOS:
generated_ids = generated_ids[1:]
if LOTTIE_EOS in generated_ids:
generated_ids = generated_ids[:generated_ids.index(LOTTIE_EOS)]
return generated_ids
def fix_lottie_json(anim):
anim_ip = int(round(anim.get("ip", 0)))
anim_op = int(round(anim.get("op", 16)))
anim["ip"] = anim_ip
anim["op"] = anim_op
anim["fr"] = int(round(anim.get("fr", 8)))
anim["ddd"] = int(anim.get("ddd", 0))
def fix_t_recursive(obj):
if isinstance(obj, dict):
if obj.get("a") == 1 and isinstance(obj.get("k"), list):
for kf in obj["k"]:
if isinstance(kf, dict) and "t" in kf:
kf["t"] = int(round(kf["t"]))
for v in obj.values():
fix_t_recursive(v)
elif isinstance(obj, list):
for item in obj:
fix_t_recursive(item)
fix_t_recursive(anim)
max_x = float(anim.get("w", 512))
max_y = float(anim.get("h", 512))
def collect_pos(layer):
nonlocal max_x, max_y
p = layer.get("ks", {}).get("p", {})
if isinstance(p, dict):
if p.get("a", 0) == 0:
pv = p.get("k", [0, 0])
if isinstance(pv, list) and len(pv) >= 2:
max_x = max(max_x, float(pv[0]))
max_y = max(max_y, float(pv[1]))
else:
for kf in p.get("k", []):
if isinstance(kf, dict):
for sv in (kf.get("s", []), kf.get("e", [])):
if isinstance(sv, list) and len(sv) >= 2:
max_x = max(max_x, float(sv[0]))
max_y = max(max_y, float(sv[1]))
for sub in layer.get("layers", []):
collect_pos(sub)
for layer in anim.get("layers", []):
collect_pos(layer)
anim["w"] = max(512, int((max_x * 1.1 + 15) // 16 * 16))
anim["h"] = max(512, int((max_y * 1.1 + 15) // 16 * 16))
valid_inds = set()
for layer in anim.get("layers", []):
if "ind" in layer:
valid_inds.add(int(layer["ind"]))
def clean_shapes(shapes):
if not isinstance(shapes, list):
return shapes
cleaned = []
for sh in shapes:
if not isinstance(sh, dict):
continue
if sh.get("ty") == "gr":
sh["it"] = clean_shapes(sh.get("it", []))
if not sh["it"]:
continue
has_tr = any(item.get("ty") == "tr" for item in sh["it"] if isinstance(item, dict))
if not has_tr:
sh["it"].append({
"ty": "tr", "nm": "",
"a": {"a": 0, "k": [0, 0], "ix": 1},
"p": {"a": 0, "k": [0, 0], "ix": 2},
"s": {"a": 0, "k": [100, 100], "ix": 3},
"r": {"a": 0, "k": 0, "ix": 6},
"o": {"a": 0, "k": 100, "ix": 7},
"sk": {"a": 0, "k": 0, "ix": 4},
"sa": {"a": 0, "k": 0, "ix": 5},
"hd": False
})
cleaned.append(sh)
return cleaned
def fix_layer(layer):
ip = int(round(layer.get("ip", anim_ip)))
op = int(round(layer.get("op", anim_op)))
layer["ip"] = max(anim_ip, ip)
layer["op"] = min(anim_op, max(layer["ip"] + 1, op))
layer["st"] = int(round(layer.get("st", anim_ip)))
if "ind" in layer:
layer["ind"] = int(layer["ind"])
if "parent" in layer:
p = int(layer["parent"])
if p in valid_inds:
layer["parent"] = p
else:
del layer["parent"]
layer.pop("ct", None)
if "shapes" in layer:
layer["shapes"] = clean_shapes(layer["shapes"])
for sub in layer.get("layers", []):
fix_layer(sub)
return layer
fixed_layers = []
for l in anim.get("layers", []):
fix_layer(l)
shapes = l.get("shapes", [])
if l.get("ty") == 4 and not shapes:
continue
fixed_layers.append(l)
anim["layers"] = fixed_layers
for asset in anim.get("assets", []):
if "layers" in asset:
fixed = []
for l in asset["layers"]:
fix_layer(l)
if l.get("ty") == 4 and not l.get("shapes"):
continue
fixed.append(l)
asset["layers"] = fixed
return anim
def tokens_to_lottie_json(generated_ids):
reconstructed_tensor = LottieTensor.from_list(generated_ids)
reconstructed_sequence = reconstructed_tensor.to_sequence()
reconstructed = from_sequence(reconstructed_sequence)
json_animation = {
"v": reconstructed.get("v", "5.5.2"),
"fr": reconstructed.get("fr", 8),
"ip": reconstructed.get("ip", 0),
"op": reconstructed.get("op", 16),
"w": reconstructed.get("w", 512),
"h": reconstructed.get("h", 512),
"nm": reconstructed.get("nm", "Animation"),
"ddd": reconstructed.get("ddd", 0),
"assets": [],
"layers": [],
}
if "fonts" in reconstructed and reconstructed["fonts"]:
fonts_data = reconstructed["fonts"]
if isinstance(fonts_data, dict) and "list" in fonts_data:
fonts_json = {"list": []}
for font in fonts_data["list"]:
if isinstance(font, Font):
fonts_json["list"].append(font_to_json(font))
else:
fonts_json["list"].append(font)
json_animation["fonts"] = fonts_json
if "chars" in reconstructed and reconstructed["chars"]:
chars_json = []
for char in reconstructed["chars"]:
if isinstance(char, Chars):
chars_json.append(char_to_json(char))
else:
chars_json.append(char)
json_animation["chars"] = chars_json
for asset in reconstructed.get("assets", []):
asset_json = dict(asset)
if "layers" in asset:
asset_json["layers"] = []
for layer in asset["layers"]:
if isinstance(layer, ShapeLayer):
asset_json["layers"].append(shape_layer_to_json(layer))
elif isinstance(layer, NullLayer):
asset_json["layers"].append(null_layer_to_json(layer))
elif isinstance(layer, PreCompLayer):
asset_json["layers"].append(precomp_layer_to_json(layer))
elif isinstance(layer, TextLayer):
asset_json["layers"].append(text_layer_to_json(layer))
elif isinstance(layer, SolidColorLayer):
asset_json["layers"].append(solid_layer_to_json(layer))
else:
asset_json["layers"].append(layer)
json_animation["assets"].append(asset_json)
for layer in reconstructed.get("layers", []):
if isinstance(layer, ShapeLayer):
json_animation["layers"].append(shape_layer_to_json(layer))
elif isinstance(layer, NullLayer):
json_animation["layers"].append(null_layer_to_json(layer))
elif isinstance(layer, PreCompLayer):
json_animation["layers"].append(precomp_layer_to_json(layer))
elif isinstance(layer, TextLayer):
json_animation["layers"].append(text_layer_to_json(layer))
elif isinstance(layer, SolidColorLayer):
json_animation["layers"].append(solid_layer_to_json(layer))
else:
json_animation["layers"].append(layer)
json_animation = fix_lottie_json(json_animation)
return json_animation
_lottie_js_cache = None
def get_lottie_js():
global _lottie_js_cache
if _lottie_js_cache is not None:
return _lottie_js_cache
local_path = "lottie.min.js"
if os.path.exists(local_path):
with open(local_path, 'r', encoding='utf-8') as f:
_lottie_js_cache = f.read()
print(f"✅ Loaded local lottie.min.js ({len(_lottie_js_cache)} bytes)")
else:
_lottie_js_cache = '<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js"></script>'
print("⚠️ Using CDN lottie.min.js (local file not found)")
return _lottie_js_cache
def create_lottie_html(animation_data, height=600):
bg_style = """
background-image:
linear-gradient(45deg, #666666 25%, transparent 25%),
linear-gradient(-45deg, #666666 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #666666 75%),
linear-gradient(-45deg, transparent 75%, #666666 75%);
background-size: 16px 16px;
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
background-color: #444444;
"""
lottie_js = get_lottie_js()
if lottie_js.startswith('<script'):
lottie_script = lottie_js
else:
lottie_script = f"<script>{lottie_js}</script>"
animation_json_escaped = json.dumps(animation_data).replace('\\', '\\\\').replace("'", "\\'")
anim_width = animation_data.get('w', 512)
anim_height = animation_data.get('h', 512)
inner_html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
{lottie_script}
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
html, body {{
width: 100%;
height: 100%;
overflow: hidden;
background: transparent;
}}
#lottie-container {{
width: 100%;
height: 100%;
{bg_style}
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}}
#lottie-animation {{
max-width: 100%;
max-height: 100%;
width: {anim_width}px;
height: {anim_height}px;
}}
.error {{ color: white; text-align: center; padding: 20px; }}
</style>
</head>
<body>
<div id="lottie-container">
<div id="lottie-animation"></div>
</div>
<script>
function renderLottie() {{
try {{
if (typeof lottie === 'undefined') {{
setTimeout(renderLottie, 100);
return;
}}
var animationData = JSON.parse('{animation_json_escaped}');
lottie.loadAnimation({{
container: document.getElementById('lottie-animation'),
renderer: 'svg',
loop: true,
autoplay: true,
animationData: animationData
}});
}} catch (e) {{
console.error('Lottie render error:', e);
document.getElementById('lottie-container').innerHTML =
'<p class="error">Failed: ' + e.message + '</p>';
}}
}}
renderLottie();
</script>
</body>
</html>"""
inner_html_b64 = base64.b64encode(inner_html.encode('utf-8')).decode('utf-8')
iframe_html = f'<iframe src="data:text/html;base64,{inner_html_b64}" style="width:100%; height:{height}px; border:none; border-radius:8px;"></iframe>'
return iframe_html
def save_json_to_temp(lottie_json):
if lottie_json is None:
return None
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, prefix='lottie_')
json.dump(lottie_json, temp_file, indent=2)
temp_file.close()
return temp_file.name
def process_text_to_lottie(text_prompt, max_tokens, use_sampling, temperature, top_p, top_k):
with generation_lock:
try:
start_time = time.time()
if not text_prompt or not text_prompt.strip():
return None, "❌ Please enter a text description", None
model, processor, device = load_model_once()
messages = build_messages("text", text_prompt=text_prompt)
inputs = prepare_inference_input(processor, messages, device)
generated_ids = generate_lottie(
model, inputs, max_tokens, device,
use_sampling, temperature, top_p, top_k
)
del inputs
if torch.cuda.is_available():
torch.cuda.empty_cache()
lottie_json = tokens_to_lottie_json(generated_ids)
html = create_lottie_html(lottie_json, height=600)
elapsed_time = time.time() - start_time
status = f"✅ Generated {len(generated_ids)} tokens | Layers: {len(lottie_json.get('layers', []))} | {lottie_json.get('fr', 8)} fps | Time: {elapsed_time:.1f}s"
temp_path = save_json_to_temp(lottie_json)
return html, status, temp_path
except Exception as e:
if torch.cuda.is_available():
torch.cuda.empty_cache()
return None, f"❌ Error: {str(e)}", None
def load_image_from_file(file_path):
ext = os.path.splitext(file_path)[1].lower()
if ext == '.svg':
import cairosvg
import io
png_bytes = cairosvg.svg2png(url=file_path, output_width=448, output_height=448)
image = PILImage.open(io.BytesIO(png_bytes))
else:
image = PILImage.open(file_path)
if image.mode == 'RGBA':
image = add_random_background(image)
else:
image = image.convert('RGB')
return image
def process_image_to_lottie(image_file, text_description, max_tokens, use_sampling, temperature, top_p, top_k):
with generation_lock:
try:
start_time = time.time()
if image_file is None:
return None, "❌ Please upload an image", None
model, processor, device = load_model_once()
image = load_image_from_file(image_file)
image = image.resize((448, 448), PILImage.LANCZOS)
desc = text_description if text_description else "A simple animation"
messages = build_messages("image", text_prompt=desc, image=image)
inputs = prepare_inference_input(processor, messages, device)
generated_ids = generate_lottie(
model, inputs, max_tokens, device,
use_sampling, temperature, top_p, top_k
)
del inputs
if torch.cuda.is_available():
torch.cuda.empty_cache()
lottie_json = tokens_to_lottie_json(generated_ids)
html = create_lottie_html(lottie_json, height=600)
elapsed_time = time.time() - start_time
status = f"✅ Generated {len(generated_ids)} tokens | Layers: {len(lottie_json.get('layers', []))} | {lottie_json.get('fr', 8)} fps | Time: {elapsed_time:.1f}s"
temp_path = save_json_to_temp(lottie_json)
return html, status, temp_path
except Exception as e:
if torch.cuda.is_available():
torch.cuda.empty_cache()
return None, f"❌ Error: {str(e)}", None
def process_video_to_lottie(video, max_tokens, use_sampling, temperature, top_p, top_k):
with generation_lock:
try:
start_time = time.time()
if video is None:
return None, "❌ Please upload a video/GIF/WebP file", None
import os
ext = os.path.splitext(video)[1].lower() if isinstance(video, str) else ''
if ext not in ('.mp4', '.avi', '.mov', '.gif', '.webp'):
return None, f"❌ Unsupported format: {ext}. Please upload MP4/AVI/MOV/GIF/WebP", None
model, processor, device = load_model_once()
frames = load_frames_from_video(video, num_frames=8)
messages = build_messages("video", video_frames=frames)
inputs = prepare_inference_input(processor, messages, device)
generated_ids = generate_lottie(
model, inputs, max_tokens, device,
use_sampling, temperature, top_p, top_k
)
del inputs
if torch.cuda.is_available():
torch.cuda.empty_cache()
lottie_json = tokens_to_lottie_json(generated_ids)
html = create_lottie_html(lottie_json, height=600)
elapsed_time = time.time() - start_time
status = f"✅ Generated {len(generated_ids)} tokens (from {len(frames)} frames) | Layers: {len(lottie_json.get('layers', []))} | {lottie_json.get('fr', 8)} fps | Time: {elapsed_time:.1f}s"
temp_path = save_json_to_temp(lottie_json)
return html, status, temp_path
except Exception as e:
if torch.cuda.is_available():
torch.cuda.empty_cache()
return None, f"❌ Error: {str(e)}", None
def create_gradio_interface():
with gr.Blocks(title="OmniLottie Demo Page", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎨 OmniLottie Demo Page")
gr.Markdown("Offical Demo Page of OmniLottie")
gr.Markdown("Generate Lottie animations from text, images, or videos")
with gr.Tabs() as tabs:
with gr.Tab("📝 Text-to-Lottie"):
gr.Markdown("""
### 💡 Prompt Tips for Better Results
**Good prompts should describe:**
1. **Main Object**: What is being animated (e.g., "a blue bird", "a yellow folder icon", "an orange piggy bank")
2. **Motion Pattern**: How it moves (e.g., "appearing, pulsing while sliding", "fading in, floating toward", "bouncing up and down")
3. **Direction**: Where it moves (e.g., "downward", "toward the top-left", "back to its start")
4. **Loop Behavior**: How it repeats (e.g., "repeating seamlessly", "looping smoothly", "repeating continuously")
**Example Patterns:**
- 🔄 **Simple Loop**: "a red ball appearing, bouncing up and down, then fading out, repeating seamlessly"
- 🎯 **Movement**: "a blue arrow sliding from left to right, then quickly returning to start, looping continuously"
- 💫 **Transformation**: "a yellow star fading in while rotating 360 degrees, holds briefly, then fading out, repeating smoothly"
- 🎨 **Static Icon**: "static illustration of a cartoon cat's face with a cute expression, light orange body, red inner ears"
- 👤 **Character**: "animated cartoon figure dressed in a beige suit with a white shirt, holding a gray tablet"
**Pro Tips:**
- Be specific about colors, shapes, and movements
- Describe motion phases clearly (appear → move → hold → return)
- Use descriptive motion verbs: sliding, pulsing, drifting, bouncing, rotating, fading
- For icons: include style details (outline, colors, decorations)
""")
with gr.Row():
with gr.Column(scale=1):
text_input = gr.Textbox(
label="Text Description",
placeholder="Example: a blue bird appearing, pulsing while sliding downward, lingers briefly, then growing back while sliding upward to reset, repeating seamlessly",
lines=5
)
with gr.Accordion("⚙️ Generation Settings", open=False):
gr.Markdown("""
**Parameter Guide:**
- **Max Tokens**: Higher = more complex animations (slower), Lower = simpler animations (faster)
- **Top-p & Top-k**: Higher = more random/creative, Lower = more stable/consistent
- **Temperature**: Higher = more diverse, Lower = more deterministic
💡 **Quick Tips:**
- For complex animations: increase max tokens to 5856
- For faster generation: reduce max tokens to 2048-3072
- For more creative results: increase top-p (0.5-0.8) and top-k (20-50)
- For consistent results: decrease top-p (0.1-0.25) and top-k (5-10)
""")
text_max_tokens = gr.Slider(512, 5856, value=5556, step=256, label="Max Tokens")
text_use_sampling = gr.Checkbox(label="Use Sampling", value=True)
text_temperature = gr.Slider(0.1, 2.0, value=0.9, step=0.1, label="Temperature")
text_top_p = gr.Slider(0.1, 1.0, value=0.25, step=0.1, label="Top-p")
text_top_k = gr.Slider(1, 100, value=5, step=1, label="Top-k")
text_generate_btn = gr.Button("🚀 Generate", variant="primary", size="lg")
# Generation time tips
gr.Markdown("""
⏱️ **Generation Time:**
- Simple icons/shapes: ~30-60 seconds (1000-2000 tokens)
- Medium animations: ~1-2 minutes (2000-3500 tokens)
- Complex characters: ~4-5 minutes (4500-6000 tokens)
Please be patient! Complex animations take time to generate. ☕
""")
text_status = gr.Markdown()
with gr.Column(scale=1):
text_output = gr.HTML(label="Animation Preview")
text_json_file = gr.File(label="📥 Download JSON", visible=True)
def get_text_examples():
examples = []
demo_txt_path = "./example/demo.txt"
if os.path.exists(demo_txt_path):
with open(demo_txt_path, 'r', encoding='utf-8') as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
examples = [[line] for line in lines[:50]]
return examples
gr.Examples(
examples=get_text_examples(),
inputs=text_input,
label="📂 Example Prompts (Click to Load)",
examples_per_page=10,
cache_examples=False
)
text_generate_btn.click(
fn=process_text_to_lottie,
inputs=[text_input, text_max_tokens, text_use_sampling, text_temperature, text_top_p, text_top_k],
outputs=[text_output, text_status, text_json_file]
)
with gr.Tab("🖼️ Text+Image-to-Lottie"):
with gr.Row():
with gr.Column(scale=1):
image_input = gr.Image(
label="Upload Image",
type="filepath",
sources=["upload"]
)
image_text_input = gr.Textbox(
label="Animation Description",
placeholder="Example: The object rotates 360 degrees",
lines=3
)
with gr.Accordion("⚙️ Generation Settings", open=False):
gr.Markdown("""
**Parameter Guide:**
- **Max Tokens**: Higher = more complex animations (slower), Lower = simpler (faster)
- **Top-p & Top-k**: Higher = more creative/random, Lower = more stable
- **Temperature**: Controls output diversity
""")
image_max_tokens = gr.Slider(512, 5556, value=5556, step=256, label="Max Tokens")
image_use_sampling = gr.Checkbox(label="Use Sampling", value=True)
image_temperature = gr.Slider(0.1, 2.0, value=0.9, step=0.1, label="Temperature")
image_top_p = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Top-p")
image_top_k = gr.Slider(1, 100, value=5, step=1, label="Top-k")
image_generate_btn = gr.Button("🚀 Generate", variant="primary", size="lg")
# Generation time tips
gr.Markdown("""
⏱️ **Generation Time:** ~1-5 minutes depending on complexity
""")
image_status = gr.Markdown()
with gr.Column(scale=1):
image_output = gr.HTML(label="Animation Preview")
image_json_file = gr.File(label="📥 Download JSON", visible=True)
def get_image_text_examples():
examples = []
demo_images_dir = "./example/demo_images"
if os.path.exists(demo_images_dir):
png_files = sorted([f for f in os.listdir(demo_images_dir) if f.endswith('.png')])
for png_file in png_files[:50]:
base_name = os.path.splitext(png_file)[0]
txt_file = os.path.join(demo_images_dir, f"{base_name}.txt")
png_path = os.path.join(demo_images_dir, png_file)
if os.path.exists(txt_file):
with open(txt_file, 'r', encoding='utf-8') as f:
text_desc = f.read().strip()
examples.append([png_path, text_desc])
return examples
gr.Examples(
examples=get_image_text_examples(),
inputs=[image_input, image_text_input],
label="📂 Example Images (Click to Load)",
examples_per_page=5,
cache_examples=False
)
image_generate_btn.click(
fn=process_image_to_lottie,
inputs=[image_input, image_text_input, image_max_tokens, image_use_sampling,
image_temperature, image_top_p, image_top_k],
outputs=[image_output, image_status, image_json_file]
)
# Tab 3: Video-to-Lottie
with gr.Tab("🎥 Video-to-Lottie"):
with gr.Row():
with gr.Column(scale=1):
video_input = gr.Video(
label="Upload Video / GIF / WebP",
sources=["upload"]
)
with gr.Accordion("⚙️ Generation Settings", open=False):
gr.Markdown("""
**Parameter Guide:**
- **Max Tokens**: Higher = more complex animations (slower), Lower = simpler (faster)
- **Top-p & Top-k**: Higher = more creative/random, Lower = more stable
- **Temperature**: Controls output diversity
""")
video_max_tokens = gr.Slider(512, 5556, value=5556, step=256, label="Max Tokens")
video_use_sampling = gr.Checkbox(label="Use Sampling", value=True)
video_temperature = gr.Slider(0.1, 2.0, value=0.9, step=0.1, label="Temperature")
video_top_p = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Top-p")
video_top_k = gr.Slider(1, 100, value=5, step=1, label="Top-k")
video_generate_btn = gr.Button("🚀 Generate", variant="primary", size="lg")
# Generation time tips
gr.Markdown("""
⏱️ **Generation Time:** ~2-5 minutes depending on video complexity
""")
video_status = gr.Markdown()
with gr.Column(scale=1):
video_output = gr.HTML(label="Animation Preview")
video_json_file = gr.File(label="📥 Download JSON", visible=True)
def get_video_examples():
examples = []
demo_video_dir = "./example/demo_video"
if os.path.exists(demo_video_dir):
video_files = sorted([os.path.join(demo_video_dir, f)
for f in os.listdir(demo_video_dir)
if f.endswith('.mp4')])
examples = [[vf] for vf in video_files[:50]]
return examples
gr.Examples(
examples=get_video_examples(),
inputs=video_input,
label="📂 Example Videos (Click to Load)",
examples_per_page=5,
cache_examples=False
)
video_generate_btn.click(
fn=process_video_to_lottie,
inputs=[video_input, video_max_tokens, video_use_sampling,
video_temperature, video_top_p, video_top_k],
outputs=[video_output, video_status, video_json_file]
)
gr.Markdown("---")
gr.Markdown("""
⚠️ **Important Note:** This demo processes one request at a time.
If another user is generating, your request will wait in queue until the current one completes.
""")
return demo
# ========== 启动应用 ==========
if __name__ == "__main__":
demo = create_gradio_interface()
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
show_error=True
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 891 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

+67
View File
@@ -0,0 +1,67 @@
import torch
import torch.nn as nn
from transformers import Qwen2_5_VLForConditionalGeneration, AutoConfig
from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import Qwen2_5_VLCausalLMOutputWithPast
from typing import Any, Dict, List, Optional, Tuple, Union
import transformers.models.qwen2_5_vl.modeling_qwen2_5_vl as qwen_modeling
class LottieDecoder(nn.Module):
"""
Autoregressive generative model for OmniLottie
"""
def __init__(self,
pix_len,
text_len,
model_path="Qwen/Qwen2.5-VL-3B-Instruct",
**kwargs):
super().__init__()
self.pix_len = pix_len
self.text_len = text_len
self.vocab_size = 192400
self.bos_token_id = 192398
self.eos_token_id = 192399
self.pad_token_id = 151643
print(f"Loading model from {model_path}...")
config = AutoConfig.from_pretrained(
model_path,
vocab_size=self.vocab_size,
bos_token_id=self.bos_token_id,
eos_token_id=self.eos_token_id,
pad_token_id=self.pad_token_id,
trust_remote_code=True
)
self.transformer = Qwen2_5_VLForConditionalGeneration.from_pretrained(
model_path,
config=config,
torch_dtype=torch.bfloat16,
attn_implementation="eager",
ignore_mismatched_sizes=True
)
self.transformer.resize_token_embeddings(self.vocab_size)
self.train()
def forward(self,
input_ids=None,
attention_mask=None,
pixel_values=None,
image_grid_thw=None,
pixel_values_videos = None,
video_grid_thw = None,
labels=None,
past_key_values=None,
use_cache=False,
**kwargs):
return NotImplementedError
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""
MMLottieBench 数据集下载脚本
用途:
从 HuggingFace 下载 OmniLottie/MMLottieBench 数据集并保存到本地
使用方法:
python download_mmlottie_bench.py
python download_mmlottie_bench.py --output_dir /custom/path
数据集结构:
该数据集使用 HuggingFace Datasets 格式
- Splits: real, synthetic
- Task types: Text-to-Lottie, Text-Image-to-Lottie, Video-to-Lottie
- Fields: id, text, image, video, task_type, subset, etc.
注意:
数据集使用 Apache Arrow 格式存储,图像和视频已嵌入其中
不需要手动解压!直接通过 datasets API 访问即可
"""
import os
import argparse
import sys
from pathlib import Path
try:
from datasets import load_dataset
except ImportError:
print("❌ Error: datasets library not installed")
print("Please install it with: pip install datasets")
sys.exit(1)
def download_and_save_dataset(output_dir):
"""
从 HuggingFace 下载 MMLottieBench 数据集并保存到本地
Args:
output_dir: 保存路径
Returns:
bool: 下载是否成功
"""
# 转换为绝对路径
output_dir = os.path.abspath(output_dir)
print("=" * 70)
print("🎨 MMLottieBench Dataset Downloader")
print("=" * 70)
print(f"📦 Repository: OmniLottie/MMLottieBench")
print(f"📁 Output directory: {output_dir}")
print("=" * 70)
print()
# 检查目录是否已存在
if os.path.exists(output_dir) and os.listdir(output_dir):
print(f"⚠️ Directory already exists and is not empty: {output_dir}")
response = input("Continue and overwrite? [y/N]: ")
if response.lower() != 'y':
print("❌ Download cancelled by user")
return False
print()
try:
print("📥 Step 1/2: Downloading dataset from HuggingFace...")
print("⏳ This may take a while depending on your network speed...")
print()
# 下载数据集(会自动使用 HF 缓存)
dataset = load_dataset("OmniLottie/MMLottieBench")
print()
print("📊 Dataset loaded successfully!")
print(f" Available splits: {list(dataset.keys())}")
for split_name, split_data in dataset.items():
print(f" - {split_name}: {len(split_data)} samples")
# 统计任务类型
task_types = {}
for sample in split_data:
task_type = sample.get('task_type', 'Unknown')
task_types[task_type] = task_types.get(task_type, 0) + 1
for task_type, count in task_types.items():
print(f"{task_type}: {count} samples")
print()
print("💾 Step 2/2: Saving dataset to disk...")
# 保存到磁盘
dataset.save_to_disk(output_dir)
print()
print("=" * 70)
print("✅ Download and save completed successfully!")
print("=" * 70)
print(f"📁 Dataset saved to: {output_dir}")
print()
print("📖 Usage in Python:")
print(" from datasets import load_from_disk")
print(f" dataset = load_from_disk('{output_dir}')")
print(" real_data = dataset['real']")
print(" synthetic_data = dataset['synthetic']")
print()
print("📖 Usage in inference:")
print(f" python inference.py --split real --sketch_weight <model>")
print()
print("💡 Note: Data is stored in Apache Arrow format")
print(" Images and videos are embedded - no need to extract!")
print(" Access them directly through the datasets API.")
print()
return True
except KeyboardInterrupt:
print("\n\n⚠️ Download interrupted by user")
return False
except Exception as e:
print()
print("=" * 70)
print("❌ Download failed!")
print("=" * 70)
print(f"Error: {str(e)}")
print()
print("💡 Troubleshooting:")
print(" 1. Check your network connection")
print(" 2. Make sure you have write permission to the target directory")
print(" 3. Install required packages: pip install datasets")
print(" 4. Try setting HF mirror: export HF_ENDPOINT=https://hf-mirror.com")
print()
print("📖 Manual access:")
print(" Visit: https://huggingface.co/datasets/OmniLottie/MMLottieBench")
print(" Or use in Python:")
print(" from datasets import load_dataset")
print(" dataset = load_dataset('OmniLottie/MMLottieBench')")
print("=" * 70)
return False
def main():
parser = argparse.ArgumentParser(
description='Download MMLottieBench dataset from HuggingFace',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Download to default location
python download_mmlottie_bench.py
# Download to specific directory
python download_mmlottie_bench.py --output_dir /data/datasets/mmlottie_bench
Dataset structure (Arrow format):
mmlottie_bench/
├── dataset_dict.json
├── real/
│ ├── data-00000-of-00001.arrow (contains images, videos, text)
│ ├── dataset_info.json
│ └── state.json
└── synthetic/
└── (same structure)
About the dataset:
MMLottieBench is a benchmark dataset for Lottie animation generation
Splits:
- real: Real-world Lottie animations (450 samples)
- synthetic: Synthetically generated samples (450 samples)
Task types (150 samples each per split):
- Text-to-Lottie: Generate from text prompt
- Text-Image-to-Lottie: Generate from text + image
- Video-to-Lottie: Generate from video
Data format:
- Stored in Apache Arrow format (efficient, compressed)
- Images: embedded as PIL.Image objects
- Videos: embedded as VideoReader objects
- Text: direct string storage
- NO manual extraction needed!
Usage after download:
1. In Python code:
from datasets import load_from_disk
dataset = load_from_disk('/data/cref/Lottie-kaiyuan/mmlottie_bench')
real_data = dataset['real']
2. In inference:
python inference.py --split real --sketch_weight <model>
For more info, see:
- MMLOTTIE_BENCH_USAGE.md
- MMLOTTIE_BENCH_DATA_ACCESS.md
"""
)
parser.add_argument(
'--output_dir',
type=str,
default='/data/cref/Lottie-kaiyuan/mmlottie_bench',
help='Output directory to save the dataset (default: /data/cref/Lottie-kaiyuan/mmlottie_bench)'
)
args = parser.parse_args()
# 执行下载
success = download_and_save_dataset(args.output_dir)
if not success:
sys.exit(1)
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
a blue bird appearing, pulsing while sliding downward, lingers briefly, then growing back while sliding upward to reset with clear phase changes, repeating seamlessly
a light blue piggy bank with a darker blue outline, with a single light blue coin with a dark blue yen symbol (£) appears above the piggy bank, then starts descending towards the piggy bank's opening
a purple butterfly appearing, turning while drifting toward the bottom-right, holds position briefly, then drifting toward the top-left back to its start while bouncing, looping smoothly
static illustration of a cartoon cat's face with a cute expression, the cat has a light orange body, red inner ears
animated cartoon-style figure dressed in a beige suit with a white shirt and dark hair, the figure holds a gray tablet in his left hand
animated character dressed in a beige suit jacket over a white shirt, paired with beige trousers and white shoes, the character has light brown hair tied back in a ponytail
a yellow folder icon fading in, sliding to the left while compressing, stays still briefly, then sliding to the right back to its start while fading out with clear phase changes, repeating seamlessly
animated character, a woman dressed in a black blazer over a white shirt and a black skirt, standing
a yellow progress bar fading in, sliding to the left while wobbling, stays still briefly, then sliding to the right back to its start while fading away, looping smoothly
animated scene featuring a chef holding a magnifying glass, the chef is dressed in a white uniform with black buttons and a traditional tall white chef's hat, the chef is standing
a blue QR code icon fading in, floating toward the top-left while pulsing, then briefly dimming, and then floating toward the bottom-right back to its start while turning, repeating continuously
animated sequence featuring a man in a beige suit holding a pink brain-like object labeled 'A.I.' with black text and pink circuit-like patterns, the man has dark hair and a beard,
bright orange piggy bank with a single black eye and a small pink ear, on top of the piggy bank, there are three green paper bills with a stylized Bitcoin symbol (₿)
cartoon-style character with black hair, wearing a gray suit with a white shirt and red tie, sits behind a wooden desk facing forward
cartoonish, yellow, smiley face emoji with pink lips
central Microsoft logo consisting of four colored squares—red, green, blue
consistent animated sequence of a stylized human figure with short brown hair and a light skin tone, the figure is plain with no additional markings, as the frames progress
dynamic emoji animation set, the emoji has a yellow face with an orange border, two eyes with red irises and black pupils
dynamic sequence of a stylized musical note symbol composed of layered black, cyan, and pink tones
five yellow stars appearing in succession, starting with one star and incrementally adding one more in each subsequent frame until all five stars are full
flat design animation of a person standing, the individual is wearing a black shirt and brown pants, and has short dark hair
green money bag icon with an Arabic word written in white, the bag has a brown strap tied around the top, to the right of the bag, there is a red circular badge containing a white lock
multiple pink cartoon crabs scattered, each crab has a simple, cute design with black eyes and antennae
orange-and-white error sign hanging from a brown rope attached to a small orange ball, the sign displaying the text 'ERROR' in dark orange and '404' in bright orange
sequence of three frames showing a static yellow diamond-shaped road sign with a black border, the sign features two stick-figure children playing on a seesaw, depicted as a simple
sequence of transformations involving a yellow logo with a white ghost-like figure outlined, four separate yellow sections containing parts of the ghost
sequence starting with an empty white screen, a light purple computer monitor fading in, followed by two silhouettes of people sitting in front of it
simple animation featuring a black silhouette icon representing two people standing side by side against, the left figure remaining stationary while the right fi
starting with a single group of three question marks in light purple, then expand into a vibrant displaying of multiple colorful question marks arranged in a scattered pattern acro
static cartoon-style illustration of a man wearing a white helmet, a white shirt, and gray pants
static cartoon-style illustration of a person sitting at a desk, the person has brown hair and is wearing a black jacket over a beige shirt, paired with beige pants and dark blue s
static illustration of a person in a beige suit holding a large, oversized blue credit card above their head, the person wears dark sunglasses and black shoes
stylized, animated letter 'U' that is blue with a black outline and simple white circles as eyes, over time
two orange lines forming an angular shape, these lines evolve into a more complex geometric design, incorporating curved arcs and layered elements
vibrant orange-pink balloon appearing, inflating and rising upwards as it filling with air, the balloon features bold white numbers '86' centered within it
woman with light brown hair styled in loose waves is standing, she is wearing a pink short-sleeved shirt, black pants
yellow, cartoon-style emoji face with brown hands raised at the sides, brown eyes
gradual transformation of a gray circular logo featuring a white cat silhouette into a black circular logo with the same white cat silhouette, the background remaining consistently
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@@ -0,0 +1 @@
A cyan-colored, elongated shape resembling a smiley face, consisting of two pink oval eyes with navy blue circular pupils positioned symmetrically. The shape rotates gradually around its horizontal axis, revealing small animated sparkles—pink and teal—that appear sporadically near the eyes, enhancing the dynamic visual effect.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

@@ -0,0 +1 @@
Two simplistic, cartoon-style figures standing behind podiums, engaging in a dialogue. Initially, the figures are partially obscured by the podiums, but as the video progresses, they gradually rise fully into view. Each figure has a speech bubble above them, indicating ongoing dialogue. The figures are primarily purple with black outlines, while the podiums are yellow with orange outlines. The background remains consistently white throughout the sequence, emphasizing the figures and their interaction.
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1 @@
A static illustration features a cartoon-style chef character wearing a white chef's hat and white jacket. The chef holds a large, bright red chat bubble icon with three white dots inside it, positioned prominently in the foreground. The chef's right hand grips the chat bubble, while the left hand is extended outward as if presenting it. There are no visible animations, movements, or transitions within the frames; the image maintains a consistent composition throughout the sequence.
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1 @@
A dynamic animation of a purple rectangular button with the white text 'Sale' centered on it. The button undergoes multiple transformations, including shifts in position, changes in perspective, and subtle color variations, giving it a three-dimensional appearance. Throughout the sequence, the button rotates slightly, creating a sense of depth and movement while maintaining its central focus in the frame.
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1 @@
A yellow folder icon gradually appears, followed by a curved gray line that extends outward, forming a circular shape around the folder. A green circle then slides into view from the right side and transforms into a green checkmark within the circle. The overall visual sequence depicts the creation and completion of a successful digital file organization process, represented through a folder icon with a green checkmark indicating confirmation or verification.
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -0,0 +1 @@
A central green square with rounded corners containing white and black text 'Uber Eats'. Attached to the top right corner of the green square is a red discount tag with a pink percentage symbol (%). The discount tag vibrates slightly, causing small pink sparkles to appear intermittently near it, creating a dynamic yet subtle animation effect.
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -0,0 +1 @@
A light gray rectangular box with rounded edges appears, representing the body of an ATM machine. On top of this box is a slightly darker gray horizontal bar, serving as the card insertion slot.', 'As the scene progresses, a light teal-colored vertical rectangle with a dark blue stripe in the center slides out from the bottom of the ATM machine, symbolizing a credit card being inserted. The card continues to move downward until it stops below the ATM slot.', 'Finally, a red location marker with a white circle at its tip appears directly beneath the credit card, indicating a geolocation feature. This completes the visual narrative, emphasizing the integration of payment and geographic services.
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1 @@
A small horizontal blue line that gradually extends into a larger rectangular frame. The frame then reveals a projected image, which includes a green and yellow pie chart in the upper left, a bar graph with varying heights in blue, green, and yellow on the right, and some placeholder text boxes in gray and the projection screen is bordered by a dark blue header and footer, each with rounded edges.
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1 @@
A sequence of four frames featuring an animated character with short brown hair, pink cheeks, black eyes, and a small black nose. The character's facial expressions change across the frames: starting with closed eyes and a frown, then transitioning to open eyes with a slight smile, followed by a squinted expression with a more pronounced smile, and finally returning to a neutral expression with slightly furrowed eyebrows. The background remains consistently white throughout all frames, emphasizing the character's facial changes. There are no additional elements or visual effects apart from the character's face, which morphs smoothly between expressions.
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@@ -0,0 +1 @@
A man in a purple shirt and black pants stands next to a large black tablet with a white screen. He holds a silver laptop in his hands, interacting with it while a green globe animation appears and expands on the tablet's screen. Surrounding the scene are several colorful circular icons: a blue globe icon on the left, a white Wi-Fi signal icon above, a yellow envelope icon to the right, an orange lightbulb icon in the top-right corner, and a blue human profile icon below the lightbulb. The globe animation on the tablet screen gradually expands and shifts position during the sequence.
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1 @@
A black location pin icon with a blue circular area at its center, containing a white ship silhouette. The pin initially appears stationary, then it rises slightly with a subtle drop shadow effect under it, creating an illusion of elevation. After rising, the pin gently descends back to its original position, returning to a stable state without the shadow effect.
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@@ -0,0 +1 @@
A simple, minimalist animation consisting of a central white figure represented by a circle and a green triangle filled with horizontal lines. Surrounding the figure are four purple L-shaped brackets positioned symmetrically at the top, bottom, left, and right. The white figure performs a slight pulsating motion, growing and shrinking in size while remaining centered. The green triangle remains static throughout the sequence, adding a textured contrast to the composition. The animation employs a clean, geometric style with smooth transitions and a steady rhythm. No other elements or background details are present, emphasizing simplicity and focus on the central figure's movement.
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1 @@
Two flat-style animated characters interacting with monetary elements. The left character, wearing a blue shirt and brown pants, holds a large golden coin with a dollar sign. The right character, dressed in a blue shirt and dark blue pants, holds a brown folder and gestures toward a stack of golden coins with dollar signs, positioned centrally. An orange arrow-shaped cloud icon moves rhythmically above the central coin stack, pointing upward, while additional white clouds appear and disappear dynamically around it. The scene remains static except for the subtle motion of the arrow cloud and the white clouds, emphasizing the financial theme through the characters' positioning and the coin imagery.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

@@ -0,0 +1 @@
A single blue speech bubble icon centered. Initially, the speech bubble appears partially filled with light blue, then gradually fills completely with solid blue. Inside the speech bubble, three small white dots appear, animate by moving slightly back and forth horizontally, and remain visible throughout the sequence.
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

@@ -0,0 +1 @@
A blue hanging tag icon with white text '92% OFF' displayed on it. The tag initially appears static. A subtle blur effect then moves across the tag, creating a ghosting effect where multiple instances of the tag overlap slightly. This blurring creates a sense of motion and depth before gradually resolving back to the original static image of the tag. The overall style is minimalist with clean lines and a focus on the motion effect.
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1 @@
A stylized, two-dimensional hourglass icon depicted in light yellow with black outlines. The upper bulb contains sand represented by a dark brown fill, which is positioned slightly off-center to indicate movement. Shadows in light gray surround the hourglass. As the sequence progresses, the hourglass rotates smoothly to achieve a vertical alignment. During this rotation, the sand appears to shift fluidly within it, emphasizing the passage of time. The graphic undergoes a transformation where the entire design fills with a solid yellow hue, enhancing its visibility and creating a bold look. The black outlines remain consistent throughout, providing structure.', 'By the end of the sequence, the hourglass is fully vertical, maintaining its solid yellow fill and black outlines. The sand within has settled into the lower bulb, forming a triangular shape that represents the completion of the time interval.
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1 @@
A simple animation of a hand holding a large yellow coin with a black dollar sign ($). The hand, depicted in light skin tone with a blue sleeve and a black cuff, moves slightly to spin the coin in place, simulating a flipping motion. The coin's rotation is smooth and continuous, emphasizing the central dollar sign, which remains clearly visible throughout the sequence. The background is consistently white, ensuring that the focus remains on the hand and coin interaction.
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -0,0 +1 @@
A circular graphic with a silver outer ring and a bright green inner circle. At the center is an orange hand making a peace sign gesture with the index and middle fingers raised. The hand appears to be slightly animated, as it undergoes minor transformations over the frames, such as slight changes in position and shape, giving the impression of motion or breathing life into the graphic. The black outlines around the hand and the rings emphasize the shapes clearly.
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1 @@
A single gray gear icon centered. The gear has a circular inner section and evenly spaced teeth around its perimeter.', 'The gear undergoes a transformation where its lines become bolder and turn black, emphasizing the mechanical detail. Simultaneously, two human-like figures, each represented by an outline with simplistic features such as hair and shoulders, appear below the gear. These figures are also outlined in black.', "In the final frame, the gear remains prominently displayed above the two human figures, both of which are positioned symmetrically on either side of the gear's vertical axis, creating a balanced composition that symbolizes human interaction with technology.
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1 @@
A dynamic animation of a blue rectangular sign with white text reading '41%' centered on it. Initially, the sign appears slightly blurred with two small rounded tabs at the top corners. As the video progresses, the sign becomes sharper, the tabs disappear, and the overall design stabilizes into a cleaner, solid blue rectangle with crisp edges and clear white text. The background remains consistently white throughout the sequence, emphasizing the transformation of the sign's appearance.
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1 @@
A small black dot at the center of the frame. The dot gradually expands into a thick, light gray ring with a darker gray section, forming an incomplete circular shape. This ring continues to expand and transform into a fully filled dark gray circle with a thin black border around it. Finally, the dark gray circle fills with black and reveals a white 'X' shape in the center.
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@@ -0,0 +1 @@
A sequence of three frames featuring an animated emoji face with a surprised expression. The face has a bright yellow circular shape, an orange hair outline at the top, black eyebrows raised upwards, and black eyes depicted as vertical lines. The mouth is an open black oval, emphasizing surprise. Throughout the sequence, the face remains centered, with subtle changes in the eye and eyebrow positions suggesting movement and animation. The overall design is minimalist and cartoonish, maintaining consistent proportions and colors across frames.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1 @@
A black rectangular button with white text reading 'PAY.' The button initially appears prominently in the center, casting a subtle shadow. Below the button, there is a symbol resembling radio waves or signal lines, depicted in black. The button undergoes a transformation sequence where it rotates slightly and shrinks while maintaining its position until it completely disappears, leaving only the radio wave symbol briefly visible before also vanishing. The background remains consistently white throughout the video, emphasizing the simplicity and focus on the button and symbol.
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

@@ -0,0 +1 @@
A black-bordered calendar icon initially appears as an empty white square, then transforms into a calendar view labeled 'August' with multiple horizontal black lines inside the frame. The calendar lines disappear, leaving only the word 'August' and a large central number '08', suggesting the date August 8th. The elements transition smoothly with gradual fades and appearances, maintaining a minimalist black-and-white design throughout the sequence.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1 @@
A central blue folder icon with a white paper symbol inside it. The folder has a gradient effect, transitioning from darker blue at the top to lighter blue at the bottom. Initially, the folder appears static, but as the video progresses, two small sparkling stars appear sequentially—one on the left side and another on the right side—before disappearing. These stars add a subtle animation effect, enhancing the visual dynamics of the scene.
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1 @@
A circular progress indicator with a central blue area and an outer gray ring. The red segment representing progress gradually increases in size clockwise, accompanied by percentage labels (5%, 16%, 26%, 35%, 65%) that appear and update dynamically within the blue area. The animation involves a smooth fill effect for the red segment and text updates, highlighting the increasing progress over time.
+1375
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
import os
import subprocess
from . import objects, parsers, utils, exporters, nvector, importers
from .nvector import *
from .utils.color import Color
try:
from .version import __version__
except ImportError:
here = os.path.dirname(os.path.abspath(__file__))
pipe = subprocess.Popen(
['git', 'describe', '--abbrev=0', '--tags'],
cwd=here,
stderr=subprocess.DEVNULL,
stdout=subprocess.PIPE
)
out, err = pipe.communicate()
if pipe.returncode == 0:
__version__ = out.strip()[1:].decode("ascii") + "+git"
else:
vfn = os.path.join(os.path.dirname(os.path.dirname(here)), "version")
if os.path.exists(vfn):
with open(vfn) as vf:
__version__ = vf.read().strip() + "+src"
else:
__version__ = "unknown"
try:
version_tuple = tuple(map(int, __version__.split("+")[0].split("."))) if __version__ != "unknown" else (0, 0, 0)
except ValueError:
version_tuple = (0, 0, 0)
__version__ = "unknown"
__all__ = ["objects", "parsers", "utils", "exporters", "nvector", "NVector", "Point", "Color", "importers"]
+19
View File
@@ -0,0 +1,19 @@
from . import base, core, sif, svg, pretty_print
from .base import exporters
from .core import export_lottie, export_tgs, export_embedded_html
from .pretty_print import prettyprint, prettyprint_summary
from .sif import export_sif
from .svg import export_svg
__all__ = [
"base", "core", "sif", "svg", "pretty_print",
"exporters", "export_lottie", "export_tgs", "export_embedded_html",
"prettyprint", "prettyprint_summary", "export_sif", "export_svg",
]
try:
from . import cairo, gif
__all__ += ["cairo", "gif"]
except ImportError:
pass
+32
View File
@@ -0,0 +1,32 @@
from ..parsers.baseporter import Baseporter, Loader, ExtraOption, io_progress
class ExporterLoader(Loader):
def __init__(self):
super().__init__(__file__, __name__, "export")
@property
def exporters(self):
return self.items
def set_options(self, parser):
group = parser.add_argument_group("Generic output options")
group.add_argument(
"--pretty", "-p",
action="store_true",
help="Pretty print (for formats that support it)",
)
group.add_argument(
"--frame",
type=int,
default=0,
help="Frame to extract (for single-image formats)",
)
super().set_options(parser)
return group
exporters = ExporterLoader()
exporter = exporters.decorator
+27
View File
@@ -0,0 +1,27 @@
import cairosvg
import io
from .base import exporter
from .svg import export_svg
def _export_cairo(func, animation, fp, frame, dpi):
intermediate = io.StringIO()
export_svg(animation, intermediate, frame)
intermediate.seek(0)
func(file_obj=intermediate, write_to=fp, dpi=dpi)
@exporter("PNG", ["png"], [], {"frame"})
def export_png(animation, fp, frame=0, dpi=96):
_export_cairo(cairosvg.svg2png, animation, fp, frame, dpi)
@exporter("PDF", ["pdf"], [], {"frame"})
def export_pdf(animation, fp, frame=0, dpi=96):
_export_cairo(cairosvg.svg2pdf, animation, fp, frame, dpi)
@exporter("PostScript", ["ps"], [], {"frame"})
def export_ps(animation, fp, frame=0, dpi=96):
_export_cairo(cairosvg.svg2ps, animation, fp, frame, dpi)
+119
View File
@@ -0,0 +1,119 @@
import sys
import json
import gzip
import codecs
from .base import exporter
from ..utils.file import open_file
from ..parsers.baseporter import ExtraOption
from .tgs_validator import TgsValidator
@exporter("Lottie JSON", ["json"], [], {"pretty"}, "lottie")
def export_lottie(animation, file, pretty=False):
with open_file(file) as fp:
kw = {}
if pretty:
kw = dict(indent=4)
json.dump(animation.to_dict(), fp, **kw)
@exporter("Telegram Animated Sticker", ["tgs"], [
ExtraOption("no_sanitize", help="Disable Sticker fit", action="store_false", dest="sanitize"),
ExtraOption("no_validate", help="Disable feature validation", action="store_false", dest="validate"),
])
def export_tgs(animation, file, sanitize=False, validate=False):
if sanitize:
animation.tgs_sanitize()
with gzip.open(file, "wb") as gzfile:
lottie_dict = animation.to_dict()
lottie_dict["tgs"] = 1
json.dump(lottie_dict, codecs.getwriter('utf-8')(gzfile))
if validate:
validator = TgsValidator()
validator(animation)
validator.check_file_size(file)
if validator.errors:
sys.stdout.write("\n".join(map(str, validator.errors))+"\n")
class HtmlOutput:
def __init__(self, animation, file):
self.animation = animation
self.file = file
def style(self):
self.file.write("""
<style>
#bodymovin { width: %spx; height: %spx; margin: auto;
background-color: white;
background-size: 64px 64px;
background-image:
linear-gradient(to right, rgba(0, 0, 0, .3) 50%%, transparent 50%%),
linear-gradient(to bottom, rgba(0, 0, 0, .3) 50%%, transparent 50%%),
linear-gradient(to bottom, white 50%%, transparent 50%%),
linear-gradient(to right, transparent 50%%, rgba(0, 0, 0, .5) 50%%);
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.5.3/lottie.js"></script>
""" % (self.animation.width, self.animation.height))
def body_pre(self):
self.file.write("""
<div id="bodymovin"></div>
<script>
var animData = {
container: document.getElementById('bodymovin'),
renderer: 'svg',
loop: true,
autoplay: true,
""")
def body_embedded(self):
self.file.write("animationData: ")
export_lottie(self.animation, self.file, True)
def body_post(self):
self.file.write("""
};
var anim = bodymovin.loadAnimation(animData);
</script>""")
def html_begin(self):
self.file.write("""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
html, body { width: 100%; height: 100%; margin: 0; }
body { display: flex; }
</style>""")
self.style()
self.file.write("</head><body>")
def html_end(self):
self.file.write("</body></html>")
@exporter("Lottie HTML", ["html", "htm"])
def export_embedded_html(animation, file):
with open_file(file) as fp:
out = HtmlOutput(animation, fp)
out.html_begin()
out.body_pre()
out.body_embedded()
out.body_post()
out.html_end()
def export_linked_html(animation, file, path):
with open_file(file) as fp:
out = HtmlOutput(animation, fp)
out.html_begin()
out.body_pre()
file.write("path: %r" % path)
out.body_post()
out.html_end()
+89
View File
@@ -0,0 +1,89 @@
import json
import string
import zipfile
from .base import exporter
from ..parsers.baseporter import ExtraOption
from ..parsers.tgs import parse_tgs
from lottie import __version__
from ..objects import assets
@exporter("dotLottie Archive", ["lottie"], [
ExtraOption("id", help="ID of the animation", default=None),
ExtraOption("append", help="Append animation to existing archive", action="store_true"),
ExtraOption("revision", help="File revision", type=int, default=None),
ExtraOption("author", help="File author", default=None),
ExtraOption("speed", help="Playback speed", type=float, default=1),
ExtraOption("theme_color", help="Theme color", type=str, default="#ffffff"),
ExtraOption("no_loop", help="Disable Looping", action="store_false", dest="loop"),
ExtraOption("no_pack", help="Don't auto-pack images", action="store_false", dest="pack_images"),
], slug="dotlottie")
def export_dotlottie(animation, file, id=None, append=False, revision=None, author=None,
speed=1.0, theme_color="#ffffff", loop=True, pack_images=True):
files = {}
if append:
with zipfile.ZipFile(file, "r") as zf:
with zf.open("manifest.json") as manifest:
meta = json.load(manifest)
for name in zf.namelist():
if name != "manifest.json":
files[name] = zf.read(name)
else:
meta = {
"generator": "Python Lottie " + __version__,
"version": 1.0,
"revision": 1,
"author": "",
"animations": [],
"custom": {}
}
if revision is not None:
meta["revision"] = revision
if author is not None:
meta["author"] = author
if id is None:
if animation.name:
idok = string.ascii_letters + string.digits + "_-"
id = "".join(filter(lambda x: x in idok, animation.name.replace(" ", "_")))
if not id:
id = "animation_%s" % len(meta["animations"])
meta["animations"].append({
"id": id,
"speed": speed,
"themeColor": theme_color,
"loop": loop,
})
if pack_images and animation.assets:
animation = animation.clone()
image_no = 0
for asset in animation.assets:
if isinstance(asset, assets.Image):
ext, data = asset.image_data()
if not ext:
continue
pathname = "images/"
while True:
basename = "image_%s.%s" % (image_no, ext)
image_no += 1
if pathname+basename not in files:
break
files[pathname+basename] = data
asset.image_path = pathname
asset.image = basename
asset.is_embedded = False
files["manifest.json"] = json.dumps(meta)
files["animations/%s.json" % id] = json.dumps(animation.to_dict())
with zipfile.ZipFile(file, "w") as zf:
for name, data in files.items():
zf.writestr(name, data)
+133
View File
@@ -0,0 +1,133 @@
import io
from PIL import Image
from PIL import features
from .cairo import export_png
from .base import exporter, io_progress
from ..parsers.baseporter import ExtraOption
def _png_gif_prepare(image):
if image.mode not in ["RGBA", "RGBa"]:
image = image.convert("RGBA")
alpha = image.getchannel("A")
image = image.convert("RGB").convert('P', palette=Image.ADAPTIVE, colors=255)
mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
image.paste(255, mask=mask)
return image
def _log_frame(fmt, frame_no=None, end=None):
if frame_no is None:
io_progress().report_message("%s frame rendering completed" % (fmt))
else:
io_progress().report_progress("%s rendering frame" % fmt, frame_no, end)
@exporter("GIF", ["gif"], [
ExtraOption("skip_frames", type=int, default=1, help="Only renderer 1 out of these many frames"),
])
def export_gif(animation, fp, dpi=96, skip_frames=1):
"""
Gif export
Note that it's a bit slow.
"""
start = int(animation.in_point)
end = int(animation.out_point)
frames = []
for i in range(start, end+1, skip_frames):
_log_frame("GIF", i, end)
file = io.BytesIO()
export_png(animation, file, i, dpi)
file.seek(0)
frames.append(_png_gif_prepare(Image.open(file)))
_log_frame("GIF")
io_progress().report_message("GIF Writing to file...")
duration = int(round(1000 / animation.frame_rate * skip_frames / 10)) * 10
frames[0].save(
fp,
format='GIF',
append_images=frames[1:],
save_all=True,
duration=duration,
loop=0,
transparency=255,
disposal=2,
)
@exporter("WebP", ["webp"], [
ExtraOption("lossless", action="store_true", help="If present, use lossless compression"),
ExtraOption("quality", type=int, default=80,
help="Compression effort between 0 and 100\n" +
"for lossy 0 gives the smallest size\n" +
"for lossless 0 gives the largest file"),
ExtraOption("method", type=int, default=0, help="Quality/speed trade-off (0=fast, 6=slower-better)"),
ExtraOption("skip_frames", type=int, default=1, help="Only renderer 1 out of these many frames"),
])
def export_webp(animation, fp, dpi=96, lossless=False, quality=80, method=0, skip_frames=1):
"""
Export WebP
See https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#webp
"""
if not features.check("webp_anim"):
raise Exception("WebP animations not supported in this system")
start = int(animation.in_point)
end = int(animation.out_point)
frames = []
for i in range(start, end+1, skip_frames):
_log_frame("WebP", i, end)
file = io.BytesIO()
export_png(animation, file, i, dpi)
file.seek(0)
frames.append(Image.open(file))
_log_frame("WebP")
io_progress().report_message("WebP Writing to file...")
duration = int(round(1000 / animation.frame_rate * skip_frames))
frames[0].save(
fp,
format='WebP',
append_images=frames[1:],
save_all=True,
duration=duration,
loop=0,
background=(0, 0, 0, 0),
lossless=lossless,
quality=quality,
method=method
)
@exporter("TIFF", ["tiff"])
def export_tiff(animation, fp, dpi=96):
"""
Export TIFF
"""
start = int(animation.in_point)
end = int(animation.out_point)
frames = []
for i in range(start, end+1):
_log_frame("TIFF", i, end)
file = io.BytesIO()
export_png(animation, file, i, dpi)
file.seek(0)
frames.append(Image.open(file))
_log_frame("TIFF")
io_progress().report_message("TIFF Writing to file...")
duration = int(round(1000 / animation.frame_rate))
frames[0].save(
fp,
format='TIFF',
append_images=frames[1:],
save_all=True,
duration=duration,
loop=0,
dpi=(dpi, dpi),
)
+71
View File
@@ -0,0 +1,71 @@
import sys
from ..objects.base import LottieObject, LottieBase
from ..objects.properties import MultiDimensional, Value, ShapeProperty
from ..objects.layers import Layer
def _prettyprint_scalar(lottie_object, out=sys.stdout):
if isinstance(lottie_object, float) and lottie_object == round(lottie_object):
lottie_object = int(lottie_object)
return str(lottie_object)
def prettyprint(lottie_object, out=sys.stdout, indent=" ", _i=""):
if isinstance(lottie_object, LottieObject):
out.write(lottie_object.__class__.__name__)
out.write('\n')
_i += indent
maxk = max(map(lambda x: len(x.name), lottie_object._props))
for k in lottie_object._props:
out.write(_i)
out.write(k.name.ljust(maxk))
out.write(' : ')
prettyprint(k.get(lottie_object), out, indent, _i)
elif isinstance(lottie_object, (list, tuple)):
if not lottie_object or (not isinstance(lottie_object[0], LottieBase) and len(lottie_object) < 16):
out.write("[")
out.write(", ".join(map(_prettyprint_scalar, lottie_object)))
out.write("]\n")
else:
out.write("[\n")
for k in lottie_object:
out.write(_i + indent)
prettyprint(k, out, indent, _i + indent)
out.write(_i)
out.write(']\n')
else:
out.write(_prettyprint_scalar(lottie_object, out))
out.write('\n')
def _prettyprint_summary_printable(obj):
if isinstance(obj, LottieObject):
return not isinstance(obj, (MultiDimensional, Value, ShapeProperty))
return obj and isinstance(obj, (list, tuple)) and isinstance(obj[0], LottieObject)
def prettyprint_summary(lottie_object, out=sys.stdout, indent=" ", _i=""):
if isinstance(lottie_object, LottieObject):
out.write(lottie_object.__class__.__name__)
name = getattr(lottie_object, "name", None)
if name:
out.write(" %r" % name)
if isinstance(lottie_object, Layer):
out.write(" %s -> %s" % (lottie_object.index, lottie_object.parent_index))
out.write('\n')
_i += indent
for k in lottie_object._props:
val = k.get(lottie_object)
if _prettyprint_summary_printable(val):
out.write(_i)
out.write(k.name)
out.write(' : ')
prettyprint_summary(val, out, indent, _i)
elif _prettyprint_summary_printable(lottie_object):
out.write("[\n")
for k in lottie_object:
out.write(_i + indent)
prettyprint_summary(k, out, indent, _i + indent)
out.write(_i)
out.write(']\n')

Some files were not shown because too many files have changed in this diff Show More