From 0c96229a238896496e6f3301780011b3b36538e4 Mon Sep 17 00:00:00 2001 From: twinsant Date: Sat, 15 Aug 2026 07:56:10 +0800 Subject: [PATCH 1/3] Add: H3_MODEL_DIR env. --- README.md | 10 ++++++++++ main.c | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/README.md b/README.md index 4750ac4..4870edf 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,16 @@ mkdir -p outputs mapping all weights or generating media. Run `./h3 --help` for the complete CLI reference. +The model directory can also be supplied through `H3_MODEL_DIR`: + +```sh +export H3_MODEL_DIR=./MiniMax-H3 +./h3 --info +``` + +The `-d`/`--model-dir` option takes precedence when both are provided. An empty +`H3_MODEL_DIR` is ignored. + Without `-p`, the same binary starts an Iris-style interactive session: ```sh diff --git a/main.c b/main.c index 7f11e47..b84d504 100644 --- a/main.c +++ b/main.c @@ -19,6 +19,7 @@ static void usage(const char *program) { " %s -d MODEL_DIR --info\n\n" "Options:\n" " -d, --model-dir PATH MiniMax-H3 local directory\n" + " (or set H3_MODEL_DIR)\n" " -p, --prompt TEXT Raw H3 prompt\n" " -o, --output PATH Output MP4 (default: outputs/h3.mp4)\n" " --width N Output width (default: 864)\n" @@ -464,6 +465,10 @@ int main(int argc, char **argv) { default: usage(argv[0]); return 2; } } + const char *env_model_dir = getenv("H3_MODEL_DIR"); + if ((!model_dir || !*model_dir) && env_model_dir && *env_model_dir) { + model_dir = env_model_dir; + } if (!model_dir) { usage(argv[0]); return 2; From 96c04058e81eb77c2aded17cc5a8a47059446392 Mon Sep 17 00:00:00 2001 From: twinsant Date: Tue, 18 Aug 2026 12:23:43 +0800 Subject: [PATCH 2/3] Add generation locks and version flag --- README.md | 10 ++++++- main.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4870edf..4a21af4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ mkdir -p outputs `--info` checks the model layout and prints the selected Metal device without mapping all weights or generating media. Run `./h3 --help` for the complete CLI -reference. +reference. Use `./h3 --version` to print the h3 version without loading a model. The model directory can also be supplied through `H3_MODEL_DIR`: @@ -350,6 +350,14 @@ prompt, seed, resolution, frame count, and step count. factor without resizing the generated video or the encoded terminal image. - `--frames-dir DIR` writes final callback frames as PPM files. Intermediate `--show` previews are not written there. +- Single-shot generation takes a non-blocking per-output lock. A concurrent + process targeting the same output or `--frames-dir` exits with an error; + separate output paths and frame directories can still run in parallel. +- Lock files use the `.h3.lock` suffix and contain the owning process ID. They + are released by the OS if the process exits unexpectedly. +- All generation sessions also take a global process lock at + `/tmp/h3-process.h3.lock`, so Metal and unified-memory use is serialized. + Set `H3_PROCESS_LOCK` to use a different lock location. - `-o ''` disables MP4 encoding; combine it with `--frames-dir` when FFmpeg is unavailable. - `--profile` reports phase wall time, Metal encoding/wait time, peak live diff --git a/main.c b/main.c index b84d504..4ee0382 100644 --- a/main.c +++ b/main.c @@ -6,11 +6,61 @@ #include #include #include +#include #include #include #include #include +#include +#include #include +#include + +typedef struct { + int fd; + char path[PATH_MAX]; +} output_lock; + +static int acquire_lock(output_lock *lock, const char *target, + const char *label) { + int length = snprintf(lock->path, sizeof(lock->path), "%s.h3.lock", + target); + if (length < 0 || (size_t)length >= sizeof(lock->path)) { + fprintf(stderr, "h3: %s path is too long for lock\n", label); + return 0; + } + lock->fd = open(lock->path, O_CREAT | O_RDWR, 0644); + if (lock->fd < 0) { + fprintf(stderr, "h3: cannot open %s lock %s: %s\n", label, + lock->path, strerror(errno)); + return 0; + } + if (flock(lock->fd, LOCK_EX | LOCK_NB) != 0) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + fprintf(stderr, "h3: %s is already being generated: %s\n", + label, target); + else + fprintf(stderr, "h3: cannot lock %s %s: %s\n", label, + target, strerror(errno)); + close(lock->fd); + lock->fd = -1; + return 0; + } + if (ftruncate(lock->fd, 0) == 0) { + char pid[32]; + int pid_length = snprintf(pid, sizeof(pid), "%ld\n", (long)getpid()); + if (pid_length > 0) write(lock->fd, pid, (size_t)pid_length); + } + return 1; +} + +static void release_lock(output_lock *lock) { + if (lock->fd >= 0) { + flock(lock->fd, LOCK_UN); + close(lock->fd); + lock->fd = -1; + } +} static void usage(const char *program) { fprintf(stderr, @@ -60,6 +110,7 @@ static void usage(const char *program) { " --zoom N Terminal image zoom (default: 2 for Retina)\n" " --profile Print per-phase Metal timing and allocation data\n" " --info Inspect model/device without mapping weights\n" + " -v, --version Show h3 version\n" " -h, --help Show this help\n", program, program, program); } @@ -305,6 +356,7 @@ int main(int argc, char **argv) { {"zoom", required_argument, NULL, OPT_ZOOM}, {"profile", no_argument, NULL, OPT_PROFILE}, {"info", no_argument, NULL, OPT_INFO}, + {"version", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; @@ -321,13 +373,17 @@ int main(int argc, char **argv) { int frames_given = 0; int seconds_given = 0; int seed_given = 0; + output_lock process_guard = {-1, {0}}; + output_lock output_guard = {-1, {0}}; + output_lock frames_guard = {-1, {0}}; int option; - while ((option = getopt_long(argc, argv, "d:p:o:h", options, NULL)) != -1) { + while ((option = getopt_long(argc, argv, "d:p:o:hv", options, NULL)) != -1) { switch (option) { case 'd': model_dir = optarg; break; case 'p': prompt = optarg; break; case 'o': output = optarg; break; case 'h': usage(argv[0]); return 0; + case 'v': printf("h3 %s\n", H3_VERSION); return 0; case OPT_WIDTH: params.width = parse_int(optarg, "width"); break; case OPT_HEIGHT: params.height = parse_int(optarg, "height"); break; case OPT_RENDER_WIDTH: @@ -477,6 +533,13 @@ int main(int argc, char **argv) { fprintf(stderr, "h3: --seconds and --frames are mutually exclusive\n"); return 2; } + int needs_process_lock = prompt || !info; + if (needs_process_lock) { + const char *lock_target = getenv("H3_PROCESS_LOCK"); + if (!lock_target || !*lock_target) lock_target = "/tmp/h3-process"; + if (!acquire_lock(&process_guard, lock_target, "generation process")) + return 1; + } if (prompt && params.steps >= 2 && params.steps <= 7 && params.denoise_reuse > 1) { fprintf(stderr, @@ -491,10 +554,24 @@ int main(int argc, char **argv) { cli.frames_dir, strerror(errno)); return 1; } + if (prompt && output && *output && + !acquire_lock(&output_guard, output, "output")) { + release_lock(&process_guard); + return 1; + } + if (prompt && cli.frames_dir && + !acquire_lock(&frames_guard, cli.frames_dir, "frames directory")) { + release_lock(&output_guard); + release_lock(&process_guard); + return 1; + } if (profile) setenv("H3_PROFILE", "1", 1); h3_ctx *ctx = h3_load_dir(model_dir); if (!ctx) { fprintf(stderr, "h3: %s\n", h3_last_error(NULL)); + release_lock(&frames_guard); + release_lock(&output_guard); + release_lock(&process_guard); return 1; } if (info) print_info(ctx); @@ -520,15 +597,22 @@ int main(int argc, char **argv) { if (cli.active) fputc('\n', stderr); fprintf(stderr, "h3: %s\n", h3_last_error(ctx)); h3_free(ctx); + release_lock(&frames_guard); + release_lock(&output_guard); + release_lock(&process_guard); return 1; } h3_result_free(result); if (output && *output) fprintf(stderr, "h3: wrote %s\n", output); if (cli.frames_dir) fprintf(stderr, "h3: wrote frames to %s\n", cli.frames_dir); + release_lock(&frames_guard); + release_lock(&output_guard); + release_lock(&process_guard); } else if (!info) { int cli_status = h3_cli_run(ctx, model_dir, ¶ms, show, seed_given); h3_free(ctx); + release_lock(&process_guard); return cli_status; } h3_free(ctx); From 559c0c01ae1e4b04dc42ea760d586bfb871e62fb Mon Sep 17 00:00:00 2001 From: twinsant Date: Wed, 19 Aug 2026 08:20:43 +0800 Subject: [PATCH 3/3] feat: add CLI log file output --- Makefile | 3 ++- h3.h | 2 +- h3_cli.c | 2 ++ h3_log.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ h3_log.h | 19 +++++++++++++++++++ main.c | 28 ++++++++++++++++++++++++++-- 6 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 h3_log.c create mode 100644 h3_log.h diff --git a/Makefile b/Makefile index bb20237..bb6c760 100644 --- a/Makefile +++ b/Makefile @@ -13,9 +13,10 @@ LIB_C := h3.c h3_host.c h3_safetensors.c h3_weights.c h3_text_encoder.c \ LIB_C += h3_video_vae.c h3_video_encoder.c h3_audio_vae.c h3_ffmpeg.c \ h3_terminal.c h3_vision_encoder.c h3_multimodal.c +CLI_C := h3_log.c LIB_M := h3_metal.m h3_gpu.m h3_tokenizer.m LIB_OBJ := $(LIB_C:.c=.o) $(LIB_M:.m=.o) -CLI_OBJ := main.o h3_cli.o linenoise.o +CLI_OBJ := main.o h3_cli.o linenoise.o $(CLI_C:.c=.o) .PHONY: all test parity real-parity clean diff --git a/h3.h b/h3.h index 29640b3..05cb8d5 100644 --- a/h3.h +++ b/h3.h @@ -9,7 +9,7 @@ extern "C" { #endif -#define H3_VERSION "0.1.0-dev" +#define H3_VERSION "0.1.1-dev" #define H3_DEFAULT_WIDTH 864 #define H3_DEFAULT_HEIGHT 480 #define H3_DEFAULT_FRAMES 56 diff --git a/h3_cli.c b/h3_cli.c index 79339c8..11da1ec 100644 --- a/h3_cli.c +++ b/h3_cli.c @@ -2,6 +2,7 @@ #include "h3_ffmpeg.h" #include "h3_host.h" +#include "h3_log.h" #include "h3_terminal.h" #include "linenoise.h" @@ -120,6 +121,7 @@ static int cli_progress(const char *phase, int completed, int total, state->total = total; state->progress_active = completed < total; fprintf(stderr, "\r%-25s %4d/%-4d", phase, completed, total); + h3_log_progress(phase, completed, total); if (!state->progress_active) fputc('\n', stderr); fflush(stderr); return 0; diff --git a/h3_log.c b/h3_log.c new file mode 100644 index 0000000..20d713f --- /dev/null +++ b/h3_log.c @@ -0,0 +1,48 @@ +#include "h3_log.h" + +#include + +static h3_log *active_log; + +int h3_log_open(h3_log *log, const char *path) { + if (!log) return 0; + log->file = NULL; + if (!path || !*path) return 1; + log->file = fopen(path, "ab"); + if (!log->file) return 0; + setvbuf(log->file, NULL, _IOLBF, 0); + return 1; +} + +void h3_log_close(h3_log *log) { + if (!log) return; + if (active_log == log) active_log = NULL; + if (log->file) fclose(log->file); + log->file = NULL; +} + +void h3_log_set_active(h3_log *log) { + active_log = log; +} + +int h3_log_fprintf(FILE *stream, const char *format, ...) { + va_list arguments; + va_start(arguments, format); + int result = vfprintf(stream, format, arguments); + va_end(arguments); + if (active_log && active_log->file && stream == stderr) { + va_start(arguments, format); + vfprintf(active_log->file, format, arguments); + va_end(arguments); + fflush(active_log->file); + } + return result; +} + +void h3_log_progress(const char *phase, int completed, int total) { + if (!active_log || !active_log->file) return; + fprintf(active_log->file, + "progress phase=%s completed=%d total=%d\n", + phase, completed, total); + fflush(active_log->file); +} \ No newline at end of file diff --git a/h3_log.h b/h3_log.h new file mode 100644 index 0000000..d3cf690 --- /dev/null +++ b/h3_log.h @@ -0,0 +1,19 @@ +#ifndef H3_LOG_H +#define H3_LOG_H + +#include + +typedef struct { + FILE *file; +} h3_log; + +int h3_log_open(h3_log *log, const char *path); +void h3_log_close(h3_log *log); +void h3_log_set_active(h3_log *log); +int h3_log_fprintf(FILE *stream, const char *format, ...) + __attribute__((format(printf, 2, 3))); +void h3_log_progress(const char *phase, int completed, int total); + +#define fprintf(stream, ...) h3_log_fprintf((stream), __VA_ARGS__) + +#endif \ No newline at end of file diff --git a/main.c b/main.c index 4ee0382..07ecf94 100644 --- a/main.c +++ b/main.c @@ -1,6 +1,7 @@ #include "h3.h" #include "h3_cli.h" #include "h3_host.h" +#include "h3_log.h" #include "h3_terminal.h" #include @@ -109,6 +110,7 @@ static void usage(const char *program) { " --show Display a frame after every denoising step (M5)\n" " --zoom N Terminal image zoom (default: 2 for Retina)\n" " --profile Print per-phase Metal timing and allocation data\n" + " --log-file PATH Append CLI diagnostics and progress to PATH\n" " --info Inspect model/device without mapping weights\n" " -v, --version Show h3 version\n" " -h, --help Show this help\n", @@ -218,6 +220,7 @@ static int cli_progress(const char *phase, int completed, int total, state->total = total; state->active = completed < total; fprintf(stderr, "\r%-25s %4d/%-4d", phase, completed, total); + h3_log_progress(phase, completed, total); if (!state->active) fputc('\n', stderr); fflush(stderr); return 0; @@ -303,7 +306,7 @@ int main(int argc, char **argv) { OPT_FIRST, OPT_LAST, OPT_REF_IMAGE, OPT_REF_IMAGE_SIZE, OPT_REF_VIDEO, OPT_REF_SILENT_VIDEO, OPT_REF_VIDEO_AUDIO, OPT_REF_AUDIO, OPT_FRAMES_DIR, OPT_SHOW, OPT_ZOOM, - OPT_PROFILE, OPT_INFO }; + OPT_PROFILE, OPT_INFO, OPT_LOG_FILE }; static const struct option options[] = { {"model-dir", required_argument, NULL, 'd'}, {"prompt", required_argument, NULL, 'p'}, @@ -355,6 +358,7 @@ int main(int argc, char **argv) { {"show", no_argument, NULL, OPT_SHOW}, {"zoom", required_argument, NULL, OPT_ZOOM}, {"profile", no_argument, NULL, OPT_PROFILE}, + {"log-file", required_argument, NULL, OPT_LOG_FILE}, {"info", no_argument, NULL, OPT_INFO}, {"version", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, @@ -370,6 +374,8 @@ int main(int argc, char **argv) { int show = 0; int profile = 0; int info = 0; + const char *log_file = NULL; + h3_log log = {0}; int frames_given = 0; int seconds_given = 0; int seed_given = 0; @@ -517,28 +523,39 @@ int main(int argc, char **argv) { } break; case OPT_PROFILE: profile = 1; break; + case OPT_LOG_FILE: log_file = optarg; break; case OPT_INFO: info = 1; break; default: usage(argv[0]); return 2; } } + if (!h3_log_open(&log, log_file)) { + fprintf(stderr, "h3: cannot open log file %s: %s\n", log_file, + strerror(errno)); + return 1; + } + h3_log_set_active(&log); const char *env_model_dir = getenv("H3_MODEL_DIR"); if ((!model_dir || !*model_dir) && env_model_dir && *env_model_dir) { model_dir = env_model_dir; } if (!model_dir) { usage(argv[0]); + h3_log_close(&log); return 2; } if (frames_given && seconds_given) { fprintf(stderr, "h3: --seconds and --frames are mutually exclusive\n"); + h3_log_close(&log); return 2; } int needs_process_lock = prompt || !info; if (needs_process_lock) { const char *lock_target = getenv("H3_PROCESS_LOCK"); if (!lock_target || !*lock_target) lock_target = "/tmp/h3-process"; - if (!acquire_lock(&process_guard, lock_target, "generation process")) + if (!acquire_lock(&process_guard, lock_target, "generation process")) { + h3_log_close(&log); return 1; + } } if (prompt && params.steps >= 2 && params.steps <= 7 && params.denoise_reuse > 1) { @@ -552,17 +569,20 @@ int main(int argc, char **argv) { errno != EEXIST) { fprintf(stderr, "h3: cannot create frames directory %s: %s\n", cli.frames_dir, strerror(errno)); + h3_log_close(&log); return 1; } if (prompt && output && *output && !acquire_lock(&output_guard, output, "output")) { release_lock(&process_guard); + h3_log_close(&log); return 1; } if (prompt && cli.frames_dir && !acquire_lock(&frames_guard, cli.frames_dir, "frames directory")) { release_lock(&output_guard); release_lock(&process_guard); + h3_log_close(&log); return 1; } if (profile) setenv("H3_PROFILE", "1", 1); @@ -572,6 +592,7 @@ int main(int argc, char **argv) { release_lock(&frames_guard); release_lock(&output_guard); release_lock(&process_guard); + h3_log_close(&log); return 1; } if (info) print_info(ctx); @@ -600,6 +621,7 @@ int main(int argc, char **argv) { release_lock(&frames_guard); release_lock(&output_guard); release_lock(&process_guard); + h3_log_close(&log); return 1; } h3_result_free(result); @@ -613,8 +635,10 @@ int main(int argc, char **argv) { int cli_status = h3_cli_run(ctx, model_dir, ¶ms, show, seed_given); h3_free(ctx); release_lock(&process_guard); + h3_log_close(&log); return cli_status; } h3_free(ctx); + h3_log_close(&log); return 0; }