@@ -817,6 +817,151 @@ def _sample_step(self, score: Score, step, temperature: float, seed: int = -1) -
817817
818818 return result
819819
820+ def score_from_tokens (
821+ self ,
822+ token_ids : list ,
823+ step_idx : int = 0 ,
824+ use_velocity : int = - 1 ,
825+ ) -> dict :
826+ """Teacher-forced log P(token_ids | context from this session).
827+
828+ Uses a single forward pass over [context_tokens + token_ids], then
829+ steps the grammar state token-by-token to get per-position masks.
830+ Returns log P of each token_id under the masked distribution (T=1).
831+
832+ step_idx: which planner step to use (0 = first/cold-start, which is
833+ the most informative for cross-conditional scoring).
834+ """
835+ import copy
836+ try :
837+ import torch
838+ except ImportError :
839+ raise ImportError ("pip install midigpt[inference]" ) from None
840+
841+ cfg = self ._request .config
842+ mask = self ._build_selection_mask ()
843+ import json as _json
844+ enc_cfg = self ._engine ._tokenizer ._vocab .config ()
845+ try :
846+ dims = sorted (
847+ set (_json .loads (enc_cfg .to_json ()).get ("num_bars_map" ) or []),
848+ reverse = True ,
849+ )
850+ except Exception :
851+ dims = []
852+ candidates = [d for d in dims if d <= cfg .model_dim ] or [cfg .model_dim ]
853+ enc_cfg .model_dim = candidates [0 ]
854+
855+ planner = _core .StepPlanner (
856+ mask , enc_cfg , cfg .bars_per_step , cfg .tracks_per_step
857+ )
858+ steps = list (planner .plan ())
859+ if step_idx >= len (steps ) or not token_ids :
860+ return {"log_probs" : [], "total_logp" : 0.0 , "n_tokens" : 0 , "per_type" : {}}
861+ step = steps [step_idx ]
862+
863+ # Build score + SessionState (minimal setup — no analyzer attributes)
864+ py_score = self ._score if isinstance (self ._score , Score ) else from_cpp (self ._score )
865+ score = self ._engine ._tokenizer .normalize_input (copy .deepcopy (py_score ))
866+
867+ # _build_constraints uses self._full_ar_ids; infill scoring has none
868+ self ._full_ar_ids = set ()
869+ state = _core .SessionState (
870+ to_cpp (score ), step ,
871+ self ._engine ._tokenizer ._vocab ,
872+ self ._build_constraints (step ),
873+ self ._engine ._tokenizer ._encoder ,
874+ self ._engine ._tokenizer ._decoder ,
875+ use_span_masks = False ,
876+ remove_future_bars = False ,
877+ use_velocity = use_velocity , use_microtiming = - 1 , genre = - 1 ,
878+ )
879+ context_tokens = list (state .context_tokens ())
880+ ctx_len = len (context_tokens )
881+
882+ # Prefill over the full sequence [ctx + token_ids]. Generation never
883+ # exceeds the positional window (it stops at the budget), but replay
884+ # scoring can: fall back to sliding-window scoring with stride =
885+ # half window, so every position keeps at least half a window of
886+ # left context.
887+ full_seq = context_tokens + list (token_ids )
888+ dev = next (self ._engine ._model .parameters ()).device
889+
890+ def _forward (seq ):
891+ inp = torch .tensor ([seq ], dtype = torch .long , device = dev )
892+ with torch .no_grad ():
893+ try :
894+ out = self ._engine ._model (inp )
895+ except Exception :
896+ try :
897+ out = self ._engine ._model (inp , None )
898+ except Exception :
899+ out = self ._engine ._model (inp .cpu ())
900+ if not isinstance (out , tuple ):
901+ out = (out ,)
902+ return out [0 ].cpu ()[0 ] # [seq_len, vocab]
903+
904+ n_pos = self ._model_max_context ()
905+ if len (full_seq ) <= n_pos :
906+ _logits_seq = _forward (full_seq )
907+
908+ def _logits_at (p ):
909+ return _logits_seq [p ]
910+ else :
911+ _stride = max (1 , n_pos // 2 )
912+ _window_cache : dict [int , torch .Tensor ] = {}
913+
914+ def _logits_at (p ):
915+ if p < n_pos :
916+ start = 0
917+ else :
918+ # smallest stride-aligned start that keeps >= n_pos - stride
919+ # tokens of left context for position p
920+ start = ((p - n_pos + 1 + _stride - 1 ) // _stride ) * _stride
921+ if start not in _window_cache :
922+ _window_cache [start ] = _forward (full_seq [start :start + n_pos ])
923+ return _window_cache [start ][p - start ]
924+
925+ vocab = self ._engine ._tokenizer ._vocab
926+ log_probs : list [float ] = []
927+ per_type : dict [str , list ] = {}
928+
929+ for i , tid in enumerate (token_ids ):
930+ logit_pos = ctx_len - 1 + i
931+ logits = _logits_at (logit_pos )
932+
933+ mask_cpu = torch .as_tensor (state .logit_mask (), dtype = torch .bool )
934+ n_legal = int (mask_cpu .sum ().item ())
935+ if n_legal > 0 :
936+ masked_logits = logits .masked_fill (~ mask_cpu , float ("-inf" ))
937+ else :
938+ masked_logits = logits
939+
940+ log_p_dist = torch .log_softmax (masked_logits , dim = - 1 )
941+ lp = float (log_p_dist [int (tid )].item ())
942+ log_probs .append (lp )
943+
944+ try :
945+ tt = vocab .get_type (int (tid )).name
946+ except Exception :
947+ tt = "?"
948+ per_type .setdefault (tt , []).append (lp )
949+
950+ try :
951+ state .advance (int (tid ))
952+ except Exception :
953+ break
954+
955+ return {
956+ "log_probs" : log_probs ,
957+ "total_logp" : float (sum (log_probs )),
958+ "n_tokens" : len (log_probs ),
959+ "per_type" : {
960+ k : {"mean" : sum (v ) / len (v ), "sum" : sum (v ), "n" : len (v )}
961+ for k , v in per_type .items ()
962+ },
963+ }
964+
820965 def _snapshot_prompt_state (self , step ) -> dict :
821966 """Per-bar prompt state for the current step, post-mask_bars patch.
822967
0 commit comments