def recognize_batch(self, inputs: schemas.PredictInput, **kwargs):
        """
        Ref: https://arxiv.org/pdf/1801.00841.pdf
        This is a greedy decoding algorithm that greedily select the best token at each time step
        Only apply for batch size > 1
        """
        with tf.name_scope(f"{self.name}_recognize"):
            features, features_length = self.feature_extraction((inputs.inputs, inputs.inputs_length), training=False)
            encoded, encoded_length, next_encoder_states = self.encoder.call_next(features, features_length, inputs.previous_encoder_states)

            nframes = tf.expand_dims(encoded_length, axis=-1)  # [B, 1]
            batch_size, max_frames, _ = shape_util.shape_list(encoded)
            # The current indices of the output of encoder, shape [B, 1]
            frame_indices = tf.zeros([batch_size, 1], dtype=tf.int32, name="frame_indices")
            # Previous predicted tokens, initially are blanks, shape [B, 1]
            previous_tokens = inputs.previous_tokens
            # Previous states of the prediction network, initially are zeros, shape [B, num_rnns, nstates, rnn_units]
            previous_decoder_states = inputs.previous_decoder_states
            # Assumption that number of tokens can not exceed (2 * the size of output of encoder + 1), this is for static runs like TPU or TFLite
            max_tokens = max_frames * 2 + 1
            # All of the tokens that are getting recognized, initially are blanks, shape [B, nframes * 2 + 1]
            tokens = tf.ones([batch_size, max_tokens], dtype=tf.int32, name="tokens") * self.blank
            # The current indices of the token that are currently being recognized, shape [B, 1], the tokens indices are started with 1 so that any
            # blank token recognized got updated to index 0 to avoid affecting results
            tokens_indices = tf.ones([batch_size, 1], dtype=tf.int32, name="tokens_indices")

            def cond(_frame_indices, _previous_tokens, _previous_decoder_states, _tokens, _tokens_indices):
                return tf.logical_not(  # Reversed so that the loop check and continue
                    # One of the following condition met will terminate the loop
                    tf.logical_or(
                        # Stop when ALL of the indices of the output of the encoder reach the end
                        tf.math.reduce_all(tf.greater_equal(_frame_indices, nframes - 1)),
                        # Stop when ALL of the indices of recognized tokens reach the end
                        tf.math.reduce_all(tf.greater_equal(_tokens_indices, max_tokens - 1)),
                    )
                )

            def body(_frame_indices, _previous_tokens, _previous_decoder_states, _tokens, _tokens_indices):
                _current_frames = tf.expand_dims(tf.gather_nd(encoded, tf.minimum(_frame_indices, nframes - 1), batch_dims=1), axis=1)  # [B, 1, E]
                _log_softmax, _states = self.call_next(_current_frames, _previous_tokens, _previous_decoder_states)
                _current_tokens = tf.reshape(tf.argmax(_log_softmax, axis=-1, output_type=tf.int32), [batch_size, 1])  # [B, 1, 1] -> [B, 1]
                # conditions, blanks are ignored
                _equal_blank = tf.equal(_current_tokens, self.blank)  # [B, 1]
                # if the token index >= max tokens, it's already finished, set to blank to ignore
                _equal_blank = tf.logical_or(_equal_blank, tf.greater_equal(_tokens_indices, max_tokens))
                # if the frame index > nframes, it's already done, set to blank to ignore
                _equal_blank = tf.logical_or(_equal_blank, tf.greater(_frame_indices, nframes))
                # update results
                _update_tokens = tf.reshape(tf.where(_equal_blank, self.blank, _current_tokens), [batch_size])  # [B]
                _update_tokens_indices = tf.where(
                    _equal_blank, 0, tf.minimum(tf.add(_tokens_indices, 1), max_tokens - 1)
                )  # blanks are getting updated at index 0 to avoid affecting results
                _tokens = tf.tensor_scatter_nd_update(
                    tensor=_tokens,
                    indices=tf.concat([tf.expand_dims(tf.range(batch_size, dtype=tf.int32), axis=-1), _update_tokens_indices], -1),  # [B, 2]
                    updates=_update_tokens,  # [B]
                )
                _tokens_indices = tf.where(_equal_blank, _tokens_indices, tf.minimum(tf.add(_tokens_indices, 1), max_tokens - 1))
                # update states
                _frame_indices = tf.where(_equal_blank, tf.add(_frame_indices, 1), _frame_indices)  # blank then next frames, else current frames
                _previous_tokens = tf.where(_equal_blank, _previous_tokens, _current_tokens)  # blank then keep prev tokens, else next tokens
                _previous_decoder_states = tf.where(
                    tf.reshape(_equal_blank, [batch_size, 1, 1, 1]), _previous_decoder_states, _states
                )  # blank then keep prev states, else next states # pylint: disable=line-too-long
                return _frame_indices, _previous_tokens, _previous_decoder_states, _tokens, _tokens_indices

            (
                frame_indices,
                next_tokens,
                next_decoder_states,
                tokens,
                tokens_indices,
            ) = tf.while_loop(cond, body, loop_vars=(frame_indices, previous_tokens, previous_decoder_states, tokens, tokens_indices))

            return schemas.PredictOutput(
                tokens=tokens,
                next_tokens=next_tokens,
                next_encoder_states=next_encoder_states,
                next_decoder_states=next_decoder_states,
            )

        Ref: https://arxiv.org/pdf/1801.00841.pdf
        This is a greedy decoding algorithm that greedily select the best token at each time step
        Only apply for batch size > 1
        
tensorflow_asr/models/transducer/base_transducer.py