char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
6de9cdb26
char/aliceai-t5-moe
1// fix problem with std::min and std::max 2#if defined(_WIN32 ) 3#define WIN32_LEAN_AND_MEAN 4#ifndef NOMINMAX 5# define NOMINMAX 6#endif 7#include <windows.h> 8#endif 9 10#include "mtmd.h" 11#include "mtmd-helper.h" 12#include "mtmd-helper-common.h" 13#include "llama.h" 14 15#include "hash/hash.h" 16 17#include <algorithm> 18#include <cinttypes> 19#include <vector> 20 21//#define MTMD_AUDIO_DEBUG 22 23#define MINIAUDIO_IMPLEMENTATION 24#ifndef MTMD_AUDIO_DEBUG 25# define MA_NO_ENCODING 26#endif 27#define MA_NO_DEVICE_IO 28#define MA_NO_RESOURCE_MANAGER 29#define MA_NO_NODE_GRAPH 30#define MA_NO_ENGINE 31#define MA_NO_GENERATION 32#define MA_API static 33#include "miniaudio/miniaudio.h" 34 35#define STB_IMAGE_IMPLEMENTATION 36#include "stb/stb_image.h" 37 38#ifdef MTMD_INTERNAL_HEADER 39#error "mtmd-helper is a public library outside of mtmd. it must not include internal headers" 40#endif 41 42#ifdef MTMD_VIDEO 43#include "sheredom/subprocess.h" 44#include <thread> 45#ifndef _WIN32 46#include <csignal> 47#include <fcntl.h> 48#include <pthread.h> 49#endif 50#endif 51 52// 53// internal logging functions 54// 55 56void mtmd_helper_log_set (ggml_log_callback log_callback ,void * user_data ) { 57if (log_callback == nullptr ) { 58log_callback = g_logger .default_callback ; 59 } 60g_logger .log_callback = log_callback ; 61g_logger .log_callback_user_data = user_data ; 62mtmd_log_set (log_callback ,user_data ); 63} 64 65// 66// helper functions 67// 68 69size_t mtmd_helper_get_n_tokens (const mtmd_input_chunks * chunks ) { 70size_t n_tokens = 0 ; 71for (size_t i = 0 ;i < mtmd_input_chunks_size (chunks );i ++ ) { 72auto chunk = mtmd_input_chunks_get (chunks ,i ); 73n_tokens += mtmd_input_chunk_get_n_tokens (chunk ); 74 } 75return n_tokens ; 76} 77 78llama_pos mtmd_helper_get_n_pos (const mtmd_input_chunks * chunks ) { 79llama_pos n_pos = 0 ; 80for (size_t i = 0 ;i < mtmd_input_chunks_size (chunks );i ++ ) { 81auto chunk = mtmd_input_chunks_get (chunks ,i ); 82n_pos += mtmd_input_chunk_get_n_pos (chunk ); 83 } 84return n_pos ; 85} 86 87void mtmd_helper_image_get_decoder_pos (const mtmd_image_tokens * chunks ,llama_pos pos_0 ,mtmd_decoder_pos * out_pos ) { 88size_t n_tokens = mtmd_image_tokens_get_n_tokens (chunks ); 89for (size_t i = 0 ;i < n_tokens ;i ++ ) { 90out_pos [i ]= mtmd_image_tokens_get_decoder_pos (chunks ,pos_0 ,i ); 91 } 92} 93 94// Helper class to set non-causal attention via RAII 95class scope_non_causal { 96public : 97scope_non_causal (llama_context * context ,bool enabled ) :context_ (context ),enabled_ (enabled ) { 98if (enabled_ ) { 99// TODO @ngxson : need to make sure only one image is processed at a time, and n_ubatch must be enough to hold the image 100llama_set_causal_attn (context_ , false); 101 } 102 } 103 ~scope_non_causal () { 104if (enabled_ ) { 105llama_set_causal_attn (context_ , true); 106 } 107 } 108 109scope_non_causal (const scope_non_causal & )= delete ; 110scope_non_causal & operator= (const scope_non_causal & )= delete ; 111 112private : 113llama_context * context_ ; 114bool enabled_ ; 115}; 116 117// Helper function for decoding an image whose embeddings have already been calculated 118int32_t mtmd_helper_decode_image_chunk ( 119mtmd_context * ctx , 120struct llama_context * lctx , 121const mtmd_input_chunk * chunk , 122float * encoded_embd , 123llama_pos n_past , 124llama_seq_id seq_id , 125int32_t n_batch , 126llama_pos * new_n_past , 127mtmd_helper_post_decode_callback callback , 128void * user_data ) { 129GGML_ASSERT (n_batch > 0 ); 130auto chunk_type = mtmd_input_chunk_get_type (chunk ); 131const char * name = chunk_type == MTMD_INPUT_CHUNK_TYPE_IMAGE ?"image" :"audio" ; 132if (chunk_type == MTMD_INPUT_CHUNK_TYPE_TEXT ) { 133LOG_ERR ("failed to decode chunk: input chunk not of image/audio type\n" ); 134return -1 ; 135 } 136 137const llama_model * model = llama_get_model (lctx ); 138int n_mmproj_embd = llama_model_n_embd_inp (model ); 139int n_pos_per_embd = mtmd_decode_use_mrope (ctx ) ?4 :1 ; 140 141int32_t n_tokens = mtmd_input_chunk_get_n_tokens (chunk ); 142int32_t i_batch = 0 ; 143int32_t n_img_batches = (n_tokens + n_batch - 1 ) /n_batch ; 144decode_embd_batch batch_embd (encoded_embd ,n_tokens ,n_pos_per_embd ,n_mmproj_embd ); 145 146if (mtmd_decode_use_mrope (ctx )) { 147if (chunk_type == MTMD_INPUT_CHUNK_TYPE_IMAGE ) { 148const auto image_tokens = mtmd_input_chunk_get_tokens_image (chunk ); 149if (!image_tokens ) { 150LOG_ERR ("failed to decode chunk: image tokens are null\n" ); 151return -1 ; 152 } 153const auto n_tokens = mtmd_image_tokens_get_n_tokens (image_tokens ); 154 std::vector < mtmd_decoder_pos > rel_pos (n_tokens ); 155mtmd_helper_image_get_decoder_pos (image_tokens ,n_past ,rel_pos .data ()); 156batch_embd .set_position_mrope_2d (rel_pos ,seq_id ); 157 }else if (chunk_type == MTMD_INPUT_CHUNK_TYPE_AUDIO ) { 158batch_embd .set_position_mrope_1d (n_past ,seq_id ); 159 }else { 160GGML_ABORT ("invalid chunk type for M-RoPE" ); 161 } 162 }else { 163batch_embd .set_position_normal (n_past ,seq_id ); 164 } 165 166const bool use_non_causal = mtmd_decode_use_non_causal (ctx ,chunk ); 167const scope_non_causal non_causal (lctx ,use_non_causal ); 168 169while (i_batch < n_img_batches ) {// split into batches 170int pos_offset = i_batch * n_batch ; 171int n_tokens_batch = std::min (n_batch ,n_tokens - pos_offset ); 172llama_batch batch_embd_view = batch_embd .get_view (pos_offset ,n_tokens_batch ); 173 174LOG_INF ("decoding %s batch %d/%d, n_tokens_batch = %d\n" ,name ,i_batch + 1 ,n_img_batches ,n_tokens_batch ); 175 176int64_t t1 = ggml_time_ms (); 177int32_t ret = llama_decode (lctx ,batch_embd_view ); 178if (ret != 0 ) { 179LOG_ERR ("failed to decode %s\n" ,name ); 180return ret ; 181 } 182 183if (callback != nullptr ) { 184ret = callback (batch_embd_view ,user_data ); 185if (ret != 0 ) { 186LOG_ERR ("post-decode callback failed\n" ); 187return ret ; 188 } 189 } 190 191LOG_INF ("%s decoded (batch %d/%d) in %" PRId64 " ms\n" ,name ,i_batch + 1 ,n_img_batches ,ggml_time_ms ()- t1 ); 192 193i_batch ++ ; 194 } 195 196n_past += mtmd_input_chunk_get_n_pos (chunk ); 197* new_n_past = n_past ; 198 199return 0 ; 200} 201 202int32_t mtmd_helper_eval_chunk_single (mtmd_context * ctx , 203struct llama_context * lctx , 204const mtmd_input_chunk * chunk , 205llama_pos n_past , 206llama_seq_id seq_id , 207int32_t n_batch , 208bool logits_last , 209llama_pos * new_n_past ) { 210GGML_ASSERT (n_batch > 0 ); 211int32_t ret ; 212llama_batch text_batch = llama_batch_init (n_batch ,0 ,1 ); 213auto chunk_type = mtmd_input_chunk_get_type (chunk ); 214 215if (chunk_type == MTMD_INPUT_CHUNK_TYPE_TEXT ) { 216size_t n_tokens ; 217const auto tokens = mtmd_input_chunk_get_tokens_text (chunk ,& n_tokens ); 218// LOG_INF("decoding text chunk, n_tokens = %zu\n", n_tokens); 219size_t i = 0 ; 220while (i < n_tokens ) {// split into batches 221text_batch .n_tokens = 0 ;// clear the batch 222for (;i < n_tokens && text_batch .n_tokens < n_batch ;i ++ ) { 223int32_t j = text_batch .n_tokens ; 224text_batch .token [j ]= tokens [i ]; 225text_batch .pos [j ]= n_past ++ ; 226text_batch .n_seq_id [j ]= 1 ; 227text_batch .seq_id [j ][0 ]= seq_id ; 228text_batch .logits [j ]= false; 229 230text_batch .n_tokens ++ ; 231 } 232bool is_last_token = (i == n_tokens ); 233if (logits_last && is_last_token ) { 234text_batch .logits [text_batch .n_tokens - 1 ]= true; 235 } 236ret = llama_decode (lctx ,text_batch ); 237if (ret != 0 ) { 238LOG_ERR ("failed to decode text\n" ); 239llama_batch_free (text_batch ); 240return ret ; 241 } 242* new_n_past += text_batch .n_tokens ; 243 } 244 245 }else if (chunk_type == MTMD_INPUT_CHUNK_TYPE_IMAGE || chunk_type == MTMD_INPUT_CHUNK_TYPE_AUDIO ) { 246const char * name = chunk_type == MTMD_INPUT_CHUNK_TYPE_IMAGE ?"image" :"audio" ; 247int64_t t0 = ggml_time_ms (); 248 249LOG_INF ("encoding %s slice...\n" ,name ); 250 251ret = mtmd_encode_chunk (ctx ,chunk ); 252if (ret != 0 ) { 253LOG_ERR ("failed to encode %s slice\n" ,name ); 254llama_batch_free (text_batch ); 255return ret ; 256 } 257 258LOG_INF ("%s slice encoded in %" PRId64 " ms\n" ,name ,ggml_time_ms ()- t0 ); 259 260float * embd = mtmd_get_output_embd (ctx ); 261ret = mtmd_helper_decode_image_chunk (ctx ,lctx ,chunk ,embd ,n_past ,seq_id ,n_batch ,new_n_past ,nullptr ,nullptr ); 262if (ret != 0 ) { 263LOG_ERR ("failed to decode %s\n" ,name ); 264llama_batch_free (text_batch ); 265return ret ; 266 } 267 }else { 268GGML_ABORT ("chunk type not supported" ); 269 } 270 271llama_batch_free (text_batch ); 272return 0 ; 273} 274 275int32_t mtmd_helper_eval_chunks (mtmd_context * ctx , 276struct llama_context * lctx , 277const mtmd_input_chunks * chunks , 278llama_pos n_past , 279llama_seq_id seq_id , 280int32_t n_batch , 281bool logits_last , 282llama_pos * new_n_past ) { 283size_t n_chunks = mtmd_input_chunks_size (chunks ); 284if (n_chunks == 0 ) { 285LOG_WRN ("no chunks to eval\n" ); 286return 0 ; 287 } 288 289for (size_t i = 0 ;i < n_chunks ;i ++ ) { 290bool chunk_logits_last = (i == n_chunks - 1 )&& logits_last ; 291auto chunk = mtmd_input_chunks_get (chunks ,i ); 292 293int32_t res = mtmd_helper_eval_chunk_single (ctx ,lctx ,chunk ,n_past ,seq_id ,n_batch ,chunk_logits_last ,& n_past ); 294if (res != 0 ) { 295LOG_ERR ("failed to eval chunk %zu\n" ,i ); 296return res ; 297 } 298* new_n_past = n_past ; 299 } 300 301return 0 ; 302} 303 304namespace audio_helpers { 305 306static bool is_audio_file (const char * buf ,size_t len ) { 307if (len < 12 ) { 308return false; 309 } 310 311// RIFF ref: https://en.wikipedia.org/wiki/Resource_Interchange_File_Format 312// WAV ref: https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html 313bool is_wav = memcmp (buf ,"RIFF" ,4 )== 0 && memcmp (buf + 8 ,"WAVE" ,4 )== 0 ; 314bool is_mp3 = len >=3 && ( 315memcmp (buf ,"ID3" ,3 )== 0 || 316// Check for MPEG sync word (simplified check) 317 ((unsigned char )buf [0 ]== 0xFF && ((unsigned char )buf [1 ]& 0xE0 )== 0xE0 ) 318 ); 319bool is_flac = memcmp (buf ,"fLaC" ,4 )== 0 ; 320 321return is_wav || is_mp3 || is_flac ; 322} 323 324// returns true if the buffer is a valid audio file 325static bool decode_audio_from_buf (const unsigned char * buf_in ,size_t len ,int target_sampler_rate , std::vector < float > & pcmf32_mono ) { 326ma_result result ; 327const int channels = 1 ; 328ma_decoder_config decoder_config = ma_decoder_config_init (ma_format_f32 ,channels ,target_sampler_rate ); 329ma_decoder decoder ; 330 331result = ma_decoder_init_memory (buf_in ,len ,& decoder_config ,& decoder ); 332if (result != MA_SUCCESS ) { 333return false; 334 } 335 336ma_uint64 frame_count ; 337ma_uint64 frames_read ; 338result = ma_decoder_get_length_in_pcm_frames (& decoder ,& frame_count ); 339if (result != MA_SUCCESS ) { 340ma_decoder_uninit (& decoder ); 341return false; 342 } 343 344pcmf32_mono .resize (frame_count ); 345result = ma_decoder_read_pcm_frames (& decoder ,pcmf32_mono .data (),frame_count ,& frames_read ); 346if (result != MA_SUCCESS ) { 347ma_decoder_uninit (& decoder ); 348return false; 349 } 350 351#ifdef MTMD_AUDIO_DEBUG 352// save audio to wav file 353ma_encoder_config config = ma_encoder_config_init (ma_encoding_format_wav ,ma_format_f32 ,1 ,target_sampler_rate ); 354ma_encoder encoder ; 355ma_encoder_init_file ("output.wav" ,& config ,& encoder ); 356ma_encoder_write_pcm_frames (& encoder ,pcmf32_mono .data (),pcmf32_mono .size (),& frames_read ); 357ma_encoder_uninit (& encoder ); 358#endif 359 360ma_decoder_uninit (& decoder ); 361return true; 362} 363 364}// namespace audio_helpers 365 366static bool is_webp_file (const unsigned char * buf ,size_t len ) { 367// WEBP ref: https://developers.google.com/speed/webp/docs/riff_container 368return len >=12 && memcmp (buf ,"RIFF" ,4 )== 0 && memcmp (buf + 8 ,"WEBP" ,4 )== 0 ; 369} 370 371#ifdef MTMD_VIDEO 372static mtmd_bitmap * decode_webp_with_ffmpeg (const mtmd_context * mctx ,const unsigned char * buf ,size_t len ,bool placeholder , 373const mtmd_helper_video_init_params & params ); 374static void mtmd_helper_video_set_id (mtmd_helper_video * vctx ,const std::string & id ); 375#endif 376 377mtmd_helper_bitmap_wrapper mtmd_helper_bitmap_init_from_buf (const mtmd_context * ctx ,const unsigned char * buf ,size_t len ,bool placeholder , 378mtmd_helper_init_opt opt ) { 379// calculate the hash if needed 380 std::string id ; 381mtmd_bitmap * result = nullptr ; 382 383GGML_UNUSED (opt );// only used by video code paths 384 385if (!placeholder ) { 386// use sha256 to prevent cache poisoning 387id = hash_sha256_hex (buf ,len ); 388 } 389 390if (audio_helpers::is_audio_file ((const char * )buf ,len )) { 391 std::vector < float > pcmf32 ; 392const int sample_rate = mtmd_get_audio_sample_rate (ctx ); 393if (sample_rate < 0 ) { 394LOG_ERR ("This model does not support audio input\n" ); 395return {nullptr ,nullptr }; 396 } 397if (!audio_helpers::decode_audio_from_buf (buf ,len ,sample_rate ,pcmf32 )) { 398LOG_ERR ("Unable to read WAV audio file from buffer\n" ); 399return {nullptr ,nullptr }; 400 } 401result = mtmd_bitmap_init_from_audio (pcmf32 .size (),placeholder ?nullptr :pcmf32 .data ()); 402mtmd_bitmap_set_id (result ,id .empty () ?nullptr :id .c_str ()); 403return {result ,nullptr }; 404 } 405 406// otherwise, we assume it's an image 407if (!result ) { 408int nx ,ny ,nc ; 409auto * data = stbi_load_from_memory (buf ,len ,& nx ,& ny ,& nc ,3 ); 410if (data ) { 411result = mtmd_bitmap_init (nx ,ny ,placeholder ?nullptr :data ); 412mtmd_bitmap_set_id (result ,id .empty () ?nullptr :id .c_str ()); 413stbi_image_free (data ); 414return {result ,nullptr }; 415 } 416// otherwise, fallthrough to video decoding (if supported) 417 } 418 419#ifdef MTMD_VIDEO 420// stb_image does not support webp; decode it with ffmpeg as a single frame 421if (!result && is_webp_file (buf ,len )) { 422result = decode_webp_with_ffmpeg (ctx ,buf ,len ,placeholder ,opt .video_params ); 423if (!result ) { 424LOG_ERR ("%s: failed to decode webp buffer\n" ,__func__ ); 425return {nullptr ,nullptr }; 426 } 427mtmd_bitmap_set_id (result ,id .empty () ?nullptr :id .c_str ()); 428return {result ,nullptr }; 429 } 430#endif 431 432// last try: load as video 433#ifdef MTMD_VIDEO 434if (!result ) { 435auto video_ctx = mtmd_helper_video_init_from_buf (ctx ,buf ,len ,opt .video_params ); 436if (!video_ctx ) { 437LOG_ERR ("%s: failed to decode buffer as either image/audio/video\n" ,__func__ ); 438return {nullptr ,nullptr }; 439 } 440mtmd_helper_video_set_id (video_ctx ,id );// propagate the hash to the frames 441result = mtmd_bitmap_init_lazy (ctx , 442id .empty () ?nullptr :id .c_str (), 443video_ctx , 444 [](size_t ,void * user_data ,mtmd_bitmap ** out_bitmap ,char ** out_text )-> int { 445auto * vctx = static_cast < mtmd_helper_video *> (user_data ); 446char * text = nullptr ; 447int ret = mtmd_helper_video_read_next (vctx ,out_bitmap ,& text ); 448* out_text = text ;// heap-allocated by read_next; freed automatically by mtmd 449return ret ; 450 }); 451return {result ,video_ctx }; 452 } 453#else 454if (!result ) { 455LOG_ERR ("%s: failed to decode buffer as either image or audio (video support not compiled in)\n" ,__func__ ); 456return {nullptr ,nullptr }; 457 } 458#endif 459 460// should not reach here 461return {nullptr ,nullptr }; 462} 463 464mtmd_helper_bitmap_wrapper mtmd_helper_bitmap_init_from_file (const mtmd_context * ctx ,const char * fname ,bool placeholder , 465mtmd_helper_init_opt opt ) { 466#ifdef _WIN32 467int wlen = MultiByteToWideChar (CP_UTF8 ,0 ,fname ,-1 ,NULL ,0 ); 468if (!wlen ) { 469LOG_ERR ("Unable to convert filename to UTF-16: %s\n" ,fname ); 470return {nullptr ,nullptr }; 471 } 472 std::vector < wchar_t > wfname (wlen ); 473wlen = MultiByteToWideChar (CP_UTF8 ,0 ,fname ,-1 ,wfname .data (),wlen ); 474if (!wlen ) { 475LOG_ERR ("Unable to convert filename to UTF-16: %s\n" ,fname ); 476return {nullptr ,nullptr }; 477 } 478FILE * f = _wfopen (wfname .data (),L"rb" ); 479#else 480FILE * f = fopen (fname ,"rb" ); 481#endif 482if (!f ) { 483LOG_ERR ("Unable to open file %s: %s\n" ,fname ,strerror (errno )); 484return {nullptr ,nullptr }; 485 } 486 487 std::vector < unsigned char > buf ; 488 489fseek (f ,0 ,SEEK_END ); 490long file_size = ftell (f ); 491fseek (f ,0 ,SEEK_SET ); 492if (file_size < 0 ) { 493LOG_ERR ("Failed to get file size of %s\n" ,fname ); 494fclose (f ); 495return {nullptr ,nullptr }; 496 } 497buf .resize (file_size ); 498 499size_t n_read = fread (buf .data (),1 ,file_size ,f ); 500fclose (f ); 501if (n_read != (size_t )file_size ) { 502LOG_ERR ("Failed to read entire file %s" ,fname ); 503return {nullptr ,nullptr }; 504 } 505 506return mtmd_helper_bitmap_init_from_buf (ctx ,buf .data (),buf .size (),placeholder ,opt ); 507} 508 509bool mtmd_helper_support_video (const mtmd_context * ctx ) { 510#ifdef MTMD_VIDEO 511return mtmd_support_vision (ctx ); 512#else 513GGML_UNUSED (ctx ); 514return false; 515#endif 516} 517 518// 519// Video input helpers 520// 521 522#ifdef MTMD_VIDEO 523 524struct mtmd_helper_video { 525const mtmd_context * mctx ; 526 std::string path ; 527 std::vector < uint8_t > input_buf ;// non-empty when initialized from buffer 528 std::string ffmpeg_bin ; 529 std::string ffprobe_bin ; 530float fps_target = 0.0f ; 531mtmd_helper_video_info info = {}; 532 std::string id ;// hash of the input video 533 534// RAII wrapper for managing subprocess 535struct subprocess_handle { 536struct subprocess_s proc = {}; 537bool created = false;// process exists and must be cleaned up 538bool alive = false;// process can still give us data 539 std::thread feeder ; 540 541subprocess_handle ()= default ; 542subprocess_handle (const subprocess_handle & )= delete ; 543subprocess_handle & operator= (const subprocess_handle & )= delete ; 544 ~subprocess_handle () {stop (); } 545 546void stop () { 547// note: alive becomes false on stdout EOF, but the process still needs cleanup 548if (!created ) { 549return ; 550 } 551subprocess_terminate (& proc ); 552#ifdef _WIN32 553// no SIGPIPE on windows: a blocked feeder only gets a broken pipe once we close our read end of the child stdin 554if (proc .hStdInput ) { 555CloseHandle (proc .hStdInput ); 556proc .hStdInput = nullptr ; 557 } 558#endif 559// join before destroy: feeder holds a FILE* from subprocess_stdin; 560// subprocess_destroy closes it, so the thread must finish first 561if (feeder .joinable ()) { 562feeder .join (); 563 } 564subprocess_join (& proc ,nullptr );// reap the child, or else it stays a zombie 565subprocess_destroy (& proc ); 566created = false; 567alive = false; 568 } 569 570FILE * stdout_pipe () { 571return subprocess_stdout (& proc ); 572 } 573 574// buf is tied to lifetime of mtmd_helper_video, so it's guaranteed to outlive the feeder thread 575void start_feeder (const std::vector < uint8_t > & buf ) { 576feeder = std::thread ([this ,& buf ]() { 577#ifndef _WIN32 578// ffmpeg can exit before it reads all the input, for example when ffprobe already got the metadata. 579// the write below must then fail with EPIPE, instead of killing the process with SIGPIPE 580sigset_t sigpipe_set ; 581sigemptyset (& sigpipe_set ); 582sigaddset (& sigpipe_set ,SIGPIPE ); 583pthread_sigmask (SIG_BLOCK ,& sigpipe_set ,nullptr );// linux sends the signal to the writing thread 584#endif 585FILE * f = subprocess_stdin (& proc ); 586if (!f ) { 587return ; 588 } 589#ifdef F_SETNOSIGPIPE 590fcntl (fileno (f ),F_SETNOSIGPIPE ,1 );// macos/bsd send it to the process, so turn it off per fd 591#endif 592fwrite (buf .data (),1 ,buf .size (),f ); 593fclose (f ); 594proc .stdin_file = nullptr ;// prevent double-close in subprocess_destroy 595 }); 596 } 597 }; 598 599subprocess_handle sp ; 600int32_t current_frame = 0 ; 601 602 std::string prompt_start = "Video:" ; 603int32_t timestamp_interval_ms = 5000 ;// emit a timestamp text every N ms (0 = disabled) 604float next_timestamp_ms = 0.0f ;// next elapsed-ms threshold at which to emit 605 606 std::vector < uint8_t > frame_buf ; 607 std::string pending_text ;// text queued to be returned before the next frame 608bool start_emitted = false; 609 610bool is_buf_input ()const { 611return !input_buf .empty (); 612 } 613 614bool probe (float fps_target_arg ) { 615const char * input_arg = is_buf_input () ?"pipe:0" :path .c_str (); 616const char * cmd []= { 617ffprobe_bin .c_str (), 618"-v" ,"quiet" , 619"-show_entries" ,"stream=width,height,r_frame_rate,nb_frames,duration" , 620"-select_streams" ,"v:0" , 621"-of" ,"default=noprint_wrappers=1" , 622input_arg , 623nullptr , 624 }; 625 626LOG_DBG ("%s: launching:" ,__func__ ); 627for (size_t i = 0 ;cmd [i ];i ++ ) {LOG_DBG (" %s" ,cmd [i ]); } 628LOG_DBG ("\n" ); 629 630subprocess_handle probe_sp ; 631if (subprocess_create (cmd , 632subprocess_option_search_user_path |subprocess_option_inherit_environment , 633& probe_sp .proc )!= 0 ) { 634LOG_ERR ("%s: failed to launch ffprobe\n" ,__func__ ); 635return false; 636 } 637probe_sp .created = true; 638probe_sp .alive = true; 639 640if (is_buf_input ()) { 641probe_sp .start_feeder (input_buf ); 642 } 643 644uint32_t width = 0 ; 645uint32_t height = 0 ; 646float orig_fps = 0.0f ; 647float duration = -1.0f ; 648int32_t n_frames_orig = -1 ; 649char line [256 ]; 650FILE * fp = probe_sp .stdout_pipe (); 651 652while (fgets (line ,sizeof (line ),fp )) { 653char * eq = strchr (line ,'=' ); 654if (!eq )continue ; 655* eq = '\0' ; 656const char * key = line ; 657const char * val = eq + 1 ; 658char * nl = (char * )strchr (val ,'\n' ); 659if (nl )* nl = '\0' ; 660 661if (strcmp (key ,"width" )== 0 ) { 662width = (uint32_t )atoi (val ); 663 }else if (strcmp (key ,"height" )== 0 ) { 664height = (uint32_t )atoi (val ); 665 }else if (strcmp (key ,"r_frame_rate" )== 0 ) { 666orig_fps = parse_rational (val ); 667 }else if (strcmp (key ,"nb_frames" )== 0 && strcmp (val ,"N/A" )!= 0 ) { 668n_frames_orig = atoi (val ); 669 }else if (strcmp (key ,"duration" )== 0 && strcmp (val ,"N/A" )!= 0 ) { 670duration = (float )atof (val ); 671 } 672 } 673 674probe_sp .stop (); 675 676if (width == 0 || height == 0 || orig_fps <=0.0f ) { 677return false; 678 } 679 680if (duration < 0.0f && n_frames_orig > 0 ) { 681duration = (float )n_frames_orig /orig_fps ; 682 } 683 684fps_target = fps_target_arg > 0.0f ?fps_target_arg :orig_fps ; 685info .width = width ; 686info .height = height ; 687info .fps = fps_target ; 688LOG_DBG ("%s: %ux%u fps=%.2f duration=%.2fs n_frames=%d\n" , 689__func__ ,width ,height ,fps_target ,duration ,info .n_frames ); 690info .n_frames = duration > 0.0f ? (int32_t )(duration * fps_target + 0.5f ) :-1 ; 691frame_buf .resize ((size_t )width * height * 3 ); 692return true; 693 } 694 695bool start_ffmpeg (float seek_seconds ) { 696char seek_buf [64 ]; 697char fps_buf [64 ]; 698 699 std::vector < const char *> cmd ; 700cmd .push_back (ffmpeg_bin .c_str ()); 701 702if (!is_buf_input ()&& seek_seconds > 0.0f ) { 703// input-side seek: fast, keyframe-accurate; only valid for seekable file inputs 704snprintf (seek_buf ,sizeof (seek_buf ),"%.6f" ,seek_seconds ); 705cmd .push_back ("-ss" ); 706cmd .push_back (seek_buf ); 707 } 708 709cmd .push_back ("-nostdin" ); 710if (is_buf_input ()) { 711// remove the 64KB read-ahead limit of cache:, or else ffmpeg cannot reach a moov atom at end of file 712cmd .push_back ("-read_ahead_limit" ); 713cmd .push_back ("-1" ); 714 } 715cmd .push_back ("-i" ); 716// cache:pipe:0 wraps stdin with a seekable in-memory cache, letting ffmpeg seek 717// backwards for container headers (e.g. MP4 moov atom at end of file) 718cmd .push_back (is_buf_input () ?"cache:pipe:0" :path .c_str ()); 719 720if (seek_seconds > 0.0f && is_buf_input ()) { 721// output-side seek: frame-accurate but decodes and discards frames up to seek point 722snprintf (seek_buf ,sizeof (seek_buf ),"%.6f" ,seek_seconds ); 723cmd .push_back ("-ss" ); 724cmd .push_back (seek_buf ); 725 } 726 727if (fps_target > 0.0f ) { 728snprintf (fps_buf ,sizeof (fps_buf ),"fps=%.6f" ,fps_target ); 729cmd .push_back ("-vf" ); 730cmd .push_back (fps_buf ); 731 } 732 733cmd .push_back ("-f" ); 734cmd .push_back ("rawvideo" ); 735cmd .push_back ("-pix_fmt" ); 736cmd .push_back ("rgb24" ); 737cmd .push_back ("pipe:1" ); 738cmd .push_back ("-loglevel" ); 739cmd .push_back ("error" ); 740cmd .push_back (nullptr ); 741 742LOG_DBG ("%s: launching:" ,__func__ ); 743for (size_t i = 0 ;cmd [i ];i ++ ) { 744LOG_DBG (" %s" ,cmd [i ]); 745 } 746LOG_DBG ("\n" ); 747 748int ret = subprocess_create ( 749cmd .data (), 750subprocess_option_search_user_path |subprocess_option_inherit_environment , 751& sp .proc ); 752 753sp .created = (ret == 0 ); 754sp .alive = (ret == 0 ); 755LOG_DBG ("%s: subprocess_create ret=%d proc_alive=%d\n" ,__func__ ,ret , (int )sp .alive ); 756 757if (sp .alive && is_buf_input ()) { 758LOG_DBG ("%s: starting feeder thread for %zu-byte buffer\n" ,__func__ ,input_buf .size ()); 759sp .start_feeder (input_buf ); 760 } 761 762return sp .alive ; 763 } 764 765void stop_ffmpeg () { 766sp .stop (); 767 } 768 769mtmd_bitmap * read_next_frame () { 770if (!sp .alive )return nullptr ; 771 772FILE * fp = sp .stdout_pipe (); 773const size_t frame_size = (size_t )info .width * info .height * 3 ; 774LOG_DBG ("%s: reading frame %d, expecting %zu bytes (%ux%u)\n" , 775__func__ ,current_frame ,frame_size ,info .width ,info .height ); 776 777size_t total_read = 0 ; 778while (total_read < frame_size ) { 779size_t n = fread (frame_buf .data ()+ total_read ,1 ,frame_size - total_read ,fp ); 780if (n == 0 ) { 781// clean EOF only if no bytes read yet; partial frame is an error 782LOG_DBG ("%s: fread returned 0 after %zu/%zu bytes (ferror=%d)\n" , 783__func__ ,total_read ,frame_size ,ferror (fp )); 784sp .alive = false; 785return nullptr ; 786 } 787total_read += n ; 788 } 789 790LOG_DBG ("%s: frame %d read OK\n" ,__func__ ,current_frame ); 791mtmd_bitmap * frame = mtmd_bitmap_init (info .width ,info .height ,frame_buf .data ()); 792mtmd_bitmap_set_mergeable (frame , true); 793if (!id .empty ()) { 794// each frame gets a unique id in the form of {hash}+{frame}, so that it can be identified in cache 795 std::string frame_id = id + "+" + std::to_string (current_frame ); 796mtmd_bitmap_set_id (frame ,frame_id .c_str ()); 797 } 798current_frame ++ ; 799return frame ; 800 } 801 802int32_t read_next (mtmd_bitmap ** out_bitmap ,char ** out_text ) { 803* out_bitmap = nullptr ; 804* out_text = nullptr ; 805 806if (!pending_text .empty ()) { 807* out_text = strdup (pending_text .c_str ()); 808pending_text .clear (); 809return * out_text ?0 :-2 ; 810 } 811 812LOG_DBG ("%s: proc_alive=%d start_emitted=%d current_frame=%d\n" , 813__func__ , (int )sp .alive , (int )start_emitted ,current_frame ); 814 815if (!sp .alive ) { 816return (current_frame == 0 ) ?-2 :-1 ; 817 } 818 819if (!start_emitted ) { 820start_emitted = true; 821if (!prompt_start .empty ()) { 822* out_text = strdup (prompt_start .c_str ()); 823return * out_text ?0 :-2 ; 824 } 825 } 826 827mtmd_bitmap * frame = read_next_frame (); 828if (!frame )return -1 ; 829* out_bitmap = frame ; 830 831if (timestamp_interval_ms > 0 ) { 832// current_frame was already incremented by read_next_frame(); undo for elapsed calc 833float elapsed_ms = (float )(current_frame - 1 ) /info .fps * 1000.0f ; 834if (elapsed_ms >=next_timestamp_ms ) { 835char ts_buf [32 ]; 836float elapsed_s = elapsed_ms /1000.0f ; 837int minutes = (int )(elapsed_s /60 ); 838float seconds = elapsed_s - minutes * 60.0f ; 839snprintf (ts_buf ,sizeof (ts_buf ),"[%dm%.2fs]" ,minutes ,seconds ); 840pending_text = ts_buf ; 841next_timestamp_ms += (float )timestamp_interval_ms ; 842 } 843 } 844 845return 0 ; 846 } 847 848static float parse_rational (const char * s ) { 849int num = 0 ,den = 1 ; 850if (sscanf (s ,"%d/%d" ,& num ,& den )== 2 && den > 0 ) { 851return (float )num / (float )den ; 852 } 853float val ; 854if (sscanf (s ,"%f" ,& val )== 1 ) { 855return val ; 856 } 857return 0.0f ; 858 } 859}; 860#endif 861 862mtmd_helper_video_init_params mtmd_helper_video_init_params_default () { 863return { 864/* fps_target */ 4.0f , 865/* ffmpeg_bin_dir */ nullptr , 866/* timestamp_interval_ms */ 5000 , 867 }; 868} 869 870mtmd_helper_init_opt mtmd_helper_init_opt_default () { 871return { 872/* video_params */ mtmd_helper_video_init_params_default (), 873 }; 874} 875 876static std::string video_resolve_bin (const char * bin_dir ,const char * name ) { 877if (!bin_dir || bin_dir [0 ]== '\0' ) { 878return name ;// rely on PATH 879 } 880 std::string result = bin_dir ; 881char last = result .back (); 882if (last != '/' && last != '\\' ) { 883#ifdef _WIN32 884result += '\\' ; 885#else 886result += '/' ; 887#endif 888 } 889result += name ; 890#ifdef _WIN32 891result += ".exe" ; 892#endif 893return result ; 894} 895 896#ifdef MTMD_VIDEO 897static void mtmd_helper_video_set_id (mtmd_helper_video * vctx ,const std::string & id ) { 898vctx -> id = id ; 899} 900 901static mtmd_bitmap * decode_webp_with_ffmpeg (const mtmd_context * mctx ,const unsigned char * buf ,size_t len ,bool placeholder , 902const mtmd_helper_video_init_params & params ) { 903mtmd_helper_video vctx ; 904vctx .mctx = mctx ; 905vctx .input_buf .assign (buf ,buf + len ); 906vctx .ffmpeg_bin = video_resolve_bin (params .ffmpeg_bin_dir ,"ffmpeg" ); 907vctx .ffprobe_bin = video_resolve_bin (params .ffmpeg_bin_dir ,"ffprobe" ); 908if (!vctx .probe (0.0f )) { 909return nullptr ; 910 } 911if (placeholder ) { 912return mtmd_bitmap_init (vctx .info .width ,vctx .info .height ,nullptr ); 913 } 914// still image: the fps filter would output no frame, so disable it 915vctx .fps_target = 0.0f ; 916if (!vctx .start_ffmpeg (0.0f )) { 917return nullptr ; 918 } 919mtmd_bitmap * frame = vctx .read_next_frame (); 920if (frame ) { 921mtmd_bitmap_set_mergeable (frame , false); 922 } 923return frame ; 924} 925#endif 926 927mtmd_helper_video * mtmd_helper_video_init ( 928const mtmd_context * mctx , 929const char * path , 930mtmd_helper_video_init_params params ) { 931#ifdef MTMD_VIDEO 932auto * ctx = new mtmd_helper_video (); 933 934ctx -> mctx = mctx ; 935ctx -> path = path ; 936ctx -> ffmpeg_bin = video_resolve_bin (params .ffmpeg_bin_dir ,"ffmpeg" ); 937ctx -> ffprobe_bin = video_resolve_bin (params .ffmpeg_bin_dir ,"ffprobe" ); 938ctx -> timestamp_interval_ms = params .timestamp_interval_ms ; 939 940if (!ctx -> probe (params .fps_target )) { 941LOG_ERR ("%s: ffprobe failed for '%s' (is ffprobe in PATH?)\n" ,__func__ ,path ); 942delete ctx ; 943return nullptr ; 944 } 945 946if (!ctx -> start_ffmpeg (0.0f )) { 947LOG_ERR ("%s: failed to start ffmpeg for '%s' (is ffmpeg in PATH?)\n" ,__func__ ,path ); 948delete ctx ; 949return nullptr ; 950 } 951 952return ctx ; 953#else 954GGML_UNUSED (mctx ); 955GGML_UNUSED (path ); 956GGML_UNUSED (params ); 957LOG_ERR ("%s: video is not supported in this build (MTMD_VIDEO is set to OFF)\n" ,__func__ ); 958return nullptr ; 959#endif 960} 961 962mtmd_helper_video * mtmd_helper_video_init_from_buf ( 963const mtmd_context * mctx , 964const unsigned char * buf ,size_t len , 965mtmd_helper_video_init_params params ) { 966#ifdef MTMD_VIDEO 967auto * ctx = new mtmd_helper_video (); 968 969ctx -> mctx = mctx ; 970ctx -> input_buf .assign (buf ,buf + len ); 971ctx -> ffmpeg_bin = video_resolve_bin (params .ffmpeg_bin_dir ,"ffmpeg" ); 972ctx -> ffprobe_bin = video_resolve_bin (params .ffmpeg_bin_dir ,"ffprobe" ); 973ctx -> timestamp_interval_ms = params .timestamp_interval_ms ; 974 975if (!ctx -> probe (params .fps_target )) { 976LOG_ERR ("%s: ffprobe failed on buffer (is ffprobe in PATH?)\n" ,__func__ ); 977delete ctx ; 978return nullptr ; 979 } 980 981if (!ctx -> start_ffmpeg (0.0f )) { 982LOG_ERR ("%s: failed to start ffmpeg on buffer (is ffmpeg in PATH?)\n" ,__func__ ); 983delete ctx ; 984return nullptr ; 985 } 986 987return ctx ; 988#else 989GGML_UNUSED (mctx ); 990GGML_UNUSED (buf ); 991GGML_UNUSED (len ); 992GGML_UNUSED (params ); 993LOG_ERR ("%s: video is not supported in this build (MTMD_VIDEO is set to OFF)\n" ,__func__ ); 994return nullptr ; 995#endif 996} 997 998void mtmd_helper_video_free (mtmd_helper_video * ctx ) { 999#ifdef MTMD_VIDEO 1000if (!ctx )return ; 1001ctx -> stop_ffmpeg (); 1002delete ctx ; 1003#else 1004GGML_UNUSED (ctx ); 1005LOG_ERR ("%s: video is not supported in this build (MTMD_VIDEO is set to OFF)\n" ,__func__ ); 1006#endif 1007} 1008 1009mtmd_helper_video_info mtmd_helper_video_get_info (const mtmd_helper_video * ctx ) { 1010#ifdef MTMD_VIDEO 1011return ctx -> info ; 1012#else 1013GGML_UNUSED (ctx ); 1014GGML_ASSERT (false&& "video is not supported in this build (MTMD_VIDEO is set to OFF)" ); 1015#endif 1016} 1017 1018int32_t mtmd_helper_video_read_next (mtmd_helper_video * ctx , 1019mtmd_bitmap ** out_bitmap ,char ** out_text ) { 1020#ifdef MTMD_VIDEO 1021if (!ctx )return -2 ; 1022return ctx -> read_next (out_bitmap ,out_text ); 1023#else 1024GGML_UNUSED (ctx ); 1025GGML_UNUSED (out_bitmap ); 1026GGML_UNUSED (out_text ); 1027GGML_ASSERT (false&& "video is not supported in this build (MTMD_VIDEO is set to OFF)" ); 1028#endif 1029} 1030 1031bool mtmd_helper_model_can_chat (const llama_context * lctx ,const mtmd_context * mctx ) { 1032if (!mctx ) { 1033return true; 1034 } 1035 1036auto * model = llama_get_model (lctx ); 1037auto * tmpl = llama_model_chat_template (model ,nullptr ); 1038auto info = mtmd_gen_audio_get_info (mctx ); 1039 1040// tts-only model cannot be used for chat (no chat template) 1041bool is_tts_only = info .type != MTMD_GEN_AUDIO_TYPE_NONE && tmpl == nullptr ; 1042 1043return !is_tts_only ; 1044}