Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ 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`:

```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:

Expand Down Expand Up @@ -340,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
Expand Down
2 changes: 1 addition & 1 deletion h3.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions h3_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "h3_ffmpeg.h"
#include "h3_host.h"
#include "h3_log.h"
#include "h3_terminal.h"
#include "linenoise.h"

Expand Down Expand Up @@ -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;
Expand Down
48 changes: 48 additions & 0 deletions h3_log.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include "h3_log.h"

#include <stdarg.h>

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);
}
19 changes: 19 additions & 0 deletions h3_log.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef H3_LOG_H
#define H3_LOG_H

#include <stdio.h>

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
117 changes: 115 additions & 2 deletions main.c
Original file line number Diff line number Diff line change
@@ -1,16 +1,67 @@
#include "h3.h"
#include "h3_cli.h"
#include "h3_host.h"
#include "h3_log.h"
#include "h3_terminal.h"

#include <errno.h>
#include <getopt.h>
#include <inttypes.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>

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,
Expand All @@ -19,6 +70,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"
Expand Down Expand Up @@ -58,7 +110,9 @@ 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",
program, program, program);
}
Expand Down Expand Up @@ -166,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;
Expand Down Expand Up @@ -251,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'},
Expand Down Expand Up @@ -303,7 +358,9 @@ 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'},
{NULL, 0, NULL, 0}
};
Expand All @@ -317,16 +374,22 @@ 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;
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:
Expand Down Expand Up @@ -460,18 +523,40 @@ 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")) {
h3_log_close(&log);
return 1;
}
}
if (prompt && params.steps >= 2 && params.steps <= 7 &&
params.denoise_reuse > 1) {
fprintf(stderr,
Expand All @@ -484,12 +569,30 @@ 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);
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);
h3_log_close(&log);
return 1;
}
if (info) print_info(ctx);
Expand All @@ -515,17 +618,27 @@ 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);
h3_log_close(&log);
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, &params, 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;
}