File size: 26,589 Bytes
12089c6
 
 
cf104cb
 
 
12089c6
 
 
 
 
 
e13d740
12089c6
 
 
 
 
 
 
e13d740
 
 
 
 
 
 
 
 
cf104cb
e13d740
 
 
 
 
cf104cb
e13d740
 
 
12089c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e13d740
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12089c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from nltk.tokenize import sent_tokenize, word_tokenize
from nltk.stem import WordNetLemmatizer
from keybert import KeyBERT
import re
from collections import defaultdict
from sklearn.cluster import AgglomerativeClustering

class TopicSegmenter:
    def __init__(self, window_size=3, similarity_threshold=0.2, context_size=2, min_segment_size=2, topic_similarity_threshold=0.3, 
                max_topics=8, hierarchical_threshold=0.6, model_name="all-MiniLM-L6-v2"):
        self.window_size = window_size
        self.similarity_threshold = similarity_threshold
        self.context_size = context_size
        self.min_segment_size = min_segment_size
        self.topic_similarity_threshold = topic_similarity_threshold
        self.max_topics = max_topics  # Maximum number of main topics
        self.hierarchical_threshold = hierarchical_threshold  # Threshold for hierarchical clustering
        
        try:
            # Explicitly set the model to a lightweight and compatible one
            import os
            os.environ["TOKENIZERS_PARALLELISM"] = "false"  # Prevent parallelism warning
            
            # Try using sentence-transformers directly first
            try:
                from sentence_transformers import SentenceTransformer
                embedding_model = SentenceTransformer(model_name)
                self.keyword_extractor = KeyBERT(model=embedding_model)
            except (ImportError, ValueError, RuntimeError) as e:
                # Fallback to default model with lower-level settings
                self.keyword_extractor = KeyBERT(model='distilbert-base-nli-mean-tokens')
        except Exception as e:
            import logging
            logging.error(f"Error initializing KeyBERT model: {str(e)}")
            # Last resort fallback to simple keyword extraction without a model
            self.keyword_extractor = None
        
        self.vectorizer = TfidfVectorizer(
            stop_words='english',
            min_df=1,
            max_df=0.95,
            ngram_range=(1, 2)
        )
        
        self.segment_vectorizer = TfidfVectorizer(
            stop_words='english',
            min_df=1,
            max_df=1.0,
            ngram_range=(1, 2)
        )
        
        self.lemmatizer = WordNetLemmatizer()
        self.topic_history = []
        self.topic_hierarchy = {}  # Store parent-child relationships
        self.fitted_vectorizer = None
        
    def preprocess_text(self, text):
        # First, split the text by timestamps
        timestamp_chunks = re.findall(r'\[(\d+)\](.*?)(?=\[\d+\]|$)', text, re.DOTALL)
        
        all_sentences = []
        original_sentences = []
        timestamp_mapping = []  # To track which timestamp each sentence belongs to
        
        for timestamp, chunk_text in timestamp_chunks:
            # Split each chunk into sentences
            chunk_sentences = sent_tokenize(chunk_text.strip())
            for sentence in chunk_sentences:
                if sentence.strip():
                    # Keep track of the original sentence
                    original_sentences.append(sentence.strip())
                    
                    # Clean and process the sentence for analysis
                    clean_sentence = re.sub(r'^[^:]+:', '', sentence).strip()
                    clean_sentence = re.sub(r'[^\w\s]', '', clean_sentence.lower())
                    words = word_tokenize(clean_sentence)
                    lemmatized = [self.lemmatizer.lemmatize(word) for word in words]
                    processed_sentence = ' '.join(lemmatized)
                    
                    all_sentences.append(processed_sentence)
                    timestamp_mapping.append(int(timestamp))
        
        return all_sentences, original_sentences, timestamp_mapping
    
    def get_topic_fingerprint(self, segment_text):
        cleaned_text = ' '.join([re.sub(r'^[^:]+:', '', sent).strip() for sent in segment_text])
        
        if self.fitted_vectorizer is None:
            self.fitted_vectorizer = self.segment_vectorizer.fit([cleaned_text])
            tfidf_matrix = self.fitted_vectorizer.transform([cleaned_text])
        else:
            tfidf_matrix = self.fitted_vectorizer.transform([cleaned_text])
            
        return tfidf_matrix.toarray()[0]

    def compare_with_previous_topics(self, current_segment):
        if not self.topic_history:
            return None, 0.0

        current_fingerprint = self.get_topic_fingerprint(current_segment)
        max_similarity = 0.0
        best_match_idx = None

        for idx, (topic_fingerprint, _, _, _, _) in enumerate(self.topic_history):
            if len(topic_fingerprint) != len(current_fingerprint):
                continue
                
            similarity = cosine_similarity([topic_fingerprint], [current_fingerprint])[0][0]
            if similarity > max_similarity:
                max_similarity = similarity
                best_match_idx = idx

        return best_match_idx, max_similarity
    
    def extract_keywords(self, sentences, top_n=3):
        if isinstance(sentences, list):
            text = ' '.join(sentences)
        else:
            text = sentences
        
        # If KeyBERT was not properly initialized, use TF-IDF as a fallback
        if self.keyword_extractor is None:
            try:
                # Use scikit-learn's TF-IDF for keyword extraction as fallback
                from sklearn.feature_extraction.text import TfidfVectorizer
                
                # Create a temporary vectorizer for this text only
                temp_vectorizer = TfidfVectorizer(
                    max_features=50,
                    stop_words='english',
                    ngram_range=(1, 2)
                )
                
                # Fit and transform on this specific text
                tfidf_matrix = temp_vectorizer.fit_transform([text])
                feature_names = temp_vectorizer.get_feature_names_out()
                
                # Get top keywords based on TF-IDF scores
                tfidf_scores = tfidf_matrix.toarray()[0]
                scored_tokens = [(feature_names[i], tfidf_scores[i]) for i in range(len(feature_names))]
                sorted_tokens = sorted(scored_tokens, key=lambda x: x[1], reverse=True)
                
                return [token for token, score in sorted_tokens[:top_n]]
            except Exception as e:
                import logging
                logging.error(f"Error in TF-IDF fallback keyword extraction: {str(e)}")
                # If everything fails, just return some generic keywords
                return ["topic", "section", "content"]
        
        try:
            keywords = self.keyword_extractor.extract_keywords(
                text, 
                keyphrase_ngram_range=(1, 2),  # Allow for 1-2 word keyphrases
                stop_words='english',
                top_n=top_n
            )
            
            sorted_keywords = sorted(keywords, key=lambda x: x[1], reverse=True)
            return [kw[0] for kw in sorted_keywords[:top_n]]
        except Exception as e:
            import logging
            logging.error(f"Error in KeyBERT keyword extraction: {str(e)}")
            # Fall back to TF-IDF method
            return self.extract_keywords(text, top_n)

    def detect_speaker_changes(self, text):
        """
        Detect points in the transcript where speakers change.
        Returns a list of indices where speaker changes occur.
        """
        speaker_pattern = re.compile(r'(?:^|\s)(?:I\'m|And I\'m|Hey I\'m|I am)\s+([A-Z][a-z]+)', re.MULTILINE)
        matches = list(speaker_pattern.finditer(text))
        
        speaker_boundaries = []
        for match in matches:
            # Find the sentence containing this speaker introduction
            sentences = sent_tokenize(text[:match.end()])
            if sentences:
                speaker_boundaries.append(len(sentences) - 1)
        
        return speaker_boundaries

    def find_closest_timestamp(self, sentence, transcript):
        """
        Find the timestamp after the most recent timestamp before the sentence.
        """
        chunks = re.findall(r'\[(\d+)\](.*?)(?=\[\d+\]|$)', transcript, re.DOTALL)
        sentence = sentence.strip().lower()
        
        previous_timestamp = 0
        current_timestamp = 0
        
        for i, (timestamp, text) in enumerate(chunks):
            current_timestamp = int(timestamp)
            text = text.strip().lower()
            
            if sentence in text or text in sentence:
                if i > 0 and i < len(chunks):
                    return current_timestamp
                return previous_timestamp
            
            previous_timestamp = current_timestamp
                
        return current_timestamp 

    def calculate_topic_importance(self, segment, total_segments):
        """
        Calculate importance score for a topic based on:
        1. Length of the segment
        2. Position in the transcript
        3. Keyword significance
        """
        # Length factor - longer segments likely more important
        length_score = min(1.0, len(segment) / 10)  # Cap at 1.0 for segments of 10+ sentences
        
        # Position factor - beginning and end often contain important information
        position_in_doc = self.topic_history.index(segment) / total_segments
        position_score = 1.0 - min(abs(position_in_doc - 0.0), abs(position_in_doc - 1.0))
        
        # Keyword significance - look for key terms indicating importance
        importance_indicators = ['important', 'key', 'main', 'primary', 'critical', 'essential', 
                                'significant', 'revolutionary', 'breakthrough', 'innovative']
        
        text = ' '.join(segment)
        significance_score = 0.0
        for indicator in importance_indicators:
            if indicator in text.lower():
                significance_score += 0.2  # Add 0.2 for each indicator found
        significance_score = min(1.0, significance_score)  # Cap at 1.0
        
        # Combined score, weighted
        combined_score = (0.4 * length_score) + (0.3 * position_score) + (0.3 * significance_score)
        return combined_score
    
    def cluster_topics(self, segments):
        """
        Cluster similar topics together to form a hierarchy
        """
        if len(segments) <= 1:
            return segments, []
            
        # Extract features for each segment
        feature_vectors = []
        for segment_idx, segment in enumerate(segments):
            feature_vectors.append(self.get_topic_fingerprint(segment))
            
        feature_matrix = np.array(feature_vectors)
        
        # Use hierarchical clustering with compatible parameters
        num_clusters = min(self.max_topics, len(segments))
        
        # Try different parameter combinations based on scikit-learn version compatibility
        try:
            # First attempt with basic parameters
            clustering = AgglomerativeClustering(
                n_clusters=num_clusters
            )
            labels = clustering.fit_predict(feature_matrix)
        except Exception as e:
            print(f"Clustering with default parameters failed: {str(e)}")
            # Fallback to manual clustering based on similarity
            labels = self.manual_clustering(feature_matrix, num_clusters)
        
        # Group segments by cluster
        clusters = defaultdict(list)
        for segment_idx, cluster_id in enumerate(labels):
            clusters[cluster_id].append(segment_idx)
            
        # Create parent-child relationships
        parent_child_map = {}
        
        for cluster_id, segment_indices in clusters.items():
            # If cluster has multiple segments, create a parent topic
            if len(segment_indices) > 1:
                # Combine segments to create a parent topic
                all_content = []
                for idx in segment_indices:
                    all_content.extend(segments[idx])
                
                # Extract the most representative keywords
                keywords = self.extract_keywords(all_content, top_n=3)
                parent_topic = f"{', '.join(keywords)}"
                
                # Add parent-child relationships
                for idx in segment_indices:
                    parent_child_map[idx] = parent_topic
        
        return labels, parent_child_map
    
    def manual_clustering(self, feature_matrix, num_clusters):
        """
        Implement a simple similarity-based clustering as fallback
        """
        n_samples = feature_matrix.shape[0]
        
        # If we have very few samples, each gets its own cluster
        if n_samples <= num_clusters:
            return np.arange(n_samples)
            
        # Calculate similarity matrix
        similarity = cosine_similarity(feature_matrix)
        
        labels = np.arange(n_samples)
        current_num_clusters = n_samples
        
        while current_num_clusters > num_clusters:
            max_similarity = -1
            merge_i, merge_j = 0, 0         
            
            for i in range(n_samples):
                for j in range(i+1, n_samples):

                    if labels[i] == labels[j]:
                        continue
                        
                    if similarity[i, j] > max_similarity:
                        max_similarity = similarity[i, j]
                        merge_i, merge_j = i, j
            
            old_label = labels[merge_j]
            new_label = labels[merge_i]
            
            for i in range(n_samples):
                if labels[i] == old_label:
                    labels[i] = new_label
            
            unique_labels = np.unique(labels)
            mapping = {old: new for new, old in enumerate(unique_labels)}
            labels = np.array([mapping[label] for label in labels])
            
            current_num_clusters -= 1
            
        return labels
    
    def segment_transcript(self, text):
        cleaned_sentences, original_sentences, timestamp_mapping = self.preprocess_text(text)
        
        if len(cleaned_sentences) == 0:
            return [], [], [], {}
        
        similarity_matrix = self.calculate_similarity_matrix(cleaned_sentences)
        
        content_boundaries = self.detect_topic_boundaries(similarity_matrix)
        
        speaker_boundaries = self.detect_speaker_changes(text)
        
        timestamp_boundaries = []
        for i in range(1, len(timestamp_mapping)):
            if timestamp_mapping[i] - timestamp_mapping[i-1] > 5:  # If there's a gap of more than 5 seconds
                timestamp_boundaries.append(i)
        
        all_boundaries = sorted(set(content_boundaries + speaker_boundaries + timestamp_boundaries))
        
        initial_segments = []
        initial_segment_texts = []
        current_topic_id = 0
        self.topic_history = []  # Reset topic history
        
        start_idx = 0
        for boundary in all_boundaries + [len(original_sentences)]:
            if boundary - start_idx < self.min_segment_size:
                continue
                
            current_segment = original_sentences[start_idx:boundary]
            segment_timestamps = timestamp_mapping[start_idx:boundary]
            
            closest_timestamp = segment_timestamps[0] if segment_timestamps else 0
            
            # Extract keywords directly from content without predefined categories
            keywords = self.extract_keywords(current_segment, top_n=4)
            
            # Create topic name directly from the top keywords - completely dynamic
            topic_name = f"{', '.join(keywords[:2])}"
            
            # Calculate importance score
            topic_importance = len(current_segment) / 3  # Simple approach: longer segments are more important
            
            # Create a topic fingerprint
            topic_fingerprint = self.get_topic_fingerprint(current_segment)
            
            # Store topic information
            self.topic_history.append((topic_fingerprint, topic_name, [current_segment], closest_timestamp, topic_importance))
            initial_segments.append(current_segment)
            initial_segment_texts.append(topic_name)
            
            start_idx = boundary
        
        # Cluster segments to form hierarchical topics
        if len(initial_segments) > 1:
            cluster_labels, parent_child_map = self.cluster_topics(initial_segments)
        else:
            cluster_labels = [0] if initial_segments else []
            parent_child_map = {}
        
        # Organize segments into hierarchical structure
        final_segments = []
        topic_mappings = []
        topic_hierarchies = {}
        
        # Group by parent topics
        parent_topic_groups = defaultdict(list)
        
        for i, segment in enumerate(initial_segments):
            topic_id = i
            
            # Check if this segment belongs to a parent topic
            if i in parent_child_map:
                parent_topic = parent_child_map[i]
                parent_id = len(self.topic_history)
                
                # If this is the first time seeing this parent topic
                if parent_topic not in parent_topic_groups:
                    # Add the parent topic to our history
                    all_content = []
                    for j, seg in enumerate(initial_segments):
                        if j in parent_child_map and parent_child_map[j] == parent_topic:
                            all_content.extend(seg)
                    
                    parent_fingerprint = self.get_topic_fingerprint(all_content)
                    parent_timestamp = self.topic_history[i][3]  # Use timestamp of first segment
                    parent_importance = 1.0  # Parent topics are most important
                    
                    self.topic_history.append((parent_fingerprint, parent_topic, [all_content], parent_timestamp, parent_importance))
                    topic_hierarchies[parent_id] = [j for j, seg in enumerate(initial_segments) 
                                               if j in parent_child_map and parent_child_map[j] == parent_topic]
                
                # Find the parent ID
                for j, (_, name, _, _, _) in enumerate(self.topic_history):
                    if name == parent_topic:
                        parent_id = j
                        break
                
                # Add to parent topic group
                parent_topic_groups[parent_topic].append((segment, topic_id))
            else:
                # This is already a main topic
                final_segments.append(segment)
                topic_mappings.append(topic_id)
        
        # Add grouped segments under their parent topics
        for parent_topic, segments_and_ids in parent_topic_groups.items():
            # Find the parent ID
            parent_id = None
            for i, (_, name, _, _, _) in enumerate(self.topic_history):
                if name == parent_topic:
                    parent_id = i
                    break
            
            if parent_id is not None:
                # Add parent as a segment
                parent_content = []
                for segment, _ in segments_and_ids:
                    parent_content.extend(segment)
                
                final_segments.append(parent_content)
                topic_mappings.append(parent_id)
                
                # Add children segments
                for segment, child_id in segments_and_ids:
                    final_segments.append(segment)
                    topic_mappings.append(child_id)
        
        # For topics that don't have many segments, merge them if they're similar
        if len(final_segments) > self.max_topics * 2:
            merged_segments = []
            merged_mappings = []
            i = 0
            
            while i < len(final_segments):
                current_segment = final_segments[i]
                current_mapping = topic_mappings[i]
                
                # If this is a short segment, try to merge with next
                if len(current_segment) <= 2 and i + 1 < len(final_segments):
                    next_segment = final_segments[i+1]
                    combined = current_segment + next_segment
                    
                    # Use keywords to see if they're related
                    current_keywords = self.extract_keywords(current_segment)
                    next_keywords = self.extract_keywords(next_segment)
                    
                    # Check for keyword overlap
                    overlap = any(kw in next_keywords for kw in current_keywords)
                    
                    if overlap:
                        # Merge the segments
                        merged_segments.append(combined)
                        merged_mappings.append(current_mapping)  # Keep the first mapping
                        i += 2  # Skip both segments
                        continue
                
                # If no merge, keep as is
                merged_segments.append(current_segment)
                merged_mappings.append(current_mapping)
                i += 1
            
            final_segments = merged_segments
            topic_mappings = merged_mappings
            
        return final_segments, topic_mappings, self.topic_history, topic_hierarchies

    def calculate_similarity_matrix(self, sentences):
        if not sentences:
            return np.array([[]])
        tfidf_matrix = self.vectorizer.fit_transform(sentences)
        return cosine_similarity(tfidf_matrix)

    def detect_topic_boundaries(self, similarity_matrix):
        boundaries = []
        n_sentences = len(similarity_matrix)
        
        if n_sentences <= 2 * self.window_size:
            return []
        
        for i in range(self.window_size, n_sentences - self.window_size):
            prev_window = similarity_matrix[i-self.window_size:i, i-self.window_size:i]
            prev_similarity = np.mean(prev_window)
            
            next_window = similarity_matrix[i:i+self.window_size, i:i+self.window_size]
            next_similarity = np.mean(next_window)
            
            cross_window = similarity_matrix[i-self.window_size:i, i:i+self.window_size]
            cross_similarity = np.mean(cross_window)
            
            # Stricter conditions for detecting boundaries
            if (cross_similarity < self.similarity_threshold and
                cross_similarity < prev_similarity * 0.7 and  # Increased constraint
                cross_similarity < next_similarity * 0.7 and  # Increased constraint
                (len(boundaries) == 0 or i - boundaries[-1] >= self.min_segment_size)):
                boundaries.append(i)
        
        return boundaries
    
