sdd-jc-methodology 0.2.0
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.
- package/.claude/commands/sdd-archive.md +122 -0
- package/.claude/commands/sdd-constitution.md +240 -0
- package/.claude/commands/sdd-execute.md +132 -0
- package/.claude/commands/sdd-propose.md +149 -0
- package/.claude/commands/sdd-seo.md +251 -0
- package/.claude/commands/sdd-specify.md +264 -0
- package/.claude/commands/sdd-test.md +128 -0
- package/.claude/commands/sdd-validate.md +165 -0
- package/.claude/skills/api-design-principles/SKILL.md +528 -0
- package/.claude/skills/api-design-principles/assets/api-design-checklist.md +155 -0
- package/.claude/skills/api-design-principles/assets/rest-api-template.py +182 -0
- package/.claude/skills/api-design-principles/references/graphql-schema-design.md +583 -0
- package/.claude/skills/api-design-principles/references/rest-best-practices.md +408 -0
- package/.claude/skills/aws-serverless/SKILL.md +323 -0
- package/.claude/skills/brainstorming/SKILL.md +96 -0
- package/.claude/skills/error-handling-patterns/SKILL.md +641 -0
- package/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/.claude/skills/frontend-design/SKILL.md +272 -0
- package/.claude/skills/nestjs-expert/SKILL.md +552 -0
- package/.claude/skills/product-manager-toolkit/SKILL.md +351 -0
- package/.claude/skills/product-manager-toolkit/references/prd_templates.md +317 -0
- package/.claude/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py +441 -0
- package/.claude/skills/product-manager-toolkit/scripts/rice_prioritizer.py +296 -0
- package/.claude/skills/react-doctor/AGENTS.md +15 -0
- package/.claude/skills/react-doctor/SKILL.md +19 -0
- package/.claude/skills/shadcn-ui/SKILL.md +1677 -0
- package/.claude/skills/shadcn-ui/references/learn.md +145 -0
- package/.claude/skills/shadcn-ui/references/official-ui-reference.md +1725 -0
- package/.claude/skills/shadcn-ui/references/reference.md +586 -0
- package/.claude/skills/shadcn-ui/references/ui-reference.md +1578 -0
- package/.claude/skills/stitch-design/README.md +50 -0
- package/.claude/skills/stitch-design/SKILL.md +84 -0
- package/.claude/skills/stitch-design/examples/DESIGN.md +22 -0
- package/.claude/skills/stitch-design/examples/enhanced-prompt.md +28 -0
- package/.claude/skills/stitch-design/references/design-mappings.md +45 -0
- package/.claude/skills/stitch-design/references/prompt-keywords.md +114 -0
- package/.claude/skills/stitch-design/references/tool-schemas.md +76 -0
- package/.claude/skills/stitch-design/workflows/edit-design.md +44 -0
- package/.claude/skills/stitch-design/workflows/generate-design-md.md +63 -0
- package/.claude/skills/stitch-design/workflows/text-to-design.md +47 -0
- package/.claude/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/.claude/skills/systematic-debugging/SKILL.md +296 -0
- package/.claude/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/.claude/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/.claude/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/.claude/skills/systematic-debugging/find-polluter.sh +63 -0
- package/.claude/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/.claude/skills/systematic-debugging/test-academic.md +14 -0
- package/.claude/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/.claude/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/.claude/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/.claude/skills/tailwind-design-system/SKILL.md +874 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.claude/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.claude/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/.claude/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.claude/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.mcp.json.example +12 -0
- package/CHANGELOG.md +61 -0
- package/LICENSE +21 -0
- package/README.md +571 -0
- package/assets/jc-fox-mark.svg +10 -0
- package/assets/jc-methodology-badge.png +0 -0
- package/bin/sdd-jc.js +379 -0
- package/package.json +43 -0
- package/scripts/gsc_verify.py +162 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Customer Interview Analyzer
|
|
4
|
+
Extracts insights, patterns, and opportunities from user interviews
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict, List, Tuple, Set
|
|
9
|
+
from collections import Counter, defaultdict
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
class InterviewAnalyzer:
|
|
13
|
+
"""Analyze customer interviews for insights and patterns"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
# Pain point indicators
|
|
17
|
+
self.pain_indicators = [
|
|
18
|
+
'frustrat', 'annoy', 'difficult', 'hard', 'confus', 'slow',
|
|
19
|
+
'problem', 'issue', 'struggle', 'challeng', 'pain', 'waste',
|
|
20
|
+
'manual', 'repetitive', 'tedious', 'boring', 'time-consuming',
|
|
21
|
+
'complicated', 'complex', 'unclear', 'wish', 'need', 'want'
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Positive indicators
|
|
25
|
+
self.delight_indicators = [
|
|
26
|
+
'love', 'great', 'awesome', 'amazing', 'perfect', 'easy',
|
|
27
|
+
'simple', 'quick', 'fast', 'helpful', 'useful', 'valuable',
|
|
28
|
+
'save', 'efficient', 'convenient', 'intuitive', 'clear'
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Feature request indicators
|
|
32
|
+
self.request_indicators = [
|
|
33
|
+
'would be nice', 'wish', 'hope', 'want', 'need', 'should',
|
|
34
|
+
'could', 'would love', 'if only', 'it would help', 'suggest',
|
|
35
|
+
'recommend', 'idea', 'what if', 'have you considered'
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Jobs to be done patterns
|
|
39
|
+
self.jtbd_patterns = [
|
|
40
|
+
r'when i\s+(.+?),\s+i want to\s+(.+?)\s+so that\s+(.+)',
|
|
41
|
+
r'i need to\s+(.+?)\s+because\s+(.+)',
|
|
42
|
+
r'my goal is to\s+(.+)',
|
|
43
|
+
r'i\'m trying to\s+(.+)',
|
|
44
|
+
r'i use \w+ to\s+(.+)',
|
|
45
|
+
r'helps me\s+(.+)',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
def analyze_interview(self, text: str) -> Dict:
|
|
49
|
+
"""Analyze a single interview transcript"""
|
|
50
|
+
text_lower = text.lower()
|
|
51
|
+
sentences = self._split_sentences(text)
|
|
52
|
+
|
|
53
|
+
analysis = {
|
|
54
|
+
'pain_points': self._extract_pain_points(sentences),
|
|
55
|
+
'delights': self._extract_delights(sentences),
|
|
56
|
+
'feature_requests': self._extract_requests(sentences),
|
|
57
|
+
'jobs_to_be_done': self._extract_jtbd(text_lower),
|
|
58
|
+
'sentiment_score': self._calculate_sentiment(text_lower),
|
|
59
|
+
'key_themes': self._extract_themes(text_lower),
|
|
60
|
+
'quotes': self._extract_key_quotes(sentences),
|
|
61
|
+
'metrics_mentioned': self._extract_metrics(text),
|
|
62
|
+
'competitors_mentioned': self._extract_competitors(text)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return analysis
|
|
66
|
+
|
|
67
|
+
def _split_sentences(self, text: str) -> List[str]:
|
|
68
|
+
"""Split text into sentences"""
|
|
69
|
+
# Simple sentence splitting
|
|
70
|
+
sentences = re.split(r'[.!?]+', text)
|
|
71
|
+
return [s.strip() for s in sentences if s.strip()]
|
|
72
|
+
|
|
73
|
+
def _extract_pain_points(self, sentences: List[str]) -> List[Dict]:
|
|
74
|
+
"""Extract pain points from sentences"""
|
|
75
|
+
pain_points = []
|
|
76
|
+
|
|
77
|
+
for sentence in sentences:
|
|
78
|
+
sentence_lower = sentence.lower()
|
|
79
|
+
for indicator in self.pain_indicators:
|
|
80
|
+
if indicator in sentence_lower:
|
|
81
|
+
# Extract context around the pain point
|
|
82
|
+
pain_points.append({
|
|
83
|
+
'quote': sentence,
|
|
84
|
+
'indicator': indicator,
|
|
85
|
+
'severity': self._assess_severity(sentence_lower)
|
|
86
|
+
})
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
return pain_points[:10] # Return top 10
|
|
90
|
+
|
|
91
|
+
def _extract_delights(self, sentences: List[str]) -> List[Dict]:
|
|
92
|
+
"""Extract positive feedback"""
|
|
93
|
+
delights = []
|
|
94
|
+
|
|
95
|
+
for sentence in sentences:
|
|
96
|
+
sentence_lower = sentence.lower()
|
|
97
|
+
for indicator in self.delight_indicators:
|
|
98
|
+
if indicator in sentence_lower:
|
|
99
|
+
delights.append({
|
|
100
|
+
'quote': sentence,
|
|
101
|
+
'indicator': indicator,
|
|
102
|
+
'strength': self._assess_strength(sentence_lower)
|
|
103
|
+
})
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
return delights[:10]
|
|
107
|
+
|
|
108
|
+
def _extract_requests(self, sentences: List[str]) -> List[Dict]:
|
|
109
|
+
"""Extract feature requests and suggestions"""
|
|
110
|
+
requests = []
|
|
111
|
+
|
|
112
|
+
for sentence in sentences:
|
|
113
|
+
sentence_lower = sentence.lower()
|
|
114
|
+
for indicator in self.request_indicators:
|
|
115
|
+
if indicator in sentence_lower:
|
|
116
|
+
requests.append({
|
|
117
|
+
'quote': sentence,
|
|
118
|
+
'type': self._classify_request(sentence_lower),
|
|
119
|
+
'priority': self._assess_request_priority(sentence_lower)
|
|
120
|
+
})
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
return requests[:10]
|
|
124
|
+
|
|
125
|
+
def _extract_jtbd(self, text: str) -> List[Dict]:
|
|
126
|
+
"""Extract Jobs to Be Done patterns"""
|
|
127
|
+
jobs = []
|
|
128
|
+
|
|
129
|
+
for pattern in self.jtbd_patterns:
|
|
130
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
131
|
+
for match in matches:
|
|
132
|
+
if isinstance(match, tuple):
|
|
133
|
+
job = ' → '.join(match)
|
|
134
|
+
else:
|
|
135
|
+
job = match
|
|
136
|
+
|
|
137
|
+
jobs.append({
|
|
138
|
+
'job': job,
|
|
139
|
+
'pattern': pattern.pattern if hasattr(pattern, 'pattern') else pattern
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return jobs[:5]
|
|
143
|
+
|
|
144
|
+
def _calculate_sentiment(self, text: str) -> Dict:
|
|
145
|
+
"""Calculate overall sentiment of the interview"""
|
|
146
|
+
positive_count = sum(1 for ind in self.delight_indicators if ind in text)
|
|
147
|
+
negative_count = sum(1 for ind in self.pain_indicators if ind in text)
|
|
148
|
+
|
|
149
|
+
total = positive_count + negative_count
|
|
150
|
+
if total == 0:
|
|
151
|
+
sentiment_score = 0
|
|
152
|
+
else:
|
|
153
|
+
sentiment_score = (positive_count - negative_count) / total
|
|
154
|
+
|
|
155
|
+
if sentiment_score > 0.3:
|
|
156
|
+
sentiment_label = 'positive'
|
|
157
|
+
elif sentiment_score < -0.3:
|
|
158
|
+
sentiment_label = 'negative'
|
|
159
|
+
else:
|
|
160
|
+
sentiment_label = 'neutral'
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
'score': round(sentiment_score, 2),
|
|
164
|
+
'label': sentiment_label,
|
|
165
|
+
'positive_signals': positive_count,
|
|
166
|
+
'negative_signals': negative_count
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
def _extract_themes(self, text: str) -> List[str]:
|
|
170
|
+
"""Extract key themes using word frequency"""
|
|
171
|
+
# Remove common words
|
|
172
|
+
stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at',
|
|
173
|
+
'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is',
|
|
174
|
+
'was', 'are', 'were', 'been', 'be', 'have', 'has',
|
|
175
|
+
'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
176
|
+
'should', 'may', 'might', 'must', 'can', 'shall',
|
|
177
|
+
'it', 'i', 'you', 'we', 'they', 'them', 'their'}
|
|
178
|
+
|
|
179
|
+
# Extract meaningful words
|
|
180
|
+
words = re.findall(r'\b[a-z]{4,}\b', text)
|
|
181
|
+
meaningful_words = [w for w in words if w not in stop_words]
|
|
182
|
+
|
|
183
|
+
# Count frequency
|
|
184
|
+
word_freq = Counter(meaningful_words)
|
|
185
|
+
|
|
186
|
+
# Extract themes (top frequent meaningful words)
|
|
187
|
+
themes = [word for word, count in word_freq.most_common(10) if count >= 3]
|
|
188
|
+
|
|
189
|
+
return themes
|
|
190
|
+
|
|
191
|
+
def _extract_key_quotes(self, sentences: List[str]) -> List[str]:
|
|
192
|
+
"""Extract the most insightful quotes"""
|
|
193
|
+
scored_sentences = []
|
|
194
|
+
|
|
195
|
+
for sentence in sentences:
|
|
196
|
+
if len(sentence) < 20 or len(sentence) > 200:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
score = 0
|
|
200
|
+
sentence_lower = sentence.lower()
|
|
201
|
+
|
|
202
|
+
# Score based on insight indicators
|
|
203
|
+
if any(ind in sentence_lower for ind in self.pain_indicators):
|
|
204
|
+
score += 2
|
|
205
|
+
if any(ind in sentence_lower for ind in self.request_indicators):
|
|
206
|
+
score += 2
|
|
207
|
+
if 'because' in sentence_lower:
|
|
208
|
+
score += 1
|
|
209
|
+
if 'but' in sentence_lower:
|
|
210
|
+
score += 1
|
|
211
|
+
if '?' in sentence:
|
|
212
|
+
score += 1
|
|
213
|
+
|
|
214
|
+
if score > 0:
|
|
215
|
+
scored_sentences.append((score, sentence))
|
|
216
|
+
|
|
217
|
+
# Sort by score and return top quotes
|
|
218
|
+
scored_sentences.sort(reverse=True)
|
|
219
|
+
return [s[1] for s in scored_sentences[:5]]
|
|
220
|
+
|
|
221
|
+
def _extract_metrics(self, text: str) -> List[str]:
|
|
222
|
+
"""Extract any metrics or numbers mentioned"""
|
|
223
|
+
metrics = []
|
|
224
|
+
|
|
225
|
+
# Find percentages
|
|
226
|
+
percentages = re.findall(r'\d+%', text)
|
|
227
|
+
metrics.extend(percentages)
|
|
228
|
+
|
|
229
|
+
# Find time metrics
|
|
230
|
+
time_metrics = re.findall(r'\d+\s*(?:hours?|minutes?|days?|weeks?|months?)', text, re.IGNORECASE)
|
|
231
|
+
metrics.extend(time_metrics)
|
|
232
|
+
|
|
233
|
+
# Find money metrics
|
|
234
|
+
money_metrics = re.findall(r'\$[\d,]+', text)
|
|
235
|
+
metrics.extend(money_metrics)
|
|
236
|
+
|
|
237
|
+
# Find general numbers with context
|
|
238
|
+
number_contexts = re.findall(r'(\d+)\s+(\w+)', text)
|
|
239
|
+
for num, context in number_contexts:
|
|
240
|
+
if context.lower() not in ['the', 'a', 'an', 'and', 'or', 'of']:
|
|
241
|
+
metrics.append(f"{num} {context}")
|
|
242
|
+
|
|
243
|
+
return list(set(metrics))[:10]
|
|
244
|
+
|
|
245
|
+
def _extract_competitors(self, text: str) -> List[str]:
|
|
246
|
+
"""Extract competitor mentions"""
|
|
247
|
+
# Common competitor indicators
|
|
248
|
+
competitor_patterns = [
|
|
249
|
+
r'(?:use|used|using|tried|trying|switch from|switched from|instead of)\s+(\w+)',
|
|
250
|
+
r'(\w+)\s+(?:is better|works better|is easier)',
|
|
251
|
+
r'compared to\s+(\w+)',
|
|
252
|
+
r'like\s+(\w+)',
|
|
253
|
+
r'similar to\s+(\w+)',
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
competitors = set()
|
|
257
|
+
for pattern in competitor_patterns:
|
|
258
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
259
|
+
competitors.update(matches)
|
|
260
|
+
|
|
261
|
+
# Filter out common words
|
|
262
|
+
common_words = {'this', 'that', 'it', 'them', 'other', 'another', 'something'}
|
|
263
|
+
competitors = [c for c in competitors if c.lower() not in common_words and len(c) > 2]
|
|
264
|
+
|
|
265
|
+
return list(competitors)[:5]
|
|
266
|
+
|
|
267
|
+
def _assess_severity(self, text: str) -> str:
|
|
268
|
+
"""Assess severity of pain point"""
|
|
269
|
+
if any(word in text for word in ['very', 'extremely', 'really', 'totally', 'completely']):
|
|
270
|
+
return 'high'
|
|
271
|
+
elif any(word in text for word in ['somewhat', 'bit', 'little', 'slightly']):
|
|
272
|
+
return 'low'
|
|
273
|
+
return 'medium'
|
|
274
|
+
|
|
275
|
+
def _assess_strength(self, text: str) -> str:
|
|
276
|
+
"""Assess strength of positive feedback"""
|
|
277
|
+
if any(word in text for word in ['absolutely', 'definitely', 'really', 'very']):
|
|
278
|
+
return 'strong'
|
|
279
|
+
return 'moderate'
|
|
280
|
+
|
|
281
|
+
def _classify_request(self, text: str) -> str:
|
|
282
|
+
"""Classify the type of request"""
|
|
283
|
+
if any(word in text for word in ['ui', 'design', 'look', 'color', 'layout']):
|
|
284
|
+
return 'ui_improvement'
|
|
285
|
+
elif any(word in text for word in ['feature', 'add', 'new', 'build']):
|
|
286
|
+
return 'new_feature'
|
|
287
|
+
elif any(word in text for word in ['fix', 'bug', 'broken', 'work']):
|
|
288
|
+
return 'bug_fix'
|
|
289
|
+
elif any(word in text for word in ['faster', 'slow', 'performance', 'speed']):
|
|
290
|
+
return 'performance'
|
|
291
|
+
return 'general'
|
|
292
|
+
|
|
293
|
+
def _assess_request_priority(self, text: str) -> str:
|
|
294
|
+
"""Assess priority of request"""
|
|
295
|
+
if any(word in text for word in ['critical', 'urgent', 'asap', 'immediately', 'blocking']):
|
|
296
|
+
return 'critical'
|
|
297
|
+
elif any(word in text for word in ['need', 'important', 'should', 'must']):
|
|
298
|
+
return 'high'
|
|
299
|
+
elif any(word in text for word in ['nice', 'would', 'could', 'maybe']):
|
|
300
|
+
return 'low'
|
|
301
|
+
return 'medium'
|
|
302
|
+
|
|
303
|
+
def aggregate_interviews(interviews: List[Dict]) -> Dict:
|
|
304
|
+
"""Aggregate insights from multiple interviews"""
|
|
305
|
+
aggregated = {
|
|
306
|
+
'total_interviews': len(interviews),
|
|
307
|
+
'common_pain_points': defaultdict(list),
|
|
308
|
+
'common_requests': defaultdict(list),
|
|
309
|
+
'jobs_to_be_done': [],
|
|
310
|
+
'overall_sentiment': {
|
|
311
|
+
'positive': 0,
|
|
312
|
+
'negative': 0,
|
|
313
|
+
'neutral': 0
|
|
314
|
+
},
|
|
315
|
+
'top_themes': Counter(),
|
|
316
|
+
'metrics_summary': set(),
|
|
317
|
+
'competitors_mentioned': Counter()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for interview in interviews:
|
|
321
|
+
# Aggregate pain points
|
|
322
|
+
for pain in interview.get('pain_points', []):
|
|
323
|
+
indicator = pain.get('indicator', 'unknown')
|
|
324
|
+
aggregated['common_pain_points'][indicator].append(pain['quote'])
|
|
325
|
+
|
|
326
|
+
# Aggregate requests
|
|
327
|
+
for request in interview.get('feature_requests', []):
|
|
328
|
+
req_type = request.get('type', 'general')
|
|
329
|
+
aggregated['common_requests'][req_type].append(request['quote'])
|
|
330
|
+
|
|
331
|
+
# Aggregate JTBD
|
|
332
|
+
aggregated['jobs_to_be_done'].extend(interview.get('jobs_to_be_done', []))
|
|
333
|
+
|
|
334
|
+
# Aggregate sentiment
|
|
335
|
+
sentiment = interview.get('sentiment_score', {}).get('label', 'neutral')
|
|
336
|
+
aggregated['overall_sentiment'][sentiment] += 1
|
|
337
|
+
|
|
338
|
+
# Aggregate themes
|
|
339
|
+
for theme in interview.get('key_themes', []):
|
|
340
|
+
aggregated['top_themes'][theme] += 1
|
|
341
|
+
|
|
342
|
+
# Aggregate metrics
|
|
343
|
+
aggregated['metrics_summary'].update(interview.get('metrics_mentioned', []))
|
|
344
|
+
|
|
345
|
+
# Aggregate competitors
|
|
346
|
+
for competitor in interview.get('competitors_mentioned', []):
|
|
347
|
+
aggregated['competitors_mentioned'][competitor] += 1
|
|
348
|
+
|
|
349
|
+
# Process aggregated data
|
|
350
|
+
aggregated['common_pain_points'] = dict(aggregated['common_pain_points'])
|
|
351
|
+
aggregated['common_requests'] = dict(aggregated['common_requests'])
|
|
352
|
+
aggregated['top_themes'] = dict(aggregated['top_themes'].most_common(10))
|
|
353
|
+
aggregated['metrics_summary'] = list(aggregated['metrics_summary'])
|
|
354
|
+
aggregated['competitors_mentioned'] = dict(aggregated['competitors_mentioned'])
|
|
355
|
+
|
|
356
|
+
return aggregated
|
|
357
|
+
|
|
358
|
+
def format_single_interview(analysis: Dict) -> str:
|
|
359
|
+
"""Format single interview analysis"""
|
|
360
|
+
output = ["=" * 60]
|
|
361
|
+
output.append("CUSTOMER INTERVIEW ANALYSIS")
|
|
362
|
+
output.append("=" * 60)
|
|
363
|
+
|
|
364
|
+
# Sentiment
|
|
365
|
+
sentiment = analysis['sentiment_score']
|
|
366
|
+
output.append(f"\n📊 Overall Sentiment: {sentiment['label'].upper()}")
|
|
367
|
+
output.append(f" Score: {sentiment['score']}")
|
|
368
|
+
output.append(f" Positive signals: {sentiment['positive_signals']}")
|
|
369
|
+
output.append(f" Negative signals: {sentiment['negative_signals']}")
|
|
370
|
+
|
|
371
|
+
# Pain Points
|
|
372
|
+
if analysis['pain_points']:
|
|
373
|
+
output.append("\n🔥 Pain Points Identified:")
|
|
374
|
+
for i, pain in enumerate(analysis['pain_points'][:5], 1):
|
|
375
|
+
output.append(f"\n{i}. [{pain['severity'].upper()}] {pain['quote'][:100]}...")
|
|
376
|
+
|
|
377
|
+
# Feature Requests
|
|
378
|
+
if analysis['feature_requests']:
|
|
379
|
+
output.append("\n💡 Feature Requests:")
|
|
380
|
+
for i, req in enumerate(analysis['feature_requests'][:5], 1):
|
|
381
|
+
output.append(f"\n{i}. [{req['type']}] Priority: {req['priority']}")
|
|
382
|
+
output.append(f" \"{req['quote'][:100]}...\"")
|
|
383
|
+
|
|
384
|
+
# Jobs to Be Done
|
|
385
|
+
if analysis['jobs_to_be_done']:
|
|
386
|
+
output.append("\n🎯 Jobs to Be Done:")
|
|
387
|
+
for i, job in enumerate(analysis['jobs_to_be_done'], 1):
|
|
388
|
+
output.append(f"{i}. {job['job']}")
|
|
389
|
+
|
|
390
|
+
# Key Themes
|
|
391
|
+
if analysis['key_themes']:
|
|
392
|
+
output.append("\n🏷️ Key Themes:")
|
|
393
|
+
output.append(", ".join(analysis['key_themes']))
|
|
394
|
+
|
|
395
|
+
# Key Quotes
|
|
396
|
+
if analysis['quotes']:
|
|
397
|
+
output.append("\n💬 Key Quotes:")
|
|
398
|
+
for i, quote in enumerate(analysis['quotes'][:3], 1):
|
|
399
|
+
output.append(f'{i}. "{quote}"')
|
|
400
|
+
|
|
401
|
+
# Metrics
|
|
402
|
+
if analysis['metrics_mentioned']:
|
|
403
|
+
output.append("\n📈 Metrics Mentioned:")
|
|
404
|
+
output.append(", ".join(analysis['metrics_mentioned']))
|
|
405
|
+
|
|
406
|
+
# Competitors
|
|
407
|
+
if analysis['competitors_mentioned']:
|
|
408
|
+
output.append("\n🏢 Competitors Mentioned:")
|
|
409
|
+
output.append(", ".join(analysis['competitors_mentioned']))
|
|
410
|
+
|
|
411
|
+
return "\n".join(output)
|
|
412
|
+
|
|
413
|
+
def main():
|
|
414
|
+
import sys
|
|
415
|
+
|
|
416
|
+
if len(sys.argv) < 2:
|
|
417
|
+
print("Usage: python customer_interview_analyzer.py <interview_file.txt>")
|
|
418
|
+
print("\nThis tool analyzes customer interview transcripts to extract:")
|
|
419
|
+
print(" - Pain points and frustrations")
|
|
420
|
+
print(" - Feature requests and suggestions")
|
|
421
|
+
print(" - Jobs to be done")
|
|
422
|
+
print(" - Sentiment analysis")
|
|
423
|
+
print(" - Key themes and quotes")
|
|
424
|
+
sys.exit(1)
|
|
425
|
+
|
|
426
|
+
# Read interview transcript
|
|
427
|
+
with open(sys.argv[1], 'r') as f:
|
|
428
|
+
interview_text = f.read()
|
|
429
|
+
|
|
430
|
+
# Analyze
|
|
431
|
+
analyzer = InterviewAnalyzer()
|
|
432
|
+
analysis = analyzer.analyze_interview(interview_text)
|
|
433
|
+
|
|
434
|
+
# Output
|
|
435
|
+
if len(sys.argv) > 2 and sys.argv[2] == 'json':
|
|
436
|
+
print(json.dumps(analysis, indent=2))
|
|
437
|
+
else:
|
|
438
|
+
print(format_single_interview(analysis))
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
main()
|