Releases: Krishna2709/giffify
Release list
video-to-gif v0.3.0
Transformations without text: clips can now be cropped, explicitly resized,
speed-adjusted, and dithered, and a new preview command extracts a single
full-colour PNG still so framing can be confirmed before any GIF is encoded.
Every transformation parameter is an integer, a bounded decimal, or a member of a
fixed enumeration, so no user-supplied text ever reaches an FFmpeg filter graph.
Captions and subtitle burn-in are deliberately excluded and deferred to 0.4.0
(spec §3.3, §25.4).
Added
- Cropping (spec FR-025):
--crop <x>:<y>:<w>:<h>in orientation-normalized
source pixels, applied before scaling, so aspect-ratio preservation, the
profile maximum width, and the no-upscale rule all evaluate against the cropped
rectangle. The requested rectangle is applied exactly — never rounded, clamped,
re-centered, or expanded; a rectangle the decoded pixel format cannot express
triggers a conversion to a non-subsampled format instead of an adjustment. - Explicit resizing (spec FR-026):
--widthand the new--height, both
integers in the closed range 2–8192. They are maximum bounds with the aspect
ratio preserved; supplying both fits the frame inside that box. An explicit
bound overrides the effective quality profile's maximum width. Upscaling
remains gated by--allow-upscale, and an oversized request is clamped back to
the effective source size. - Playback-speed adjustment (spec FR-027):
--speed, a decimal multiplier
from 0.25 to 4.0 with at most three fractional digits, implemented by retiming
presentation timestamps. The selected source range is unchanged; the output GIF
duration becomesround(durationMs / speed). No frames are interpolated, and
retiming is applied before frame-rate conversion so the requested fps describes
the finished GIF. - Dithering control (spec FR-028):
--ditherwith the fixed enumeration
none,bayer,floyd_steinberg,sierra2,sierra2_4a, plus
--bayer-scale(0–5) forbayer. Documented per-profile defaults (§15.5):
bayerwith scale 5 forsmall,sierra2_4aforbalanced,high, and
custom— these reproduce 0.1.0/0.2.0 output, so a job that specifies no
dither is functionally equivalent to earlier releases. previewsubcommand (spec FR-029, §12.9): extracts a single full-colour
PNG still instead of producing a GIF.preview --input <source> --at <timestamp>for one frame, orpreview --manifest <manifest>for one still
per clip at that clip's start timestamp with that clip's effective
transformations. Preview output is never palette-quantized. Crop, resize, and
orientation normalization apply exactly as they would for the corresponding
GIF;speed,fps,loop,colors,dither, andbayerScaleare accepted,
change nothing, and produce oneTRANSFORMATION_NOT_APPLICABLEwarning.
--dry-run, collision policies, project-boundary rules, and remote sources all
apply unchanged. Generated names are<video-stem>_<at>.png, or
<clip-name>_<start>.pngin the manifest form.- Configuration: a
transformationsobject in.video-to-gif.jsonwith
width,height,speed,dither, andbayerScale(spec §9.6).cropis
not permitted there — a rectangle is only meaningful against one specific
source — andvalidate-configrejects it with the field path
transformations.crop. - Manifest fields (spec §10.4, §11.2):
crop,width,height,speed,
dither, andbayerScaleat the top level, at the clip level, or both, in
both JSON and CSV manifests. JSON accepts either the object form
{ "x", "y", "width", "height" }or the string form"x:y:width:height"; CSV
uses the string form in acropcolumn, where an empty cell means "not
specified for this row". - Transformation precedence (spec FR-024, refining §9.3): clip-level manifest
field > command-line flag > top-level manifest field > project configuration >
built-in default. A per-clip value is more specific than a batch-wide flag and
wins; every non-transformation setting follows §9.3 unchanged. - Result reporting (spec FR-030): every
createdentry gains a
transformationsobject (crop,sourceWidth,sourceHeight,
effectiveSourceWidth,effectiveSourceHeight,speed,dither,
bayerScale,upscaled) andoutputDurationMs; apreviewsarray carries
path,atMs,width,height,sizeBytes, and the sametransformations
object;summarygains apreviewscount. Preview entries never appear in
createdand are never counted bysummary.created. Preview extraction emits
progress under stagepreview. - Warnings with stable leading tokens (spec §13.4):
UPSCALE_NOT_ALLOWED
when an explicitly supplied bound was clamped to the effective source size, and
TRANSFORMATION_NOT_APPLICABLEwhen settings a still frame cannot express were
supplied topreview. - Documentation: new
references/transformations.md(spec NFR-007) covering
the crop coordinate model, the width/height bounds and their interaction with
profiles and upscaling, the speed multiplier and its duration math, the dither
enumeration with size/quality guidance, preview frames, per-clip manifest
transformations, and the filter-chain order.
Changed
- Output dimensions are unchanged. Every 0.1.0 and 0.2.0 invocation produces
the same GIF it did: with no transformation specified, output is
byte-comparable to 0.2.0 for the same source, range, profile, and
configuration.--widthkeeps its 0.1.0 meaning as a maximum output width, and
its derived height is unchanged. This was verified by executing the released
0.2.0 engine and 0.3.0 side by side over profile-only,--width, and manifest
widthinvocations across several source geometries and comparing both the
reported dimensions and the produced GIF bytes. Configuration, manifest, and
structured result schema versions all remain1, and no new exit code is
introduced — invalid transformations reuse exit 6 (INVALID_CROP,
INVALID_DIMENSIONS,INVALID_SPEED,INVALID_DITHER), apreview --output-namewith a non-.pngextension reuses exit 2 (INVALID_USAGE), and
a preview collision reuses exit 7. - Dimension parity, stated exactly (spec FR-026). An explicitly supplied
width/heightis honored exactly, odd values included: GIF is a palette-based
format without chroma subsampling, so it imposes no even-dimension constraint
and rounding an explicit bound would silently contradict the request. A
derived dimension is rounded to the nearest integer and may itself be odd —
the same rule 0.1.0 used, on every path (profile-only, width-only,
height-only, and both-bounds). No even-rounding is applied anywhere. - Manifest
widthis now range-checked (rejected-input change). FR-026
requires both dimension bounds to be integers in the closed range 2–8192, and
that check applies to the manifestwidthfield, which existed in 0.1.0. A
manifest carryingwidth: 1orwidth: 10000was accepted by 0.2.0 and is now
rejected withINVALID_DIMENSIONSand exit 6. No previously accepted output
changes — a value in 2–8192 behaves exactly as before — but a manifest relying
on an out-of-range width will need that value corrected. Whitespace-padded
values such as" 480"are unaffected and still accepted: a CSV cell is
trimmed before validation in every column, and a JSON manifestwidthstring
is trimmed exactly as 0.2.0 trimmed it. Fields introduced in 0.3.0 (height,
crop,speed,dither,bayerScale) have no such legacy, so in JSON they
take the strict grammar with no padding allowed. - Dithering is now a public option. The 0.1.0 allowance to change dithering
internally is superseded: an explicitly requested mode andbayerScaleare
honored exactly and will not change across patch releases, while a profile's
default may change only in a minor release with a changelog entry (spec §15.5).
Security
- SEC-018 — Transformation parameter validation. Transformation values become
arguments inside an FFmpeg filter graph and are treated as an injection
surface. Only integers, bounded decimals, and members of the fixed
enumerations are accepted; free text, filter strings, filter-graph fragments,
filter scripts, FFmpeg expressions, and option key-value pairs are rejected
from the command line, manifests, and configuration alike. Every parameter is
parsed and range-checked before any filter graph is constructed, and the
graph is built exclusively from values the engine re-serializes from its own
validated numeric and enum types — user-supplied text is never concatenated
into a filter graph. Any value containing a character outside its grammar is
rejected — this covers at least inner whitespace, inner newlines, and the
characters, ; ' " \ [ ] = % ( ) $ ` *. The colon is permitted only as the
field separator inside--crop, which must contain exactly three colons and
four unsigned integer fields.
Two deliberate exceptions concern surrounding whitespace only, and neither
reaches the filter graph.--ditheris compared after its surrounding
whitespace is trimmed (FR-028), so"sierra2_4a\n"is accepted — what the
engine then emits is the matched enum member, never the supplied text, so a
padded value and a clean one produce byte-identical arguments. A CSV cell is
likewise trimmed before its grammar runs, consistently across every column and
as 0.2.0 did forwidth; the strict grammar still runs on the trimmed text,
so inner whitespace, embedded newlines, and every metacharacter above remain
rejected, and padding a hostile value does not launder it. The
identical validated filter chain is applied to the palette-generation pass and
the encoding pass, so palette generation cannot be driven by a different or
unvalidated parameter set. No user-s...
video-to-gif v0.2.0
Opt-in remote source acquisition: video-to-gif can now download a source from a
remote http/https URL before converting it locally. The capability is
disabled by default and gated by explicit enablement/approval; conversion
itself stays local and network-isolated, and all remote access is download-only.
Added
- Opt-in remote HTTP/HTTPS source acquisition (spec FR-018..023): a URL may be
supplied wherever a source is accepted (--inputor a manifestinputfield),
downloaded into a secure temporary directory and handed to the existing local
pipeline as untrusted local media. - Remote CLI flags:
--allow-remote(enable acquisition for one run),
--keep-remote-source(retain the download and report its path),
--remote-adapter ytdlp(acquire a video-page URL through the optional,
never-bundled yt-dlp adapter),--allow-insecure-http(acknowledge an
unencrypted http transfer), and--allow-remote-address(approve a specific
private/loopback address for the SSRF check; repeatable). - Configuration:
remoteSources(disabled(default) /ask/enabled),
keepRemoteSource, and download limitslimits.maxDownloadBytes
(default 2 GiB) andlimits.maxDownloadSeconds(default 900 s). - Download progress emitted on stderr under stage
download(bytes received
and, when the total size is known, a percentage). - Exit code 14 (
REMOTE_DOWNLOAD_FAILED) for a network error, HTTP error
status, or truncated/timed-out remote download. doctoryt-dlp reporting:doctornow reports whether the optional yt-dlp
adapter is available and, when present, its version. Its absence is never a
failure.
Changed
- A URL supplied under the default configuration now returns
REMOTE_DISABLED
(statusremote_disabled, exit 8) and performs no network access. In 0.1.0 the
same situation returnedUNSUPPORTED_REMOTE_SOURCE; that remains the documented
behavior of the 0.1.0 product.
Security
- SEC-012 — Remote source network boundary. Only the acquisition component is
network-capable; inspection/palette/encode stay network-isolated under SEC-010,
and a downloaded file is treated as untrusted local media. - SEC-013 — URL scheme allowlist.
httpsalways;httponly with an explicit
unencrypted-transfer warning;fileand every other scheme rejected as
UNSUPPORTED_URL_SCHEME(exit 5), enforced before any connection and on every
redirect. - SEC-014 — Private-network / SSRF protection and connection pinning. Loopback,
private, link-local/unique-local, and cloud-metadata addresses are blocked as
PRIVATE_NETWORK_BLOCKED(exit 8) unless explicitly approved; the direct path
resolves the host, validates every address, and pins the connection to the
validated address (DNS-rebinding resistant), re-checking every redirect. - SEC-015 — Credential and token redaction. Any URL echoed anywhere is reduced
to scheme/host/path; signed-URL query strings and userinfo are stripped and
never stored in configuration or manifests. - SEC-016 — Download hardening. Size ceiling enforced on bytes actually
received, wall-clock timeout, and a free-disk pre-check; partial downloads are
always removed. - SEC-017 — DRM and access-control integrity. DRM-protected/access-controlled
sources are rejected asDRM_PROTECTED(exit 5); no circumvention is attempted. - yt-dlp adapter residual risk. The optional yt-dlp adapter performs its own
DNS resolution, connection, and redirect-following that the engine cannot pin.
Its scheme, DRM, and SSRF host checks are enforced best-effort before launch
but do not carry the direct path's connection-pinning guarantee; a
TOCTOU/rebinding change or a redirect yt-dlp follows to a private address is not
guaranteed to be blocked (accepted residual risk, documented per SEC-014).
Fixed
- Marketplace files moved to the repository root (
.claude-plugin/marketplace.json
and.agents/plugins/marketplace.json, previously undermarketplaces/).
claude plugin marketplace add owner/reporesolves the marketplace manifest at
the repository root, so the nested layout made the plugin uninstallable from
the repo. Pluginsourcepaths now correctly resolve to./packages/...from
the root.
video-to-gif v0.1.0
Initial implementation of the video-to-gif Agent Skill: deterministic,
timestamp-based conversion of local video files into optimized animated GIFs,
packaged for both Claude Code and Codex from one shared source.
Added
- Agent Skill (
src/skill/video-to-gif/) withSKILL.md,scripts/,
references/, andassets/, following the open Agent Skills format. - Deterministic Python engine (
scripts/video_to_gif.py+vtg/package),
standard library only, non-interactive, with a structured (--json) result
contract and JSON Lines progress on stderr. - CLI commands (spec §12):
doctor,inspect,create,batch
(including--dry-runpreflight),validate-config, andvalidate-manifest;
createsupports--startwith either--endor--duration, and an
explicit--output-name. - Timestamp handling: accepts
SS,SS.mmm,MM:SS,HH:MM:SSforms and
normalizes to integer milliseconds; preflight validation before any encoding
withfail/skip/clamppolicies (defaultfail). - Batch conversion from repeated ranges, JSON manifests, and CSV manifests,
withcontinueOnError(default true) and per-clip created/failed/skipped
reporting. - Quality profiles
small,balanced,high, andcustom(widths are
maximums; aspect ratio preserved; no upscaling by default). - FFmpeg two-pass palette pipeline (
palettegen→paletteuse) writing to a
temporary path and atomically moving verified output into place. - Project configuration (
.video-to-gif.json) with documented precedence:
CLI arg > request instruction > project config > built-in default. - Collision protection: engine never overwrites by default; policies
fail,
overwrite,unique,skip. - Cancellation and cleanup that stop FFmpeg, remove partial/palette/temporary
files, and preserve completed GIFs. - Security controls SEC-001..SEC-011, including FFmpeg network isolation
(-protocol_whitelist file,pipe, rejection of reference-following containers)
and resource limits (default 600 s wall-clock / 2 GiB temporary disk per clip;
RESOURCE_LIMIT_EXCEEDED, exit 13). - Packaging tooling:
tools/build_packages.py(generates the Claude and Codex
plugin packages from canonical source, verifies byte-identical copies, rejects
media/temp files, emits SHA-256 checksums) andtools/validate_release.py
(release integrity checks per spec §21.4). - Marketplace metadata for Claude Code (
.claude-plugin/marketplace.json)
and Codex (.agents/plugins/marketplace.json), pinning version 0.1.0. - Documentation:
README.md,SECURITY.md,CONTRIBUTING.md, and
docs/architecture.md,docs/security.md,docs/release-process.md. - Continuous integration matrix across Ubuntu, macOS, and Windows on Python
3.10 and 3.12 (spec §22.3).
Fixed
- Windows cancellation and temp-file cleanup (spec §6.1, §16, SEC-011). The
engine now registers aSIGBREAKhandler on Windows alongsideSIGINT/
SIGTERM, so aCTRL_BREAK_EVENTdelivered to its process group cancels
cleanly. All cleanup paths (resource-limit breach, cancellation, failure, and
the conversion pipeline's temp-dir removal) now wait for the terminated FFmpeg
process to exit and delete temp artifacts with a bounded retry, so partial
.vtg-*.gif.tmpoutput andvtg-*palette temp directories no longer leak on
Windows where a just-killed process briefly holds its file handles. The
cancellation test harness launches the engine with
CREATE_NEW_PROCESS_GROUPand cancels viaCTRL_BREAK_EVENTon Windows
(SIGINT elsewhere). - Portable structured paths on Windows (spec §6.1, §13). The
pathand
outputDirectoryfields of the JSON result now always use forward slashes, so
the structured contract is deterministic across platforms instead of leaking
Windows\separators. The test fixtures also clean their own temp
directories with a bounded retry so a subprocess's transient handle lock on
Windows cannot fail teardown.
Security
- Local-only processing with no network access in 0.1.0; URL sources return
UNSUPPORTED_REMOTE_SOURCE(SEC-005). Network isolation is enforced at the
FFmpeg layer (SEC-010). SeeSECURITY.md.
Notes
- Resolved decisions (spec §26): repository
giffify, plugin name
video-to-gif, maintainer Krishna2709, license MIT. Remaining open items
(exact profile values and others) are tracked in spec §26.