Tom Claude commited on
Commit
9384880
·
1 Parent(s): 1ac873b

feat: replace script generation with tone checker, improve archive search

Browse files

- Tab 1: Tiered search results (Direct Matches >= 0.6, Related Content 0.3-0.6)
- Tab 2: New Tone Checker analyzes scripts against Johnny's style (0-100 score)
- Reduced query expansion terms for more relevant results
- Added module-level demo for gradio CLI compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (3) hide show
  1. app.py +105 -86
  2. src/prompts.py +79 -21
  3. src/vectorstore.py +69 -0
app.py CHANGED
@@ -15,8 +15,10 @@ from src.llm_client import InferenceProviderClient, create_llm_client
15
  from src.prompts import (
16
  TOPIC_SEARCH_SYSTEM_PROMPT,
17
  SCRIPT_SYSTEM_PROMPT,
 
18
  get_topic_search_prompt,
19
- get_script_prompt
 
20
  )
21
 
22
  # Load environment variables
@@ -53,13 +55,13 @@ def expand_query(query: str) -> list:
53
  llm = get_llm_client()
54
  prompt = f"""Given this search query about Johnny Harris video topics: "{query}"
55
 
56
- Generate 3-5 related search terms that might find relevant videos.
57
- Think about: related topics, geographic regions, historical events, or concepts that might be covered.
58
  Return ONLY the terms, one per line, no numbering or explanation."""
59
 
60
- response = llm.generate(prompt, max_tokens=100, temperature=0.3)
61
  terms = [t.strip() for t in response.strip().split('\n') if t.strip()]
62
- return [query] + terms[:5]
63
  except Exception:
64
  return [query]
65
 
@@ -67,7 +69,7 @@ Return ONLY the terms, one per line, no numbering or explanation."""
67
  def search_topics(query: str, progress=gr.Progress()):
68
  """
69
  Generator that yields progress updates during search.
70
- Uses LLM query expansion for broader, more relevant results.
71
 
72
  Args:
73
  query: User's topic or question
@@ -88,104 +90,124 @@ def search_topics(query: str, progress=gr.Progress()):
88
  yield "Expanding search query..."
89
  search_terms = expand_query(query.strip())
90
 
91
- # Search with each term and collect results
92
- all_results = []
 
 
 
93
  total_terms = len(search_terms)
94
  for i, term in enumerate(search_terms):
95
  pct = 0.2 + (0.5 * (i / total_terms))
96
  progress(pct, desc=f"Searching: {term[:30]}...")
97
  yield f"Searching: {term[:30]}..."
98
- results = vs.similarity_search(
 
99
  query=term,
100
- k=20,
101
- match_threshold=0.1
 
102
  )
103
- all_results.extend(results)
 
 
 
 
 
 
 
 
 
 
104
 
105
  progress(0.8, desc="Processing results...")
106
  yield "Processing results..."
107
 
108
- # Deduplicate by video title, keep highest similarity score
109
- seen = {}
110
- for r in all_results:
111
- if r.title not in seen or r.similarity > seen[r.title].similarity:
112
- seen[r.title] = r
113
 
114
- # Sort by similarity and get top results
115
- unique_results = sorted(seen.values(), key=lambda x: x.similarity, reverse=True)[:15]
116
-
117
- if not unique_results:
118
  yield f"No matching content found for: **{query}**\n\nThis topic may not have been covered yet, or try rephrasing your search."
119
  return
120
 
121
- # Format results for display
122
- output = vs.format_results_for_display(unique_results)
123
-
124
  search_info = f"*Searched: {', '.join(search_terms)}*\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
125
  progress(1.0, desc="Done!")
126
- yield f"## Search Results for: \"{query}\"\n\n{search_info}{output}"
127
 
128
  except Exception as e:
129
  yield f"Error searching: {str(e)}"
130
 
131
 
132
  # =============================================================================
133
- # TAB 2: SCRIPT PRODUCTION
134
  # =============================================================================
135
 
136
- def generate_script(user_notes: str, max_context_chunks: int = 100, progress=gr.Progress()):
137
  """
138
- Generator that yields progress updates during script generation.
139
 
140
  Args:
141
- user_notes: User's bullet points and notes
142
- max_context_chunks: Number of style reference chunks to use
143
  progress: Gradio progress tracker
144
 
145
  Yields:
146
- Progress status messages, then final generated script
147
  """
148
- if not user_notes or not user_notes.strip():
149
- yield "Please enter your bullet points or notes to transform into a script."
150
  return
