creative-tagger 0.1.0__tar.gz
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.
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: creative-tagger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Creative Tagger — structured creative intelligence API
|
|
5
|
+
Author: Stephen Lavender
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ads,creative,creative-intelligence,marketing,naming-conventions,taxonomy
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: httpx>=0.28.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Creative Tagger Python SDK
|
|
16
|
+
|
|
17
|
+
Python client for [Creative Tagger](https://github.com/stephenlavender/creative-tagger) — structured creative intelligence API for performance marketing.
|
|
18
|
+
|
|
19
|
+
Analyze any ad creative (video, image, carousel, landing page, email) and get back structured classification across 28 taxonomy dimensions.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install creative-tagger
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from creative_tagger import CreativeTagger
|
|
31
|
+
|
|
32
|
+
ct = CreativeTagger(api_key="ct_...")
|
|
33
|
+
|
|
34
|
+
# Analyze a video ad
|
|
35
|
+
result = ct.analyze("./ad_video.mp4", brand="Brand")
|
|
36
|
+
print(result.naming.default)
|
|
37
|
+
# → BRAND_UGC_Creator_LoFi_VOMus-Pop-Conv_ShopNow_9x16_30s_V1
|
|
38
|
+
|
|
39
|
+
print(result.visual.hook_type) # → UGC
|
|
40
|
+
print(result.messaging_angle) # → ProbSol
|
|
41
|
+
print(result.creative_type) # → Testimonial
|
|
42
|
+
print(result.production_type) # → LoFiUGC
|
|
43
|
+
print(result.offer_type) # → PctOff
|
|
44
|
+
|
|
45
|
+
# Analyze a landing page
|
|
46
|
+
result = ct.analyze_url("https://example.com/lp", brand="BrandX")
|
|
47
|
+
print(result.visual_hierarchy) # → HeroFocus
|
|
48
|
+
|
|
49
|
+
# Analyze email HTML
|
|
50
|
+
result = ct.analyze_email(html_string, brand="BrandX")
|
|
51
|
+
print(result.email_structure) # → SingleCTA
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Batch Analysis
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
results = ct.analyze_batch(["ad1.mp4", "ad2.jpg", "ad3.png"], brand="Brand")
|
|
58
|
+
|
|
59
|
+
# Export to CSV-ready rows
|
|
60
|
+
import csv
|
|
61
|
+
rows = [r.to_row() for r in results]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Async Support
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
result = await ct.analyze_async("./ad.mp4", brand="Brand")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Local Development
|
|
71
|
+
|
|
72
|
+
Point to a local Creative Tagger API:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
ct = CreativeTagger(base_url="http://localhost:8000")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Creative Tagger Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for [Creative Tagger](https://github.com/stephenlavender/creative-tagger) — structured creative intelligence API for performance marketing.
|
|
4
|
+
|
|
5
|
+
Analyze any ad creative (video, image, carousel, landing page, email) and get back structured classification across 28 taxonomy dimensions.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install creative-tagger
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from creative_tagger import CreativeTagger
|
|
17
|
+
|
|
18
|
+
ct = CreativeTagger(api_key="ct_...")
|
|
19
|
+
|
|
20
|
+
# Analyze a video ad
|
|
21
|
+
result = ct.analyze("./ad_video.mp4", brand="Brand")
|
|
22
|
+
print(result.naming.default)
|
|
23
|
+
# → BRAND_UGC_Creator_LoFi_VOMus-Pop-Conv_ShopNow_9x16_30s_V1
|
|
24
|
+
|
|
25
|
+
print(result.visual.hook_type) # → UGC
|
|
26
|
+
print(result.messaging_angle) # → ProbSol
|
|
27
|
+
print(result.creative_type) # → Testimonial
|
|
28
|
+
print(result.production_type) # → LoFiUGC
|
|
29
|
+
print(result.offer_type) # → PctOff
|
|
30
|
+
|
|
31
|
+
# Analyze a landing page
|
|
32
|
+
result = ct.analyze_url("https://example.com/lp", brand="BrandX")
|
|
33
|
+
print(result.visual_hierarchy) # → HeroFocus
|
|
34
|
+
|
|
35
|
+
# Analyze email HTML
|
|
36
|
+
result = ct.analyze_email(html_string, brand="BrandX")
|
|
37
|
+
print(result.email_structure) # → SingleCTA
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Batch Analysis
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
results = ct.analyze_batch(["ad1.mp4", "ad2.jpg", "ad3.png"], brand="Brand")
|
|
44
|
+
|
|
45
|
+
# Export to CSV-ready rows
|
|
46
|
+
import csv
|
|
47
|
+
rows = [r.to_row() for r in results]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Async Support
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
result = await ct.analyze_async("./ad.mp4", brand="Brand")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Local Development
|
|
57
|
+
|
|
58
|
+
Point to a local Creative Tagger API:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
ct = CreativeTagger(base_url="http://localhost:8000")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "creative-tagger"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python SDK for Creative Tagger — structured creative intelligence API"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [{name = "Stephen Lavender"}]
|
|
9
|
+
keywords = ["creative", "taxonomy", "ads", "marketing", "creative-intelligence", "naming-conventions"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Topic :: Software Development :: Libraries",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"httpx>=0.28.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/creative_tagger"]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Creative Tagger API client."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CreativeTagger:
|
|
9
|
+
"""Client for the Creative Tagger API.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from creative_tagger import CreativeTagger
|
|
13
|
+
|
|
14
|
+
ct = CreativeTagger(api_key="ct_...")
|
|
15
|
+
|
|
16
|
+
# Analyze a local file
|
|
17
|
+
result = ct.analyze("./ad_video.mp4", brand="Brand")
|
|
18
|
+
|
|
19
|
+
# Analyze a URL
|
|
20
|
+
result = ct.analyze_url("https://example.com/landing", brand="Brand")
|
|
21
|
+
|
|
22
|
+
# Analyze email HTML
|
|
23
|
+
result = ct.analyze_email("<html>...</html>", brand="Brand")
|
|
24
|
+
|
|
25
|
+
# Access results
|
|
26
|
+
print(result.naming.default)
|
|
27
|
+
print(result.visual.hook_type)
|
|
28
|
+
print(result.messaging_angle)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
api_key: str = "",
|
|
34
|
+
base_url: str = "https://api.creativetagger.dev",
|
|
35
|
+
timeout: float = 120.0,
|
|
36
|
+
):
|
|
37
|
+
self.base_url = base_url.rstrip("/")
|
|
38
|
+
self.timeout = timeout
|
|
39
|
+
self._headers = {}
|
|
40
|
+
if api_key:
|
|
41
|
+
self._headers["X-API-Key"] = api_key
|
|
42
|
+
|
|
43
|
+
def analyze(
|
|
44
|
+
self,
|
|
45
|
+
file_path: str,
|
|
46
|
+
brand: str = "Brand",
|
|
47
|
+
version: int = 1,
|
|
48
|
+
format: str | None = None,
|
|
49
|
+
) -> "AnalyzeResult":
|
|
50
|
+
"""Analyze a local file (image, video, or multiple for carousel).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
file_path: Path to the file to analyze.
|
|
54
|
+
brand: Brand name for naming conventions.
|
|
55
|
+
version: Creative version number.
|
|
56
|
+
format: Force format (video, image, carousel, etc.). Auto-detected if omitted.
|
|
57
|
+
"""
|
|
58
|
+
path = Path(file_path).expanduser().resolve()
|
|
59
|
+
if not path.exists():
|
|
60
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
61
|
+
|
|
62
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
63
|
+
data = {"brand_name": brand, "version": str(version)}
|
|
64
|
+
if format:
|
|
65
|
+
data["format"] = format
|
|
66
|
+
|
|
67
|
+
with open(path, "rb") as f:
|
|
68
|
+
resp = client.post(
|
|
69
|
+
f"{self.base_url}/analyze",
|
|
70
|
+
files={"file": (path.name, f)},
|
|
71
|
+
data=data,
|
|
72
|
+
headers=self._headers,
|
|
73
|
+
)
|
|
74
|
+
resp.raise_for_status()
|
|
75
|
+
return AnalyzeResult(resp.json())
|
|
76
|
+
|
|
77
|
+
def analyze_url(
|
|
78
|
+
self,
|
|
79
|
+
url: str,
|
|
80
|
+
brand: str = "Brand",
|
|
81
|
+
version: int = 1,
|
|
82
|
+
) -> "AnalyzeResult":
|
|
83
|
+
"""Analyze a URL (landing page, or direct file URL).
|
|
84
|
+
|
|
85
|
+
Landing pages are rendered via headless browser.
|
|
86
|
+
File URLs (ending in .mp4, .jpg, etc.) are downloaded and analyzed.
|
|
87
|
+
"""
|
|
88
|
+
is_page = not any(
|
|
89
|
+
url.lower().endswith(ext)
|
|
90
|
+
for ext in (".mp4", ".mov", ".jpg", ".jpeg", ".png", ".webp", ".gif")
|
|
91
|
+
)
|
|
92
|
+
data = {"brand_name": brand, "version": str(version)}
|
|
93
|
+
if is_page:
|
|
94
|
+
data["page_url"] = url
|
|
95
|
+
else:
|
|
96
|
+
data["file_url"] = url
|
|
97
|
+
|
|
98
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
99
|
+
resp = client.post(
|
|
100
|
+
f"{self.base_url}/analyze",
|
|
101
|
+
data=data,
|
|
102
|
+
headers=self._headers,
|
|
103
|
+
)
|
|
104
|
+
resp.raise_for_status()
|
|
105
|
+
return AnalyzeResult(resp.json())
|
|
106
|
+
|
|
107
|
+
def analyze_email(
|
|
108
|
+
self,
|
|
109
|
+
html: str,
|
|
110
|
+
brand: str = "Brand",
|
|
111
|
+
version: int = 1,
|
|
112
|
+
) -> "AnalyzeResult":
|
|
113
|
+
"""Analyze email HTML content."""
|
|
114
|
+
data = {
|
|
115
|
+
"brand_name": brand,
|
|
116
|
+
"version": str(version),
|
|
117
|
+
"html_content": html,
|
|
118
|
+
}
|
|
119
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
120
|
+
resp = client.post(
|
|
121
|
+
f"{self.base_url}/analyze",
|
|
122
|
+
data=data,
|
|
123
|
+
headers=self._headers,
|
|
124
|
+
)
|
|
125
|
+
resp.raise_for_status()
|
|
126
|
+
return AnalyzeResult(resp.json())
|
|
127
|
+
|
|
128
|
+
def analyze_batch(
|
|
129
|
+
self,
|
|
130
|
+
file_paths: list[str],
|
|
131
|
+
brand: str = "Brand",
|
|
132
|
+
) -> list["AnalyzeResult"]:
|
|
133
|
+
"""Analyze multiple files sequentially."""
|
|
134
|
+
return [self.analyze(fp, brand=brand) for fp in file_paths]
|
|
135
|
+
|
|
136
|
+
async def analyze_async(
|
|
137
|
+
self,
|
|
138
|
+
file_path: str,
|
|
139
|
+
brand: str = "Brand",
|
|
140
|
+
version: int = 1,
|
|
141
|
+
format: str | None = None,
|
|
142
|
+
) -> "AnalyzeResult":
|
|
143
|
+
"""Async version of analyze()."""
|
|
144
|
+
path = Path(file_path).expanduser().resolve()
|
|
145
|
+
if not path.exists():
|
|
146
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
147
|
+
|
|
148
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
149
|
+
data = {"brand_name": brand, "version": str(version)}
|
|
150
|
+
if format:
|
|
151
|
+
data["format"] = format
|
|
152
|
+
|
|
153
|
+
with open(path, "rb") as f:
|
|
154
|
+
resp = await client.post(
|
|
155
|
+
f"{self.base_url}/analyze",
|
|
156
|
+
files={"file": (path.name, f)},
|
|
157
|
+
data=data,
|
|
158
|
+
headers=self._headers,
|
|
159
|
+
)
|
|
160
|
+
resp.raise_for_status()
|
|
161
|
+
return AnalyzeResult(resp.json())
|
|
162
|
+
|
|
163
|
+
def health(self) -> bool:
|
|
164
|
+
"""Check if the API is reachable."""
|
|
165
|
+
try:
|
|
166
|
+
with httpx.Client(timeout=5.0) as client:
|
|
167
|
+
resp = client.get(f"{self.base_url}/health")
|
|
168
|
+
return resp.status_code == 200
|
|
169
|
+
except Exception:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class AnalyzeResult:
|
|
174
|
+
"""Wrapper around the API response with attribute access.
|
|
175
|
+
|
|
176
|
+
Access any field as an attribute:
|
|
177
|
+
result.format → "video"
|
|
178
|
+
result.visual.hook_type → "UGC"
|
|
179
|
+
result.naming.default → "NEMAH_UGC_Creator_LoFi_..."
|
|
180
|
+
result.messaging_angle → "ProbSol"
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, data: dict):
|
|
184
|
+
self._data = data
|
|
185
|
+
|
|
186
|
+
def __getattr__(self, name: str):
|
|
187
|
+
if name.startswith("_"):
|
|
188
|
+
raise AttributeError(name)
|
|
189
|
+
value = self._data.get(name)
|
|
190
|
+
if isinstance(value, dict):
|
|
191
|
+
return AnalyzeResult(value)
|
|
192
|
+
return value
|
|
193
|
+
|
|
194
|
+
def __repr__(self):
|
|
195
|
+
fmt = self._data.get("format", "?")
|
|
196
|
+
hook = self._data.get("visual", {}).get("hook_type", "?")
|
|
197
|
+
naming = self._data.get("naming", {}).get("default", "?")
|
|
198
|
+
return f"<AnalyzeResult format={fmt} hook={hook} naming={naming}>"
|
|
199
|
+
|
|
200
|
+
def to_dict(self) -> dict:
|
|
201
|
+
"""Return the raw API response as a dict."""
|
|
202
|
+
return self._data
|
|
203
|
+
|
|
204
|
+
def to_row(self) -> dict:
|
|
205
|
+
"""Flatten to a single-level dict suitable for CSV/DataFrame."""
|
|
206
|
+
v = self._data.get("visual", {})
|
|
207
|
+
n = self._data.get("naming", {})
|
|
208
|
+
a = self._data.get("audio") or {}
|
|
209
|
+
return {
|
|
210
|
+
"format": self._data.get("format"),
|
|
211
|
+
"hook_type": v.get("hook_type"),
|
|
212
|
+
"hook_style": self._data.get("hook_style"),
|
|
213
|
+
"visual_style": v.get("visual_style"),
|
|
214
|
+
"talent_type": v.get("talent_type"),
|
|
215
|
+
"cta_type": v.get("cta_type"),
|
|
216
|
+
"cta_placement": self._data.get("cta_placement"),
|
|
217
|
+
"primary_emotion": v.get("primary_emotion"),
|
|
218
|
+
"messaging_angle": self._data.get("messaging_angle"),
|
|
219
|
+
"creative_type": self._data.get("creative_type"),
|
|
220
|
+
"production_type": self._data.get("production_type"),
|
|
221
|
+
"product_presence": self._data.get("product_presence"),
|
|
222
|
+
"offer_type": self._data.get("offer_type"),
|
|
223
|
+
"offer_detail": self._data.get("offer_detail"),
|
|
224
|
+
"brand_presence": self._data.get("brand_presence"),
|
|
225
|
+
"seasonality": self._data.get("seasonality"),
|
|
226
|
+
"text_overlay_treatment": self._data.get("text_overlay_treatment"),
|
|
227
|
+
"social_proof_elements": self._data.get("social_proof_elements"),
|
|
228
|
+
"aspect_ratio": v.get("aspect_ratio"),
|
|
229
|
+
"duration_seconds": v.get("duration_seconds"),
|
|
230
|
+
"video_length_bucket": self._data.get("video_length_bucket"),
|
|
231
|
+
"audio_type": a.get("audio_type"),
|
|
232
|
+
"audio_shortcode": self._data.get("audio_shortcode"),
|
|
233
|
+
"naming_default": n.get("default"),
|
|
234
|
+
"naming_compact": n.get("compact"),
|
|
235
|
+
"model_used": self._data.get("model_used"),
|
|
236
|
+
"processing_time_ms": self._data.get("processing_time_ms"),
|
|
237
|
+
}
|