summaryrefslogtreecommitdiff
path: root/main.py
blob: a58e02fd3a85a6dcf13cbfe732962d080c0a41a4 (plain)
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
import os
import sys
import json
import backoff
import openai
from datetime import timedelta
from tqdm import tqdm

client = openai.OpenAI(
    #base_url="http://localhost:8080/v1",
    #base_url="https://api.openai.com/v1",
    base_url="https://openrouter.ai/api/v1",
    api_key=os.getenv("OPENROUTER_API_KEY")
)

# smaller models than this (besides dense llama 3.1 405b) really struggle
MODEL = "deepseek/deepseek-chat-v3.1"

SYSTEM_PROMPT = """
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.

You 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.

**Strict Rules:**
- Only identify segments that are clearly pre-recorded dynamic ads with a noticeable tone shift.
- Do NOT include host-read sponsorships that blend naturally with the content. Focus only on the "distracting" pre-recorded elements.
- 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.
- 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.
- Ensure the start and end times correspond precisely to the ad segment in the transcript.
- Output ONLY the timestamp range(s) in the specified format. No other text, no JSON, no explanations.
- 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.

**Examples:**

---
**Example 1 Input:**
[00:00:00.000 --> 00:00:04.880]   Now, you can watch the hottest movies and series with Hulu on Disney+.
[00:00:04.880 --> 00:00:08.400]   With Hulu, say, yes, chef, to a new season of FX is the Bear.
[00:00:08.400 --> 00:00:13.000]   And don't miss Marvel Television's action-packed new series, Ironheart, on Disney+.
[00:00:13.000 --> 00:00:17.060]   All of these and more now streaming with Hulu on Disney+, with a bundle subscription.
[00:00:17.060 --> 00:00:18.040]   Terms apply.
[00:00:18.040 --> 00:00:20.980]   Visit Disneyplus.com slash Hulu for details.
[00:00:20.980 --> 00:00:27.440]   This episode is brought to you by DoorDash, because the summer of Dash Pass is back.
[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.
[00:00:33.420 --> 00:00:35.640]   So sign up for Dash Pass now if you haven't already.
[00:00:35.640 --> 00:00:37.800]   Dash Pass benefits apply only to eligible orders. Terms apply.
[00:00:37.800 --> 00:00:40.080]   This episode is brought to you by Amazon.
[00:00:40.080 --> 00:00:43.200]   This off-the-college season, save on college, save the everyday.
[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.
[00:00:48.140 --> 00:00:49.720]   You know, I remember a story.
[00:00:49.720 --> 00:00:52.980]   My roommate and I decided to bunk our beds.
[00:00:52.980 --> 00:00:56.540]   So we just cut up some plastic hangers and just jammed them in there.
[00:00:56.540 --> 00:00:59.780]   We could have really used some metal pins.
[00:00:59.780 --> 00:01:01.500]   Wonder who that roommate could have been.
[00:01:01.500 --> 00:01:05.900]   So remember, with Amazon's low off-the-college prices, save on college, save the everyday.
[00:01:05.900 --> 00:01:07.820]   Shop off-the-college at Amazon.
[00:01:07.820 --> 00:01:13.400]   Good evening, gentle listener, and welcome to Distractable, a Wood Elf production.
[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.
[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.
[00:01:26.200 --> 00:01:31.240]   Bob derides the last son of Krypton, while Mark extols the virtues of the web-slinger.

**Example 1 Output:**
00:00:00.000-00:00:20.980
00:00:20.980-00:00:37.800
00:00:37.800-00:01:07.820

---
**Example 2 Input:**
[00:38:07.480 --> 00:38:10.820]   This podcast is brought to you by Carvana.
[00:38:10.820 --> 00:38:13.660]   Got a car to sell, but no time to waste?
[00:38:13.660 --> 00:38:17.820]   Hop on to Carvana.com to get a real offer for your car in seconds.
[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
[00:38:23.060 --> 00:38:26.000]   the offer, Carvana will pay you as soon as you hand the keys over.
[00:38:26.360 --> 00:38:29.060]   They even offer same day pickup in many cities.
[00:38:29.060 --> 00:38:35.740]   Save your time, score some cash, and sell your car the convenient way to Carvana.
[00:38:36.360 --> 00:38:37.000]   Pickup times vary.
[00:38:37.000 --> 00:38:37.620]   Fees may apply.

**Example 2 Output:**
00:38:07.480-00:38:37.620

---
**Example 3 Input:**
[00:03:11.060 --> 00:03:12.720]   I'm always very happy to do this.
[00:03:12.720 --> 00:03:14.800]   High on the list of things that you want to do.
[00:03:14.800 --> 00:03:16.260]   High on the list of things that I want to do.
[00:03:16.620 --> 00:03:20.260]   Low on the list is recording after having just arrived home.
[00:03:20.260 --> 00:03:21.260]   I do this.
[00:03:21.260 --> 00:03:22.360]   I do this every now and then.
[00:03:22.360 --> 00:03:25.720]   I will arrive home from a trip, and I will record.
[00:03:25.720 --> 00:03:29.280]   And it is a very disorienting experience.
[00:03:29.280 --> 00:03:32.960]   As I warned you at some point during the episode today,
[00:03:32.960 --> 00:03:34.460]   you're likely to get a little bit loopy,
[00:03:34.460 --> 00:03:35.940]   because that's what happens to me.
[00:03:35.940 --> 00:03:37.260]   Yeah.
[00:03:37.260 --> 00:03:41.900]   I mean, we're recording today because there's literally no other day
[00:03:41.900 --> 00:03:45.400]   that could work for us for doing the show.
[00:03:45.400 --> 00:03:49.040]   We moved it, like, twice already, and here we are.

**Example 3 Output:**


---
**Example 4 Input:**
[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.
[01:46:32.220 --> 01:46:36.320]   So if you don't use shortcuts, this is completely worthless to you.
[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.
[01:46:54.380 --> 01:46:57.460]   So Logger is my app of the year.
[01:46:59.320 --> 01:47:02.180]   This episode of Cortex is brought to you by FitBod.
[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.
[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.
[01:47:12.400 --> 01:47:15.580]   Everybody has their own path when it comes to personal fitness.
[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.
[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.
[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.
[01:47:40.940 --> 01:47:44.500]   And it builds your best possible workout with the use of exercise science.
[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.
[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.
[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.
[01:48:07.380 --> 01:48:10.100]   And it makes me feel confident about the work that I'm going to be doing.
[01:48:10.100 --> 01:48:14.340]   Muscles improve when they're working in concert with the entire musculoskeletal system.
[01:48:14.340 --> 01:48:19.360]   So overworking some muscles while underworking others can negatively impact your overall results.
[01:48:19.360 --> 01:48:24.280]   This is why FitBod tracks muscle fatigue and recovery to design a well-balanced workout routine,
[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.
[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.
[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.
[01:48:44.020 --> 01:48:45.960]   So you should go and check it out for yourself.
[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.
[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.
[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.
[01:49:04.380 --> 01:49:09.840]   So go now and get your customized fitness plan at FitBod.me slash Cortex.
[01:49:09.980 --> 01:49:14.180]   Once again, that is FitBod.me slash Cortex, and you'll get 25% off your membership.
[01:49:14.180 --> 01:49:17.980]   A thanks to FitBod for their continued support of this show and Relay.
[01:49:17.980 --> 01:49:20.120]   We've made it to home screens.
[01:49:20.120 --> 01:49:22.360]   Oh, home screens. Right, right.
[01:49:22.360 --> 01:49:23.820]   How complicated can it be?
[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.

**Example 4 Output:**
01:46:59.320-01:49:17.980

---
**Example 5 Input:**
[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.
[00:32:26.000 --> 00:32:26.720]   Sure.
[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...
[00:32:32.820 --> 00:32:36.060]   Does it ever roll over the farm and collect the dogs and hogs?
[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.
[00:32:44.840 --> 00:32:45.900]   That is accurate.
[00:32:46.820 --> 00:32:50.880]   Amazon One Medical presents Painful Thoughts.
[00:32:50.880 --> 00:32:54.540]   I've been on hold to make a doctor's appointment for 23 minutes now.
[00:32:54.540 --> 00:33:00.320]   The automated voice has told me 47 times that my call is very important to them.
[00:33:00.320 --> 00:33:01.540]   Hmm.
[00:33:01.540 --> 00:33:05.760]   I'm starting to think that they don't think my call is important at all.
[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.
[00:33:13.020 --> 00:33:14.460]   Amazon One Medical.
[00:33:14.460 --> 00:33:16.360]   Healthcare just got less painful.
[00:33:17.640 --> 00:33:19.800]   This episode is brought to you by Diet Coke.
[00:33:19.800 --> 00:33:22.740]   You know that moment when you just need to hit pause and refresh?
[00:33:22.740 --> 00:33:25.280]   An ice-cold Diet Coke isn't just a break.
[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.
[00:33:28.760 --> 00:33:31.500]   Always refreshing, still the same great taste.
[00:33:31.500 --> 00:33:32.440]   Diet Coke.
[00:33:32.440 --> 00:33:33.820]   Make time for you time.
[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.
[00:33:46.900 --> 00:33:51.220]   But tell me please, what is a Yule Hole?
[00:33:51.220 --> 00:33:55.620]   Y-U-L-E hyphen H-O-L-E.
[00:33:55.680 --> 00:33:59.720]   I mean, you put your Yule log in the Yule Hole.

**Example 5 Output:**
00:32:46.820-00:33:16.360
00:33:17.640-00:33:33.820

---
**Example 6 Input:**
[00:24:21.080 --> 00:24:21.920]   I figured out the game.
[00:24:21.920 --> 00:24:22.620]   I know what it is.
[00:24:22.620 --> 00:24:22.860]   All right.
[00:24:22.860 --> 00:24:23.280]   That's fair.
[00:24:23.280 --> 00:24:24.380]   I haven't figured it out yet.
[00:24:24.380 --> 00:24:26.220]   I'm going to test my hypothesis during the next round.
[00:24:26.220 --> 00:24:26.520]   All right.
[00:24:26.520 --> 00:24:26.920]   Good luck.
[00:24:27.280 --> 00:24:28.320]   I guess we'll find out.
[00:24:28.320 --> 00:24:33.060]   This episode is brought to you by Hershey's.
[00:24:33.060 --> 00:24:35.540]   When it comes to a sweet treat, I love a combo.
[00:24:35.540 --> 00:24:39.980]   Like those two flavors that just go together perfectly, like a little party in your mouth.
[00:24:39.980 --> 00:24:42.160]   I think most people are with me on this, right?
[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.
[00:24:46.720 --> 00:24:48.680]   If you haven't had it, where have you been?
[00:24:48.880 --> 00:24:53.380]   Rich, smooth, white cream, packed with crunchy, chocolatey cookie bits in every bite.
[00:24:53.380 --> 00:24:55.780]   It's a delicious balance of flavor and texture.
[00:24:55.780 --> 00:24:57.420]   Hershey's Cookies and Cream.
[00:24:57.420 --> 00:24:59.820]   Creamy, crunchy, and all yours.
[00:24:59.820 --> 00:25:03.460]   Find Hershey's Cookies and Cream bars at a store near you today.
[00:25:03.460 --> 00:25:07.540]   This episode is brought to you by Vitamin Water.
[00:25:07.540 --> 00:25:10.540]   Living in a big city like Cincinnati has challenges.
[00:25:10.540 --> 00:25:11.900]   That's so big, man.
[00:25:11.900 --> 00:25:13.500]   LA's big like Cincinnati.
[00:25:13.500 --> 00:25:18.200]   Thankfully, Vitamin Water's new zero-sugar flavors like rehydrate pineapple passion fruit,
[00:25:18.340 --> 00:25:23.120]   squeezed lemonade, and XXX acai blueberry pomegranate are packed with essential vitamins
[00:25:23.120 --> 00:25:25.700]   for when you're traveling across that big city.
[00:25:25.700 --> 00:25:29.040]   So whatever the reason, grab a Vitamin Water today.
[00:25:29.040 --> 00:25:30.880]   Copyright 2025, Glasso.
[00:25:30.880 --> 00:25:33.540]   Vitamin Water is a registered trademark of Glasso.
[00:25:33.540 --> 00:25:37.520]   Next topic here.
[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,

**Example 6 Output:**
00:24:28.320-00:25:03.460
00:25:03.460-00:25:30.880

---
**Example 7 Input:**
[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
[00:14:05.240 --> 00:14:10.400]   maybe email would be the replacement for slack and all it all comes full circle
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[00:15:41.800 --> 00:15:47.100]   touch screen device and the all new liquid video engine that lunar display have developed brings
[00:15:47.100 --> 00:15:53.180]   significantly reduced latency and faster screen refresh rate than ever before i use lunar display
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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
[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

**Example 7 Output:**
00:14:17.500-00:17:05.040

---
**Example 8 Input:**
[00:27:11.039 --> 00:27:14.480] Why aren't stadium seats just toilets?
[00:27:14.720 --> 00:27:19.839] Like the Romans of old, like the ancient Greek bathhouse.
[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.
[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.
[00:27:37.440 --> 00:27:38.480] Oh, I gotta go to the bathroom.

**Example 8 Output:**

"""

