sponsorblock-ml / src /segment.py
Joshua Lochner
Remove unused imports
bdfb4b1
raw
history blame
4.6 kB
import preprocess
from dataclasses import dataclass, field
@dataclass
class SegmentationArguments:
pause_threshold: int = field(default=2.5, metadata={
'help': 'When the time between words is greater than pause threshold, force into a new segment'})
def get_overlapping_chunks_of_tokens(tokens, size, overlap):
for i in range(0, len(tokens), size-overlap+1):
yield tokens[i:i+size]
# Generate up to SAFETY_TOKENS_PERCENTAGE*max_tokens tokens
MIN_SAFETY_TOKENS = 8
SAFETY_TOKENS_PERCENTAGE = 0.9765625
# e.g. 512 -> 500, 768 -> 750
# TODO play around with this?
OVERLAP_TOKEN_PERCENTAGE = 0.5 # 0.25
def add_labels_to_words(words, sponsor_segments):
for sponsor_segment in sponsor_segments:
for w in extract_segment(words, sponsor_segment['start'], sponsor_segment['end']):
w['category'] = sponsor_segment['category']
return words
def generate_labelled_segments(words, tokenizer, segmentation_args, sponsor_segments):
segments = generate_segments(words, tokenizer, segmentation_args)
labelled_segments = list(
map(lambda x: add_labels_to_words(x, sponsor_segments), segments))
return labelled_segments
def word_start(word):
return word['start']
def word_end(word):
return word.get('end', word['start'])
def generate_segments(words, tokenizer, segmentation_args):
first_pass_segments = []
for index, word in enumerate(words):
# Get length of tokenized word
word['cleaned'] = preprocess.clean_text(word['text'])
word['num_tokens'] = len(
tokenizer(word['cleaned'], add_special_tokens=False, truncation=True).input_ids)
# Add new segment
if index == 0 or word_start(words[index]) - word_end(words[index-1]) >= segmentation_args.pause_threshold:
first_pass_segments.append([word])
else: # Add to current segment
first_pass_segments[-1].append(word)
max_q_size = round(SAFETY_TOKENS_PERCENTAGE * tokenizer.model_max_length)
buffer_size = OVERLAP_TOKEN_PERCENTAGE*max_q_size # tokenizer.model_max_length
# In second pass, we split those segments if too big
second_pass_segments = []
for segment in first_pass_segments:
current_segment_num_tokens = 0
current_segment = []
for word in segment:
new_seg = current_segment_num_tokens + \
word['num_tokens'] >= max_q_size
if new_seg:
# Adding this token would make it have too many tokens
# We save this batch and create new
second_pass_segments.append(current_segment)
# Add tokens to current segment
current_segment.append(word)
current_segment_num_tokens += word['num_tokens']
if not new_seg:
continue
# Just created a new segment, so we remove until we only have buffer_size tokens
last_index = 0
while current_segment_num_tokens > buffer_size and current_segment:
current_segment_num_tokens -= current_segment[last_index]['num_tokens']
last_index += 1
current_segment = current_segment[last_index:]
if current_segment: # Add remaining segment
second_pass_segments.append(current_segment)
# Cleaning up, delete 'num_tokens' from each word
# for segment in second_pass_segments:
for word in words:
word.pop('num_tokens', None)
return second_pass_segments
def extract_segment(words, start, end, map_function=None):
"""Extracts all words with time in [start, end]"""
a = binary_search(words, 0, len(words), start, True)
b = min(binary_search(words, 0, len(words), end, False) + 1, len(words))
to_transform = map_function is not None and callable(map_function)
return [
map_function(words[i]) if to_transform else words[i] for i in range(a, b)
]
def binary_search(words, start_index, end_index, time, below):
"""Binary search to get first index of word whose start/end time is greater/less than some value"""
if start_index >= end_index:
return end_index
middle_index = (start_index + end_index) // 2
middle_time = word_start(
words[middle_index]) if below else word_end(words[middle_index])
# TODO if above: if time < middle_time binary_search(start, middle-1)
if time <= middle_time:
return binary_search(words, start_index, middle_index, time, below)
else:
return binary_search(words, middle_index + 1, end_index, time, below)