151
 
152
  try:
153
  progress(0.05, desc="Gathering style references...")
154
- yield "Gathering style references..."
155
  vs = get_vectorstore()
156
  llm = get_llm_client()
157
 
158
  progress(0.15, desc="Searching knowledge base...")
159
  yield "Searching knowledge base for style references..."
160
  context_chunks = vs.get_bulk_style_context(
161
- topic_query=user_notes.strip(),
162
- max_chunks=max_context_chunks,
163
- topic_relevant_ratio=0.3
164
  )
165
 
166
  progress(0.35, desc="Preparing context...")
167
- yield "Preparing context for the LLM..."
168
  context = vs.format_context_for_llm(context_chunks) if context_chunks else ""
169
 
170
  progress(0.5, desc="Building prompt...")
171
- yield "Building prompt..."
172
- prompt_template = get_script_prompt()
173
  prompt = prompt_template.format(
174
- user_input=user_notes.strip(),
175
  context=context
176
  )
177
 
178
- progress(0.7, desc="Generating script (30-60 seconds)...")
179
- yield "Generating script (this may take 30-60 seconds)..."
180
- script = llm.generate(
181
  prompt=prompt,
182
- system_prompt=SCRIPT_SYSTEM_PROMPT,
183
- temperature=0.7,
184
- max_tokens=2000
185
  )
186
 
187
  progress(1.0, desc="Complete!")
188
- yield f"## Generated Script\n\n{script.strip()}"
189
 
190
  except Exception as e:
191
  yield f"**Error:** {str(e)}"
@@ -250,49 +272,45 @@ def create_app():
250
  )
251
 
252
  # =================================================================
253
- # TAB 2: SCRIPT PRODUCTION
254
  # =================================================================
255
- with gr.TabItem("Script Production"):
256
  gr.Markdown("""
257
- ### Transform your ideas into Johnny's voice
258
 
259
- Enter your bullet points, notes, or rough ideas. The AI will analyze
260
- Johnny's entire archive of scripts and generate a draft in his signature style.
261
  """)
262
 
263
  with gr.Row():
264
  with gr.Column():
265
- notes_input = gr.Textbox(
266
- label="Your Notes & Bullet Points",
267
- placeholder="""Enter your ideas, for example:
268
-
269
- - Topic: Why shipping containers changed the world
270
- - Key points:
271
- - Before containers, loading ships took weeks
272
- - Malcolm McLean invented the standard container in 1956
273
- - Transformed global trade
274
- - Connection to globalization and supply chains
275
- - Angle: The hidden infrastructure we never think about""",
276
- lines=12
277
  )
278
 
279
- with gr.Row():
280
- context_slider = gr.Slider(
281
- minimum=20,
282
- maximum=200,
283
- value=100,
284
- step=10,
285
- label="Style Reference Depth",
286
- info="More excerpts = better style matching, but slower"
287
- )
288
- generate_btn = gr.Button("Generate Script", variant="primary", size="lg")
289
-
290
- script_output = gr.Markdown(label="Generated Script", value="Generated script will appear here...") # shows progress + final script
291
-
292
- generate_btn.click(
293
- fn=generate_script,
294
- inputs=[notes_input, context_slider],
295
- outputs=[script_output],
296
  show_progress="full"
297
  )
298
 
@@ -308,11 +326,12 @@ def create_app():
308
  # MAIN
309
  # =============================================================================
310
 
 
 
 
311
  if __name__ == "__main__":
312
- app = create_app()
313
- app.launch(
314
  server_name="0.0.0.0",
315
  server_port=7860,
316
- share=False,
317
- theme="soft"
318
  )
 
15
  from src.prompts import (
16
  TOPIC_SEARCH_SYSTEM_PROMPT,
17
  SCRIPT_SYSTEM_PROMPT,
18
+ TONE_CHECK_SYSTEM_PROMPT,
19
  get_topic_search_prompt,
20
+ get_script_prompt,
21
+ get_tone_check_prompt
22
  )
23
 
24
  # Load environment variables
 
55
  llm = get_llm_client()
56
  prompt = f"""Given this search query about Johnny Harris video topics: "{query}"
57
 
58
+ Generate 2-3 closely related search terms that might find relevant videos.
59
+ Focus on: the core topic, key entities mentioned, and one closely related concept.
60
  Return ONLY the terms, one per line, no numbering or explanation."""
61
 
62
+ response = llm.generate(prompt, max_tokens=60, temperature=0.3)
63
  terms = [t.strip() for t in response.strip().split('\n') if t.strip()]