# --- Helper Functions ---
def seconds_to_timestamp(total_seconds: float) -> str:
    """Converts total seconds to HH:MM:SS.mmm string."""
    if total_seconds < 0:
        total_seconds = 0
    integer_seconds = int(total_seconds)
    milliseconds = int((total_seconds - integer_seconds) * 1000)
    td = timedelta(seconds=integer_seconds)
    hours, remainder = divmod(td.seconds, 3600)
    minutes, seconds = divmod(remainder, 60)
    return f"{hours:02}:{minutes:02}:{int(seconds):02}.{milliseconds:03}"

def timestamp_to_seconds(ts_str: str) -> float:
    """Converts HH:MM:SS.mmm string to total seconds."""
    parts = ts_str.split(':')
    hours = int(parts[0])
    minutes = int(parts[1])
    seconds_ms_str = parts[2].split('.')
    seconds = int(seconds_ms_str[0])
    milliseconds = int(seconds_ms_str[1])
    return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000.0

def segment_by_punctuation(asr_results: list, min_words_per_segment: int = 3) -> list[dict]:
    """Converts ASR results into sentence-level segments based on punctuation."""
    segments = []
    punctuations = {".", "?", "!"}
    for result in asr_results:
        token_map = result.get("token_map", [])
        if not token_map:
            continue

        tokens = [pair[0] for pair in token_map]
        timestamps = [pair[1] for pair in token_map]

        current_segment_start_idx = 0
        for i, token in enumerate(tokens):
            is_end_of_sentence = any(p in token.strip() for p in punctuations)
            
            if (is_end_of_sentence or (i == len(tokens) - 1)) and len(tokens) > current_segment_start_idx:
                segment_tokens = tokens[current_segment_start_idx : i + 1]

                if len(segment_tokens) >= min_words_per_segment:
                    start_time = timestamps[current_segment_start_idx]
                    end_time = timestamps[i]
                    text = "".join(segment_tokens).strip()

                    segments.append({
                        "start_time": start_time,
                        "end_time": end_time,
                        "text": text
                    })

                current_segment_start_idx = i + 1
    return segments

