@@ -515,6 +515,75 @@ def _is_audio_chunk(self, chunk_type: int) -> bool:
515515 == self ._mtmd_cpp .mtmd_input_chunk_type .MTMD_INPUT_CHUNK_TYPE_AUDIO
516516 )
517517
518+ def _mtmd_tokenize (
519+ self ,
520+ llama : "llama_core.Llama" ,
521+ text : str ,
522+ bitmaps : Optional [list ] = None ,
523+ chunks : Optional [Any ] = None ,
524+ ) -> Any :
525+ """
526+ Perform MTMD hybrid tokenization.
527+
528+ This function isolates the llama.cpp mtmd_tokenize call
529+ so that prompt construction logic is decoupled from runtime execution.
530+
531+ It guarantees:
532+ - stable interface for future async/batch decoding
533+ - isolated error handling for tokenizer failures
534+ - clean separation between prompt building and C++ binding
535+ """
536+
537+ if chunks is None :
538+ chunks = self ._mtmd_cpp .mtmd_input_chunks_init ()
539+ if chunks is None :
540+ raise ValueError (
541+ f"{ self .log_prefix } (_mtmd_tokenize): failed to init mtmd_input_chunks"
542+ )
543+
544+ # Validate strict alignment between rendered media markers and provided bitmaps
545+ # to ensure MTMD tokenization consistency and prevent decoding mismatch errors.
546+ if bitmaps is not None :
547+ marker_count = text .count (self .media_marker )
548+ if marker_count != len (bitmaps ):
549+ raise ValueError (
550+ f"{ self .log_prefix } (_mtmd_tokenize): marker mismatch "
551+ f"(marker_count={ marker_count } , bitmap_count={ len (bitmaps )} )"
552+ )
553+
554+ input_text = self ._mtmd_cpp .mtmd_input_text ()
555+ input_text .text = ctypes .c_char_p (text .encode ("utf-8" ))
556+ input_text .add_special = (llama .n_tokens == 0 )
557+ input_text .parse_special = True
558+
559+ bitmap_array = None
560+ n_bitmaps = 0
561+
562+ if bitmaps :
563+ n_bitmaps = len (bitmaps )
564+ bitmap_array = (self ._mtmd_cpp .mtmd_bitmap_p_ctypes * n_bitmaps )(* bitmaps )
565+ else :
566+ bitmap_array = None
567+ n_bitmaps = 0
568+
569+ result = self ._mtmd_cpp .mtmd_tokenize (
570+ self .mtmd_ctx ,
571+ chunks ,
572+ ctypes .byref (input_text ),
573+ bitmap_array ,
574+ n_bitmaps ,
575+ )
576+
577+ if result != 0 :
578+ raise ValueError (
579+ f"{ self .log_prefix } (_mtmd_tokenize): tokenize failed\n "
580+ f"- result={ result } \n "
581+ f"- text_len={ len (text )} \n "
582+ f"- n_bitmaps={ n_bitmaps } \n "
583+ )
584+
585+ return chunks
586+
518587 def _process_mtmd_prompt (
519588 self ,
520589 llama : llama_core .Llama ,
@@ -572,8 +641,12 @@ def _process_mtmd_prompt(
572641 text = text .replace (item ["url" ], media_marker )
573642
574643 if self .verbose :
575- print (f"{ self .log_prefix } (_process_mtmd_prompt): Rendered prompt length: { len (text )} chars, Media count: { len (media_items )} ." , file = sys .stderr )
576- print (f"{ self .log_prefix } (_process_mtmd_prompt): Rendered prompt: { text } " , file = sys .stderr )
644+ print (
645+ f"{ self .log_prefix } (_process_mtmd_prompt): "
646+ f"Rendered prompt length: { len (text )} chars, Media count: { len (media_items )} .\n "
647+ f"Rendered prompt: { text } " ,
648+ file = sys .stderr ,
649+ )
577650
578651 # 3. Pre-allocate bitmap array to guarantee chronological order during concurrent decoding
579652 bitmaps = [None ] * len (media_items )
@@ -614,37 +687,21 @@ def _create_bitmap_func(idx: int, item: dict):
614687 # If there are no images, set the bitmaps to empty.
615688 bitmaps = []
616689
617- # 4. Initialize mtmd_input_chunks
618- input_text = self ._mtmd_cpp .mtmd_input_text ()
619- input_text .text = text .encode ('utf-8' )
620- input_text .add_special = (llama .n_tokens == 0 )
621- input_text .parse_special = True
622-
623- chunks = self ._mtmd_cpp .mtmd_input_chunks_init ()
624- if chunks is None :
625- raise ValueError (f"{ self .log_prefix } (mtmd_input_chunks_init): Failed to initialize mtmd_input_chunks." )
626-
627- # 5. Hybrid Tokenization (Text + Media binding)
628- if len (bitmaps ) > 0 :
629- bitmap_array = (self ._mtmd_cpp .mtmd_bitmap_p_ctypes * len (bitmaps ))(* bitmaps )
630- result = self ._mtmd_cpp .mtmd_tokenize (
631- self .mtmd_ctx , chunks , ctypes .byref (input_text ), bitmap_array , len (bitmaps )
632- )
633- else :
634- result = self ._mtmd_cpp .mtmd_tokenize (
635- self .mtmd_ctx , chunks , ctypes .byref (input_text ), None , 0
636- )
637-
638- if result != 0 :
639- raise ValueError (f"{ self .log_prefix } (mtmd_tokenize): Unable to tokenize prompt, res = { result } ." )
690+ # 4. Hybrid Tokenization (Text + Media)
691+ chunks = self ._mtmd_tokenize (
692+ llama = llama ,
693+ text = text ,
694+ bitmaps = bitmaps ,
695+ chunks = None ,
696+ )
640697
641698 # Video helper contexts only need to stay alive until mtmd_tokenize() completes.
642699 if video_cleanup :
643700 for video_ctx in video_cleanup :
644701 self ._mtmd_cpp .mtmd_helper_video_free (video_ctx )
645702 video_cleanup .clear ()
646703
647- # 6 . Virtual Token Ledger Construction
704+ # 5 . Virtual Token Ledger Construction
648705 full_prompt_ids = []
649706 chunk_token_spans = []
650707 current_idx = 0
0 commit comments