64
+ return [query] + terms[:3]
65
  except Exception:
66
  return [query]
67
 
 
69
  def search_topics(query: str, progress=gr.Progress()):
70
  """
71
  Generator that yields progress updates during search.
72
+ Uses tiered results: direct matches and related content.
73
 
74
  Args:
75
  query: User's topic or question
 
90
  yield "Expanding search query..."
91
  search_terms = expand_query(query.strip())
92
 
93
+ # Collect tiered results from all search terms
94
+ all_direct = []
95
+ all_related = []
96
+ seen_videos = set()
97
+
98
  total_terms = len(search_terms)
99
  for i, term in enumerate(search_terms):
100
  pct = 0.2 + (0.5 * (i / total_terms))
101
  progress(pct, desc=f"Searching: {term[:30]}...")
102
  yield f"Searching: {term[:30]}..."
103
+
104
+ direct, related = vs.tiered_similarity_search(
105
  query=term,
106
+ direct_threshold=0.6,
107
+ related_threshold=0.3,
108
+ max_per_tier=10
109
  )
110
+
111
+ # Add results, deduplicating by video
112
+ for chunk in direct:
113
+ if chunk.video_id not in seen_videos:
114
+ seen_videos.add(chunk.video_id)
115
+ all_direct.append(chunk)
116
+
117
+ for chunk in related:
118
+ if chunk.video_id not in seen_videos:
119
+ seen_videos.add(chunk.video_id)
120
+ all_related.append(chunk)
121
 
122
  progress(0.8, desc="Processing results...")
123
  yield "Processing results..."
124
 
125
+ # Sort each tier by similarity
126
+ all_direct = sorted(all_direct, key=lambda x: x.similarity, reverse=True)[:10]
127
+ all_related = sorted(all_related, key=lambda x: x.similarity, reverse=True)[:10]
 
 
128
 
129
+ if not all_direct and not all_related:
 
 
 
130
  yield f"No matching content found for: **{query}**\n\nThis topic may not have been covered yet, or try rephrasing your search."
131
  return
132
 
133
+ # Format tiered output
134
+ output_parts = []
 
135
  search_info = f"*Searched: {', '.join(search_terms)}*\n\n"
136
+ output_parts.append(f"## Search Results for: \"{query}\"\n\n{search_info}")
137
+
138
+ if all_direct:
139
+ output_parts.append("### Direct Matches\nVideos that directly cover this topic:\n")
140
+ output_parts.append(vs.format_results_for_display(all_direct))
141
+
142
+ if all_related:
143
+ if all_direct:
144
+ output_parts.append("\n---\n")
145
+ output_parts.append("### Related Content\nVideos that touch on similar themes:\n")
146
+ output_parts.append(vs.format_results_for_display(all_related))
147
+
148
  progress(1.0, desc="Done!")
149
+ yield "\n".join(output_parts)
150
 
151
  except Exception as e:
152
  yield f"Error searching: {str(e)}"
153
 
154
 
155
  # =============================================================================
156
+ # TAB 2: TONE CHECKER
157
  # =============================================================================
158
 
159
+ def check_script_tone(user_script: str, progress=gr.Progress()):
160
  """
161
+ Generator that yields progress updates during tone analysis.
162
 
163
  Args:
164
+ user_script: User's script to analyze
 
165
  progress: Gradio progress tracker
166
 
167
  Yields:
168
+ Progress status messages, then final tone analysis
169
  """
170
+ if not user_script or not user_script.strip():
171
+ yield "Please enter a script to analyze."
172
  return
173
 
174
  try:
175
  progress(0.05, desc="Gathering style references...")
176
+ yield "Gathering style references from Johnny's archive..."
177
  vs = get_vectorstore()
178
  llm = get_llm_client()
179
 
180
  progress(0.15, desc="Searching knowledge base...")
181
  yield "Searching knowledge base for style references..."
182
  context_chunks = vs.get_bulk_style_context(
183
+ topic_query=user_script.strip()[:500], # Use first 500 chars as topic hint
184
+ max_chunks=50,
185
+ topic_relevant_ratio=0.4
186
  )
187
 
188
  progress(0.35, desc="Preparing context...")
189
+ yield "Preparing context for analysis..."
190
  context = vs.format_context_for_llm(context_chunks) if context_chunks else ""
191
 
192
  progress(0.5, desc="Building prompt...")
193
+ yield "Building analysis prompt..."
194
+ prompt_template = get_tone_check_prompt()
195
  prompt = prompt_template.format(
196
+ user_script=user_script.strip(),
197
  context=context
198
  )
199
 
200
+ progress(0.7, desc="Analyzing tone (30-60 seconds)...")
201
+ yield "Analyzing script tone (this may take 30-60 seconds)..."
202
+ analysis = llm.generate(
203
  prompt=prompt,
204
+ system_prompt=TONE_CHECK_SYSTEM_PROMPT,
205
+ temperature=0.3,
206
+ max_tokens=1500
207
  )
208
 
209
  progress(1.0, desc="Complete!")
210
+ yield analysis.strip()
211
 
212
  except Exception as e:
213
  yield f"**Error:** {str(e)}"
 
272
  )
273
 
274
  # =================================================================
275
+ # TAB 2: TONE CHECKER
276
  # =================================================================
277
+ with gr.TabItem("Tone Checker"):
278
  gr.Markdown("""