def chunk_segments(segments: list[dict], chunk_duration_seconds: int = 240, overlap_seconds: int = 60) -> list[list[dict]]:
    """Groups sentence-level segments into fixed-size, overlapping chunks."""
    if not segments:
        return []

    chunks = []
    step_seconds = chunk_duration_seconds - overlap_seconds
    if step_seconds <= 0:
        raise ValueError("Overlap must be smaller than chunk duration.")

    transcript_end_time = segments[-1]['end_time']
    window_start_time = 0.0
    while window_start_time < transcript_end_time:
        window_end_time = window_start_time + chunk_duration_seconds
        current_chunk_segments = [s for s in segments if s['start_time'] < window_end_time and s['end_time'] > window_start_time]
        if current_chunk_segments:
            chunks.append(current_chunk_segments)
        window_start_time += step_seconds
    return chunks

def format_chunk_for_prompt(chunk: list[dict]) -> str:
    """Formats a chunk of segments into the final plain text prompt format."""
    return "\n".join(
        f"[{seconds_to_timestamp(s['start_time'])} --> {seconds_to_timestamp(s['end_time'])}] {s['text']}"
        for s in chunk
    )

@backoff.on_exception(backoff.expo, (openai.RateLimitError, openai.InternalServerError), max_tries=8)
def call_api_with_backoff(**kwargs):
    """Makes a completions call with exponential backoff."""
    return client.chat.completions.create(**kwargs)

