summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore7
-rw-r--r--format.py274
-rw-r--r--main.py270
3 files changed, 134 insertions, 417 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6ade941
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
1venv
2*~
3.*
4*.mp3
5*.wav
6*.json
7*.jsonl
diff --git a/format.py b/format.py
deleted file mode 100644
index c1acba7..0000000
--- a/format.py
+++ /dev/null
@@ -1,274 +0,0 @@
1import json
2import sys
3from datetime import timedelta
4
5SYSTEM_PROMPT_TEMPLATE = """
6You 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.
7
8You will be provided with segments of a podcast transcript, formatted with timestamps. Your output must be ONLY the start and end timestamp of the identified advertising segment, separated by a hyphen, in the format HH:MM:SS.mmm-HH:MM:SS.mmm. If there are multiple ad segments, output each on a new line. If no ad segments are found, output nothing.
9
10**Strict Rules:**
11- Only identify segments that are clearly pre-recorded dynamic ads with a noticeable tone shift.
12- Do NOT include host-read sponsorships that blend naturally with the content. Focus only on the "distracting" pre-recorded elements.
13- 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.
14- Ensure the start and end times correspond precisely to the ad segment in the transcript.
15- Output ONLY the timestamp range(s) in the specified format. No other text, no JSON, no explanations.
16- 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.
17
18**Examples:**
19
20---
21**Example 1 Input:**
22[00:00:00.000 --> 00:00:04.880] Now, you can watch the hottest movies and series with Hulu on Disney+.
23[00:00:04.880 --> 00:00:08.400] With Hulu, say, yes, chef, to a new season of FX is the Bear.
24[00:00:08.400 --> 00:00:13.000] And don't miss Marvel Television's action-packed new series, Ironheart, on Disney+.
25[00:00:13.000 --> 00:00:17.060] All of these and more now streaming with Hulu on Disney+, with a bundle subscription.
26[00:00:17.060 --> 00:00:18.040] Terms apply.
27[00:00:18.040 --> 00:00:20.980] Visit Disneyplus.com slash Hulu for details.
28[00:00:20.980 --> 00:00:27.440] This episode is brought to you by DoorDash, because the summer of Dash Pass is back.
29[00:00:27.440 --> 00:00:33.420] Check daily from June 26th through July 30th for massive summer savings on restaurants, groceries, and brands.
30[00:00:33.420 --> 00:00:35.640] So sign up for Dash Pass now if you haven't already.
31[00:00:35.640 --> 00:00:37.800] Dash Pass benefits apply only to eligible orders. Terms apply.
32[00:00:37.800 --> 00:00:40.080] This episode is brought to you by Amazon.
33[00:00:40.080 --> 00:00:43.200] This off-the-college season, save on college, save the everyday.
34[00:00:43.200 --> 00:00:48.140] Literally every supply you need for school, like pens and stuff that you just assume you have.
35[00:00:48.140 --> 00:00:49.720] You know, I remember a story.
36[00:00:49.720 --> 00:00:52.980] My roommate and I decided to bunk our beds.
37[00:00:52.980 --> 00:00:56.540] So we just cut up some plastic hangers and just jammed them in there.
38[00:00:56.540 --> 00:00:59.780] We could have really used some metal pins.
39[00:00:59.780 --> 00:01:01.500] Wonder who that roommate could have been.
40[00:01:01.500 --> 00:01:05.900] So remember, with Amazon's low off-the-college prices, save on college, save the everyday.
41[00:01:05.900 --> 00:01:07.820] Shop off-the-college at Amazon.
42[00:01:07.820 --> 00:01:13.400] Good evening, gentle listener, and welcome to Distractable, a Wood Elf production.
43[00:01:13.400 --> 00:01:20.380] And this week, Wade sets a caddish challenge for his fellow incorrigibles, for it shall be a clash of the titters.
44[00:01:20.380 --> 00:01:26.200] Mark and Wade will be tasked with resisting the lure of mirth, and to entice it from their opponent.
45[00:01:26.200 --> 00:01:31.240] Bob derides the last son of Krypton, while Mark extols the virtues of the web-slinger.
46
47**Example 1 Output:**
4800:00:00.000-00:00:20.980
4900:00:20.980-00:00:37.800
5000:00:37.800-00:01:07.820
51
52---
53**Example 2 Input:**
54[00:38:07.480 --> 00:38:10.820] This podcast is brought to you by Carvana.
55[00:38:10.820 --> 00:38:13.660] Got a car to sell, but no time to waste?
56[00:38:13.660 --> 00:38:17.820] Hop on to Carvana.com to get a real offer for your car in seconds.
57[00:38:17.820 --> 00:38:23.060] All you have to do is enter your license plate, answer a few quick questions, and if you accept
58[00:38:23.060 --> 00:38:26.000] the offer, Carvana will pay you as soon as you hand the keys over.
59[00:38:26.360 --> 00:38:29.060] They even offer same day pickup in many cities.
60[00:38:29.060 --> 00:38:35.740] Save your time, score some cash, and sell your car the convenient way to Carvana.
61[00:38:36.360 --> 00:38:37.000] Pickup times vary.
62[00:38:37.000 --> 00:38:37.620] Fees may apply.
63
64**Example 2 Output:**
6500:38:07.480-00:38:37.620
66
67---
68**Example 3 Input:**
69[00:03:11.060 --> 00:03:12.720] I'm always very happy to do this.
70[00:03:12.720 --> 00:03:14.800] High on the list of things that you want to do.
71[00:03:14.800 --> 00:03:16.260] High on the list of things that I want to do.
72[00:03:16.620 --> 00:03:20.260] Low on the list is recording after having just arrived home.
73[00:03:20.260 --> 00:03:21.260] I do this.
74[00:03:21.260 --> 00:03:22.360] I do this every now and then.
75[00:03:22.360 --> 00:03:25.720] I will arrive home from a trip, and I will record.
76[00:03:25.720 --> 00:03:29.280] And it is a very disorienting experience.
77[00:03:29.280 --> 00:03:32.960] As I warned you at some point during the episode today,
78[00:03:32.960 --> 00:03:34.460] you're likely to get a little bit loopy,
79[00:03:34.460 --> 00:03:35.940] because that's what happens to me.
80[00:03:35.940 --> 00:03:37.260] Yeah.
81[00:03:37.260 --> 00:03:41.900] I mean, we're recording today because there's literally no other day
82[00:03:41.900 --> 00:03:45.400] that could work for us for doing the show.
83[00:03:45.400 --> 00:03:49.040] We moved it, like, twice already, and here we are.
84
85**Example 3 Output:**
86
87
88---
89**Example 4 Input:**
90[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.
91[01:46:32.220 --> 01:46:36.320] So if you don't use shortcuts, this is completely worthless to you.
92[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
93tly what's happening and exactly where things are going wrong.
94[01:46:54.380 --> 01:46:57.460] So Logger is my app of the year.
95[01:46:59.320 --> 01:47:02.180] This episode of Cortex is brought to you by FitBod.
96[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.
97[01:47:06.040 --> 01:47:12.400] And that is why I want to let you know that FitBod is both an easy and affordable way to build a fitness plan just for you.
98[01:47:12.400 --> 01:47:15.580] Everybody has their own path when it comes to personal fitness.
99[01:47:15.580 --> 01:47:25.520] That's why FitBod uses data to make sure they customize things to suit you perfectly, adapting as you improve so every workout will be challenging, pushing you to make the progress that you want.
100[01:47:26.080 --> 01:47:33.180] You'll see superior results when a workout program is tailored to you exactly, matching your body, your experience, your environment, and goals.
101[01:47:33.180 --> 01:47:40.940] FitBod stores this information in your FitBod gym profile where it tracks your muscle recovery, making sure that you avoid burnout and keep up your momentum.
102[01:47:40.940 --> 01:47:44.500] And it builds your best possible workout with the use of exercise science.
103[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.
104[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.
105[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
106 with the instructions.
107 [01:48:07.380 --> 01:48:10.100] And it makes me feel confident about the work that I'm going to be doing.
108 [01:48:10.100 --> 01:48:14.340] Muscles improve when they're working in concert with the entire musculoskeletal system.
109 [01:48:14.340 --> 01:48:19.360] So overworking some muscles while underworking others can negatively impact your overall results.
110 [01:48:19.360 --> 01:48:24.280] This is why FitBod tracks muscle fatigue and recovery to design a well-balanced workout routine,
111 [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.
112 [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.
113 [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.
114 [01:48:44.020 --> 01:48:45.960] So you should go and check it out for yourself.
115 [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.
116 [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.
117 [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.
118 [01:49:04.380 --> 01:49:09.840] So go now and get your customized fitness plan at FitBod.me slash Cortex.
119 [01:49:09.980 --> 01:49:14.180] Once again, that is FitBod.me slash Cortex, and you'll get 25% off your membership.
120 [01:49:14.180 --> 01:49:17.980] A thanks to FitBod for their continued support of this show and Relay.
121 [01:49:17.980 --> 01:49:20.120] We've made it to home screens.
122 [01:49:20.120 --> 01:49:22.360] Oh, home screens. Right, right.
123 [01:49:22.360 --> 01:49:23.820] How complicated can it be?
124 [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.
125
126**Example 4 Output:**
12701:46:59.320-01:49:17.980
128
129---
130**Example 5 Input:**
131[00:32:20.460 --> 00:32:26.000] You know what got me there is Mark saying hodgepodge, I was like, oh, it's like a Katamari ball.
132[00:32:26.000 --> 00:32:26.720] Sure.
133[00:32:26.720 --> 00:32:32.820] I mean, that's double points for, for accuracy, if there's ever been such a thing, that's...
134[00:32:32.820 --> 00:32:36.060] Does it ever roll over the farm and collect the dogs and hogs?
135[00:32:36.060 --> 00:32:44.840] That's not in the, what I have, but like, yeah, theoretically, in your scenario, that would be a hog-ma-dog if that were to have happen.
136[00:32:44.840 --> 00:32:45.900] That is accurate.
137[00:32:46.820 --> 00:32:50.880] Amazon One Medical presents Painful Thoughts.
138[00:32:50.880 --> 00:32:54.540] I've been on hold to make a doctor's appointment for 23 minutes now.
139[00:32:54.540 --> 00:33:00.320] The automated voice has told me 47 times that my call is very important to them.
140[00:33:00.320 --> 00:33:01.540] Hmm.
141[00:33:01.540 --> 00:33:05.760] I'm starting to think that they don't think my call is important at all.
142[00:33:05.760 --> 00:33:13.020] With Amazon One Medical 24-7 virtual care, you'll get help fast, without having to remain on the line to make an appointment.
143[00:33:13.020 --> 00:33:14.460] Amazon One Medical.
144[00:33:14.460 --> 00:33:16.360] Healthcare just got less painful.
145[00:33:17.640 --> 00:33:19.800] This episode is brought to you by Diet Coke.
146[00:33:19.800 --> 00:33:22.740] You know that moment when you just need to hit pause and refresh?
147[00:33:22.740 --> 00:33:25.280] An ice-cold Diet Coke isn't just a break.
148[00:33:25.280 --> 00:33:28.760] It's your chance to catch your breath and savor a moment that's all about you.
149[00:33:28.760 --> 00:33:31.500] Always refreshing, still the same great taste.
150[00:33:31.500 --> 00:33:32.440] Diet Coke.
151[00:33:32.440 --> 00:33:33.820] Make time for you time.
152[00:33:37.320 --> 00:33:46.600] Alright, well this one I think is gonna mislead you in that it sounds funnier than it maybe is, so I'm gonna give you that hint to start off.
153[00:33:46.900 --> 00:33:51.220] But tell me please, what is a Yule Hole?
154[00:33:51.220 --> 00:33:55.620] Y-U-L-E hyphen H-O-L-E.
155[00:33:55.680 --> 00:33:59.720] I mean, you put your Yule log in the Yule Hole.
156
157**Example 5 Output:**
15800:32:46.820-00:33:16.360
15900:33:17.640-00:33:33.820
160
161---
162**Example 6 Input:**
163[00:24:21.080 --> 00:24:21.920] I figured out the game.
164[00:24:21.920 --> 00:24:22.620] I know what it is.
165[00:24:22.620 --> 00:24:22.860] All right.
166[00:24:22.860 --> 00:24:23.280] That's fair.
167[00:24:23.280 --> 00:24:24.380] I haven't figured it out yet.
168[00:24:24.380 --> 00:24:26.220] I'm going to test my hypothesis during the next round.
169[00:24:26.220 --> 00:24:26.520] All right.
170[00:24:26.520 --> 00:24:26.920] Good luck.
171[00:24:27.280 --> 00:24:28.320] I guess we'll find out.
172[00:24:28.320 --> 00:24:33.060] This episode is brought to you by Hershey's.
173[00:24:33.060 --> 00:24:35.540] When it comes to a sweet treat, I love a combo.
174[00:24:35.540 --> 00:24:39.980] Like those two flavors that just go together perfectly, like a little party in your mouth.
175[00:24:39.980 --> 00:24:42.160] I think most people are with me on this, right?
176[00:24:42.160 --> 00:24:46.720] If you want the ultimate combo, I think you should give Hershey's Cookies and Cream a try.
177[00:24:46.720 --> 00:24:48.680] If you haven't had it, where have you been?
178[00:24:48.880 --> 00:24:53.380] Rich, smooth, white cream, packed with crunchy, chocolatey cookie bits in every bite.
179[00:24:53.380 --> 00:24:55.780] It's a delicious balance of flavor and texture.
180[00:24:55.780 --> 00:24:57.420] Hershey's Cookies and Cream.
181[00:24:57.420 --> 00:24:59.820] Creamy, crunchy, and all yours.
182[00:24:59.820 --> 00:25:03.460] Find Hershey's Cookies and Cream bars at a store near you today.
183[00:25:03.460 --> 00:25:07.540] This episode is brought to you by Vitamin Water.
184[00:25:07.540 --> 00:25:10.540] Living in a big city like Cincinnati has challenges.
185[00:25:10.540 --> 00:25:11.900] That's so big, man.
186[00:25:11.900 --> 00:25:13.500] LA's big like Cincinnati.
187[00:25:13.500 --> 00:25:18.200] Thankfully, Vitamin Water's new zero-sugar flavors like rehydrate pineapple passion fruit,
188[00:25:18.340 --> 00:25:23.120] squeezed lemonade, and XXX acai blueberry pomegranate are packed with essential vitamins
189[00:25:23.120 --> 00:25:25.700] for when you're traveling across that big city.
190[00:25:25.700 --> 00:25:29.040] So whatever the reason, grab a Vitamin Water today.
191[00:25:29.040 --> 00:25:30.880] Copyright 2025, Glasso.
192[00:25:30.880 --> 00:25:33.540] Vitamin Water is a registered trademark of Glasso.
193[00:25:33.540 --> 00:25:37.520] Next topic here.
194[00:25:37.520 --> 00:25:42.300] Imagine like when we walk around, if it was like in the movies where a theme song would play,
195
196**Example 6 Output:**
19700:24:28.320-00:25:03.460
19800:25:03.460-00:25:30.880
199
200---
201**Example 7 Input:**
202[00:14:00.160 --> 00:14:05.240] every day to somebody you know we're gonna we're the new slack we're the replacement for slack
203[00:14:05.240 --> 00:14:10.400] maybe email would be the replacement for slack and all it all comes full circle
204[00:14:10.400 --> 00:14:17.500] chat clients and email are just a like a rubber is eating its own tail forever all roads lead to email
205[00:14:17.500 --> 00:14:26.640] this episode of cortex is brought to you by our friends at lunar display lunar display are the makers
206[00:14:26.640 --> 00:14:33.840] of the only hardware solution that will turn your ipad into a wireless display for your mac which means
207[00:14:33.840 --> 00:14:40.320] you will have a second display with you that is super portable with basically zero lag and stunning image
208[00:14:40.320 --> 00:14:47.480] quality setting up extra screens is a fiddly task but lunar display makes this so easy you plug
209[00:14:47.480 --> 00:14:53.180] this beautiful little dongle into the back of your mac and you are ready to go everything works
210[00:14:53.180 --> 00:15:00.080] seamlessly over wi-fi and then you have a second screen for your mac on your ipad you can set it next to
211[00:15:00.080 --> 00:15:05.840] your screen on your desk and you have an extra monitor there for whenever you need it but it also works
212[00:15:05.840 --> 00:15:11.100] over usb as well so imagine you're traveling maybe you're on a plane you're on a train maybe you've been
213[00:15:11.100 --> 00:15:16.560] lucky enough to get an extra seat next to you so you can take advantage of some additional desk space you can just
214[00:15:16.560 --> 00:15:21.860] plug your ipad and your mac together with a usb cable and you'll be able to use your lunar display
215[00:15:21.860 --> 00:15:29.160] on your ipad as a second screen it is absolutely wonderful and then you have that advantage of
216[00:15:29.160 --> 00:15:35.520] extra screen real estate wherever you need it lunar display is a complete extension for your mac it supports
217[00:15:35.520 --> 00:15:41.800] external keyboards on the ipad as well as the apple pencil and touch interactions turning your mac into a
218[00:15:41.800 --> 00:15:47.100] touch screen device and the all new liquid video engine that lunar display have developed brings
219[00:15:47.100 --> 00:15:53.180] significantly reduced latency and faster screen refresh rate than ever before i use lunar display
220[00:15:53.180 --> 00:15:59.700] every single day i have a mac mini in my office that doesn't even have a display plugged into it at all i just
221[00:15:59.700 --> 00:16:05.020] have a lunar display plugged into it so then i'm able to use that mac mini as a server for a bunch of
222[00:16:05.020 --> 00:16:10.320] tasks at home but i can also from wherever i want at home i can open up the lunar display
223[00:16:10.320 --> 00:16:15.940] app on my ipad and get something done quick if i need to on the mac so i have a bunch of little tasks
224[00:16:15.940 --> 00:16:21.200] that i might need to complete that sometimes ios can't handle for me but now i don't need to worry
225[00:16:21.200 --> 00:16:28.840] because mac os basically lives as an application on my ipad i absolutely adore my lunar display if you have
226[00:16:28.840 --> 00:16:35.700] an ipad and a mac just get one of these things to try it out it is so freaking cool listeners of this
227[00:16:35.700 --> 00:16:42.400] show can get an exclusive 10 discount on lunar display just go to lunar display.com and enter the
228[00:16:42.400 --> 00:16:52.720] promo code cortex at checkout that is l-u-n-a-d-i-s-p-l-a-y.com promo code cortex at checkout for that 10
229[00:16:52.720 --> 00:16:59.100] percent off go there now upgrade your setup you're gonna love it just like i do lunar display.com
230[00:16:59.100 --> 00:17:05.040] promo code cortex for 10 off our thanks to lunar display for their support of this show and all of
231[00:17:05.040 --> 00:17:13.500] relay fm so one month has been completed in 2019 so we have started our years with our yearly themes in
232[00:17:13.500 --> 00:17:19.400] mind yeah how has year of order 2 which is how i'm thinking of it how has that begun for you
233[00:17:19.400 --> 00:17:26.260] i don't like year of order 2 so much year of more order it's the year of reorder
234[00:17:26.260 --> 00:17:32.620] ah yes i still personally i prefer year of order 2 but sure year of reorder will so you can do whatever
235
236**Example 7 Output:**
23700:14:17.500-00:17:05.040
238"""
239
240def seconds_to_timestamp(total_seconds: float) -> str:
241 """Converts total seconds to HH:MM:SS.mmm string."""
242 if total_seconds < 0:
243 total_seconds = 0
244 integer_seconds = int(total_seconds)
245 milliseconds = int((total_seconds - integer_seconds) * 1000)
246
247 # Use timedelta for robust conversion to hours, minutes, seconds
248 td = timedelta(seconds=integer_seconds)
249 hours, remainder = divmod(td.seconds, 3600)
250 minutes, seconds = divmod(remainder, 60)
251
252 # Manually format to ensure HH is always present and seconds has 3 decimal places
253 return f"{hours:02}:{minutes:02}:{int(seconds):02}.{milliseconds:03}"
254
255def format_transcript(filepath: str):
256 print(SYSTEM_PROMPT_TEMPLATE)
257 print("**User Input:**")
258
259 with open(filepath, 'r', encoding='utf-8') as f:
260 data = json.load(f)
261
262 for segment in data:
263 start_ts = seconds_to_timestamp(segment['start_time'])
264 end_ts = seconds_to_timestamp(segment['end_time'])
265 text = segment['text']
266
267 print(f"[{start_ts} --> {end_ts}]\t{text}")
268
269if __name__ == "__main__":
270 if len(sys.argv) < 2:
271 print("Usage: python format_script.py <path_to_json_file>", file=sys.stderr)
272 sys.exit(1)
273
274 format_transcript(sys.argv[1])
diff --git a/main.py b/main.py
index dfcb7db..c7a1ff5 100644
--- a/main.py
+++ b/main.py
@@ -1,16 +1,16 @@
1import os 1import os
2import re
3import sys 2import sys
3import json
4import backoff 4import backoff
5import openai 5import openai
6import json 6from datetime import timedelta
7from datetime import datetime, timedelta
8from tqdm import tqdm 7from tqdm import tqdm
9 8
10client = openai.OpenAI( 9client = openai.OpenAI(
11 #base_url="http://localhost:8080/v1", 10 #base_url="http://localhost:8080/v1",
12 #base_url="https://api.openai.com/v1", 11 #base_url="https://api.openai.com/v1",
13 base_url="https://openrouter.ai/api/v1", 12 base_url="https://openrouter.ai/api/v1",
13 api_key=os.getenv("OPENROUTER_API_KEY")
14) 14)
15 15
16#MODEL = "deepseek/deepseek-r1-0528:free" 16#MODEL = "deepseek/deepseek-r1-0528:free"
@@ -20,7 +20,7 @@ MODEL = "deepseek/deepseek-chat-v3-0324:free" # twice as slow as paid
20#MODEL = "google/gemini-2.0-flash-exp:free" # fine 20#MODEL = "google/gemini-2.0-flash-exp:free" # fine
21#MODEL = "qwen/qwen3-30b-a3b" # sort of works, but slow 21#MODEL = "qwen/qwen3-30b-a3b" # sort of works, but slow
22 22
23SYSTEM_PROMPT_TEMPLATE = """ 23SYSTEM_PROMPT = """
24You 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. 24You 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.
25 25
26You will be provided with segments of a podcast transcript, formatted with timestamps. Your output must be ONLY the start and end timestamp of the identified advertising segment, separated by a hyphen, in the format HH:MM:SS.mmm-HH:MM:SS.mmm. If there are multiple ad segments, output each on a new line. If no ad segments are found, output nothing. 26You will be provided with segments of a podcast transcript, formatted with timestamps. Your output must be ONLY the start and end timestamp of the identified advertising segment, separated by a hyphen, in the format HH:MM:SS.mmm-HH:MM:SS.mmm. If there are multiple ad segments, output each on a new line. If no ad segments are found, output nothing.
@@ -255,32 +255,17 @@ tly what's happening and exactly where things are going wrong.
25500:14:17.500-00:17:05.040 25500:14:17.500-00:17:05.040
256""" 256"""
257 257
258def parse_whisper_transcript(transcript_text: str) -> list[dict]: 258# --- Helper Functions ---
259 """ 259def seconds_to_timestamp(total_seconds: float) -> str:
260 Parses a Whisper-generated transcript into a list of dictionaries, 260 """Converts total seconds to HH:MM:SS.mmm string."""
261 each containing 'start_time', 'end_time', and 'text'. 261 if total_seconds < 0:
262 """ 262 total_seconds = 0
263 segments = [] 263 integer_seconds = int(total_seconds)
264 # Regex to match timestamp format [HH:MM:SS.mmm --> HH:MM:SS.mmm] and the following text 264 milliseconds = int((total_seconds - integer_seconds) * 1000)
265 pattern = re.compile(r"^\[(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})\]\s*(.*)$", re.MULTILINE) 265 td = timedelta(seconds=integer_seconds)
266 266 hours, remainder = divmod(td.seconds, 3600)
267 for line in transcript_text.strip().split('\n'): 267 minutes, seconds = divmod(remainder, 60)
268 match = pattern.match(line) 268 return f"{hours:02}:{minutes:02}:{int(seconds):02}.{milliseconds:03}"
269 if match:
270 start_time, end_time, text = match.groups()
271 segments.append({
272 "start_time": start_time,
273 "end_time": end_time,
274 "text": text.strip()
275 })
276 return segments
277
278def format_transcript_segment(segments: list[dict]) -> str:
279 """Formats a list of segment dictionaries back into the prompt-friendly string."""
280 formatted_lines = []
281 for s in segments:
282 formatted_lines.append(f"[{s['start_time']} --> {s['end_time']}] {s['text']}")
283 return "\n".join(formatted_lines)
284 269
285def timestamp_to_seconds(ts_str: str) -> float: 270def timestamp_to_seconds(ts_str: str) -> float:
286 """Converts HH:MM:SS.mmm string to total seconds.""" 271 """Converts HH:MM:SS.mmm string to total seconds."""
@@ -292,148 +277,147 @@ def timestamp_to_seconds(ts_str: str) -> float:
292 milliseconds = int(seconds_ms_str[1]) 277 milliseconds = int(seconds_ms_str[1])
293 return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000.0 278 return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000.0
294 279
295def seconds_to_timestamp(total_seconds: float) -> str: 280def segment_by_punctuation(asr_results: list, min_words_per_segment: int = 3) -> list[dict]:
296 """Converts total seconds to HH:MM:SS.mmm string.""" 281 """Converts ASR results into sentence-level segments based on punctuation."""
297 integer_seconds = int(total_seconds) 282 segments = []
298 milliseconds = int((total_seconds - integer_seconds) * 1000) 283 punctuations = {".", "?", "!"}
284 for result in asr_results:
285 token_map = result.get("token_map", [])
286 if not token_map:
287 continue
299 288
300 td = timedelta(seconds=integer_seconds) 289 tokens = [pair[0] for pair in token_map]
301 hours, remainder = divmod(td.total_seconds(), 3600) 290 timestamps = [pair[1] for pair in token_map]
302 minutes, seconds = divmod(remainder, 60) 291
303 292 current_segment_start_idx = 0
304 return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}.{milliseconds:03}" 293 for i, token in enumerate(tokens):
294 is_end_of_sentence = any(p in token.strip() for p in punctuations)
295
296 if is_end_of_sentence or (i == len(tokens) - 1):
297 segment_tokens = tokens[current_segment_start_idx : i + 1]
298
299 if len(segment_tokens) >= min_words_per_segment:
300 start_time = timestamps[current_segment_start_idx]
301 end_time = timestamps[i]
302 text = "".join(segment_tokens).strip()
303
304 segments.append({
305 "start_time": start_time,
306 "end_time": end_time,
307 "text": text
308 })
309 current_segment_tokens = []
310 return segments
305 311
306def chunk_transcript( 312def chunk_segments(segments: list[dict], chunk_duration_seconds: int = 240, overlap_seconds: int = 60) -> list[list[dict]]:
307 segments: list[dict], 313 """Groups sentence-level segments into fixed-size, overlapping chunks."""
308 chunk_duration_seconds: int = 240,
309 overlap_seconds: int = 60
310) -> list[list[dict]]:
311 if not segments: 314 if not segments:
312 return [] 315 return []
313 316
314 chunks = [] 317 chunks = []
315
316 # Calculate step size for the window
317 step_seconds = chunk_duration_seconds - overlap_seconds 318 step_seconds = chunk_duration_seconds - overlap_seconds
318 if step_seconds <= 0: 319 if step_seconds <= 0:
319 raise ValueError("Overlap must be smaller than chunk duration.") 320 raise ValueError("Overlap must be smaller than chunk duration.")
320 321
321 transcript_end_time = timestamp_to_seconds(segments[-1]['end_time']) 322 transcript_end_time = segments[-1]['end_time']
322
323 window_start_time = 0.0 323 window_start_time = 0.0
324 while window_start_time < transcript_end_time: 324 while window_start_time < transcript_end_time:
325 window_end_time = window_start_time + chunk_duration_seconds 325 window_end_time = window_start_time + chunk_duration_seconds
326 326 current_chunk_segments = [s for s in segments if s['start_time'] < window_end_time and s['end_time'] > window_start_time]
327 current_chunk_segments = []
328 for s in segments:
329 seg_start = timestamp_to_seconds(s['start_time'])
330 seg_end = timestamp_to_seconds(s['end_time'])
331
332 # Add segment if it has any overlap with the current window
333 if seg_start < window_end_time and seg_end > window_start_time:
334 current_chunk_segments.append(s)
335
336 if current_chunk_segments: 327 if current_chunk_segments:
337 chunks.append(current_chunk_segments) 328 chunks.append(current_chunk_segments)
338
339 window_start_time += step_seconds 329 window_start_time += step_seconds
340
341 # Break if the last segment has been fully processed
342 if window_start_time > timestamp_to_seconds(current_chunk_segments[-1]['end_time']):
343 break
344
345 return chunks 330 return chunks
346 331
347@backoff.on_exception(backoff.expo, (openai.RateLimitError, openai.InternalServerError)) 332def format_chunk_for_prompt(chunk: list[dict]) -> str:
348def completions_with_backoff(**kwargs): 333 """Formats a chunk of segments into the final plain text prompt format."""
349 return client.chat.completions.create(**kwargs) 334 return "\n".join(
335 f"[{seconds_to_timestamp(s['start_time'])} --> {seconds_to_timestamp(s['end_time'])}] {s['text']}"
336 for s in chunk
337 )
350 338
351def call_openai_api(chunk_text: str, system_prompt: str) -> str: 339@backoff.on_exception(backoff.expo, (openai.RateLimitError, openai.InternalServerError), max_tries=8)
352 """ 340def call_api_with_backoff(**kwargs):
353 Makes an API call to OpenAI with the given system prompt and transcript chunk. 341 """Makes a completions call with exponential backoff."""
354 """ 342 return client.chat.completions.create(**kwargs)
355 try:
356 response = completions_with_backoff(
357 #model="Gemma-3-4B_32K",
358 #model="google/gemini-2.0-flash-exp:free",
359 #model="gpt-4.1",
360 model=MODEL,
361 messages=[
362 {"role": "system", "content": system_prompt},
363 {"role": "user", "content": f"Input Transcript:\n{chunk_text}"}
364 ],
365 temperature=0.0,
366 max_tokens=1024, # realistically it'd be under like 512
367 #extra_body={"provider": {"quantizations": ["fp8"], "sort": "price"}},
368 extra_body={"provider": {"sort": "price"}},
369 )
370 return response.choices[0].message.content.strip()
371 except Exception as e:
372 #print(response)
373 #print(e)
374 return None
375
376
377def process_file(input_file: str, output_file: str):
378 with open(input_file, "r", encoding="utf-8") as f:
379 full_transcript_content = f.read()
380
381 # --- Processing ---
382 parsed_segments = parse_whisper_transcript(full_transcript_content)
383
384 # Adjust the trigger phrase if "brought to you by" isn't consistently sufficient
385 # For more complex cases, you might use a list of trigger phrases:
386 # ["brought to you by", "a quick word from our sponsor", "we'll be right back"]
387 transcript_chunks = chunk_transcript(segments=parsed_segments)
388
389 all_ad_timestamps = []
390
391 # Process each chunk with the OpenAI API
392 for i, chunk in enumerate(transcript_chunks):
393 if not chunk:
394 continue
395 343
396 user_prompt = format_transcript_segment(chunk) 344# --- Main Processing Logic ---
397 345
398 try: 346def process_transcript_json(input_json_path: str, output_jsonl_path: str):
399 ad_timestamps_str = call_openai_api(user_prompt, SYSTEM_PROMPT_TEMPLATE) 347 """Loads, segments, chunks, and labels a transcript JSON file."""
400 except Exception as e: 348 print(f"Processing {input_json_path}...")
401 print(f"Error calling OpenAI API: {e}") 349
402 continue 350 # 1. Load the raw ASR output
351 with open(input_json_path, 'r', encoding='utf-8') as f:
352 asr_data = json.load(f)
403 353
404 while ad_timestamps_str == None: 354 # 2. Convert token-level data into sentence-level segments
405 ad_timestamps_str = call_openai_api(user_prompt, SYSTEM_PROMPT_TEMPLATE) 355 sentence_segments = segment_by_punctuation(asr_data)
356 if not sentence_segments:
357 print("No segments found after processing. Exiting.")
358 return
406 359
407 if ad_timestamps_str.startswith("[") or "no output" in ad_timestamps_str.lower() or "no ad" in ad_timestamps_str.lower(): 360 # 3. Group sentences into overlapping chunks for the LLM
408 ad_timestamps_str = "" 361 transcript_chunks = chunk_segments(sentence_segments)
409 362
363 final_results = []
364 for i, chunk in enumerate(tqdm(transcript_chunks, desc="Labeling Chunks")):
365 # 4. Format the chunk into the plain text required by the prompt
366 prompt_text = format_chunk_for_prompt(chunk)
410 367
411 promptid = os.path.dirname(input_file).split('/')[-1] + "_" 368 try:
412 promptid += os.path.splitext(os.path.basename(output_file))[0].replace(" ", "") + f"_chunk{i}" 369 response = call_api_with_backoff(
370 model=MODEL,
371 messages=[
372 {"role": "system", "content": SYSTEM_PROMPT},
373 {"role": "user", "content": f"Input Transcript:\n{prompt_text}"}
374 ],
375 temperature=0.0,
376 max_tokens=1024,
377 )
378
379 if response and response.choices:
380 response_text = response.choices[0].message.content.strip()
381 else:
382 response_text = ""
383 except Exception as e:
384 print(f"\nAPI call failed for chunk {i}: {e}")
385 continue
413 386
414 # Split by newlines in case the model returns multiple segments 387 if response_text.startswith("[") or "no output" in response_text.lower() or "no ad" in response_text.lower():
415 found_timestamps = [ts.strip() for ts in ad_timestamps_str.split('\n') if ts.strip()] 388 response_text = ""
416 all_ad_timestamps.append({"id": promptid, f"text": "Instruction: Identify the timestamp range of the pre-recorded ad in the following transcript. Output only the HH:MM:SS.mmm-HH:MM:SS.mmm range.\n\nTranscript:\n" + user_prompt, "target": "\n".join(found_timestamps)})
417 389
418 with open(output_file, "w") as f: 390
419 for s in all_ad_timestamps: 391 # Format for the fine-tuning dataset
420 f.write(json.dumps(s) + "\n") 392 prompt_id = os.path.splitext(os.path.basename(input_json_path))[0] + f"_chunk{i}"
393 full_prompt_for_finetuning = f"Instruction: Identify the timestamp range of the pre-recorded ad in the following transcript. Output only the HH:MM:SS.mmm-HH:MM:SS.mmm range.\n\nTranscript:\n{prompt_text}"
394
395 final_results.append({
396 "id": prompt_id,
397 "text": full_prompt_for_finetuning,
398 "target": response_text.strip()
399 })
400
401 # 5. Save results to a .jsonl file
402 with open(output_jsonl_path, "w", encoding="utf-8") as f:
403 for result in final_results:
404 f.write(json.dumps(result) + "\n")
405
406 print(f"Finished processing. Saved {len(final_results)} labeled chunks to {output_jsonl_path}")
421 407
422def main(): 408def main():
423 os.makedirs("data/", exist_ok=True) 409 if len(sys.argv) < 3:
424 paths = sys.argv[1:] 410 print("Usage: python label_generator.py <input_transcript.json> <output_labels.jsonl>")
425 for p in paths: 411 sys.exit(1)
426 out = "data/" 412
427 out += os.path.dirname(p).split('/')[-1] + "_" 413 input_path = sys.argv[1]
428 out += os.path.splitext(os.path.basename(p))[0] + ".jsonl" 414 output_path = sys.argv[2]
429 if os.path.isfile(out): 415
430 paths.remove(p) 416 if os.path.exists(output_path):
431 417 print(f"Output file {output_path} already exists. Skipping.")
432 for p in tqdm(paths): 418 return
433 out = "data/" 419
434 out += os.path.dirname(p).split('/')[-1] + "_" 420 process_transcript_json(input_path, output_path)
435 out += os.path.splitext(os.path.basename(p))[0] + ".jsonl"
436 process_file(p, out)
437 421
438if __name__ == "__main__": 422if __name__ == "__main__":
439 main() 423 main()