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,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .venv/
7
+ .env
@@ -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,6 @@
1
+ """Creative Tagger Python SDK — structured creative intelligence for any ad format."""
2
+
3
+ from creative_tagger.client import CreativeTagger
4
+
5
+ __all__ = ["CreativeTagger"]
6
+ __version__ = "0.1.0"
@@ -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
+ }