feat(youtube): extract transcript highlights like Reddit comment gems
Add extract_transcript_highlights() that scores sentences by specificity (numbers, proper nouns, topic relevance) and filters YouTube filler (subscribe, welcome back, etc). Top 5 highlights shown as structured bullets in compact output. Full transcript moved to collapsible <details> block so the LLM reads highlights first, full text on demand. SKILL.md updated to instruct the judge agent to quote highlights directly in synthesis, same as Reddit top comments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,7 @@ def normalize_youtube_items(
|
||||
date_confidence="high",
|
||||
engagement=engagement,
|
||||
transcript_snippet=item.get("transcript_snippet", ""),
|
||||
transcript_highlights=item.get("transcript_highlights", []),
|
||||
relevance=item.get("relevance", 0.7),
|
||||
why_relevant=item.get("why_relevant", ""),
|
||||
))
|
||||
|
||||
@@ -257,8 +257,15 @@ def render_compact(report: schema.Report, limit: int = 15, missing_keys: str = "
|
||||
lines.append(f"**{item.id}** (score:{item.score}) {item.channel_name}{date_str}{eng_str}{_xref_tag(item)}")
|
||||
lines.append(f" {item.title}")
|
||||
lines.append(f" {item.url}")
|
||||
if item.transcript_highlights:
|
||||
lines.append(" Highlights:")
|
||||
for hl in item.transcript_highlights[:5]:
|
||||
lines.append(f' - "{hl}"')
|
||||
if item.transcript_snippet:
|
||||
lines.append(f" Transcript: {item.transcript_snippet}")
|
||||
word_count = len(item.transcript_snippet.split())
|
||||
lines.append(f" <details><summary>Full transcript ({word_count} words)</summary>")
|
||||
lines.append(f" {item.transcript_snippet}")
|
||||
lines.append(" </details>")
|
||||
lines.append(f" *{item.why_relevant}*")
|
||||
lines.append("")
|
||||
|
||||
|
||||
@@ -210,6 +210,7 @@ class YouTubeItem:
|
||||
date_confidence: str = "high" # YouTube dates are always reliable
|
||||
engagement: Optional[Engagement] = None
|
||||
transcript_snippet: str = ""
|
||||
transcript_highlights: List[str] = field(default_factory=list)
|
||||
relevance: float = 0.7
|
||||
why_relevant: str = ""
|
||||
subs: SubScores = field(default_factory=SubScores)
|
||||
@@ -226,6 +227,7 @@ class YouTubeItem:
|
||||
'date_confidence': self.date_confidence,
|
||||
'engagement': self.engagement.to_dict() if self.engagement else None,
|
||||
'transcript_snippet': self.transcript_snippet,
|
||||
'transcript_highlights': self.transcript_highlights,
|
||||
'relevance': self.relevance,
|
||||
'why_relevant': self.why_relevant,
|
||||
'subs': self.subs.to_dict(),
|
||||
@@ -655,6 +657,7 @@ class Report:
|
||||
date_confidence=y.get('date_confidence', 'high'),
|
||||
engagement=eng,
|
||||
transcript_snippet=y.get('transcript_snippet', ''),
|
||||
transcript_highlights=y.get('transcript_highlights', []),
|
||||
relevance=y.get('relevance', 0.7),
|
||||
why_relevant=y.get('why_relevant', ''),
|
||||
subs=subs,
|
||||
|
||||
@@ -38,6 +38,52 @@ TRANSCRIPT_MAX_WORDS = 5000
|
||||
from .relevance import token_overlap_relevance as _compute_relevance
|
||||
|
||||
|
||||
def extract_transcript_highlights(transcript: str, topic: str, limit: int = 5) -> List[str]:
|
||||
"""Extract quotable highlights from a YouTube transcript.
|
||||
|
||||
Similar to reddit_enrich.extract_comment_insights() but for
|
||||
continuous speech-to-text rather than threaded comments.
|
||||
"""
|
||||
if not transcript:
|
||||
return []
|
||||
|
||||
sentences = re.split(r'(?<=[.!?])\s+', transcript)
|
||||
|
||||
filler = [
|
||||
r"^(hey |hi |what's up|welcome back|in today's video|don't forget to)",
|
||||
r"(subscribe|like and comment|hit the bell|check out the link|down below)",
|
||||
r"^(so |and |but |okay |alright |um |uh )",
|
||||
r"(thanks for watching|see you (next|in the)|bye)",
|
||||
]
|
||||
|
||||
topic_words = [w.lower() for w in topic.lower().split() if len(w) > 2]
|
||||
|
||||
candidates = []
|
||||
for sent in sentences:
|
||||
sent = sent.strip()
|
||||
words = sent.split()
|
||||
if len(words) < 8 or len(words) > 50:
|
||||
continue
|
||||
if any(re.search(p, sent, re.IGNORECASE) for p in filler):
|
||||
continue
|
||||
|
||||
score = 0
|
||||
if re.search(r'\d', sent):
|
||||
score += 2
|
||||
if re.search(r'[A-Z][a-z]+', sent):
|
||||
score += 1
|
||||
if '?' in sent:
|
||||
score += 1
|
||||
sent_lower = sent.lower()
|
||||
if any(w in sent_lower for w in topic_words):
|
||||
score += 2
|
||||
|
||||
candidates.append((score, sent))
|
||||
|
||||
candidates.sort(key=lambda x: -x[0])
|
||||
return [sent for _, sent in candidates[:limit]]
|
||||
|
||||
|
||||
def _log(msg: str):
|
||||
"""Log to stderr."""
|
||||
sys.stderr.write(f"[YouTube] {msg}\n")
|
||||
@@ -345,11 +391,15 @@ def search_and_transcribe(
|
||||
top_ids = [item["video_id"] for item in items[:transcript_limit]]
|
||||
transcripts = fetch_transcripts_parallel(top_ids)
|
||||
|
||||
# Step 3: Attach transcripts to items
|
||||
# Step 3: Attach transcripts and extract highlights
|
||||
core_topic = _extract_core_subject(topic)
|
||||
for item in items:
|
||||
vid = item["video_id"]
|
||||
transcript = transcripts.get(vid)
|
||||
item["transcript_snippet"] = transcript or ""
|
||||
item["transcript_highlights"] = extract_transcript_highlights(
|
||||
transcript or "", core_topic,
|
||||
)
|
||||
|
||||
return {"items": items}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user