Official Python SDK for the BrightBean API — YouTube packaging scoring.
Project description
brightbean — Python SDK
Official Python SDK for the BrightBean API. YouTube creator analytics: score titles and thumbnails for click-through-rate, analyze video hooks, benchmark channels and videos against their niche, and surface ranked content opportunities.
Install
pip install brightbean
Requires Python 3.10+.
Quickstart
from brightbean import BrightBean
with BrightBean(api_key="bb_...") as client:
result = client.score(
title="10 Tips to Boost Your Coding Productivity",
thumbnail_url="https://i.ytimg.com/vi/abc123/maxresdefault.jpg",
)
print(f"score={result.score:.2f} percentile={result.percentile}")
Async
import asyncio
from brightbean import AsyncBrightBean
async def main():
async with AsyncBrightBean(api_key="bb_...") as client:
return await client.score(title="My epic video")
asyncio.run(main())
Every method documented below has the same signature on AsyncBrightBean; just
await the call.
API reference
Response objects are typed dataclasses. Field names match the wire format
(snake_case). Nullable fields are explicitly annotated … | None.
client.score(...) — packaging CTR
Score a YouTube packaging (title and/or thumbnail) for predicted click-through rate. The server auto-detects the mode from your inputs and charges credits accordingly.
result = client.score(
title="10 Tips to Boost Your Coding Productivity",
thumbnail_url="https://i.ytimg.com/vi/abc123/maxresdefault.jpg",
)
print(result.score, result.percentile, result.niche_label)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
title |
str | None |
conditional* | None |
Max 250 chars. |
thumbnail_url |
str | None |
conditional* | None |
Public URL. Max 1024 chars. Mutually exclusive with thumbnail_base64. |
thumbnail_base64 |
str | None |
conditional* | None |
Base64-encoded image. Max 12.5 MB encoded. Mutually exclusive with thumbnail_url. |
channel_url |
str | None |
no | None |
Reserved — currently accepted but not consumed by the model. |
* At least one of title, thumbnail_url, or thumbnail_base64 is required.
Returns PackagingScoreResponse
| Field | Type | Description |
|---|---|---|
score_id |
UUID |
Opaque identifier for this scoring call. |
score |
float (0–1) |
Calibrated CTR percentile rank. |
percentile |
int (0–100) |
round(score * 100). |
raw_score |
float | None |
Continuous pre-calibration sigmoid output. Useful for fine-grained comparisons that the percentile-ranked score would round into the same plateau. |
mode |
"title" | "thumbnail" | "combined" |
Server-detected scoring mode. |
niche_slug |
str |
Canonical niche slug. Same taxonomy as /v1/research/* and /v1/benchmark/*. |
niche_label |
str |
Human-readable niche name. |
niche_confidence |
float |
Confidence (cosine similarity) of the niche assignment. |
Cost: 1 credit for title-only, 2 credits for thumbnail-only, 3 credits for combined (the combined mode captures interaction effects the single-input modes miss).
client.video_hook(...) — hook quality of the first ~6 s
Score the first ~6 seconds of a YouTube video for hook quality. Returns a structured analysis with archetype classification, five dimension scores, and actionable strengths / weaknesses / suggestions.
result = client.video_hook(
youtube_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
)
print(result.primary_archetype, result.overall_score)
print(result.scores.clarity, result.scores.tension)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
youtube_url |
str |
yes | — | Public YouTube URL, max 1024 chars. Accepts youtube.com/watch?v=…, youtu.be/…, youtube.com/shorts/…, youtube.com/embed/…. |
Returns VideoHookScoreResponse
| Field | Type | Description |
|---|---|---|
score_id |
UUID |
Opaque identifier for this scoring call. |
primary_archetype |
str |
One of 13 archetypes (curiosity_gap, direct_question, bold_claim, in_medias_res, stakes_statement, promise, pattern_interrupt, authority, demonstration, empathy, negative_framing, cold_open, greetings). |
secondary_archetype |
str | None |
Optional second archetype if the hook blends two. |
scores |
Scores (nested) |
Five 0–10 dimension scores. See below. |
overall_score |
int (0–100) |
Holistic hook-quality score. |
transcript |
str |
First-6-seconds transcript. May be blank for music-only or visual-only openings. |
visual_summary |
str |
Short description of the visual content. |
strengths |
list[str] (2–4 items) |
What the hook does well. |
weaknesses |
list[str] (2–4 items) |
What the hook does poorly. |
suggestions |
list[str] (2–4 items) |
Concrete edits the creator could make. |
delta_vs_niche_top |
int |
Difference between this hook's overall_score and the niche's top performers. |
key_differences_vs_top |
list[str] (1–3 items) |
Specific ways this hook differs from niche-top hooks. |
Nested scores:
| Field | Type | Description |
|---|---|---|
clarity |
int (0–10) |
How quickly the viewer understands the topic. |
specificity |
int (0–10) |
How concrete the promise is. |
tension |
int (0–10) |
Stakes / curiosity gap. |
visual_energy |
int (0–10) |
Movement, cuts, visual contrast. |
pace |
int (0–10) |
Words / cuts per second. |
Cost: 10 credits per call.
client.benchmark_channel(...) — channel vs. niche
Benchmark a YouTube channel against its niche distribution. Fetches the channel's recent videos, classifies the niche, and percentile-ranks engagement and title patterns.
result = client.benchmark_channel(
url="https://www.youtube.com/@MarquesBrownlee",
)
print(result.niche.slug, result.niche.match_strength)
print(result.channel.engagement_percentiles.overall)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
url |
str |
yes | — | Channel URL or @handle, max 1024 chars. Accepts youtube.com/@handle, /channel/UC…, /c/<name>, /user/<legacy>. |
Returns BenchmarkChannelResponse — two top-level objects, channel and niche.
channel
| Field | Type | Description |
|---|---|---|
channel_id |
str |
YouTube channel ID (UC…). |
title |
str |
Channel display name. |
subscriber_count |
int |
Public subscriber count at fetch time. |
video_count |
int |
Total public videos. |
engagement |
object | Raw ratios. view_to_sub_ratio, like_to_view_ratio, comment_to_view_ratio — each float | None. |
engagement_percentiles |
object | The same three ratios plus overall, each ranked into int (0–100) | None against the niche distribution. |
sample_window_days |
int |
Date span (in days) of the fetched sample, so callers can weight the signal against the channel's posting cadence. |
title_patterns |
object | Aggregate stats: mean_length_chars, mean_length_words, share_with_question_mark, share_with_number, median_uppercase_ratio, share_with_emoji. Each float | None (or int | None for the length fields). |
niche
| Field | Type | Description |
|---|---|---|
slug |
str |
Canonical niche slug. |
name |
str |
Human-readable niche name. |
match_score |
float |
Cosine similarity of the channel to the niche centroid. |
match_strength |
"strong" | "moderate" | "weak" |
Categorized confidence. |
engagement |
object | Three ratio-keyed distribution objects (view_to_sub_ratio, like_to_view_ratio, comment_to_view_ratio). Each carries p10, p25, p50, p75, p90, p95 — float | None. |
title_patterns |
object | Same fields as channel.title_patterns plus common_niche_phrases: list of {phrase: str, frequency: float | None, used_by_channel: bool}. |
exemplar_channels |
list (up to 5) |
{title: str | None, subscriber_count: int | None}. |
Cost: 5 credits per call.
client.benchmark_video(...) — single video vs. niche
Benchmark one YouTube video against its niche distribution.
result = client.benchmark_video(
url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
)
print(result.video.engagement_percentiles.overall)
print(result.video.title_patterns.fits_niche_patterns)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
url |
str |
yes | — | YouTube video URL, max 1024 chars. Accepts youtube.com/watch?v=…, youtu.be/…, youtube.com/shorts/…, youtube.com/embed/…. |
Returns BenchmarkVideoResponse — two top-level objects, video and niche.
video
| Field | Type | Description |
|---|---|---|
video_id |
str |
11-character YouTube video ID. |
title |
str |
Video title. May be blank. |
channel_title |
str |
Owning channel's display name. May be blank. |
published_at |
datetime |
UTC publish timestamp. |
view_count |
int |
Public view count at fetch time. |
like_count |
int | None |
None when likes are hidden on the video. |
comment_count |
int | None |
None when comments are disabled. |
engagement |
object | view_to_sub_ratio, like_to_view_ratio, comment_to_view_ratio — each float | None. |
engagement_percentiles |
object | Same three ratios plus overall, each int (0–100) | None. |
title_patterns |
object | length_chars (int | None), has_question (bool), has_number (bool), has_emoji (bool), fits_niche_patterns (bool). |
niche
Same shape as benchmark_channel's niche — slug, name, match_score,
match_strength, ratio-keyed engagement distributions, title_patterns with
common_niche_phrases. The used_by_channel flag on each common phrase here
means "did this single video's title use the phrase"; the serializer field name
stays the same across both benchmark endpoints.
Cost: 3 credits per call.
client.niches() — list catalogued niches
List every catalogued YouTube niche for the active research run. Use the
slug values as the niche argument to client.content_gaps(...).
result = client.niches()
for n in result.niches:
print(n.slug, n.gap_count)
Parameters: none.
Returns NicheListResponse
| Field | Type | Description |
|---|---|---|
niches |
list[NicheSummary] |
Flat array of catalogued niches. |
Each NicheSummary:
| Field | Type | Description |
|---|---|---|
slug |
str |
Canonical niche slug. |
name |
str |
Human-readable name. |
gap_count |
int |
Number of content gaps catalogued for that niche in the active run, for the default gap types. 0 means the niche exists in the taxonomy but no gaps have been ranked yet. |
Cost: 1 credit per call.
client.content_gaps(...) — ranked content opportunities
Return ranked content opportunities (gaps) for a YouTube niche.
result = client.content_gaps(
niche="python_programming_tutorials",
gap_type=["underserved", "stale"], # or just "underserved"
limit=10,
min_score=60,
)
for gap in result.gaps:
print(gap.opportunity_score, gap.canonical_title)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
niche |
str |
yes | — | Slug from client.niches(), max 100 chars. |
gap_type |
str | list[str] | None |
no | None (server defaults to ["underserved", "stale"]) |
One of "underserved", "stale", "competitive", or a list of those. "competitive" is opt-in (omit from the list to exclude it). |
limit |
int | None |
no | None (server default: 20) |
1–50. |
min_score |
int | None |
no | None (server default: 0) |
Minimum opportunity_score (0–100) to include. |
Returns ContentGapsResponse
| Field | Type | Description |
|---|---|---|
niche |
object | {slug: str, name: str}. |
gaps |
list[ContentGapsItem] |
Ranked gaps (see below). |
Each gap (ContentGapsItem):
| Field | Type | Description |
|---|---|---|
canonical_title |
str |
Suggested canonical title for the gap. |
opportunity_score |
int (0–100) |
Composite ranking score. |
gap_type |
"underserved" | "stale" | "competitive" |
Which category the gap falls into. |
components |
object | {demand: float, supply: float, recency: float} — the three signals that make up opportunity_score. |
explanation |
str |
Short rationale for why this gap exists. May be blank. |
suggested_angles |
list[str] |
Concrete video angles the creator could take. |
evidence |
object | Supporting signals. See below. |
Nested evidence:
| Field | Type | Description |
|---|---|---|
newest_quality_video_age_days |
int | None |
Age (in days) of the newest high-quality video in this gap; None if no qualifying video exists. |
trends_appearance_count |
int |
How many times this topic appeared in trends signals. |
autocomplete_rank |
int | None |
YouTube autocomplete rank, if observed. |
residual_outlier_count |
int |
Number of outlier signals contributing to the score. |
top_competitors |
list (up to 5) |
{title: str, channel_title: str, subscriber_count: int | None, view_count: int, age_days: int, published_at: datetime}. |
related_queries |
list[str] (up to 5) |
Adjacent search queries. |
Cost: 5 credits per call.
client.me() — account info
Return account info and plan details for the authenticated API key. Free to call.
me = client.me()
print(me.email, me.plan.name, me.credits_remaining)
Parameters: none.
Returns MeResponse
| Field | Type | Description |
|---|---|---|
email |
str |
Account email. |
full_name |
str |
May be blank if the user hasn't set it. |
plan |
Plan (nested) |
{name: str, slug: str, monthly_credits: int, rate_limit_per_minute: int}. |
credits_remaining |
int |
Credits left in the current billing period. |
period_end |
datetime |
When the current billing period ends and credits reset. |
Cost: free (no credits deducted).
client.usage() — credit balance + recent activity
Return the credit balance and recent API activity for the authenticated API key. Free to call.
usage = client.usage()
print(usage.credits_remaining, "/", usage.credits_total)
for entry in usage.recent_activity:
print(entry.created_at, entry.endpoint, entry.delta)
Parameters: none.
Returns UsageResponse
| Field | Type | Description |
|---|---|---|
credits_remaining |
int |
Credits left in the current billing period. |
credits_total |
int |
Total credits for the current plan (resets at period_end). |
period_end |
datetime |
When the current billing period ends. |
recent_activity |
list[CreditLedgerEntry] |
Up to 20 most-recent credit-ledger entries. |
Each CreditLedgerEntry:
| Field | Type | Description |
|---|---|---|
delta |
int |
Signed change. Negative for API charges. |
balance_after |
int |
Credit balance immediately after this entry. |
reason |
str |
Free-form reason (e.g. "api_call", "monthly_reset"). |
endpoint |
str |
API endpoint path that triggered the entry. May be blank for non-API entries. |
created_at |
datetime |
UTC timestamp. |
Cost: free (no credits deducted).
Errors
The SDK raises typed exceptions for each documented status code:
| Status | Exception | Notes |
|---|---|---|
| 400 | BadRequestError |
Validation failed. |
| 401 | AuthenticationError |
Missing, invalid, or expired API key. |
| 402 | InsufficientCreditsError |
Account is out of credits. |
| 429 | RateLimitError |
Too many requests. retry_after (int seconds). |
| 503 | ServiceUnavailableError |
Scoring service down. retry_after (int seconds). |
| 5xx | ServerError |
Other server-side failure. |
| network | NetworkError |
DNS, connection refused, TLS, timeout. |
All API errors inherit BrightBeanAPIError. Network failures raise
NetworkError. Both inherit BrightBeanError for catch-all handling.
Retries
Idempotent GETs (me(), usage()) retry automatically on 5xx and network
errors with exponential backoff + jitter. Tune via max_retries:
client = BrightBean(api_key="bb_...", max_retries=5)
POST endpoints and paid GETs are never auto-retried because the server may
have charged credits before the network response was lost. Catch
RateLimitError or ServiceUnavailableError and back off using retry_after
if you need retry semantics.
Configuration
BrightBean(
api_key="bb_...", # required
base_url="https://api.brightbean.xyz",
timeout=30.0, # seconds, or httpx.Timeout(...)
max_retries=3, # idempotent GETs only
user_agent=None, # override the default UA string
)
base_url defaults to https://api.brightbean.xyz. Override for staging or
local testing.
Development
pip install -e .[dev]
openapi-python-client generate \
--path ../../openapi.yaml \
--config generator.yaml \
--meta=none \
--output-path ./brightbean/_generated \
--overwrite
pytest && ruff check . && mypy brightbean
The brightbean/_generated/ directory is regenerated from the upstream
OpenAPI spec. The hand-written facade (brightbean/_client.py,
brightbean/errors.py, brightbean/__init__.py) wraps it to give the
ergonomic BrightBean import shape.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file brightbean-1.0.0.tar.gz.
File metadata
- Download URL: brightbean-1.0.0.tar.gz
- Upload date:
- Size: 38.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c5a2714de5a4f42d424a528bde1bbe8e376a32b98bb716cde883ad57fc9ecf9
|
|
| MD5 |
42891917b06b66a64eeab8ca208effc3
|
|
| BLAKE2b-256 |
6f675d3411fba07a4daeff135288792dbe3253550835e3574cb7560e8eb75e38
|
Provenance
The following attestation bundles were made for brightbean-1.0.0.tar.gz:
Publisher:
python-release.yml on JanSchm/social-intelligence-app
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
brightbean-1.0.0.tar.gz -
Subject digest:
9c5a2714de5a4f42d424a528bde1bbe8e376a32b98bb716cde883ad57fc9ecf9 - Sigstore transparency entry: 1547231387
- Sigstore integration time:
-
Permalink:
JanSchm/social-intelligence-app@9877b2abbab1ebf581f82d7ec3d8be0c51d1dc67 -
Branch / Tag:
refs/tags/python-v1.0.0 - Owner: https://github.com/JanSchm
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-release.yml@9877b2abbab1ebf581f82d7ec3d8be0c51d1dc67 -
Trigger Event:
push
-
Statement type:
File details
Details for the file brightbean-1.0.0-py3-none-any.whl.
File metadata
- Download URL: brightbean-1.0.0-py3-none-any.whl
- Upload date:
- Size: 80.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1662bb1bbacceeee7e0a9980e7084f553c90e6f44942fd6412732a77cc1b34f8
|
|
| MD5 |
76a0a214a65d57aa25ffb9ac2e0a07ba
|
|
| BLAKE2b-256 |
1415215c43a95420515d2681bbab03009a02caf07980890f71d3e8da659e22a7
|
Provenance
The following attestation bundles were made for brightbean-1.0.0-py3-none-any.whl:
Publisher:
python-release.yml on JanSchm/social-intelligence-app
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
brightbean-1.0.0-py3-none-any.whl -
Subject digest:
1662bb1bbacceeee7e0a9980e7084f553c90e6f44942fd6412732a77cc1b34f8 - Sigstore transparency entry: 1547231408
- Sigstore integration time:
-
Permalink:
JanSchm/social-intelligence-app@9877b2abbab1ebf581f82d7ec3d8be0c51d1dc67 -
Branch / Tag:
refs/tags/python-v1.0.0 - Owner: https://github.com/JanSchm
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-release.yml@9877b2abbab1ebf581f82d7ec3d8be0c51d1dc67 -
Trigger Event:
push
-
Statement type: