Did you know that various AI chatbots can actually write GMIC scripts?
I thought GMIC was too obscure (sorry David
) but I tried one and it knew how to do it.
Might be fun for you experienced guys to see how good or bad they are at it.
Even when you frown on it or donât need it I was thinking: what if you use it to add comments as to what each line does? Seems that might be convenient - if it works.
Or how about using it to exchange long form for short form or vice versa?
I saw that David Tschumperle has in the past remarked he uses chatbots to translate, so heâs not adverse to them it seems, but did he think of using it for such things I wonder. Commenting code and using long-form to illustrate things is a pesky thing Iâm sure, but what if a bot can do it?
Yes, actually that is the first thing I do when testing a new LLM model : ask for the creation of some custom GâMIC scripts. I think this is a good test because GâMIC is not a common programming language, and there is not a lot of documentation about it.
A few weeks ago, Iâve been surprised by Google Gemini 2.5, which was able to produce some basic GâMIC scripts to draw things in images. That is the first time a LLM produced a working GâMIC command out-of-the-box, without requiring additionnal iterations of prompting.
Definitely, those things are really useful for a lot of tasks. The progress made by LLMs those last few years is really impressive. I keep an eye on them, and I test them when I can. They are the future of human-machine interaction.
That is interesting. If I had a LLM buddy that played nice, I am sure I would be more motivated to dive into GâMIC again, or at least update/refine my existing filters/commands.
Here is a typical example of interaction Iâm trying (here with Gemini 2.5) :
Iâm pretty sure this can be useful indeed, to learn some basics of the GâMIC language!
I actually used ChatGPT to solve some few problems (After verifications). So, it can be a good tool of last resort.
Another example, which I find really impressive :
Prompt:
Here is the definition of a g'mic command that I don't understand : "add_copymark :
foreach { nm={n} 0 => $nm . =>[0] {n} k[0] }
". Can you explain me what does it do, globally and step by step ?
Answer (Google Gemini 2.5):
Model
 Thoughts (experimental)
