> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Love/Hate → Brand Voice Refinement

> Extract love/hate phrases from G2 reviews to build a Mavera Brand Voice with preferred and avoid terms

## Scenario

Your G2 reviews contain the exact language your customers use to describe what they love and hate about your product. These phrases are gold for brand voice — "love" phrases become preferred terms; "hate" phrases become terms to avoid. You extract these from reviews and feed them into Mavera's Brand Voice engine with explicit preferred and avoid term lists.

**Flow:** G2 `GET /survey-responses` (your product) → Extract love/hate phrases → Mavera `POST /brand-voices` (with preferred\_terms + avoid\_terms)

## Architecture

```mermaid theme={"dark"}
flowchart LR
    A["G2 GET /survey-responses (own product)"] --> B["Extract love/hate phrases"]
    B --> C["POST /api/v1/brand-voices"]
    C --> D["Brand Voice profile"]
```

## Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time, re
  from collections import Counter

  G2 = os.environ["G2_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  G2_BASE = "https://data.g2.com/api/v1"
  G2_H = {"Authorization": f"Token token={G2}", "Content-Type": "application/vnd.api+json"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  # 1. Pull own product reviews
  reviews = []
  page = 1
  while len(reviews) < 200:
      r = requests.get(f"{G2_BASE}/survey-responses",
          headers=G2_H,
          params={"page[size]": 50, "page[number]": page})
      if r.status_code == 429:
          time.sleep(1)
          continue
      r.raise_for_status()
      data = r.json().get("data", [])
      if not data:
          break
      reviews.extend(data)
      page += 1
      time.sleep(0.1)

  # 2. Extract love and hate phrases
  love_texts = []
  hate_texts = []
  for rev in reviews:
      attrs = rev.get("attributes", {})
      for key, val in attrs.get("comment_answers", {}).items():
          text = val if isinstance(val, str) else val.get("text", "")
          if not text.strip():
              continue
          k = key.lower()
          if "love" in k or "best" in k or "like most" in k:
              love_texts.append(text)
          elif "dislike" in k or "hate" in k or "don't like" in k or "cons" in k:
              hate_texts.append(text)

  # 3. Extract key phrases
  def extract_phrases(texts, min_count=2):
      words = Counter()
      bigrams = Counter()
      for t in texts:
          clean = re.sub(r'[^\w\s]', '', t.lower())
          tokens = clean.split()
          words.update(tokens)
          for i in range(len(tokens) - 1):
              bigrams[f"{tokens[i]} {tokens[i+1]}"] += 1
      stop = {"the", "a", "an", "is", "it", "to", "and", "of", "in", "for", "that", "this", "with", "on", "i", "we", "our"}
      phrases = [(p, c) for p, c in bigrams.items() if c >= min_count and not all(w in stop for w in p.split())]
      return sorted(phrases, key=lambda x: -x[1])[:20]

  love_phrases = extract_phrases(love_texts)
  hate_phrases = extract_phrases(hate_texts)

  preferred_terms = [p for p, _ in love_phrases[:15]]
  avoid_terms = [p for p, _ in hate_phrases[:15]]

  # 4. Create Brand Voice
  love_samples = "\n\n---\n\n".join(love_texts[:15])

  bv = requests.post(f"{MV_BASE}/brand-voices",
      headers=MV_H,
      json={
          "name": "G2 Customer Voice",
          "samples": [love_samples],
          "preferred_terms": preferred_terms,
          "avoid_terms": avoid_terms,
      }).json()

  print(f"Brand Voice: {bv['id']}")
  print(f"\nPreferred terms ({len(preferred_terms)}):")
  for p, c in love_phrases[:10]:
      print(f"  ✓ '{p}' ({c}x)")
  print(f"\nAvoid terms ({len(avoid_terms)}):")
  for p, c in hate_phrases[:10]:
      print(f"  ✗ '{p}' ({c}x)")

  # 5. Test with a generation
  from openai import OpenAI
  mavera = OpenAI(api_key=MV, base_url=MV_BASE)
  test = mavera.responses.create(model="mavera-1",
      input=[{"role": "user", "content": "Write a 100-word product description for our landing page."}],
      extra_body={"brand_voice_id": bv["id"]})
  print(f"\n--- Test Generation ---\n{test.output[0].content[0].text}")
  ```

  ```javascript JavaScript theme={"dark"}
  const G2 = process.env.G2_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const G2_BASE = "https://data.g2.com/api/v1";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const G2_H = { Authorization: `Token token=${G2}`, "Content-Type": "application/vnd.api+json" };
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const OpenAI = require("openai").default;

  // 1. Pull reviews
  const reviews = [];
  let page = 1;
  while (reviews.length < 200) {
    const res = await fetch(`${G2_BASE}/survey-responses?page[size]=50&page[number]=${page}`, { headers: G2_H });
    if (res.status === 429) { await new Promise((r) => setTimeout(r, 1000)); continue; }
    const data = (await res.json()).data || [];
    if (!data.length) break;
    reviews.push(...data);
    page++;
    await new Promise((r) => setTimeout(r, 100));
  }

  // 2. Extract love/hate
  const loveTexts = [], hateTexts = [];
  for (const rev of reviews) {
    for (const [key, val] of Object.entries(rev.attributes?.comment_answers || {})) {
      const text = typeof val === "string" ? val : val?.text || "";
      if (!text.trim()) continue;
      const k = key.toLowerCase();
      if (k.includes("love") || k.includes("best")) loveTexts.push(text);
      else if (k.includes("dislike") || k.includes("hate") || k.includes("cons")) hateTexts.push(text);
    }
  }

  // 3. Extract phrases
  function extractPhrases(texts, minCount = 2) {
    const bigrams = {};
    const stop = new Set(["the", "a", "an", "is", "it", "to", "and", "of", "in", "for", "that", "this", "with", "on", "i", "we"]);
    for (const t of texts) {
      const tokens = t.toLowerCase().replace(/[^\w\s]/g, "").split(/\s+/);
      for (let i = 0; i < tokens.length - 1; i++) {
        const bg = `${tokens[i]} ${tokens[i + 1]}`;
        bigrams[bg] = (bigrams[bg] || 0) + 1;
      }
    }
    return Object.entries(bigrams)
      .filter(([p, c]) => c >= minCount && !p.split(" ").every((w) => stop.has(w)))
      .sort(([, a], [, b]) => b - a).slice(0, 20);
  }

  const lovePhrases = extractPhrases(loveTexts);
  const hatePhrases = extractPhrases(hateTexts);

  // 4. Brand Voice
  const bv = await fetch(`${MV_BASE}/brand-voices`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "G2 Customer Voice",
      samples: [loveTexts.slice(0, 15).join("\n\n---\n\n")],
      preferred_terms: lovePhrases.slice(0, 15).map(([p]) => p),
      avoid_terms: hatePhrases.slice(0, 15).map(([p]) => p),
    }),
  }).then((r) => r.json());

  console.log(`Brand Voice: ${bv.id}`);
  lovePhrases.slice(0, 10).forEach(([p, c]) => console.log(`  ✓ '${p}' (${c}x)`));
  hatePhrases.slice(0, 10).forEach(([p, c]) => console.log(`  ✗ '${p}' (${c}x)`));

  // 5. Test generation
  const mavera = new OpenAI({ apiKey: MV, baseURL: MV_BASE });
  const test = await mavera.responses.create({
    model: "mavera-1",
    input: [{ role: "user", content: "Write a 100-word landing page description." }],
    extra_body: { brand_voice_id: bv.id },
  });
  console.log(`\n--- Test ---\n${test.output[0].content[0].text}`);
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
Brand Voice: bv_g2_voice_8m2

Preferred terms (15):
  ✓ 'easy use' (42x)
  ✓ 'customer support' (31x)
  ✓ 'saves time' (28x)
  ✓ 'intuitive interface' (19x)
  ✓ 'game changer' (14x)

Avoid terms (15):
  ✗ 'steep learning' (18x)
  ✗ 'slow loading' (12x)
  ✗ 'limited integrations' (9x)
  ✗ 'pricing confusing' (7x)
  ✗ 'missing features' (6x)

--- Test Generation ---
Stop guessing what your customers think — know it. Our platform turns raw
feedback into tested messaging in minutes, not months. The interface is
intuitive enough for marketers and powerful enough for researchers. Set up
your first focus group before lunch; have persona-validated copy by EOD.
```

## Error Handling

<AccordionGroup>
  <Accordion title="Insufficient review volume">Phrase extraction needs 50+ reviews for meaningful bigrams. With fewer, use individual word frequency or pass raw text directly to Brand Voice without phrase extraction.</Accordion>
  <Accordion title="Multilingual reviews">G2 reviews may be in multiple languages. Filter by `attributes.language` or use Mave to translate before phrase extraction.</Accordion>
</AccordionGroup>
