diff options
| author | vin <git@vineetk.net> | 2025-08-23 01:33:21 -0400 |
|---|---|---|
| committer | vin <git@vineetk.net> | 2025-08-23 10:58:54 -0400 |
| commit | 78decc2ddd05519780153a8d09f68884e8d11fc6 (patch) | |
| tree | 049dce292cb72cbcdda5e923d2833f9568c5ea58 | |
| parent | 3b4865a270888643446d9092ba55d69b79117e5e (diff) | |
add highlight/visualizer script and update system prompt
| -rw-r--r-- | highlight.py | 118 | ||||
| -rw-r--r-- | main.py | 67 |
2 files changed, 155 insertions, 30 deletions
diff --git a/highlight.py b/highlight.py new file mode 100644 index 0000000..361a0d2 --- /dev/null +++ b/highlight.py | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | import sys | ||
| 2 | import json | ||
| 3 | import re | ||
| 4 | from datetime import timedelta | ||
| 5 | |||
| 6 | # ANSI escape codes for terminal colors | ||
| 7 | class Colors: | ||
| 8 | GREEN = '\033[92m' # Highlight for valid ad text | ||
| 9 | RED = '\033[91m' # Highlight for invalid target ranges | ||
| 10 | BLUE = '\033[94m' # Header color | ||
| 11 | RESET = '\033[0m' # Reset to default color | ||
| 12 | |||
| 13 | def timestamp_to_seconds(ts_str: str) -> float: | ||
| 14 | """Converts HH:MM:SS.mmm string to total seconds. Returns -1.0 on failure.""" | ||
| 15 | try: | ||
| 16 | parts = ts_str.split(':') | ||
| 17 | h, m = int(parts[0]), int(parts[1]) | ||
| 18 | s_ms = parts[2].split('.') | ||
| 19 | s, ms = int(s_ms[0]), int(s_ms[1]) | ||
| 20 | return h * 3600 + m * 60 + s + ms / 1000.0 | ||
| 21 | except (ValueError, IndexError): | ||
| 22 | return -1.0 | ||
| 23 | |||
| 24 | def visualize_entry(entry: dict): | ||
| 25 | """Parses and prints a single entry from the jsonl file with highlighting.""" | ||
| 26 | entry_id = entry.get("id", "N/A") | ||
| 27 | full_text = entry.get("text", "") | ||
| 28 | target_str = entry.get("target", "") | ||
| 29 | |||
| 30 | print(f"{Colors.BLUE}{'='*80}\nID: {entry_id}{Colors.RESET}") | ||
| 31 | |||
| 32 | # --- 1. Parse the transcript from the 'text' field --- | ||
| 33 | transcript_lines = [] | ||
| 34 | # Regex to find the start of the actual transcript text | ||
| 35 | transcript_match = re.search(r"Transcript:\n(.*)", full_text, re.DOTALL) | ||
| 36 | if not transcript_match: | ||
| 37 | print(f"{Colors.RED}Could not find transcript text in entry.{Colors.RESET}") | ||
| 38 | return | ||
| 39 | |||
| 40 | raw_transcript = transcript_match.group(1).strip() | ||
| 41 | line_pattern = re.compile(r"\[(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})\](.*)") | ||
| 42 | |||
| 43 | min_transcript_time, max_transcript_time = float('inf'), float('-inf') | ||
| 44 | |||
| 45 | for line in raw_transcript.split('\n'): | ||
| 46 | match = line_pattern.match(line) | ||
| 47 | if match: | ||
| 48 | start_str, end_str, text = match.groups() | ||
| 49 | start_sec = timestamp_to_seconds(start_str) | ||
| 50 | end_sec = timestamp_to_seconds(end_str) | ||
| 51 | transcript_lines.append({"start": start_sec, "end": end_sec, "line": line}) | ||
| 52 | min_transcript_time = min(min_transcript_time, start_sec) | ||
| 53 | max_transcript_time = max(max_transcript_time, end_sec) | ||
| 54 | |||
| 55 | # --- 2. Parse and validate the target ranges --- | ||
| 56 | valid_target_ranges = [] | ||
| 57 | has_invalid_target = False | ||
| 58 | |||
| 59 | if target_str: | ||
| 60 | for target_line in target_str.strip().split('\n'): | ||
| 61 | if '-' not in target_line: | ||
| 62 | has_invalid_target = True | ||
| 63 | continue | ||
| 64 | |||
| 65 | start_target_str, end_target_str = target_line.split('-', 1) | ||
| 66 | start_target_sec = timestamp_to_seconds(start_target_str.strip()) | ||
| 67 | end_target_sec = timestamp_to_seconds(end_target_str.strip()) | ||
| 68 | |||
| 69 | # Validation checks | ||
| 70 | is_valid_format = start_target_sec != -1.0 and end_target_sec != -1.0 | ||
| 71 | is_valid_range = is_valid_format and start_target_sec < end_target_sec | ||
| 72 | # Check if the target overlaps with the transcript's time frame at all | ||
| 73 | is_overlapping = is_valid_range and start_target_sec < max_transcript_time and end_target_sec > min_transcript_time | ||
| 74 | |||
| 75 | if is_overlapping: | ||
| 76 | valid_target_ranges.append({"start": start_target_sec, "end": end_target_sec}) | ||
| 77 | else: | ||
| 78 | has_invalid_target = True | ||
| 79 | |||
| 80 | # --- 3. Print results with highlighting --- | ||
| 81 | print(f"TARGET: ", end="") | ||
| 82 | if has_invalid_target: | ||
| 83 | print(f"{Colors.RED}{target_str or 'EMPTY'}{Colors.RESET}") | ||
| 84 | else: | ||
| 85 | print(f"{Colors.GREEN}{target_str or 'EMPTY'}{Colors.RESET}") | ||
| 86 | print("-" * 30) | ||
| 87 | |||
| 88 | for line_info in transcript_lines: | ||
| 89 | is_ad_line = False | ||
| 90 | for target_range in valid_target_ranges: | ||
| 91 | # Check for overlap between the line's time and the target's time | ||
| 92 | if line_info['start'] < target_range['end'] and line_info['end'] > target_range['start']: | ||
| 93 | is_ad_line = True | ||
| 94 | break | ||
| 95 | |||
| 96 | if is_ad_line: | ||
| 97 | print(f"{Colors.GREEN}{line_info['line']}{Colors.RESET}") | ||
| 98 | else: | ||
| 99 | print(line_info['line']) | ||
| 100 | |||
| 101 | def main(filepath: str): | ||
| 102 | try: | ||
| 103 | with open(filepath, 'r', encoding='utf-8') as f: | ||
| 104 | for line in f: | ||
| 105 | try: | ||
| 106 | entry = json.loads(line) | ||
| 107 | visualize_entry(entry) | ||
| 108 | except json.JSONDecodeError: | ||
| 109 | print(f"{Colors.RED}Error: Could not decode JSON line: {line.strip()}{Colors.RESET}") | ||
| 110 | except FileNotFoundError: | ||
| 111 | print(f"{Colors.RED}Error: File not found at {filepath}{Colors.RESET}") | ||
| 112 | |||
| 113 | if __name__ == "__main__": | ||
| 114 | if len(sys.argv) < 2: | ||
| 115 | print("Usage: python highlight.py <path_to_labels.jsonl>") | ||
| 116 | sys.exit(1) | ||
| 117 | |||
| 118 | main(sys.argv[1]) | ||
| @@ -13,12 +13,8 @@ client = openai.OpenAI( | |||
| 13 | api_key=os.getenv("OPENROUTER_API_KEY") | 13 | api_key=os.getenv("OPENROUTER_API_KEY") |
| 14 | ) | 14 | ) |
| 15 | 15 | ||
| 16 | #MODEL = "deepseek/deepseek-r1-0528:free" | 16 | # smaller models than this (besides dense llama 3.1 405b) really struggle |
| 17 | #MODEL = "deepseek/deepseek-chat-v3-0324" # good with some minor editing of output ranges | 17 | MODEL = "deepseek/deepseek-chat-v3.1" |
| 18 | MODEL = "deepseek/deepseek-chat-v3-0324:free" # twice as slow as paid | ||
| 19 | #MODEL = "google/gemini-2.5-flash-lite-preview-06-17" # too many false-positives | ||
| 20 | #MODEL = "google/gemini-2.0-flash-exp:free" # fine | ||
| 21 | #MODEL = "qwen/qwen3-30b-a3b" # sort of works, but slow | ||
| 22 | 18 | ||
| 23 | SYSTEM_PROMPT = """ | 19 | SYSTEM_PROMPT = """ |
| 24 | You are an expert podcast content analyzer. Your task is to identify and extract pre-recorded dynamic advertising segments from podcast transcripts. These ads typically have a distinct tone shift, often using more direct, persuasive language, and frequently include calls to action, product mentions, or specific brand names. | 20 | You are an expert podcast content analyzer. Your task is to identify and extract pre-recorded dynamic advertising segments from podcast transcripts. These ads typically have a distinct tone shift, often using more direct, persuasive language, and frequently include calls to action, product mentions, or specific brand names. |
| @@ -29,6 +25,7 @@ You will be provided with segments of a podcast transcript, formatted with times | |||
| 29 | - Only identify segments that are clearly pre-recorded dynamic ads with a noticeable tone shift. | 25 | - Only identify segments that are clearly pre-recorded dynamic ads with a noticeable tone shift. |
| 30 | - Do NOT include host-read sponsorships that blend naturally with the content. Focus only on the "distracting" pre-recorded elements. | 26 | - Do NOT include host-read sponsorships that blend naturally with the content. Focus only on the "distracting" pre-recorded elements. |
| 31 | - The end of an ad segment is marked by the return to the original podcast discussion, a clear transition to new content, or the end of the ad's persuasive language and calls to action. | 27 | - The end of an ad segment is marked by the return to the original podcast discussion, a clear transition to new content, or the end of the ad's persuasive language and calls to action. |
| 28 | - A segment should only be classified as an ad if it contains an abrupt topic change AND specific marketing language (e.g., product names, calls to action, special offers). A topic change alone is NOT an ad. | ||
| 32 | - Ensure the start and end times correspond precisely to the ad segment in the transcript. | 29 | - Ensure the start and end times correspond precisely to the ad segment in the transcript. |
| 33 | - Output ONLY the timestamp range(s) in the specified format. No other text, no JSON, no explanations. | 30 | - Output ONLY the timestamp range(s) in the specified format. No other text, no JSON, no explanations. |
| 34 | - If no ad segments are found, your output MUST be completely empty. Generating any text, including explanations like '[No output...]', is a failure to follow instructions. The only valid output in this case is a blank string. | 31 | - If no ad segments are found, your output MUST be completely empty. Generating any text, including explanations like '[No output...]', is a failure to follow instructions. The only valid output in this case is a blank string. |
| @@ -107,8 +104,7 @@ You will be provided with segments of a podcast transcript, formatted with times | |||
| 107 | **Example 4 Input:** | 104 | **Example 4 Input:** |
| 108 | [01:46:23.940 --> 01:46:32.220] It has so rapidly become one of the most critical apps on my whole system, I cannot even believe it. | 105 | [01:46:23.940 --> 01:46:32.220] It has so rapidly become one of the most critical apps on my whole system, I cannot even believe it. |
| 109 | [01:46:32.220 --> 01:46:36.320] So if you don't use shortcuts, this is completely worthless to you. | 106 | [01:46:32.220 --> 01:46:36.320] So if you don't use shortcuts, this is completely worthless to you. |
| 110 | [01:46:36.320 --> 01:46:54.380] But if you make even mildly complicated shortcuts, you have to be using Logger and it will just completely unlock your ability to be able to make much more complicated shortcuts because you can always know exac | 107 | [01:46:36.320 --> 01:46:54.380] But if you make even mildly complicated shortcuts, you have to be using Logger and it will just completely unlock your ability to be able to make much more complicated shortcuts because you can always know exactly what's happening and exactly where things are going wrong. |
| 111 | tly what's happening and exactly where things are going wrong. | ||
| 112 | [01:46:54.380 --> 01:46:57.460] So Logger is my app of the year. | 108 | [01:46:54.380 --> 01:46:57.460] So Logger is my app of the year. |
| 113 | [01:46:59.320 --> 01:47:02.180] This episode of Cortex is brought to you by FitBod. | 109 | [01:46:59.320 --> 01:47:02.180] This episode of Cortex is brought to you by FitBod. |
| 114 | [01:47:02.180 --> 01:47:06.040] When you're looking to change your fitness level, it's hard to know where to get started. | 110 | [01:47:02.180 --> 01:47:06.040] When you're looking to change your fitness level, it's hard to know where to get started. |
| @@ -120,26 +116,25 @@ tly what's happening and exactly where things are going wrong. | |||
| 120 | [01:47:40.940 --> 01:47:44.500] And it builds your best possible workout with the use of exercise science. | 116 | [01:47:40.940 --> 01:47:44.500] And it builds your best possible workout with the use of exercise science. |
| 121 | [01:47:44.500 --> 01:47:50.260] They have analyzed billions of data points that have been fine-tuned by FitBod certified personal trainers. | 117 | [01:47:44.500 --> 01:47:50.260] They have analyzed billions of data points that have been fine-tuned by FitBod certified personal trainers. |
| 122 | [01:47:50.260 --> 01:47:57.180] And you can make sure you're going to be learning new movements the right way thanks to the more than 1,000 demonstration videos that are all in the FitBod app. | 118 | [01:47:50.260 --> 01:47:57.180] And you can make sure you're going to be learning new movements the right way thanks to the more than 1,000 demonstration videos that are all in the FitBod app. |
| 123 | [01:47:57.180 --> 01:48:07.380] This is my favorite feature of FitBod that when I get shown a new exercise, I have a perfect way to be able to understand how to do it because of all the videos that they can show me from different angles along | 119 | [01:47:57.180 --> 01:48:07.380] This is my favorite feature of FitBod that when I get shown a new exercise, I have a perfect way to be able to understand how to do it because of all the videos that they can show me from different angles along with the instructions. |
| 124 | with the instructions. | 120 | [01:48:07.380 --> 01:48:10.100] And it makes me feel confident about the work that I'm going to be doing. |
| 125 | [01:48:07.380 --> 01:48:10.100] And it makes me feel confident about the work that I'm going to be doing. | 121 | [01:48:10.100 --> 01:48:14.340] Muscles improve when they're working in concert with the entire musculoskeletal system. |
| 126 | [01:48:10.100 --> 01:48:14.340] Muscles improve when they're working in concert with the entire musculoskeletal system. | 122 | [01:48:14.340 --> 01:48:19.360] So overworking some muscles while underworking others can negatively impact your overall results. |
| 127 | [01:48:14.340 --> 01:48:19.360] So overworking some muscles while underworking others can negatively impact your overall results. | 123 | [01:48:19.360 --> 01:48:24.280] This is why FitBod tracks muscle fatigue and recovery to design a well-balanced workout routine, |
| 128 | [01:48:19.360 --> 01:48:24.280] This is why FitBod tracks muscle fatigue and recovery to design a well-balanced workout routine, | 124 | [01:48:24.280 --> 01:48:30.960] which also means you'll never get bored as the app mixes up your workouts with new exercises, rep schemes, supersets, and circuits. |
| 129 | [01:48:24.280 --> 01:48:30.960] which also means you'll never get bored as the app mixes up your workouts with new exercises, rep schemes, supersets, and circuits. | 125 | [01:48:30.960 --> 01:48:37.760] The FitBod app is super easy to use and it integrates with your Apple Watch, WearOS smartwatch, and apps like Strava, Fitbit, and Apple Health. |
| 130 | [01:48:30.960 --> 01:48:37.760] The FitBod app is super easy to use and it integrates with your Apple Watch, WearOS smartwatch, and apps like Strava, Fitbit, and Apple Health. | 126 | [01:48:37.760 --> 01:48:44.020] You've already heard in this episode why Gray loves FitBod, and Gray Loving FitBod is the reason why they became a sponsor of the show. |
| 131 | [01:48:37.760 --> 01:48:44.020] You've already heard in this episode why Gray loves FitBod, and Gray Loving FitBod is the reason why they became a sponsor of the show. | 127 | [01:48:44.020 --> 01:48:45.960] So you should go and check it out for yourself. |
| 132 | [01:48:44.020 --> 01:48:45.960] So you should go and check it out for yourself. | 128 | [01:48:45.960 --> 01:48:51.740] You'll be able to benefit from all of the incredible work from FitBod to help you in your fitness journey. |
| 133 | [01:48:45.960 --> 01:48:51.740] You'll be able to benefit from all of the incredible work from FitBod to help you in your fitness journey. | 129 | [01:48:51.740 --> 01:48:58.440] Personalized training of this quality can be expensive, but FitBod is just $12.99 a month or $79.99 a year. |
| 134 | [01:48:51.740 --> 01:48:58.440] Personalized training of this quality can be expensive, but FitBod is just $12.99 a month or $79.99 a year. | 130 | [01:48:58.440 --> 01:49:04.380] But you can get 25% off your membership by signing up today at FitBod.me slash Cortex. |
| 135 | [01:48:58.440 --> 01:49:04.380] But you can get 25% off your membership by signing up today at FitBod.me slash Cortex. | 131 | [01:49:04.380 --> 01:49:09.840] So go now and get your customized fitness plan at FitBod.me slash Cortex. |
| 136 | [01:49:04.380 --> 01:49:09.840] So go now and get your customized fitness plan at FitBod.me slash Cortex. | 132 | [01:49:09.980 --> 01:49:14.180] Once again, that is FitBod.me slash Cortex, and you'll get 25% off your membership. |
| 137 | [01:49:09.980 --> 01:49:14.180] Once again, that is FitBod.me slash Cortex, and you'll get 25% off your membership. | 133 | [01:49:14.180 --> 01:49:17.980] A thanks to FitBod for their continued support of this show and Relay. |
| 138 | [01:49:14.180 --> 01:49:17.980] A thanks to FitBod for their continued support of this show and Relay. | 134 | [01:49:17.980 --> 01:49:20.120] We've made it to home screens. |
| 139 | [01:49:17.980 --> 01:49:20.120] We've made it to home screens. | 135 | [01:49:20.120 --> 01:49:22.360] Oh, home screens. Right, right. |
| 140 | [01:49:20.120 --> 01:49:22.360] Oh, home screens. Right, right. | 136 | [01:49:22.360 --> 01:49:23.820] How complicated can it be? |
| 141 | [01:49:22.360 --> 01:49:23.820] How complicated can it be? | 137 | [01:49:23.920 --> 01:49:29.820] So as like last time, my home screens are broken down into a selection of focus modes too. |
| 142 | [01:49:23.920 --> 01:49:29.820] So as like last time, my home screens are broken down into a selection of focus modes too. | ||
| 143 | 138 | ||
| 144 | **Example 4 Output:** | 139 | **Example 4 Output:** |
| 145 | 01:46:59.320-01:49:17.980 | 140 | 01:46:59.320-01:49:17.980 |
| @@ -253,6 +248,17 @@ tly what's happening and exactly where things are going wrong. | |||
| 253 | 248 | ||
| 254 | **Example 7 Output:** | 249 | **Example 7 Output:** |
| 255 | 00:14:17.500-00:17:05.040 | 250 | 00:14:17.500-00:17:05.040 |
| 251 | |||
| 252 | --- | ||
| 253 | **Example 8 Input:** | ||
| 254 | [00:27:11.039 --> 00:27:14.480] Why aren't stadium seats just toilets? | ||
| 255 | [00:27:14.720 --> 00:27:19.839] Like the Romans of old, like the ancient Greek bathhouse. | ||
| 256 | [00:27:20.480 --> 00:27:29.920] Everything was cool and you could chit sit next to your bro while you talked out some philosophy or watched a little sporting event, maybe a little wrestling. | ||
| 257 | [00:27:30.160 --> 00:27:37.359] Why are we because in the stadium, one of the biggest problems is you oh like, oh, it's between innings, oh, is it's halftime. | ||
| 258 | [00:27:37.440 --> 00:27:38.480] Oh, I gotta go to the bathroom. | ||
| 259 | |||
| 260 | **Example 8 Output:** | ||
| 261 | |||
| 256 | """ | 262 | """ |
| 257 | 263 | ||
| 258 | # --- Helper Functions --- | 264 | # --- Helper Functions --- |
| @@ -293,7 +299,7 @@ def segment_by_punctuation(asr_results: list, min_words_per_segment: int = 3) -> | |||
| 293 | for i, token in enumerate(tokens): | 299 | for i, token in enumerate(tokens): |
| 294 | is_end_of_sentence = any(p in token.strip() for p in punctuations) | 300 | is_end_of_sentence = any(p in token.strip() for p in punctuations) |
| 295 | 301 | ||
| 296 | if is_end_of_sentence or (i == len(tokens) - 1): | 302 | if (is_end_of_sentence or (i == len(tokens) - 1)) and len(tokens) > current_segment_start_idx: |
| 297 | segment_tokens = tokens[current_segment_start_idx : i + 1] | 303 | segment_tokens = tokens[current_segment_start_idx : i + 1] |
| 298 | 304 | ||
| 299 | if len(segment_tokens) >= min_words_per_segment: | 305 | if len(segment_tokens) >= min_words_per_segment: |
| @@ -306,7 +312,8 @@ def segment_by_punctuation(asr_results: list, min_words_per_segment: int = 3) -> | |||
| 306 | "end_time": end_time, | 312 | "end_time": end_time, |
| 307 | "text": text | 313 | "text": text |
| 308 | }) | 314 | }) |
| 309 | current_segment_tokens = [] | 315 | |
| 316 | current_segment_start_idx = i + 1 | ||
| 310 | return segments | 317 | return segments |
| 311 | 318 | ||
| 312 | def chunk_segments(segments: list[dict], chunk_duration_seconds: int = 240, overlap_seconds: int = 60) -> list[list[dict]]: | 319 | def chunk_segments(segments: list[dict], chunk_duration_seconds: int = 240, overlap_seconds: int = 60) -> list[list[dict]]: |
