Skip to content

Commit b967b7a

Browse files
committed
fix: tensor & testcases
1 parent 06cb949 commit b967b7a

2 files changed

Lines changed: 145 additions & 167 deletions

File tree

keras_hub/src/models/gemma3n/gemma3n_audio_converter.py

Lines changed: 111 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import math
22

33
import numpy as np
4-
5-
try:
6-
import tensorflow as tf
7-
except ImportError:
8-
tf = None
94
from keras import KerasTensor
105
from keras import ops
116
from keras import random
@@ -522,153 +517,134 @@ def call(
522517
return_attention_mask=True,
523518
):
524519
if isinstance(raw_speech, KerasTensor):
525-
return self.compute_output_spec(raw_speech)
526-
if isinstance(raw_speech, (list, tuple)):
527-
speech_list = [
528-
np.asarray(speech).reshape(-1) for speech in raw_speech
529-
]
530-
input_features_list, attention_mask_list = self.pad(
531-
speech_list,
532-
padding=padding,
533-
max_length=max_length,
534-
truncation=truncation,
535-
pad_to_multiple_of=pad_to_multiple_of,
536-
return_attention_mask=return_attention_mask,
520+
return self.compute_output_spec(
521+
raw_speech, return_attention_mask=return_attention_mask
537522
)
538-
if not return_attention_mask:
539-
attention_mask_list = [None] * len(input_features_list)
540-
prepared_features = []
541-
prepared_masks = []
542-
for speech, mask in zip(input_features_list, attention_mask_list):
543-
speech = ops.convert_to_tensor(
544-
np.asarray(speech).reshape(-1), dtype=self.compute_dtype
545-
)
546523

547-
mask_tensor = (
548-
ops.convert_to_tensor(
549-
np.asarray(mask).reshape(-1), dtype="int32"
550-
)
551-
if mask is not None
552-
else None
524+
if ops.is_tensor(raw_speech):
525+
waveform = ops.cast(raw_speech, dtype=self.compute_dtype)
526+
waveform_rank = len(ops.shape(waveform))
527+
if waveform_rank not in (1, 2):
528+
raise ValueError(
529+
f"`raw_speech` must have rank 1 or 2. "
530+
f"Received shape: {ops.shape(raw_speech)}."
553531
)
554-
555-
# Single waveform -> 1-D input.
556-
features, feature_mask = self._extract_spectrogram(
557-
speech, mask_tensor
532+
if (
533+
max_length is not None
534+
and pad_to_multiple_of is not None
535+
and max_length % pad_to_multiple_of != 0
536+
):
537+
max_length = (
538+
(max_length // pad_to_multiple_of) + 1
539+
) * pad_to_multiple_of
540+
541+
if truncation and max_length is not None:
542+
waveform = waveform[..., :max_length]
543+
544+
if padding == "max_length" and max_length is not None:
545+
current_len = ops.shape(waveform)[-1]
546+
pad_len = ops.maximum(0, max_length - current_len)
547+
paddings = (
548+
[[0, pad_len]]
549+
if waveform_rank == 1
550+
else [[0, 0], [0, pad_len]]
558551
)
559-
features = ops.reshape(features, (-1, self.feature_size))
560-
if return_attention_mask:
561-
if feature_mask is None:
562-
feature_mask = ops.ones(
563-
(ops.shape(features)[0],), dtype="int32"
564-
)
565-
else:
566-
feature_mask = ops.reshape(feature_mask, (-1,))
567-
feature_mask = ops.cast(feature_mask, "int32")
568-
569-
prepared_masks.append(feature_mask)
570-
prepared_features.append(features)
571-
572-
input_features = ops.stack(prepared_features, axis=0)
552+
waveform = ops.pad(
553+
waveform, paddings, constant_values=self.padding_value
554+
)
555+
elif padding == "longest" and pad_to_multiple_of is not None:
556+
current_len = ops.shape(waveform)[-1]
557+
rem = current_len % pad_to_multiple_of
558+
pad_len = (pad_to_multiple_of - rem) % pad_to_multiple_of
559+
paddings = (
560+
[[0, pad_len]]
561+
if waveform_rank == 1
562+
else [[0, 0], [0, pad_len]]
563+
)
564+
waveform = ops.pad(
565+
waveform, paddings, constant_values=self.padding_value
566+
)
567+
573568
if return_attention_mask:
574-
input_features_mask = ops.stack(prepared_masks, axis=0)
569+
current_len = ops.shape(waveform)[-1]
570+
if waveform_rank == 1:
571+
mask = ops.ones((current_len,), dtype="int32")
572+
else:
573+
batch_size = ops.shape(waveform)[0]
574+
mask = ops.ones((batch_size, current_len), dtype="int32")
575575
else:
576-
input_features_mask = None
577-
return input_features, input_features_mask
576+
mask = None
577+
578+
features, feature_mask = self._extract_spectrogram(waveform, mask)
579+
return features, feature_mask
578580

581+
# Handle NumPy inputs and determine whether input is batched
579582
if isinstance(raw_speech, (list, tuple)):
580-
raw_speech_tensor = ops.stack(
581-
[
582-
speech
583-
if ops.is_tensor(speech)
584-
else ops.convert_to_tensor(speech)
585-
for speech in raw_speech
586-
],
587-
axis=0,
588-
)
589-
elif ops.is_tensor(raw_speech):
590-
raw_speech_tensor = raw_speech
583+
is_batched = True
584+
speech_list = [
585+
ops.convert_to_numpy(s).reshape(-1) for s in raw_speech
586+
]
591587
else:
592-
raw_speech_tensor = ops.convert_to_tensor(raw_speech)
588+
speech_np = ops.convert_to_numpy(raw_speech)
589+
if speech_np.ndim == 1:
590+
is_batched = False
591+
speech_list = [speech_np.reshape(-1)]
592+
elif speech_np.ndim == 2:
593+
is_batched = True
594+
speech_list = [s.reshape(-1) for s in speech_np]
595+
else:
596+
raise ValueError(
597+
f"`raw_speech` must have rank 1 or 2. "
598+
f"Received shape: {speech_np.shape}."
599+
)
593600

594-
rank = ops.ndim(raw_speech_tensor)
595-
if rank == 1:
596-
speech_np = np.asarray(raw_speech_tensor).reshape(-1)
601+
# Pad or truncate using self.pad()
602+
input_features_list, attention_mask_list = self.pad(
603+
speech_list,
604+
padding=padding,
605+
max_length=max_length,
606+
truncation=truncation,
607+
pad_to_multiple_of=pad_to_multiple_of,
608+
return_attention_mask=return_attention_mask,
609+
)
597610

598-
input_features_list, attention_mask_list = self.pad(
599-
[speech_np],
600-
padding=padding,
601-
max_length=max_length,
602-
truncation=truncation,
603-
pad_to_multiple_of=pad_to_multiple_of,
604-
return_attention_mask=return_attention_mask,
611+
if not input_features_list:
612+
features = ops.zeros(
613+
(0, 0, self.feature_size)
614+
if is_batched
615+
else (0, self.feature_size),
616+
dtype=self.compute_dtype,
605617
)
606-
if not input_features_list:
607-
features = ops.zeros(
608-
(0, self.feature_size), dtype=self.compute_dtype
609-
)
610-
mask = (
611-
ops.zeros((0,), dtype="int32")
612-
if return_attention_mask
613-
else None
614-
)
615-
return features, mask
616-
617-
speech = ops.convert_to_tensor(
618-
input_features_list[0], dtype=self.compute_dtype
618+
mask = (
619+
ops.zeros((0, 0) if is_batched else (0,), dtype="int32")
620+
if return_attention_mask
621+
else None
619622
)
620-
if return_attention_mask:
621-
mask = ops.convert_to_tensor(
622-
attention_mask_list[0], dtype="int32"
623-
)
624-
else:
625-
mask = None
626-
features, feature_mask = self._extract_spectrogram(speech, mask)
627-
features = ops.reshape(features, (-1, self.feature_size))
623+
return features, mask
628624

629-
if return_attention_mask:
630-
if feature_mask is None:
631-
feature_mask = ops.ones(
632-
(ops.shape(features)[0],), dtype="int32"
633-
)
634-
else:
635-
feature_mask = ops.reshape(feature_mask, (-1,))
636-
feature_mask = ops.cast(feature_mask, "int32")
637-
else:
638-
feature_mask = None
639-
return features, feature_mask
640-
641-
def process_one_audio(speech):
642-
speech = ops.reshape(speech, (-1,))
643-
speech = ops.cast(speech, self.compute_dtype)
644-
speech_batched = ops.expand_dims(speech, axis=0)
645-
speech_length = ops.shape(speech_batched)[1]
646-
speech_mask = ops.ones((1, speech_length), dtype="int32")
647-
features, feature_mask = self._extract_spectrogram(
648-
speech_batched, speech_mask
625+
# Stack into tensors
626+
padded_speech = ops.convert_to_tensor(
627+
np.stack(input_features_list, axis=0), dtype=self.compute_dtype
628+
)
629+
padded_mask = (
630+
ops.convert_to_tensor(
631+
np.stack(attention_mask_list, axis=0), dtype="int32"
649632
)
650-
features = ops.reshape(features, (-1, self.feature_size))
651-
features = ops.cast(features, self.compute_dtype)
652-
if feature_mask is None:
653-
num_frames = ops.shape(features)[0]
654-
feature_mask = ops.ones((num_frames,), dtype="int32")
655-
else:
656-
feature_mask = ops.reshape(feature_mask, (-1,))
657-
feature_mask = ops.cast(feature_mask, "int32")
658-
return features, feature_mask
633+
if return_attention_mask
634+
else None
635+
)
636+
637+
# If unbatched (1D), squeeze the batch dimension
638+
if not is_batched:
639+
padded_speech = ops.squeeze(padded_speech, axis=0)
640+
if padded_mask is not None:
641+
padded_mask = ops.squeeze(padded_mask, axis=0)
659642

660-
input_features, input_features_mask = tf.map_fn(
661-
process_one_audio,
662-
raw_speech_tensor,
663-
fn_output_signature=(
664-
tf.TensorSpec(
665-
shape=(None, self.feature_size),
666-
dtype=tf.as_dtype(self.compute_dtype),
667-
),
668-
tf.TensorSpec(shape=(None,), dtype=tf.int32),
669-
),
643+
# Extract spectrograms (natively supports 1D and 2D with Keras ops)
644+
features, feature_mask = self._extract_spectrogram(
645+
padded_speech, padded_mask
670646
)
671-
return input_features, input_features_mask
647+
return features, feature_mask
672648

673649
def get_config(self):
674650
config = super().get_config()

keras_hub/src/models/gemma3n/gemma3n_audio_converter_test.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
from keras import ops
23

34
from keras_hub.src.models.gemma3n.gemma3n_audio_converter import (
45
Gemma3nAudioConverter,
@@ -110,12 +111,13 @@ def test_normalization(self):
110111
self.assertEqual(len(outputs_norm), 2)
111112
features_no_norm, _ = outputs_no_norm
112113
features_norm, _ = outputs_norm
114+
features_no_norm_np = ops.convert_to_numpy(features_no_norm)
113115
# We would want outputs to be different.
114116
self.assertNotAllClose(features_no_norm, features_norm)
115117
# Manually normalize and check for closeness.
116-
manual_norm_features = (features_no_norm - np.array(mean)) / np.array(
117-
stddev
118-
)
118+
manual_norm_features = (
119+
features_no_norm_np - np.array(mean)
120+
) / np.array(stddev)
119121
self.assertAllClose(manual_norm_features, features_norm)
120122

121123
def test_serialization(self):
@@ -150,12 +152,11 @@ def test_normalization_preserves_mask(self):
150152
features_1, mask_1 = converter_no_norm(self.input_data)
151153
features_2, mask_2 = converter_norm(self.input_data)
152154

153-
np.testing.assert_array_equal(mask_1, mask_2)
154-
assert features_1.shape == features_2.shape
155+
self.assertAllEqual(mask_1, mask_2)
156+
self.assertEqual(features_1.shape, features_2.shape)
155157

156158
def test_batched_audio(self):
157159
converter = Gemma3nAudioConverter(**self.init_kwargs)
158-
159160
audio_1 = np.sin(
160161
2
161162
* np.pi
@@ -180,34 +181,35 @@ def test_batched_audio(self):
180181
self.assertEqual(mask.shape[0], 2)
181182
self.assertEqual(mask.shape[1], features.shape[1])
182183

183-
self.assertTrue(np.all(mask[0]))
184-
self.assertTrue(np.any(mask[1] == 0))
184+
mask_np = ops.convert_to_numpy(mask)
185+
self.assertTrue(np.all(mask_np[0]))
186+
self.assertTrue(np.any(mask_np[1] == 0))
185187

186-
def test_truncation(self):
187-
converter = Gemma3nAudioConverter(**self.init_kwargs)
188+
def test_truncation(self):
189+
converter = Gemma3nAudioConverter(**self.init_kwargs)
188190

189-
max_length = 1024
190-
features, mask = converter(
191-
self.input_data[0],
192-
padding="max_length",
193-
max_length=max_length,
194-
truncation=True,
195-
)
191+
max_length = 1024
192+
features, mask = converter(
193+
self.input_data[0],
194+
padding="max_length",
195+
max_length=max_length,
196+
truncation=True,
197+
)
196198

197-
frame_length = int(
198-
round(self.sampling_rate * self.frame_length_ms / 1000.0)
199-
)
200-
hop_length = int(
201-
round(self.sampling_rate * self.hop_length_ms / 1000.0)
202-
)
199+
frame_length = int(
200+
round(self.sampling_rate * self.frame_length_ms / 1000.0)
201+
)
202+
hop_length = int(
203+
round(self.sampling_rate * self.hop_length_ms / 1000.0)
204+
)
203205

204-
# _extract_spectrogram() uses frame_length + 1 because of
205-
# the preemphasis calculation.
206-
sequence_length = frame_length + 1
207-
num_frames = ((max_length - sequence_length) // hop_length) + 1
206+
# _extract_spectrogram() uses frame_length + 1 because of
207+
# the preemphasis calculation.
208+
sequence_length = frame_length + 1
209+
num_frames = ((max_length - sequence_length) // hop_length) + 1
208210

209-
self.assertEqual(
210-
features.shape,
211-
(num_frames, self.feature_size),
212-
)
213-
self.assertEqual(mask.shape, (num_frames,))
211+
self.assertEqual(
212+
features.shape,
213+
(num_frames, self.feature_size),
214+
)
215+
self.assertEqual(mask.shape, (num_frames,))

0 commit comments

Comments
 (0)