summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.py292
-rw-r--r--transcript.py42
2 files changed, 334 insertions, 0 deletions
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..27b588c
--- /dev/null
+++ b/main.py
@@ -0,0 +1,292 @@
1import re
2import sys
3from openai import OpenAI
4from datetime import datetime, timedelta
5
6def parse_whisper_transcript(transcript_text: str) -> list[dict]:
7 """
8 Parses a Whisper-generated transcript into a list of dictionaries,
9 each containing 'start_time', 'end_time', and 'text'.
10 """
11 segments = []
12 # Regex to match timestamp format [HH:MM:SS.mmm --> HH:MM:SS.mmm] and the following text
13 # It now correctly handles the leading space before text
14 pattern = re.compile(r"^\[(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})\]\s*(.*)$", re.MULTILINE)
15
16 for line in transcript_text.strip().split('\n'):
17 match = pattern.match(line)
18 if match:
19 start_time, end_time, text = match.groups()
20 segments.append({
21 "start_time": start_time,
22 "end_time": end_time,
23 "text": text.strip()
24 })
25 return segments
26
27def format_transcript_segment(segments: list[dict]) -> str:
28 """Formats a list of segment dictionaries back into the prompt-friendly string."""
29 formatted_lines = []
30 for s in segments:
31 formatted_lines.append(f"[{s['start_time']} --> {s['end_time']}] {s['text']}")
32 return "\n".join(formatted_lines)
33
34def timestamp_to_seconds(ts_str: str) -> float:
35 """Converts HH:MM:SS.mmm string to total seconds."""
36 parts = ts_str.split(':')
37 hours = int(parts[0])
38 minutes = int(parts[1])
39 seconds_ms_str = parts[2].split('.')
40 seconds = int(seconds_ms_str[0])
41 milliseconds = int(seconds_ms_str[1])
42 return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000.0
43
44def seconds_to_timestamp(total_seconds: float) -> str:
45 """Converts total seconds to HH:MM:SS.mmm string."""
46 # Split total_seconds into integer seconds and fractional milliseconds
47 integer_seconds = int(total_seconds)
48 milliseconds = int((total_seconds - integer_seconds) * 1000)
49
50 td = timedelta(seconds=integer_seconds)
51 hours, remainder = divmod(td.total_seconds(), 3600)
52 minutes, seconds = divmod(remainder, 60)
53
54 return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}.{milliseconds:03}"
55
56def chunk_transcript(
57 segments: list[dict],
58 trigger_phrase: str = "brought to you by",
59 max_chunk_duration_minutes: int = 4 # New parameter for max duration
60) -> list[list[dict]]:
61 """
62 Chunks the transcript segments based on occurrences of a trigger phrase,
63 or a maximum time duration, whichever comes first.
64 Each chunk starts at a trigger phrase and ends just before the next,
65 or when max_chunk_duration_minutes is reached, or at the end of transcript.
66 """
67 chunks = []
68 current_chunk_start_idx = 0
69 max_chunk_duration_seconds = max_chunk_duration_minutes * 60
70
71 # Ensure there are segments to process
72 if not segments:
73 return []
74
75 for i, segment in enumerate(segments):
76 is_trigger_segment = trigger_phrase.lower() in segment["text"].lower()
77
78 # Get the start time of the current logical chunk being built
79 current_chunk_start_time_seconds = timestamp_to_seconds(segments[current_chunk_start_idx]['start_time'])
80
81 # Get the end time of the current segment being considered for inclusion
82 current_segment_end_time_seconds = timestamp_to_seconds(segment['end_time'])
83
84 # Calculate the duration if this segment were to be the end of the current chunk
85 current_chunk_duration = current_segment_end_time_seconds - current_chunk_start_time_seconds
86
87 # Condition 1: New trigger phrase found (and it's not the start of the very first chunk)
88 # We also need to ensure the trigger is not the first segment of the current chunk,
89 # otherwise it would create an empty chunk.
90 if is_trigger_segment and (i > current_chunk_start_idx):
91 # End the previous chunk *before* this new trigger segment
92 chunks.append(segments[current_chunk_start_idx:i])
93 current_chunk_start_idx = i # Start a new chunk from this trigger segment
94 continue # Move to the next segment after processing the split
95
96 # Condition 2: Max duration exceeded
97 # This check should happen for any segment after the start of a chunk
98 if current_chunk_duration >= max_chunk_duration_seconds and i > current_chunk_start_idx:
99 # End the current chunk *at* the current segment
100 chunks.append(segments[current_chunk_start_idx:i+1])
101 current_chunk_start_idx = i + 1 # Start a new chunk from the next segment
102 # We don't 'continue' here, as the new chunk needs to start and the loop will increment 'i'
103
104 # Add the last chunk if any segments remain. This covers the case where the transcript
105 # ends without another trigger or exceeding the time limit.
106 if current_chunk_start_idx < len(segments):
107 chunks.append(segments[current_chunk_start_idx:])
108
109 return chunks
110
111def call_openai_api(client: OpenAI, chunk_text: str, system_prompt: str) -> str:
112 """
113 Makes an API call to OpenAI with the given system prompt and transcript chunk.
114 """
115 try:
116 response = client.chat.completions.create(
117 #model="Gemma-3-4B_32K",
118 #model="google/gemini-2.0-flash-exp:free",
119 model="gpt-4.1",
120 messages=[
121 {"role": "system", "content": system_prompt},
122 {"role": "user", "content": f"Input Transcript:\n{chunk_text}"}
123 ],
124 temperature=0.0,
125 )
126 print(response)
127 # Extract content from the first choice's message
128 return response.choices[0].message.content.strip()
129 except Exception as e:
130 print(f"Error calling OpenAI API: {e}")
131 return ""
132
133def main():
134 # --- Configuration ---
135 client = OpenAI(
136 #api_key="none",
137 api_key="REDACTED",
138 #base_url="http://localhost:8080/v1",
139 base_url="https://api.openai.com/v1",
140 )
141
142 with open(sys.argv[1], "r", encoding="utf-8") as f:
143 full_transcript_content = f.read()
144
145 # The system prompt you provided earlier, including examples
146 SYSTEM_PROMPT_TEMPLATE = """
147You 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.
148
149You 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.
150
151**Strict Rules:**
152- Only identify segments that are clearly pre-recorded dynamic ads with a noticeable tone shift.
153- Do NOT include host-read sponsorships that blend naturally with the content. Focus only on the "distracting" pre-recorded elements.
154- 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.
155- Ensure the start and end times correspond precisely to the ad segment in the transcript.
156- Output ONLY the timestamp range(s) in the specified format. No other text, no JSON, no explanations.
157
158**Examples:**
159
160---
161**Example 1 Input:**
162[00:00:00.000 --> 00:00:02.480] This episode is brought to you by Apple Cash.
163[00:00:02.480 --> 00:00:07.040] Sending payments used to be clunky, unnecessarily difficult, and weirdly invasive at times,
164[00:00:07.040 --> 00:00:08.560] until I discovered Apple Cash.
165[00:00:08.560 --> 00:00:11.020] With Apple Cash, payments are private by design,
166[00:00:11.020 --> 00:00:14.900] so I don't have to deal with public feeds, awkward reactions, or other payment drama.
167[00:00:14.900 --> 00:00:18.780] I can send cash in messages right in the conversations I'm already having,
168[00:00:18.780 --> 00:00:20.060] which is super convenient.
169[00:00:20.060 --> 00:00:22.600] There's also a cool feature called Tap to Cash
170[00:00:22.600 --> 00:00:25.900] that lets you pay somebody nearby by holding your iPhone near theirs.
171[00:00:25.900 --> 00:00:28.560] Switch to Apple Cash and start sending privately.
172[00:00:28.620 --> 00:00:31.900] Apple Cash services are provided by Green Dot Bank, member FDIC.
173
174**Example 1 Output:**
17500:00:00.000-00:00:31.900
176
177---
178**Example 2 Input:**
179[00:38:07.480 --> 00:38:10.820] This podcast is brought to you by Carvana.
180[00:38:10.820 --> 00:38:13.660] Got a car to sell, but no time to waste?
181[00:38:13.660 --> 00:38:17.820] Hop on to Carvana.com to get a real offer for your car in seconds.
182[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
183[00:38:23.060 --> 00:38:26.000] the offer, Carvana will pay you as soon as you hand the keys over.
184[00:38:26.360 --> 00:38:29.060] They even offer same day pickup in many cities.
185[00:38:29.060 --> 00:38:35.740] Save your time, score some cash, and sell your car the convenient way to Carvana.
186[00:38:36.360 --> 00:38:37.000] Pickup times vary.
187[00:38:37.000 --> 00:38:37.620] Fees may apply.
188
189**Example 2 Output:**
19000:38:07.480-00:38:37.620
191
192---
193**Example 3 Input:**
194[00:14:46.000 --> 00:14:50.860] This episode is brought to you by Diet Coke.
195[00:14:50.860 --> 00:14:53.800] You know that moment when you just need to hit pause and refresh?
196[00:14:54.380 --> 00:14:56.320] An ice-cold Diet Coke isn't just a break.
197[00:14:56.320 --> 00:14:59.800] It's your chance to catch your breath and savor a moment that's all about you.
198[00:15:00.000 --> 00:15:02.540] Always refreshing, still the same great taste.
199[00:15:02.540 --> 00:15:03.500] Diet Coke.
200[00:15:03.500 --> 00:15:04.860] Make time for you time.
201
202**Example 3 Output:**
20300:14:46.000-00:15:04.860
204
205---
206**Example 4 Input:**
207[00:15:00.000 --> 00:15:20.000] So what are your thoughts on the new policy changes that were just announced?
208[00:15:20.000 --> 00:15:45.000] Well, I think it's a step in the right direction, but there are definitely some areas that need more clarification.
209[00:15:45.000 --> 00:16:10.000] For instance, the section on renewable energy credits could be more explicit.
210[00:16:10.000 --> 00:16:15.000] This next part of our conversation might be a bit technical, but I think it's important to delve into the specifics.
211
212**Example 4 Output:**
213
214
215---
216**Example 5 Input:**
217[00:11:27.080 --> 00:11:28.180] You're going to be using the same thing.
218[00:11:28.180 --> 00:11:32.480] You've got to, like, get outside that bubble or you're just, you're going to bore yourself.
219[00:11:32.480 --> 00:11:40.820] This episode of Cortex is brought to you by Squarespace, the all-in-one website platform designed to help you stand out and succeed online.
220[00:11:40.820 --> 00:11:43.920] Whether you're just getting started or scaling your own business,
221[00:11:43.920 --> 00:11:46.840] Squarespace gives you everything you need to claim your domain,
222[00:11:46.840 --> 00:11:49.380] showcase your offerings of a professional website,
223[00:14:50.000 --> 00:14:52.220] grow your brand, and get paid all in one place.
224[00:14:52.220 --> 00:14:54.960] It's so easy to get started with Squarespace.
225[00:14:54.960 --> 00:14:59.380] In fact, they've made it easier than ever before with their new system, Blueprint AI.
226[00:14:59.380 --> 00:15:05.140] Squarespace's AI-enhanced website builder lets you quickly and easily build a site bespoke to your
227business.
228[00:15:05.140 --> 00:15:08.140] Just input some basic information about your industry and goals.
229[00:15:08.140 --> 00:15:09.480] This isn't the only way.
230[00:15:09.480 --> 00:15:12.200] In fact, you can go in and choose from one of their beautiful templates,
231[00:15:12.200 --> 00:15:14.620] their professionally designed beautiful templates,
232[00:15:14.940 --> 00:15:16.900] and customize it to your heart's content.
233[00:15:16.900 --> 00:15:20.800] One of the things that I've always loved about Squarespace is their drag-and-drop tools,
234[00:15:20.800 --> 00:15:26.440] buttons, sliders, selectors, where you can go in and customize your design for your website
235[00:15:26.440 --> 00:15:29.420] by actually looking at the design for your website while you're doing it.
236[00:15:29.460 --> 00:15:30.620] You don't need to know any code.
237[00:15:30.620 --> 00:15:31.880] I love this.
238[00:13:31.200 --> 00:13:35.220] I want to talk about the other devices that are in your working life.
239[00:13:35.220 --> 00:13:39.060] What are the other computers that you use to get things done?
240[00:13:39.060 --> 00:13:45.000] So there are three computers that live in my life.
241
242**Example 5 Output:**
24300:11:32.480-00:13:31.200
244"""
245 # --- Processing ---
246 print("Parsing full transcript...")
247 parsed_segments = parse_whisper_transcript(full_transcript_content)
248 print(f"Parsed {len(parsed_segments)} segments.")
249
250 print("Chunking transcript...")
251 # Adjust the trigger phrase if "brought to you by" isn't consistently sufficient
252 # For more complex cases, you might use a list of trigger phrases:
253 # ["brought to you by", "a quick word from our sponsor", "we'll be right back"]
254 transcript_chunks = chunk_transcript(parsed_segments, trigger_phrase="brought to you by")
255 print(f"Created {len(transcript_chunks)} chunks.")
256
257 print(transcript_chunks)
258
259 all_ad_timestamps = []
260
261 # Process each chunk with the OpenAI API
262 for i, chunk in enumerate(transcript_chunks):
263 if not chunk:
264 continue
265
266 chunk_text_for_api = format_transcript_segment(chunk)
267 print(f"\nProcessing Chunk {i+1}/{len(transcript_chunks)} (starting at {chunk[0]['start_time']})...")
268
269 # Add the "Your Turn" header before the chunk content
270 user_prompt_content = f"**Your Turn - Input Transcript:**\n{chunk_text_for_api}"
271
272 print(user_prompt_content)
273
274 ad_timestamps_str = call_openai_api(client, user_prompt_content, SYSTEM_PROMPT_TEMPLATE)
275
276 if ad_timestamps_str:
277 # Split by newlines in case the model returns multiple segments
278 found_timestamps = [ts.strip() for ts in ad_timestamps_str.split('\n') if ts.strip()]
279 all_ad_timestamps.extend(found_timestamps)
280 print(f"Found ad timestamps in chunk {i+1}: {', '.join(found_timestamps)}")
281 else:
282 print(f"No ad timestamps found in chunk {i+1}.")
283
284 print("\n--- All Identified Ad Timestamps ---")
285 if all_ad_timestamps:
286 for ts in all_ad_timestamps:
287 print(ts)
288 else:
289 print("No ad segments found in the entire transcript.")
290
291if __name__ == "__main__":
292 main()
diff --git a/transcript.py b/transcript.py
new file mode 100644
index 0000000..e4a63dc
--- /dev/null
+++ b/transcript.py
@@ -0,0 +1,42 @@
1import os
2import subprocess
3import sys
4from tqdm import tqdm
5
6# hardcoded because lazy
7WHISPER_DIR = "/data/src/clones/llm/whisper.cpp"
8WHISPER_MODEL = "ggml-large-v3-turbo-q5_0"
9#WHISPER_MODEL = "ggml-distil-large-v3.5"
10
11def transcribe_file(input_file):
12 base, _ = os.path.splitext(input_file)
13 output_file = base + '.txt'
14
15 if os.path.exists(output_file):
16 return
17
18 whisper_cli = f"{WHISPER_DIR}/build/bin/whisper-cli"
19 model_path = f"{WHISPER_DIR}/models/{WHISPER_MODEL}.bin"
20
21 cmd = [whisper_cli, '-m', model_path, input_file]
22
23 result = subprocess.run(cmd, capture_output=True, text=True)
24 if result.returncode != 0:
25 print(f"Error transcribing {input_file}: {result.stderr}")
26 return
27 with open(output_file, 'w', encoding='utf-8') as f:
28 f.write(result.stdout)
29
30def main():
31 inputs = sys.argv[1:]
32 if not inputs:
33 print("No input files given")
34 return
35
36 with tqdm(inputs) as pbar:
37 for file in pbar:
38 pbar.set_description(f"Transcribing {os.path.basename(file)}")
39 transcribe_file(file)
40
41if __name__ == '__main__':
42 main()