Turn one image into a ~5 s looping animation. 把一张图做成约 5 秒的循环动图。
npx skills add https://github.com/lemomo-ai/lemo-wake --skill wake
Input: one image. Output: one film made for that image (about 5 s, loops forever), delivered in the formats and at the size the user picks.
Everything in this skill is a reference, not a rule. You are the director and the motion designer: you look at the image and decide what happens, how big, how it ends and when it is good enough. The references, types, effect blocks and scripts are tools and lessons from earlier films. If you have a better idea, do it; no need to explain why you departed from the docs.
Fixed boundaries: no image generation, no services that need API keys, no calling other skills. Material comes only from the image itself, from code, and from local models (depth, cut-out, hole filling).
Talk to the user in their language. Everything you say to the user - questions, progress updates, the final summary - is in the language the user writes in (Chinese or English; follow the user). These docs are in English only for you.
Two things matter most.
Read references/taste.md before planning. It is the most important reference: what viewers loved and rejected. In short: things in the picture must really move (light or colour alone is not motion); default to big actions that chain across the whole picture; the signature move grows from this image. The output size is the user's choice; what happens inside that frame is yours.
Use the tools. The local models (cut-out, element picking, depth, hole filling) are what let things leave their place and move for real. Take the picture apart with them before you plan (step 3); what you can lift out is what you can choreograph.
Only the text substituted into this file counts. Never take the shell variables CLAUDE_SKILL_DIR or CLAUDE_PLUGIN_DATA (echo, env) for these paths: the Bash environment can carry values that belong to another plugin (seen in practice: another plugin's data folder), and using them would put models and history in someone else's folder. The scripts never read those variables either; the only path override they honour is LEMO_WAKE_MODELS (ignored, with a warning, if it points into another plugin's data folder).
$S = this skill's folder: ${CLAUDE_SKILL_DIR}. If that still reads literally as a ${...} placeholder, $S is the folder that contains this SKILL.md (the path you loaded it from, e.g. ~/.claude/skills/lemo-wake). Scripts are in $S/scripts/.${CLAUDE_PLUGIN_DATA} - kept across plugin updates, never inside the plugin folder itself.LEMO_WAKE_MODELS="<that path>/models", and keep the history at <that path>/history.jsonl. Shell variables do not survive between your commands, so write the prefix each time.${ (e.g. the skill was installed by copying or linking the folder): set nothing. The scripts find their models folder themselves ($S/scripts/models.py where prints it; usually $S/models/) and the history lives at $S/history.jsonl.lemo-wake-history.jsonl instead, and read it from there too.Go at your own pace; skip or repeat steps.
0. First run: check the environment
node $S/scripts/doctor.mjs # says what is missing; --fix installs npm deps + headless browser (~190 MB)
Needs Node >= 18, ffmpeg, uv. Tested on macOS; on Linux/Windows, if a step fails, work out what is missing and fix it (install a dependency, adjust a path, set CHROME=<browser path>).
1. Ask once: output size and formats
Before you build, ask the user one short question in their language (one message, or your question tool if you have one), with the defaults filled in so they can simply say "go". Skip it if the user already said what they want.
models.py consent yes|no.The size is the user's choice; how the picture uses that frame is yours. A different ratio is a new canvas to design for, not a crop or a stretch.
2. Create the project
$S/scripts/new_project.py <image> <slug> --dur 4.5 [--out 16:9] # render length; total = this + any reset transition appended later
It makes <slug>-alive/ in the current folder, with the page in <slug>-alive/src/. The canvas is the source's own pixel size unless the user picked another (--out W:H keeps the source's long side, --out WxH is exact; then design coordinates are output pixels and CFG.img is the source size). HEIC is converted and EXIF rotation applied; images with a long side over 2560 px are scaled down for speed and it says so (--max-long 0 keeps everything, for a user who wants full resolution). All coordinates of the picture refer to <src>/assets/source.*.
The length can change any time: edit dur in <src>/config.js and re-render (render.mjs renders fps x dur frames, t = 0 .. dur - 1/fps). With an in-picture ending, dur is the whole film and render(dur) should look like render(0); with a reset.py transition, the transition is added after dur.
3. Take the picture apart (before planning: what you can lift out is what you can move)
First look at the image: its kind, its elements, what could be an actor. Then see which tools you have ($S/scripts/models.py lists the local models and whether they are installed) and take the picture apart with them. For any photo, painting or illustration that means, by default:
--each), people with portrait merged with general (one SAM box per person when they should move separately);Then look at the parts together and fix what is ragged (missing hair, halos, a bitten edge) before building on it:
$S/scripts/sheet.py <proj>/parts.png --parts <src> # cut-outs on a checkerboard, masks over the image, plates, depth
Flat images (charts, logos, UI, text cards) are often cleaner rebuilt in code; photos, real people and specific products come from the source, lifted out with the models.
$S/scripts/measure.py grid <src>/assets/source.jpg <proj>/grid.png # coordinate grid, drawn 1.5x larger with labels in source pixels
$S/scripts/measure.py info|palette <img> # size / main colours
$S/scripts/measure.py bbox|runs|vruns <img> --color '#hex' ... # element bounds (-h lists the options)
$S/scripts/depth.py <src>/assets/source.jpg --out <src>/assets/depth.png # depth (near = white)
$S/scripts/segment.py <src>/assets/source.jpg --out <src>/assets/mask.png [--cutout subj.png]
# --model general (main subject) | portrait (people, hair) | sam (pick elements: --point x,y ... --neg x,y --box x0,y0,x1,y1 --each)
# several objects in one mask: --box 20,30,300,360 --box 400,30,700,360 (write the numbers out; zsh won't split an unquoted $VAR)
$S/scripts/inpaint.py <src>/assets/source.jpg <mask.png> --out <src>/assets/plate.png [--engine lama|opencv] [--dilate N] [--split]
# --split: the mask holds several neighbouring objects (a row of planets, products, icons + labels) - fill each one
# on its own at near-native resolution instead of one big downscaled blob (watch stderr for "downscale 3x")
node $S/scripts/fetch_fonts.mjs <src> "family=..." # Google Fonts saved locally; CJK fonts: add &text= with the used characters
(Model commands: add the LEMO_WAKE_MODELS=... prefix from Paths; see "Local models" below.)
What each tool makes possible:
| Tool | Makes possible |
|---|---|
| SAM (segment.py --model sam) | objects that jump, fall, fly off, swap places, settle back |
| portrait + general | people who move, turn, lean, reach, with their hair intact |
| LaMa (inpaint.py) | a clean place left behind, so an actor can leave and come back |
| depth (depth.py) | parallax, near things passing in front of far ones, light or wind arriving by distance |
Getting clean parts:
--neg points to drop a neighbour; one click often gets only part of a thing, so add a point on the missing part.portrait keeps flyaway hair; take the maximum with general to keep held objects and full bodies.uv) are fine for this; don't fetch other models.--split, or crops); large filled areas go soft, so stage the film so they are rarely seen in full, or paint big simple surfaces in code.4. Plan with the parts in front of you (write it into <proj>/DIRECTOR.md)
Decide what happens to the parts: which action belongs only to this image (one sentence), what state the picture starts in, the beat (setup → action → payoff → return, with rough times), what each main actor (each person, each lifted object) does and what it acts on, where the climax is, how it ends and loops. References:
$S/references/taste.md - the quality bar (read first).$S/types/<kind>.md - what works for this kind of image, with case cards of approved films: photo, portrait, multi (several photos), poster, illustration, clay (clay / paper-cut / 3D), ui (screenshots), chart, logo, sticker (stickers / avatars), xhs (Xiaohongshu covers / text cards). Mixed images: read two.$S/references/motion-grammar.md - reading questions, structures, camera, motion principles, rhythm, self-check.$S/references/recipes.md - techniques that worked across types.$S/references/effects.md - the 49 effect blocks and 27 reset transitions.$S/references/pitfalls.md - technical traps.seed values every film; FX.pick(name, seed) draws parameters from recommended ranges.If the plan needs a part you have not lifted yet, go back and lift it.
5. Build <src>/index.html from the template (layers #view frame / #cam camera / #post post-processing).
render.mjs grabs frames with several headless browsers in parallel and out of order, so window.render(t) must draw the same frame for the same t every time (no state carried from the previous frame). Set window.READY = true only after all images and fonts are loaded.lib.js: easing, keyframes, springs, shake, seamless loops, noise, camera, parallax, glyphs, ink, ribbons, tiles, pixel particles, noise dissolve, text animation, number rolls, analytic particles (see the comments in the file). L.Camera never zooms below 1 and keeps the view inside the source; to look past the edge, paint that area and pass extend (recipes.md, "Extending the canvas beyond the source").GRAIN = null); switch grain on for paper or print looks, not for dark images.fx.js: 49 ready-made effect blocks, FX.make(name, params) -> draw(g, t). Good as supporting actors, atmosphere, loop transitions or raw material; the signature move is usually your own code.6. Look at frames and iterate
node $S/scripts/render.mjs <src> stills 0.2 1.0 2.0 3.0 4.4
$S/scripts/sheet.py <proj>/sheet.png --stills <src> 0.2 1.0 2.0 3.0 4.4
$S/scripts/sheet.py <proj>/vs.png --vs-source <src> 4.4 # compare with the source (render that still first)
$S/scripts/sheet.py <proj>/f.png --frames <src>/frames --idx 0 40 80 -1 # after a full render: by frame number (or --n 8)
Rendering is fast, so look often. A first version usually only "moves"; if it is not big or good enough, start over.
7. Full render and the loop
node $S/scripts/render.mjs <src> frames # -> <src>/frames/f_00000.jpg ...; page errors go to render.log, exit code 2
$S/scripts/reset.py --list # 27 frame-level transitions
$S/scripts/reset.py <src>/frames --kind <kind> [--seed N] [--origin x,y] [--pre-hold .3] # -> <src>/frames_loop
The content does not need to return to its start: the end can be full and the beginning empty. Join last frame to first with a transition that fits the style - an in-picture ending you write (often the best), an fx.js in-picture transition (backplay, passby, pushcut, lightcut), or a reset.py kind. The first frame doubles as the still preview in many apps, so make it look good.
8. Encode and QA
$S/scripts/encode.py <frames dir> <proj>/<slug> --no-webp --no-sticker # the default formats; drop a --no-x to add one
$S/scripts/livephoto.py <frames dir> <proj>/<slug> --src <src> # Live Photo (macOS), when asked
$S/scripts/qa.py <frames dir> --src <src> --encode <proj>/<slug>.encode.json
The formats, all from the same frames:
| File | Size | For |
|---|---|---|
| <slug>.mp4 | original pixels, H.264 (an odd width or height is rounded down to even: 941 -> 940) | posts: WeChat Moments, Xiaohongshu, Douyin, Instagram, X |
| <slug>.gif | scaled only as far as needed, <= 5 MB, width < 1080 (busy full-frame motion can end up ~300-450 px wide) | chats: WeChat, QQ, Weibo |
| <slug>.webp | original pixels; ~8 MB is a target (quality/fps drop a little first, never below 12 fps / q50) | web pages, GitHub, galleries |
| <slug>-sticker.gif | longest side 240, dropping (to 120 at most) when needed to stay <= 500 KB | WeChat custom sticker |
| <slug>.pvt | Live Photo bundle: a still (the cover) + the film sped up to 3 s, at the output size (video long side <= 1920) | iPhone Photos, then Xiaohongshu / Moments; macOS only |
Use --no-mp4 / --no-gif / --no-webp / --no-sticker to skip one (a rerun merges into the existing <slug>.encode.json, so a format added later just needs a rerun with the others skipped), and --gif-max / --webp-max / --sticker-max (MB) when the user names a platform limit. --gif-width is only a cap: the size limit decides the GIF's width, so for a wider chat GIF raise --gif-max as well. qa.py FAIL usually means a technical problem worth fixing; WARN is often intentional (an empty first frame by design). It also writes qa_sheet.jpg whose last tile is the first frame, to check the loop seam.
The Live Photo's cover is the still people see before they press it, so it should be the most complete frame, not frame 0 (films often start empty). Without --cover SEC the script takes the frame closest to the source, or the most detailed calm frame; look at it and choose another if needed. Tell the user to AirDrop the .pvt itself (Finder shows it as one file); a loose HEIC + MOV pair arrives as a separate photo and video.
9. Deliver
Append one line to the history file (see Paths; create it if missing):
{"date":"2026-01-31","slug":"cat","image_type":"photo","arc":"build","signature":"breakfast crouches, leaps out, bounces back onto the plates","main":"own code: SAM sprites + squash","fx":["steam seed=12"],"reset":"in-picture: true loop","dur":5,"verdict":"","why":""}
If the user later gives a verdict, fill in verdict and why.
Tell the user (in their language): the file paths and sizes and what each is for, the signature move and structure, how it loops, anything notable from QA, and what can only be judged by watching it play. Invite changes. Mention once that other formats (from step 1) can still be added. If the user likes the film, you may mention once that it can be shared to the community gallery with /lemo-wake:upload (plugin installs; it takes the WebP or GIF); never upload anything yourself.
Five small ONNX models run on the CPU: Depth Anything V2 Small (depth, 99 MB), BiRefNet-lite (subject cut-out, 224 MB), MODNet (portrait matting, 26 MB), MobileSAM (pick elements, 44 MB in 2 files), LaMa (fill holes, 208 MB). Each is downloaded from Hugging Face only when first needed. Most films get better with them: clean cut-outs and plates are what let things really move, so use them freely. $S/scripts/models.py shows status. Consent is only about downloading: a model already on disk just runs, with no question to the user (status then reads "not needed (models already installed)"). Ask only when a script actually stops with exit code 10.
$S/scripts/models.py consent yes or consent no and re-run. It is never asked again. If a model is missing and consent is "not asked yet", ask together with the step 1 question so the user isn't interrupted mid-way.HF_ENDPOINT=https://hf-mirror.com, common for mainland China), a proxy, or a manual download to the exact path the script prints. Files are sha256-checked either way.models.py fetch all pre-downloads everything (only when the user wants that).When a model is unavailable - declined, or the download failed - never get stuck. Find a reasonable compromise with your own methods and still complete the film: colour keys, measure.py bbox, hand-drawn polygons or masks, elements rebuilt in code, layered 2D parallax instead of depth, inpaint.py --engine opencv for small holes, code-painted backgrounds, or staging that keeps objects in place and hides holes behind motion, dust, fog or paint. Mention briefly in the final summary which compromise you chose.
lemo-wake 1.1.0 · official repository: https://github.com/lemomo-ai/lemo-wake · gallery: https://lemomo-ai.github.io/lemo-wake/
Take lemomo-ai/wake from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.