luckiest-co 1.0.47 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/ab-testing/CHANGELOG.md +5 -0
- package/skills/ab-testing/SKILL.md +12 -9
- package/skills/ab-testing/evals/evals.json +18 -7
- package/skills/ab-testing/references/sample-size-guide.md +2 -2
- package/skills/ads/CHANGELOG.md +5 -0
- package/skills/ads/SKILL.md +4 -4
- package/skills/ai-seo/CHANGELOG.md +7 -0
- package/skills/ai-seo/SKILL.md +27 -20
- package/skills/analytics/CHANGELOG.md +6 -0
- package/skills/analytics/SKILL.md +4 -3
- package/skills/analytics/evals/evals.json +27 -0
- package/skills/analytics/references/ga4-implementation.md +7 -3
- package/skills/analytics/references/gtm-implementation.md +8 -2
- package/skills/aso/CHANGELOG.md +5 -0
- package/skills/aso/SKILL.md +4 -4
- package/skills/aso/evals/evals.json +15 -2
- package/skills/aso/references/benchmarks.md +4 -2
- package/skills/aso/references/google-play-specs.md +5 -1
- package/skills/brainstorm/SKILL.md +59 -16
- package/skills/brainstorm/builder-check-prompt.md +23 -0
- package/skills/churn-prevention/CHANGELOG.md +5 -0
- package/skills/churn-prevention/SKILL.md +3 -3
- package/skills/churn-prevention/evals/evals.json +14 -0
- package/skills/churn-prevention/references/dunning-playbook.md +6 -1
- package/skills/coder-brainstorming/CHANGELOG.md +10 -0
- package/skills/coder-brainstorming/SKILL.md +62 -20
- package/skills/coder-brainstorming/builder-check-prompt.md +23 -0
- package/skills/coder-brainstorming/scripts/server.cjs +1 -1
- package/skills/coder-debugging/CHANGELOG.md +4 -0
- package/skills/coder-debugging/SKILL.md +5 -5
- package/skills/coder-requesting-code-review/CHANGELOG.md +4 -0
- package/skills/coder-requesting-code-review/SKILL.md +4 -4
- package/skills/coder-subagent-driven-development/CHANGELOG.md +4 -0
- package/skills/coder-subagent-driven-development/SKILL.md +3 -3
- package/skills/coder-subagent-driven-development/scripts/sdd-workspace +3 -1
- package/skills/cold-email/CHANGELOG.md +7 -0
- package/skills/cold-email/SKILL.md +27 -7
- package/skills/cold-email/evals/evals.json +30 -0
- package/skills/cold-email/references/benchmarks.md +15 -11
- package/skills/cold-email/references/deliverability.md +110 -0
- package/skills/cold-email/references/linkedin-outreach.md +91 -0
- package/skills/cold-email/references/multichannel-cadence.md +79 -0
- package/skills/cold-email/references/outbound-plays.md +88 -0
- package/skills/cold-email/references/personalization.md +11 -0
- package/skills/cold-email/references/reply-handling.md +65 -0
- package/skills/competitor-profiling/CHANGELOG.md +5 -0
- package/skills/competitor-profiling/SKILL.md +4 -4
- package/skills/content-strategy/CHANGELOG.md +5 -0
- package/skills/content-strategy/SKILL.md +5 -3
- package/skills/copy-editing/CHANGELOG.md +5 -0
- package/skills/copy-editing/SKILL.md +5 -5
- package/skills/copy-editing/references/content-refresh.md +33 -0
- package/skills/cro/CHANGELOG.md +5 -0
- package/skills/cro/SKILL.md +14 -3
- package/skills/cro/references/form.md +1 -1
- package/skills/directory-submissions/CHANGELOG.md +5 -0
- package/skills/directory-submissions/SKILL.md +13 -4
- package/skills/directory-submissions/evals/evals.json +15 -1
- package/skills/directory-submissions/references/directory-list.md +2 -4
- package/skills/emails/CHANGELOG.md +5 -0
- package/skills/emails/SKILL.md +14 -4
- package/skills/launch/CHANGELOG.md +7 -0
- package/skills/launch/SKILL.md +8 -4
- package/skills/launch/evals/evals.json +29 -0
- package/skills/launch/references/shipped-changes.md +84 -0
- package/skills/launch/references/site-launch-qa.md +55 -0
- package/skills/lead-magnets/CHANGELOG.md +5 -0
- package/skills/lead-magnets/SKILL.md +6 -4
- package/skills/lead-magnets/evals/evals.json +18 -0
- package/skills/lead-magnets/references/content-to-skill-corpus-design.md +89 -0
- package/skills/lead-magnets/references/content-to-skill-release.md +71 -0
- package/skills/lead-magnets/references/content-to-skill.md +108 -0
- package/skills/marketing-plan/CHANGELOG.md +5 -0
- package/skills/marketing-plan/SKILL.md +3 -3
- package/skills/marketing-plan/evals/evals.json +13 -0
- package/skills/marketing-plan/references/budget-planning.md +26 -17
- package/skills/model-router/CHANGELOG.md +6 -0
- package/skills/model-router/SKILL.md +3 -3
- package/skills/model-router/scripts/route.py +1 -1
- package/skills/model-router/scripts/test_route.py +5 -0
- package/skills/pricing/CHANGELOG.md +5 -0
- package/skills/pricing/SKILL.md +4 -4
- package/skills/pricing/evals/evals.json +12 -0
- package/skills/pricing/references/research-methods.md +8 -4
- package/skills/programmatic-seo/CHANGELOG.md +5 -0
- package/skills/programmatic-seo/SKILL.md +8 -4
- package/skills/programmatic-seo/evals/evals.json +13 -0
- package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
- package/skills/prospecting/CHANGELOG.md +7 -0
- package/skills/prospecting/SKILL.md +27 -13
- package/skills/prospecting/evals/evals.json +41 -0
- package/skills/prospecting/references/account-research.md +59 -0
- package/skills/prospecting/references/b2b-prospecting.md +1 -1
- package/skills/prospecting/references/compliance.md +44 -6
- package/skills/prospecting/references/data-sources.md +13 -15
- package/skills/prospecting/references/enrichment-playbook.md +61 -0
- package/skills/prospecting/references/saas-prospecting.md +1 -1
- package/skills/prospecting/references/signal-plays.md +99 -0
- package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
- package/skills/referrals/CHANGELOG.md +5 -0
- package/skills/referrals/SKILL.md +10 -4
- package/skills/referrals/evals/evals.json +99 -0
- package/skills/referrals/references/affiliate-operations.md +64 -0
- package/skills/referrals/references/affiliate-programs.md +2 -2
- package/skills/referrals/references/program-examples.md +14 -6
- package/skills/referrals/references/review-platform-rules.md +36 -0
- package/skills/referrals/references/review-program-operations.md +62 -0
- package/skills/referrals/references/review-programs.md +96 -0
- package/skills/revops/CHANGELOG.md +5 -0
- package/skills/revops/SKILL.md +10 -6
- package/skills/revops/evals/evals.json +13 -0
- package/skills/revops/references/automation-playbooks.md +1 -1
- package/skills/revops/references/inbound-routing.md +43 -0
- package/skills/revops/references/lifecycle-definitions.md +31 -0
- package/skills/sales-enablement/CHANGELOG.md +5 -0
- package/skills/sales-enablement/SKILL.md +6 -4
- package/skills/sales-enablement/evals/evals.json +14 -0
- package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
- package/skills/schema/CHANGELOG.md +5 -0
- package/skills/schema/SKILL.md +20 -8
- package/skills/schema/evals/evals.json +40 -0
- package/skills/schema/references/schema-examples.md +9 -15
- package/skills/seo-audit/CHANGELOG.md +5 -0
- package/skills/seo-audit/SKILL.md +10 -16
- package/skills/seo-audit/evals/evals.json +68 -0
- package/skills/seo-audit/references/international-seo.md +9 -11
- package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
- package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
- package/skills/seo-audit/references/local-seo.md +86 -0
- package/skills/seo-audit/references/rankings-push.md +62 -0
- package/skills/seo-audit/references/title-tags.md +89 -0
- package/skills/signup/CHANGELOG.md +5 -0
- package/skills/signup/SKILL.md +7 -3
- package/skills/site-architecture/CHANGELOG.md +5 -0
- package/skills/site-architecture/SKILL.md +5 -4
- package/skills/site-architecture/evals/evals.json +25 -0
- package/skills/site-architecture/references/platforms-and-migration.md +84 -0
- package/skills/social/CHANGELOG.md +5 -0
- package/skills/social/SKILL.md +4 -4
- package/skills/trends/.skillignore +3 -0
- package/skills/trends/CHANGELOG.md +20 -0
- package/skills/trends/SKILL.md +12 -8
- package/skills/trends/references/agent-mode.md +36 -0
- package/skills/trends/references/category-peers.md +38 -0
- package/skills/trends/references/comparison.md +227 -0
- package/skills/trends/references/competitors.md +29 -0
- package/skills/trends/references/discovery.md +90 -0
- package/skills/trends/references/engine-contract.md +111 -1889
- package/skills/trends/references/followups.md +112 -0
- package/skills/trends/references/grok-bot-x.md +55 -0
- package/skills/trends/references/hiring-signals.md +19 -0
- package/skills/trends/references/library-queue.md +33 -0
- package/skills/trends/references/recommendations.md +86 -0
- package/skills/trends/references/research-runbook.md +781 -0
- package/skills/trends/references/runtime.md +165 -0
- package/skills/trends/references/save-html-brief.md +39 -20
- package/skills/trends/references/setup-wizard.md +253 -0
- package/skills/trends/references/synthesis.md +289 -0
- package/skills/trends/scripts/box_chrome_login.py +209 -0
- package/skills/trends/scripts/briefing.py +9 -17
- package/skills/trends/scripts/last30days.py +3247 -280
- package/skills/trends/scripts/lib/agentcookie.py +196 -0
- package/skills/trends/scripts/lib/amazon.py +877 -0
- package/skills/trends/scripts/lib/arxiv.py +42 -8
- package/skills/trends/scripts/lib/backends.py +769 -0
- package/skills/trends/scripts/lib/bird_x.py +340 -57
- package/skills/trends/scripts/lib/bluesky.py +84 -10
- package/skills/trends/scripts/lib/bounded_get.py +156 -0
- package/skills/trends/scripts/lib/brightdata.py +291 -0
- package/skills/trends/scripts/lib/categories.py +7 -1
- package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
- package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
- package/skills/trends/scripts/lib/cluster.py +31 -54
- package/skills/trends/scripts/lib/competitors.py +10 -1
- package/skills/trends/scripts/lib/cookie_extract.py +221 -44
- package/skills/trends/scripts/lib/cookie_paths.py +27 -0
- package/skills/trends/scripts/lib/corpus.py +439 -0
- package/skills/trends/scripts/lib/devto.py +313 -0
- package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
- package/skills/trends/scripts/lib/doctor.py +2166 -0
- package/skills/trends/scripts/lib/dripstack.py +203 -0
- package/skills/trends/scripts/lib/entity_extract.py +37 -0
- package/skills/trends/scripts/lib/env.py +1092 -138
- package/skills/trends/scripts/lib/fanout.py +11 -1
- package/skills/trends/scripts/lib/feed.py +61 -0
- package/skills/trends/scripts/lib/fetch_memo.py +135 -0
- package/skills/trends/scripts/lib/freshness.py +566 -0
- package/skills/trends/scripts/lib/fusion.py +314 -28
- package/skills/trends/scripts/lib/github.py +438 -24
- package/skills/trends/scripts/lib/grok_x.py +1237 -0
- package/skills/trends/scripts/lib/grounding.py +129 -11
- package/skills/trends/scripts/lib/hackernews.py +61 -4
- package/skills/trends/scripts/lib/health.py +373 -0
- package/skills/trends/scripts/lib/hosted.py +333 -0
- package/skills/trends/scripts/lib/html_publish.py +34 -0
- package/skills/trends/scripts/lib/html_render.py +169 -3
- package/skills/trends/scripts/lib/http.py +1215 -70
- package/skills/trends/scripts/lib/instagram.py +132 -9
- package/skills/trends/scripts/lib/jobs.py +4 -2
- package/skills/trends/scripts/lib/library.py +308 -0
- package/skills/trends/scripts/lib/library_index.py +477 -0
- package/skills/trends/scripts/lib/linkedin.py +271 -24
- package/skills/trends/scripts/lib/log.py +5 -2
- package/skills/trends/scripts/lib/meta_ads.py +982 -0
- package/skills/trends/scripts/lib/normalize.py +382 -28
- package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
- package/skills/trends/scripts/lib/permission_preflight.py +42 -3
- package/skills/trends/scripts/lib/perplexity.py +1080 -468
- package/skills/trends/scripts/lib/pipeline.py +5079 -211
- package/skills/trends/scripts/lib/planner.py +396 -53
- package/skills/trends/scripts/lib/polymarket.py +285 -23
- package/skills/trends/scripts/lib/prescriptions.py +432 -0
- package/skills/trends/scripts/lib/private_files.py +21 -0
- package/skills/trends/scripts/lib/providers.py +231 -10
- package/skills/trends/scripts/lib/quality_nudge.py +122 -37
- package/skills/trends/scripts/lib/reddit.py +177 -12
- package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
- package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
- package/skills/trends/scripts/lib/reddit_listing.py +215 -14
- package/skills/trends/scripts/lib/reddit_public.py +12 -13
- package/skills/trends/scripts/lib/reddit_search.py +304 -0
- package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
- package/skills/trends/scripts/lib/registers.py +134 -0
- package/skills/trends/scripts/lib/relevance.py +17 -0
- package/skills/trends/scripts/lib/render.py +2161 -271
- package/skills/trends/scripts/lib/rerank.py +623 -55
- package/skills/trends/scripts/lib/resolve.py +50 -1
- package/skills/trends/scripts/lib/safari_cookies.py +42 -15
- package/skills/trends/scripts/lib/schema.py +782 -1
- package/skills/trends/scripts/lib/setup_wizard.py +779 -120
- package/skills/trends/scripts/lib/signals.py +206 -7
- package/skills/trends/scripts/lib/stocktwits.py +374 -0
- package/skills/trends/scripts/lib/subproc.py +307 -53
- package/skills/trends/scripts/lib/techmeme.py +89 -64
- package/skills/trends/scripts/lib/telegram.py +317 -0
- package/skills/trends/scripts/lib/tiktok.py +13 -9
- package/skills/trends/scripts/lib/topic_shape.py +340 -0
- package/skills/trends/scripts/lib/transcribe.py +12 -3
- package/skills/trends/scripts/lib/trustpilot.py +256 -5
- package/skills/trends/scripts/lib/truthsocial.py +9 -1
- package/skills/trends/scripts/lib/ui.py +6 -0
- package/skills/trends/scripts/lib/usage.py +132 -0
- package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
- package/skills/trends/scripts/lib/windows_job.py +138 -0
- package/skills/trends/scripts/lib/x_api.py +824 -0
- package/skills/trends/scripts/lib/x_envelope.py +733 -0
- package/skills/trends/scripts/lib/x_judge.py +292 -0
- package/skills/trends/scripts/lib/xai_x.py +26 -3
- package/skills/trends/scripts/lib/xquik.py +89 -15
- package/skills/trends/scripts/lib/xurl_x.py +284 -91
- package/skills/trends/scripts/lib/youtube_yt.py +570 -119
- package/skills/trends/scripts/setup-keychain.sh +12 -4
- package/skills/trends/scripts/setup-pass.sh +3 -0
- package/skills/trends/scripts/store.py +444 -50
- package/skills/trends/scripts/watchlist.py +38 -11
- package/skills/trends/scripts/x_lane_parity.py +107 -0
- package/skills/video-studio/CHANGELOG.md +4 -0
- package/skills/video-studio/SKILL.md +4 -4
- package/skills/video-studio/references/styles/product-flight.md +45 -0
- package/skills/video-studio/references/styles.md +3 -1
- package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
- package/skills/video-studio-motion/CHANGELOG.md +4 -0
- package/skills/video-studio-motion/SKILL.md +8 -5
- package/skills/youtube-planner/ATTRIBUTION.md +25 -0
- package/skills/youtube-planner/CHANGELOG.md +12 -0
- package/skills/youtube-planner/SKILL.md +191 -0
- package/skills/youtube-planner/references/plan-template.md +57 -0
- package/skills/youtube-planner/references/stages.md +119 -0
- package/skills/youtube-planner/scripts/floor_ceiling.py +132 -0
- package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/skills/trends/scripts/lib/reddit_rss.py +0 -224
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Deterministic topic naming and junk-shape classification for discovery.
|
|
2
|
+
|
|
3
|
+
Discovery mode surfaces short, named, content-worthy topics instead of raw
|
|
4
|
+
post titles. This module is the pure-function, stdlib-only stage-1 fallback
|
|
5
|
+
for that pipeline (used when no LLM is available, and as the deterministic
|
|
6
|
+
baseline the LLM path is judged against):
|
|
7
|
+
|
|
8
|
+
- ``distill_topic_name(title, snippet)`` distills a listing title into a
|
|
9
|
+
2-6 word searchable topic name: question/framing scaffolding is stripped,
|
|
10
|
+
proper-noun / digit-bearing entity phrases are preferred and emitted as an
|
|
11
|
+
ORDERED phrase in title order (never a bag of words), and the cleaned,
|
|
12
|
+
truncated title is the final fallback so the result is never empty for any
|
|
13
|
+
title with word content.
|
|
14
|
+
- ``is_junk_shape(title, snippet)`` flags listing shapes that should never
|
|
15
|
+
become topics: help-me questions, beginner asks, and first-person musings.
|
|
16
|
+
Launch titles ("Show HN: ...") and entity-bearing news statements are not
|
|
17
|
+
junk.
|
|
18
|
+
|
|
19
|
+
Both functions take plain strings and return plain values - no candidate
|
|
20
|
+
objects, no config, no I/O - so they are trivially testable and reusable.
|
|
21
|
+
|
|
22
|
+
Names produced here are used downstream as short search queries and grounding
|
|
23
|
+
strings, so they never carry trailing punctuation or quote characters. Per the
|
|
24
|
+
head-token convention, callers must never assume a distilled name appears as a
|
|
25
|
+
contiguous substring of any document.
|
|
26
|
+
|
|
27
|
+
Token conventions (stopwords, capital/digit entity signals) are inherited from
|
|
28
|
+
``entity_extract`` and extended here; unlike ``extract_text_entities`` this
|
|
29
|
+
module preserves title order and original casing because the output is a
|
|
30
|
+
human-readable phrase, not a matching set. Non-Latin (CJK) titles never crash:
|
|
31
|
+
they carry no Latin entity signal, so they fall through to the cleaned-title
|
|
32
|
+
path, capped at ``_MAX_NAME_CHARS``.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
from typing import List, NamedTuple, Optional
|
|
39
|
+
|
|
40
|
+
from .entity_extract import ENTITY_STOPWORDS
|
|
41
|
+
|
|
42
|
+
_MAX_NAME_WORDS = 6
|
|
43
|
+
_MAX_NAME_CHARS = 80
|
|
44
|
+
|
|
45
|
+
# Extends the shared entity stopwords with pronouns, auxiliaries, contractions
|
|
46
|
+
# and musing filler that read as capitalized sentence-openers in titles but are
|
|
47
|
+
# never entities ("My", "Everyone", "Don't", ...). Deliberate casualty: the
|
|
48
|
+
# acronyms "US" and "IT" are swallowed by their pronoun homographs.
|
|
49
|
+
_ANCHOR_STOPWORDS = frozenset(ENTITY_STOPWORDS) | frozenset({
|
|
50
|
+
"i", "i'm", "i've", "i'd", "i'll", "me", "my", "mine", "myself",
|
|
51
|
+
"we", "we're", "we've", "our", "ours", "us",
|
|
52
|
+
"you", "you're", "your", "yours",
|
|
53
|
+
"am", "were", "be", "why", "when", "where", "which", "whom", "whose",
|
|
54
|
+
"does", "did", "doing", "done", "should", "shall", "may", "might", "must",
|
|
55
|
+
"if", "or", "so", "as", "any", "anyone", "anybody", "someone", "somebody",
|
|
56
|
+
"everyone", "everybody", "nobody", "none", "no", "yes",
|
|
57
|
+
"please", "thanks", "thank", "really", "actually", "very", "well",
|
|
58
|
+
"while", "during", "still", "even", "ever", "never", "always",
|
|
59
|
+
"don't", "dont", "can't", "cant", "won't", "wont", "isn't", "isnt",
|
|
60
|
+
"aren't", "arent", "doesn't", "doesnt", "didn't", "didnt",
|
|
61
|
+
"it's", "that's", "there's", "here's", "let's", "what's", "who's", "how's",
|
|
62
|
+
"mean", "means", "meant", "same", "thing", "things", "stuff",
|
|
63
|
+
"way", "ways", "lot", "lots", "kind", "sort",
|
|
64
|
+
"today", "yesterday", "tomorrow",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
# Characters stripped from token edges for display (internal hyphens/dots in
|
|
68
|
+
# "open-source" / "example.com" survive). Includes unicode dashes/ellipsis.
|
|
69
|
+
_EDGE_CHARS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~–—…"
|
|
70
|
+
_TRAILING_JUNK = ".,;:!?…'\"`- "
|
|
71
|
+
|
|
72
|
+
_POSSESSIVE_RE = re.compile(r"(?<=\w)'s\b", re.IGNORECASE)
|
|
73
|
+
_DOUBLE_QUOTE_RE = re.compile(r"[\"“”„«»]")
|
|
74
|
+
_LONE_APOSTROPHE_RE = re.compile(r"(?<!\w)'|'(?!\w)")
|
|
75
|
+
_SENTENCE_END_RE = re.compile(r"[.!?;:,]$")
|
|
76
|
+
|
|
77
|
+
# Framing scaffolding stripped (iteratively) from the start of a title before
|
|
78
|
+
# naming: forum labels, interrogative openers, first-person setup, politeness
|
|
79
|
+
# filler, and leading articles. Junk *classification* has its own patterns
|
|
80
|
+
# below; these only clean the string we name from.
|
|
81
|
+
_SCAFFOLD_RES = [re.compile(p, re.IGNORECASE) for p in (
|
|
82
|
+
r"^(show hn|ask hn|tell hn|launch hn|psa|eli5|tifu|til|discussion|"
|
|
83
|
+
r"question|help|advice|update|rant|vent|meta)\s*[:\-–—]\s*",
|
|
84
|
+
r"^(how|what|when|where|which|why|who)\s+"
|
|
85
|
+
r"(do|does|did|is|are|was|were|am|can|could|should|would|will|to|i|we|you|your|my|one)\s+",
|
|
86
|
+
r"^(is|are|does|do|did|can|could|should|would|will|has|have|am)\s+"
|
|
87
|
+
r"(there|it|this|anyone|anybody|someone|somebody|we|you|i|they|my|your)\s+",
|
|
88
|
+
r"^(i|we)\s+(think|believe|feel|guess|wonder|noticed|realized|have run|"
|
|
89
|
+
r"have been|have|had|am|was|were|just|finally|recently|need|want|"
|
|
90
|
+
r"would like|tried|keep|built|made|created|wrote|spent)\s+",
|
|
91
|
+
r"^(i'm|i've|i'd|we're|we've)\s+",
|
|
92
|
+
r"^my\s+(coworker|co-worker|colleague|boss|friend|manager|team|company|"
|
|
93
|
+
r"startup|wife|husband|partner|mom|dad|mother|father|brother|sister|"
|
|
94
|
+
r"son|daughter|kid|kids|roommate|neighbor)\s+\w+\s+",
|
|
95
|
+
r"^(hey|hi|hello|guys|folks|please|okay|ok|so|honestly|serious question)[,!\s]\s*",
|
|
96
|
+
r"^(a|an|the)\s+",
|
|
97
|
+
)]
|
|
98
|
+
|
|
99
|
+
# --- junk-shape markers (matched against the cleaned, lowercased title) -----
|
|
100
|
+
|
|
101
|
+
_LAUNCH_RE = re.compile(r"^(show hn|launch hn)\b")
|
|
102
|
+
# Leading interrogatives: wh-words count only with a question follow-through
|
|
103
|
+
# ("What is the best..." is junk; "What Gemma 4 means..." is an explainer).
|
|
104
|
+
_WH_JUNK_RE = re.compile(
|
|
105
|
+
r"^(how|what|why|when|where|which|who)\s+"
|
|
106
|
+
r"(do|does|did|is|are|was|were|am|can|could|should|would|will|to|i|we|you|your|my|one)\b"
|
|
107
|
+
)
|
|
108
|
+
_AUX_JUNK_RE = re.compile(
|
|
109
|
+
r"^(is|are|does|do|did|can|could|should|would|will|has|have|am)\s+"
|
|
110
|
+
r"(there|it|this|anyone|anybody|someone|somebody|we|you|i|they|my|your)\b"
|
|
111
|
+
)
|
|
112
|
+
_HELP_RE = re.compile(
|
|
113
|
+
r"\bneed (some |a little )?(help|advice)\b|\bplease help\b|\bhelp me\b|"
|
|
114
|
+
r"^help\b|\bany (advice|recommendation|recommendations|suggestions|recs|tips)\b|"
|
|
115
|
+
r"\blooking for (advice|recommendations|suggestions|help|tips)\b|"
|
|
116
|
+
r"\bwhere (do|should|would) (i|we) (even )?(start|begin)\b|\bwhere to start\b|"
|
|
117
|
+
r"\bbeginner (question|here)\b|\bnoob (question|here)\b|"
|
|
118
|
+
r"\btotal beginner\b|\bcomplete beginner\b|\bam i missing something\b|"
|
|
119
|
+
r"\brecommend me\b"
|
|
120
|
+
)
|
|
121
|
+
_MUSING_RE = re.compile(
|
|
122
|
+
r"^(i think|i feel|i believe|i guess|i wonder|i have been|i've been|i keep|"
|
|
123
|
+
r"my thoughts|thoughts on|unpopular opinion|hot take|am i the only one|"
|
|
124
|
+
r"is it just me|anyone else|does anyone else|rant|vent|change my mind|cmv)\b"
|
|
125
|
+
)
|
|
126
|
+
_EVERYONE_RE = re.compile(
|
|
127
|
+
r"\beveryone (is|does|says|seems|keeps|wants)\b.{0,80}\bbut (do|are|can|should|will|did) we\b"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _Token(NamedTuple):
|
|
132
|
+
display: str # edge-punctuation-stripped, original casing
|
|
133
|
+
lower: str
|
|
134
|
+
is_anchor: bool # proper-noun / digit / acronym entity signal
|
|
135
|
+
breaks_after: bool # sentence/clause boundary follows this token
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def distill_topic_name(title: str, snippet: str = "") -> str:
|
|
139
|
+
"""Distill a listing title (+ optional snippet) into a 2-6 word topic name.
|
|
140
|
+
|
|
141
|
+
The name is an ordered phrase built from entity anchors in title order,
|
|
142
|
+
safe to use as a short search query: <= 6 words, <= 80 chars, no trailing
|
|
143
|
+
punctuation, no quote characters. Never empty for any input with word
|
|
144
|
+
content (the sole exception: title AND snippet contain no word characters,
|
|
145
|
+
which returns "").
|
|
146
|
+
"""
|
|
147
|
+
base = _normalize(title) or _normalize(snippet)
|
|
148
|
+
if not base:
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
stripped = _strip_scaffolding(base)
|
|
152
|
+
tokens = _tokenize(stripped)
|
|
153
|
+
if not tokens:
|
|
154
|
+
tokens = _tokenize(base)
|
|
155
|
+
if not tokens:
|
|
156
|
+
return ""
|
|
157
|
+
words = [t.display for t in tokens]
|
|
158
|
+
|
|
159
|
+
# Already-short titles pass through unless a stronger entity phrase is
|
|
160
|
+
# buried mid-title (first word not an anchor while anchors exist).
|
|
161
|
+
if len(words) <= _MAX_NAME_WORDS and (tokens[0].is_anchor or not any(t.is_anchor for t in tokens)):
|
|
162
|
+
return _finalize(words)
|
|
163
|
+
|
|
164
|
+
phrase = _entity_phrase(tokens)
|
|
165
|
+
if phrase:
|
|
166
|
+
return _finalize(phrase)
|
|
167
|
+
|
|
168
|
+
# Title had no entity anchors: try the snippet's leading entity phrase.
|
|
169
|
+
if snippet:
|
|
170
|
+
snippet_tokens = _tokenize(_strip_scaffolding(_normalize(snippet)))
|
|
171
|
+
snippet_phrase = _entity_phrase(snippet_tokens)
|
|
172
|
+
if snippet_phrase:
|
|
173
|
+
return _finalize(snippet_phrase)
|
|
174
|
+
|
|
175
|
+
# Final fallback: cleaned title truncated to the word cap.
|
|
176
|
+
return _finalize(words[:_MAX_NAME_WORDS])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def is_junk_shape(title: str, snippet: str = "") -> bool:
|
|
180
|
+
"""True when the listing shape is not content-worthy.
|
|
181
|
+
|
|
182
|
+
Rule-based markers: leading interrogatives, help/advice asks, first-person
|
|
183
|
+
musings, and trailing "?" with no named entity in the title. Launch titles
|
|
184
|
+
("Show HN: ...") and entity-bearing news statements are not junk. The
|
|
185
|
+
snippet is consulted only when the title itself has no entity anchors.
|
|
186
|
+
"""
|
|
187
|
+
cleaned = _normalize(title)
|
|
188
|
+
if not cleaned:
|
|
189
|
+
cleaned = _normalize(snippet)
|
|
190
|
+
if not cleaned:
|
|
191
|
+
return True # nothing nameable at all
|
|
192
|
+
lower = cleaned.lower()
|
|
193
|
+
|
|
194
|
+
if _LAUNCH_RE.search(lower):
|
|
195
|
+
return False
|
|
196
|
+
if _WH_JUNK_RE.search(lower) or _AUX_JUNK_RE.search(lower):
|
|
197
|
+
return True
|
|
198
|
+
if _HELP_RE.search(lower) or _MUSING_RE.search(lower) or _EVERYONE_RE.search(lower):
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
has_entity = any(t.is_anchor for t in _tokenize(cleaned))
|
|
202
|
+
if lower.endswith(("?", "?")) and not has_entity:
|
|
203
|
+
return True
|
|
204
|
+
if not has_entity and snippet:
|
|
205
|
+
snippet_lower = _normalize(snippet).lower()
|
|
206
|
+
if (_HELP_RE.search(snippet_lower) or _MUSING_RE.search(snippet_lower)
|
|
207
|
+
or _AUX_JUNK_RE.search(snippet_lower) or _EVERYONE_RE.search(snippet_lower)):
|
|
208
|
+
return True
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _normalize(text: str) -> str:
|
|
216
|
+
"""Collapse whitespace, drop quote characters, fold possessives ("4's" -> "4")."""
|
|
217
|
+
if not text:
|
|
218
|
+
return ""
|
|
219
|
+
text = text.replace("’", "'").replace("‘", "'").replace("`", "'").replace("´", "'")
|
|
220
|
+
text = _POSSESSIVE_RE.sub("", text)
|
|
221
|
+
text = _DOUBLE_QUOTE_RE.sub(" ", text)
|
|
222
|
+
text = _LONE_APOSTROPHE_RE.sub(" ", text)
|
|
223
|
+
return " ".join(text.split())
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _strip_scaffolding(text: str) -> str:
|
|
227
|
+
"""Iteratively strip question/framing scaffolding from the title start."""
|
|
228
|
+
for _ in range(6):
|
|
229
|
+
before = text
|
|
230
|
+
for pattern in _SCAFFOLD_RES:
|
|
231
|
+
text = pattern.sub("", text, count=1).lstrip(" ,-")
|
|
232
|
+
if text == before:
|
|
233
|
+
break
|
|
234
|
+
return text.strip()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _is_anchor(display: str) -> bool:
|
|
238
|
+
"""Entity signal per entity_extract conventions: capitals, digits, acronyms."""
|
|
239
|
+
if not display:
|
|
240
|
+
return False
|
|
241
|
+
if display.lower() in _ANCHOR_STOPWORDS:
|
|
242
|
+
return False
|
|
243
|
+
if any(c.isdigit() for c in display):
|
|
244
|
+
return True
|
|
245
|
+
if len(display) < 2:
|
|
246
|
+
return False
|
|
247
|
+
if display[0].isupper():
|
|
248
|
+
return True
|
|
249
|
+
return any(c.isupper() for c in display[1:]) # iPhone, gpt4all-style
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _tokenize(text: str) -> List[_Token]:
|
|
253
|
+
"""Split into display tokens, tagging entity anchors and clause boundaries."""
|
|
254
|
+
tokens: List[_Token] = []
|
|
255
|
+
for raw in text.split():
|
|
256
|
+
display = raw.strip(_EDGE_CHARS)
|
|
257
|
+
if not display:
|
|
258
|
+
# Pure-punctuation token (a bare dash, "..."): clause boundary.
|
|
259
|
+
if tokens:
|
|
260
|
+
tokens[-1] = tokens[-1]._replace(breaks_after=True)
|
|
261
|
+
continue
|
|
262
|
+
tokens.append(_Token(
|
|
263
|
+
display=display,
|
|
264
|
+
lower=display.lower(),
|
|
265
|
+
is_anchor=_is_anchor(display),
|
|
266
|
+
breaks_after=bool(_SENTENCE_END_RE.search(raw)),
|
|
267
|
+
))
|
|
268
|
+
return tokens
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _entity_phrase(tokens: List[_Token]) -> Optional[List[str]]:
|
|
272
|
+
"""Build an ordered phrase from entity-anchor runs, in title order.
|
|
273
|
+
|
|
274
|
+
Adjacent anchor runs separated by <= 2 contentful (non-stopword,
|
|
275
|
+
non-boundary) words are merged with their connecting words kept, so the
|
|
276
|
+
phrase stays readable ("AI agent handle Slack", not "AI Slack"). Runs are
|
|
277
|
+
then concatenated in title order up to the word cap.
|
|
278
|
+
"""
|
|
279
|
+
runs: List[tuple[int, int]] = [] # inclusive (start, end) token indices
|
|
280
|
+
i = 0
|
|
281
|
+
while i < len(tokens):
|
|
282
|
+
if tokens[i].is_anchor:
|
|
283
|
+
j = i
|
|
284
|
+
while j + 1 < len(tokens) and tokens[j + 1].is_anchor and not tokens[j].breaks_after:
|
|
285
|
+
j += 1
|
|
286
|
+
runs.append((i, j))
|
|
287
|
+
i = j + 1
|
|
288
|
+
else:
|
|
289
|
+
i += 1
|
|
290
|
+
if not runs:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
merged = [runs[0]]
|
|
294
|
+
for start, end in runs[1:]:
|
|
295
|
+
prev_start, prev_end = merged[-1]
|
|
296
|
+
gap = tokens[prev_end + 1:start]
|
|
297
|
+
if (
|
|
298
|
+
0 < len(gap) <= 2
|
|
299
|
+
and not tokens[prev_end].breaks_after
|
|
300
|
+
and all(g.lower not in _ANCHOR_STOPWORDS and not g.breaks_after for g in gap)
|
|
301
|
+
):
|
|
302
|
+
merged[-1] = (prev_start, end)
|
|
303
|
+
else:
|
|
304
|
+
merged.append((start, end))
|
|
305
|
+
|
|
306
|
+
words: List[str] = []
|
|
307
|
+
last_index: Optional[int] = None
|
|
308
|
+
for start, end in merged:
|
|
309
|
+
span = [t.display for t in tokens[start:end + 1]]
|
|
310
|
+
if not words and len(span) > _MAX_NAME_WORDS:
|
|
311
|
+
span = span[:_MAX_NAME_WORDS]
|
|
312
|
+
end = start + _MAX_NAME_WORDS - 1
|
|
313
|
+
if len(words) + len(span) > _MAX_NAME_WORDS:
|
|
314
|
+
break
|
|
315
|
+
words.extend(span)
|
|
316
|
+
last_index = end
|
|
317
|
+
|
|
318
|
+
# Readability extension: pull in one attached plural noun ("Slack replies").
|
|
319
|
+
if words and len(words) < _MAX_NAME_WORDS and last_index is not None:
|
|
320
|
+
nxt = tokens[last_index + 1] if last_index + 1 < len(tokens) else None
|
|
321
|
+
if (
|
|
322
|
+
nxt is not None
|
|
323
|
+
and not tokens[last_index].breaks_after
|
|
324
|
+
and not nxt.is_anchor
|
|
325
|
+
and nxt.display.islower()
|
|
326
|
+
and nxt.display.endswith("s")
|
|
327
|
+
and nxt.lower not in _ANCHOR_STOPWORDS
|
|
328
|
+
):
|
|
329
|
+
words.append(nxt.display)
|
|
330
|
+
|
|
331
|
+
return words or None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _finalize(words: List[str]) -> str:
|
|
335
|
+
"""Join to a query-safe name: char cap, no trailing punctuation or quotes."""
|
|
336
|
+
name = " ".join(w for w in words if w).strip()
|
|
337
|
+
if len(name) > _MAX_NAME_CHARS:
|
|
338
|
+
name = name[:_MAX_NAME_CHARS].rstrip()
|
|
339
|
+
name = name.strip(_TRAILING_JUNK)
|
|
340
|
+
return " ".join(name.split())
|
|
@@ -24,7 +24,7 @@ import tempfile
|
|
|
24
24
|
from dataclasses import dataclass, field
|
|
25
25
|
from typing import Optional
|
|
26
26
|
|
|
27
|
-
from . import env, health, subproc
|
|
27
|
+
from . import env, health, http, log, subproc, usage
|
|
28
28
|
|
|
29
29
|
# Whisper's documented upload ceiling. We compress to stay under it and chunk
|
|
30
30
|
# when a single clip still exceeds it.
|
|
@@ -177,7 +177,11 @@ def _transcribe_chunk(
|
|
|
177
177
|
for name, key in providers:
|
|
178
178
|
try:
|
|
179
179
|
text = _post_audio(name, path, key, timeout=timeout)
|
|
180
|
-
except Exception: # noqa: BLE001 - any provider failure -> try the next
|
|
180
|
+
except Exception as exc: # noqa: BLE001 - any provider failure -> try the next
|
|
181
|
+
# Surface the failing provider + error (e.g. Groq's Cloudflare 403,
|
|
182
|
+
# "error code: 1010") under LAST30DAYS_DEBUG instead of silently
|
|
183
|
+
# degrading to an empty transcript (#983).
|
|
184
|
+
log.debug(f"transcribe: provider {name} failed on a chunk: {exc!r}")
|
|
181
185
|
text = None
|
|
182
186
|
if text is not None:
|
|
183
187
|
return text, name
|
|
@@ -214,10 +218,15 @@ def _post_audio(provider: str, path: str, api_key: str, timeout: float) -> Optio
|
|
|
214
218
|
headers={
|
|
215
219
|
"Authorization": f"Bearer {api_key}",
|
|
216
220
|
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
221
|
+
# A plain-urllib UA is fingerprinted and 403'd by Cloudflare-style
|
|
222
|
+
# edges (Groq returns "error code: 1010", #983). Send the skill's
|
|
223
|
+
# canonical identity instead, same as lib/http.py's get/post.
|
|
224
|
+
"User-Agent": http.USER_AGENT,
|
|
217
225
|
},
|
|
218
226
|
method="POST",
|
|
219
227
|
)
|
|
220
|
-
|
|
228
|
+
usage.begin(provider)
|
|
229
|
+
with http.open_request(req, timeout) as resp:
|
|
221
230
|
data = json.loads(resp.read().decode("utf-8"))
|
|
222
231
|
return data.get("text")
|
|
223
232
|
|