Auto
Expand to view model thoughts
chevron_right
The G'MIC command add_copymark is designed to append a "copymark" suffix to the names of selected images. This is particularly useful for distinguishing images that have been duplicated or modified. Let's break down its functionality, both in terms of its overall purpose and a step-by-step analysis of its definition.
### Global Functionality
In essence, the add_copymark command iterates through each of the currently selected images and renames them by adding a suffix. This prevents naming conflicts when you've made copies of images and want to keep track of the originals.
### Step-by-Step Explanation
The definition of the add_copymark command is as follows:
code Gmic
```
add_copymark :
foreach { nm={n} 0 => $nm . =>[0] {n} k[0] }
```
Here is a detailed breakdown of each component of this command:
1. **foreach { ... }** : This initiates a loop that will execute the commands contained within the curly braces for each image in the current selection. G'MIC processes images in a list, and foreach allows for individual manipulation of each image in that list.[[1](https://www.google.com/url?sa=E&q=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fgrounding-api-redirect%2FAUZIYQE2Q60lZU9a_0U9idd79RjaaPnJWgrM_tLpSs7IN3h6A6hN--Zi8fhESfYXfKBeuPHVapmOdszg_VVqB0Rvk2YeZrJkMrU2TA7iM07hhHXGaDlKrGV-uGlUUf0IpgerLKhGwDHztC-JGYJnQ4Jr-xwxqEklVbUn-469H05fHRgn)]
2. **nm={n}** : Inside the foreach loop, this is the first action performed for each image.
* {n} is a substitution expression that retrieves the name of the current image being processed in the loop.[[2](https://www.google.com/url?sa=E&q=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fgrounding-api-redirect%2FAUZIYQFZGIqzeO5mcSnH-CPTq5uB6laKH1GxbJy724isEDCVKeunLdvRaWBfIN2eCmUkPtslEYpRqjtetKdvYbLX_eK9Y2L94toqfyT-ROjNO-cihS6dMFxvGXMTLbAtENr1wv2zCf44UBFvyfXpJXw%3D)]
* nm= is a variable assignment. It creates a new variable named nm and assigns the name of the current image to it.[[3](https://www.google.com/url?sa=E&q=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fgrounding-api-redirect%2FAUZIYQELgyPQH15RCFkVMLSjNQGrUZpwebt0LNNtrIMKQkbDBEMOFAXANlixNO8a6xYtevJYEOp-dYjJVjEZbCY3bQZUa1MPdh3Dg-MT8qkgtQk1yHKMJkGx9mEVPjmHeGpysD421g66tAwAl2QHemX-uueefA3U4tDljfSAgMQ%3D)] So, for each image, nm will hold that image's name.
3. **0 => $nm** : This part of the command renames a newly created image.
* 0 creates a new, empty image.
* => is a shortcut for the name command, which is used to assign a name to an image.[[4](https://www.google.com/url?sa=E&q=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fgrounding-api-redirect%2FAUZIYQHCIgJTW2l2fcgVB9KN8FuudOg0GhKz9fh4-mFOReBorNm70khaIKNcZ4VDitwSmxaRia8G6AltDVjQefZEBQqPz_2g-5NhYJQRCUouMHGIz2DBfouUoJmW0HcZRYsWLg%3D%3D)]
* $nm retrieves the value of the variable nm (which, as we established, is the name of the current image from the foreach loop).
* Therefore, 0 => $nm creates a new empty image and gives it the same name as the original image currently being processed.
4. **. =>[0] {n}** : This is where the actual renaming of the original image happens.
* The dot . is a shortcut to select the last image in the list. In the context of the foreach loop, this refers to the original image currently being processed.
* =>[0] uses the name command to assign a new name to the selected image (the original one). The [0] specifies that the name should be taken from the image at index 0, which is the newly created empty image.
* {n} in this context appends the original name as a suffix. When used in the name command in this manner, it effectively renames the target image to have the name of the image at index 0, followed by its own original name. Since the image at index 0 has the original name, this results in the original name being appended to itself, with a separator. The "copymark" is the underscore that G'MIC automatically adds when concatenating names in this way.
5. **k[0]** : This is the final step within the loop for each image.
* k is a shortcut for the keep command.[[5](https://www.google.com/url?sa=E&q=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fgrounding-api-redirect%2FAUZIYQGc6Hv89noy6dKAGLrG-9YgUZShgBEHqKNw07X2mAqnmxPcx6bW_i7b1jsLpCBeGf6-Vn5Qm4Gkbx0q-TtKq0GUtOjT0swx0wqy-_EMGFYZ7K03h5N0fJQWlFQm4Kab)]
* [0] specifies that only the image at index 0 should be kept.
* At this point in the loop, the image list for the current iteration contains the newly named original image and the temporary empty image. This command removes the temporary empty image, leaving only the renamed original image.
After the foreach loop completes this sequence for all selected images, the result is that every image in the original selection has been renamed with its original name appended as a suffix (e.g., "image_name_copy").
I would also be of the mindset that if you really persist, explain things to the LLM very carefully, it can often produce code that meets your wishes.
I used to do videos and put them on YouTube (of music by artists I liked but also my own experimental composition) - I would often (mainly in fact) write Python scripts which would produce batch files which ran in cmd (when I was using Windoze) with calls to gmic cli, using metadata I gleaned from using Sonic Annotator (which read an audio file and produced a CSV of, sometimes 10âs of thousands of values per file) to inform 1 or 2 parameters of, for instance, a Deformations filter like Distort Lens or otherwise to make the visuals match the audio in an âinterestingâ way! See for example my video for this XTC song: XTC - Senses Working Overtime : Free Download, Borrow, and Streaming : Internet Archive
Now I âonlyâ have an Android tablet - I canât use Sonic Annotator, but obviously I can use gmic from the command line, so I asked ChatGPT for a similar type of audio/video manipulation, I asked it to include a call to gmic, and after going back and forth about 6 or 7 times it eventually came up with this (pretty cool) Python script (the gmic call just uses the mul command but itâs integral to the results.)
(gmic call is on line 165 I believe)
#!/usr/bin/env python3
"""
Audioâdriven brightness modulator for video (Termuxâfriendly)
Pipeline (single command):
1) Reads video FPS + duration via ffprobe.
2) Decodes audio to raw float32 mono via ffmpeg (no heavy Python deps).
3) Computes RMS per video frame and maps to [0..1] factor using dB scaling.
4) Extracts video frames with ffmpeg at exact FPS.
5) Multiplies each frame by its factor using G'MIC (brightness to black).
6) Reassembles processed frames with original audio via ffmpeg.
Usage:
python audio_driven_brightness_termux.py \
--video input.mp4 --audio audio.wav --out output.mp4 \
[--floor-db -60] [--ceil-db 0] [--fps auto] [--sr 48000] \
[--ext jpg] [--qscale 3] [--keep]
Notes:
- "floor-db" is the silence floor. Anything <= floor-db maps to factor=0 (black).
- "ceil-db" is the loud reference. Anything >= ceil-db maps to factor=1 (original).
- Works when audio and video durations are equal (or very close). Final video is clipped to the shorter stream.
- Requires: ffmpeg, gmic, python3 (stdlib only), and a POSIX shell (Termux OK).
"""
import argparse
import json
import math
import os
import shutil
import subprocess
import sys
from fractions import Fraction
from pathlib import Path
def run(cmd, **kwargs):
"""Run a command, raise on error, return CompletedProcess."""
print("$", " ".join(cmd))
return subprocess.run(cmd, check=True, **kwargs)
def capture(cmd):
"""Run a command and capture stdout as text."""
out = subprocess.check_output(cmd)
return out.decode("utf-8", "replace")
def probe_fps_duration(video_path):
# Get r_frame_rate and duration via ffprobe JSON
info = capture([
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=r_frame_rate:format=duration",
"-of", "json",
video_path
])
data = json.loads(info)
# FPS
r = data["streams"][0]["r_frame_rate"] # e.g. "25/1"
num, den = r.split("/")
fps = float(Fraction(int(num), int(den)))
# Duration (seconds)
duration = float(data["format"]["duration"]) if "format" in data and "duration" in data["format"] else None
return fps, duration
def decode_audio_to_pcm(audio_path, sr=48000):
"""Decode to mono float32 PCM via ffmpeg and return bytes."""
proc = subprocess.Popen(
[
"ffmpeg", "-y", "-i", audio_path,
"-ac", "1", "-ar", str(sr), "-f", "f32le", "-",
],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
raw = proc.stdout.read()
proc.stdout.close()
ret = proc.wait()
if ret != 0:
raise RuntimeError("ffmpeg failed to decode audio")
return raw
def pcm_to_rms_per_frame(raw_bytes, sr, fps, duration_s):
import array
# Convert bytes to float32 array without NumPy
floats = array.array('f')
floats.frombytes(raw_bytes)
total_samples = len(floats)
if duration_s is None:
duration_s = total_samples / sr
n_frames = int(round(duration_s * fps))
if n_frames <= 0:
raise ValueError("Computed zero frames; check FPS/duration")
samples_per_frame_f = sr / fps
rms_list = []
pos = 0
for i in range(n_frames):
start = int(round(i * samples_per_frame_f))
end = int(round((i + 1) * samples_per_frame_f))
if end > total_samples:
end = total_samples
if end <= start:
# No samples in this slice (e.g., last partial frame); repeat last value or 0
val = rms_list[-1] if rms_list else 0.0
rms_list.append(val)
continue
# Compute RMS for [start:end)
acc = 0.0
cnt = end - start
# Manual RMS (no NumPy)
for s in floats[start:end]:
acc += s * s
mean_sq = acc / cnt
rms = math.sqrt(mean_sq)
rms_list.append(rms)
return rms_list
def rms_to_db_factors(rms_list, floor_db=-60.0, ceil_db=0.0):
"""Map per-frame RMS to [0..1] via dB scaling with clamping."""
factors = []
eps = 1e-12
for rms in rms_list:
db = 20.0 * math.log10(max(rms, eps)) # -inf..0
if db <= floor_db:
f = 0.0
elif db >= ceil_db:
f = 1.0
else:
f = (db - floor_db) / (ceil_db - floor_db)
factors.append(f)
return factors
def write_text_lines(path, values, fmt="{:.6f}"):
with open(path, "w") as f:
for v in values:
f.write((fmt.format(v) if isinstance(v, (float, int)) else str(v)) + "\n")
def extract_frames(video_path, out_dir, fps, ext="jpg", qscale=3):
out_dir.mkdir(parents=True, exist_ok=True)
pat = str(out_dir / f"frame_%06d.{ext}")
# Use fps filter to force exact count at CFR
run([
"ffmpeg", "-y", "-i", video_path,
"-vf", f"fps={fps}", "-qscale:v", str(qscale), pat
])
return pat
def process_frames_with_gmic(frames_dir, out_dir, factors, ext="jpg"):
out_dir.mkdir(parents=True, exist_ok=True)
n = len(factors)
for i, factor in enumerate(factors, start=1):
inp = frames_dir / f"frame_{i:06d}.{ext}"
outp = out_dir / f"frame_{i:06d}.{ext}"
if not inp.exists():
# Stop if frames ended early
break
# Multiply brightness uniformly across channels
# Using -clamp afterwards to avoid over/underflow surprises
run(["gmic", str(inp), "-mul", f"{factor:.6f}", "-o", str(outp)])
def assemble_video_from_frames(frames_dir, fps, audio_path, out_path, ext="jpg"):
pat = str(frames_dir / f"frame_%06d.{ext}")
cmd = [
"ffmpeg", "-y", "-framerate", f"{fps}", "-i", pat,
"-i", audio_path,
"-shortest",
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18",
"-c:a", "aac", "-b:a", "192k",
out_path,
]
run(cmd)
def main():
ap = argparse.ArgumentParser(description="Audioâdriven brightness for video using ffmpeg+G'MIC (Termuxâfriendly)")
ap.add_argument("--video", required=True, help="Input video (no audio used from it)")
ap.add_argument("--audio", required=True, help="External audio (same duration as video)")
ap.add_argument("--out", required=True, help="Output video path")
ap.add_argument("--floor-db", type=float, default=-60.0, help="Silence floor dB â factor 0")
ap.add_argument("--ceil-db", type=float, default=0.0, help="Loud ref dB â factor 1")
ap.add_argument("--fps", default="auto", help="Frames per second (default: auto from video)")
ap.add_argument("--sr", type=int, default=48000, help="Audio sample rate for analysis")
ap.add_argument("--ext", default="jpg", choices=["jpg", "png"], help="Intermediate frame format")
ap.add_argument("--qscale", type=int, default=3, help="FFmpeg qscale for frame extraction (2=best, 5=ok)")
ap.add_argument("--keep", action="store_true", help="Keep temp folders")
args = ap.parse_args()
for binname in ("ffmpeg", "ffprobe", "gmic"):
if shutil.which(binname) is None:
print(f"ERROR: Required tool not found: {binname}")
sys.exit(2)
video = args.video
audio = args.audio
out = args.out
fps, duration = probe_fps_duration(video)
if args.fps != "auto":
fps = float(args.fps)
if fps <= 0:
raise ValueError("Invalid FPS from video; specify --fps")
work = Path("_audbright_work")
frames_dir = work / "frames"
proc_dir = work / "proc"
work.mkdir(exist_ok=True)
print("[1/6] Decoding audio â PCM ...")
raw = decode_audio_to_pcm(audio, sr=args.sr)
print("[2/6] Computing RMS per frame ...")
rms = pcm_to_rms_per_frame(raw, sr=args.sr, fps=fps, duration_s=duration)
print("[3/6] Mapping RMSâdBâfactors ...")
factors = rms_to_db_factors(rms, floor_db=args.floor_db, ceil_db=args.ceil_db)
write_text_lines(work / "factors.txt", factors)
print("[4/6] Extracting frames ...")
extract_frames(video, frames_dir, fps=fps, ext=args.ext, qscale=args.qscale)
print("[5/6] Applying brightness with G'MIC ... (this can take time)")
process_frames_with_gmic(frames_dir, proc_dir, factors, ext=args.ext)
print("[6/6] Reassembling video ...")
assemble_video_from_frames(proc_dir, fps=fps, audio_path=audio, out_path=out, ext=args.ext)
if not args.keep:
shutil.rmtree(work, ignore_errors=True)
print("Done:", out)
if __name__ == "__main__":
main()
It helped produce this: Erik Satie - Gnossiennes : Free Download, Borrow, and Streaming : Internet Archive
The input video was done using one of the ultrawarp filters combined with fx_morph_layers IIRC⌠Then the AI did the rest with the audio levels informing the brightness / opacity via gmic.
Iâm interested to know how you cleared the copyright aspect of doing this?
@EspE1
Not sure really, i never had a problem until something from Stereolab was flagged⌠so I got back to YouTube explaining i used my own copy of the song, it was not a monetized channel so no revenue was being taken from the artist and that it was a kind of âtransformativeâ work⌠as the combination of their music with my animations made it into a separate artwork (I think I was stretching it with that oneâŚ).
Anyway, that seemed to work, as I didnât have a problem after that, with most artists, except with stuff by the Cocteau Twins for some reason.




