diff --git a/.gitignore b/.gitignore index 58171df..77ce9f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/.*/ +.DS_Store node_modules/ diff --git a/AGENTS.md b/AGENTS.md index 9875db2..20962f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Repo notes for AI coding agents (Codex, Claude, others). Microsoft Clippit/Clippy ACS extracted into: - 43 animated WebPs (one per ACS animation), in `assets/animations/webp/` -- A Codex-compatible pet package in `assets/pet/` (matches the `/hatch` skill spec: 1536x1872 transparent WebP atlas with 192x208 cells, 8 cols x 9 rows, plus `pet.json`) +- A Codex-compatible v2 pet package in `assets/pet/` (1536x2288 transparent WebP atlas with 192x208 cells, 8 cols x 11 rows, plus `pet.json`) The Codex pet is published in releases as `clippy-pet.zip`. To install: @@ -29,7 +29,8 @@ third-party/ source files (NOT licensed for redistribution) decompress.wasm ACS image decompressor from computernewb/MSAgent-Chat assets/ animations/ derived: per-animation webp + sheets + strips + manifest.json - pet/ derived: 8x9 atlas + pet.json + contact-sheet + review.json + pet/ derived: 8x11 v2 atlas + pet.json + contact-sheet + review.json + v2-look-directions.png source: 16 clockwise directions in two 8-cell rows scripts/ all Bun TypeScript, use sharp for image work extract-acs-images.ts ACS -> .out/acs-images/*.png + images.json split-clippy-animations.ts orchestrator: runs extract, then writes assets/animations/* @@ -48,7 +49,7 @@ bun scripts/build-clippy-pet.ts # also installs into ~/.codex/pets/clip bun scripts/update-readme-gallery.ts ``` -Build is reproducible: re-running produces byte-identical outputs (modulo paths in `assets/pet/review.json`). Multiple ACS animations share the same underlying frame data, so some webp files are byte-identical by design (e.g. CONGRATULATE/GETWIZARDY). +Build is reproducible: re-running produces byte-identical outputs. The 9 standard rows come from the ACS extraction; the two v2 look-direction rows are appended from the checked-in `assets/pet/v2-look-directions.png` source. Multiple ACS animations share the same underlying frame data, so some webp files are byte-identical by design (e.g. CONGRATULATE/GETWIZARDY). ## Conventions @@ -62,9 +63,9 @@ Build is reproducible: re-running produces byte-identical outputs (modulo paths ## /hatch skill compatibility -`assets/pet/spritesheet.webp` and `assets/pet/pet.json` together form a Codex pet that satisfies the `hatch-pet` skill's acceptance criteria: 1536x1872 transparent atlas, 192x208 cells, the 9 standard rows (`idle`, `running-right`, `running-left`, `waving`, `jumping`, `failed`, `waiting`, `running`, `review`). +`assets/pet/spritesheet.webp` and `assets/pet/pet.json` together form a Codex v2 pet that satisfies the `hatch-pet` skill's acceptance criteria: 1536x2288 transparent atlas, 192x208 cells, the 9 standard rows (`idle`, `running-right`, `running-left`, `waving`, `jumping`, `failed`, `waiting`, `running`, `review`), the v2 neutral/front frame in idle row column 6, followed by two rows containing 16 clockwise looking directions. -This pet is built by extraction + sampling, not by `$imagegen`. That bypasses the normal `/hatch` flow but produces a compatible artifact; do not run `/hatch` over this repo. +The standard animation rows are built by extraction + sampling rather than `$imagegen`. The checked-in v2 look-direction source strip was produced and visually validated through `/hatch`; normal regeneration does not invoke image generation. ## Build a custom pet from these animations @@ -92,4 +93,4 @@ Rules for editing: - The script samples frames evenly through each source animation via `sampleIndices`, so a long source animation becomes a smaller number of evenly-spaced poses. - After editing, re-run `bun scripts/build-clippy-pet.ts`. It writes `assets/pet/spritesheet.webp` + `pet.json` and installs them at `~/.codex/pets/clippy/`. To install under a different name, change the `id` in the generated `pet.json` (or edit it in the script before re-running) and move the folder to `~/.codex/pets//`. -The pipeline is: read the animations manifest -> for each row, pick frames from one source animation -> trim/scale/center each frame into a cell -> composite into the atlas -> write PNG, lossless WebP, and a labeled contact sheet for QA. No `$imagegen` involved. +The pipeline is: read the animations manifest -> for each standard row, pick frames from one source animation -> trim/scale/center each frame into a cell -> append the validated v2 direction strip -> composite the atlas -> write PNG, lossless WebP, and a labeled contact sheet for QA. No image generation occurs during the build. diff --git a/README.md b/README.md index 8ec8d8e..5fdcb1c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Microsoft Agent Clippit/Clippy extracted into animated WebP and a Codex pet package. +The pet uses the Codex v2 sprite format, including 16 clockwise looking directions +in 22.5-degree steps after the 9 standard animation rows. The builder also places +the required neutral/front frame in idle row column 6. + > Licensing note: these generated assets are derived from Microsoft Clippit artwork and > animation data. Do not assume this repo grants redistribution rights for Microsoft assets. @@ -25,6 +29,12 @@ To install a prebuilt pet without running anything, download `clippy-pet.zip` fr Pick a different mix of the 43 animations below for any of the 9 `/hatch` rows by editing `ROW_SPECS` in `scripts/build-clippy-pet.ts`, then re-run it. See [AGENTS.md](AGENTS.md#build-a-custom-pet-from-these-animations) for the row spec format, frame-sampling rules, and the cell geometry constants. +The standard rows remain reproducible from the ACS source. The two v2 looking-direction +rows are appended from [`assets/pet/v2-look-directions.png`](assets/pet/v2-look-directions.png). +The ACS contains eight principal LOOK animations; the checked-in strip adds the eight +intermediate poses required by Codex v2 and was produced and visually validated through +the `/hatch` workflow. + ## Animations | # | Name | WebP | Sheet | Strip | Frames | Duration | diff --git a/assets/pet/contact-sheet.png b/assets/pet/contact-sheet.png index edbaa75..5258848 100644 Binary files a/assets/pet/contact-sheet.png and b/assets/pet/contact-sheet.png differ diff --git a/assets/pet/pet.json b/assets/pet/pet.json index 73ca87a..e3dd4e0 100644 --- a/assets/pet/pet.json +++ b/assets/pet/pet.json @@ -2,5 +2,6 @@ "id": "clippy", "displayName": "Clippy", "description": "A classic paperclip assistant rebuilt from Microsoft Agent animation frames.", - "spritesheetPath": "spritesheet.webp" + "spritesheetPath": "spritesheet.webp", + "spriteVersionNumber": 2 } diff --git a/assets/pet/review.json b/assets/pet/review.json index e2477a1..9d6886c 100644 --- a/assets/pet/review.json +++ b/assets/pet/review.json @@ -2,7 +2,7 @@ "atlas": "assets/pet/spritesheet.png", "spritesheet_webp": "assets/pet/spritesheet.webp", "contact_sheet": "assets/pet/contact-sheet.png", - "pet_dir": "C:\\Users\\Dimava\\.codex\\pets\\clippy", + "pet_dir": "${CODEX_HOME:-$HOME/.codex}/pets/clippy", "rows": [ { "state": "idle", @@ -49,6 +49,14 @@ "source_frame": 43, "image_index": 0, "cell": ".out/pet/frames/idle/05.png" + }, + { + "column": 6, + "role": "v2-neutral", + "source_animation": "IDLE1_1", + "source_frame": 1, + "image_index": 0, + "cell": ".out/pet/frames/idle/06.png" } ] }, @@ -456,6 +464,98 @@ "cell": ".out/pet/frames/review/05.png" } ] + }, + { + "state": "look-direction", + "row": 9, + "frames": [ + { + "column": 0, + "direction_degrees": 0, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 1, + "direction_degrees": 22.5, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 2, + "direction_degrees": 45, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 3, + "direction_degrees": 67.5, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 4, + "direction_degrees": 90, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 5, + "direction_degrees": 112.5, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 6, + "direction_degrees": 135, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 7, + "direction_degrees": 157.5, + "source": "assets/pet/v2-look-directions.png" + } + ] + }, + { + "state": "look-direction", + "row": 10, + "frames": [ + { + "column": 0, + "direction_degrees": 180, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 1, + "direction_degrees": 202.5, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 2, + "direction_degrees": 225, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 3, + "direction_degrees": 247.5, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 4, + "direction_degrees": 270, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 5, + "direction_degrees": 292.5, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 6, + "direction_degrees": 315, + "source": "assets/pet/v2-look-directions.png" + }, + { + "column": 7, + "direction_degrees": 337.5, + "source": "assets/pet/v2-look-directions.png" + } + ] } ] } diff --git a/assets/pet/spritesheet.png b/assets/pet/spritesheet.png index 944ca60..c31b47b 100644 Binary files a/assets/pet/spritesheet.png and b/assets/pet/spritesheet.png differ diff --git a/assets/pet/spritesheet.webp b/assets/pet/spritesheet.webp index 4b6fc7d..166886a 100644 Binary files a/assets/pet/spritesheet.webp and b/assets/pet/spritesheet.webp differ diff --git a/assets/pet/v2-look-directions.png b/assets/pet/v2-look-directions.png new file mode 100644 index 0000000..ab4e914 Binary files /dev/null and b/assets/pet/v2-look-directions.png differ diff --git a/scripts/build-clippy-pet.ts b/scripts/build-clippy-pet.ts index 4cda58f..7b76b7c 100644 --- a/scripts/build-clippy-pet.ts +++ b/scripts/build-clippy-pet.ts @@ -8,6 +8,7 @@ const root = `${import.meta.dir}/..`; const animationsManifest = `${root}/assets/animations/manifest.json`; const acsImageDir = `${root}/.out/acs-images`; const assetsPet = `${root}/assets/pet`; +const v2LookDirections = `${assetsPet}/v2-look-directions.png`; const reviewDir = `${root}/.out/pet`; const petDir = path.resolve( (process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex")), @@ -17,9 +18,12 @@ const petDir = path.resolve( const CELL_W = 192; const CELL_H = 208; const COLUMNS = 8; -const ROWS = 9; +const STANDARD_ROWS = 9; +const LOOK_DIRECTION_ROWS = 2; +const ROWS = STANDARD_ROWS + LOOK_DIRECTION_ROWS; const ATLAS_W = CELL_W * COLUMNS; const ATLAS_H = CELL_H * ROWS; +const LOOK_DIRECTIONS = Array.from({ length: 16 }, (_, index) => index * 22.5); type RowSpec = [state: string, row: number, frameCount: number, animationName: string]; const ROW_SPECS: RowSpec[] = [ @@ -43,6 +47,9 @@ type Animation = { if (!fs.existsSync(animationsManifest)) { throw new Error("Run scripts/split-clippy-animations.ts before building the pet."); } +if (!fs.existsSync(v2LookDirections)) { + throw new Error(`Missing Codex v2 look-direction source: ${v2LookDirections}`); +} const manifest = JSON.parse(fs.readFileSync(animationsManifest, "utf-8")) as { animations: Animation[] }; const animationsByName = new Map(manifest.animations.map((a) => [a.name, a])); @@ -104,6 +111,8 @@ fs.mkdirSync(petDir, { recursive: true }); const atlasComposites: sharp.OverlayOptions[] = []; const rowsOut: Array<{ state: string; row: number; frames: object[] }> = []; +let extendedNeutralCell: Buffer | undefined; +let extendedNeutralSource: { sourceFrame: number; imageIndex: number } | undefined; for (const [state, row, frameCount, animationName] of ROW_SPECS) { const animation = animationsByName.get(animationName); @@ -114,13 +123,20 @@ for (const [state, row, frameCount, animationName] of ROW_SPECS) { const rowFrames = []; for (let column = 0; column < selected.length; column++) { const sourceFrame = selected[column]; - const imageIndex = animation.frames[sourceFrame].image_index; + if (sourceFrame === undefined) throw new Error(`missing sampled frame ${column} for ${animationName}`); + const source = animation.frames[sourceFrame]; + if (!source) throw new Error(`missing source frame ${sourceFrame} for ${animationName}`); + const imageIndex = source.image_index; const cell = imageIndex >= 0 ? await fitToCell(`${acsImageDir}/${String(imageIndex).padStart(3, "0")}.png`) : await emptyCell(); const cellPath = `${stateDir}/${String(column).padStart(2, "0")}.png`; await Bun.write(cellPath, cell); atlasComposites.push({ input: cell, top: row * CELL_H, left: column * CELL_W }); + if (state === "idle" && column === 0) { + extendedNeutralCell = cell; + extendedNeutralSource = { sourceFrame, imageIndex }; + } rowFrames.push({ column, source_animation: animationName, @@ -132,6 +148,70 @@ for (const [state, row, frameCount, animationName] of ROW_SPECS) { rowsOut.push({ state, row, frames: rowFrames }); } +if (!extendedNeutralCell || !extendedNeutralSource) { + throw new Error("Could not derive the Codex v2 neutral frame from idle column 0"); +} +const neutralColumn = 6; +const neutralCellPath = `${framesRoot}/idle/${String(neutralColumn).padStart(2, "0")}.png`; +await Bun.write(neutralCellPath, extendedNeutralCell); +atlasComposites.push({ input: extendedNeutralCell, top: 0, left: neutralColumn * CELL_W }); +rowsOut[0]?.frames.push({ + column: neutralColumn, + role: "v2-neutral", + source_animation: "IDLE1_1", + source_frame: extendedNeutralSource.sourceFrame + 1, + image_index: extendedNeutralSource.imageIndex, + cell: path.relative(root, neutralCellPath).replaceAll("\\", "/"), +}); + +const lookMetadata = await sharp(v2LookDirections).metadata(); +if ( + lookMetadata.width !== ATLAS_W + || lookMetadata.height !== LOOK_DIRECTION_ROWS * CELL_H + || lookMetadata.channels !== 4 + || !lookMetadata.hasAlpha +) { + throw new Error( + `Expected ${v2LookDirections} to be a ${ATLAS_W}x${LOOK_DIRECTION_ROWS * CELL_H} RGBA image`, + ); +} + +for (let index = 0; index < LOOK_DIRECTIONS.length; index++) { + const direction = LOOK_DIRECTIONS[index]; + if (direction === undefined) throw new Error(`missing look direction ${index}`); + const row = STANDARD_ROWS + Math.floor(index / COLUMNS); + const column = index % COLUMNS; + const cell = await sharp(v2LookDirections) + .extract({ left: column * CELL_W, top: (row - STANDARD_ROWS) * CELL_H, width: CELL_W, height: CELL_H }) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + let hasVisiblePixel = false; + for (let offset = 3; offset < cell.data.length; offset += 4) { + if (cell.data[offset] !== 0) { + hasVisiblePixel = true; + break; + } + } + if (!hasVisiblePixel) { + throw new Error(`Look-direction cell ${index} (${direction} degrees) is empty`); + } +} + +atlasComposites.push({ input: v2LookDirections, top: STANDARD_ROWS * CELL_H, left: 0 }); +for (let lookRow = 0; lookRow < LOOK_DIRECTION_ROWS; lookRow++) { + const row = STANDARD_ROWS + lookRow; + rowsOut.push({ + state: "look-direction", + row, + frames: Array.from({ length: COLUMNS }, (_, column) => ({ + column, + direction_degrees: LOOK_DIRECTIONS[lookRow * COLUMNS + column], + source: path.relative(root, v2LookDirections).replaceAll("\\", "/"), + })), + }); +} + const atlas = await sharp({ create: { width: ATLAS_W, height: ATLAS_H, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, }) @@ -145,7 +225,11 @@ await Bun.write(spritesheetPng, atlas); await sharp(atlas).webp({ lossless: true, quality: 100, effort: 6 }).toFile(spritesheetWebp); const labelsSvg = `${ - ROW_SPECS.map(([state, row]) => `${state}`).join("") + [ + ...ROW_SPECS.map(([state, row]) => [state, row] as const), + ["look 000-157.5", 9] as const, + ["look 180-337.5", 10] as const, + ].map(([state, row]) => `${state}`).join("") }${ Array.from({ length: COLUMNS + 1 }, (_, c) => ``).join("") }${ @@ -167,6 +251,7 @@ const petJson = { displayName: "Clippy", description: "A classic paperclip assistant rebuilt from Microsoft Agent animation frames.", spritesheetPath: "spritesheet.webp", + spriteVersionNumber: 2, }; await Bun.write(`${assetsPet}/pet.json`, JSON.stringify(petJson, null, 2) + "\n"); await Bun.write(`${petDir}/pet.json`, JSON.stringify(petJson, null, 2) + "\n"); @@ -176,7 +261,7 @@ const review = { atlas: path.relative(root, spritesheetPng).replaceAll("\\", "/"), spritesheet_webp: path.relative(root, spritesheetWebp).replaceAll("\\", "/"), contact_sheet: path.relative(root, `${assetsPet}/contact-sheet.png`).replaceAll("\\", "/"), - pet_dir: petDir, + pet_dir: "${CODEX_HOME:-$HOME/.codex}/pets/clippy", rows: rowsOut, }; await Bun.write(`${assetsPet}/review.json`, JSON.stringify(review, null, 2) + "\n"); diff --git a/scripts/update-readme-gallery.ts b/scripts/update-readme-gallery.ts index 2d90ad2..ab30f38 100644 --- a/scripts/update-readme-gallery.ts +++ b/scripts/update-readme-gallery.ts @@ -28,6 +28,10 @@ const content = `# Codex Clippy Microsoft Agent Clippit/Clippy extracted into animated WebP and a Codex pet package. +The pet uses the Codex v2 sprite format, including 16 clockwise looking directions +in 22.5-degree steps after the 9 standard animation rows. The builder also places +the required neutral/front frame in idle row column 6. + > Licensing note: these generated assets are derived from Microsoft Clippit artwork and > animation data. Do not assume this repo grants redistribution rights for Microsoft assets. @@ -51,6 +55,12 @@ To install a prebuilt pet without running anything, download \`clippy-pet.zip\` Pick a different mix of the 43 animations below for any of the 9 \`/hatch\` rows by editing \`ROW_SPECS\` in \`scripts/build-clippy-pet.ts\`, then re-run it. See [AGENTS.md](AGENTS.md#build-a-custom-pet-from-these-animations) for the row spec format, frame-sampling rules, and the cell geometry constants. +The standard rows remain reproducible from the ACS source. The two v2 looking-direction +rows are appended from [\`assets/pet/v2-look-directions.png\`](assets/pet/v2-look-directions.png). +The ACS contains eight principal LOOK animations; the checked-in strip adds the eight +intermediate poses required by Codex v2 and was produced and visually validated through +the \`/hatch\` workflow. + ## Animations | # | Name | WebP | Sheet | Strip | Frames | Duration |