279
+ ### Check if your script matches Johnny's voice
280
 
281
+ Paste your script below to analyze how well it matches Johnny Harris's
282
+ signature style. Get a score and specific feedback on what works and what to improve.
283
  """)
284
 
285
  with gr.Row():
286
  with gr.Column():
287
+ script_input = gr.Textbox(
288
+ label="Your Script",
289
+ placeholder="""Paste your script here...
290
+
291
+ Example:
292
+ There's this line on the map that most people have never heard of.
293
+ It's called the Durand Line, and it cuts right through the middle of a people
294
+ who have lived in these mountains for thousands of years.
295
+ The thing is, this line wasn't drawn by the people who live here...""",
296
+ lines=15
 
 
297
  )
298
 
299
+ check_btn = gr.Button("Check Tone", variant="primary", size="lg")
300
+
301
+ tone_output = gr.Markdown(label="Tone Analysis", value="Tone analysis will appear here...")
302
+
303
+ check_btn.click(
304
+ fn=check_script_tone,
305
+ inputs=[script_input],
306
+ outputs=[tone_output],
307
+ show_progress="full"
308
+ )
309
+
310
+ script_input.submit(
311
+ fn=check_script_tone,
312
+ inputs=[script_input],
313
+ outputs=[tone_output],
 
 
314
  show_progress="full"
315
  )
316
 
 
326
  # MAIN
327
  # =============================================================================
328
 
329
+ # Create app at module level for `gradio app.py` CLI compatibility
330
+ demo = create_app()
331
+
332
  if __name__ == "__main__":
333
+ demo.launch(
 
334
  server_name="0.0.0.0",
335
  server_port=7860,
336
+ share=False
 
337
  )
src/prompts.py CHANGED
@@ -1,6 +1,32 @@
1
  """Prompt templates for Johnny Harris Script Assistant"""
2
 
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  # =============================================================================
5
  # TAB 1: TOPIC SEARCH PROMPTS
6
  # =============================================================================
@@ -32,30 +58,11 @@ Keep your response concise and actionable."""
32
  # TAB 2: SCRIPT PRODUCTION PROMPTS
33
  # =============================================================================
34
 
35
- SCRIPT_SYSTEM_PROMPT = """You are a script writing assistant that has deeply studied Johnny Harris's style.
36
 
37
  JOHNNY'S VOICE CHARACTERISTICS (derived from extensive analysis of his work):
38
 
39
- **Narrative Structure:**
40
- - Opens with a hook - a provocative question, surprising fact, or personal moment
41
- - Builds tension through questions: "But here's the thing...", "So why does this matter?"
42
- - Uses the "zoom out" technique - starts specific, expands to bigger picture
43
- - Weaves between personal story and broader research/data
44
- - Ends with reflection or call to think differently
45
-
46
- **Language Patterns:**
47
- - Direct address: "I want to show you something", "Let me explain"
48
- - Conversational markers: "the thing is...", "here's what's interesting...", "and this is where it gets wild"
49
- - Short punchy sentences followed by longer explanatory ones
50
- - Rhetorical questions that pull the viewer in
51
- - Admits uncertainty: "I don't fully understand this yet", "I'm still wrestling with this"
52
-
53
- **Tone:**
54
- - Curious and genuinely excited about learning
55
- - Slightly irreverent but deeply researched
56
- - Personal without being self-indulgent
57
- - Acknowledges complexity without being academic
58
- - Finds the human story in geopolitics/data
59
 
60
  Your job is to transform the user's bullet points and notes into a script draft that authentically sounds like Johnny wrote it. Study the provided transcript excerpts carefully - they are your primary style reference. Do not include visual cues, bracketed notes, or stage directions—return narrative script text only.