if __name__ == "__main__":
    transcript = """
[0] Hey I'm Stanley, I'm a Stanford CS major. I did fun at Engineering at Facebook. [4] Hey I'm Andy, I'm also a Stanford CS major and I did platform engineering at Facebook. [8] I'm Evan, I was on the founding team of Vivo, the music video service. [12] And I'm Tony, I was a product editor at Square. [15] And the four of us came together about six months ago to work on software for small business owners. [20] But we didn't have a need at first so we just went out and talked to all the small business owners we could find. [24] [6] After over a hundred interviews, we came across a really interesting problem with small business restaurants in an area like this. [30] Yeah, so it turns out restaurants in Palo Alto don't deliver even though they really [34] want to, but they can't afford it. [36] But their consumers are craving for it, but the places that the consumers love just can't [41] deliver. [42] And we also found out about these delivery drivers who had a ton of spare time and they [45] all wanted an extra cash during that downtime. [48] Right, so that's when we built an initial product, PaloAltodelivery.com, and how it [53] worked is the customer goes to the website and places it all in order that gets automatically [59] [32] since the restaurant [60] And then we as the dispatchers with some pretty neat routing and badging algorithms were able to send the drivers efficiently to [66] Get the orders to the customers at a really fast time. Yeah, and the four of us actually started off as delivery drivers and [74] Over time we hired more as we grew and in our first month of launch with not much marketing [80] We got over 150 paying customers in the Palo Alto area, which was really awesome [84] And from that we generated over $10,000 in sales
    """
    
    segmenter = TopicSegmenter(
        window_size=2,  # Smaller window for more granular segmentation
        similarity_threshold=0.15,  # Lower threshold to detect more subtle changes
        context_size=1,
        min_segment_size=2,  # Smaller minimum size for segments
        topic_similarity_threshold=0.25,  # Lower threshold to better distinguish topics
        max_topics=5,  # Target number of main topics
        hierarchical_threshold=0.6  # Threshold for hierarchical clustering
    )
    
    segments, topic_mappings, topic_history, topic_hierarchies = segmenter.segment_transcript(transcript)
    
    print("Topic Segmentation Analysis:\n")
    print(f"Found {len(set(topic_mappings))} distinct topics")
    
    # Print hierarchical topic structure
    print("\nTopic Hierarchy:")
    for parent_id, child_ids in topic_hierarchies.items():
        parent_name = topic_history[parent_id][1]
        print(f"Main Topic: {parent_name}")
        for child_id in child_ids:
            child_name = topic_history[child_id][1]
            print(f"  - Subtopic: {child_name}")
    
    print("\nDetailed Segments:")
    for i, (segment, topic_id) in enumerate(zip(segments, topic_mappings)):
        print(f"Segment {i+1} (Part of {topic_history[topic_id][1]}):")
        print(f"Closest Timestamp: {topic_history[topic_id][3]} seconds")
        print("-" * 50)
        print("\n".join(segment[:2]) + "..." if len(segment) > 2 else "\n".join(segment))
        print("-" * 50 + "\n")