# --- Main Processing Logic ---

def process_transcript_json(input_json_path: str, output_jsonl_path: str):
    """Loads, segments, chunks, and labels a transcript JSON file."""
    print(f"Processing {input_json_path}...")
    
    # 1. Load the raw ASR output
    with open(input_json_path, 'r', encoding='utf-8') as f:
        asr_data = json.load(f)

    # 2. Convert token-level data into sentence-level segments
    sentence_segments = segment_by_punctuation(asr_data)
    if not sentence_segments:
        print("No segments found after processing. Exiting.")
        return

    # 3. Group sentences into overlapping chunks for the LLM
    transcript_chunks = chunk_segments(sentence_segments)

    final_results = []
    for i, chunk in enumerate(tqdm(transcript_chunks, desc="Labeling Chunks")):
        # 4. Format the chunk into the plain text required by the prompt
        prompt_text = format_chunk_for_prompt(chunk)

        try:
            response = call_api_with_backoff(
                model=MODEL,
                messages=[
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": f"Input Transcript:\n{prompt_text}"}
                ],
                temperature=0.0,
                max_tokens=1024,
            )

            if response and response.choices:
                response_text = response.choices[0].message.content.strip()
            else:
                response_text = ""
        except Exception as e:
            print(f"\nAPI call failed for chunk {i}: {e}")
            continue

        if response_text.startswith("[") or "no output" in response_text.lower() or "no ad" in response_text.lower():
            response_text = ""

        
        # Format for the fine-tuning dataset
        prompt_id = os.path.splitext(os.path.basename(input_json_path))[0] + f"_chunk{i}"
        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}"
        
        final_results.append({
            "id": prompt_id,
            "text": full_prompt_for_finetuning,
            "target": response_text.strip()
        })

    # 5. Save results to a .jsonl file
    with open(output_jsonl_path, "w", encoding="utf-8") as f:
        for result in final_results:
            f.write(json.dumps(result) + "\n")
    
    print(f"Finished processing. Saved {len(final_results)} labeled chunks to {output_jsonl_path}")

def main():
    if len(sys.argv) < 3:
        print("Usage: python label_generator.py <input_transcript.json> <output_labels.jsonl>")
        sys.exit(1)
        
    input_path = sys.argv[1]
    output_path = sys.argv[2]
    
    if os.path.exists(output_path):
        print(f"Output file {output_path} already exists. Skipping.")
        return
        
    process_transcript_json(input_path, output_path)

if __name__ == "__main__":
    main()