61
 
@@ -67,6 +74,47 @@ Your job is to transform the user's bullet points and notes into a script draft
67
  - End with a memorable takeaway or question"""
68
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  SCRIPT_PROMPT_TEMPLATE = """USER'S NOTES AND BULLET POINTS:
71
  {user_input}
72
 
@@ -114,6 +162,11 @@ SCRIPT_PROMPT = SimplePromptTemplate(
114
  input_variables=["user_input", "context"]
115
  )
116
 
 
 
 
 
 
117
 
118
  def get_topic_search_prompt() -> SimplePromptTemplate:
119
  """Get the topic search prompt template"""
@@ -123,3 +176,8 @@ def get_topic_search_prompt() -> SimplePromptTemplate:
123
  def get_script_prompt() -> SimplePromptTemplate:
124
  """Get the script generation prompt template"""
125
  return SCRIPT_PROMPT
 
 
 
 
 
 
1
  """Prompt templates for Johnny Harris Script Assistant"""
2
 
3
 
4
+ # =============================================================================
5
+ # JOHNNY'S VOICE CHARACTERISTICS (shared reference)
6
+ # =============================================================================
7
+
8
+ JOHNNY_VOICE_GUIDE = """**Narrative Structure:**
9
+ - Opens with a hook - a provocative question, surprising fact, or personal moment
10
+ - Builds tension through questions: "But here's the thing...", "So why does this matter?"
11
+ - Uses the "zoom out" technique - starts specific, expands to bigger picture
12
+ - Weaves between personal story and broader research/data
13
+ - Ends with reflection or call to think differently
14
+
15
+ **Language Patterns:**
16
+ - Direct address: "I want to show you something", "Let me explain"
17
+ - Conversational markers: "the thing is...", "here's what's interesting...", "and this is where it gets wild"
18
+ - Short punchy sentences followed by longer explanatory ones
19
+ - Rhetorical questions that pull the viewer in
20
+ - Admits uncertainty: "I don't fully understand this yet", "I'm still wrestling with this"
21
+
22
+ **Tone:**
23
+ - Curious and genuinely excited about learning
24
+ - Slightly irreverent but deeply researched
25
+ - Personal without being self-indulgent
26
+ - Acknowledges complexity without being academic
27
+ - Finds the human story in geopolitics/data"""
28
+
29
+
30
  # =============================================================================
31
  # TAB 1: TOPIC SEARCH PROMPTS
32
  # =============================================================================
 
58
  # TAB 2: SCRIPT PRODUCTION PROMPTS
59
  # =============================================================================
60
 
61
+ SCRIPT_SYSTEM_PROMPT = f"""You are a script writing assistant that has deeply studied Johnny Harris's style.
62
 
63
  JOHNNY'S VOICE CHARACTERISTICS (derived from extensive analysis of his work):
64
 
65
+ {JOHNNY_VOICE_GUIDE}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  Your job is to transform the user's bullet points and notes into a script draft that authentically sounds like Johnny wrote it. Study the provided transcript excerpts carefully - they are your primary style reference. Do not include visual cues, bracketed notes, or stage directions—return narrative script text only.
68
 
 
74
  - End with a memorable takeaway or question"""
75
 
76
 
