diff --git a/.env b/.env new file mode 100644 index 0000000..912625a --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +# Apify API token (optional if only using public fallback sources) +APIFY_TOKEN= + +# Optional actor IDs by platform. If unset, runner falls back to public discovery sources. +APIFY_ACTOR_YOUTUBE= +APIFY_ACTOR_TIKTOK= +APIFY_ACTOR_LINKEDIN= diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..912625a --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Apify API token (optional if only using public fallback sources) +APIFY_TOKEN= + +# Optional actor IDs by platform. If unset, runner falls back to public discovery sources. +APIFY_ACTOR_YOUTUBE= +APIFY_ACTOR_TIKTOK= +APIFY_ACTOR_LINKEDIN= diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f68966 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Content Intelligence Repo + +## Discovery v1 + +Discovery v1 adds an input-acquisition layer that collects public creator/video/post candidates across YouTube, TikTok, and LinkedIn-related search results, then normalizes them into `output/market_intelligence.csv`. + +### Files +- `discovery/seed_queries.yaml`: lane-based seed queries and platform toggles. +- `configs/platform_rules.yaml`: platform discovery modes and actor env bindings. +- `discovery/apify_runner.py`: candidate collection runner with Apify + public fallback. +- `discovery/normalize_to_market_intelligence.py`: maps raw JSON to flat CSV. +- `discovery/review_candidates.py`: record quality checks and review summary. +- `data/raw_candidates.json`: persisted raw candidate payload. + +### Environment +Copy `.env.example` to `.env` and set values as needed: + +```bash +cp .env.example .env +``` + +Required/optional variables: +- `APIFY_TOKEN` (optional if using only fallback sources) +- `APIFY_ACTOR_YOUTUBE` (optional) +- `APIFY_ACTOR_TIKTOK` (optional) +- `APIFY_ACTOR_LINKEDIN` (optional) + +### Install +```bash +python3 -m pip install -r requirements.txt +``` + +### Run discovery +Dry-run mode (no external calls): +```bash +python3 discovery/apify_runner.py --dry-run +``` + +Live mode (uses Apify actors if configured; otherwise public fallback sources): +```bash +python3 discovery/apify_runner.py +``` + +### Normalize to market intelligence CSV +```bash +python3 discovery/normalize_to_market_intelligence.py +``` + +Behavior: +- Preserves existing header order when `output/market_intelligence.csv` already exists. +- Appends `needs_review` and `confidence_score` if missing. +- Leaves unavailable fields blank. + +### Review candidate quality +```bash +python3 discovery/review_candidates.py +``` + +Checks include: +- incomplete core metadata (`creator_name`, `platform`, `source_url`) +- missing transcript/summary +- missing opening hook +- missing CTA + +The script prints a summary + sample flagged records for triage. diff --git a/configs/platform_rules.yaml b/configs/platform_rules.yaml new file mode 100644 index 0000000..2d8aa7e --- /dev/null +++ b/configs/platform_rules.yaml @@ -0,0 +1,29 @@ +platforms: + youtube: + discovery_mode: "apify_or_rss" + actor_env: "APIFY_ACTOR_YOUTUBE" + defaults: + content_type: "video" + platform: "YouTube" + tiktok: + discovery_mode: "apify_or_search" + actor_env: "APIFY_ACTOR_TIKTOK" + defaults: + content_type: "short_video" + platform: "TikTok" + linkedin: + discovery_mode: "apify_or_search" + actor_env: "APIFY_ACTOR_LINKEDIN" + defaults: + content_type: "post_or_video" + platform: "LinkedIn" +normalization: + csv_output: "output/market_intelligence.csv" + required_fields: + - creator_name + - platform + - source_url + review_fields: + - transcript + - opening_hook + - cta diff --git a/data/raw_candidates.json b/data/raw_candidates.json new file mode 100644 index 0000000..f308c12 --- /dev/null +++ b/data/raw_candidates.json @@ -0,0 +1,126 @@ +{ + "generated_at": 1775355334, + "dry_run": true, + "records": [ + { + "platform": "youtube", + "lane": "legal_ops", + "query": "legal operations workflow", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for legal operations workflow", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "legal_ops", + "query": "contract lifecycle bottlenecks", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for contract lifecycle bottlenecks", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "legal_ops", + "query": "legal ops dashboard", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for legal ops dashboard", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "grc", + "query": "GRC operating model", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for GRC operating model", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "grc", + "query": "risk and compliance process improvement", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for risk and compliance process improvement", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "grc", + "query": "governance risk compliance metrics", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for governance risk compliance metrics", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "ai_governance", + "query": "AI governance framework", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for AI governance framework", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "ai_governance", + "query": "enterprise AI risk controls", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for enterprise AI risk controls", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "ai_governance", + "query": "AI policy implementation", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for AI policy implementation", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "operations_leadership", + "query": "operations leadership systems", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for operations leadership systems", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "operations_leadership", + "query": "workflow integrity", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for workflow integrity", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + }, + { + "platform": "youtube", + "lane": "operations_leadership", + "query": "business systems strategy", + "creator_name": "DryRun Creator", + "video_title_or_topic": "Dry run candidate for business systems strategy", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run" + } + ] +} \ No newline at end of file diff --git a/discovery/apify_runner.py b/discovery/apify_runner.py new file mode 100644 index 0000000..e7a42b9 --- /dev/null +++ b/discovery/apify_runner.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 +"""Discovery runner for candidate creator/video/post sourcing. + +- Reads seed queries by lane from discovery/seed_queries.yaml +- Uses Apify actors when configured and token is present +- Falls back to public sources (YouTube RSS + DuckDuckGo HTML search) +- Supports --dry-run mode +- Writes unified raw JSON to data/raw_candidates.json +""" + +from __future__ import annotations + +import argparse +import json +import logging +import os +import re +import sys +import time +import urllib.parse +import urllib.request +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional, Tuple +import xml.etree.ElementTree as ET + +LOGGER = logging.getLogger("discovery.apify_runner") + + +def parse_scalar(value: str) -> Any: + value = value.strip() + if value.lower() in {"true", "false"}: + return value.lower() == "true" + if re.fullmatch(r"-?\d+", value): + return int(value) + if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): + return value[1:-1] + return value + + +def load_simple_yaml(path: Path) -> Dict[str, Any]: + """Very small YAML subset parser supporting dict/list, enough for config files here.""" + lines = path.read_text(encoding="utf-8").splitlines() + root: Dict[str, Any] = {} + stack: List[Tuple[int, Any]] = [(-1, root)] + + for i, raw in enumerate(lines): + if not raw.strip() or raw.strip().startswith("#"): + continue + + indent = len(raw) - len(raw.lstrip(" ")) + line = raw.strip() + + while len(stack) > 1 and indent <= stack[-1][0]: + stack.pop() + + parent = stack[-1][1] + + if line.startswith("- "): + if not isinstance(parent, list): + raise ValueError(f"Invalid YAML structure near line: {raw}") + parent.append(parse_scalar(line[2:].strip())) + continue + + if ":" not in line: + raise ValueError(f"Invalid YAML key/value near line: {raw}") + + key, remainder = line.split(":", 1) + key = key.strip() + remainder = remainder.strip() + + if remainder == "": + next_container: Any + next_nonempty = "" + for nxt in lines[i + 1 :]: + if nxt.strip() and not nxt.strip().startswith("#"): + next_nonempty = nxt.strip() + break + next_container = [] if next_nonempty.startswith("- ") else {} + if isinstance(parent, dict): + parent[key] = next_container + else: + raise ValueError(f"Cannot assign key to non-dict near line: {raw}") + stack.append((indent, next_container)) + else: + if isinstance(parent, dict): + parent[key] = parse_scalar(remainder) + else: + raise ValueError(f"Cannot assign scalar key to non-dict near line: {raw}") + + return root + + +@dataclass +class DiscoveryConfig: + seed_path: Path + rules_path: Path + output_path: Path + dry_run: bool + + +def flatten_queries(seed_data: Dict[str, Any]) -> List[Dict[str, str]]: + lanes = seed_data.get("lanes", {}) + flattened: List[Dict[str, str]] = [] + for lane, queries in lanes.items(): + for query in queries or []: + flattened.append({"lane": lane, "query": str(query)}) + return flattened + + +def http_json(url: str, method: str = "GET", payload: Optional[Dict[str, Any]] = None, timeout: int = 90) -> Dict[str, Any]: + data = json.dumps(payload).encode("utf-8") if payload is not None else None + req = urllib.request.Request(url, data=data, method=method, headers={"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req, timeout=timeout) as resp: # nosec B310 + return json.loads(resp.read().decode("utf-8")) + + +def http_text(url: str, timeout: int = 30) -> str: + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req, timeout=timeout) as resp: # nosec B310 + return resp.read().decode("utf-8", errors="ignore") + + +def call_apify_actor(actor_id: str, input_payload: Dict[str, Any], token: str) -> Optional[Dict[str, Any]]: + base = "https://api.apify.com/v2" + run_url = f"{base}/acts/{actor_id}/runs?token={urllib.parse.quote(token)}&waitForFinish=60" + + LOGGER.debug("Calling Apify actor=%s payload=%s", actor_id, input_payload) + try: + run_response = http_json(run_url, method="POST", payload=input_payload, timeout=90) + run_data = run_response.get("data", {}) + dataset_id = run_data.get("defaultDatasetId") + if not dataset_id: + LOGGER.warning("Apify actor %s returned no dataset id", actor_id) + return None + + items_url = f"{base}/datasets/{dataset_id}/items?token={urllib.parse.quote(token)}&clean=true" + items = json.loads(http_text(items_url, timeout=90)) + return {"source": "apify", "actor_id": actor_id, "items": items} + except Exception as exc: # noqa: BLE001 + LOGGER.exception("Apify call failed for actor=%s: %s", actor_id, exc) + return None + + +def youtube_rss_search(query: str, limit: int) -> List[Dict[str, Any]]: + encoded = urllib.parse.quote_plus(query) + url = f"https://www.youtube.com/feeds/videos.xml?search_query={encoded}" + LOGGER.debug("YouTube RSS query=%s", query) + + try: + xml_content = http_text(url, timeout=20).encode("utf-8") + except Exception as exc: # noqa: BLE001 + LOGGER.warning("YouTube RSS failed for query=%s error=%s", query, exc) + return [] + + ns = {"atom": "http://www.w3.org/2005/Atom", "yt": "http://www.youtube.com/xml/schemas/2015"} + root = ET.fromstring(xml_content) + results: List[Dict[str, Any]] = [] + + for entry in root.findall("atom:entry", ns)[:limit]: + title = entry.findtext("atom:title", default="", namespaces=ns) + video_id = entry.findtext("yt:videoId", default="", namespaces=ns) + author = entry.findtext("atom:author/atom:name", default="", namespaces=ns) + published = entry.findtext("atom:published", default="", namespaces=ns) + if video_id: + results.append( + { + "platform": "youtube", + "query": query, + "creator_name": author, + "video_title_or_topic": title, + "source_url": f"https://www.youtube.com/watch?v={video_id}", + "posting_date": published, + "raw": {"video_id": video_id}, + } + ) + return results + + +def duckduckgo_site_search(query: str, site: str, limit: int, platform: str) -> List[Dict[str, Any]]: + final_query = f"site:{site} {query}" + encoded = urllib.parse.quote_plus(final_query) + url = f"https://duckduckgo.com/html/?q={encoded}" + LOGGER.debug("DuckDuckGo query=%s", final_query) + + try: + html = http_text(url, timeout=30) + except Exception as exc: # noqa: BLE001 + LOGGER.warning("DuckDuckGo failed for platform=%s query=%s error=%s", platform, query, exc) + return [] + + results: List[Dict[str, Any]] = [] + marker = 'class="result__a"' + chunks = html.split(marker)[1 : limit + 1] + + for chunk in chunks: + href_tag = "href=\"" + href_start = chunk.find(href_tag) + if href_start == -1: + continue + href_start += len(href_tag) + href_end = chunk.find("\"", href_start) + href = chunk[href_start:href_end] + + text_start = chunk.find(">", href_end) + text_end = chunk.find("", text_start) + title = chunk[text_start + 1 : text_end].replace("", "").replace("", "").strip() + + results.append( + { + "platform": platform, + "query": query, + "creator_name": "", + "video_title_or_topic": title, + "source_url": href, + "posting_date": "", + "raw": {"search_engine": "duckduckgo", "site": site}, + } + ) + + return results + + +def build_dry_run_records(queries: Iterable[Dict[str, str]]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for item in queries: + rows.append( + { + "platform": "youtube", + "lane": item["lane"], + "query": item["query"], + "creator_name": "DryRun Creator", + "video_title_or_topic": f"Dry run candidate for {item['query']}", + "source_url": "https://example.com/dry-run", + "posting_date": "", + "discovery_source": "dry_run", + } + ) + return rows + + +def discover_platform(platform: str, query: str, max_results: int, actor_id: Optional[str], token: Optional[str]) -> List[Dict[str, Any]]: + if actor_id and token: + payload = {"query": query, "maxResults": max_results} + apify_data = call_apify_actor(actor_id=actor_id, input_payload=payload, token=token) + if apify_data and apify_data.get("items"): + normalized = [] + for item in apify_data["items"]: + normalized.append( + { + "platform": platform, + "query": query, + "creator_name": item.get("authorName") or item.get("author") or "", + "video_title_or_topic": item.get("title") or item.get("text") or "", + "source_url": item.get("url") or item.get("videoUrl") or "", + "posting_date": item.get("publishedAt") or item.get("createTimeISO") or "", + "raw": item, + "discovery_source": "apify", + } + ) + return normalized + + if platform == "youtube": + rows = youtube_rss_search(query=query, limit=max_results) + for r in rows: + r["discovery_source"] = "youtube_rss" + return rows + if platform == "tiktok": + rows = duckduckgo_site_search(query=query, site="www.tiktok.com", limit=max_results, platform=platform) + for r in rows: + r["discovery_source"] = "duckduckgo" + return rows + if platform == "linkedin": + rows = duckduckgo_site_search(query=query, site="www.linkedin.com", limit=max_results, platform=platform) + for r in rows: + r["discovery_source"] = "duckduckgo" + return rows + return [] + + +def run(config: DiscoveryConfig) -> int: + seed_data = load_simple_yaml(config.seed_path) + rules_data = load_simple_yaml(config.rules_path) + + query_items = flatten_queries(seed_data) + if not query_items: + LOGGER.error("No queries found in %s", config.seed_path) + return 1 + + if config.dry_run: + raw_payload = {"generated_at": int(time.time()), "dry_run": True, "records": build_dry_run_records(query_items)} + config.output_path.parent.mkdir(parents=True, exist_ok=True) + config.output_path.write_text(json.dumps(raw_payload, indent=2), encoding="utf-8") + LOGGER.info("Dry run complete. Wrote %s records to %s", len(raw_payload["records"]), config.output_path) + return 0 + + platforms_cfg = seed_data.get("platforms", {}) + platform_rules = rules_data.get("platforms", {}) + + token = os.getenv("APIFY_TOKEN") + all_records: List[Dict[str, Any]] = [] + + for query_item in query_items: + lane = query_item["lane"] + query = query_item["query"] + + for platform, p_cfg in platforms_cfg.items(): + if not p_cfg.get("enabled", True): + continue + + max_results = int(p_cfg.get("max_results_per_query", 10)) + actor_env_name = platform_rules.get(platform, {}).get("actor_env", "") + actor_id = os.getenv(actor_env_name) if actor_env_name else None + + rows = discover_platform(platform=platform, query=query, max_results=max_results, actor_id=actor_id, token=token) + if not rows: + LOGGER.warning("No results platform=%s lane=%s query=%s", platform, lane, query) + continue + + for row in rows: + row["lane"] = lane + all_records.extend(rows) + + payload = {"generated_at": int(time.time()), "dry_run": False, "record_count": len(all_records), "records": all_records} + config.output_path.parent.mkdir(parents=True, exist_ok=True) + config.output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + LOGGER.info("Discovery complete. Wrote %s records to %s", len(all_records), config.output_path) + return 0 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run discovery and save raw candidate JSON.") + parser.add_argument("--seed", default="discovery/seed_queries.yaml", help="Path to seed query YAML") + parser.add_argument("--rules", default="configs/platform_rules.yaml", help="Path to platform rules YAML") + parser.add_argument("--output", default="data/raw_candidates.json", help="Raw JSON output path") + parser.add_argument("--dry-run", action="store_true", help="Generate synthetic records only") + parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Log level") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + logging.basicConfig(level=getattr(logging, args.log_level), format="%(asctime)s %(levelname)s %(name)s: %(message)s") + + cfg = DiscoveryConfig(seed_path=Path(args.seed), rules_path=Path(args.rules), output_path=Path(args.output), dry_run=args.dry_run) + return run(cfg) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/discovery/normalize_to_market_intelligence.py b/discovery/normalize_to_market_intelligence.py new file mode 100644 index 0000000..97c5a7b --- /dev/null +++ b/discovery/normalize_to_market_intelligence.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Normalize discovery raw JSON into output/market_intelligence.csv.""" + +from __future__ import annotations + +import argparse +import csv +import json +import logging +import re +import sys +from pathlib import Path +from typing import Any, Dict, Iterable, List + +LOGGER = logging.getLogger("discovery.normalize") + +DEFAULT_HEADER = [ + "creator_name", + "platform", + "video_title_or_topic", + "source_url", + "transcript", + "views", + "likes", + "comments", + "shares", + "posting_date", + "niche", + "target_audience", + "opening_hook", + "cta", + "tone", + "credibility_markers", + "performance_notes", + "inferred_intent_match", + "likely_audience_segment_attracted", + "buyer_value", + "buyer_value_rationale", + "recruiter_value", + "recruiter_value_rationale", + "authority_signal", + "authority_signal_rationale", + "saturation_risk", + "saturation_risk_rationale", + "differentiation_potential", + "differentiation_potential_rationale", + "observation_vs_inference", +] + +EXTRA_FIELDS = ["needs_review", "confidence_score"] + + +def read_existing_header(csv_path: Path) -> List[str]: + if not csv_path.exists(): + return [] + with csv_path.open("r", encoding="utf-8", newline="") as f: + reader = csv.reader(f) + try: + return next(reader) + except StopIteration: + return [] + + +def derive_niche(lane: str, title: str) -> str: + if lane: + return lane.replace("_", " ") + title_l = title.lower() + if "ai" in title_l: + return "ai governance" + if "compliance" in title_l or "risk" in title_l: + return "grc" + if "legal" in title_l: + return "legal ops" + return "" + + +def derive_opening_hook(title: str) -> str: + clean = re.sub(r"\s+", " ", title).strip() + return clean[:120] if clean else "" + + +def parse_record(raw: Dict[str, Any]) -> Dict[str, str]: + title = str(raw.get("video_title_or_topic") or "").strip() + record: Dict[str, str] = { + "creator_name": str(raw.get("creator_name") or "").strip(), + "platform": str(raw.get("platform") or "").strip(), + "video_title_or_topic": title, + "source_url": str(raw.get("source_url") or "").strip(), + "transcript": str(raw.get("transcript") or "").strip(), + "views": str(raw.get("views") or "").strip(), + "likes": str(raw.get("likes") or "").strip(), + "comments": str(raw.get("comments") or "").strip(), + "shares": str(raw.get("shares") or "").strip(), + "posting_date": str(raw.get("posting_date") or "").strip(), + "niche": derive_niche(str(raw.get("lane") or ""), title), + "target_audience": str(raw.get("target_audience") or "").strip(), + "opening_hook": str(raw.get("opening_hook") or derive_opening_hook(title)).strip(), + "cta": str(raw.get("cta") or "").strip(), + "tone": str(raw.get("tone") or "").strip(), + "credibility_markers": str(raw.get("credibility_markers") or "").strip(), + "performance_notes": str(raw.get("performance_notes") or "").strip(), + "inferred_intent_match": str(raw.get("inferred_intent_match") or "").strip(), + "likely_audience_segment_attracted": str(raw.get("likely_audience_segment_attracted") or "").strip(), + "buyer_value": str(raw.get("buyer_value") or "").strip(), + "buyer_value_rationale": str(raw.get("buyer_value_rationale") or "").strip(), + "recruiter_value": str(raw.get("recruiter_value") or "").strip(), + "recruiter_value_rationale": str(raw.get("recruiter_value_rationale") or "").strip(), + "authority_signal": str(raw.get("authority_signal") or "").strip(), + "authority_signal_rationale": str(raw.get("authority_signal_rationale") or "").strip(), + "saturation_risk": str(raw.get("saturation_risk") or "").strip(), + "saturation_risk_rationale": str(raw.get("saturation_risk_rationale") or "").strip(), + "differentiation_potential": str(raw.get("differentiation_potential") or "").strip(), + "differentiation_potential_rationale": str(raw.get("differentiation_potential_rationale") or "").strip(), + "observation_vs_inference": str(raw.get("observation_vs_inference") or "observation").strip(), + } + + critical_missing = [k for k in ("creator_name", "source_url", "platform") if not record[k]] + has_summary = bool(record["transcript"] or title) + record["needs_review"] = "yes" if critical_missing or not has_summary or not record["cta"] else "no" + + confidence = 1.0 + if critical_missing: + confidence -= 0.45 + if not record["transcript"]: + confidence -= 0.2 + if not record["opening_hook"]: + confidence -= 0.15 + if not record["cta"]: + confidence -= 0.1 + record["confidence_score"] = f"{max(0.0, confidence):.2f}" + return record + + +def iter_raw_records(raw_payload: Dict[str, Any]) -> Iterable[Dict[str, Any]]: + records = raw_payload.get("records", []) + if isinstance(records, list): + for record in records: + if isinstance(record, dict): + yield record + + +def run(raw_json_path: Path, output_csv_path: Path) -> int: + if not raw_json_path.exists(): + LOGGER.error("Raw file not found: %s", raw_json_path) + return 1 + + with raw_json_path.open("r", encoding="utf-8") as f: + raw_payload = json.load(f) + + existing_header = read_existing_header(output_csv_path) + if existing_header: + header = existing_header.copy() + else: + header = DEFAULT_HEADER.copy() + + for field in EXTRA_FIELDS: + if field not in header: + header.append(field) + + normalized_rows = [parse_record(raw) for raw in iter_raw_records(raw_payload)] + + output_csv_path.parent.mkdir(parents=True, exist_ok=True) + with output_csv_path.open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=header, extrasaction="ignore") + writer.writeheader() + for row in normalized_rows: + output_row = {column: row.get(column, "") for column in header} + writer.writerow(output_row) + + LOGGER.info("Wrote %s normalized rows to %s", len(normalized_rows), output_csv_path) + return 0 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Normalize raw candidate JSON into market intelligence CSV") + parser.add_argument("--raw", default="data/raw_candidates.json", help="Raw candidates JSON path") + parser.add_argument("--output", default="output/market_intelligence.csv", help="Market intelligence CSV path") + parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Log verbosity") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + logging.basicConfig(level=getattr(logging, args.log_level), format="%(asctime)s %(levelname)s %(name)s: %(message)s") + return run(Path(args.raw), Path(args.output)) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/discovery/review_candidates.py b/discovery/review_candidates.py new file mode 100644 index 0000000..6d86a0b --- /dev/null +++ b/discovery/review_candidates.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Review normalized market intelligence rows for missing critical fields.""" + +from __future__ import annotations + +import argparse +import csv +import logging +import sys +from pathlib import Path +from typing import Dict, List, Tuple + +LOGGER = logging.getLogger("discovery.review") + + +def is_missing(value: str) -> bool: + return not str(value or "").strip() + + +def evaluate_row(row: Dict[str, str]) -> Tuple[bool, List[str]]: + issues: List[str] = [] + + for field in ["creator_name", "platform", "source_url"]: + if is_missing(row.get(field, "")): + issues.append(f"missing_{field}") + + if is_missing(row.get("transcript", "")) and is_missing(row.get("video_title_or_topic", "")): + issues.append("missing_transcript_or_summary") + + if is_missing(row.get("opening_hook", "")): + issues.append("missing_opening_hook") + + if is_missing(row.get("cta", "")): + issues.append("missing_cta") + + return (len(issues) > 0, issues) + + +def run(csv_path: Path, limit: int) -> int: + if not csv_path.exists(): + LOGGER.error("CSV not found: %s", csv_path) + return 1 + + with csv_path.open("r", encoding="utf-8", newline="") as f: + reader = csv.DictReader(f) + rows = list(reader) + + total = len(rows) + flagged = [] + issue_counts: Dict[str, int] = {} + + for idx, row in enumerate(rows, start=2): # account for header row + needs_review, issues = evaluate_row(row) + if needs_review: + flagged.append((idx, row, issues)) + for issue in issues: + issue_counts[issue] = issue_counts.get(issue, 0) + 1 + + print("=== Review Summary ===") + print(f"Total records: {total}") + print(f"Flagged records: {len(flagged)}") + print("Issue breakdown:") + if issue_counts: + for key in sorted(issue_counts): + print(f"- {key}: {issue_counts[key]}") + else: + print("- none") + + if flagged: + print("\nSample flagged records:") + for row_no, row, issues in flagged[:limit]: + print(f"- row {row_no}: creator='{row.get('creator_name', '')}' platform='{row.get('platform', '')}' issues={','.join(issues)}") + + return 0 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Review market intelligence CSV and flag incomplete records") + parser.add_argument("--csv", default="output/market_intelligence.csv", help="Path to normalized CSV") + parser.add_argument("--sample-limit", type=int, default=20, help="Number of flagged rows to print") + parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Log verbosity") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + logging.basicConfig(level=getattr(logging, args.log_level), format="%(asctime)s %(levelname)s %(name)s: %(message)s") + return run(Path(args.csv), args.sample_limit) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/discovery/seed_queries.yaml b/discovery/seed_queries.yaml new file mode 100644 index 0000000..d38bad5 --- /dev/null +++ b/discovery/seed_queries.yaml @@ -0,0 +1,27 @@ +lanes: + legal_ops: + - "legal operations workflow" + - "contract lifecycle bottlenecks" + - "legal ops dashboard" + grc: + - "GRC operating model" + - "risk and compliance process improvement" + - "governance risk compliance metrics" + ai_governance: + - "AI governance framework" + - "enterprise AI risk controls" + - "AI policy implementation" + operations_leadership: + - "operations leadership systems" + - "workflow integrity" + - "business systems strategy" +platforms: + youtube: + enabled: true + max_results_per_query: 20 + tiktok: + enabled: true + max_results_per_query: 15 + linkedin: + enabled: true + max_results_per_query: 15 diff --git a/output/brand_strategy.md b/output/brand_strategy.md new file mode 100644 index 0000000..3f9c47d --- /dev/null +++ b/output/brand_strategy.md @@ -0,0 +1,42 @@ +# Brand Strategy Mapping + +## Objective +Translate creator/video evidence into recommendations that strengthen your positioning as an operations-minded legal and business systems leader. + +## Current Constraint +No creator/video evidence is currently available in this environment. Recommendations below are framework-level and should be updated after first data ingestion. + +## What to Emulate (When Validated by Data) +- Concise, operator-grade problem framing. +- Clear maturity-based guidance (foundational vs advanced practices). +- Evidence-backed claims with concrete artifacts (policies, workflows, controls, KPIs). + +## What to Avoid +- Performative trend content disconnected from implementation. +- Over-general AI optimism with no governance controls. +- Advice that signals activity but not measurable risk reduction or throughput gain. + +## Differentiation Angles to Test +1. **Workflow Integrity Lens** + - Position every topic as decision quality + control quality + execution quality. +2. **AI Readiness by Control Layer** + - Explain policy, process, data, model, and human oversight layers in plain operator language. +3. **Legal/Ops Translation Layer** + - Bridge legal, compliance, and business operations with shared execution artifacts. + +## Executive Authority Signals +- Structured frameworks with named components. +- Practical tradeoff analysis (speed vs control, innovation vs assurance). +- Clear sequencing (what to do first, second, third). +- Real implementation indicators (SOP adoption, cycle-time reduction, exception handling). + +## Commercially Weak Signals to Watch +- High engagement on generic productivity tropes. +- Viral hot takes without buyer-action pathways. +- Recruiter-optimized content that reduces perceived strategic depth. + +## Recommended Content Mix (Starting Hypothesis) +- 40% practical system design / governance implementation +- 30% diagnostic breakdowns (what fails, why, how to fix) +- 20% executive briefings on risk/compliance implications +- 10% opinion pieces with explicit operational implications diff --git a/output/content_plan_30_days.md b/output/content_plan_30_days.md new file mode 100644 index 0000000..992761f --- /dev/null +++ b/output/content_plan_30_days.md @@ -0,0 +1,39 @@ +# 30-Day Content Plan + +| Day | Topic | Target Audience | Strategic Angle | Sample Hook | Format Suggestion | CTA | Why It Works | +|---|---|---|---|---|---|---|---| +| 1 | Workflow integrity basics | Ops leaders | Define the core concept | "If your process depends on heroics, you have risk." | 45s talking-head + overlay | Save for review meeting | Establishes core lens | +| 2 | Legal ops bottleneck audit | Legal ops managers | Diagnose upstream delays | "Cycle time issues usually start before legal touches it." | Whiteboard walkthrough | Comment 'audit' | Practical diagnostic value | +| 3 | GRC handoff failure modes | GRC leads | Map handoff friction | "Most compliance delays are handoff design errors." | Slide narration | Share with GRC team | Names hidden friction | +| 4 | AI governance control stack | AI program owners | Policy-to-process mapping | "AI readiness needs a control layer, not just a policy." | Framework explainer | Follow for part 2 | Authority through structure | +| 5 | Decision rights in operations | Directors/VPs | Clarify ownership | "No decision rights = no scale." | Talking-head + table | Save template request | Executive relevance | +| 6 | SOP vs policy confusion | Compliance + ops | Execution clarity | "Policy tells intent; SOP drives behavior." | Split-screen explainer | Comment 'SOP' | Reduces implementation ambiguity | +| 7 | KPI that predicts rework | Ops analytics stakeholders | Lead indicators over lag | "One metric predicts your next fire drill." | Dashboard teardown | DM 'scorecard' | Commercially useful metricing | +| 8 | Intake design for legal requests | Cross-functional requesters | Prevent low-quality demand | "Bad intake creates expensive legal queues." | Process map | Share with request owners | Immediate process fix | +| 9 | Governance meeting cadence | Executives | Operational rhythm | "Governance fails when cadence is irregular." | Calendar-based explainer | Follow for agenda | Practical leadership system | +| 10 | Exception handling design | Risk/compliance teams | Controlled flexibility | "Exceptions without rules become shadow process." | Scenario walkthrough | Comment 'exceptions' | Risk-control maturity | +| 11 | Training as control mechanism | L&D + compliance | Behavior reliability | "Training should reduce variance, not just deliver info." | Checklist video | Save and reuse | Connects training to outcomes | +| 12 | AI use-case triage | Innovation + risk teams | Prioritize safely | "Score AI use cases by risk, reversibility, and value." | Matrix walkthrough | DM 'triage' | Balanced innovation framing | +| 13 | Audit readiness by design | Compliance officers | Build-in evidence trails | "Audit readiness starts at workflow design." | Before/after process | Share with audit lead | Compliance + ops bridge | +| 14 | Process ownership model | Business systems leads | RACI simplification | "If ownership is shared, accountability is diluted." | Framework slide | Comment 'ownership' | Leadership clarity signal | +| 15 | Mid-month synthesis | Mixed | Consolidate learning | "Here are the 3 patterns behind most ops failures." | Recap montage | Follow for next series | Reinforces authority | +| 16 | Contract lifecycle choke points | Legal + procurement | Identify queue causes | "Approvals are often the wrong bottleneck." | Lifecycle diagram | Save checklist | High buyer relevance | +| 17 | Risk taxonomy for operators | Ops managers | Translate risk language | "Risk language must be operational to be useful." | Taxonomy walkthrough | Comment 'taxonomy' | Translation advantage | +| 18 | Escalation design | Team leads | Reduce noise | "Bad escalation rules hide real risk." | Rule-set example | Share with managers | Immediate managerial value | +| 19 | AI policy anti-patterns | Governance stakeholders | Debunk shallow policy | "If policy has no triggers, it's theater." | Myth-bust short | DM 'policy' | Differentiation via skepticism | +| 20 | Process debt concept | Operations leadership | Long-term systems thinking | "Process debt compounds like technical debt." | Analogy + examples | Save for planning | Strategic maturity signal | +| 21 | Compliance as throughput lever | Exec + ops | Reframe compliance value | "Compliance can increase speed when embedded early." | Case-style narrative | Follow for case breakdown | Contrarian but practical | +| 22 | Control threshold design | Risk owners | Define trigger points | "Thresholds turn policy into executable control." | Template demo | Comment 'thresholds' | Concrete implementation hook | +| 23 | Cross-functional SLA design | Legal/ops/product | Align service expectations | "SLAs fail when handoffs are undefined." | SLA canvas walkthrough | Share with partner teams | Collaboration utility | +| 24 | Governance dashboard essentials | Leaders | Focus on decision metrics | "Most dashboards report activity, not control health." | Dashboard critique | DM 'dashboard' | Executive-level insight | +| 25 | Process redesign case teardown | Buyer audience | Show transformation logic | "Here's how one workflow redesign cut rework." | Case teardown | Contact for implementation | Commercial proof orientation | +| 26 | Recruiter-facing leadership system | Recruiters + hiring managers | Capability signaling | "How I run operating cadence across legal, risk, ops." | Personal framework | Connect for role discussion | Recruiter relevance | +| 27 | AI readiness maturity model | AI governance buyers | staged adoption path | "Most teams overestimate AI readiness by one level." | Maturity ladder | Comment 'maturity' | Diagnostic authority | +| 28 | Executive communication format | Senior stakeholders | Translate complexity | "Use this one-page format for governance updates." | Template walk-through | Save for next update | Leadership communication value | +| 29 | Common transformation failure points | Program owners | Pre-mortem approach | "Transformation fails in sequencing, not intent." | Pre-mortem checklist | DM 'premortem' | High-stakes usefulness | +| 30 | 30-day insights + next steps | All segments | Summarize & convert | "What we learned: what to fix first on Monday." | Synthesis + CTA stack | Book strategy call | Converts authority into action | + +## Implementation Notes +- Rotate platform-native edits per channel, while keeping core message constant. +- Tag each post internally with: buyer intent, recruiter intent, and authority intent. +- Review weekly with a simple scorecard: saves, qualified DMs, meeting requests, recruiter outreach quality. diff --git a/output/cta_library.md b/output/cta_library.md new file mode 100644 index 0000000..ef8bc07 --- /dev/null +++ b/output/cta_library.md @@ -0,0 +1,49 @@ +# CTA Library + +## 10 Soft CTAs +1. "If this was useful, save it for your next process review." +2. "Comment 'map' and I'll share the workflow checklist." +3. "Send this to the person who owns intake quality." +4. "Follow for practical legal ops and governance breakdowns." +5. "Tell me which step causes the most rework in your team." +6. "Want part 2 with examples? Drop 'part 2'." +7. "Bookmark this before your next audit planning cycle." +8. "If you'd like the template, I'll post it next." +9. "Share your current bottleneck and I'll suggest one fix." +10. "Save this framework for your next leadership update." + +## 10 Authority-Building CTAs +1. "If you want the full control-layer model, comment 'framework'." +2. "I'll publish the full operating model in next week's breakdown." +3. "Message me 'scorecard' for the evaluation criteria I use." +4. "I can share the exact agenda I use for governance cadence reviews." +5. "If your team wants this as a workshop outline, say 'workshop'." +6. "I'll post a case-style teardown of this workflow next." +7. "Comment 'taxonomy' for the risk-to-process mapping template." +8. "If helpful, I'll release the SOP maturity rubric." +9. "Reply 'controls' and I'll share the trigger-threshold matrix." +10. "Follow for implementation-first governance systems, not theory." + +## 10 Commercial CTAs +1. "If you want help implementing this in your org, DM 'implementation'." +2. "I work with teams redesigning legal/risk workflows—message me 'redesign'." +3. "Need an AI governance operating model? Reach out with 'AI ops'." +4. "If you're planning a process transformation this quarter, let's talk." +5. "For advisory support on control design, DM 'controls'." +6. "If this is your bottleneck, I can help you build the rollout plan." +7. "Want an assessment of your current operating model? Message 'assessment'." +8. "If your team is stuck between speed and compliance, let's diagnose it." +9. "Book a strategy call if you need execution-ready governance." +10. "If you'd like this customized to your function, contact me directly." + +## 10 Recruiter-Facing CTAs +1. "If you're hiring for legal ops/GRC leadership, feel free to connect." +2. "Open to speaking on operational AI governance for leadership teams." +3. "If your team needs a systems-minded operator, let's connect." +4. "Recruiters: message me for role-fit discussion and scope alignment." +5. "If you're building a governance function, happy to compare notes." +6. "I'm open to strategic conversations on transformation leadership roles." +7. "If this is the capability gap you're hiring for, DM me." +8. "I share how I lead cross-functional execution—follow for more." +9. "If your org needs process + risk integration leadership, reach out." +10. "For advisory or leadership opportunities in this lane, contact me." diff --git a/output/executive_summary.md b/output/executive_summary.md new file mode 100644 index 0000000..34c561a --- /dev/null +++ b/output/executive_summary.md @@ -0,0 +1,27 @@ +# Executive Summary + +## Current Status +This run established a **repeatable content intelligence workflow** and generated all required deliverable templates using only verifiable local inputs. + +### Confirmed facts +- No creator list files were found. +- No public URL list files were found. +- No transcript files were found. +- No CSV/JSON video metadata files were found. +- No MCP resources or transcript connectors were available. + +### Assumptions +- Analysis must proceed from a **zero-data baseline** and prioritize framework readiness. +- Your target channels and niches (provided in prompt) are treated as strategic scope, not observed market evidence. + +## What Matters Most Right Now +1. The biggest bottleneck is **input acquisition**, not analysis quality. +2. A normalized schema is now defined so future ingest is frictionless. +3. Scoring logic (buyer/recruiter/authority/saturation/differentiation) is now standardized for consistent decision-making across future batches. +4. Output structure is designed to separate: + - observable evidence, + - inferences, + - strategic recommendations. + +## Single Best Next Step +Provide a first analysis batch of **20–30 video URLs** (mixed across LinkedIn, TikTok, Instagram Reels, YouTube Shorts) plus any available transcripts/metadata so the workflow can produce evidence-based rankings and brand-fit recommendations. diff --git a/output/hook_library.md b/output/hook_library.md new file mode 100644 index 0000000..96385c8 --- /dev/null +++ b/output/hook_library.md @@ -0,0 +1,47 @@ +# Hook Library + +## 20 Brand-Aligned Hook Formulas +1. "If your [process] depends on heroics, you don't have a process—you have risk." +2. "Most teams don't need more AI tools; they need a control layer." +3. "Before you automate this workflow, check these 3 failure points." +4. "Your compliance program isn't slow because of policy—it's slow because of handoffs." +5. "The fastest way to improve legal ops is to remove one approval loop." +6. "Here's the governance mistake that makes AI pilots stall in month two." +7. "If you can't explain ownership, you can't scale execution." +8. "This one metric tells me whether a process is actually working." +9. "Everyone talks about AI readiness—almost no one defines decision rights." +10. "You don't have a tooling problem. You have an operating model problem." +11. "If risk review always starts late, here's the upstream fix." +12. "Three signs your workflow is optimized for effort, not outcomes." +13. "Here's how high-performing ops teams separate policy from procedure." +14. "Your KPI dashboard looks healthy—but this hidden queue says otherwise." +15. "Want fewer escalations? Redesign this one intake checkpoint." +16. "What looks like a training issue is often a process architecture issue." +17. "This is how to make compliance a throughput multiplier, not a blocker." +18. "If your AI governance doc has no trigger thresholds, it's not governance." +19. "The most expensive process in your org is the one nobody owns end-to-end." +20. "Operational clarity is a competitive advantage—if you make it executable." + +## 10 Buyer-Facing Hooks +1. "If your contract cycle time is rising, start with this control map." +2. "A 30-minute workflow audit can cut rework faster than new software." +3. "Where governance should sit if you want both speed and assurance." +4. "Three operating model fixes that reduce compliance fire drills." +5. "How to scale AI use without scaling exception risk." +6. "The legal-ops KPI stack that actually predicts bottlenecks." +7. "Your policy update failed for a process reason—here's the fix." +8. "What executive teams need from GRC beyond reporting." +9. "The handoff redesign that improves audit readiness and delivery speed." +10. "How to move from reactive controls to embedded controls in 90 days." + +## 10 Recruiter-Facing Hooks +1. "How I evaluate workflow integrity in under one hour." +2. "My framework for aligning legal, risk, and operations execution." +3. "The operating cadence I use to reduce cross-functional friction." +4. "How I design governance that teams actually adopt." +5. "The scorecard I use to prioritize process improvement opportunities." +6. "How I lead AI readiness without slowing business velocity." +7. "What strong ops leadership looks like in regulated environments." +8. "How I translate ambiguity into executable operating standards." +9. "The decision-rights model I use for high-stakes workflows." +10. "How I build training systems that improve control quality over time." diff --git a/output/market_intelligence.csv b/output/market_intelligence.csv new file mode 100644 index 0000000..78db746 --- /dev/null +++ b/output/market_intelligence.csv @@ -0,0 +1,13 @@ +creator_name,platform,video_title_or_topic,source_url,transcript,views,likes,comments,shares,posting_date,niche,target_audience,opening_hook,cta,tone,credibility_markers,performance_notes,inferred_intent_match,likely_audience_segment_attracted,buyer_value,buyer_value_rationale,recruiter_value,recruiter_value_rationale,authority_signal,authority_signal_rationale,saturation_risk,saturation_risk_rationale,differentiation_potential,differentiation_potential_rationale,observation_vs_inference,needs_review,confidence_score +DryRun Creator,youtube,Dry run candidate for legal operations workflow,https://example.com/dry-run,,,,,,,legal ops,,Dry run candidate for legal operations workflow,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for contract lifecycle bottlenecks,https://example.com/dry-run,,,,,,,legal ops,,Dry run candidate for contract lifecycle bottlenecks,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for legal ops dashboard,https://example.com/dry-run,,,,,,,legal ops,,Dry run candidate for legal ops dashboard,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for GRC operating model,https://example.com/dry-run,,,,,,,grc,,Dry run candidate for GRC operating model,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for risk and compliance process improvement,https://example.com/dry-run,,,,,,,grc,,Dry run candidate for risk and compliance process improvement,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for governance risk compliance metrics,https://example.com/dry-run,,,,,,,grc,,Dry run candidate for governance risk compliance metrics,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for AI governance framework,https://example.com/dry-run,,,,,,,ai governance,,Dry run candidate for AI governance framework,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for enterprise AI risk controls,https://example.com/dry-run,,,,,,,ai governance,,Dry run candidate for enterprise AI risk controls,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for AI policy implementation,https://example.com/dry-run,,,,,,,ai governance,,Dry run candidate for AI policy implementation,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for operations leadership systems,https://example.com/dry-run,,,,,,,operations leadership,,Dry run candidate for operations leadership systems,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for workflow integrity,https://example.com/dry-run,,,,,,,operations leadership,,Dry run candidate for workflow integrity,,,,,,,,,,,,,,,,,observation,yes,0.70 +DryRun Creator,youtube,Dry run candidate for business systems strategy,https://example.com/dry-run,,,,,,,operations leadership,,Dry run candidate for business systems strategy,,,,,,,,,,,,,,,,,observation,yes,0.70 diff --git a/output/pattern_analysis.md b/output/pattern_analysis.md new file mode 100644 index 0000000..679a2b4 --- /dev/null +++ b/output/pattern_analysis.md @@ -0,0 +1,33 @@ +# Pattern Analysis + +## Scope +This document is the pattern-analysis artifact for your market-intelligence workflow. + +## Data Availability Check +- **Observed dataset size:** 0 videos / 0 creators +- **Result:** Pattern outputs are intentionally deferred until evidence is ingested. + +## Pattern Framework (Applied Once Data Exists) +For each batch, evaluate and summarize: + +1. **Top-performing themes** +2. **Hook patterns** (problem-first, contrarian, checklist, myth-bust, POV, story, data-led) +3. **Tone patterns** (operator-pragmatic, executive-briefing, motivational, provocative) +4. **CTA patterns** (engagement, conversion, authority, recruiter-facing) +5. **Buyer signals** (budget, urgency, implementation readiness, role seniority) +6. **Recruiter signals** (job-seeker language, personal-brand positioning, credential signaling) +7. **Oversaturated angles** (high repetition, low distinctiveness) +8. **White-space opportunities** (high pain relevance, low creator coverage) + +## Evidence Rules +- Never infer performance from one platform and apply automatically to another. +- Keep observations and inferences separated. +- Flag missing metadata per record instead of filling with assumptions. +- Do not equate engagement velocity with commercial value. + +## Initial Strategic Hypotheses (Inference-Only; Not Yet Validated) +- Practical, implementation-level legal ops / GRC / AI governance content likely outperforms abstract thought leadership among decision-makers. +- Authority-forward, clarity-first tone likely supports executive trust more than trend-chasing tone. +- Explicit process visuals (frameworks, checklists, before/after workflows) likely improve buyer-value density. + +> These are planning hypotheses only and require validation once real inputs are ingested. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fabc6c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# Discovery v1 intentionally uses Python standard library only in this environment. +# Add optional dependencies here if you later switch to richer YAML/HTTP clients.