tubesentry 0.1.0__py3-none-any.whl
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.
- tubesentry/__init__.py +96 -0
- tubesentry/cli.py +180 -0
- tubesentry/contracts/__init__.py +51 -0
- tubesentry/contracts/exceptions.py +21 -0
- tubesentry/contracts/schemas.py +135 -0
- tubesentry/contracts/taxonomy.py +66 -0
- tubesentry/core/__init__.py +22 -0
- tubesentry/core/auditor.py +336 -0
- tubesentry/core/heuristics.py +238 -0
- tubesentry/core/seed_db.py +140 -0
- tubesentry/io/__init__.py +13 -0
- tubesentry/io/fixtures.py +202 -0
- tubesentry/io/ingestor.py +379 -0
- tubesentry/mcp_server.py +206 -0
- tubesentry/pro/__init__.py +8 -0
- tubesentry/pro/bridge.py +77 -0
- tubesentry/pro/installer.py +132 -0
- tubesentry/py.typed +1 -0
- tubesentry-0.1.0.dist-info/METADATA +234 -0
- tubesentry-0.1.0.dist-info/RECORD +23 -0
- tubesentry-0.1.0.dist-info/WHEEL +4 -0
- tubesentry-0.1.0.dist-info/entry_points.txt +3 -0
- tubesentry-0.1.0.dist-info/licenses/LICENSE +21 -0
tubesentry/__init__.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from tubesentry.contracts.schemas import AuditSummaryReport
|
|
10
|
+
from tubesentry.core.auditor import run_free_audit
|
|
11
|
+
from tubesentry.io.ingestor import load_placement_report
|
|
12
|
+
from tubesentry.pro import bridge
|
|
13
|
+
from tubesentry.pro.bridge import dispatch_pro_audit, is_pro_available
|
|
14
|
+
from tubesentry.pro.installer import install_pro
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def audit_placements(
|
|
20
|
+
report: str | Path | pd.DataFrame,
|
|
21
|
+
target_languages: list[str] | None = None,
|
|
22
|
+
target_cpa: float | None = None,
|
|
23
|
+
api_key: str | None = None,
|
|
24
|
+
youtube_api_key: str | None = None,
|
|
25
|
+
export_exclusions_csv: str | None = None,
|
|
26
|
+
export_65k_shield_csv: str | None = None,
|
|
27
|
+
export_html_deck: str | None = None,
|
|
28
|
+
white_label_config: dict[str, Any] | None = None,
|
|
29
|
+
print_banner: bool = True,
|
|
30
|
+
ad_url: str | None = None,
|
|
31
|
+
ad_text: str | None = None,
|
|
32
|
+
gemini_api_key: str | None = None,
|
|
33
|
+
) -> AuditSummaryReport:
|
|
34
|
+
"""Audits Google Ads ('Where ads showed') or DV360 YouTube placement reports for ad waste.
|
|
35
|
+
|
|
36
|
+
- Free Mode (default): Computes exact waste totals across all 6 categories,
|
|
37
|
+
unmasks the Top 25 worst offending channels, and prints the diagnostic banner.
|
|
38
|
+
- Pro Mode (with valid `api_key` and `tubesentry_pro` installed): Uses the 250k
|
|
39
|
+
channel intelligence registry, 50-ID batch API enrichment, unmasks all channels,
|
|
40
|
+
evaluates Gemini creative targeting fit, and exports Google Ads Editor exclusion files & white-label HTML deck.
|
|
41
|
+
"""
|
|
42
|
+
langs = target_languages or ["de", "en"]
|
|
43
|
+
df = load_placement_report(report) if not isinstance(report, pd.DataFrame) else report
|
|
44
|
+
|
|
45
|
+
effective_key = api_key or os.getenv("TUBESENTRY_API_KEY")
|
|
46
|
+
|
|
47
|
+
# If Pro is available and a license key is provided (or Pro exports requested)
|
|
48
|
+
if is_pro_available() and bridge.tubesentry_pro is not None and effective_key:
|
|
49
|
+
return dispatch_pro_audit(
|
|
50
|
+
df=df,
|
|
51
|
+
target_languages=langs,
|
|
52
|
+
target_cpa=target_cpa,
|
|
53
|
+
api_key=effective_key,
|
|
54
|
+
youtube_api_key=youtube_api_key,
|
|
55
|
+
export_exclusions_csv=export_exclusions_csv,
|
|
56
|
+
export_65k_shield_csv=export_65k_shield_csv,
|
|
57
|
+
export_html_deck=export_html_deck,
|
|
58
|
+
white_label_config=white_label_config,
|
|
59
|
+
ad_url=ad_url,
|
|
60
|
+
ad_text=ad_text,
|
|
61
|
+
gemini_api_key=gemini_api_key,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# If caller specifically requested Pro exports without Pro/license, route through dispatcher to surface upgrade error
|
|
65
|
+
if export_exclusions_csv or export_65k_shield_csv or export_html_deck or ad_url or ad_text:
|
|
66
|
+
return dispatch_pro_audit(
|
|
67
|
+
df=df,
|
|
68
|
+
target_languages=langs,
|
|
69
|
+
target_cpa=target_cpa,
|
|
70
|
+
api_key=effective_key,
|
|
71
|
+
youtube_api_key=youtube_api_key,
|
|
72
|
+
export_exclusions_csv=export_exclusions_csv,
|
|
73
|
+
export_65k_shield_csv=export_65k_shield_csv,
|
|
74
|
+
export_html_deck=export_html_deck,
|
|
75
|
+
white_label_config=white_label_config,
|
|
76
|
+
ad_url=ad_url,
|
|
77
|
+
ad_text=ad_text,
|
|
78
|
+
gemini_api_key=gemini_api_key,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return run_free_audit(
|
|
82
|
+
df=df,
|
|
83
|
+
target_languages=langs,
|
|
84
|
+
target_cpa=target_cpa,
|
|
85
|
+
print_banner=print_banner,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = [
|
|
90
|
+
"AuditSummaryReport",
|
|
91
|
+
"__version__",
|
|
92
|
+
"audit_placements",
|
|
93
|
+
"install_pro",
|
|
94
|
+
"is_pro_available",
|
|
95
|
+
"load_placement_report",
|
|
96
|
+
]
|
tubesentry/cli.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
import tubesentry
|
|
9
|
+
from tubesentry.contracts.exceptions import ProLicenseError
|
|
10
|
+
from tubesentry.io.fixtures import generate_synthetic_placement_csv
|
|
11
|
+
from tubesentry.pro.installer import install_pro
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="tubesentry",
|
|
15
|
+
help="TubeSentry — Local-First YouTube & PMax Ad Placement Waste Auditor & MCP Server",
|
|
16
|
+
add_completion=False,
|
|
17
|
+
)
|
|
18
|
+
console = Console()
|
|
19
|
+
err_console = Console(stderr=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command()
|
|
23
|
+
def demo(
|
|
24
|
+
out_dir: Path = typer.Option(
|
|
25
|
+
Path("./demo_youtube"),
|
|
26
|
+
"--out-dir",
|
|
27
|
+
"-o",
|
|
28
|
+
help="Directory to save the generated synthetic placement CSV report.",
|
|
29
|
+
),
|
|
30
|
+
rows: int = typer.Option(
|
|
31
|
+
25000,
|
|
32
|
+
"--rows",
|
|
33
|
+
"-n",
|
|
34
|
+
help="Number of synthetic placement rows to generate.",
|
|
35
|
+
),
|
|
36
|
+
lang: str = typer.Option(
|
|
37
|
+
"de",
|
|
38
|
+
"--lang",
|
|
39
|
+
"-l",
|
|
40
|
+
help="Localized number and text formatting ('de' or 'en').",
|
|
41
|
+
),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Generates a realistic 25,000-row Google Ads PMax/YouTube placement CSV and runs an immediate audit."""
|
|
44
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
csv_file = out_dir / "google_ads_where_ads_showed_sample.csv"
|
|
46
|
+
|
|
47
|
+
console.print(
|
|
48
|
+
f"[bold blue][*] Generating {rows:,} synthetic Google Ads placement rows...[/bold blue]"
|
|
49
|
+
)
|
|
50
|
+
generate_synthetic_placement_csv(
|
|
51
|
+
output_path=csv_file,
|
|
52
|
+
num_rows=rows,
|
|
53
|
+
language=lang,
|
|
54
|
+
)
|
|
55
|
+
console.print(f"[bold green][OK] Saved synthetic report to:[/bold green] {csv_file}")
|
|
56
|
+
console.print("[bold yellow][*] Running immediate TubeSentry diagnostic pass...[/bold yellow]\n")
|
|
57
|
+
|
|
58
|
+
tubesentry.audit_placements(
|
|
59
|
+
report=csv_file,
|
|
60
|
+
target_languages=["de", "en"] if lang == "de" else ["en"],
|
|
61
|
+
print_banner=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def audit(
|
|
67
|
+
report_csv: Path = typer.Argument(
|
|
68
|
+
...,
|
|
69
|
+
help="Path to Google Ads or DV360 placement CSV/TSV export file.",
|
|
70
|
+
exists=True,
|
|
71
|
+
file_okay=True,
|
|
72
|
+
dir_okay=False,
|
|
73
|
+
readable=True,
|
|
74
|
+
),
|
|
75
|
+
lang: str = typer.Option(
|
|
76
|
+
"de,en",
|
|
77
|
+
"--lang",
|
|
78
|
+
"-l",
|
|
79
|
+
help="Comma-separated target languages for geotargeting verification (e.g. 'de,en').",
|
|
80
|
+
),
|
|
81
|
+
cpa: float | None = typer.Option(
|
|
82
|
+
None,
|
|
83
|
+
"--cpa",
|
|
84
|
+
help="Target CPA in account currency to evaluate Performance Sinkhole bleed.",
|
|
85
|
+
),
|
|
86
|
+
key: str | None = typer.Option(
|
|
87
|
+
None,
|
|
88
|
+
"--key",
|
|
89
|
+
"-k",
|
|
90
|
+
help="TubeSentry Pro Polar.sh API key (or set TUBESENTRY_API_KEY).",
|
|
91
|
+
),
|
|
92
|
+
out_exclusions: Path | None = typer.Option(
|
|
93
|
+
None,
|
|
94
|
+
"--out-exclusions",
|
|
95
|
+
help="Path to export reactive Google Ads Editor exclusion CSV (Pro feature).",
|
|
96
|
+
),
|
|
97
|
+
out_65k: Path | None = typer.Option(
|
|
98
|
+
None,
|
|
99
|
+
"--out-65k",
|
|
100
|
+
help="Path to export full 65,000-channel protection shield CSV (Pro feature).",
|
|
101
|
+
),
|
|
102
|
+
html: Path | None = typer.Option(
|
|
103
|
+
None,
|
|
104
|
+
"--html",
|
|
105
|
+
help="Path to export interactive white-label HTML audit deck (Pro feature).",
|
|
106
|
+
),
|
|
107
|
+
ad_url: str | None = typer.Option(
|
|
108
|
+
None,
|
|
109
|
+
"--ad-url",
|
|
110
|
+
help="YouTube Ad video URL or creative link to profile targeting fit using Gemini.",
|
|
111
|
+
),
|
|
112
|
+
ad_text: str | None = typer.Option(
|
|
113
|
+
None,
|
|
114
|
+
"--ad-text",
|
|
115
|
+
help="Ad copy, transcript, or campaign brief to profile ICP intent.",
|
|
116
|
+
),
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Audits a Google Ads or DV360 YouTube placement report for wasted ad spend."""
|
|
119
|
+
target_languages = [s.strip().lower() for s in lang.split(",") if s.strip()]
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
tubesentry.audit_placements(
|
|
123
|
+
report=report_csv,
|
|
124
|
+
target_languages=target_languages,
|
|
125
|
+
target_cpa=cpa,
|
|
126
|
+
api_key=key,
|
|
127
|
+
export_exclusions_csv=str(out_exclusions) if out_exclusions else None,
|
|
128
|
+
export_65k_shield_csv=str(out_65k) if out_65k else None,
|
|
129
|
+
export_html_deck=str(html) if html else None,
|
|
130
|
+
print_banner=True,
|
|
131
|
+
ad_url=ad_url,
|
|
132
|
+
ad_text=ad_text,
|
|
133
|
+
)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
err_console.print(f"[bold red]Audit error:[/bold red] {e}")
|
|
136
|
+
raise typer.Exit(code=1) from e
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.command()
|
|
140
|
+
def mcp() -> None:
|
|
141
|
+
"""Starts the FastMCP stdio server for Claude Desktop, Claude Code, and Cursor."""
|
|
142
|
+
from tubesentry.mcp_server import main as run_mcp
|
|
143
|
+
|
|
144
|
+
run_mcp()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.command()
|
|
148
|
+
def auth(
|
|
149
|
+
key: str | None = typer.Option(
|
|
150
|
+
None,
|
|
151
|
+
"--key",
|
|
152
|
+
"-k",
|
|
153
|
+
help="TubeSentry Pro license key from Polar.sh (e.g. 'TS_PRO_...').",
|
|
154
|
+
),
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Validates a Polar.sh license key and installs/activates TubeSentry Pro."""
|
|
157
|
+
license_key = key
|
|
158
|
+
if not license_key:
|
|
159
|
+
license_key = typer.prompt("TubeSentry Pro license key", hide_input=True)
|
|
160
|
+
|
|
161
|
+
console.print("[bold blue][*] Validating license key with Polar.sh...[/bold blue]")
|
|
162
|
+
try:
|
|
163
|
+
success = install_pro(license_key)
|
|
164
|
+
if success:
|
|
165
|
+
console.print(
|
|
166
|
+
"[bold green][OK] License validated successfully! TubeSentry Pro is active.[/bold green]"
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
console.print("[bold red][FAIL] Installation failed.[/bold red]")
|
|
170
|
+
raise typer.Exit(code=1)
|
|
171
|
+
except ProLicenseError as e:
|
|
172
|
+
console.print(f"[bold red][FAIL] License validation error:[/bold red] {e}")
|
|
173
|
+
raise typer.Exit(code=1) from e
|
|
174
|
+
except Exception as e:
|
|
175
|
+
console.print(f"[bold red][FAIL] Error installing Pro extension:[/bold red] {e}")
|
|
176
|
+
raise typer.Exit(code=1) from e
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
app()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from tubesentry.contracts.exceptions import (
|
|
2
|
+
MalformedPlacementCSVError,
|
|
3
|
+
ProFeatureNotInstalledError,
|
|
4
|
+
ProLicenseError,
|
|
5
|
+
QuotaExceededError,
|
|
6
|
+
TubeSentryError,
|
|
7
|
+
)
|
|
8
|
+
from tubesentry.contracts.schemas import (
|
|
9
|
+
AuditSummaryReport,
|
|
10
|
+
ClassifiedChannelRecord,
|
|
11
|
+
StandardizedPlacementRow,
|
|
12
|
+
WasteCategoryBreakdown,
|
|
13
|
+
WasteCategoryType,
|
|
14
|
+
)
|
|
15
|
+
from tubesentry.contracts.taxonomy import (
|
|
16
|
+
BITMASK_ANIMATION,
|
|
17
|
+
BITMASK_EDUCATION,
|
|
18
|
+
BITMASK_ENTERTAINMENT,
|
|
19
|
+
BITMASK_GAMING,
|
|
20
|
+
BITMASK_KIDS,
|
|
21
|
+
BITMASK_LIFESTYLE,
|
|
22
|
+
BITMASK_MUSIC,
|
|
23
|
+
BITMASK_NEWS,
|
|
24
|
+
CATEGORY_DESCRIPTIONS,
|
|
25
|
+
FREEBASE_TOPIC_MAPPINGS,
|
|
26
|
+
WasteCategory,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"BITMASK_ANIMATION",
|
|
31
|
+
"BITMASK_EDUCATION",
|
|
32
|
+
"BITMASK_ENTERTAINMENT",
|
|
33
|
+
"BITMASK_GAMING",
|
|
34
|
+
"BITMASK_KIDS",
|
|
35
|
+
"BITMASK_LIFESTYLE",
|
|
36
|
+
"BITMASK_MUSIC",
|
|
37
|
+
"BITMASK_NEWS",
|
|
38
|
+
"CATEGORY_DESCRIPTIONS",
|
|
39
|
+
"FREEBASE_TOPIC_MAPPINGS",
|
|
40
|
+
"AuditSummaryReport",
|
|
41
|
+
"ClassifiedChannelRecord",
|
|
42
|
+
"MalformedPlacementCSVError",
|
|
43
|
+
"ProFeatureNotInstalledError",
|
|
44
|
+
"ProLicenseError",
|
|
45
|
+
"QuotaExceededError",
|
|
46
|
+
"StandardizedPlacementRow",
|
|
47
|
+
"TubeSentryError",
|
|
48
|
+
"WasteCategory",
|
|
49
|
+
"WasteCategoryBreakdown",
|
|
50
|
+
"WasteCategoryType",
|
|
51
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TubeSentryError(Exception):
|
|
5
|
+
"""Base exception for all TubeSentry errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MalformedPlacementCSVError(TubeSentryError):
|
|
9
|
+
"""Raised when an uploaded placement report CSV/TSV has invalid structure or missing columns."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QuotaExceededError(TubeSentryError):
|
|
13
|
+
"""Raised when YouTube Data API v3 daily quota limit is reached."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProFeatureNotInstalledError(TubeSentryError):
|
|
17
|
+
"""Raised when a Pro feature is accessed without `tubesentry_pro` installed."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProLicenseError(TubeSentryError):
|
|
21
|
+
"""Raised when Polar.sh license validation fails or license key is revoked/missing."""
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
WasteCategoryType = Literal[
|
|
8
|
+
"CLEAN",
|
|
9
|
+
"MADE_FOR_KIDS",
|
|
10
|
+
"PASSIVE_AUDIO_MUSIC",
|
|
11
|
+
"LOW_AGE_GAMING",
|
|
12
|
+
"FOREIGN_SCRIPT_LEAK",
|
|
13
|
+
"AI_SLOP_CLICK_FARM",
|
|
14
|
+
"PERFORMANCE_SINKHOLE",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StandardizedPlacementRow(BaseModel):
|
|
19
|
+
"""Normalized row extracted from Google Ads or DV360 placement report."""
|
|
20
|
+
raw_placement: str = Field(..., description="Original placement URL or string from CSV")
|
|
21
|
+
channel_id: str | None = Field(default=None, pattern=r"^UC[a-zA-Z0-9_-]{22}$")
|
|
22
|
+
handle_or_video_id: str | None = Field(default=None)
|
|
23
|
+
channel_title: str | None = Field(default=None)
|
|
24
|
+
placement_type: str = Field(default="YouTube channel")
|
|
25
|
+
impressions: int = Field(default=0, ge=0)
|
|
26
|
+
views: int = Field(default=0, ge=0)
|
|
27
|
+
clicks: int = Field(default=0, ge=0)
|
|
28
|
+
cost: float = Field(default=0.0, ge=0.0)
|
|
29
|
+
conversions: float = Field(default=0.0, ge=0.0)
|
|
30
|
+
currency_code: str = Field(default="EUR")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ClassifiedChannelRecord(BaseModel):
|
|
34
|
+
"""Enriched and scored YouTube channel record."""
|
|
35
|
+
channel_id: str
|
|
36
|
+
channel_title: str
|
|
37
|
+
waste_category: WasteCategoryType
|
|
38
|
+
waste_confidence: float = Field(..., ge=0.0, le=1.0)
|
|
39
|
+
made_for_kids_official: bool = False
|
|
40
|
+
is_young_kids: bool = False
|
|
41
|
+
granular_sub_category: str | None = None
|
|
42
|
+
thumbnail_url: str | None = None
|
|
43
|
+
detected_language: str | None = None
|
|
44
|
+
detected_country: str | None = None
|
|
45
|
+
topic_categories: list[str] = Field(default_factory=list)
|
|
46
|
+
cost: float
|
|
47
|
+
impressions: int
|
|
48
|
+
views: int
|
|
49
|
+
clicks: int
|
|
50
|
+
conversions: float
|
|
51
|
+
reason_summary: str
|
|
52
|
+
is_masked: bool = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WasteCategoryBreakdown(BaseModel):
|
|
56
|
+
"""Spend and channel count summary for an individual waste category."""
|
|
57
|
+
category: WasteCategoryType
|
|
58
|
+
wasted_spend: float = 0.0
|
|
59
|
+
wasted_spend_percentage: float = 0.0
|
|
60
|
+
channel_count: int = 0
|
|
61
|
+
impressions: int = 0
|
|
62
|
+
views: int = 0
|
|
63
|
+
clicks: int = 0
|
|
64
|
+
conversions: float = 0.0
|
|
65
|
+
description: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CreativeTargetingProfile(BaseModel):
|
|
69
|
+
"""Ideal targeting persona and contextual boundaries extracted from an ad creative."""
|
|
70
|
+
ad_url: str | None = None
|
|
71
|
+
ad_title: str | None = None
|
|
72
|
+
brand_or_product: str = "Advertiser Campaign"
|
|
73
|
+
target_persona: str = Field(..., description="Target audience / Ideal Customer Profile (ICP)")
|
|
74
|
+
target_intent: str = Field(default="", description="Core value proposition and intent")
|
|
75
|
+
recommended_contextual_topics: list[str] = Field(
|
|
76
|
+
default_factory=list,
|
|
77
|
+
description="Ideal YouTube topic/category entities (e.g. Cloud computing, Enterprise software)",
|
|
78
|
+
)
|
|
79
|
+
negative_anti_personas: list[str] = Field(
|
|
80
|
+
default_factory=list,
|
|
81
|
+
description="Incompatible audiences/categories (e.g. Children, Toddlers, Gaming, Passive Audio)",
|
|
82
|
+
)
|
|
83
|
+
recommended_languages: list[str] = Field(default_factory=lambda: ["en", "de"])
|
|
84
|
+
profiling_source: str = "gemini" # 'gemini' | 'heuristic' | 'brief'
|
|
85
|
+
summary: str = ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ExpectationVsRealityItem(BaseModel):
|
|
89
|
+
"""Individual dimension contrast comparing creative intent against actual delivery."""
|
|
90
|
+
dimension: str
|
|
91
|
+
creative_expectation: str
|
|
92
|
+
delivery_reality: str
|
|
93
|
+
alignment_status: Literal["ON_TARGET", "NEUTRAL", "MISALIGNED", "CRITICAL_WASTE"]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CreativeAlignmentSummary(BaseModel):
|
|
97
|
+
"""Comparison of creative targeting expectations vs actual ad placement delivery."""
|
|
98
|
+
profile: CreativeTargetingProfile
|
|
99
|
+
overall_alignment_score: float = Field(..., ge=0.0, le=10.0, description="1-10 alignment score")
|
|
100
|
+
alignment_verdict: str = Field(..., description="High-level narrative verdict")
|
|
101
|
+
on_target_spend: float = 0.0
|
|
102
|
+
on_target_percentage: float = 0.0
|
|
103
|
+
off_target_spend: float = 0.0
|
|
104
|
+
off_target_percentage: float = 0.0
|
|
105
|
+
neutral_spend: float = 0.0
|
|
106
|
+
neutral_percentage: float = 0.0
|
|
107
|
+
top_misaligned_channels: list[dict[str, Any]] = Field(default_factory=list)
|
|
108
|
+
expectation_vs_reality: list[ExpectationVsRealityItem] = Field(default_factory=list)
|
|
109
|
+
strategic_recommendations: list[str] = Field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AuditSummaryReport(BaseModel):
|
|
113
|
+
"""Comprehensive executive audit report generated from placement analysis."""
|
|
114
|
+
total_placements_audited: int
|
|
115
|
+
total_spend: float
|
|
116
|
+
total_wasted_spend: float
|
|
117
|
+
wasted_spend_percentage: float
|
|
118
|
+
total_waste_channels: int
|
|
119
|
+
total_impressions_audited: int = 0
|
|
120
|
+
total_wasted_impressions: int = 0
|
|
121
|
+
wasted_impressions_percentage: float = 0.0
|
|
122
|
+
weighted_quality_score: float = 10.0
|
|
123
|
+
top_topic_categories: list[dict[str, Any]] = Field(default_factory=list)
|
|
124
|
+
currency_code: str = "EUR"
|
|
125
|
+
category_breakdowns: dict[str, WasteCategoryBreakdown]
|
|
126
|
+
top_offenders: list[ClassifiedChannelRecord]
|
|
127
|
+
masked_offenders_count: int = 0
|
|
128
|
+
is_pro_audit: bool = False
|
|
129
|
+
coppa_kids_risk_spend: float = 0.0
|
|
130
|
+
annualized_recoverable_runrate: float = 0.0
|
|
131
|
+
exclusions_csv_path: str | None = None
|
|
132
|
+
shield_65k_csv_path: str | None = None
|
|
133
|
+
html_report_path: str | None = None
|
|
134
|
+
upgrade_prompt: str | None = None
|
|
135
|
+
creative_alignment: CreativeAlignmentSummary | None = None
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WasteCategory(str, Enum):
|
|
7
|
+
"""Canonical 6-Vector Waste Taxonomy for YouTube Placement Audits."""
|
|
8
|
+
CLEAN = "CLEAN"
|
|
9
|
+
MADE_FOR_KIDS = "MADE_FOR_KIDS"
|
|
10
|
+
PASSIVE_AUDIO_MUSIC = "PASSIVE_AUDIO_MUSIC"
|
|
11
|
+
LOW_AGE_GAMING = "LOW_AGE_GAMING"
|
|
12
|
+
FOREIGN_SCRIPT_LEAK = "FOREIGN_SCRIPT_LEAK"
|
|
13
|
+
AI_SLOP_CLICK_FARM = "AI_SLOP_CLICK_FARM"
|
|
14
|
+
PERFORMANCE_SINKHOLE = "PERFORMANCE_SINKHOLE"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Freebase topic IDs and Wikipedia ontology mappings from YouTube Data API v3
|
|
18
|
+
FREEBASE_TOPIC_MAPPINGS = {
|
|
19
|
+
# Music topics
|
|
20
|
+
"/m/04rlf": "Music",
|
|
21
|
+
"/m/02mscn": "Christian music",
|
|
22
|
+
"/m/0ggq0m": "Classical music",
|
|
23
|
+
"/m/01lyv": "Country",
|
|
24
|
+
"/m/02lkt": "Electronic music",
|
|
25
|
+
"/m/0g293": "Music of Latin America",
|
|
26
|
+
"/m/064t9": "Pop music",
|
|
27
|
+
"/m/06cq9": "Reggae",
|
|
28
|
+
"/m/06j6l": "Rhythm and blues",
|
|
29
|
+
"/m/06by7": "Rock music",
|
|
30
|
+
"/m/0gywn": "Soul music",
|
|
31
|
+
"/m/0zdp4": "Hip hop music",
|
|
32
|
+
# Gaming topics
|
|
33
|
+
"/m/0bzvm2": "Gaming",
|
|
34
|
+
"/m/025zzc": "Action game",
|
|
35
|
+
"/m/02ntfj": "Action-adventure game",
|
|
36
|
+
"/m/0b1vjn": "Casual game",
|
|
37
|
+
"/m/0403hg": "Role-playing video game",
|
|
38
|
+
"/m/02hygl": "Simulation video game",
|
|
39
|
+
"/m/04q1x3q": "Sports game",
|
|
40
|
+
"/m/01sjng": "Strategy video game",
|
|
41
|
+
# Children & Animation topics
|
|
42
|
+
"/m/0164x2": "Children's music",
|
|
43
|
+
"/m/03k3r": "Animated cartoon",
|
|
44
|
+
"/m/019_rr": "Lifestyle",
|
|
45
|
+
"/m/028sqc": "Music of Asia",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Bitmask constants for compressed 250k registry
|
|
49
|
+
BITMASK_MUSIC = 1 << 0
|
|
50
|
+
BITMASK_GAMING = 1 << 1
|
|
51
|
+
BITMASK_KIDS = 1 << 2
|
|
52
|
+
BITMASK_ANIMATION = 1 << 3
|
|
53
|
+
BITMASK_ENTERTAINMENT = 1 << 4
|
|
54
|
+
BITMASK_EDUCATION = 1 << 5
|
|
55
|
+
BITMASK_NEWS = 1 << 6
|
|
56
|
+
BITMASK_LIFESTYLE = 1 << 7
|
|
57
|
+
|
|
58
|
+
CATEGORY_DESCRIPTIONS: dict[WasteCategory, str] = {
|
|
59
|
+
WasteCategory.CLEAN: "Verified clean audience placement with standard engagement.",
|
|
60
|
+
WasteCategory.MADE_FOR_KIDS: "Toddler & COPPA nursery rhymes, unboxing, cartoons, and child-directed channels.",
|
|
61
|
+
WasteCategory.PASSIVE_AUDIO_MUSIC: "Background audio, lofi beats, sleep music, rain noise, and zero-attention streams.",
|
|
62
|
+
WasteCategory.LOW_AGE_GAMING: "Underage gameplay (Roblox, Minecraft, Brawl Stars, Skibidi, Gacha) with accidental child taps.",
|
|
63
|
+
WasteCategory.FOREIGN_SCRIPT_LEAK: "Foreign language and non-Latin script leakage mismatched with campaign geotargeting.",
|
|
64
|
+
WasteCategory.AI_SLOP_CLICK_FARM: "High-frequency synthetic AI content farms, faceless TTS shorts, and click arbitrage.",
|
|
65
|
+
WasteCategory.PERFORMANCE_SINKHOLE: "Empirical budget bleed placements exceeding 3x Target CPA with zero conversions.",
|
|
66
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from tubesentry.core.auditor import run_free_audit
|
|
2
|
+
from tubesentry.core.heuristics import (
|
|
3
|
+
compute_non_latin_ratio,
|
|
4
|
+
evaluate_heuristics,
|
|
5
|
+
is_ai_slop_text,
|
|
6
|
+
is_low_age_gaming_text,
|
|
7
|
+
is_made_for_kids_text,
|
|
8
|
+
is_passive_music_text,
|
|
9
|
+
)
|
|
10
|
+
from tubesentry.core.seed_db import SEED_CHANNELS, lookup_seed_channel
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SEED_CHANNELS",
|
|
14
|
+
"compute_non_latin_ratio",
|
|
15
|
+
"evaluate_heuristics",
|
|
16
|
+
"is_ai_slop_text",
|
|
17
|
+
"is_low_age_gaming_text",
|
|
18
|
+
"is_made_for_kids_text",
|
|
19
|
+
"is_passive_music_text",
|
|
20
|
+
"lookup_seed_channel",
|
|
21
|
+
"run_free_audit",
|
|
22
|
+
]
|