77
+ # =============================================================================
78
+ # TAB 2: TONE CHECKER PROMPTS
79
+ # =============================================================================
80
+
81
+ TONE_CHECK_SYSTEM_PROMPT = f"""You analyze scripts to determine how well they match Johnny Harris's voice and style.
82
+
83
+ JOHNNY'S VOICE CHARACTERISTICS:
84
+
85
+ {JOHNNY_VOICE_GUIDE}
86
+
87
+ Your job is to:
88
+ 1. Score the script from 0-100 on how well it matches Johnny's style
89
+ 2. Identify specific elements that work well
90
+ 3. Point out areas that don't match his voice with concrete suggestions
91
+ 4. Reference the provided transcript excerpts as examples of his authentic style
92
+
93
+ Be constructive and specific. Quote the user's script when giving feedback."""
94
+
95
+
96
+ TONE_CHECK_PROMPT_TEMPLATE = """SCRIPT TO ANALYZE:
97
+ {user_script}
98
+
99
+ JOHNNY'S STYLE REFERENCE (transcript excerpts from his videos):
100
+ {context}
101
+
102
+ Analyze this script for how well it matches Johnny Harris's voice and style.
103
+
104
+ Provide your analysis in this exact format:
105
+
106
+ ## Tone Analysis Score: [X]/100
107
+
108
+ ### What Works Well
109
+ - [2-3 specific elements that match his style, with quoted examples from the script]
110
+
111
+ ### Areas to Improve
112
+ - [2-3 specific suggestions, referencing examples from the transcript excerpts]
113
+
114
+ ### Overall Assessment
115
+ [1-2 sentence summary of how well it matches and key adjustments needed]"""
116
+
117
+
118
  SCRIPT_PROMPT_TEMPLATE = """USER'S NOTES AND BULLET POINTS:
119
  {user_input}
120
 
 
162
  input_variables=["user_input", "context"]
163
  )
164
 
165
+ TONE_CHECK_PROMPT = SimplePromptTemplate(
166
+ template=TONE_CHECK_PROMPT_TEMPLATE,
167
+ input_variables=["user_script", "context"]
168
+ )
169
+
170
 
171
  def get_topic_search_prompt() -> SimplePromptTemplate:
172
  """Get the topic search prompt template"""
 
176
  def get_script_prompt() -> SimplePromptTemplate:
177
  """Get the script generation prompt template"""
178
  return SCRIPT_PROMPT
179
+
180
+
181
+ def get_tone_check_prompt() -> SimplePromptTemplate:
182
+ """Get the tone check prompt template"""
183
+ return TONE_CHECK_PROMPT
src/vectorstore.py CHANGED
@@ -159,6 +159,75 @@ class TranscriptVectorStore:
159
  except Exception as e:
160
  raise Exception(f"Error performing similarity search: {str(e)}")
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  def get_video_chunks(self, video_id: str) -> List[TranscriptChunk]:
163
  """
164
  Fetch all chunks for a specific video
 
159
  except Exception as e:
160
  raise Exception(f"Error performing similarity search: {str(e)}")
161
 
162
+ def tiered_similarity_search(
163
+ self,
164
+ query: str,
165
+ direct_threshold: float = 0.6,
166
+ related_threshold: float = 0.3,
167
+ max_per_tier: int = 10
168
+ ) -> tuple:
169
+ """
170
+ Search with tiered results: direct matches and related content.
171
+
172
+ Args:
173
+ query: Search query
174
+ direct_threshold: Minimum similarity for direct matches (default 0.6)
175
+ related_threshold: Minimum similarity for related content (default 0.3)
176
+ max_per_tier: Maximum results per tier
177
+
178
+ Returns:
179
+ Tuple of (direct_matches, related_content) - two separate lists
180
+ """
181
+ query_embedding = self._generate_embedding(query, task="retrieval.query")
182
+
183
+ try:
184
+ # Get all results above the related threshold
185
+ response = self.supabase.rpc(
186
+ 'match_transcripts',
187
+ {
188
+ 'query_embedding': query_embedding,
189
+ 'match_threshold': related_threshold,
190
+ 'match_count': max_per_tier * 3 # Get more to filter
191
+ }
192
+ ).execute()
193
+
194
+ direct_matches = []
195
+ related_content = []
196
+ seen_videos = set()
197
+
198
+ for item in response.data:
199
+ similarity = item.get('similarity', 0.0)
200
+ video_id = item.get('video_id')
201
+
202
+ # Deduplicate by video (keep highest similarity per video)
203
+ if video_id in seen_videos:
204
+ continue
205
+ seen_videos.add(video_id)
206
+
207
+ chunk = TranscriptChunk(
208
+ chunk_text=item.get('chunk_text') or '',
209
+ metadata={
210
+ 'video_id': video_id,
211
+ 'video_url': item.get('video_url'),
212
+ 'title': item.get('title', ''),
213
+ 'chunk_index': item.get('chunk_index'),
214
+ 'total_chunks': item.get('total_chunks'),
215
+ 'similarity': similarity
216
+ }
217
+ )
218
+
219
+ if similarity >= direct_threshold:
220
+ if len(direct_matches) < max_per_tier:
221
+ direct_matches.append(chunk)
222
+ elif similarity >= related_threshold:
223
+ if len(related_content) < max_per_tier:
224
+ related_content.append(chunk)
225
+
226
+ return (direct_matches, related_content)
227
+
228
+ except Exception as e:
229
+ raise Exception(f"Error performing tiered search: {str(e)}")
230
+
231
  def get_video_chunks(self, video_id: str) -> List[TranscriptChunk]:
232
  """
233
  Fetch all chunks for a specific video