DIrtyCha commited on
Commit
acaf471
·
1 Parent(s): a6c915f

Initial commit from PainReport

Browse files
Files changed (48) hide show
  1. .env.example +12 -0
  2. .gitattributes +2 -35
  3. .gitignore +14 -0
  4. Backend/data/questionnaire_form.xlsx +0 -0
  5. Backend/images/Tingling.jpg +0 -0
  6. Backend/images/burning.jpg +0 -0
  7. Backend/images/dull.jpg +0 -0
  8. Backend/images/sharp.png +0 -0
  9. Backend/images/throbbing.jpg +0 -0
  10. Backend/inference/__init__.py +6 -0
  11. Backend/inference/rule_engine.py +355 -0
  12. Backend/main.py +214 -0
  13. Backend/models/__init__.py +6 -0
  14. Backend/models/pain_schema.py +192 -0
  15. Backend/ontology/__init__.py +20 -0
  16. Backend/ontology/mcgill_translations.py +268 -0
  17. Backend/ontology/pain_mapping.py +449 -0
  18. Backend/ontology/pain_mapping_multilingual.py +320 -0
  19. Backend/pipeline/__init__.py +6 -0
  20. Backend/pipeline/pain_assessment_pipeline.py +644 -0
  21. Backend/read_xlsx.py +33 -0
  22. Backend/scripts/format_pain_descriptors.py +85 -0
  23. Backend/scripts/multilingual_pain_data.json +1525 -0
  24. Backend/scripts/pain_descriptors_formatted.py +1533 -0
  25. Backend/scripts/parse_multilingual_data.py +132 -0
  26. Backend/services/__init__.py +0 -0
  27. Backend/services/conversation_service.py +52 -0
  28. Backend/services/llm_service.py +796 -0
  29. Backend/services/neuro_symbolic_service.py +304 -0
  30. Backend/services/semantic_distance_service.py +124 -0
  31. Backend/services/semantic_distance_service_biolord.py +320 -0
  32. Backend/services/semantic_distance_service_v2.py +259 -0
  33. Backend/services/whisper_service.py +27 -0
  34. Backend/test_multilingual_pipeline.py +112 -0
  35. Backend/utils/__init__.py +1 -0
  36. Backend/utils/language_detector.py +112 -0
  37. Backend/utils/report_generator.py +292 -0
  38. Frontend/demo.html +942 -0
  39. Procfile +1 -0
  40. app.py +126 -0
  41. quick_test.py +180 -0
  42. requirements.txt +12 -0
  43. start_server.bat +47 -0
  44. test_api.py +166 -0
  45. test_biolord.py +132 -0
  46. test_crosslingual.py +0 -0
  47. test_mcgill_matching.py +147 -0
  48. test_report.py +80 -0
.env.example ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Example environment variables
2
+ # Copy this file to .env and fill in your values
3
+
4
+ # Required: OpenAI API key for GPT-5.2 report generation
5
+ OPENAI_API_KEY=sk-your-api-key-here
6
+
7
+ # Optional: Choose embedding model
8
+ # Options: "biolord" (recommended, free, local) or "openai" (paid, API)
9
+ EMBEDDING_MODEL=biolord
10
+
11
+ # Optional: API configuration (for Hugging Face Spaces)
12
+ # API_URL=http://localhost:8000
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # env
2
+ .env
3
+ Backend/.env
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.pyc
9
+
10
+ # IDE
11
+ .vscode/
12
+
13
+ report.txt
14
+ MULTILINGUAL_USAGE_GUIDE.md
Backend/data/questionnaire_form.xlsx ADDED
Binary file (48.2 kB). View file
 
Backend/images/Tingling.jpg ADDED
Backend/images/burning.jpg ADDED
Backend/images/dull.jpg ADDED
Backend/images/sharp.png ADDED
Backend/images/throbbing.jpg ADDED
Backend/inference/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """
2
+ Rule-based clinical inference engine for deterministic medical reasoning.
3
+ """
4
+ from .rule_engine import ClinicalRule, RuleEngine
5
+
6
+ __all__ = ['ClinicalRule', 'RuleEngine']
Backend/inference/rule_engine.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Deterministic rule-based clinical inference engine.
3
+
4
+ Applies expert-validated If-Then rules to structured pain ontology data.
5
+ NO probabilistic reasoning or LLM-based inference - all clinical logic is
6
+ deterministic and traceable.
7
+
8
+ This module implements the symbolic reasoning component of the neuro-symbolic
9
+ hybrid architecture, ensuring medical decisions are explainable and evidence-based.
10
+ """
11
+
12
+ from typing import List, Dict, Callable
13
+ from dataclasses import dataclass
14
+ import sys
15
+ import os
16
+
17
+ # Add Backend to path for imports
18
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
+
20
+ from models.pain_schema import PainOntology, ClinicalRecommendation
21
+
22
+
23
+ @dataclass
24
+ class ClinicalRule:
25
+ """
26
+ Represents a single clinical decision rule.
27
+
28
+ Each rule consists of:
29
+ - Condition: A function that evaluates PainOntology and returns True/False
30
+ - Action: The recommendation to make if condition is met
31
+ - Evidence fields: Which PainOntology fields to include as evidence
32
+ - Guideline reference: Citation to clinical guideline supporting the rule
33
+ """
34
+ rule_id: str
35
+ name: str
36
+ condition: Callable[[PainOntology], bool]
37
+ recommendation: str
38
+ evidence_fields: List[str]
39
+ guideline_reference: str = None
40
+ confidence: str = "high"
41
+
42
+
43
+ class RuleEngine:
44
+ """
45
+ Expert system rule engine for pain assessment.
46
+
47
+ Applies deterministic clinical rules to structured pain data and generates
48
+ explainable recommendations with complete evidence chains.
49
+
50
+ All rules are:
51
+ 1. Based on established clinical guidelines
52
+ 2. Deterministic (no probabilistic inference)
53
+ 3. Fully explainable (evidence is explicitly tracked)
54
+ 4. Independently verifiable by medical experts
55
+ """
56
+
57
+ def __init__(self):
58
+ """Initialize rule engine and load clinical decision rules."""
59
+ self.rules: List[ClinicalRule] = []
60
+ self._initialize_rules()
61
+
62
+ def _initialize_rules(self):
63
+ """
64
+ Initialize clinical decision rules.
65
+
66
+ Each rule is based on established clinical guidelines and pain management
67
+ best practices. Rules are evaluated in order of priority.
68
+ """
69
+
70
+ # ========== RULE A: Chronic Pain + Depressive Symptoms → Behavioral Therapy ==========
71
+ def rule_a_condition(pain_data: PainOntology) -> bool:
72
+ """
73
+ Chronic pain with significant affective distress requires multimodal approach.
74
+
75
+ Based on: Wisconsin Medical Examining Board Guidelines for Chronic Pain Management
76
+ Rationale: Chronic pain with depression benefits from CBT and non-pharmacologic
77
+ interventions before considering pharmacological escalation.
78
+ """
79
+ temporal_chronic = (
80
+ pain_data.temporal_pattern and
81
+ ("chronic" in pain_data.temporal_pattern.lower() or "months" in pain_data.temporal_pattern.lower())
82
+ )
83
+
84
+ emotion_depressed = pain_data.emotion and any(
85
+ term in pain_data.emotion.lower()
86
+ for term in ["depressed", "depression", "despair", "hopeless"]
87
+ )
88
+
89
+ return temporal_chronic and emotion_depressed
90
+
91
+ self.rules.append(ClinicalRule(
92
+ rule_id="RULE_A",
93
+ name="Chronic Pain + Depression → Behavioral Therapy",
94
+ condition=rule_a_condition,
95
+ recommendation=(
96
+ "Recommend behavioral therapy (CBT) per Wisconsin chronic pain guidelines. "
97
+ "Chronic pain with significant depressive symptoms benefits from "
98
+ "culturally concordant behavioral interventions. Prioritize non-pharmacologic "
99
+ "multidisciplinary care before considering pharmacological escalation."
100
+ ),
101
+ evidence_fields=["temporal_pattern", "emotion"],
102
+ guideline_reference="Wisconsin Medical Examining Board Guidelines for Chronic Pain Management",
103
+ confidence="high"
104
+ ))
105
+
106
+ # ========== RULE B: Neuropathic Pain in Distal Extremities → Peripheral Neuropathy Screening ==========
107
+ def rule_b_condition(pain_data: PainOntology) -> bool:
108
+ """
109
+ Neuropathic pain in hands/feet suggests peripheral neuropathy.
110
+
111
+ Rationale: Classic presentation of peripheral neuropathy includes neuropathic
112
+ pain descriptors (electric-shock, tingling, burning) in distal extremities.
113
+ Requires screening for underlying causes (diabetes, vitamin B12 deficiency, etc.)
114
+ """
115
+ neuropathic = pain_data.pain_type and "neuropathic" in pain_data.pain_type.lower()
116
+
117
+ distal_location = pain_data.location and any(
118
+ loc in pain_data.location.lower()
119
+ for loc in ["lower extremities", "feet", "hands", "legs", "arms", "extremities"]
120
+ )
121
+
122
+ return neuropathic and distal_location
123
+
124
+ self.rules.append(ClinicalRule(
125
+ rule_id="RULE_B",
126
+ name="Neuropathic Pain in Distal Extremities → Peripheral Neuropathy Screening",
127
+ condition=rule_b_condition,
128
+ recommendation=(
129
+ "Recommend peripheral neuropathy screening. "
130
+ "Neuropathic pain in distal extremities suggests possible peripheral nerve pathology. "
131
+ "Consider neurological examination and investigation of potential underlying causes "
132
+ "(diabetes mellitus, vitamin B12 deficiency, autoimmune conditions, medication toxicity)."
133
+ ),
134
+ evidence_fields=["pain_type", "location"],
135
+ confidence="high"
136
+ ))
137
+
138
+ # ========== RULE C: Severe Functional Impact → Multidisciplinary Pain Clinic Referral ==========
139
+ def rule_c_condition(pain_data: PainOntology) -> bool:
140
+ """
141
+ Severe functional impairment requires comprehensive pain management.
142
+
143
+ Rationale: Pain causing significant functional disability (sleep, work, mobility)
144
+ often requires multidisciplinary approach beyond primary care.
145
+ """
146
+ has_functional_impact = pain_data.functional_impact is not None
147
+
148
+ severe_impact = has_functional_impact and any(
149
+ term in pain_data.functional_impact.lower()
150
+ for term in ["severe", "unable", "cannot", "impossible", "interfere", "disability"]
151
+ )
152
+
153
+ return severe_impact
154
+
155
+ self.rules.append(ClinicalRule(
156
+ rule_id="RULE_C",
157
+ name="Severe Functional Impact → Multidisciplinary Pain Clinic",
158
+ condition=rule_c_condition,
159
+ recommendation=(
160
+ "Consider referral to multidisciplinary pain clinic. "
161
+ "Severe functional impairment indicates need for comprehensive pain management "
162
+ "involving physical therapy, occupational therapy, psychological support, and "
163
+ "coordinated medical management."
164
+ ),
165
+ evidence_fields=["functional_impact", "temporal_pattern"],
166
+ confidence="high"
167
+ ))
168
+
169
+ # ========== RULE D: Burning Pain → Consider Inflammatory or Neuropathic Etiology ==========
170
+ def rule_d_condition(pain_data: PainOntology) -> bool:
171
+ """
172
+ Burning pain quality suggests specific etiologies.
173
+
174
+ Rationale: Burning pain can indicate inflammatory processes or small fiber neuropathy.
175
+ """
176
+ burning_pain = pain_data.pain_type and "burning" in pain_data.pain_type.lower()
177
+ return burning_pain
178
+
179
+ self.rules.append(ClinicalRule(
180
+ rule_id="RULE_D",
181
+ name="Burning Pain → Inflammatory/Neuropathic Workup",
182
+ condition=rule_d_condition,
183
+ recommendation=(
184
+ "Burning pain quality suggests possible inflammatory or small fiber neuropathic etiology. "
185
+ "Consider evaluation for inflammatory conditions, nerve injury, or small fiber neuropathy. "
186
+ "May benefit from topical treatments or neuropathic pain medications."
187
+ ),
188
+ evidence_fields=["pain_type", "location"],
189
+ confidence="medium"
190
+ ))
191
+
192
+ def evaluate(self, pain_data: PainOntology) -> List[ClinicalRecommendation]:
193
+ """
194
+ Apply all rules to the pain data and return triggered recommendations.
195
+
196
+ Each recommendation includes:
197
+ - The clinical recommendation text
198
+ - Which rule triggered it
199
+ - The specific evidence (field values) that triggered the rule
200
+ - Confidence level
201
+ - Guideline reference
202
+
203
+ Args:
204
+ pain_data: Structured pain ontology data
205
+
206
+ Returns:
207
+ List of clinical recommendations with complete evidence chains
208
+
209
+ Example:
210
+ >>> pain = PainOntology(
211
+ ... pain_type="Neuropathic (Electric-shock-like)",
212
+ ... location="Lower extremities",
213
+ ... temporal_pattern="Chronic (4 months)",
214
+ ... emotion="Depressed"
215
+ ... )
216
+ >>> engine = RuleEngine()
217
+ >>> recommendations = engine.evaluate(pain)
218
+ >>> # Returns recommendations for RULE_A and RULE_B
219
+ """
220
+ recommendations = []
221
+
222
+ for rule in self.rules:
223
+ try:
224
+ if rule.condition(pain_data):
225
+ # Extract evidence from specified fields
226
+ evidence = {}
227
+ for field in rule.evidence_fields:
228
+ if hasattr(pain_data, field):
229
+ value = getattr(pain_data, field)
230
+ if value is not None: # Only include non-None values
231
+ evidence[field] = value
232
+
233
+ recommendations.append(ClinicalRecommendation(
234
+ recommendation=rule.recommendation,
235
+ triggered_by_rule=f"{rule.rule_id}: {rule.name}",
236
+ evidence=evidence,
237
+ confidence=rule.confidence,
238
+ guideline_reference=rule.guideline_reference
239
+ ))
240
+ except Exception as e:
241
+ # Log error but continue evaluating other rules
242
+ print(f"Warning: Error evaluating {rule.rule_id}: {str(e)}")
243
+ continue
244
+
245
+ return recommendations
246
+
247
+ def generate_reasoning_chain(
248
+ self,
249
+ pain_data: PainOntology,
250
+ recommendations: List[ClinicalRecommendation],
251
+ ontology_mappings: List[Dict]
252
+ ) -> List[str]:
253
+ """
254
+ Generate human-readable reasoning chain showing complete decision pathway.
255
+
256
+ This provides full transparency from patient input to clinical recommendations,
257
+ enabling clinical validation and building trust in the system.
258
+
259
+ The reasoning chain includes:
260
+ 1. Ontology mapping (Chinese → English medical terms)
261
+ 2. Structured pain data extraction
262
+ 3. Rule evaluation and triggers
263
+ 4. Final recommendations with evidence
264
+
265
+ Args:
266
+ pain_data: Structured pain ontology data
267
+ recommendations: List of triggered recommendations
268
+ ontology_mappings: List of multilingual→English mappings
269
+
270
+ Returns:
271
+ List of reasoning step strings
272
+
273
+ Example output:
274
+ [
275
+ "=== Ontology Mapping ===",
276
+ "Input '电击一样' → Mapped to 'Electric-shock-like (Neuropathic)'",
277
+ "=== Structured Pain Data ===",
278
+ "Pain Type: Neuropathic (Electric-shock-like)",
279
+ "=== Rule Engine Evaluation ===",
280
+ "✓ Triggered: RULE_B",
281
+ " Evidence: {'pain_type': 'Neuropathic', 'location': 'Lower extremities'}"
282
+ ]
283
+ """
284
+ chain = []
285
+
286
+ # ===== Step 1: Show ontology mappings =====
287
+ chain.append("=== Ontology Mapping ===")
288
+ if ontology_mappings:
289
+ for mapping in ontology_mappings:
290
+ # Show: matched text in user input → dictionary term → English translation
291
+ matched = mapping.get('matched_text', mapping.get('original_term', 'N/A'))
292
+ original = mapping.get('original_term', 'N/A')
293
+ english = mapping.get('mapped_english', 'N/A')
294
+
295
+ # Display: what user said → what it matches → English term
296
+ if matched != original:
297
+ display_input = f"{matched} (matches '{original}')"
298
+ else:
299
+ display_input = matched
300
+
301
+ chain.append(
302
+ f"Input '{display_input}' → "
303
+ f"Mapped to '{english}' "
304
+ f"({mapping.get('dimension', 'N/A')}, confidence: {mapping.get('confidence', 'N/A')})"
305
+ )
306
+ else:
307
+ chain.append("No specific pain descriptors were mapped from ontology dictionary.")
308
+
309
+ # ===== Step 2: Show structured data extraction =====
310
+ chain.append("\n=== Structured Pain Data ===")
311
+ chain.append(f"Pain Type: {pain_data.pain_type}")
312
+ chain.append(f"Location: {pain_data.location}")
313
+ chain.append(f"Temporal Pattern: {pain_data.temporal_pattern}")
314
+ if pain_data.intensity and pain_data.intensity != "Not explicitly stated":
315
+ chain.append(f"Intensity: {pain_data.intensity}")
316
+ if pain_data.emotion:
317
+ chain.append(f"Emotional Dimension: {pain_data.emotion}")
318
+ if pain_data.functional_impact:
319
+ chain.append(f"Functional Impact: {pain_data.functional_impact}")
320
+
321
+ # ===== Step 3: Show rule triggers and recommendations =====
322
+ chain.append("\n=== Rule Engine Evaluation ===")
323
+ if recommendations:
324
+ for rec in recommendations:
325
+ chain.append(f"✓ Triggered: {rec.triggered_by_rule}")
326
+ chain.append(f" Evidence: {rec.evidence}")
327
+ chain.append(f" → Recommendation: {rec.recommendation}")
328
+ if rec.guideline_reference:
329
+ chain.append(f" → Guideline: {rec.guideline_reference}")
330
+ chain.append("") # Blank line for readability
331
+ else:
332
+ chain.append("No specific clinical rules triggered.")
333
+ chain.append("Standard pain assessment and management pathway recommended.")
334
+
335
+ return chain
336
+
337
+ def add_rule(self, rule: ClinicalRule):
338
+ """
339
+ Add a custom clinical rule to the engine.
340
+
341
+ This allows for dynamic rule expansion and customization based on
342
+ specific clinical contexts or institutional guidelines.
343
+
344
+ Args:
345
+ rule: ClinicalRule instance to add
346
+ """
347
+ self.rules.append(rule)
348
+
349
+ def get_rule_count(self) -> int:
350
+ """Return the number of active rules in the engine."""
351
+ return len(self.rules)
352
+
353
+ def get_rule_ids(self) -> List[str]:
354
+ """Return list of all rule IDs for reference."""
355
+ return [rule.rule_id for rule in self.rules]
Backend/main.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ import os
5
+
6
+ from services.whisper_service import transcribeAudio
7
+ from services.llm_service import analyzePainDescription
8
+ from services.conversation_service import generateFollowUpQuestions
9
+ from services.neuro_symbolic_service import analyze_pain_neuro_symbolic, get_system_info
10
+
11
+ # Smart embedding service selection
12
+ EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "biolord") # Options: "biolord", "openai"
13
+
14
+ if EMBEDDING_MODEL == "biolord":
15
+ print(f"[Main] Using BioLORD-2023-M embeddings (medical specialist)")
16
+ from services.semantic_distance_service_biolord import precompute_dictionary_embeddings
17
+ else:
18
+ print(f"[Main] Using OpenAI embeddings (general purpose)")
19
+ from services.semantic_distance_service_v2 import precompute_dictionary_embeddings
20
+
21
+ from pydantic import BaseModel
22
+ from typing import List, Dict
23
+
24
+
25
+ class ConversationRequest(BaseModel):
26
+ history: List[Dict]
27
+
28
+
29
+ app = FastAPI(
30
+ title = "Pain Report Platform",
31
+ version = "0.2.0" # Updated to v0.2.0 with BioLORD support
32
+ )
33
+
34
+ app.add_middleware(
35
+ CORSMiddleware,
36
+ allow_origins=["*"],
37
+ allow_credentials=True,
38
+ allow_methods=["*"],
39
+ allow_headers=["*"],
40
+ )
41
+
42
+ app.mount("/images", StaticFiles(directory="images"), name="images")
43
+
44
+ @app.get("/")
45
+ async def root():
46
+ return {
47
+ "message": "This is pain report platform backend API.",
48
+ "status": "running"
49
+ }
50
+
51
+ @app.get("/health")
52
+ async def healthCheck():
53
+ return {
54
+ "status": "healthy",
55
+ "message": "The API is healthy and running",
56
+ "version": "0.1.3"
57
+ }
58
+
59
+
60
+ @app.post("/api/analyze-audio")
61
+ async def analyze_audio(file: UploadFile = File(...)):
62
+ #check file
63
+ if not file.content_type.startswith("audio/"):
64
+ return {"error": "Invalid file type.",
65
+ "message": "Please upload an audio file."}
66
+
67
+ #read file
68
+ audioBytes = await file.read()
69
+
70
+ try:
71
+ transcription = transcribeAudio(audioBytes, language=None)
72
+
73
+ analysis = analyzePainDescription(transcription["text"])
74
+
75
+ return {
76
+ "status": "success",
77
+ "message": "Audio file received",
78
+ "size": len(audioBytes),
79
+ "filename": file.filename,
80
+ "trancription": transcription["text"],
81
+ "language": transcription["language"],
82
+ "analysis": analysis
83
+ }
84
+
85
+ except Exception as e:
86
+ return {
87
+ "status": "error",
88
+ "message": str(e)
89
+ }
90
+
91
+
92
+ @app.post("/api/follow-up")
93
+ async def getFollowUpQuestion(request: ConversationRequest):
94
+ try:
95
+ followUp = generateFollowUpQuestions(request.history)
96
+
97
+ return {
98
+ "status": "success",
99
+ "followup": followUp
100
+ }
101
+
102
+ except Exception as e:
103
+ return {
104
+ "status": "error",
105
+ "message": str(e)
106
+ }
107
+
108
+
109
+ @app.post("/api/analyze-text-neuro-symbolic")
110
+ async def analyzeTextNeuroSymbolic(request: dict):
111
+ """
112
+ Analyze text pain description using neuro-symbolic architecture.
113
+
114
+ This is the new upgraded analysis endpoint that uses:
115
+ - LLM for narrow-scope entity extraction only
116
+ - Ontology mapping for multilingual medical terminology
117
+ - Rule-based engine for deterministic clinical recommendations
118
+
119
+ Returns structured pain data with complete explainability and reasoning chain.
120
+ """
121
+ try:
122
+ patient_text = request.get("text", "")
123
+ if not patient_text:
124
+ return {
125
+ "status": "error",
126
+ "message": "No text provided"
127
+ }
128
+
129
+ # Execute neuro-symbolic pipeline
130
+ analysis = analyze_pain_neuro_symbolic(patient_text)
131
+ return analysis
132
+
133
+ except Exception as e:
134
+ return {
135
+ "status": "error",
136
+ "message": str(e)
137
+ }
138
+
139
+
140
+ @app.post("/api/analyze-audio-neuro-symbolic")
141
+ async def analyzeAudioNeuroSymbolic(file: UploadFile = File(...)):
142
+ """
143
+ Analyze audio pain description using neuro-symbolic architecture.
144
+
145
+ Combines:
146
+ 1. Whisper transcription (audio → text)
147
+ 2. Neuro-symbolic analysis (text → structured clinical data)
148
+
149
+ Returns complete explainable report with reasoning chain.
150
+ """
151
+ if not file.content_type.startswith("audio/"):
152
+ return {
153
+ "error": "Invalid file type.",
154
+ "message": "Please upload an audio file."
155
+ }
156
+
157
+ audioBytes = await file.read()
158
+
159
+ try:
160
+ # Step 1: Transcribe audio
161
+ transcription_result = transcribeAudio(audioBytes, language=None)
162
+ original_transcription = transcription_result["text"]
163
+
164
+ # Step 2: Neuro-symbolic analysis (includes normalization + ontology mapping)
165
+ analysis = analyze_pain_neuro_symbolic(original_transcription)
166
+
167
+ # Merge transcription info with analysis results
168
+ # The analysis already contains transcription normalization in analysis["transcription"]
169
+ return {
170
+ "status": "success",
171
+ "message": "Audio analyzed successfully using neuro-symbolic architecture",
172
+ "size": len(audioBytes),
173
+ "filename": file.filename,
174
+ "whisper_language": transcription_result["language"],
175
+ **analysis # Spread analysis results (includes transcription, structured_data, etc.)
176
+ }
177
+
178
+ except Exception as e:
179
+ return {
180
+ "status": "error",
181
+ "message": str(e)
182
+ }
183
+
184
+
185
+ @app.get("/api/system-info")
186
+ async def getSystemInfo():
187
+ """
188
+ Get information about the neuro-symbolic pain assessment system.
189
+
190
+ Returns system configuration, capabilities, and limitations.
191
+ Useful for documentation and debugging.
192
+ """
193
+ try:
194
+ info = get_system_info()
195
+ return {
196
+ "status": "success",
197
+ "system_info": info
198
+ }
199
+ except Exception as e:
200
+ return {
201
+ "status": "error",
202
+ "message": str(e)
203
+ }
204
+
205
+ @app.on_event("startup")
206
+ async def startup_event():
207
+ print("[Startup] Precomputing dictionary embeddings...")
208
+ precompute_dictionary_embeddings()
209
+ print("[Startup] System ready!")
210
+
211
+ if __name__ == "__main__":
212
+ import uvicorn
213
+ uvicorn.run(app, host="0.0.0.0", port=8000)
214
+
Backend/models/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic data models for neuro-symbolic pain assessment.
3
+ """
4
+ from .pain_schema import PainOntology, ClinicalRecommendation, ExplainableReport
5
+
6
+ __all__ = ['PainOntology', 'ClinicalRecommendation', 'ExplainableReport']
Backend/models/pain_schema.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic data models for structured pain assessment.
3
+
4
+ This module defines the core data structures for the neuro-symbolic pain assessment system:
5
+ - PainOntology: Structured representation of pain characteristics
6
+ - ClinicalRecommendation: Rule-based clinical recommendations with evidence
7
+ - ExplainableReport: Complete assessment output with reasoning chain
8
+ """
9
+
10
+ from pydantic import BaseModel, Field
11
+ from typing import Optional, List, Dict, Any
12
+
13
+
14
+ class PainOntology(BaseModel):
15
+ """
16
+ Core pain ontology representing structured clinical pain data.
17
+
18
+ Maps unstructured patient descriptions to standardized medical terminology
19
+ aligned with McGill Pain Questionnaire (SF-MPQ) and SNOMED CT.
20
+
21
+ This is the minimal semantic structure that all pain descriptions must converge to.
22
+ """
23
+
24
+ pain_type: str = Field(
25
+ description="Physical sensation description and neuropathic/nociceptive classification. "
26
+ "E.g., 'Neuropathic (Electric-shock-like, Tingling)' or 'Nociceptive (Aching, Burning)'"
27
+ )
28
+
29
+ intensity: Optional[str] = Field(
30
+ default="Not explicitly stated",
31
+ description="Pain intensity: numeric (0-10) or qualitative (Mild/Moderate/Severe). "
32
+ "Only capture if explicitly stated by patient."
33
+ )
34
+
35
+ location: str = Field(
36
+ description="Anatomical location of pain. E.g., 'Lower back', 'Both knees', 'Upper extremities'"
37
+ )
38
+
39
+ emotion: Optional[str] = Field(
40
+ default=None,
41
+ description="Affective dimension: emotional distress associated with pain. "
42
+ "E.g., 'Depressed', 'Anxious', 'Exhausting', 'Frustrated'. "
43
+ "Maps to McGill Pain Questionnaire Affective dimension."
44
+ )
45
+
46
+ temporal_pattern: str = Field(
47
+ description="Onset, frequency, and duration of pain. "
48
+ "E.g., 'Chronic (>3 months)', 'Acute (<3 months)', 'Intermittent', 'Constant'"
49
+ )
50
+
51
+ functional_impact: Optional[str] = Field(
52
+ default=None,
53
+ description="Impact on daily activities and quality of life. "
54
+ "E.g., 'Severe sleep interference', 'Unable to work', 'Limited mobility'"
55
+ )
56
+
57
+ class Config:
58
+ """Pydantic configuration."""
59
+ json_schema_extra = {
60
+ "example": {
61
+ "pain_type": "Neuropathic (Electric-shock-like, Tingling)",
62
+ "intensity": "Not explicitly stated",
63
+ "location": "Lower back to lower extremities",
64
+ "emotion": "Depressed",
65
+ "temporal_pattern": "Chronic (4 months)",
66
+ "functional_impact": "Severe sleep interference"
67
+ }
68
+ }
69
+
70
+
71
+ class ClinicalRecommendation(BaseModel):
72
+ """
73
+ Represents a single clinical recommendation triggered by the rule engine.
74
+
75
+ Each recommendation is linked to a specific clinical decision rule and includes
76
+ the evidence (structured data fields) that triggered the rule.
77
+ """
78
+
79
+ recommendation: str = Field(
80
+ description="Clinical action or pathway recommendation"
81
+ )
82
+
83
+ triggered_by_rule: str = Field(
84
+ description="Name/ID of the rule that triggered this recommendation"
85
+ )
86
+
87
+ evidence: Dict[str, Any] = Field(
88
+ description="Specific field values from PainOntology that triggered the rule"
89
+ )
90
+
91
+ confidence: str = Field(
92
+ default="high",
93
+ description="Confidence level of the recommendation: high/medium/low"
94
+ )
95
+
96
+ guideline_reference: Optional[str] = Field(
97
+ default=None,
98
+ description="Reference to clinical guideline or evidence base. "
99
+ "E.g., 'Wisconsin Medical Examining Board Guidelines for Chronic Pain Management'"
100
+ )
101
+
102
+ class Config:
103
+ """Pydantic configuration."""
104
+ json_schema_extra = {
105
+ "example": {
106
+ "recommendation": "Recommend behavioral therapy (CBT) per Wisconsin chronic pain guidelines",
107
+ "triggered_by_rule": "RULE_A: Chronic Pain + Depression",
108
+ "evidence": {
109
+ "temporal_pattern": "Chronic (4 months)",
110
+ "emotion": "Depressed"
111
+ },
112
+ "confidence": "high",
113
+ "guideline_reference": "Wisconsin Medical Examining Board Guidelines"
114
+ }
115
+ }
116
+
117
+
118
+ class ExplainableReport(BaseModel):
119
+ """
120
+ Final output containing structured data, reasoning chain, and readable report.
121
+
122
+ This is the complete output of the neuro-symbolic pain assessment pipeline,
123
+ providing full transparency from input to clinical recommendations.
124
+
125
+ Key components:
126
+ - structured_data: Normalized pain ontology
127
+ - ontology_mapping_trace: How Chinese terms were mapped to English medical terms
128
+ - clinical_recommendations: Rule-triggered recommendations with evidence
129
+ - reasoning_chain: Step-by-step reasoning from input to output
130
+ - physician_summary: Human-readable clinical summary
131
+ """
132
+
133
+ structured_data: PainOntology = Field(
134
+ description="Structured pain data following the PainOntology schema"
135
+ )
136
+
137
+ ontology_mapping_trace: List[Dict[str, Any]] = Field(
138
+ description="Trace of Chinese input → English medical term mapping. "
139
+ "Each entry shows: chinese_input, mapped_english, dimension, pain_type, SNOMED CT code, confidence"
140
+ )
141
+
142
+ clinical_recommendations: List[ClinicalRecommendation] = Field(
143
+ description="List of clinical recommendations triggered by rule engine"
144
+ )
145
+
146
+ reasoning_chain: List[str] = Field(
147
+ description="Human-readable step-by-step reasoning process showing complete decision pathway"
148
+ )
149
+
150
+ physician_summary: str = Field(
151
+ description="Natural language summary for clinical review"
152
+ )
153
+
154
+ class Config:
155
+ """Pydantic configuration."""
156
+ json_schema_extra = {
157
+ "example": {
158
+ "structured_data": {
159
+ "pain_type": "Neuropathic (Electric-shock-like, Tingling)",
160
+ "intensity": "Not explicitly stated",
161
+ "location": "Lower back to lower extremities",
162
+ "emotion": "Depressed",
163
+ "temporal_pattern": "Chronic (4 months)",
164
+ "functional_impact": "Severe sleep interference"
165
+ },
166
+ "ontology_mapping_trace": [
167
+ {
168
+ "multilingual_input": "electric-shock-like",
169
+ "mapped_english": "Electric-shock-like",
170
+ "dimension": "sensory",
171
+ "pain_type": "neuropathic",
172
+ "snomed_ct": "60924000",
173
+ "confidence": "high"
174
+ }
175
+ ],
176
+ "clinical_recommendations": [
177
+ {
178
+ "recommendation": "Recommend behavioral therapy (CBT)",
179
+ "triggered_by_rule": "RULE_A: Chronic Pain + Depression",
180
+ "evidence": {"temporal_pattern": "Chronic", "emotion": "Depressed"},
181
+ "confidence": "high"
182
+ }
183
+ ],
184
+ "reasoning_chain": [
185
+ "=== Ontology Mapping ===",
186
+ "Input 'electric-shock-like' → Mapped to 'Electric-shock-like (Neuropathic)'",
187
+ "=== Rule Engine Evaluation ===",
188
+ "✓ Triggered: RULE_A"
189
+ ],
190
+ "physician_summary": "Patient presents with chronic pain (4 months duration)..."
191
+ }
192
+ }
Backend/ontology/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cross-lingual pain ontology mapping and terminology alignment.
3
+ """
4
+ from .pain_mapping import (
5
+ CHINESE_PAIN_DESCRIPTORS,
6
+ TEMPORAL_PATTERNS,
7
+ ANATOMICAL_LOCATIONS,
8
+ map_chinese_to_english,
9
+ extract_temporal_pattern,
10
+ extract_anatomical_location
11
+ )
12
+
13
+ __all__ = [
14
+ 'CHINESE_PAIN_DESCRIPTORS',
15
+ 'TEMPORAL_PATTERNS',
16
+ 'ANATOMICAL_LOCATIONS',
17
+ 'map_chinese_to_english',
18
+ 'extract_temporal_pattern',
19
+ 'extract_anatomical_location'
20
+ ]
Backend/ontology/mcgill_translations.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ McGill Pain Questionnaire - Multilingual Translations
3
+
4
+ Standard McGill pain descriptors translated into Chinese, Korean, Spanish, and Hmong.
5
+ These translations are INDEPENDENT from the system's multilingual_pain_data.json dictionary.
6
+
7
+ **Purpose:**
8
+ Auxiliary semantic matching when system dictionary doesn't have a match.
9
+ Uses BioLORD for same-language medical semantic understanding.
10
+
11
+ **Architecture:**
12
+ Chinese patient "蚂蚁爬" → BioLORD → Chinese McGill "蚁爬感" → English "formication"
13
+ Korean patient "개미 감각" → BioLORD → Korean McGill "개미가 기어가는 느낌" → English "formication"
14
+
15
+ **McGill Standard Reference:**
16
+ Melzack, R. (1975). The McGill Pain Questionnaire: Major properties and scoring methods.
17
+ """
18
+
19
+ # Chinese McGill Pain Descriptors (中文麦吉尔疼痛问卷)
20
+ CHINESE_MCGILL = {
21
+ # Sensory - Neuropathic (神经性疼痛)
22
+ "灼烧感": {"english": "burning", "type": "neuropathic", "dimension": "sensory",
23
+ "aliases": ["火烧火燎", "灼热感", "烧灼感", "火辣辣的疼", "辣的疼"]},
24
+ "刺痛": {"english": "tingling", "type": "neuropathic", "dimension": "sensory"},
25
+ "麻木": {"english": "numbness", "type": "neuropathic", "dimension": "sensory"},
26
+ "蚁爬感": {"english": "formication", "type": "neuropathic", "dimension": "sensory",
27
+ "aliases": ["蚂蚁爬", "像蚂蚁在爬", "虫爬感"]},
28
+ "针刺感": {"english": "pins and needles", "type": "neuropathic", "dimension": "sensory",
29
+ "aliases": ["针扎感", "针刺样"]},
30
+ "电击感": {"english": "electric shock", "type": "neuropathic", "dimension": "sensory",
31
+ "aliases": ["电击样", "像电击一样", "触电感"]},
32
+ "放射痛": {"english": "shooting", "type": "neuropathic", "dimension": "sensory"},
33
+ "刀刺样痛": {"english": "stabbing", "type": "neuropathic", "dimension": "sensory"},
34
+ "锐痛": {"english": "sharp", "type": "neuropathic", "dimension": "sensory"},
35
+ "穿刺样痛": {"english": "piercing", "type": "neuropathic", "dimension": "sensory"},
36
+ "蛰刺感": {"english": "stinging", "type": "neuropathic", "dimension": "sensory",
37
+ "aliases": ["蚊虫叮咬样", "刺痒感"]},
38
+
39
+ # Sensory - Nociceptive (伤害性疼痛)
40
+ "酸痛": {"english": "aching", "type": "nociceptive", "dimension": "sensory"},
41
+ "跳痛": {"english": "throbbing", "type": "nociceptive", "dimension": "sensory",
42
+ "aliases": ["搏动性疼痛", "一跳一跳疼"]},
43
+ "捶击样痛": {"english": "pounding", "type": "nociceptive", "dimension": "sensory"},
44
+ "敲打样痛": {"english": "beating", "type": "nociceptive", "dimension": "sensory"},
45
+ "脉冲样痛": {"english": "pulsing", "type": "nociceptive", "dimension": "sensory"},
46
+ "绞痛": {"english": "cramping", "type": "nociceptive", "dimension": "sensory",
47
+ "aliases": ["痉挛性疼痛"]},
48
+ "啃咬样痛": {"english": "gnawing", "type": "nociceptive", "dimension": "sensory"},
49
+ "压榨样痛": {"english": "crushing", "type": "nociceptive", "dimension": "sensory",
50
+ "aliases": ["像被大象踩一样", "像被压碎", "像被车碾过", "压着的感觉"]},
51
+ "压迫感": {"english": "pressing", "type": "nociceptive", "dimension": "sensory",
52
+ "aliases": ["像被石头压着", "压着痛", "压痛"]},
53
+ "挤压感": {"english": "squeezing", "type": "nociceptive", "dimension": "sensory",
54
+ "aliases": ["被挤压", "紧缩感"]},
55
+ "牵拉痛": {"english": "pulling", "type": "nociceptive", "dimension": "sensory"},
56
+ "撕裂痛": {"english": "tearing", "type": "nociceptive", "dimension": "sensory"},
57
+ "裂开样痛": {"english": "splitting", "type": "nociceptive", "dimension": "sensory"},
58
+ "痛楚": {"english": "sore", "type": "nociceptive", "dimension": "sensory"},
59
+ "触痛": {"english": "tender", "type": "nociceptive", "dimension": "sensory"},
60
+ "钝痛": {"english": "dull", "type": "nociceptive", "dimension": "sensory"},
61
+ "沉重感": {"english": "heavy", "type": "nociceptive", "dimension": "sensory",
62
+ "aliases": ["重的感觉", "像被重物压着", "沉甸甸"]},
63
+
64
+ # Thermal (温度性)
65
+ "发热感": {"english": "hot", "type": "nociceptive", "dimension": "sensory",
66
+ "aliases": ["热痛", "火辣辣", "烫痛"]},
67
+ "冷痛": {"english": "cold", "type": "nociceptive", "dimension": "sensory"},
68
+ "冰冷刺痛": {"english": "freezing", "type": "nociceptive", "dimension": "sensory"},
69
+ "灼热痛": {"english": "scalding", "type": "nociceptive", "dimension": "sensory"},
70
+
71
+ # Affective (情感性)
72
+ "令人疲惫": {"english": "exhausting", "type": "affective", "dimension": "affective"},
73
+ "令人劳累": {"english": "tiring", "type": "affective", "dimension": "affective"},
74
+ "麻烦的": {"english": "troublesome", "type": "affective", "dimension": "affective"},
75
+ "悲惨的": {"english": "miserable", "type": "affective", "dimension": "affective"},
76
+ "无法忍受": {"english": "unbearable", "type": "affective", "dimension": "affective"},
77
+ "可怕的": {"english": "frightful", "type": "affective", "dimension": "affective"},
78
+ "恐怖的": {"english": "terrifying", "type": "affective", "dimension": "affective"},
79
+ "残酷的": {"english": "cruel", "type": "affective", "dimension": "affective"},
80
+ "凶恶的": {"english": "vicious", "type": "affective", "dimension": "affective"},
81
+ "折磨人": {"english": "punishing", "type": "affective", "dimension": "affective"},
82
+
83
+ # Evaluative (评价性)
84
+ "恼人的": {"english": "annoying", "type": "evaluative", "dimension": "evaluative"},
85
+ "纠缠不休": {"english": "nagging", "type": "evaluative", "dimension": "evaluative"},
86
+ "强烈的": {"english": "intense", "type": "evaluative", "dimension": "evaluative"},
87
+ }
88
+
89
+ # Korean McGill Pain Descriptors (한국어 맥길 통증 설문지)
90
+ KOREAN_MCGILL = {
91
+ # Sensory - Neuropathic
92
+ "타는 느낌": {"english": "burning", "type": "neuropathic", "dimension": "sensory"},
93
+ "따끔거림": {"english": "tingling", "type": "neuropathic", "dimension": "sensory"},
94
+ "무감각": {"english": "numbness", "type": "neuropathic", "dimension": "sensory"},
95
+ "개미가 기어가는 느낌": {"english": "formication", "type": "neuropathic", "dimension": "sensory",
96
+ "aliases": ["개미 감각", "벌레 기어가는"]},
97
+ "바늘로 찌르는": {"english": "pins and needles", "type": "neuropathic", "dimension": "sensory"},
98
+ "전기 충격": {"english": "electric shock", "type": "neuropathic", "dimension": "sensory"},
99
+ "쏘는": {"english": "shooting", "type": "neuropathic", "dimension": "sensory"},
100
+ "칼로 찌르는": {"english": "stabbing", "type": "neuropathic", "dimension": "sensory"},
101
+ "날카로운": {"english": "sharp", "type": "neuropathic", "dimension": "sensory"},
102
+ "꿰뚫는": {"english": "piercing", "type": "neuropathic", "dimension": "sensory"},
103
+ "쏘는 통증": {"english": "stinging", "type": "neuropathic", "dimension": "sensory"},
104
+
105
+ # Sensory - Nociceptive
106
+ "쑤시는": {"english": "aching", "type": "nociceptive", "dimension": "sensory"},
107
+ "욱신거리는": {"english": "throbbing", "type": "nociceptive", "dimension": "sensory"},
108
+ "두드리는": {"english": "pounding", "type": "nociceptive", "dimension": "sensory"},
109
+ "때리는": {"english": "beating", "type": "nociceptive", "dimension": "sensory"},
110
+ "맥박": {"english": "pulsing", "type": "nociceptive", "dimension": "sensory"},
111
+ "경련": {"english": "cramping", "type": "nociceptive", "dimension": "sensory"},
112
+ "갉아먹는": {"english": "gnawing", "type": "nociceptive", "dimension": "sensory"},
113
+ "으스러지는": {"english": "crushing", "type": "nociceptive", "dimension": "sensory"},
114
+ "누르는": {"english": "pressing", "type": "nociceptive", "dimension": "sensory"},
115
+ "조이는": {"english": "squeezing", "type": "nociceptive", "dimension": "sensory"},
116
+ "당기는": {"english": "pulling", "type": "nociceptive", "dimension": "sensory"},
117
+ "찢어지는": {"english": "tearing", "type": "nociceptive", "dimension": "sensory"},
118
+ "갈라지는": {"english": "splitting", "type": "nociceptive", "dimension": "sensory"},
119
+ "아픈": {"english": "sore", "type": "nociceptive", "dimension": "sensory"},
120
+ "압통": {"english": "tender", "type": "nociceptive", "dimension": "sensory"},
121
+ "둔한": {"english": "dull", "type": "nociceptive", "dimension": "sensory"},
122
+ "무거운": {"english": "heavy", "type": "nociceptive", "dimension": "sensory"},
123
+
124
+ # Thermal
125
+ "뜨거운": {"english": "hot", "type": "nociceptive", "dimension": "sensory"},
126
+ "차가운": {"english": "cold", "type": "nociceptive", "dimension": "sensory"},
127
+ "얼어붙는": {"english": "freezing", "type": "nociceptive", "dimension": "sensory"},
128
+ "데는": {"english": "scalding", "type": "nociceptive", "dimension": "sensory"},
129
+
130
+ # Affective
131
+ "지치게 하는": {"english": "exhausting", "type": "affective", "dimension": "affective"},
132
+ "피곤하게 하는": {"english": "tiring", "type": "affective", "dimension": "affective"},
133
+ "귀찮은": {"english": "troublesome", "type": "affective", "dimension": "affective"},
134
+ "비참한": {"english": "miserable", "type": "affective", "dimension": "affective"},
135
+ "참을 수 없는": {"english": "unbearable", "type": "affective", "dimension": "affective"},
136
+ "무서운": {"english": "frightful", "type": "affective", "dimension": "affective"},
137
+ "공포스러운": {"english": "terrifying", "type": "affective", "dimension": "affective"},
138
+ "잔인한": {"english": "cruel", "type": "affective", "dimension": "affective"},
139
+ "악의적인": {"english": "vicious", "type": "affective", "dimension": "affective"},
140
+ "처벌하는": {"english": "punishing", "type": "affective", "dimension": "affective"},
141
+
142
+ # Evaluative
143
+ "짜증나는": {"english": "annoying", "type": "evaluative", "dimension": "evaluative"},
144
+ "괴롭히는": {"english": "nagging", "type": "evaluative", "dimension": "evaluative"},
145
+ "강렬한": {"english": "intense", "type": "evaluative", "dimension": "evaluative"},
146
+ }
147
+
148
+ # Spanish McGill Pain Descriptors (Cuestionario de Dolor McGill en Español)
149
+ SPANISH_MCGILL = {
150
+ # Sensory - Neuropathic
151
+ "ardiente": {"english": "burning", "type": "neuropathic", "dimension": "sensory",
152
+ "aliases": ["quemante", "que arde"]},
153
+ "hormigueo": {"english": "tingling", "type": "neuropathic", "dimension": "sensory"},
154
+ "entumecimiento": {"english": "numbness", "type": "neuropathic", "dimension": "sensory",
155
+ "aliases": ["adormecimiento"]},
156
+ "sensación de hormigas": {"english": "formication", "type": "neuropathic", "dimension": "sensory",
157
+ "aliases": ["como hormigas caminando", "hormigueo intenso"]},
158
+ "alfileres y agujas": {"english": "pins and needles", "type": "neuropathic", "dimension": "sensory"},
159
+ "choque eléctrico": {"english": "electric shock", "type": "neuropathic", "dimension": "sensory",
160
+ "aliases": ["descarga eléctrica"]},
161
+ "punzante": {"english": "shooting", "type": "neuropathic", "dimension": "sensory"},
162
+ "apuñalante": {"english": "stabbing", "type": "neuropathic", "dimension": "sensory"},
163
+ "agudo": {"english": "sharp", "type": "neuropathic", "dimension": "sensory"},
164
+ "perforante": {"english": "piercing", "type": "neuropathic", "dimension": "sensory"},
165
+ "punzada": {"english": "stinging", "type": "neuropathic", "dimension": "sensory"},
166
+
167
+ # Sensory - Nociceptive
168
+ "dolor sordo": {"english": "aching", "type": "nociceptive", "dimension": "sensory"},
169
+ "pulsátil": {"english": "throbbing", "type": "nociceptive", "dimension": "sensory",
170
+ "aliases": ["latiendo", "palpitante"]},
171
+ "martilleante": {"english": "pounding", "type": "nociceptive", "dimension": "sensory"},
172
+ "golpeante": {"english": "beating", "type": "nociceptive", "dimension": "sensory"},
173
+ "pulsante": {"english": "pulsing", "type": "nociceptive", "dimension": "sensory"},
174
+ "calambre": {"english": "cramping", "type": "nociceptive", "dimension": "sensory"},
175
+ "roedor": {"english": "gnawing", "type": "nociceptive", "dimension": "sensory"},
176
+ "aplastante": {"english": "crushing", "type": "nociceptive", "dimension": "sensory"},
177
+ "presión": {"english": "pressing", "type": "nociceptive", "dimension": "sensory"},
178
+ "apretante": {"english": "squeezing", "type": "nociceptive", "dimension": "sensory"},
179
+ "tirante": {"english": "pulling", "type": "nociceptive", "dimension": "sensory"},
180
+ "desgarrante": {"english": "tearing", "type": "nociceptive", "dimension": "sensory"},
181
+ "dividiendo": {"english": "splitting", "type": "nociceptive", "dimension": "sensory"},
182
+ "adolorido": {"english": "sore", "type": "nociceptive", "dimension": "sensory"},
183
+ "sensible": {"english": "tender", "type": "nociceptive", "dimension": "sensory"},
184
+ "sordo": {"english": "dull", "type": "nociceptive", "dimension": "sensory"},
185
+ "pesado": {"english": "heavy", "type": "nociceptive", "dimension": "sensory"},
186
+
187
+ # Thermal
188
+ "caliente": {"english": "hot", "type": "nociceptive", "dimension": "sensory"},
189
+ "frío": {"english": "cold", "type": "nociceptive", "dimension": "sensory"},
190
+ "congelante": {"english": "freezing", "type": "nociceptive", "dimension": "sensory"},
191
+ "escaldante": {"english": "scalding", "type": "nociceptive", "dimension": "sensory"},
192
+
193
+ # Affective
194
+ "agotador": {"english": "exhausting", "type": "affective", "dimension": "affective"},
195
+ "cansador": {"english": "tiring", "type": "affective", "dimension": "affective"},
196
+ "problemático": {"english": "troublesome", "type": "affective", "dimension": "affective"},
197
+ "miserable": {"english": "miserable", "type": "affective", "dimension": "affective"},
198
+ "insoportable": {"english": "unbearable", "type": "affective", "dimension": "affective"},
199
+ "espantoso": {"english": "frightful", "type": "affective", "dimension": "affective"},
200
+ "aterrador": {"english": "terrifying", "type": "affective", "dimension": "affective"},
201
+ "cruel": {"english": "cruel", "type": "affective", "dimension": "affective"},
202
+ "vicioso": {"english": "vicious", "type": "affective", "dimension": "affective"},
203
+ "castigador": {"english": "punishing", "type": "affective", "dimension": "affective"},
204
+
205
+ # Evaluative
206
+ "molesto": {"english": "annoying", "type": "evaluative", "dimension": "evaluative"},
207
+ "persistente": {"english": "nagging", "type": "evaluative", "dimension": "evaluative"},
208
+ "intenso": {"english": "intense", "type": "evaluative", "dimension": "evaluative"},
209
+ }
210
+
211
+ # Hmong McGill Pain Descriptors (Hmong McGill Mob Nug)
212
+ HMONG_MCGILL = {
213
+ # Sensory - Neuropathic
214
+ "kub hnyiab": {"english": "burning", "type": "neuropathic", "dimension": "sensory"},
215
+ "tub nkeeg": {"english": "tingling", "type": "neuropathic", "dimension": "sensory"},
216
+ "loog": {"english": "numbness", "type": "neuropathic", "dimension": "sensory"},
217
+ "zoo li ntsaum nkag": {"english": "formication", "type": "neuropathic", "dimension": "sensory",
218
+ "aliases": ["zoo li kab nkag", "ntsaum taug kev"]},
219
+ "koob thiab tus pin": {"english": "pins and needles", "type": "neuropathic", "dimension": "sensory"},
220
+ "mob hluav taw xob": {"english": "electric shock", "type": "neuropathic", "dimension": "sensory"},
221
+ "tua": {"english": "shooting", "type": "neuropathic", "dimension": "sensory"},
222
+ "ntaus ntaj": {"english": "stabbing", "type": "neuropathic", "dimension": "sensory"},
223
+ "ntse": {"english": "sharp", "type": "neuropathic", "dimension": "sensory"},
224
+ "piercing": {"english": "piercing", "type": "neuropathic", "dimension": "sensory"},
225
+ "tom": {"english": "stinging", "type": "neuropathic", "dimension": "sensory"},
226
+
227
+ # Sensory - Nociceptive
228
+ "mob": {"english": "aching", "type": "nociceptive", "dimension": "sensory"},
229
+ "dhia": {"english": "throbbing", "type": "nociceptive", "dimension": "sensory"},
230
+ "ntaus": {"english": "pounding", "type": "nociceptive", "dimension": "sensory"},
231
+ "ntaus": {"english": "beating", "type": "nociceptive", "dimension": "sensory"},
232
+ "pulsing": {"english": "pulsing", "type": "nociceptive", "dimension": "sensory"},
233
+ "cramping": {"english": "cramping", "type": "nociceptive", "dimension": "sensory"},
234
+ "zom": {"english": "gnawing", "type": "nociceptive", "dimension": "sensory"},
235
+ "tsoo": {"english": "crushing", "type": "nociceptive", "dimension": "sensory"},
236
+ "nias": {"english": "pressing", "type": "nociceptive", "dimension": "sensory"},
237
+ "nyem": {"english": "squeezing", "type": "nociceptive", "dimension": "sensory"},
238
+ "rub": {"english": "pulling", "type": "nociceptive", "dimension": "sensory"},
239
+ "tsuas": {"english": "tearing", "type": "nociceptive", "dimension": "sensory"},
240
+ "sib cais": {"english": "splitting", "type": "nociceptive", "dimension": "sensory"},
241
+ "mob": {"english": "sore", "type": "nociceptive", "dimension": "sensory"},
242
+ "rhiab": {"english": "tender", "type": "nociceptive", "dimension": "sensory"},
243
+ "dull": {"english": "dull", "type": "nociceptive", "dimension": "sensory"},
244
+ "hnyav": {"english": "heavy", "type": "nociceptive", "dimension": "sensory"},
245
+
246
+ # Thermal
247
+ "kub": {"english": "hot", "type": "nociceptive", "dimension": "sensory"},
248
+ "txias": {"english": "cold", "type": "nociceptive", "dimension": "sensory"},
249
+ "khov": {"english": "freezing", "type": "nociceptive", "dimension": "sensory"},
250
+ "scalding": {"english": "scalding", "type": "nociceptive", "dimension": "sensory"},
251
+
252
+ # Affective
253
+ "nkees": {"english": "exhausting", "type": "affective", "dimension": "affective"},
254
+ "nkees": {"english": "tiring", "type": "affective", "dimension": "affective"},
255
+ "teeb meem": {"english": "troublesome", "type": "affective", "dimension": "affective"},
256
+ "tu siab": {"english": "miserable", "type": "affective", "dimension": "affective"},
257
+ "tsis tau": {"english": "unbearable", "type": "affective", "dimension": "affective"},
258
+ "ntshai": {"english": "frightful", "type": "affective", "dimension": "affective"},
259
+ "txaus ntshai": {"english": "terrifying", "type": "affective", "dimension": "affective"},
260
+ "siab phem": {"english": "cruel", "type": "affective", "dimension": "affective"},
261
+ "phem": {"english": "vicious", "type": "affective", "dimension": "affective"},
262
+ "rau txim": {"english": "punishing", "type": "affective", "dimension": "affective"},
263
+
264
+ # Evaluative
265
+ "ntxhov siab": {"english": "annoying", "type": "evaluative", "dimension": "evaluative"},
266
+ "nagging": {"english": "nagging", "type": "evaluative", "dimension": "evaluative"},
267
+ "muaj zog": {"english": "intense", "type": "evaluative", "dimension": "evaluative"},
268
+ }
Backend/ontology/pain_mapping.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chinese-English pain ontology mapping dictionary.
3
+
4
+ Maps culturally-specific Chinese pain descriptors to standardized English medical terminology
5
+ aligned with McGill Pain Questionnaire (SF-MPQ) and SNOMED CT.
6
+
7
+ This module provides deterministic, dictionary-based mapping to ensure medical accuracy
8
+ and cross-cultural semantic alignment.
9
+ """
10
+
11
+ from typing import List, Dict, Optional, Any
12
+
13
+
14
+ # Chinese pain descriptor mappings
15
+ # Each entry maps a Chinese term to its English medical equivalent with metadata
16
+ CHINESE_PAIN_DESCRIPTORS = {
17
+ # ========== Neuropathic Pain Descriptors ==========
18
+ "针扎": {
19
+ "english": "Pricking",
20
+ "aliases": ["像针扎一样", "针刺", "如针扎", "针扎样"],
21
+ "dimension": "sensory",
22
+ "pain_type": "neuropathic",
23
+ "mcgill_category": "Sensory",
24
+ "description": "Sharp, needle-like pain sensation typical of nerve irritation"
25
+ },
26
+
27
+ "触电": {
28
+ "english": "Electric-shock-like",
29
+ "aliases": ["像触电一样", "电击", "过电", "触电样", "电击样"],
30
+ "dimension": "sensory",
31
+ "pain_type": "neuropathic",
32
+ "mcgill_category": "Sensory",
33
+ "description": "Sharp, sudden, shocking nerve pain resembling electric shock"
34
+ },
35
+
36
+ "麻": {
37
+ "english": "Tingling",
38
+ "aliases": ["发麻", "麻木", "麻刺", "又麻又痛", "麻痹"],
39
+ "dimension": "sensory",
40
+ "pain_type": "neuropathic",
41
+ "mcgill_category": "Sensory",
42
+ "description": "Tingling, numbness, or pins-and-needles sensation"
43
+ },
44
+
45
+ "刺": {
46
+ "english": "Stabbing",
47
+ "aliases": ["刺痛", "刀刺", "刺骨", "像刀刺一样"],
48
+ "dimension": "sensory",
49
+ "pain_type": "neuropathic",
50
+ "mcgill_category": "Sensory",
51
+ "description": "Sharp, stabbing pain sensation"
52
+ },
53
+
54
+ "蚂蚁": {
55
+ "english": "Formication (crawling sensation)",
56
+ "aliases": ["蚂蚁在咬", "蚂蚁爬", "虫子爬", "有东西在爬"],
57
+ "dimension": "sensory",
58
+ "pain_type": "neuropathic",
59
+ "mcgill_category": "Sensory",
60
+ "description": "Crawling, tingling sensation like insects on skin"
61
+ },
62
+
63
+ # ========== Nociceptive Pain Descriptors ==========
64
+ "火辣辣": {
65
+ "english": "Burning",
66
+ "aliases": ["烧灼", "灼烧", "火烧", "灼热", "火辣"],
67
+ "dimension": "sensory",
68
+ "pain_type": "nociceptive",
69
+ "mcgill_category": "Sensory",
70
+ "description": "Hot, burning sensation"
71
+ },
72
+
73
+ "隐痛": {
74
+ "english": "Aching",
75
+ "aliases": ["隐隐作痛", "隐隐的痛", "隐约的痛"],
76
+ "dimension": "sensory",
77
+ "pain_type": "nociceptive",
78
+ "mcgill_category": "Sensory",
79
+ "description": "Dull, continuous aching pain"
80
+ },
81
+
82
+ "酸痛": {
83
+ "english": "Sore",
84
+ "aliases": ["酸", "发酸", "又酸又痛"],
85
+ "dimension": "sensory",
86
+ "pain_type": "nociceptive",
87
+ "mcgill_category": "Sensory",
88
+ "description": "Sore, achy muscle pain"
89
+ },
90
+
91
+ "胀": {
92
+ "english": "Distended",
93
+ "aliases": ["胀痛", "又酸又胀", "发胀", "膨胀"],
94
+ "dimension": "sensory",
95
+ "pain_type": "nociceptive",
96
+ "mcgill_category": "Sensory",
97
+ "description": "Distending, swelling pain sensation"
98
+ },
99
+
100
+ "跳": {
101
+ "english": "Throbbing",
102
+ "aliases": ["跳痛", "一跳一跳的", "像心跳一样"],
103
+ "dimension": "sensory",
104
+ "pain_type": "nociceptive",
105
+ "mcgill_category": "Sensory",
106
+ "description": "Pulsating, throbbing pain"
107
+ },
108
+
109
+ "钝痛": {
110
+ "english": "Dull",
111
+ "aliases": ["钝钝的", "不尖锐"],
112
+ "dimension": "sensory",
113
+ "pain_type": "nociceptive",
114
+ "mcgill_category": "Sensory",
115
+ "description": "Dull, non-sharp pain"
116
+ },
117
+
118
+
119
+ # ========== Affective (Emotional) Descriptors ==========
120
+ "郁闷": {
121
+ "english": "Depressed",
122
+ "aliases": ["抑郁", "心情不好", "情绪低落", "沮丧", "低落"],
123
+ "dimension": "affective",
124
+ "mcgill_category": "Affective",
125
+ "description": "Emotional distress and depressive symptoms associated with pain"
126
+ },
127
+
128
+ "烦躁": {
129
+ "english": "Anxious",
130
+ "aliases": ["焦虑", "心烦", "烦", "不安", "烦恼"],
131
+ "dimension": "affective",
132
+ "mcgill_category": "Affective",
133
+ "description": "Anxiety, irritability, and restlessness"
134
+ },
135
+
136
+ "累": {
137
+ "english": "Exhausting",
138
+ "aliases": ["疲惫", "累得慌", "精疲力竭", "疲劳", "乏力"],
139
+ "dimension": "affective",
140
+ "mcgill_category": "Affective",
141
+ "description": "Physical and emotional exhaustion from persistent pain"
142
+ },
143
+
144
+ "难受": {
145
+ "english": "Distressing",
146
+ "aliases": ["痛苦", "受罪", "���磨"],
147
+ "dimension": "affective",
148
+ "mcgill_category": "Affective",
149
+ "description": "Overall distress and suffering"
150
+ },
151
+
152
+ "绝望": {
153
+ "english": "Hopeless",
154
+ "aliases": ["没希望", "无望"],
155
+ "dimension": "affective",
156
+ "mcgill_category": "Affective",
157
+ "description": "Sense of hopelessness and despair"
158
+ }
159
+
160
+
161
+ }
162
+
163
+
164
+ # Temporal pattern mappings
165
+ TEMPORAL_PATTERNS = {
166
+ "几个月": "Chronic (>3 months)",
167
+ "好几个月": "Chronic (>3 months)",
168
+ "很久": "Chronic (>3 months)",
169
+ "长期": "Chronic (>3 months)",
170
+ "一直": "Constant",
171
+ "总是": "Constant",
172
+ "经常": "Frequent",
173
+ "偶尔": "Intermittent",
174
+ "时不时": "Intermittent",
175
+ "有时候": "Intermittent",
176
+ "突然": "Acute onset",
177
+ "最近": "Recent onset",
178
+ "反复": "Recurring",
179
+ "每天": "Daily",
180
+ "晚上": "Nocturnal"
181
+ }
182
+
183
+
184
+ # Anatomical location mappings
185
+ ANATOMICAL_LOCATIONS = {
186
+ "腰": "Lower back",
187
+ "后腰": "Lower back",
188
+ "腰部": "Lower back",
189
+ "腿": "Lower extremities",
190
+ "下肢": "Lower extremities",
191
+ "大腿": "Thighs",
192
+ "小腿": "Legs",
193
+ "膝盖": "Knees",
194
+ "膝": "Knees",
195
+ "手": "Hands",
196
+ "上肢": "Upper extremities",
197
+ "脚": "Feet",
198
+ "足": "Feet",
199
+ "头": "Head",
200
+ "颈": "Neck",
201
+ "脖子": "Neck",
202
+ "肩": "Shoulders",
203
+ "背": "Back",
204
+ "胸": "Chest",
205
+ "腹": "Abdomen",
206
+ "肚子": "Abdomen",
207
+ "浑身": "Whole body"
208
+
209
+ }
210
+
211
+
212
+ def map_chinese_to_english(chinese_text: str) -> List[Dict[str, Any]]:
213
+ """
214
+ Map Chinese pain descriptors to standardized English medical terminology.
215
+
216
+ Uses dictionary-based exact and fuzzy matching to identify pain descriptors
217
+ in patient text and map them to McGill Pain Questionnaire dimensions and
218
+ SNOMED CT codes.
219
+
220
+ Args:
221
+ chinese_text: Raw Chinese patient description
222
+
223
+ Returns:
224
+ List of mappings, each containing:
225
+ - chinese_input: The Chinese term found in text
226
+ - mapped_english: Standardized English medical term
227
+ - dimension: sensory/affective
228
+ - pain_type: neuropathic/nociceptive (if applicable)
229
+ - snomed_ct: SNOMED CT code (if applicable)
230
+ - confidence: Mapping confidence (high/medium/low)
231
+
232
+ Example:
233
+ >>> mappings = map_chinese_to_english("腰部像触电一样的麻痛,很郁闷")
234
+ >>> # Returns mappings for "触电" → "Electric-shock-like" and "郁闷" → "Depressed"
235
+ """
236
+ mappings = []
237
+
238
+ # Iterate through all defined pain descriptors
239
+ for chinese_term, term_data in CHINESE_PAIN_DESCRIPTORS.items():
240
+ # Check if main term appears in text
241
+ if chinese_term in chinese_text:
242
+ mappings.append({
243
+ "chinese_input": chinese_term,
244
+ "mapped_english": term_data["english"],
245
+ "dimension": term_data["dimension"],
246
+ "pain_type": term_data.get("pain_type"),
247
+ "confidence": "high",
248
+ "mcgill_category": term_data.get("mcgill_category")
249
+ })
250
+ else:
251
+ # Check aliases for fuzzy matching
252
+ for alias in term_data.get("aliases", []):
253
+ if alias in chinese_text:
254
+ mappings.append({
255
+ "chinese_input": alias,
256
+ "mapped_english": term_data["english"],
257
+ "dimension": term_data["dimension"],
258
+ "pain_type": term_data.get("pain_type"),
259
+ "confidence": "high",
260
+ "mcgill_category": term_data.get("mcgill_category")
261
+ })
262
+ break # Only match once per term to avoid duplicates
263
+
264
+ return mappings
265
+
266
+
267
+ def extract_temporal_pattern(chinese_text: str) -> Optional[str]:
268
+ """
269
+ Extract and standardize temporal pattern from Chinese text.
270
+
271
+ Identifies duration, frequency, and onset patterns and maps them to
272
+ standardized clinical terminology.
273
+
274
+ Args:
275
+ chinese_text: Raw Chinese patient description
276
+
277
+ Returns:
278
+ Standardized temporal pattern string or None if not found
279
+
280
+ Example:
281
+ >>> extract_temporal_pattern("已经好几个月了")
282
+ 'Chronic (>3 months)'
283
+ """
284
+ # Check for duration indicators (prioritize more specific patterns)
285
+ # Look for "X个月" pattern first
286
+ import re
287
+
288
+ # Extract numeric duration
289
+ month_match = re.search(r'(\d+)\s*个月', chinese_text)
290
+ if month_match:
291
+ months = int(month_match.group(1))
292
+ if months >= 3:
293
+ return f"Chronic ({months} months)"
294
+ else:
295
+ return f"Acute (<3 months, {months} months)"
296
+
297
+ # Check predefined temporal patterns
298
+ for chinese_pattern, english_pattern in TEMPORAL_PATTERNS.items():
299
+ if chinese_pattern in chinese_text:
300
+ return english_pattern
301
+
302
+ return None
303
+
304
+
305
+ def extract_anatomical_location(chinese_text: str) -> List[str]:
306
+ """
307
+ Extract anatomical locations from Chinese text.
308
+
309
+ Identifies body parts mentioned in patient description and maps them
310
+ to standardized anatomical terminology.
311
+
312
+ Args:
313
+ chinese_text: Raw Chinese patient description
314
+
315
+ Returns:
316
+ List of standardized anatomical location strings
317
+
318
+ Example:
319
+ >>> extract_anatomical_location("腰部到腿部都痛")
320
+ ['Lower back', 'Lower extremities']
321
+ """
322
+ locations = []
323
+
324
+ for chinese_loc, english_loc in ANATOMICAL_LOCATIONS.items():
325
+ if chinese_loc in chinese_text:
326
+ if english_loc not in locations: # Avoid duplicates
327
+ locations.append(english_loc)
328
+
329
+ return locations
330
+
331
+
332
+ def get_unmapped_descriptors(chinese_text: str, mappings: List[Dict]) -> List[str]:
333
+ """
334
+ Identify pain-related words in text that were not mapped to standard terminology.
335
+
336
+ This helps track coverage gaps in the mapping dictionary and identify
337
+ culturally-specific terms that may need to be added.
338
+
339
+ Args:
340
+ chinese_text: Raw Chinese patient description
341
+ mappings: List of mappings returned by map_chinese_to_english()
342
+
343
+ Returns:
344
+ List of Chinese pain-related terms that were not mapped
345
+
346
+ Note:
347
+ This is a simple heuristic-based approach. For production use,
348
+ consider using NER or more sophisticated linguistic analysis.
349
+ """
350
+ # Common pain-related indicator words in Chinese
351
+ pain_indicators = ["痛", "疼", "酸", "麻", "胀", "难受", "不舒服"]
352
+
353
+ unmapped = []
354
+ # Support both old format (chinese_input) and new format (original_term)
355
+ mapped_terms = {m.get("chinese_input") or m.get("original_term") for m in mappings}
356
+
357
+ # Simple heuristic: look for pain indicator characters
358
+ for indicator in pain_indicators:
359
+ if indicator in chinese_text and indicator not in mapped_terms:
360
+ # Extract context around the indicator (basic approach)
361
+ import re
362
+ pattern = f".{{0,3}}{indicator}.{{0,3}}"
363
+ matches = re.findall(pattern, chinese_text)
364
+ for match in matches:
365
+ if match not in mapped_terms and match not in unmapped:
366
+ unmapped.append(match)
367
+
368
+ return unmapped
369
+
370
+
371
+ def suggest_similar_terms(unmapped_term: str, language: str = "zh") -> List[Dict[str, Any]]:
372
+ """
373
+ Suggest similar pain descriptors from the dictionary for unmapped terms.
374
+
375
+ This function provides NON-DEFINITIVE suggestions for terms not found in the dictionary.
376
+ It helps clinicians understand what the patient MIGHT be trying to express,
377
+ but DOES NOT make automatic mappings.
378
+
379
+ Args:
380
+ unmapped_term: Pain descriptor not found in dictionary
381
+ language: Language code (currently supports 'zh' for Chinese)
382
+
383
+ Returns:
384
+ List of dictionaries containing similar terms and their properties:
385
+ [
386
+ {
387
+ "dictionary_term": "刺痛",
388
+ "english": "Stabbing",
389
+ "similarity_reason": "shares character '痛'",
390
+ "confidence": "low", # Always low for suggestions
391
+ "note": "⚠️ Suggestion only - clinical review required"
392
+ }
393
+ ]
394
+
395
+ Example:
396
+ >>> suggest_similar_terms("猛痛", "zh")
397
+ [
398
+ {
399
+ "dictionary_term": "刺痛",
400
+ "english": "Stabbing",
401
+ "similarity_reason": "shares '痛' character, both indicate sharp pain",
402
+ "confidence": "low",
403
+ "note": "⚠️ This is a suggestion based on character similarity. The patient's exact term '猛痛' should be noted for clinical context."
404
+ }
405
+ ]
406
+ """
407
+ if language != "zh":
408
+ return [] # Only support Chinese for now
409
+
410
+ suggestions = []
411
+
412
+ # Extract key characters from unmapped term
413
+ pain_chars = set()
414
+ for char in ["痛", "疼", "酸", "麻", "胀", "刺", "钝", "跳", "抽", "紧", "沉", "胀"]:
415
+ if char in unmapped_term:
416
+ pain_chars.add(char)
417
+
418
+ if not pain_chars:
419
+ return []
420
+
421
+ # Search dictionary for terms sharing similar characters
422
+ for dict_term, metadata in CHINESE_PAIN_DESCRIPTORS.items():
423
+ shared_chars = pain_chars & set(dict_term)
424
+
425
+ if shared_chars:
426
+ similarity_reason = f"shares character(s): {', '.join(shared_chars)}"
427
+
428
+ # Check if both terms share semantic elements
429
+ if len(unmapped_term) > 1 and len(dict_term) > 1:
430
+ # Check for substring match
431
+ if unmapped_term in dict_term or dict_term in unmapped_term:
432
+ similarity_reason += " (possible variant or related form)"
433
+
434
+ suggestions.append({
435
+ "dictionary_term": dict_term,
436
+ "english": metadata["english"],
437
+ "pain_type": metadata.get("pain_type", "unknown"),
438
+ "dimension": metadata.get("dimension", "unknown"),
439
+ "similarity_reason": similarity_reason,
440
+ "confidence": "low", # Always low - this is just a suggestion
441
+ "note": f"⚠️ Suggestion only. Patient used '{unmapped_term}' which is not in our dictionary. "
442
+ f"'{dict_term}' is similar but may not match patient's intent. Clinical review recommended."
443
+ })
444
+
445
+ # Sort by number of shared characters (most similar first)
446
+ suggestions.sort(key=lambda x: len([c for c in pain_chars if c in x["dictionary_term"]]), reverse=True)
447
+
448
+ # Return top 3 suggestions at most
449
+ return suggestions[:3]
Backend/ontology/pain_mapping_multilingual.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multilingual pain ontology mapping dictionary.
3
+
4
+ Extends the Chinese-English mapping to support Korean, Spanish, and Hmong languages.
5
+ Maps culturally-specific pain descriptors to standardized English medical terminology
6
+ aligned with McGill Pain Questionnaire (SF-MPQ) and SNOMED CT.
7
+
8
+ Supported Languages:
9
+ - Chinese (Simplified)
10
+ - Korean (Hangul)
11
+ - Spanish (Castilian)
12
+ - Hmong
13
+
14
+ This module provides deterministic, dictionary-based mapping to ensure medical accuracy
15
+ and cross-cultural semantic alignment.
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import sys
21
+ from typing import List, Dict, Optional, Any
22
+
23
+ # Add parent directory to path for imports
24
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
25
+
26
+ from utils.language_detector import detect_language, LanguageCode
27
+
28
+ # Load multilingual pain descriptors from JSON
29
+ def _load_pain_descriptors():
30
+ """Load pain descriptors from JSON file"""
31
+ current_dir = os.path.dirname(os.path.abspath(__file__))
32
+ json_path = os.path.join(current_dir, '..', 'scripts', 'multilingual_pain_data.json')
33
+
34
+ with open(json_path, 'r', encoding='utf-8') as f:
35
+ return json.load(f)
36
+
37
+
38
+ def _extract_core_terms(term: str, language: str) -> List[str]:
39
+ """
40
+ Extract core pain descriptor terms to enable fuzzy matching across all languages.
41
+
42
+ Handles linguistic variations:
43
+ - Chinese: Removes suffixes like 'de-tong', 'de-teng', 'de-ganjue'
44
+ - Korean: Removes verb endings like 'hada', 'apeuda'
45
+ - Spanish: Extracts root words, handles verb forms
46
+ - Hmong: Splits compound words
47
+
48
+ Args:
49
+ term: Original term from ontology dictionary
50
+ language: Language code ('zh', 'ko', 'es', 'hmong', 'en')
51
+
52
+ Returns:
53
+ List of core term variants (including original)
54
+
55
+ Example:
56
+ >>> _extract_core_terms("yi-chou-yi-chou-de-tong", "zh")
57
+ ['yi-chou-yi-chou', 'yi-chou-yi-chou-de-tong']
58
+ >>> _extract_core_terms("punzante", "es")
59
+ ['punzante', 'punz'] # root form
60
+ """
61
+ cores = []
62
+
63
+ if language == 'zh': # Chinese
64
+ # Remove common pain-related suffixes to extract core descriptor
65
+ suffixes = ['的痛', '的疼', '的感觉', '痛', '疼', '的']
66
+ core = term
67
+
68
+ for suffix in suffixes:
69
+ if core.endswith(suffix) and len(core) > len(suffix):
70
+ core = core[:-len(suffix)]
71
+ cores.append(core)
72
+ break
73
+
74
+ cores.append(term) # Also keep original term for exact matching
75
+
76
+ elif language == 'ko': # Korean
77
+ # Handle Korean verb conjugations and descriptive forms
78
+ if term.endswith('하다') and len(term) > 2:
79
+ cores.append(term[:-2])
80
+ elif term.endswith('아프다') and len(term) > 3:
81
+ cores.append(term[:-3])
82
+ elif term.endswith('듯') and len(term) > 2:
83
+ cores.append(term[:-2])
84
+ cores.append(term)
85
+
86
+ elif language == 'es': # Spanish
87
+ # Handle common Spanish suffixes and verb forms
88
+ # Examples: "punzante" → "punz", "ardiente" → "ardi"
89
+ spanish_suffixes = ['ante', 'ente', 'ción', 'miento', 'oso', 'osa']
90
+ core = term.lower()
91
+
92
+ for suffix in spanish_suffixes:
93
+ if core.endswith(suffix) and len(core) > len(suffix) + 3:
94
+ cores.append(core[:-len(suffix)])
95
+ break
96
+
97
+ # Also try matching first 4+ characters for root
98
+ if len(term) >= 4:
99
+ cores.append(term[:4])
100
+
101
+ cores.append(term)
102
+
103
+ elif language == 'hmong': # Hmong
104
+ # Hmong often uses compound words separated by spaces
105
+ # Split and try individual words too
106
+ if ' ' in term:
107
+ words = term.split()
108
+ cores.extend(words) # Add individual words
109
+ cores.append(term) # Keep full phrase
110
+
111
+ else: # English - keep as is
112
+ cores.append(term)
113
+
114
+ return list(set(cores)) # Remove duplicates
115
+
116
+ # Load data at module level
117
+ _MULTILINGUAL_DATA = _load_pain_descriptors()
118
+
119
+ # Flatten the multilingual data for easier access
120
+ # Structure: {language: {term: {english, dimension, pain_type, ...}}}
121
+ KOREAN_PAIN_DESCRIPTORS = {}
122
+ for category in ['neuropathic', 'nociceptive', 'affective']:
123
+ for term, data in _MULTILINGUAL_DATA['korean'].get(category, {}).items():
124
+ KOREAN_PAIN_DESCRIPTORS[term] = {
125
+ **data,
126
+ 'pain_type': category if category != 'affective' else None,
127
+ 'dimension': 'affective' if category == 'affective' else 'sensory'
128
+ }
129
+
130
+ SPANISH_PAIN_DESCRIPTORS = {}
131
+ for category in ['neuropathic', 'nociceptive', 'affective']:
132
+ for term, data in _MULTILINGUAL_DATA['spanish'].get(category, {}).items():
133
+ SPANISH_PAIN_DESCRIPTORS[term] = {
134
+ **data,
135
+ 'pain_type': category if category != 'affective' else None,
136
+ 'dimension': 'affective' if category == 'affective' else 'sensory'
137
+ }
138
+
139
+ HMONG_PAIN_DESCRIPTORS = {}
140
+ for category in ['neuropathic', 'nociceptive', 'affective']:
141
+ for term, data in _MULTILINGUAL_DATA['hmong'].get(category, {}).items():
142
+ HMONG_PAIN_DESCRIPTORS[term] = {
143
+ **data,
144
+ 'pain_type': category if category != 'affective' else None,
145
+ 'dimension': 'affective' if category == 'affective' else 'sensory'
146
+ }
147
+
148
+ CHINESE_PAIN_DESCRIPTORS = {}
149
+ for category in ['neuropathic', 'nociceptive', 'affective']:
150
+ for term, data in _MULTILINGUAL_DATA['chinese'].get(category, {}).items():
151
+ CHINESE_PAIN_DESCRIPTORS[term] = {
152
+ **data,
153
+ 'pain_type': category if category != 'affective' else None,
154
+ 'dimension': 'affective' if category == 'affective' else 'sensory'
155
+ }
156
+
157
+ # Map language codes to descriptor dictionaries
158
+ LANGUAGE_DESCRIPTORS = {
159
+ 'zh': CHINESE_PAIN_DESCRIPTORS,
160
+ 'ko': KOREAN_PAIN_DESCRIPTORS,
161
+ 'es': SPANISH_PAIN_DESCRIPTORS,
162
+ 'hmong': HMONG_PAIN_DESCRIPTORS,
163
+ 'en': {} # English input doesn't need translation
164
+ }
165
+
166
+
167
+ def map_multilingual_to_english(
168
+ text: str,
169
+ language: Optional[LanguageCode] = None
170
+ ) -> List[Dict[str, Any]]:
171
+ """
172
+ Map multilingual pain descriptors to standardized English medical terminology.
173
+
174
+ Supports Chinese, Korean, Spanish, Hmong, and English input.
175
+ Uses dictionary-based exact matching to identify pain descriptors
176
+ and map them to McGill Pain Questionnaire dimensions.
177
+
178
+ Args:
179
+ text: Raw patient description in any supported language
180
+ language: Language code ('zh', 'ko', 'es', 'hmong', 'en').
181
+ If None, will auto-detect from text.
182
+
183
+ Returns:
184
+ List of mappings, each containing:
185
+ - original_term: The term found in text
186
+ - mapped_english: Standardized English medical term
187
+ - dimension: sensory/affective
188
+ - pain_type: neuropathic/nociceptive (if applicable)
189
+ - confidence: Mapping confidence (high/medium/low)
190
+ - detected_language: The detected or specified language
191
+
192
+ Example:
193
+ >>> # Chinese input
194
+ >>> mappings = map_multilingual_to_english("I have burning pain")
195
+ >>> # Returns mapping for "burning pain" → "burning"
196
+
197
+ >>> # Korean input
198
+ >>> mappings = map_multilingual_to_english("허리가 따끔거리듯이 아프다")
199
+ >>> # Returns mapping for "따끔거리다" → "sting"
200
+ """
201
+ # Auto-detect language if not specified
202
+ if language is None:
203
+ language = detect_language(text)
204
+
205
+ # Get appropriate descriptor dictionary
206
+ descriptors = LANGUAGE_DESCRIPTORS.get(language, {})
207
+
208
+ if not descriptors:
209
+ # If language not supported or English input
210
+ return [{
211
+ "original_term": text,
212
+ "mapped_english": text, # Pass through for English
213
+ "dimension": "unknown",
214
+ "pain_type": None,
215
+ "confidence": "low",
216
+ "detected_language": language
217
+ }]
218
+
219
+ mappings = []
220
+
221
+ # Iterate through all defined pain descriptors for this language
222
+ for term, term_data in descriptors.items():
223
+ # Extract core terms for fuzzy matching
224
+ core_terms = _extract_core_terms(term, language)
225
+
226
+ # Try to match any core term variant
227
+ matched_core = None
228
+ for core in core_terms:
229
+ # For Chinese, allow single character matches (麻, 疼, 痛, 酸, etc.)
230
+ # For other languages, require at least 2 characters to avoid false positives
231
+ min_length = 1 if language == 'zh' else 2
232
+ if core in text and len(core) >= min_length:
233
+ matched_core = core
234
+ break
235
+
236
+ if matched_core:
237
+ # Determine confidence based on match type
238
+ confidence = "high" if matched_core == term else "medium"
239
+
240
+ mappings.append({
241
+ "original_term": term, # Original ontology term
242
+ "matched_text": matched_core, # What actually matched in user input
243
+ "mapped_english": term_data["english"],
244
+ "dimension": term_data["dimension"],
245
+ "pain_type": term_data.get("pain_type"),
246
+ "confidence": confidence,
247
+ "mcgill_dimension": term_data.get("mcgill_dimension", "sensory"),
248
+ "detected_language": language
249
+ })
250
+
251
+ return mappings
252
+
253
+
254
+ def get_supported_languages() -> List[str]:
255
+ """
256
+ Get list of all supported languages.
257
+
258
+ Returns:
259
+ List of language codes
260
+ """
261
+ return ['zh', 'ko', 'es', 'hmong', 'en']
262
+
263
+
264
+ def get_descriptor_count(language: LanguageCode) -> int:
265
+ """
266
+ Get the number of pain descriptors available for a language.
267
+
268
+ Args:
269
+ language: Language code
270
+
271
+ Returns:
272
+ Number of descriptors
273
+ """
274
+ return len(LANGUAGE_DESCRIPTORS.get(language, {}))
275
+
276
+
277
+ # Re-export temporal and anatomical mappings from original module
278
+ # These are language-agnostic or can be expanded in the future
279
+ try:
280
+ from ontology.pain_mapping import (
281
+ TEMPORAL_PATTERNS,
282
+ ANATOMICAL_LOCATIONS,
283
+ extract_temporal_pattern,
284
+ extract_anatomical_location
285
+ )
286
+ except ImportError:
287
+ # Fallback if running as standalone
288
+ from pain_mapping import (
289
+ TEMPORAL_PATTERNS,
290
+ ANATOMICAL_LOCATIONS,
291
+ extract_temporal_pattern,
292
+ extract_anatomical_location
293
+ )
294
+
295
+
296
+ if __name__ == '__main__':
297
+ # Test multilingual mapping
298
+ test_cases = [
299
+ ("我有火辣辣的疼痛", "zh"),
300
+ ("허리가 따끔거리듯이 아프다", "ko"),
301
+ ("Tengo un dolor agudo", "es"),
302
+ ("Kuv mob mob heev", "hmong"),
303
+ ]
304
+
305
+ print("Multilingual Pain Mapping Tests:")
306
+ print("=" * 70)
307
+
308
+ for text, expected_lang in test_cases:
309
+ print(f"\nText: {text}")
310
+ print(f"Expected language: {expected_lang}")
311
+
312
+ mappings = map_multilingual_to_english(text)
313
+
314
+ if mappings:
315
+ print(f"Mappings found: {len(mappings)}")
316
+ for m in mappings:
317
+ print(f" - {m['original_term']} → {m['mapped_english']} ({m['pain_type']})")
318
+ print(f" Language: {m['detected_language']}, Confidence: {m['confidence']}")
319
+ else:
320
+ print(" No mappings found")
Backend/pipeline/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """
2
+ End-to-end neuro-symbolic pain assessment pipeline orchestration.
3
+ """
4
+ from .pain_assessment_pipeline import PainAssessmentPipeline
5
+
6
+ __all__ = ['PainAssessmentPipeline']
Backend/pipeline/pain_assessment_pipeline.py ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ End-to-end neuro-symbolic pain assessment pipeline with multilingual support.
3
+
4
+ Orchestrates: LLM extraction -> Ontology mapping -> JSON structuring -> Rule engine -> Report generation
5
+
6
+ Supported Languages: Chinese (中文), Korean (한국어), Spanish (Español), Hmong
7
+
8
+ This module implements the complete data flow from unstructured patient input to
9
+ structured, explainable clinical recommendations. LLM is used ONLY for narrow-scope
10
+ entity extraction and optional report formatting. All clinical logic is deterministic
11
+ and rule-based.
12
+ """
13
+
14
+ from typing import Dict, Any, List
15
+ import sys
16
+ import os
17
+
18
+ # Add Backend to path for imports
19
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
20
+
21
+ from models.pain_schema import PainOntology, ExplainableReport
22
+ from ontology.pain_mapping_multilingual import (
23
+ map_multilingual_to_english,
24
+ extract_temporal_pattern,
25
+ extract_anatomical_location,
26
+ get_supported_languages
27
+ )
28
+ from ontology.pain_mapping import get_unmapped_descriptors, suggest_similar_terms
29
+ from utils.language_detector import detect_language, get_language_name, LanguageCode
30
+ from inference.rule_engine import RuleEngine
31
+ from utils.report_generator import generate_comprehensive_report, translate_to_english_simple
32
+
33
+ # Smart semantic distance service selection
34
+ EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "biolord")
35
+
36
+ if EMBEDDING_MODEL == "biolord":
37
+ from services.semantic_distance_service_biolord import calculate_semantic_distances
38
+ else:
39
+ from services.semantic_distance_service_v2 import calculate_semantic_distances
40
+
41
+
42
+ class PainAssessmentPipeline:
43
+ """
44
+ Orchestrates the complete neuro-symbolic pain assessment workflow.
45
+
46
+ Pipeline stages:
47
+ 1. Input Reception - Receive raw patient text
48
+ 2. LLM Entity Extraction - Extract entities using constrained LLM (NER only)
49
+ 3. Ontology Mapping - Map Chinese terms to English medical terminology
50
+ 4. JSON Structuring - Populate PainOntology Pydantic model
51
+ 5. Rule Engine - Apply deterministic clinical decision rules
52
+ 6. Report Generation - Create final explainable report
53
+
54
+ LLM is used ONLY for narrow-scope NER and final report formatting.
55
+ All clinical logic is deterministic and rule-based.
56
+ """
57
+
58
+ def __init__(self, verbose: bool = True):
59
+ """
60
+ Initialize the pain assessment pipeline.
61
+
62
+ Args:
63
+ verbose: If True, print progress messages for each pipeline stage
64
+ """
65
+ self.rule_engine = RuleEngine()
66
+ self.verbose = verbose
67
+
68
+ def _log(self, message: str):
69
+ """Print log message if verbose mode is enabled."""
70
+ if self.verbose:
71
+ print(message)
72
+
73
+ def execute(
74
+ self,
75
+ patient_text: str,
76
+ llm_entities: Dict[str, Any] = None,
77
+ language: LanguageCode = None
78
+ ) -> ExplainableReport:
79
+ """
80
+ Execute the complete pain assessment pipeline with multilingual support.
81
+
82
+ Args:
83
+ patient_text: Raw patient pain description in any supported language
84
+ (Chinese, Korean, Spanish, Hmong, English)
85
+ llm_entities: Optional pre-extracted LLM entities (for testing or caching)
86
+ If None, will need to call LLM service externally
87
+ language: Optional language code ('zh', 'ko', 'es', 'hmong', 'en')
88
+ If None, will auto-detect from text
89
+
90
+ Returns:
91
+ ExplainableReport with structured data, reasoning chain, and recommendations
92
+
93
+ Example:
94
+ >>> pipeline = PainAssessmentPipeline()
95
+ >>> # Chinese input
96
+ >>> text = "最近四个月腰部到腿部总是像触电一样的麻痛,晚上痛得睡不着,心情很郁闷"
97
+ >>> llm_entities = {
98
+ ... "pain_descriptors": ["触电一样", "麻痛"],
99
+ ... "location": "腰部到腿部",
100
+ ... "duration_phrase": "四个月",
101
+ ... "emotion_keywords": ["郁闷"],
102
+ ... "functional_impact": "睡不着"
103
+ ... }
104
+ >>> report = pipeline.execute(text, llm_entities)
105
+ >>>
106
+ >>> # Korean input
107
+ >>> text_ko = "허리가 따끔거리듯이 아프다"
108
+ >>> report = pipeline.execute(text_ko, language='ko')
109
+ """
110
+
111
+ # ===== Save context for GPT report generation =====
112
+ self.original_patient_text = patient_text
113
+ self.detected_language = None
114
+ self.current_ontology_mappings = []
115
+
116
+ # ===== Node 1: Input Reception with Language Detection =====
117
+ self._log(f"[Node 1] Received input: {patient_text[:100]}...")
118
+
119
+ # Auto-detect language if not specified
120
+ if language is None:
121
+ language = detect_language(patient_text)
122
+
123
+ language_name = get_language_name(language)
124
+ self.detected_language = language_name # Save language name
125
+ self._log(f"[Node 1] Detected language: {language_name} ({language})")
126
+
127
+ # ===== Node 2: LLM Entity Extraction (Narrow-Scope NER) =====
128
+ # Note: LLM extraction is handled externally by llm_service.py
129
+ # This pipeline receives the extracted entities as input
130
+ self._log("[Node 2] Using provided LLM entity extraction...")
131
+ if llm_entities is None:
132
+ llm_entities = {} # Fallback to empty dict if not provided
133
+
134
+ # ===== Node 3: Ontology Mapping (Deterministic) =====
135
+ self._log(f"[Node 3] Performing {language_name} → English ontology mapping...")
136
+ ontology_mappings = map_multilingual_to_english(patient_text, language)
137
+ self.current_ontology_mappings = ontology_mappings # Save for GPT use
138
+ temporal_pattern = extract_temporal_pattern(patient_text)
139
+ locations = extract_anatomical_location(patient_text)
140
+
141
+ # Check for unmapped descriptors
142
+ unmapped = get_unmapped_descriptors(patient_text, ontology_mappings)
143
+ unmapped_suggestions = [] # Store suggestions for unmapped terms
144
+
145
+ if unmapped:
146
+ self._log(f"[Node 3] ⚠️ Unmapped pain descriptors found: {unmapped}")
147
+ self._log(f"[Node 3] Generating similarity suggestions (non-definitive)...")
148
+
149
+ # Generate suggestions for each unmapped term
150
+ for term in unmapped:
151
+ suggestions = suggest_similar_terms(term, language)
152
+ if suggestions:
153
+ unmapped_suggestions.append({
154
+ "unmapped_term": term,
155
+ "suggestions": suggestions
156
+ })
157
+ self._log(f"[Node 3] '{term}' → Found {len(suggestions)} similar dictionary term(s)")
158
+
159
+ # Add suggestions to ontology mappings for display
160
+ for item in unmapped_suggestions:
161
+ for suggestion in item["suggestions"]:
162
+ ontology_mappings.append({
163
+ "original_term": item["unmapped_term"],
164
+ "mapped_english": f"⚠️ {suggestion['english']} (suggested)",
165
+ "pain_type": suggestion.get("pain_type", "unknown"),
166
+ "dimension": suggestion.get("dimension", "unknown"),
167
+ "confidence": "suggestion_only", # Mark as suggestion
168
+ "is_suggestion": True,
169
+ "suggestion_note": suggestion["note"],
170
+ "similarity_reason": suggestion["similarity_reason"]
171
+ })
172
+
173
+ self._log(f"[Node 3] Found {len(ontology_mappings)} term mapping(s) from {language_name}")
174
+
175
+ # ===== Node 3.5: Semantic Analysis V2 (Multilingual Dictionary Matching) =====
176
+ if unmapped:
177
+ self._log(f"[Node 3.5] Analyzing {len(unmapped)} unmapped terms (V2: Multilingual dictionary)...")
178
+ semantic_analysis = calculate_semantic_distances(
179
+ unmapped_terms=unmapped, # Original native language expressions
180
+ patient_text=patient_text,
181
+ language=language_name
182
+ )
183
+ else:
184
+ semantic_analysis = None
185
+
186
+ self.semantic_analysis = semantic_analysis
187
+
188
+ # ===== Node 4: Forced JSON Structuring =====
189
+ self._log("[Node 4] Constructing PainOntology JSON...")
190
+ pain_data = self._construct_pain_ontology(
191
+ llm_entities,
192
+ ontology_mappings,
193
+ temporal_pattern,
194
+ locations,
195
+ unmapped
196
+ )
197
+
198
+ # ===== Node 5: Rule Engine (Symbolic Reasoning) =====
199
+ self._log("[Node 5] Applying clinical decision rules...")
200
+ recommendations = self.rule_engine.evaluate(pain_data)
201
+ self._log(f"[Node 5] Triggered {len(recommendations)} recommendation(s)")
202
+
203
+ reasoning_chain = self.rule_engine.generate_reasoning_chain(
204
+ pain_data,
205
+ recommendations,
206
+ ontology_mappings
207
+ )
208
+
209
+ # ===== Node 6: Report Generation =====
210
+ self._log("[Node 6] Generating final clinical report...")
211
+ physician_summary = self._generate_summary(
212
+ pain_data,
213
+ recommendations,
214
+ unmapped
215
+ )
216
+
217
+ # Assemble final explainable report
218
+ report = ExplainableReport(
219
+ structured_data=pain_data,
220
+ ontology_mapping_trace=ontology_mappings,
221
+ clinical_recommendations=recommendations,
222
+ reasoning_chain=reasoning_chain,
223
+ physician_summary=physician_summary
224
+ )
225
+
226
+ self._log("[Pipeline] Execution complete!")
227
+ return report
228
+
229
+ def execute_with_mappings(
230
+ self,
231
+ patient_text: str,
232
+ llm_entities: Dict[str, Any],
233
+ ontology_mappings: List[Dict],
234
+ language: LanguageCode = None
235
+ ) -> ExplainableReport:
236
+ """
237
+ Execute pipeline with pre-computed ontology mappings.
238
+
239
+ Used when LLM has already matched terms from provided vocabulary.
240
+ Skips the ontology mapping step and uses pre-translated mappings.
241
+
242
+ Args:
243
+ patient_text: Patient description
244
+ llm_entities: LLM extracted structured fields (pain_descriptors = unique/unmapped terms)
245
+ ontology_mappings: Pre-computed term translations from vocabulary
246
+ language: Language code
247
+
248
+ Returns:
249
+ ExplainableReport with full analysis
250
+ """
251
+ if language is None:
252
+ language = detect_language(patient_text)
253
+
254
+ # ===== Save context for GPT report generation =====
255
+ self.original_patient_text = patient_text
256
+ language_name = get_language_name(language)
257
+ self.detected_language = language_name
258
+ self.current_ontology_mappings = ontology_mappings
259
+
260
+ self._log(f"[Pipeline-Fast] Using {len(ontology_mappings)} pre-computed mappings")
261
+
262
+ # Extract patterns
263
+ temporal_pattern = extract_temporal_pattern(patient_text)
264
+ locations = extract_anatomical_location(patient_text)
265
+
266
+ # Unmapped descriptors = unique expressions in llm_entities.pain_descriptors
267
+ # These are creative/metaphorical terms not in dictionary (e.g., "蚂蚁在爬", "따끔거리다")
268
+ unmapped = llm_entities.get("pain_descriptors", [])
269
+
270
+ # ===== Semantic Analysis V2: Multilingual Dictionary Matching =====
271
+ if unmapped:
272
+ self._log(f"[Pipeline-Fast] Analyzing {len(unmapped)} unmapped terms (V2: Multilingual dictionary)...")
273
+ self.semantic_analysis = calculate_semantic_distances(
274
+ unmapped_terms=unmapped, # Original native language expressions
275
+ patient_text=patient_text,
276
+ language=language_name
277
+ )
278
+ # Debug: Print semantic analysis results
279
+ if self.semantic_analysis:
280
+ self._log(f"[Pipeline-Fast] Semantic analysis completed: {self.semantic_analysis}")
281
+ else:
282
+ self.semantic_analysis = None
283
+
284
+ # Construct pain data
285
+ pain_data = self._construct_pain_ontology(
286
+ llm_entities,
287
+ ontology_mappings,
288
+ temporal_pattern,
289
+ locations,
290
+ unmapped
291
+ )
292
+
293
+ # Apply rules
294
+ recommendations = self.rule_engine.evaluate(pain_data)
295
+ reasoning_chain = self.rule_engine.generate_reasoning_chain(
296
+ pain_data, recommendations, ontology_mappings
297
+ )
298
+
299
+ # Generate report
300
+ physician_summary = self._generate_summary(pain_data, recommendations, unmapped)
301
+
302
+ report = ExplainableReport(
303
+ structured_data=pain_data,
304
+ ontology_mapping_trace=ontology_mappings,
305
+ clinical_recommendations=recommendations,
306
+ reasoning_chain=reasoning_chain,
307
+ physician_summary=physician_summary
308
+ )
309
+
310
+ self._log("[Pipeline-Fast] Execution complete!")
311
+ return report
312
+
313
+ def _construct_pain_ontology(
314
+ self,
315
+ llm_entities: Dict[str, Any],
316
+ ontology_mappings: List[Dict],
317
+ temporal_pattern: str,
318
+ locations: List[str],
319
+ unmapped_descriptors: List[str]
320
+ ) -> PainOntology:
321
+ """
322
+ Construct PainOntology from mapped data.
323
+
324
+ Combines LLM extraction with deterministic ontology mapping to populate
325
+ the structured pain model. Prioritizes ontology mapping over LLM extraction
326
+ for medical terms to ensure accuracy.
327
+
328
+ Args:
329
+ llm_entities: Dictionary of entities extracted by LLM
330
+ ontology_mappings: List of Chinese→English mappings from ontology
331
+ temporal_pattern: Standardized temporal pattern
332
+ locations: List of anatomical locations
333
+ unmapped_descriptors: Pain descriptors that couldn't be mapped
334
+
335
+ Returns:
336
+ PainOntology instance with all fields populated
337
+ """
338
+
339
+ # ===== Extract pain type from ontology mappings =====
340
+ pain_types = []
341
+ pain_classification = None
342
+
343
+ for mapping in ontology_mappings:
344
+ if mapping['dimension'] == 'sensory':
345
+ pain_types.append(mapping['mapped_english'])
346
+ if not pain_classification and mapping.get('pain_type'):
347
+ pain_classification = mapping['pain_type'].capitalize()
348
+
349
+ # Construct pain type string
350
+ if pain_types:
351
+ pain_type_str = f"{pain_classification or 'Mixed'} ({', '.join(pain_types)})"
352
+ else:
353
+ # Fallback to LLM extraction if no ontology mapping
354
+ llm_descriptors = llm_entities.get('pain_descriptors', [])
355
+ if llm_descriptors:
356
+ pain_type_str = f"Unclassified ({', '.join(llm_descriptors)})"
357
+ else:
358
+ pain_type_str = "Not clearly specified"
359
+
360
+ # Add note if unmapped descriptors exist
361
+ if unmapped_descriptors:
362
+ pain_type_str += f" [Unmapped terms: {', '.join(unmapped_descriptors[:3])}]"
363
+
364
+ # ===== Extract emotion from affective mappings =====
365
+ emotions = [
366
+ mapping['mapped_english']
367
+ for mapping in ontology_mappings
368
+ if mapping['dimension'] == 'affective'
369
+ ]
370
+
371
+ # Fallback to LLM if no ontology mapping
372
+ if not emotions and llm_entities.get('emotion_keywords'):
373
+ emotions = llm_entities['emotion_keywords']
374
+
375
+ emotion_str = ', '.join(emotions) if emotions else None
376
+
377
+ # ===== Construct location string =====
378
+ if locations:
379
+ location_str = ', '.join(locations)
380
+ else:
381
+ location_str = llm_entities.get('location') or 'Not specified'
382
+
383
+ # ===== Temporal pattern =====
384
+ if not temporal_pattern:
385
+ temporal_pattern = llm_entities.get('temporal_pattern') or "Not specified"
386
+
387
+ # ===== Intensity (from LLM only, as it's factual extraction) =====
388
+ # Format: "Original text [English translation]" for bilingual display
389
+ intensity_raw = llm_entities.get('intensity', 'Not explicitly stated')
390
+ if intensity_raw and intensity_raw != 'Not explicitly stated' and intensity_raw != 'Not stated':
391
+ # Try to translate if non-English
392
+ intensity_en = translate_to_english_simple(intensity_raw)
393
+ # If translation differs from original, use bilingual format
394
+ if intensity_en != intensity_raw:
395
+ intensity = f"{intensity_raw} [{intensity_en}]"
396
+ else:
397
+ intensity = intensity_raw
398
+ else:
399
+ intensity = intensity_raw
400
+
401
+ # ===== Functional impact =====
402
+ # Format: "Original text [English translation]" for bilingual display
403
+ functional_impact_raw = llm_entities.get('functional_impact')
404
+ if functional_impact_raw:
405
+ # Try to translate if non-English
406
+ functional_impact_en = translate_to_english_simple(functional_impact_raw)
407
+ # If translation differs from original, use bilingual format
408
+ if functional_impact_en != functional_impact_raw:
409
+ functional_impact = f"{functional_impact_raw} [{functional_impact_en}]"
410
+ else:
411
+ functional_impact = functional_impact_raw
412
+ else:
413
+ functional_impact = None
414
+
415
+ return PainOntology(
416
+ pain_type=pain_type_str,
417
+ intensity=intensity,
418
+ location=location_str,
419
+ emotion=emotion_str,
420
+ temporal_pattern=temporal_pattern,
421
+ functional_impact=functional_impact
422
+ )
423
+
424
+ def _generate_summary(
425
+ self,
426
+ pain_data: PainOntology,
427
+ recommendations: List,
428
+ unmapped_descriptors: List[str]
429
+ ) -> str:
430
+ """
431
+ Generate clinical summary using GPT comprehensive report.
432
+
433
+ Calls GPT to generate bilingual/multilingual clinical report after
434
+ rule-based analysis completes.
435
+ """
436
+ try:
437
+ # Call GPT to generate comprehensive report
438
+ report = generate_comprehensive_report(
439
+ original_text=self.original_patient_text,
440
+ structured_data=pain_data.model_dump(),
441
+ ontology_mappings=self.current_ontology_mappings,
442
+ clinical_recommendations=[rec.model_dump() for rec in recommendations],
443
+ detected_language=self.detected_language or "Chinese", # Default to Chinese
444
+ semantic_analysis=self.semantic_analysis # Pass semantic distance results
445
+ )
446
+
447
+ # Add MAPPED terms section (direct dictionary matches)
448
+ mapped_count = 0
449
+ if self.current_ontology_mappings:
450
+ # Filter out suggestions, only show exact mappings
451
+ exact_mappings = [m for m in self.current_ontology_mappings
452
+ if not m.get('is_suggestion') and m.get('confidence') != 'suggestion_only']
453
+
454
+ if exact_mappings:
455
+ mapped_count = len(exact_mappings)
456
+ report += "\n\n---\n\n## ✅ Successfully Mapped Pain Descriptors\n\n"
457
+ report += "*These terms were found directly in the standard medical pain dictionary:*\n\n"
458
+
459
+ for mapping in exact_mappings[:10]: # Limit display
460
+ original = mapping.get('original_term', '')
461
+ english = mapping.get('mapped_english', '')
462
+ pain_type = mapping.get('pain_type', 'unknown')
463
+ if original and english:
464
+ report += f"- **{original}** → {english} (Category: {pain_type})\n"
465
+
466
+ if len(exact_mappings) > 10:
467
+ report += f"\n*...and {len(exact_mappings) - 10} more mapped terms*\n"
468
+
469
+ # Add UNMAPPED terms semantic analysis section
470
+ if self.semantic_analysis and self.semantic_analysis.get('unmapped_analysis'):
471
+ report += "\n\n---\n\n## 🔬 Unmapped Terms - Semantic Distance Analysis (AI-Assisted Interpretation)\n\n"
472
+ report += "*The following expressions were NOT found in the standard dictionary. Our AI system performed semantic analysis to suggest possible medical term matches:*\n\n"
473
+ report += f"**Total unmapped terms analyzed:** {len(self.semantic_analysis['unmapped_analysis'])}\n\n"
474
+
475
+ for item in self.semantic_analysis['unmapped_analysis']:
476
+ original = item['original_term'] # Patient's native language expression
477
+ matched_native = item.get('matched_mcgill_native') # Best matching dictionary term (native lang)
478
+ standard_english = item.get('matched_standard_english') # Dictionary's medical English translation
479
+ matches = item['closest_matches']
480
+ confidence = item['confidence']
481
+ lang_code = item.get('language', 'unknown')
482
+
483
+ report += f"### Original Expression: \"{original}\"\n\n"
484
+
485
+ # Show dictionary match (works for all languages: Chinese, Korean, Spanish, Hmong)
486
+ if matched_native and standard_english:
487
+ report += f"**Best Dictionary Match:** {matched_native} → **{standard_english}**\n\n"
488
+
489
+ report += f"**Confidence Level:** {confidence.upper()}\n\n"
490
+ report += "**Top 3 Similar Medical Terms (from dictionary):**\n\n"
491
+
492
+ for i, match in enumerate(matches[:3], 1):
493
+ similarity_pct = match['score'] * 100
494
+ native_term = match.get('native_term', match.get('chinese_term', match.get('term', '')))
495
+ english_term = match.get('english', '')
496
+ report += f"{i}. **{native_term}** ({english_term}) - Similarity: {match['score']:.3f} ({similarity_pct:.1f}%)\n"
497
+
498
+ # Improved interpretation
499
+ top_score = matches[0]['score']
500
+ if top_score > 0.75:
501
+ strength = "strong"
502
+ elif top_score > 0.60:
503
+ strength = "moderate"
504
+ else:
505
+ strength = "weak"
506
+
507
+ report += f"\n**Clinical Interpretation:** The semantic analysis shows {strength} similarity "
508
+ if matched_native and standard_english:
509
+ report += f"(score: {top_score:.3f}) to the dictionary term '{matched_native}' ({standard_english}). "
510
+ report += f"This suggests the patient may be experiencing {standard_english.lower()}-type pain sensations.\n\n"
511
+ else:
512
+ report += f"(score: {top_score:.3f}) to medical terms in the dictionary.\n\n"
513
+ report += "---\n\n"
514
+
515
+ report += "---\n\n**Summary:**\n"
516
+ report += f"- ✅ Mapped (direct dictionary match): {mapped_count} terms\n"
517
+ report += f"- 🔬 Unmapped (AI-assisted analysis): {len(self.semantic_analysis['unmapped_analysis'])} terms\n\n"
518
+ report += "*Note: These are AI-generated suggestions based on semantic embedding similarity. Scores closer to 1.0 indicate stronger semantic relationships. Always verify with clinical assessment.*\n\n"
519
+
520
+ # Add unmapped term warning
521
+ if unmapped_descriptors:
522
+ report += f"\n\n**⚠️ Note | 注意**: Some pain descriptors could not be automatically mapped: {', '.join(unmapped_descriptors[:3])}. Manual clinical review recommended."
523
+
524
+ # Ensure Clinical Action Plan is included (add if missing)
525
+ if "⚕️ Clinical Action Plan" not in report and "Clinical Action Plan" not in report:
526
+ report += "\n\n---\n\n## ⚕️ Clinical Action Plan\n\n"
527
+ if recommendations:
528
+ for i, rec in enumerate(recommendations, 1):
529
+ report += f"**{i}. {rec.triggered_by_rule}**\n\n"
530
+ report += f"{rec.recommendation}\n\n"
531
+ if rec.guideline_reference:
532
+ report += f"*Reference: {rec.guideline_reference}*\n\n"
533
+ else:
534
+ report += "Based on the pain assessment:\n\n"
535
+ report += "1. **Comprehensive Clinical Evaluation**: Conduct detailed pain assessment with standardized scales and physical examination.\n\n"
536
+ report += "2. **Documentation**: Document all pain characteristics, triggers, and functional impacts for ongoing monitoring.\n\n"
537
+ report += "3. **Individualized Management**: Develop treatment plan based on complete clinical picture and patient preferences.\n\n"
538
+
539
+ return report
540
+
541
+ except Exception as e:
542
+ # If GPT fails, fallback to template generation
543
+ import traceback
544
+ error_details = traceback.format_exc()
545
+ self._log(f"[Warning] GPT report generation failed: {e}")
546
+ self._log(f"[Error Details] {error_details}")
547
+ print(f"\n{'='*80}")
548
+ print(f"[ERROR] Report generation failed!")
549
+ print(f"Exception: {e}")
550
+ print(f"Traceback:\n{error_details}")
551
+ print(f"{'='*80}\n")
552
+ return self._generate_summary_template(pain_data, recommendations, unmapped_descriptors)
553
+
554
+ def _generate_summary_template(
555
+ self,
556
+ pain_data: PainOntology,
557
+ recommendations: List,
558
+ unmapped_descriptors: List[str]
559
+ ) -> str:
560
+ """Template-based summary as fallback if GPT fails."""
561
+
562
+ summary_parts = []
563
+
564
+ # ===== Patient Presentation =====
565
+ summary_parts.append("**Patient Presentation:**\n")
566
+
567
+ # Temporal pattern and location
568
+ if pain_data.temporal_pattern != "Not specified":
569
+ summary_parts.append(
570
+ f"Patient presents with {pain_data.temporal_pattern.lower()} "
571
+ f"pain localized to {pain_data.location.lower()}. "
572
+ )
573
+ else:
574
+ summary_parts.append(f"Patient presents with pain in {pain_data.location.lower()}. ")
575
+
576
+ # Pain characteristics
577
+ if pain_data.pain_type != "Not clearly specified":
578
+ summary_parts.append(
579
+ f"Pain is characterized as {pain_data.pain_type.lower()}. "
580
+ )
581
+
582
+ # Intensity
583
+ if pain_data.intensity and pain_data.intensity != "Not explicitly stated":
584
+ summary_parts.append(f"Pain intensity: {pain_data.intensity}. ")
585
+
586
+ # Emotional/functional impact
587
+ if pain_data.emotion:
588
+ summary_parts.append(
589
+ f"Patient reports significant affective distress ({pain_data.emotion.lower()}). "
590
+ )
591
+
592
+ if pain_data.functional_impact:
593
+ summary_parts.append(
594
+ f"Functional impact noted: {pain_data.functional_impact.lower()}. "
595
+ )
596
+
597
+ # Unmapped terms warning
598
+ if unmapped_descriptors:
599
+ summary_parts.append(
600
+ f"\n**Note:** Some pain descriptors could not be automatically mapped to "
601
+ f"standard medical terminology ({', '.join(unmapped_descriptors[:3])}). "
602
+ f"Manual clinical review recommended.\n"
603
+ )
604
+
605
+ # ===== Clinical Recommendations =====
606
+ if recommendations:
607
+ summary_parts.append("\n**Clinical Recommendations:**\n")
608
+ for i, rec in enumerate(recommendations, 1):
609
+ summary_parts.append(f"\n{i}. {rec.recommendation}\n")
610
+ if rec.guideline_reference:
611
+ summary_parts.append(f" *Reference: {rec.guideline_reference}*\n")
612
+ summary_parts.append(f" *Evidence: {rec.evidence}*\n")
613
+ else:
614
+ summary_parts.append(
615
+ "\n**Clinical Recommendations:**\n"
616
+ "Standard pain assessment and management pathway recommended. "
617
+ "Consider detailed clinical interview for further characterization.\n"
618
+ )
619
+
620
+ return ''.join(summary_parts)
621
+
622
+ def get_pipeline_info(self) -> Dict[str, Any]:
623
+ """
624
+ Get information about the current pipeline configuration.
625
+
626
+ Returns:
627
+ Dictionary with pipeline metadata
628
+ """
629
+ return {
630
+ "pipeline_version": "2.0.0",
631
+ "architecture": "Neuro-Symbolic Hybrid (Multilingual)",
632
+ "supported_languages": get_supported_languages(),
633
+ "rule_count": self.rule_engine.get_rule_count(),
634
+ "active_rules": self.rule_engine.get_rule_ids(),
635
+ "ontology_coverage": {
636
+ "total_descriptors": 373,
637
+ "chinese_terms": 88,
638
+ "korean_terms": 116,
639
+ "spanish_terms": 105,
640
+ "hmong_terms": 64,
641
+ "temporal_patterns": 14,
642
+ "anatomical_locations": 21
643
+ }
644
+ }
Backend/read_xlsx.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Temporary script to read and display the xlsx content.
3
+ This will help us understand the structure and extend to multilingual support.
4
+ """
5
+ import pandas as pd
6
+ import sys
7
+ import os
8
+
9
+ # Add Backend to path
10
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
11
+
12
+ try:
13
+ # Read the xlsx file
14
+ xlsx_path = os.path.join(os.path.dirname(__file__), 'data', 'questionnaire_form.xlsx')
15
+
16
+ # Try to read all sheets
17
+ xl_file = pd.ExcelFile(xlsx_path)
18
+ print(f"📊 Found {len(xl_file.sheet_names)} sheet(s): {xl_file.sheet_names}\n")
19
+
20
+ for sheet_name in xl_file.sheet_names:
21
+ print(f"\n{'='*60}")
22
+ print(f"📋 Sheet: {sheet_name}")
23
+ print('='*60)
24
+
25
+ df = pd.read_excel(xlsx_path, sheet_name=sheet_name)
26
+ print(f"\nColumns: {df.columns.tolist()}")
27
+ print(f"Rows: {len(df)}\n")
28
+ print(df.head(20).to_string())
29
+
30
+ except Exception as e:
31
+ print(f"❌ Error reading xlsx: {e}")
32
+ print("\nℹ️ Make sure pandas and openpyxl are installed:")
33
+ print(" pip install pandas openpyxl")
Backend/scripts/format_pain_descriptors.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Convert multilingual_pain_data.json to Python dictionary format
3
+ for pain_mapping.py
4
+ """
5
+ import json
6
+ import os
7
+
8
+ def format_dict_for_python(data, indent=1):
9
+ """Format dictionary data as Python code"""
10
+ lines = []
11
+ tab = " " * indent
12
+
13
+ for key, value in data.items():
14
+ if isinstance(value, dict):
15
+ # Check if it's a descriptor dict or category dict
16
+ if 'english' in value:
17
+ # It's a descriptor
18
+ lines.append(f'{tab}"{key}": {{')
19
+ lines.append(f'{tab} "english": "{value["english"]}",')
20
+ lines.append(f'{tab} "mcgill_dimension": "sensory",')
21
+
22
+ # Add SNOMED CT if available
23
+ if value.get('snomed_ct'):
24
+ lines.append(f'{tab} "snomed_ct": "{value["snomed_ct"]}"')
25
+ else:
26
+ lines.append(f'{tab} "snomed_ct": None')
27
+
28
+ lines.append(f'{tab}}},')
29
+ else:
30
+ # It's a category
31
+ lines.append(f'{tab}"{key}": {{')
32
+ lines.extend(format_dict_for_python(value, indent + 1))
33
+ lines.append(f'{tab}}},')
34
+
35
+ return lines
36
+
37
+ def main():
38
+ script_dir = os.path.dirname(os.path.abspath(__file__))
39
+ json_path = os.path.join(script_dir, 'multilingual_pain_data.json')
40
+
41
+ with open(json_path, 'r', encoding='utf-8') as f:
42
+ data = json.load(f)
43
+
44
+ output_lines = []
45
+
46
+ # Generate dictionaries for each language
47
+ lang_names = {
48
+ 'chinese': 'CHINESE',
49
+ 'korean': 'KOREAN',
50
+ 'spanish': 'SPANISH',
51
+ 'hmong': 'HMONG'
52
+ }
53
+
54
+ for lang_key, lang_upper in lang_names.items():
55
+ output_lines.append(f"\n# {lang_upper} Pain Descriptors")
56
+ output_lines.append(f"{lang_upper}_PAIN_DESCRIPTORS = {{")
57
+
58
+ lang_data = data[lang_key]
59
+ for category in ['neuropathic', 'nociceptive', 'affective']:
60
+ if category in lang_data and lang_data[category]:
61
+ output_lines.append(f' "{category}": {{')
62
+
63
+ for term, info in lang_data[category].items():
64
+ output_lines.append(f' "{term}": {{')
65
+ output_lines.append(f' "english": "{info["english"]}",')
66
+ output_lines.append(f' "mcgill_dimension": "sensory"')
67
+ output_lines.append(f' }},')
68
+
69
+ output_lines.append(f' }},')
70
+
71
+ output_lines.append(f"}}\n")
72
+
73
+ # Save to file
74
+ output_path = os.path.join(script_dir, 'pain_descriptors_formatted.py')
75
+ with open(output_path, 'w', encoding='utf-8') as f:
76
+ f.write('\n'.join(output_lines))
77
+
78
+ print(f"✅ Formatted pain descriptors saved to: {output_path}")
79
+ print(f"\nStatistics:")
80
+ for lang_key, lang_upper in lang_names.items():
81
+ total = sum(len(data[lang_key].get(cat, {})) for cat in ['neuropathic', 'nociceptive', 'affective'])
82
+ print(f" {lang_upper}: {total} terms")
83
+
84
+ if __name__ == '__main__':
85
+ main()
Backend/scripts/multilingual_pain_data.json ADDED
@@ -0,0 +1,1525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "chinese": {
3
+ "neuropathic": {
4
+ "火辣辣的疼": {
5
+ "english": "burning",
6
+ "mcgill_dimension": "sensory"
7
+ },
8
+ "麻的": {
9
+ "english": "numb",
10
+ "mcgill_dimension": "sensory"
11
+ },
12
+ "刺骨痛": {
13
+ "english": "piercing",
14
+ "mcgill_dimension": "sensory"
15
+ },
16
+ "刺痛": {
17
+ "english": "tingling",
18
+ "mcgill_dimension": "sensory"
19
+ },
20
+ "剧烈的疼": {
21
+ "english": "sharp",
22
+ "mcgill_dimension": "sensory"
23
+ },
24
+ "蚊虫叮咬的刺疼": {
25
+ "english": "stinging",
26
+ "mcgill_dimension": "sensory"
27
+ }
28
+ },
29
+ "nociceptive": {
30
+ "疼": {
31
+ "english": "aching",
32
+ "mcgill_dimension": "sensory"
33
+ },
34
+ "急性的": {
35
+ "english": "acute",
36
+ "mcgill_dimension": "sensory"
37
+ },
38
+ "极度的疼痛": {
39
+ "english": "agonizing",
40
+ "mcgill_dimension": "sensory"
41
+ },
42
+ "跳动的痛": {
43
+ "english": "beating",
44
+ "mcgill_dimension": "sensory"
45
+ },
46
+ "强烈的疼痛": {
47
+ "english": "blinding",
48
+ "mcgill_dimension": "sensory"
49
+ },
50
+ "被刺穿的疼": {
51
+ "english": "boring",
52
+ "mcgill_dimension": "sensory"
53
+ },
54
+ "短暂的": {
55
+ "english": "brief",
56
+ "mcgill_dimension": "sensory"
57
+ },
58
+ "慢性的": {
59
+ "english": "chronic",
60
+ "mcgill_dimension": "sensory"
61
+ },
62
+ "冷痛": {
63
+ "english": "cold",
64
+ "mcgill_dimension": "sensory"
65
+ },
66
+ "不间断的": {
67
+ "english": "constant",
68
+ "mcgill_dimension": "sensory"
69
+ },
70
+ "冷的": {
71
+ "english": "cool",
72
+ "mcgill_dimension": "sensory"
73
+ },
74
+ "绞痛": {
75
+ "english": "cramp",
76
+ "mcgill_dimension": "sensory"
77
+ },
78
+ "压迫痛": {
79
+ "english": "crushing",
80
+ "mcgill_dimension": "sensory"
81
+ },
82
+ "切割痛": {
83
+ "english": "cutting",
84
+ "mcgill_dimension": "sensory"
85
+ },
86
+ "拉扯痛": {
87
+ "english": "pulling",
88
+ "mcgill_dimension": "sensory"
89
+ },
90
+ "可怕的痛苦": {
91
+ "english": "dreadful",
92
+ "mcgill_dimension": "sensory"
93
+ },
94
+ "钻痛": {
95
+ "english": "drilling",
96
+ "mcgill_dimension": "sensory"
97
+ },
98
+ "隐约的疼痛": {
99
+ "english": "dull",
100
+ "mcgill_dimension": "sensory"
101
+ },
102
+ "疼到没力气": {
103
+ "english": "exhaust",
104
+ "mcgill_dimension": "sensory"
105
+ },
106
+ "可怕的痛": {
107
+ "english": "fearful",
108
+ "mcgill_dimension": "sensory"
109
+ },
110
+ "一阵阵的": {
111
+ "english": "fitful",
112
+ "mcgill_dimension": "sensory"
113
+ },
114
+ "一闪而过的痛": {
115
+ "english": "flashing",
116
+ "mcgill_dimension": "sensory"
117
+ },
118
+ "闪烁的痛": {
119
+ "english": "Flickering",
120
+ "mcgill_dimension": "sensory"
121
+ },
122
+ "冷疼": {
123
+ "english": "freezing",
124
+ "mcgill_dimension": "sensory"
125
+ },
126
+ "疼的可怕": {
127
+ "english": "frightful",
128
+ "mcgill_dimension": "sensory"
129
+ },
130
+ "折磨的痛": {
131
+ "english": "gnawing",
132
+ "mcgill_dimension": "sensory"
133
+ },
134
+ "折磨人的": {
135
+ "english": "gruelling",
136
+ "mcgill_dimension": "sensory"
137
+ },
138
+ "非常痛": {
139
+ "english": "heavy",
140
+ "mcgill_dimension": "sensory"
141
+ },
142
+ "热的": {
143
+ "english": "hot",
144
+ "mcgill_dimension": "sensory"
145
+ },
146
+ "使...难受": {
147
+ "english": "hurt",
148
+ "mcgill_dimension": "sensory"
149
+ },
150
+ "强烈的": {
151
+ "english": "intense",
152
+ "mcgill_dimension": "sensory"
153
+ },
154
+ "痒的": {
155
+ "english": "itchy",
156
+ "mcgill_dimension": "sensory"
157
+ },
158
+ "跳动的疼": {
159
+ "english": "jumping",
160
+ "mcgill_dimension": "sensory"
161
+ },
162
+ "及其": {
163
+ "english": "to kill",
164
+ "mcgill_dimension": "sensory"
165
+ },
166
+ "撕裂痛": {
167
+ "english": "lacerating",
168
+ "mcgill_dimension": "sensory"
169
+ },
170
+ "撕裂的痛": {
171
+ "english": "lancinating",
172
+ "mcgill_dimension": "sensory"
173
+ },
174
+ "使人不得安宁": {
175
+ "english": "nagging",
176
+ "mcgill_dimension": "sensory"
177
+ },
178
+ "钻心的": {
179
+ "english": "nauseating",
180
+ "mcgill_dimension": "sensory"
181
+ },
182
+ "渗透的": {
183
+ "english": "penetrating",
184
+ "mcgill_dimension": "sensory"
185
+ },
186
+ "一阵一阵的痛": {
187
+ "english": "periodic",
188
+ "mcgill_dimension": "sensory"
189
+ },
190
+ "掐疼": {
191
+ "english": "pinching",
192
+ "mcgill_dimension": "sensory"
193
+ },
194
+ "重击痛": {
195
+ "english": "pounding",
196
+ "mcgill_dimension": "sensory"
197
+ },
198
+ "压着痛": {
199
+ "english": "pressing",
200
+ "mcgill_dimension": "sensory"
201
+ },
202
+ "搏动性痛": {
203
+ "english": "pulsing",
204
+ "mcgill_dimension": "sensory"
205
+ },
206
+ "颤抖": {
207
+ "english": "quivering",
208
+ "mcgill_dimension": "sensory"
209
+ },
210
+ "发散性疼痛": {
211
+ "english": "radiating",
212
+ "mcgill_dimension": "sensory"
213
+ },
214
+ "粗糙的": {
215
+ "english": "raspy",
216
+ "mcgill_dimension": "sensory"
217
+ },
218
+ "有节奏的": {
219
+ "english": "rhythmic",
220
+ "mcgill_dimension": "sensory"
221
+ },
222
+ "烫伤": {
223
+ "english": "scalding",
224
+ "mcgill_dimension": "sensory"
225
+ },
226
+ "灼痛": {
227
+ "english": "searing",
228
+ "mcgill_dimension": "sensory"
229
+ },
230
+ "剧烈疼痛": {
231
+ "english": "smarting",
232
+ "mcgill_dimension": "sensory"
233
+ },
234
+ "酸痛": {
235
+ "english": "sore",
236
+ "mcgill_dimension": "sensory"
237
+ },
238
+ "分裂痛": {
239
+ "english": "splitting",
240
+ "mcgill_dimension": "sensory"
241
+ },
242
+ "扩散性疼痛": {
243
+ "english": "spreading",
244
+ "mcgill_dimension": "sensory"
245
+ },
246
+ "挤压的疼痛": {
247
+ "english": "squeezing",
248
+ "mcgill_dimension": "sensory"
249
+ },
250
+ "令人窒息的": {
251
+ "english": "suffocating",
252
+ "mcgill_dimension": "sensory"
253
+ },
254
+ "紧张的": {
255
+ "english": "taut",
256
+ "mcgill_dimension": "sensory"
257
+ },
258
+ "撕裂的": {
259
+ "english": "tearing",
260
+ "mcgill_dimension": "sensory"
261
+ },
262
+ "一碰就痛": {
263
+ "english": "tender",
264
+ "mcgill_dimension": "sensory"
265
+ },
266
+ "程度很高的痛苦": {
267
+ "english": "terrifying",
268
+ "mcgill_dimension": "sensory"
269
+ },
270
+ "一抽一抽的痛": {
271
+ "english": "throbbing",
272
+ "mcgill_dimension": "sensory"
273
+ },
274
+ "顽固的": {
275
+ "english": "unyielding.",
276
+ "mcgill_dimension": "sensory"
277
+ },
278
+ "疲倦": {
279
+ "english": "to tire",
280
+ "mcgill_dimension": "sensory"
281
+ },
282
+ "折磨": {
283
+ "english": "torturing",
284
+ "mcgill_dimension": "sensory"
285
+ },
286
+ "剧烈的痛苦": {
287
+ "english": "vicious",
288
+ "mcgill_dimension": "sensory"
289
+ },
290
+ "极为痛苦的": {
291
+ "english": "wrenching",
292
+ "mcgill_dimension": "sensory"
293
+ },
294
+ "头": {
295
+ "english": "Head",
296
+ "mcgill_dimension": "sensory"
297
+ },
298
+ "脖子": {
299
+ "english": "Neck",
300
+ "mcgill_dimension": "sensory"
301
+ },
302
+ "脸": {
303
+ "english": "Face",
304
+ "mcgill_dimension": "sensory"
305
+ },
306
+ "手": {
307
+ "english": "Hands",
308
+ "mcgill_dimension": "sensory"
309
+ },
310
+ "手臂": {
311
+ "english": "Arms",
312
+ "mcgill_dimension": "sensory"
313
+ },
314
+ "背": {
315
+ "english": "Back",
316
+ "mcgill_dimension": "sensory"
317
+ },
318
+ "腿": {
319
+ "english": "Legs",
320
+ "mcgill_dimension": "sensory"
321
+ },
322
+ "脚": {
323
+ "english": "Feet",
324
+ "mcgill_dimension": "sensory"
325
+ },
326
+ "胸腔": {
327
+ "english": "Chest",
328
+ "mcgill_dimension": "sensory"
329
+ },
330
+ "腹部": {
331
+ "english": "Abdomen",
332
+ "mcgill_dimension": "sensory"
333
+ },
334
+ "肺": {
335
+ "english": "lungs",
336
+ "mcgill_dimension": "sensory"
337
+ },
338
+ "心脏": {
339
+ "english": "heart",
340
+ "mcgill_dimension": "sensory"
341
+ }
342
+ },
343
+ "affective": {
344
+ "烦人的": {
345
+ "english": "annoying",
346
+ "mcgill_dimension": "sensory"
347
+ },
348
+ "痛苦": {
349
+ "english": "miserable",
350
+ "mcgill_dimension": "sensory"
351
+ },
352
+ "麻烦的": {
353
+ "english": "troublesome",
354
+ "mcgill_dimension": "sensory"
355
+ },
356
+ "难以忍受的": {
357
+ "english": "unbearable",
358
+ "mcgill_dimension": "sensory"
359
+ }
360
+ }
361
+ },
362
+ "korean": {
363
+ "neuropathic": {
364
+ "따끔거리다": {
365
+ "english": "sting",
366
+ "mcgill_dimension": "sensory"
367
+ },
368
+ "찌르다": {
369
+ "english": "stabbing",
370
+ "mcgill_dimension": "sensory"
371
+ },
372
+ "타는것 같다": {
373
+ "english": "burning",
374
+ "mcgill_dimension": "sensory"
375
+ },
376
+ "아리다": {
377
+ "english": "stinging",
378
+ "mcgill_dimension": "sensory"
379
+ },
380
+ "얼얼하다": {
381
+ "english": "numb",
382
+ "mcgill_dimension": "sensory"
383
+ },
384
+ "쏘듯이 아프다": {
385
+ "english": "shooting",
386
+ "mcgill_dimension": "sensory"
387
+ },
388
+ "바늘로 찌르듯": {
389
+ "english": "pricking",
390
+ "mcgill_dimension": "sensory"
391
+ },
392
+ "칼로 찌르듯": {
393
+ "english": "stabbing",
394
+ "mcgill_dimension": "sensory"
395
+ },
396
+ "쓰라리다": {
397
+ "english": "sharp",
398
+ "mcgill_dimension": "sensory"
399
+ },
400
+ "화끈거리다": {
401
+ "english": "burning",
402
+ "mcgill_dimension": "sensory"
403
+ },
404
+ "서물서물하다": {
405
+ "english": "tingling",
406
+ "mcgill_dimension": "sensory"
407
+ },
408
+ "톡 쏘듯이 아프다": {
409
+ "english": "stinging",
410
+ "mcgill_dimension": "sensory"
411
+ },
412
+ "지치게 아프다": {
413
+ "english": "exhausting",
414
+ "mcgill_dimension": "sensory"
415
+ },
416
+ "뼈를 쳐미듯이 아프다": {
417
+ "english": "piercing",
418
+ "mcgill_dimension": "sensory"
419
+ },
420
+ "저리다": {
421
+ "english": "numb",
422
+ "mcgill_dimension": "sensory"
423
+ }
424
+ },
425
+ "nociceptive": {
426
+ "꼬집히다": {
427
+ "english": "pinched",
428
+ "mcgill_dimension": "sensory"
429
+ },
430
+ "뻐근하다": {
431
+ "english": "stiff",
432
+ "mcgill_dimension": "sensory"
433
+ },
434
+ "조이다": {
435
+ "english": "constricting",
436
+ "mcgill_dimension": "sensory"
437
+ },
438
+ "찢어지다": {
439
+ "english": "torn",
440
+ "mcgill_dimension": "sensory"
441
+ },
442
+ "터지다": {
443
+ "english": "broken",
444
+ "mcgill_dimension": "sensory"
445
+ },
446
+ "팽팽하다": {
447
+ "english": "tight",
448
+ "mcgill_dimension": "sensory"
449
+ },
450
+ "긁히다": {
451
+ "english": "scrape",
452
+ "mcgill_dimension": "sensory"
453
+ },
454
+ "꿈틀거리다": {
455
+ "english": "wriggle",
456
+ "mcgill_dimension": "sensory"
457
+ },
458
+ "둔하다": {
459
+ "english": "obtuse",
460
+ "mcgill_dimension": "sensory"
461
+ },
462
+ "무디다": {
463
+ "english": "dull",
464
+ "mcgill_dimension": "sensory"
465
+ },
466
+ "뻗치다": {
467
+ "english": "sticking out",
468
+ "mcgill_dimension": "sensory"
469
+ },
470
+ "쐬다": {
471
+ "english": "get stung",
472
+ "mcgill_dimension": "sensory"
473
+ },
474
+ "오싹하다": {
475
+ "english": "feel a chill",
476
+ "mcgill_dimension": "sensory"
477
+ },
478
+ "지지다": {
479
+ "english": "frying",
480
+ "mcgill_dimension": "sensory"
481
+ },
482
+ "화끈거리다": {
483
+ "english": "hot",
484
+ "mcgill_dimension": "sensory"
485
+ },
486
+ "부딪히다": {
487
+ "english": "hitting",
488
+ "mcgill_dimension": "sensory"
489
+ },
490
+ "쓰라리다": {
491
+ "english": "sore",
492
+ "mcgill_dimension": "sensory"
493
+ },
494
+ "으스러지다": {
495
+ "english": "be shattered",
496
+ "mcgill_dimension": "sensory"
497
+ },
498
+ "끊어지다": {
499
+ "english": "cut",
500
+ "mcgill_dimension": "sensory"
501
+ },
502
+ "살을 에이는 듯한 아픔.": {
503
+ "english": "penetrating",
504
+ "mcgill_dimension": "sensory"
505
+ },
506
+ "울리다": {
507
+ "english": "ringing",
508
+ "mcgill_dimension": "sensory"
509
+ },
510
+ "쪼개지다": {
511
+ "english": "splitting",
512
+ "mcgill_dimension": "sensory"
513
+ },
514
+ "시리다": {
515
+ "english": "cool",
516
+ "mcgill_dimension": "sensory"
517
+ },
518
+ "부서지다": {
519
+ "english": "smashing",
520
+ "mcgill_dimension": "sensory"
521
+ },
522
+ "싸하다": {
523
+ "english": "pungent",
524
+ "mcgill_dimension": "sensory"
525
+ },
526
+ "잘리다": {
527
+ "english": "be chopped",
528
+ "mcgill_dimension": "sensory"
529
+ },
530
+ "찌릿하다": {
531
+ "english": "throbbing",
532
+ "mcgill_dimension": "sensory"
533
+ },
534
+ "깎이다": {
535
+ "english": "peeling",
536
+ "mcgill_dimension": "sensory"
537
+ },
538
+ "깨지다": {
539
+ "english": "cracking",
540
+ "mcgill_dimension": "sensory"
541
+ },
542
+ "갈리다": {
543
+ "english": "be ground",
544
+ "mcgill_dimension": "sensory"
545
+ },
546
+ "비틀리다": {
547
+ "english": "be twisted",
548
+ "mcgill_dimension": "sensory"
549
+ },
550
+ "빠지다": {
551
+ "english": "falling out,",
552
+ "mcgill_dimension": "sensory"
553
+ },
554
+ "뻣뻣하다": {
555
+ "english": "stiff",
556
+ "mcgill_dimension": "sensory"
557
+ },
558
+ "삐드득": {
559
+ "english": "creaking",
560
+ "mcgill_dimension": "sensory"
561
+ },
562
+ "쑤시다": {
563
+ "english": "poking",
564
+ "mcgill_dimension": "sensory"
565
+ },
566
+ "가물가물 아프다": {
567
+ "english": "flickering",
568
+ "mcgill_dimension": "sensory"
569
+ },
570
+ "지근거리다": {
571
+ "english": "nagging",
572
+ "mcgill_dimension": "sensory"
573
+ },
574
+ "욱신욱신하다": {
575
+ "english": "pulsing",
576
+ "mcgill_dimension": "sensory"
577
+ },
578
+ "들먹거리다": {
579
+ "english": "beating",
580
+ "mcgill_dimension": "sensory"
581
+ },
582
+ "쾅쾅치듯이 아프다": {
583
+ "english": "pounding",
584
+ "mcgill_dimension": "sensory"
585
+ },
586
+ "움찔하게 아프다": {
587
+ "english": "jumping",
588
+ "mcgill_dimension": "sensory"
589
+ },
590
+ "따끔하다": {
591
+ "english": "flashing",
592
+ "mcgill_dimension": "sensory"
593
+ },
594
+ "송곳으로 찌르���": {
595
+ "english": "boring",
596
+ "mcgill_dimension": "sensory"
597
+ },
598
+ "구멍을 뚫듯이": {
599
+ "english": "drilling",
600
+ "mcgill_dimension": "sensory"
601
+ },
602
+ "칼로 찔러 쑤시듯": {
603
+ "english": "lancinating",
604
+ "mcgill_dimension": "sensory"
605
+ },
606
+ "베듯이 아프다": {
607
+ "english": "cutting",
608
+ "mcgill_dimension": "sensory"
609
+ },
610
+ "도려내듯 아프다": {
611
+ "english": "lacerating",
612
+ "mcgill_dimension": "sensory"
613
+ },
614
+ "꼬집듯 따끔하다": {
615
+ "english": "pinching",
616
+ "mcgill_dimension": "sensory"
617
+ },
618
+ "누르듯 아프다": {
619
+ "english": "pressing",
620
+ "mcgill_dimension": "sensory"
621
+ },
622
+ "꽉 무는듯 아프다": {
623
+ "english": "gnawing",
624
+ "mcgill_dimension": "sensory"
625
+ },
626
+ "꽉 지는듯 아프다": {
627
+ "english": "cramping",
628
+ "mcgill_dimension": "sensory"
629
+ },
630
+ "짓이기는 듯 아프다": {
631
+ "english": "crushing",
632
+ "mcgill_dimension": "sensory"
633
+ },
634
+ "결린다": {
635
+ "english": "tugging",
636
+ "mcgill_dimension": "sensory"
637
+ },
638
+ "땅긴다": {
639
+ "english": "pulling",
640
+ "mcgill_dimension": "sensory"
641
+ },
642
+ "뒤틀리듯 아프다": {
643
+ "english": "wrenching",
644
+ "mcgill_dimension": "sensory"
645
+ },
646
+ "따끈하다": {
647
+ "english": "hot",
648
+ "mcgill_dimension": "sensory"
649
+ },
650
+ "물이나 불에 애듯이 아프다": {
651
+ "english": "scalding",
652
+ "mcgill_dimension": "sensory"
653
+ },
654
+ "불로 지지듯이 아프다": {
655
+ "english": "searing",
656
+ "mcgill_dimension": "sensory"
657
+ },
658
+ "근질근질하게 아프다": {
659
+ "english": "itchy",
660
+ "mcgill_dimension": "sensory"
661
+ },
662
+ "아리다": {
663
+ "english": "bitter",
664
+ "mcgill_dimension": "sensory"
665
+ },
666
+ "욱신거리다": {
667
+ "english": "smarting",
668
+ "mcgill_dimension": "sensory"
669
+ },
670
+ "멍하다": {
671
+ "english": "dull",
672
+ "mcgill_dimension": "sensory"
673
+ },
674
+ "우리하다": {
675
+ "english": "dull",
676
+ "mcgill_dimension": "sensory"
677
+ },
678
+ "둔하게 아프다": {
679
+ "english": "hurting",
680
+ "mcgill_dimension": "sensory"
681
+ },
682
+ "쑤신다": {
683
+ "english": "aching",
684
+ "mcgill_dimension": "sensory"
685
+ },
686
+ "빠개지듯 아프다": {
687
+ "english": "heavy",
688
+ "mcgill_dimension": "sensory"
689
+ },
690
+ "만지면 아프다": {
691
+ "english": "tender",
692
+ "mcgill_dimension": "sensory"
693
+ },
694
+ "누르면 아프다": {
695
+ "english": "tender",
696
+ "mcgill_dimension": "sensory"
697
+ },
698
+ "꽉 찬 것 같다": {
699
+ "english": "taut",
700
+ "mcgill_dimension": "sensory"
701
+ },
702
+ "갈아내듯이 아프다": {
703
+ "english": "rasping",
704
+ "mcgill_dimension": "sensory"
705
+ },
706
+ "터질듯이 아프다": {
707
+ "english": "splitting",
708
+ "mcgill_dimension": "sensory"
709
+ },
710
+ "살살 아프다": {
711
+ "english": "sickening",
712
+ "mcgill_dimension": "sensory"
713
+ },
714
+ "숨이 막힐듯 아프다": {
715
+ "english": "suffocating",
716
+ "mcgill_dimension": "sensory"
717
+ },
718
+ "겁나게 아프다": {
719
+ "english": "fearful",
720
+ "mcgill_dimension": "sensory"
721
+ },
722
+ "소름 끼치게 아프다": {
723
+ "english": "frightful",
724
+ "mcgill_dimension": "sensory"
725
+ },
726
+ "까무러치게 아프다": {
727
+ "english": "terrifying",
728
+ "mcgill_dimension": "sensory"
729
+ },
730
+ "쩔쩔매게 아프다": {
731
+ "english": "punishing",
732
+ "mcgill_dimension": "sensory"
733
+ },
734
+ "기진맥진하게 아프다": {
735
+ "english": "grueling",
736
+ "mcgill_dimension": "sensory"
737
+ },
738
+ "지독하게 아프다": {
739
+ "english": "dreadful",
740
+ "mcgill_dimension": "sensory"
741
+ },
742
+ "무지막하게 아프다": {
743
+ "english": "vicious",
744
+ "mcgill_dimension": "sensory"
745
+ },
746
+ "죽을 정도로 아프다": {
747
+ "english": "killing",
748
+ "mcgill_dimension": "sensory"
749
+ },
750
+ "고약하게 아프다": {
751
+ "english": "wretched",
752
+ "mcgill_dimension": "sensory"
753
+ },
754
+ "정신 못차리게 아프다": {
755
+ "english": "blinding",
756
+ "mcgill_dimension": "sensory"
757
+ },
758
+ "지속적으로 대단히 아프다": {
759
+ "english": "intense",
760
+ "mcgill_dimension": "sensory"
761
+ },
762
+ "참을수 없게 아프다": {
763
+ "english": "unberable",
764
+ "mcgill_dimension": "sensory"
765
+ },
766
+ "번져가면서 아프다": {
767
+ "english": "spreading",
768
+ "mcgill_dimension": "sensory"
769
+ },
770
+ "통증이 삐친다": {
771
+ "english": "radiating",
772
+ "mcgill_dimension": "sensory"
773
+ },
774
+ "관통하듯이 아프다": {
775
+ "english": "penetrating",
776
+ "mcgill_dimension": "sensory"
777
+ },
778
+ "조인다": {
779
+ "english": "tight",
780
+ "mcgill_dimension": "sensory"
781
+ },
782
+ "끌어당기듯이 아프다": {
783
+ "english": "drawing",
784
+ "mcgill_dimension": "sensory"
785
+ },
786
+ "쥐어짜듯이 아프다": {
787
+ "english": "squeezing",
788
+ "mcgill_dimension": "sensory"
789
+ },
790
+ "찢어지는듯 아프다": {
791
+ "english": "tearing",
792
+ "mcgill_dimension": "sensory"
793
+ },
794
+ "싸늘하게 아프다": {
795
+ "english": "cold",
796
+ "mcgill_dimension": "sensory"
797
+ },
798
+ "오싹하게 아프다": {
799
+ "english": "freezing",
800
+ "mcgill_dimension": "sensory"
801
+ },
802
+ "토할 정도로 아프다": {
803
+ "english": "nauseating",
804
+ "mcgill_dimension": "sensory"
805
+ },
806
+ "괴롭게 아프다": {
807
+ "english": "agonizing",
808
+ "mcgill_dimension": "sensory"
809
+ },
810
+ "고문 받는것 처럼 아프다": {
811
+ "english": "torturing",
812
+ "mcgill_dimension": "sensory"
813
+ }
814
+ },
815
+ "affective": {
816
+ "노곤하게 아프다": {
817
+ "english": "tiring",
818
+ "mcgill_dimension": "sensory"
819
+ },
820
+ "신경이 자꾸 쓰이게 아프다": {
821
+ "english": "annoying",
822
+ "mcgill_dimension": "sensory"
823
+ },
824
+ "난처하게 아프다": {
825
+ "english": "troublesome",
826
+ "mcgill_dimension": "sensory"
827
+ },
828
+ "괴롭게 아프다": {
829
+ "english": "miserable",
830
+ "mcgill_dimension": "sensory"
831
+ }
832
+ }
833
+ },
834
+ "spanish": {
835
+ "neuropathic": {
836
+ "de ardor": {
837
+ "english": "burning",
838
+ "mcgill_dimension": "sensory"
839
+ },
840
+ "quemadura": {
841
+ "english": "burning",
842
+ "mcgill_dimension": "sensory"
843
+ },
844
+ "quemazón": {
845
+ "english": "burning",
846
+ "mcgill_dimension": "sensory"
847
+ },
848
+ "adormecido": {
849
+ "english": "numb",
850
+ "mcgill_dimension": "sensory"
851
+ },
852
+ "perforante": {
853
+ "english": "piercing",
854
+ "mcgill_dimension": "sensory"
855
+ },
856
+ "pinchazo": {
857
+ "english": "pricking",
858
+ "mcgill_dimension": "sensory"
859
+ },
860
+ "agudo": {
861
+ "english": "sharp",
862
+ "mcgill_dimension": "sensory"
863
+ },
864
+ "expandirse": {
865
+ "english": "shooting",
866
+ "mcgill_dimension": "sensory"
867
+ },
868
+ "punzante": {
869
+ "english": "stabbing",
870
+ "mcgill_dimension": "sensory"
871
+ },
872
+ "picazón": {
873
+ "english": "stinging",
874
+ "mcgill_dimension": "sensory"
875
+ },
876
+ "hormigueo": {
877
+ "english": "tingling",
878
+ "mcgill_dimension": "sensory"
879
+ }
880
+ },
881
+ "nociceptive": {
882
+ "dolorido": {
883
+ "english": "aching",
884
+ "mcgill_dimension": "sensory"
885
+ },
886
+ "agudo": {
887
+ "english": "acute",
888
+ "mcgill_dimension": "sensory"
889
+ },
890
+ "anónico": {
891
+ "english": "agonizing",
892
+ "mcgill_dimension": "sensory"
893
+ },
894
+ "batiente": {
895
+ "english": "beating",
896
+ "mcgill_dimension": "sensory"
897
+ },
898
+ "agobiante": {
899
+ "english": "heavy",
900
+ "mcgill_dimension": "sensory"
901
+ },
902
+ "sofocante": {
903
+ "english": "suffocating",
904
+ "mcgill_dimension": "sensory"
905
+ },
906
+ "ceguera": {
907
+ "english": "blinding",
908
+ "mcgill_dimension": "sensory"
909
+ },
910
+ "terebrante": {
911
+ "english": "boring",
912
+ "mcgill_dimension": "sensory"
913
+ },
914
+ "breve": {
915
+ "english": "brief",
916
+ "mcgill_dimension": "sensory"
917
+ },
918
+ "quemadura": {
919
+ "english": "burn",
920
+ "mcgill_dimension": "sensory"
921
+ },
922
+ "crónico": {
923
+ "english": "chronic",
924
+ "mcgill_dimension": "sensory"
925
+ },
926
+ "helante": {
927
+ "english": "cold",
928
+ "mcgill_dimension": "sensory"
929
+ },
930
+ "constante": {
931
+ "english": "constant",
932
+ "mcgill_dimension": "sensory"
933
+ },
934
+ "frío": {
935
+ "english": "cool",
936
+ "mcgill_dimension": "sensory"
937
+ },
938
+ "calambre": {
939
+ "english": "cramp",
940
+ "mcgill_dimension": "sensory"
941
+ },
942
+ "retortijón": {
943
+ "english": "cramp",
944
+ "mcgill_dimension": "sensory"
945
+ },
946
+ "triturante": {
947
+ "english": "crushing",
948
+ "mcgill_dimension": "sensory"
949
+ },
950
+ "incisión": {
951
+ "english": "cut",
952
+ "mcgill_dimension": "sensory"
953
+ },
954
+ "cortante": {
955
+ "english": "cutting",
956
+ "mcgill_dimension": "sensory"
957
+ },
958
+ "de estiramiento": {
959
+ "english": "drawing",
960
+ "mcgill_dimension": "sensory"
961
+ },
962
+ "atemorizante": {
963
+ "english": "dreadful",
964
+ "mcgill_dimension": "sensory"
965
+ },
966
+ "taladrante": {
967
+ "english": "drilling",
968
+ "mcgill_dimension": "sensory"
969
+ },
970
+ "leve": {
971
+ "english": "dull",
972
+ "mcgill_dimension": "sensory"
973
+ },
974
+ "agotar": {
975
+ "english": "exhaust",
976
+ "mcgill_dimension": "sensory"
977
+ },
978
+ "dar miedo": {
979
+ "english": "fearful",
980
+ "mcgill_dimension": "sensory"
981
+ },
982
+ "intermitente": {
983
+ "english": "fitful",
984
+ "mcgill_dimension": "sensory"
985
+ },
986
+ "destello de": {
987
+ "english": "flickering",
988
+ "mcgill_dimension": "sensory"
989
+ },
990
+ "centelleante": {
991
+ "english": "flashing",
992
+ "mcgill_dimension": "sensory"
993
+ },
994
+ "congelante": {
995
+ "english": "freezing",
996
+ "mcgill_dimension": "sensory"
997
+ },
998
+ "alarmante": {
999
+ "english": "frightful",
1000
+ "mcgill_dimension": "sensory"
1001
+ },
1002
+ "lacerante": {
1003
+ "english": "lancinating",
1004
+ "mcgill_dimension": "sensory"
1005
+ },
1006
+ "mordicante": {
1007
+ "english": "gnawing",
1008
+ "mcgill_dimension": "sensory"
1009
+ },
1010
+ "agotador": {
1011
+ "english": "gruelling",
1012
+ "mcgill_dimension": "sensory"
1013
+ },
1014
+ "caliente": {
1015
+ "english": "hot",
1016
+ "mcgill_dimension": "sensory"
1017
+ },
1018
+ "doler": {
1019
+ "english": "to hurt",
1020
+ "mcgill_dimension": "sensory"
1021
+ },
1022
+ "intenso": {
1023
+ "english": "intense",
1024
+ "mcgill_dimension": "sensory"
1025
+ },
1026
+ "comezón": {
1027
+ "english": "itch",
1028
+ "mcgill_dimension": "sensory"
1029
+ },
1030
+ "picante": {
1031
+ "english": "itchy",
1032
+ "mcgill_dimension": "sensory"
1033
+ },
1034
+ "saltón": {
1035
+ "english": "jumping",
1036
+ "mcgill_dimension": "sensory"
1037
+ },
1038
+ "matar": {
1039
+ "english": "kill",
1040
+ "mcgill_dimension": "sensory"
1041
+ },
1042
+ "molesto": {
1043
+ "english": "nagging",
1044
+ "mcgill_dimension": "sensory"
1045
+ },
1046
+ "dar náuseas": {
1047
+ "english": "nauseating",
1048
+ "mcgill_dimension": "sensory"
1049
+ },
1050
+ "penetrante": {
1051
+ "english": "penetrating",
1052
+ "mcgill_dimension": "sensory"
1053
+ },
1054
+ "regular": {
1055
+ "english": "periodic",
1056
+ "mcgill_dimension": "sensory"
1057
+ },
1058
+ "periódico": {
1059
+ "english": "periodic",
1060
+ "mcgill_dimension": "sensory"
1061
+ },
1062
+ "pellizcante": {
1063
+ "english": "pinching",
1064
+ "mcgill_dimension": "sensory"
1065
+ },
1066
+ "latiendo": {
1067
+ "english": "pounding",
1068
+ "mcgill_dimension": "sensory"
1069
+ },
1070
+ "hacer presión sobre": {
1071
+ "english": "pressing",
1072
+ "mcgill_dimension": "sensory"
1073
+ },
1074
+ "tensar": {
1075
+ "english": "pulling",
1076
+ "mcgill_dimension": "sensory"
1077
+ },
1078
+ "palpitar": {
1079
+ "english": "pulsing",
1080
+ "mcgill_dimension": "sensory"
1081
+ },
1082
+ "temblando": {
1083
+ "english": "quivering",
1084
+ "mcgill_dimension": "sensory"
1085
+ },
1086
+ "irradiar": {
1087
+ "english": "radiating",
1088
+ "mcgill_dimension": "sensory"
1089
+ },
1090
+ "ronca": {
1091
+ "english": "raspy",
1092
+ "mcgill_dimension": "sensory"
1093
+ },
1094
+ "rítmico": {
1095
+ "english": "rhythmic",
1096
+ "mcgill_dimension": "sensory"
1097
+ },
1098
+ "hirviente": {
1099
+ "english": "scalding",
1100
+ "mcgill_dimension": "sensory"
1101
+ },
1102
+ "ardiente": {
1103
+ "english": "searing",
1104
+ "mcgill_dimension": "sensory"
1105
+ },
1106
+ "seco": {
1107
+ "english": "smarting",
1108
+ "mcgill_dimension": "sensory"
1109
+ },
1110
+ "adolorido": {
1111
+ "english": "sore",
1112
+ "mcgill_dimension": "sensory"
1113
+ },
1114
+ "severo": {
1115
+ "english": "splitting",
1116
+ "mcgill_dimension": "sensory"
1117
+ },
1118
+ "que se extiende": {
1119
+ "english": "spreading",
1120
+ "mcgill_dimension": "sensory"
1121
+ },
1122
+ "apretar": {
1123
+ "english": "squeezing",
1124
+ "mcgill_dimension": "sensory"
1125
+ },
1126
+ "tirante": {
1127
+ "english": "taut",
1128
+ "mcgill_dimension": "sensory"
1129
+ },
1130
+ "desgarrador": {
1131
+ "english": "wrenching",
1132
+ "mcgill_dimension": "sensory"
1133
+ },
1134
+ "sensible": {
1135
+ "english": "tender",
1136
+ "mcgill_dimension": "sensory"
1137
+ },
1138
+ "aterrador": {
1139
+ "english": "terrifying",
1140
+ "mcgill_dimension": "sensory"
1141
+ },
1142
+ "punzante": {
1143
+ "english": "throbbing",
1144
+ "mcgill_dimension": "sensory"
1145
+ },
1146
+ "firme": {
1147
+ "english": "unyielding",
1148
+ "mcgill_dimension": "sensory"
1149
+ },
1150
+ "cansarse": {
1151
+ "english": "to tire",
1152
+ "mcgill_dimension": "sensory"
1153
+ },
1154
+ "torturante": {
1155
+ "english": "torturing",
1156
+ "mcgill_dimension": "sensory"
1157
+ },
1158
+ "maligno": {
1159
+ "english": "vicious",
1160
+ "mcgill_dimension": "sensory"
1161
+ },
1162
+ "la cabeza": {
1163
+ "english": "Head",
1164
+ "mcgill_dimension": "sensory"
1165
+ },
1166
+ "el cuello": {
1167
+ "english": "Neck",
1168
+ "mcgill_dimension": "sensory"
1169
+ },
1170
+ "la cara": {
1171
+ "english": "Face",
1172
+ "mcgill_dimension": "sensory"
1173
+ },
1174
+ "las manos": {
1175
+ "english": "Hands",
1176
+ "mcgill_dimension": "sensory"
1177
+ },
1178
+ "los brazos": {
1179
+ "english": "Arms",
1180
+ "mcgill_dimension": "sensory"
1181
+ },
1182
+ "la espalda": {
1183
+ "english": "Back",
1184
+ "mcgill_dimension": "sensory"
1185
+ },
1186
+ "las piernas": {
1187
+ "english": "Legs",
1188
+ "mcgill_dimension": "sensory"
1189
+ },
1190
+ "los pies": {
1191
+ "english": "Feet",
1192
+ "mcgill_dimension": "sensory"
1193
+ },
1194
+ "el pecho": {
1195
+ "english": "Chest",
1196
+ "mcgill_dimension": "sensory"
1197
+ },
1198
+ "el abdomen": {
1199
+ "english": "Abdomen",
1200
+ "mcgill_dimension": "sensory"
1201
+ },
1202
+ "los pulmones": {
1203
+ "english": "lungs",
1204
+ "mcgill_dimension": "sensory"
1205
+ },
1206
+ "el corazón": {
1207
+ "english": "heart",
1208
+ "mcgill_dimension": "sensory"
1209
+ },
1210
+ "angustioso": {
1211
+ "english": "distressing",
1212
+ "mcgill_dimension": "sensory"
1213
+ },
1214
+ "escalofrio": {
1215
+ "english": "chill",
1216
+ "mcgill_dimension": "sensory"
1217
+ },
1218
+ "migraña": {
1219
+ "english": "Throbbing headache",
1220
+ "mcgill_dimension": "sensory"
1221
+ },
1222
+ "Nervios": {
1223
+ "english": "anxious",
1224
+ "mcgill_dimension": "sensory"
1225
+ },
1226
+ "susto": {
1227
+ "english": "anxiety",
1228
+ "mcgill_dimension": "sensory"
1229
+ },
1230
+ "tenso": {
1231
+ "english": "muscle tension",
1232
+ "mcgill_dimension": "sensory"
1233
+ },
1234
+ "irritarse": {
1235
+ "english": "irritated",
1236
+ "mcgill_dimension": "sensory"
1237
+ },
1238
+ "entumir": {
1239
+ "english": "Fall asleep",
1240
+ "mcgill_dimension": "sensory"
1241
+ }
1242
+ },
1243
+ "affective": {
1244
+ "fastidioso": {
1245
+ "english": "annoying",
1246
+ "mcgill_dimension": "sensory"
1247
+ },
1248
+ "atroz": {
1249
+ "english": "miserable",
1250
+ "mcgill_dimension": "sensory"
1251
+ },
1252
+ "pesado": {
1253
+ "english": "troublesome",
1254
+ "mcgill_dimension": "sensory"
1255
+ },
1256
+ "insoportable": {
1257
+ "english": "unbearable",
1258
+ "mcgill_dimension": "sensory"
1259
+ }
1260
+ }
1261
+ },
1262
+ "hmong": {
1263
+ "neuropathic": {
1264
+ "Plev": {
1265
+ "english": "stinging",
1266
+ "mcgill_dimension": "sensory"
1267
+ },
1268
+ "Muab Nkaug": {
1269
+ "english": "stabbing",
1270
+ "mcgill_dimension": "sensory"
1271
+ },
1272
+ "Kub Heev": {
1273
+ "english": "burning",
1274
+ "mcgill_dimension": "sensory"
1275
+ },
1276
+ "Loog": {
1277
+ "english": "numb",
1278
+ "mcgill_dimension": "sensory"
1279
+ },
1280
+ "Mob ntse": {
1281
+ "english": "picking and pricking",
1282
+ "mcgill_dimension": "sensory"
1283
+ },
1284
+ "Txais hluas taws xob": {
1285
+ "english": "receive an electric shock",
1286
+ "mcgill_dimension": "sensory"
1287
+ },
1288
+ "Nkaug": {
1289
+ "english": "stabbing",
1290
+ "mcgill_dimension": "sensory"
1291
+ },
1292
+ "Khaus": {
1293
+ "english": "tingling",
1294
+ "mcgill_dimension": "sensory"
1295
+ },
1296
+ "Mob": {
1297
+ "english": "stinging",
1298
+ "mcgill_dimension": "sensory"
1299
+ }
1300
+ },
1301
+ "nociceptive": {
1302
+ "Rub Leeg": {
1303
+ "english": "tugging",
1304
+ "mcgill_dimension": "sensory"
1305
+ },
1306
+ "leg": {
1307
+ "english": "twisted",
1308
+ "mcgill_dimension": "sensory"
1309
+ },
1310
+ "De": {
1311
+ "english": "pinching",
1312
+ "mcgill_dimension": "sensory"
1313
+ },
1314
+ "Ntuag": {
1315
+ "english": "tearing",
1316
+ "mcgill_dimension": "sensory"
1317
+ },
1318
+ "Raug Ntuag": {
1319
+ "english": "be torn",
1320
+ "mcgill_dimension": "sensory"
1321
+ },
1322
+ "Piam": {
1323
+ "english": "broken",
1324
+ "mcgill_dimension": "sensory"
1325
+ },
1326
+ "Khaus Khaus": {
1327
+ "english": "be scratched",
1328
+ "mcgill_dimension": "sensory"
1329
+ },
1330
+ "Tig": {
1331
+ "english": "wriggle,",
1332
+ "mcgill_dimension": "sensory"
1333
+ },
1334
+ "Obtuse": {
1335
+ "english": "obtuse",
1336
+ "mcgill_dimension": "sensory"
1337
+ },
1338
+ "Tawm": {
1339
+ "english": "sticking out",
1340
+ "mcgill_dimension": "sensory"
1341
+ },
1342
+ "Plev": {
1343
+ "english": "get stung",
1344
+ "mcgill_dimension": "sensory"
1345
+ },
1346
+ "Txias": {
1347
+ "english": "feel a chill",
1348
+ "mcgill_dimension": "sensory"
1349
+ },
1350
+ "Tawg": {
1351
+ "english": "cracking",
1352
+ "mcgill_dimension": "sensory"
1353
+ },
1354
+ "Nkau": {
1355
+ "english": "penetrating",
1356
+ "mcgill_dimension": "sensory"
1357
+ },
1358
+ "Cheeb yob tsis tau": {
1359
+ "english": "having a convulsive fit",
1360
+ "mcgill_dimension": "sensory"
1361
+ },
1362
+ "Hnyav": {
1363
+ "english": "feel heavy",
1364
+ "mcgill_dimension": "sensory"
1365
+ },
1366
+ "Nyob qis tab sis kho": {
1367
+ "english": "be low but steady",
1368
+ "mcgill_dimension": "sensory"
1369
+ },
1370
+ "Nqaij Khov": {
1371
+ "english": "feels like frostbite",
1372
+ "mcgill_dimension": "sensory"
1373
+ },
1374
+ "Hnov nqaij khaus ntawm ib cheem tsam": {
1375
+ "english": "tingle, feel a twinge in the area",
1376
+ "mcgill_dimension": "sensory"
1377
+ },
1378
+ "Mob, Loog": {
1379
+ "english": "aching, throbbing",
1380
+ "mcgill_dimension": "sensory"
1381
+ },
1382
+ "Muab tswj": {
1383
+ "english": "be twisted",
1384
+ "mcgill_dimension": "sensory"
1385
+ },
1386
+ "Tsw muaj zog": {
1387
+ "english": "pungent",
1388
+ "mcgill_dimension": "sensory"
1389
+ },
1390
+ "Mob": {
1391
+ "english": "throbbing",
1392
+ "mcgill_dimension": "sensory"
1393
+ },
1394
+ "Tev": {
1395
+ "english": "peeling",
1396
+ "mcgill_dimension": "sensory"
1397
+ },
1398
+ "Pleb": {
1399
+ "english": "cracking",
1400
+ "mcgill_dimension": "sensory"
1401
+ },
1402
+ "Nyob ruaj khov": {
1403
+ "english": "be ground",
1404
+ "mcgill_dimension": "sensory"
1405
+ },
1406
+ "Muab Tswj": {
1407
+ "english": "be twisted",
1408
+ "mcgill_dimension": "sensory"
1409
+ },
1410
+ "Txawv": {
1411
+ "english": "out of place",
1412
+ "mcgill_dimension": "sensory"
1413
+ },
1414
+ "Ntog": {
1415
+ "english": "falling out,",
1416
+ "mcgill_dimension": "sensory"
1417
+ },
1418
+ "Suab Tawg": {
1419
+ "english": "creaking",
1420
+ "mcgill_dimension": "sensory"
1421
+ },
1422
+ "Chob": {
1423
+ "english": "poking",
1424
+ "mcgill_dimension": "sensory"
1425
+ },
1426
+ "ci ntsa iab": {
1427
+ "english": "flickering",
1428
+ "mcgill_dimension": "sensory"
1429
+ },
1430
+ "Tshee": {
1431
+ "english": "quivering",
1432
+ "mcgill_dimension": "sensory"
1433
+ },
1434
+ "Co heev": {
1435
+ "english": "pulsing",
1436
+ "mcgill_dimension": "sensory"
1437
+ },
1438
+ "Ntaus": {
1439
+ "english": "pounding",
1440
+ "mcgill_dimension": "sensory"
1441
+ },
1442
+ "Teeb ntsai": {
1443
+ "english": "flashing",
1444
+ "mcgill_dimension": "sensory"
1445
+ },
1446
+ "La la Li": {
1447
+ "english": "boring",
1448
+ "mcgill_dimension": "sensory"
1449
+ },
1450
+ "Tho": {
1451
+ "english": "drilling",
1452
+ "mcgill_dimension": "sensory"
1453
+ },
1454
+ "Nkaug": {
1455
+ "english": "lancinating",
1456
+ "mcgill_dimension": "sensory"
1457
+ },
1458
+ "Hlai": {
1459
+ "english": "lacerating",
1460
+ "mcgill_dimension": "sensory"
1461
+ },
1462
+ "Tom": {
1463
+ "english": "gnawing",
1464
+ "mcgill_dimension": "sensory"
1465
+ },
1466
+ "Mob qaib": {
1467
+ "english": "cramping",
1468
+ "mcgill_dimension": "sensory"
1469
+ },
1470
+ "Tswj": {
1471
+ "english": "wrenching",
1472
+ "mcgill_dimension": "sensory"
1473
+ },
1474
+ "Kub Heev": {
1475
+ "english": "scalding",
1476
+ "mcgill_dimension": "sensory"
1477
+ },
1478
+ "Hlawv": {
1479
+ "english": "searing",
1480
+ "mcgill_dimension": "sensory"
1481
+ },
1482
+ "Mos Mos": {
1483
+ "english": "tender",
1484
+ "mcgill_dimension": "sensory"
1485
+ },
1486
+ "Rub nruj": {
1487
+ "english": "taut",
1488
+ "mcgill_dimension": "sensory"
1489
+ },
1490
+ "Ua pa hnyav": {
1491
+ "english": "rasping",
1492
+ "mcgill_dimension": "sensory"
1493
+ },
1494
+ "Mob heev": {
1495
+ "english": "sickening",
1496
+ "mcgill_dimension": "sensory"
1497
+ },
1498
+ "Tsim Txos": {
1499
+ "english": "grueling",
1500
+ "mcgill_dimension": "sensory"
1501
+ },
1502
+ "Hnyav heev": {
1503
+ "english": "intense",
1504
+ "mcgill_dimension": "sensory"
1505
+ },
1506
+ "Kis log Tuag": {
1507
+ "english": "radiating",
1508
+ "mcgill_dimension": "sensory"
1509
+ },
1510
+ "Rub": {
1511
+ "english": "drawing",
1512
+ "mcgill_dimension": "sensory"
1513
+ },
1514
+ "Thab": {
1515
+ "english": "nagging",
1516
+ "mcgill_dimension": "sensory"
1517
+ },
1518
+ "Tsis zoo siab": {
1519
+ "english": "dreadful",
1520
+ "mcgill_dimension": "sensory"
1521
+ }
1522
+ },
1523
+ "affective": {}
1524
+ }
1525
+ }
Backend/scripts/pain_descriptors_formatted.py ADDED
@@ -0,0 +1,1533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # CHINESE Pain Descriptors
3
+ CHINESE_PAIN_DESCRIPTORS = {
4
+ "neuropathic": {
5
+ "火辣辣的疼": {
6
+ "english": "burning",
7
+ "mcgill_dimension": "sensory"
8
+ },
9
+ "麻的": {
10
+ "english": "numb",
11
+ "mcgill_dimension": "sensory"
12
+ },
13
+ "刺骨痛": {
14
+ "english": "piercing",
15
+ "mcgill_dimension": "sensory"
16
+ },
17
+ "刺痛": {
18
+ "english": "tingling",
19
+ "mcgill_dimension": "sensory"
20
+ },
21
+ "剧烈的疼": {
22
+ "english": "sharp",
23
+ "mcgill_dimension": "sensory"
24
+ },
25
+ "蚊虫叮咬的刺疼": {
26
+ "english": "stinging",
27
+ "mcgill_dimension": "sensory"
28
+ },
29
+ },
30
+ "nociceptive": {
31
+ "疼": {
32
+ "english": "aching",
33
+ "mcgill_dimension": "sensory"
34
+ },
35
+ "急性的": {
36
+ "english": "acute",
37
+ "mcgill_dimension": "sensory"
38
+ },
39
+ "极度的疼痛": {
40
+ "english": "agonizing",
41
+ "mcgill_dimension": "sensory"
42
+ },
43
+ "跳动的痛": {
44
+ "english": "beating",
45
+ "mcgill_dimension": "sensory"
46
+ },
47
+ "强烈的疼痛": {
48
+ "english": "blinding",
49
+ "mcgill_dimension": "sensory"
50
+ },
51
+ "被刺穿的疼": {
52
+ "english": "boring",
53
+ "mcgill_dimension": "sensory"
54
+ },
55
+ "短暂的": {
56
+ "english": "brief",
57
+ "mcgill_dimension": "sensory"
58
+ },
59
+ "慢性的": {
60
+ "english": "chronic",
61
+ "mcgill_dimension": "sensory"
62
+ },
63
+ "冷痛": {
64
+ "english": "cold",
65
+ "mcgill_dimension": "sensory"
66
+ },
67
+ "不间断的": {
68
+ "english": "constant",
69
+ "mcgill_dimension": "sensory"
70
+ },
71
+ "冷的": {
72
+ "english": "cool",
73
+ "mcgill_dimension": "sensory"
74
+ },
75
+ "绞痛": {
76
+ "english": "cramp",
77
+ "mcgill_dimension": "sensory"
78
+ },
79
+ "压迫痛": {
80
+ "english": "crushing",
81
+ "mcgill_dimension": "sensory"
82
+ },
83
+ "切割痛": {
84
+ "english": "cutting",
85
+ "mcgill_dimension": "sensory"
86
+ },
87
+ "拉扯痛": {
88
+ "english": "pulling",
89
+ "mcgill_dimension": "sensory"
90
+ },
91
+ "可怕的痛苦": {
92
+ "english": "dreadful",
93
+ "mcgill_dimension": "sensory"
94
+ },
95
+ "钻痛": {
96
+ "english": "drilling",
97
+ "mcgill_dimension": "sensory"
98
+ },
99
+ "隐约的疼痛": {
100
+ "english": "dull",
101
+ "mcgill_dimension": "sensory"
102
+ },
103
+ "疼到没力气": {
104
+ "english": "exhaust",
105
+ "mcgill_dimension": "sensory"
106
+ },
107
+ "可怕的痛": {
108
+ "english": "fearful",
109
+ "mcgill_dimension": "sensory"
110
+ },
111
+ "一阵阵的": {
112
+ "english": "fitful",
113
+ "mcgill_dimension": "sensory"
114
+ },
115
+ "一闪而过的痛": {
116
+ "english": "flashing",
117
+ "mcgill_dimension": "sensory"
118
+ },
119
+ "闪烁的痛": {
120
+ "english": "Flickering",
121
+ "mcgill_dimension": "sensory"
122
+ },
123
+ "冷疼": {
124
+ "english": "freezing",
125
+ "mcgill_dimension": "sensory"
126
+ },
127
+ "疼的可怕": {
128
+ "english": "frightful",
129
+ "mcgill_dimension": "sensory"
130
+ },
131
+ "折磨的痛": {
132
+ "english": "gnawing",
133
+ "mcgill_dimension": "sensory"
134
+ },
135
+ "折磨人的": {
136
+ "english": "gruelling",
137
+ "mcgill_dimension": "sensory"
138
+ },
139
+ "非常痛": {
140
+ "english": "heavy",
141
+ "mcgill_dimension": "sensory"
142
+ },
143
+ "热的": {
144
+ "english": "hot",
145
+ "mcgill_dimension": "sensory"
146
+ },
147
+ "使...难受": {
148
+ "english": "hurt",
149
+ "mcgill_dimension": "sensory"
150
+ },
151
+ "强烈的": {
152
+ "english": "intense",
153
+ "mcgill_dimension": "sensory"
154
+ },
155
+ "痒的": {
156
+ "english": "itchy",
157
+ "mcgill_dimension": "sensory"
158
+ },
159
+ "跳动的疼": {
160
+ "english": "jumping",
161
+ "mcgill_dimension": "sensory"
162
+ },
163
+ "及其": {
164
+ "english": "to kill",
165
+ "mcgill_dimension": "sensory"
166
+ },
167
+ "撕裂痛": {
168
+ "english": "lacerating",
169
+ "mcgill_dimension": "sensory"
170
+ },
171
+ "撕裂的痛": {
172
+ "english": "lancinating",
173
+ "mcgill_dimension": "sensory"
174
+ },
175
+ "使人不得安宁": {
176
+ "english": "nagging",
177
+ "mcgill_dimension": "sensory"
178
+ },
179
+ "钻心的": {
180
+ "english": "nauseating",
181
+ "mcgill_dimension": "sensory"
182
+ },
183
+ "渗透的": {
184
+ "english": "penetrating",
185
+ "mcgill_dimension": "sensory"
186
+ },
187
+ "一阵一阵的痛": {
188
+ "english": "periodic",
189
+ "mcgill_dimension": "sensory"
190
+ },
191
+ "掐疼": {
192
+ "english": "pinching",
193
+ "mcgill_dimension": "sensory"
194
+ },
195
+ "重击痛": {
196
+ "english": "pounding",
197
+ "mcgill_dimension": "sensory"
198
+ },
199
+ "压着痛": {
200
+ "english": "pressing",
201
+ "mcgill_dimension": "sensory"
202
+ },
203
+ "搏动性痛": {
204
+ "english": "pulsing",
205
+ "mcgill_dimension": "sensory"
206
+ },
207
+ "颤抖": {
208
+ "english": "quivering",
209
+ "mcgill_dimension": "sensory"
210
+ },
211
+ "发散性疼痛": {
212
+ "english": "radiating",
213
+ "mcgill_dimension": "sensory"
214
+ },
215
+ "粗糙的": {
216
+ "english": "raspy",
217
+ "mcgill_dimension": "sensory"
218
+ },
219
+ "有节奏的": {
220
+ "english": "rhythmic",
221
+ "mcgill_dimension": "sensory"
222
+ },
223
+ "烫伤": {
224
+ "english": "scalding",
225
+ "mcgill_dimension": "sensory"
226
+ },
227
+ "灼痛": {
228
+ "english": "searing",
229
+ "mcgill_dimension": "sensory"
230
+ },
231
+ "剧烈疼痛": {
232
+ "english": "smarting",
233
+ "mcgill_dimension": "sensory"
234
+ },
235
+ "酸痛": {
236
+ "english": "sore",
237
+ "mcgill_dimension": "sensory"
238
+ },
239
+ "分裂痛": {
240
+ "english": "splitting",
241
+ "mcgill_dimension": "sensory"
242
+ },
243
+ "扩散性疼痛": {
244
+ "english": "spreading",
245
+ "mcgill_dimension": "sensory"
246
+ },
247
+ "挤压的疼痛": {
248
+ "english": "squeezing",
249
+ "mcgill_dimension": "sensory"
250
+ },
251
+ "令人窒息的": {
252
+ "english": "suffocating",
253
+ "mcgill_dimension": "sensory"
254
+ },
255
+ "紧张的": {
256
+ "english": "taut",
257
+ "mcgill_dimension": "sensory"
258
+ },
259
+ "撕裂的": {
260
+ "english": "tearing",
261
+ "mcgill_dimension": "sensory"
262
+ },
263
+ "一碰就痛": {
264
+ "english": "tender",
265
+ "mcgill_dimension": "sensory"
266
+ },
267
+ "程度很高的痛苦": {
268
+ "english": "terrifying",
269
+ "mcgill_dimension": "sensory"
270
+ },
271
+ "一抽一抽的痛": {
272
+ "english": "throbbing",
273
+ "mcgill_dimension": "sensory"
274
+ },
275
+ "顽固的": {
276
+ "english": "unyielding.",
277
+ "mcgill_dimension": "sensory"
278
+ },
279
+ "疲倦": {
280
+ "english": "to tire",
281
+ "mcgill_dimension": "sensory"
282
+ },
283
+ "折磨": {
284
+ "english": "torturing",
285
+ "mcgill_dimension": "sensory"
286
+ },
287
+ "剧烈的痛苦": {
288
+ "english": "vicious",
289
+ "mcgill_dimension": "sensory"
290
+ },
291
+ "极为痛苦的": {
292
+ "english": "wrenching",
293
+ "mcgill_dimension": "sensory"
294
+ },
295
+ "头": {
296
+ "english": "Head",
297
+ "mcgill_dimension": "sensory"
298
+ },
299
+ "脖子": {
300
+ "english": "Neck",
301
+ "mcgill_dimension": "sensory"
302
+ },
303
+ "脸": {
304
+ "english": "Face",
305
+ "mcgill_dimension": "sensory"
306
+ },
307
+ "手": {
308
+ "english": "Hands",
309
+ "mcgill_dimension": "sensory"
310
+ },
311
+ "手臂": {
312
+ "english": "Arms",
313
+ "mcgill_dimension": "sensory"
314
+ },
315
+ "背": {
316
+ "english": "Back",
317
+ "mcgill_dimension": "sensory"
318
+ },
319
+ "腿": {
320
+ "english": "Legs",
321
+ "mcgill_dimension": "sensory"
322
+ },
323
+ "脚": {
324
+ "english": "Feet",
325
+ "mcgill_dimension": "sensory"
326
+ },
327
+ "胸腔": {
328
+ "english": "Chest",
329
+ "mcgill_dimension": "sensory"
330
+ },
331
+ "腹部": {
332
+ "english": "Abdomen",
333
+ "mcgill_dimension": "sensory"
334
+ },
335
+ "肺": {
336
+ "english": "lungs",
337
+ "mcgill_dimension": "sensory"
338
+ },
339
+ "心脏": {
340
+ "english": "heart",
341
+ "mcgill_dimension": "sensory"
342
+ },
343
+ },
344
+ "affective": {
345
+ "烦人的": {
346
+ "english": "annoying",
347
+ "mcgill_dimension": "sensory"
348
+ },
349
+ "痛苦": {
350
+ "english": "miserable",
351
+ "mcgill_dimension": "sensory"
352
+ },
353
+ "麻烦的": {
354
+ "english": "troublesome",
355
+ "mcgill_dimension": "sensory"
356
+ },
357
+ "难以忍受的": {
358
+ "english": "unbearable",
359
+ "mcgill_dimension": "sensory"
360
+ },
361
+ },
362
+ }
363
+
364
+
365
+ # KOREAN Pain Descriptors
366
+ KOREAN_PAIN_DESCRIPTORS = {
367
+ "neuropathic": {
368
+ "따끔거리다": {
369
+ "english": "sting",
370
+ "mcgill_dimension": "sensory"
371
+ },
372
+ "찌르다": {
373
+ "english": "stabbing",
374
+ "mcgill_dimension": "sensory"
375
+ },
376
+ "타는것 같다": {
377
+ "english": "burning",
378
+ "mcgill_dimension": "sensory"
379
+ },
380
+ "아리다": {
381
+ "english": "stinging",
382
+ "mcgill_dimension": "sensory"
383
+ },
384
+ "얼얼하다": {
385
+ "english": "numb",
386
+ "mcgill_dimension": "sensory"
387
+ },
388
+ "쏘듯이 아프다": {
389
+ "english": "shooting",
390
+ "mcgill_dimension": "sensory"
391
+ },
392
+ "바늘로 찌르듯": {
393
+ "english": "pricking",
394
+ "mcgill_dimension": "sensory"
395
+ },
396
+ "칼로 찌르듯": {
397
+ "english": "stabbing",
398
+ "mcgill_dimension": "sensory"
399
+ },
400
+ "쓰라리다": {
401
+ "english": "sharp",
402
+ "mcgill_dimension": "sensory"
403
+ },
404
+ "화끈거리다": {
405
+ "english": "burning",
406
+ "mcgill_dimension": "sensory"
407
+ },
408
+ "서물서물하다": {
409
+ "english": "tingling",
410
+ "mcgill_dimension": "sensory"
411
+ },
412
+ "톡 쏘듯이 아프다": {
413
+ "english": "stinging",
414
+ "mcgill_dimension": "sensory"
415
+ },
416
+ "지치게 아프다": {
417
+ "english": "exhausting",
418
+ "mcgill_dimension": "sensory"
419
+ },
420
+ "뼈를 쳐미듯이 아프다": {
421
+ "english": "piercing",
422
+ "mcgill_dimension": "sensory"
423
+ },
424
+ "저리다": {
425
+ "english": "numb",
426
+ "mcgill_dimension": "sensory"
427
+ },
428
+ },
429
+ "nociceptive": {
430
+ "꼬집히다": {
431
+ "english": "pinched",
432
+ "mcgill_dimension": "sensory"
433
+ },
434
+ "뻐근하다": {
435
+ "english": "stiff",
436
+ "mcgill_dimension": "sensory"
437
+ },
438
+ "조이다": {
439
+ "english": "constricting",
440
+ "mcgill_dimension": "sensory"
441
+ },
442
+ "찢어지다": {
443
+ "english": "torn",
444
+ "mcgill_dimension": "sensory"
445
+ },
446
+ "터지다": {
447
+ "english": "broken",
448
+ "mcgill_dimension": "sensory"
449
+ },
450
+ "팽팽하다": {
451
+ "english": "tight",
452
+ "mcgill_dimension": "sensory"
453
+ },
454
+ "긁히다": {
455
+ "english": "scrape",
456
+ "mcgill_dimension": "sensory"
457
+ },
458
+ "꿈틀거리다": {
459
+ "english": "wriggle",
460
+ "mcgill_dimension": "sensory"
461
+ },
462
+ "둔하다": {
463
+ "english": "obtuse",
464
+ "mcgill_dimension": "sensory"
465
+ },
466
+ "무디다": {
467
+ "english": "dull",
468
+ "mcgill_dimension": "sensory"
469
+ },
470
+ "뻗치다": {
471
+ "english": "sticking out",
472
+ "mcgill_dimension": "sensory"
473
+ },
474
+ "쐬다": {
475
+ "english": "get stung",
476
+ "mcgill_dimension": "sensory"
477
+ },
478
+ "오싹하다": {
479
+ "english": "feel a chill",
480
+ "mcgill_dimension": "sensory"
481
+ },
482
+ "지지다": {
483
+ "english": "frying",
484
+ "mcgill_dimension": "sensory"
485
+ },
486
+ "화끈거리다": {
487
+ "english": "hot",
488
+ "mcgill_dimension": "sensory"
489
+ },
490
+ "부딪히다": {
491
+ "english": "hitting",
492
+ "mcgill_dimension": "sensory"
493
+ },
494
+ "쓰라리다": {
495
+ "english": "sore",
496
+ "mcgill_dimension": "sensory"
497
+ },
498
+ "으스러지다": {
499
+ "english": "be shattered",
500
+ "mcgill_dimension": "sensory"
501
+ },
502
+ "끊어지다": {
503
+ "english": "cut",
504
+ "mcgill_dimension": "sensory"
505
+ },
506
+ "살을 에이는 듯한 아픔.": {
507
+ "english": "penetrating",
508
+ "mcgill_dimension": "sensory"
509
+ },
510
+ "울리다": {
511
+ "english": "ringing",
512
+ "mcgill_dimension": "sensory"
513
+ },
514
+ "쪼개지다": {
515
+ "english": "splitting",
516
+ "mcgill_dimension": "sensory"
517
+ },
518
+ "시리다": {
519
+ "english": "cool",
520
+ "mcgill_dimension": "sensory"
521
+ },
522
+ "부서지다": {
523
+ "english": "smashing",
524
+ "mcgill_dimension": "sensory"
525
+ },
526
+ "싸하다": {
527
+ "english": "pungent",
528
+ "mcgill_dimension": "sensory"
529
+ },
530
+ "잘리다": {
531
+ "english": "be chopped",
532
+ "mcgill_dimension": "sensory"
533
+ },
534
+ "찌릿하다": {
535
+ "english": "throbbing",
536
+ "mcgill_dimension": "sensory"
537
+ },
538
+ "깎이다": {
539
+ "english": "peeling",
540
+ "mcgill_dimension": "sensory"
541
+ },
542
+ "깨지다": {
543
+ "english": "cracking",
544
+ "mcgill_dimension": "sensory"
545
+ },
546
+ "갈리다": {
547
+ "english": "be ground",
548
+ "mcgill_dimension": "sensory"
549
+ },
550
+ "비틀리다": {
551
+ "english": "be twisted",
552
+ "mcgill_dimension": "sensory"
553
+ },
554
+ "빠지다": {
555
+ "english": "falling out,",
556
+ "mcgill_dimension": "sensory"
557
+ },
558
+ "뻣뻣하다": {
559
+ "english": "stiff",
560
+ "mcgill_dimension": "sensory"
561
+ },
562
+ "삐드득": {
563
+ "english": "creaking",
564
+ "mcgill_dimension": "sensory"
565
+ },
566
+ "쑤시다": {
567
+ "english": "poking",
568
+ "mcgill_dimension": "sensory"
569
+ },
570
+ "가물가물 아프다": {
571
+ "english": "flickering",
572
+ "mcgill_dimension": "sensory"
573
+ },
574
+ "지근거리다": {
575
+ "english": "nagging",
576
+ "mcgill_dimension": "sensory"
577
+ },
578
+ "욱신욱신하다": {
579
+ "english": "pulsing",
580
+ "mcgill_dimension": "sensory"
581
+ },
582
+ "들먹거리다": {
583
+ "english": "beating",
584
+ "mcgill_dimension": "sensory"
585
+ },
586
+ "쾅쾅치듯이 아프다": {
587
+ "english": "pounding",
588
+ "mcgill_dimension": "sensory"
589
+ },
590
+ "움찔하게 아프다": {
591
+ "english": "jumping",
592
+ "mcgill_dimension": "sensory"
593
+ },
594
+ "따끔하다": {
595
+ "english": "flashing",
596
+ "mcgill_dimension": "sensory"
597
+ },
598
+ "송곳으로 찌르듯": {
599
+ "english": "boring",
600
+ "mcgill_dimension": "sensory"
601
+ },
602
+ "구멍을 뚫듯이": {
603
+ "english": "drilling",
604
+ "mcgill_dimension": "sensory"
605
+ },
606
+ "칼로 찔러 쑤시듯": {
607
+ "english": "lancinating",
608
+ "mcgill_dimension": "sensory"
609
+ },
610
+ "베듯이 아프다": {
611
+ "english": "cutting",
612
+ "mcgill_dimension": "sensory"
613
+ },
614
+ "도려내듯 아프다": {
615
+ "english": "lacerating",
616
+ "mcgill_dimension": "sensory"
617
+ },
618
+ "꼬집듯 따끔하다": {
619
+ "english": "pinching",
620
+ "mcgill_dimension": "sensory"
621
+ },
622
+ "누르듯 아프다": {
623
+ "english": "pressing",
624
+ "mcgill_dimension": "sensory"
625
+ },
626
+ "꽉 무는듯 아프다": {
627
+ "english": "gnawing",
628
+ "mcgill_dimension": "sensory"
629
+ },
630
+ "꽉 지는듯 아프다": {
631
+ "english": "cramping",
632
+ "mcgill_dimension": "sensory"
633
+ },
634
+ "짓이기는 듯 아프다": {
635
+ "english": "crushing",
636
+ "mcgill_dimension": "sensory"
637
+ },
638
+ "결린다": {
639
+ "english": "tugging",
640
+ "mcgill_dimension": "sensory"
641
+ },
642
+ "땅긴다": {
643
+ "english": "pulling",
644
+ "mcgill_dimension": "sensory"
645
+ },
646
+ "뒤틀리듯 아프다": {
647
+ "english": "wrenching",
648
+ "mcgill_dimension": "sensory"
649
+ },
650
+ "따끈하다": {
651
+ "english": "hot",
652
+ "mcgill_dimension": "sensory"
653
+ },
654
+ "물이나 불에 애듯이 아프다": {
655
+ "english": "scalding",
656
+ "mcgill_dimension": "sensory"
657
+ },
658
+ "불로 지지듯이 아프다": {
659
+ "english": "searing",
660
+ "mcgill_dimension": "sensory"
661
+ },
662
+ "근질근질하게 아프다": {
663
+ "english": "itchy",
664
+ "mcgill_dimension": "sensory"
665
+ },
666
+ "아리다": {
667
+ "english": "bitter",
668
+ "mcgill_dimension": "sensory"
669
+ },
670
+ "욱신거리다": {
671
+ "english": "smarting",
672
+ "mcgill_dimension": "sensory"
673
+ },
674
+ "멍하다": {
675
+ "english": "dull",
676
+ "mcgill_dimension": "sensory"
677
+ },
678
+ "우리하다": {
679
+ "english": "dull",
680
+ "mcgill_dimension": "sensory"
681
+ },
682
+ "둔하게 아프다": {
683
+ "english": "hurting",
684
+ "mcgill_dimension": "sensory"
685
+ },
686
+ "쑤신다": {
687
+ "english": "aching",
688
+ "mcgill_dimension": "sensory"
689
+ },
690
+ "빠개지듯 아프다": {
691
+ "english": "heavy",
692
+ "mcgill_dimension": "sensory"
693
+ },
694
+ "만지면 아프다": {
695
+ "english": "tender",
696
+ "mcgill_dimension": "sensory"
697
+ },
698
+ "누르면 아프다": {
699
+ "english": "tender",
700
+ "mcgill_dimension": "sensory"
701
+ },
702
+ "꽉 찬 것 같다": {
703
+ "english": "taut",
704
+ "mcgill_dimension": "sensory"
705
+ },
706
+ "갈아내듯이 아프다": {
707
+ "english": "rasping",
708
+ "mcgill_dimension": "sensory"
709
+ },
710
+ "터질듯이 아프다": {
711
+ "english": "splitting",
712
+ "mcgill_dimension": "sensory"
713
+ },
714
+ "살살 아프다": {
715
+ "english": "sickening",
716
+ "mcgill_dimension": "sensory"
717
+ },
718
+ "숨이 막힐듯 아프다": {
719
+ "english": "suffocating",
720
+ "mcgill_dimension": "sensory"
721
+ },
722
+ "겁나게 아프다": {
723
+ "english": "fearful",
724
+ "mcgill_dimension": "sensory"
725
+ },
726
+ "소름 끼치게 아프다": {
727
+ "english": "frightful",
728
+ "mcgill_dimension": "sensory"
729
+ },
730
+ "까무러치게 아프다": {
731
+ "english": "terrifying",
732
+ "mcgill_dimension": "sensory"
733
+ },
734
+ "쩔쩔매게 아프다": {
735
+ "english": "punishing",
736
+ "mcgill_dimension": "sensory"
737
+ },
738
+ "기진맥진하게 아프다": {
739
+ "english": "grueling",
740
+ "mcgill_dimension": "sensory"
741
+ },
742
+ "지독하게 아프다": {
743
+ "english": "dreadful",
744
+ "mcgill_dimension": "sensory"
745
+ },
746
+ "무지막하게 아프다": {
747
+ "english": "vicious",
748
+ "mcgill_dimension": "sensory"
749
+ },
750
+ "죽을 정도로 아프다": {
751
+ "english": "killing",
752
+ "mcgill_dimension": "sensory"
753
+ },
754
+ "고약하게 아프다": {
755
+ "english": "wretched",
756
+ "mcgill_dimension": "sensory"
757
+ },
758
+ "정신 못차리게 아프다": {
759
+ "english": "blinding",
760
+ "mcgill_dimension": "sensory"
761
+ },
762
+ "지속적으로 대단히 아프다": {
763
+ "english": "intense",
764
+ "mcgill_dimension": "sensory"
765
+ },
766
+ "참을수 없게 아프다": {
767
+ "english": "unberable",
768
+ "mcgill_dimension": "sensory"
769
+ },
770
+ "번져가면서 아프다": {
771
+ "english": "spreading",
772
+ "mcgill_dimension": "sensory"
773
+ },
774
+ "통증이 삐친다": {
775
+ "english": "radiating",
776
+ "mcgill_dimension": "sensory"
777
+ },
778
+ "관통하듯이 아프다": {
779
+ "english": "penetrating",
780
+ "mcgill_dimension": "sensory"
781
+ },
782
+ "조인다": {
783
+ "english": "tight",
784
+ "mcgill_dimension": "sensory"
785
+ },
786
+ "끌어당기듯이 아프다": {
787
+ "english": "drawing",
788
+ "mcgill_dimension": "sensory"
789
+ },
790
+ "쥐어짜듯이 아프다": {
791
+ "english": "squeezing",
792
+ "mcgill_dimension": "sensory"
793
+ },
794
+ "찢어지는듯 아프다": {
795
+ "english": "tearing",
796
+ "mcgill_dimension": "sensory"
797
+ },
798
+ "싸늘하게 아프다": {
799
+ "english": "cold",
800
+ "mcgill_dimension": "sensory"
801
+ },
802
+ "오싹하게 아프다": {
803
+ "english": "freezing",
804
+ "mcgill_dimension": "sensory"
805
+ },
806
+ "토할 정도로 아프다": {
807
+ "english": "nauseating",
808
+ "mcgill_dimension": "sensory"
809
+ },
810
+ "괴롭게 아프다": {
811
+ "english": "agonizing",
812
+ "mcgill_dimension": "sensory"
813
+ },
814
+ "고문 받는것 처럼 아프다": {
815
+ "english": "torturing",
816
+ "mcgill_dimension": "sensory"
817
+ },
818
+ },
819
+ "affective": {
820
+ "노곤하게 아프다": {
821
+ "english": "tiring",
822
+ "mcgill_dimension": "sensory"
823
+ },
824
+ "신경이 자꾸 쓰이게 아프다": {
825
+ "english": "annoying",
826
+ "mcgill_dimension": "sensory"
827
+ },
828
+ "난처하게 아프다": {
829
+ "english": "troublesome",
830
+ "mcgill_dimension": "sensory"
831
+ },
832
+ "괴롭게 아프다": {
833
+ "english": "miserable",
834
+ "mcgill_dimension": "sensory"
835
+ },
836
+ },
837
+ }
838
+
839
+
840
+ # SPANISH Pain Descriptors
841
+ SPANISH_PAIN_DESCRIPTORS = {
842
+ "neuropathic": {
843
+ "de ardor": {
844
+ "english": "burning",
845
+ "mcgill_dimension": "sensory"
846
+ },
847
+ "quemadura": {
848
+ "english": "burning",
849
+ "mcgill_dimension": "sensory"
850
+ },
851
+ "quemazón": {
852
+ "english": "burning",
853
+ "mcgill_dimension": "sensory"
854
+ },
855
+ "adormecido": {
856
+ "english": "numb",
857
+ "mcgill_dimension": "sensory"
858
+ },
859
+ "perforante": {
860
+ "english": "piercing",
861
+ "mcgill_dimension": "sensory"
862
+ },
863
+ "pinchazo": {
864
+ "english": "pricking",
865
+ "mcgill_dimension": "sensory"
866
+ },
867
+ "agudo": {
868
+ "english": "sharp",
869
+ "mcgill_dimension": "sensory"
870
+ },
871
+ "expandirse": {
872
+ "english": "shooting",
873
+ "mcgill_dimension": "sensory"
874
+ },
875
+ "punzante": {
876
+ "english": "stabbing",
877
+ "mcgill_dimension": "sensory"
878
+ },
879
+ "picazón": {
880
+ "english": "stinging",
881
+ "mcgill_dimension": "sensory"
882
+ },
883
+ "hormigueo": {
884
+ "english": "tingling",
885
+ "mcgill_dimension": "sensory"
886
+ },
887
+ },
888
+ "nociceptive": {
889
+ "dolorido": {
890
+ "english": "aching",
891
+ "mcgill_dimension": "sensory"
892
+ },
893
+ "agudo": {
894
+ "english": "acute",
895
+ "mcgill_dimension": "sensory"
896
+ },
897
+ "anónico": {
898
+ "english": "agonizing",
899
+ "mcgill_dimension": "sensory"
900
+ },
901
+ "batiente": {
902
+ "english": "beating",
903
+ "mcgill_dimension": "sensory"
904
+ },
905
+ "agobiante": {
906
+ "english": "heavy",
907
+ "mcgill_dimension": "sensory"
908
+ },
909
+ "sofocante": {
910
+ "english": "suffocating",
911
+ "mcgill_dimension": "sensory"
912
+ },
913
+ "ceguera": {
914
+ "english": "blinding",
915
+ "mcgill_dimension": "sensory"
916
+ },
917
+ "terebrante": {
918
+ "english": "boring",
919
+ "mcgill_dimension": "sensory"
920
+ },
921
+ "breve": {
922
+ "english": "brief",
923
+ "mcgill_dimension": "sensory"
924
+ },
925
+ "quemadura": {
926
+ "english": "burn",
927
+ "mcgill_dimension": "sensory"
928
+ },
929
+ "crónico": {
930
+ "english": "chronic",
931
+ "mcgill_dimension": "sensory"
932
+ },
933
+ "helante": {
934
+ "english": "cold",
935
+ "mcgill_dimension": "sensory"
936
+ },
937
+ "constante": {
938
+ "english": "constant",
939
+ "mcgill_dimension": "sensory"
940
+ },
941
+ "frío": {
942
+ "english": "cool",
943
+ "mcgill_dimension": "sensory"
944
+ },
945
+ "calambre": {
946
+ "english": "cramp",
947
+ "mcgill_dimension": "sensory"
948
+ },
949
+ "retortijón": {
950
+ "english": "cramp",
951
+ "mcgill_dimension": "sensory"
952
+ },
953
+ "triturante": {
954
+ "english": "crushing",
955
+ "mcgill_dimension": "sensory"
956
+ },
957
+ "incisión": {
958
+ "english": "cut",
959
+ "mcgill_dimension": "sensory"
960
+ },
961
+ "cortante": {
962
+ "english": "cutting",
963
+ "mcgill_dimension": "sensory"
964
+ },
965
+ "de estiramiento": {
966
+ "english": "drawing",
967
+ "mcgill_dimension": "sensory"
968
+ },
969
+ "atemorizante": {
970
+ "english": "dreadful",
971
+ "mcgill_dimension": "sensory"
972
+ },
973
+ "taladrante": {
974
+ "english": "drilling",
975
+ "mcgill_dimension": "sensory"
976
+ },
977
+ "leve": {
978
+ "english": "dull",
979
+ "mcgill_dimension": "sensory"
980
+ },
981
+ "agotar": {
982
+ "english": "exhaust",
983
+ "mcgill_dimension": "sensory"
984
+ },
985
+ "dar miedo": {
986
+ "english": "fearful",
987
+ "mcgill_dimension": "sensory"
988
+ },
989
+ "intermitente": {
990
+ "english": "fitful",
991
+ "mcgill_dimension": "sensory"
992
+ },
993
+ "destello de": {
994
+ "english": "flickering",
995
+ "mcgill_dimension": "sensory"
996
+ },
997
+ "centelleante": {
998
+ "english": "flashing",
999
+ "mcgill_dimension": "sensory"
1000
+ },
1001
+ "congelante": {
1002
+ "english": "freezing",
1003
+ "mcgill_dimension": "sensory"
1004
+ },
1005
+ "alarmante": {
1006
+ "english": "frightful",
1007
+ "mcgill_dimension": "sensory"
1008
+ },
1009
+ "lacerante": {
1010
+ "english": "lancinating",
1011
+ "mcgill_dimension": "sensory"
1012
+ },
1013
+ "mordicante": {
1014
+ "english": "gnawing",
1015
+ "mcgill_dimension": "sensory"
1016
+ },
1017
+ "agotador": {
1018
+ "english": "gruelling",
1019
+ "mcgill_dimension": "sensory"
1020
+ },
1021
+ "caliente": {
1022
+ "english": "hot",
1023
+ "mcgill_dimension": "sensory"
1024
+ },
1025
+ "doler": {
1026
+ "english": "to hurt",
1027
+ "mcgill_dimension": "sensory"
1028
+ },
1029
+ "intenso": {
1030
+ "english": "intense",
1031
+ "mcgill_dimension": "sensory"
1032
+ },
1033
+ "comezón": {
1034
+ "english": "itch",
1035
+ "mcgill_dimension": "sensory"
1036
+ },
1037
+ "picante": {
1038
+ "english": "itchy",
1039
+ "mcgill_dimension": "sensory"
1040
+ },
1041
+ "saltón": {
1042
+ "english": "jumping",
1043
+ "mcgill_dimension": "sensory"
1044
+ },
1045
+ "matar": {
1046
+ "english": "kill",
1047
+ "mcgill_dimension": "sensory"
1048
+ },
1049
+ "molesto": {
1050
+ "english": "nagging",
1051
+ "mcgill_dimension": "sensory"
1052
+ },
1053
+ "dar náuseas": {
1054
+ "english": "nauseating",
1055
+ "mcgill_dimension": "sensory"
1056
+ },
1057
+ "penetrante": {
1058
+ "english": "penetrating",
1059
+ "mcgill_dimension": "sensory"
1060
+ },
1061
+ "regular": {
1062
+ "english": "periodic",
1063
+ "mcgill_dimension": "sensory"
1064
+ },
1065
+ "periódico": {
1066
+ "english": "periodic",
1067
+ "mcgill_dimension": "sensory"
1068
+ },
1069
+ "pellizcante": {
1070
+ "english": "pinching",
1071
+ "mcgill_dimension": "sensory"
1072
+ },
1073
+ "latiendo": {
1074
+ "english": "pounding",
1075
+ "mcgill_dimension": "sensory"
1076
+ },
1077
+ "hacer presión sobre": {
1078
+ "english": "pressing",
1079
+ "mcgill_dimension": "sensory"
1080
+ },
1081
+ "tensar": {
1082
+ "english": "pulling",
1083
+ "mcgill_dimension": "sensory"
1084
+ },
1085
+ "palpitar": {
1086
+ "english": "pulsing",
1087
+ "mcgill_dimension": "sensory"
1088
+ },
1089
+ "temblando": {
1090
+ "english": "quivering",
1091
+ "mcgill_dimension": "sensory"
1092
+ },
1093
+ "irradiar": {
1094
+ "english": "radiating",
1095
+ "mcgill_dimension": "sensory"
1096
+ },
1097
+ "ronca": {
1098
+ "english": "raspy",
1099
+ "mcgill_dimension": "sensory"
1100
+ },
1101
+ "rítmico": {
1102
+ "english": "rhythmic",
1103
+ "mcgill_dimension": "sensory"
1104
+ },
1105
+ "hirviente": {
1106
+ "english": "scalding",
1107
+ "mcgill_dimension": "sensory"
1108
+ },
1109
+ "ardiente": {
1110
+ "english": "searing",
1111
+ "mcgill_dimension": "sensory"
1112
+ },
1113
+ "seco": {
1114
+ "english": "smarting",
1115
+ "mcgill_dimension": "sensory"
1116
+ },
1117
+ "adolorido": {
1118
+ "english": "sore",
1119
+ "mcgill_dimension": "sensory"
1120
+ },
1121
+ "severo": {
1122
+ "english": "splitting",
1123
+ "mcgill_dimension": "sensory"
1124
+ },
1125
+ "que se extiende": {
1126
+ "english": "spreading",
1127
+ "mcgill_dimension": "sensory"
1128
+ },
1129
+ "apretar": {
1130
+ "english": "squeezing",
1131
+ "mcgill_dimension": "sensory"
1132
+ },
1133
+ "tirante": {
1134
+ "english": "taut",
1135
+ "mcgill_dimension": "sensory"
1136
+ },
1137
+ "desgarrador": {
1138
+ "english": "wrenching",
1139
+ "mcgill_dimension": "sensory"
1140
+ },
1141
+ "sensible": {
1142
+ "english": "tender",
1143
+ "mcgill_dimension": "sensory"
1144
+ },
1145
+ "aterrador": {
1146
+ "english": "terrifying",
1147
+ "mcgill_dimension": "sensory"
1148
+ },
1149
+ "punzante": {
1150
+ "english": "throbbing",
1151
+ "mcgill_dimension": "sensory"
1152
+ },
1153
+ "firme": {
1154
+ "english": "unyielding",
1155
+ "mcgill_dimension": "sensory"
1156
+ },
1157
+ "cansarse": {
1158
+ "english": "to tire",
1159
+ "mcgill_dimension": "sensory"
1160
+ },
1161
+ "torturante": {
1162
+ "english": "torturing",
1163
+ "mcgill_dimension": "sensory"
1164
+ },
1165
+ "maligno": {
1166
+ "english": "vicious",
1167
+ "mcgill_dimension": "sensory"
1168
+ },
1169
+ "la cabeza": {
1170
+ "english": "Head",
1171
+ "mcgill_dimension": "sensory"
1172
+ },
1173
+ "el cuello": {
1174
+ "english": "Neck",
1175
+ "mcgill_dimension": "sensory"
1176
+ },
1177
+ "la cara": {
1178
+ "english": "Face",
1179
+ "mcgill_dimension": "sensory"
1180
+ },
1181
+ "las manos": {
1182
+ "english": "Hands",
1183
+ "mcgill_dimension": "sensory"
1184
+ },
1185
+ "los brazos": {
1186
+ "english": "Arms",
1187
+ "mcgill_dimension": "sensory"
1188
+ },
1189
+ "la espalda": {
1190
+ "english": "Back",
1191
+ "mcgill_dimension": "sensory"
1192
+ },
1193
+ "las piernas": {
1194
+ "english": "Legs",
1195
+ "mcgill_dimension": "sensory"
1196
+ },
1197
+ "los pies": {
1198
+ "english": "Feet",
1199
+ "mcgill_dimension": "sensory"
1200
+ },
1201
+ "el pecho": {
1202
+ "english": "Chest",
1203
+ "mcgill_dimension": "sensory"
1204
+ },
1205
+ "el abdomen": {
1206
+ "english": "Abdomen",
1207
+ "mcgill_dimension": "sensory"
1208
+ },
1209
+ "los pulmones": {
1210
+ "english": "lungs",
1211
+ "mcgill_dimension": "sensory"
1212
+ },
1213
+ "el corazón": {
1214
+ "english": "heart",
1215
+ "mcgill_dimension": "sensory"
1216
+ },
1217
+ "angustioso": {
1218
+ "english": "distressing",
1219
+ "mcgill_dimension": "sensory"
1220
+ },
1221
+ "escalofrio": {
1222
+ "english": "chill",
1223
+ "mcgill_dimension": "sensory"
1224
+ },
1225
+ "migraña": {
1226
+ "english": "Throbbing headache",
1227
+ "mcgill_dimension": "sensory"
1228
+ },
1229
+ "Nervios": {
1230
+ "english": "anxious",
1231
+ "mcgill_dimension": "sensory"
1232
+ },
1233
+ "susto": {
1234
+ "english": "anxiety",
1235
+ "mcgill_dimension": "sensory"
1236
+ },
1237
+ "tenso": {
1238
+ "english": "muscle tension",
1239
+ "mcgill_dimension": "sensory"
1240
+ },
1241
+ "irritarse": {
1242
+ "english": "irritated",
1243
+ "mcgill_dimension": "sensory"
1244
+ },
1245
+ "entumir": {
1246
+ "english": "Fall asleep",
1247
+ "mcgill_dimension": "sensory"
1248
+ },
1249
+ },
1250
+ "affective": {
1251
+ "fastidioso": {
1252
+ "english": "annoying",
1253
+ "mcgill_dimension": "sensory"
1254
+ },
1255
+ "atroz": {
1256
+ "english": "miserable",
1257
+ "mcgill_dimension": "sensory"
1258
+ },
1259
+ "pesado": {
1260
+ "english": "troublesome",
1261
+ "mcgill_dimension": "sensory"
1262
+ },
1263
+ "insoportable": {
1264
+ "english": "unbearable",
1265
+ "mcgill_dimension": "sensory"
1266
+ },
1267
+ },
1268
+ }
1269
+
1270
+
1271
+ # HMONG Pain Descriptors
1272
+ HMONG_PAIN_DESCRIPTORS = {
1273
+ "neuropathic": {
1274
+ "Plev": {
1275
+ "english": "stinging",
1276
+ "mcgill_dimension": "sensory"
1277
+ },
1278
+ "Muab Nkaug": {
1279
+ "english": "stabbing",
1280
+ "mcgill_dimension": "sensory"
1281
+ },
1282
+ "Kub Heev": {
1283
+ "english": "burning",
1284
+ "mcgill_dimension": "sensory"
1285
+ },
1286
+ "Loog": {
1287
+ "english": "numb",
1288
+ "mcgill_dimension": "sensory"
1289
+ },
1290
+ "Mob ntse": {
1291
+ "english": "picking and pricking",
1292
+ "mcgill_dimension": "sensory"
1293
+ },
1294
+ "Txais hluas taws xob": {
1295
+ "english": "receive an electric shock",
1296
+ "mcgill_dimension": "sensory"
1297
+ },
1298
+ "Nkaug": {
1299
+ "english": "stabbing",
1300
+ "mcgill_dimension": "sensory"
1301
+ },
1302
+ "Khaus": {
1303
+ "english": "tingling",
1304
+ "mcgill_dimension": "sensory"
1305
+ },
1306
+ "Mob": {
1307
+ "english": "stinging",
1308
+ "mcgill_dimension": "sensory"
1309
+ },
1310
+ },
1311
+ "nociceptive": {
1312
+ "Rub Leeg": {
1313
+ "english": "tugging",
1314
+ "mcgill_dimension": "sensory"
1315
+ },
1316
+ "leg": {
1317
+ "english": "twisted",
1318
+ "mcgill_dimension": "sensory"
1319
+ },
1320
+ "De": {
1321
+ "english": "pinching",
1322
+ "mcgill_dimension": "sensory"
1323
+ },
1324
+ "Ntuag": {
1325
+ "english": "tearing",
1326
+ "mcgill_dimension": "sensory"
1327
+ },
1328
+ "Raug Ntuag": {
1329
+ "english": "be torn",
1330
+ "mcgill_dimension": "sensory"
1331
+ },
1332
+ "Piam": {
1333
+ "english": "broken",
1334
+ "mcgill_dimension": "sensory"
1335
+ },
1336
+ "Khaus Khaus": {
1337
+ "english": "be scratched",
1338
+ "mcgill_dimension": "sensory"
1339
+ },
1340
+ "Tig": {
1341
+ "english": "wriggle,",
1342
+ "mcgill_dimension": "sensory"
1343
+ },
1344
+ "Obtuse": {
1345
+ "english": "obtuse",
1346
+ "mcgill_dimension": "sensory"
1347
+ },
1348
+ "Tawm": {
1349
+ "english": "sticking out",
1350
+ "mcgill_dimension": "sensory"
1351
+ },
1352
+ "Plev": {
1353
+ "english": "get stung",
1354
+ "mcgill_dimension": "sensory"
1355
+ },
1356
+ "Txias": {
1357
+ "english": "feel a chill",
1358
+ "mcgill_dimension": "sensory"
1359
+ },
1360
+ "Tawg": {
1361
+ "english": "cracking",
1362
+ "mcgill_dimension": "sensory"
1363
+ },
1364
+ "Nkau": {
1365
+ "english": "penetrating",
1366
+ "mcgill_dimension": "sensory"
1367
+ },
1368
+ "Cheeb yob tsis tau": {
1369
+ "english": "having a convulsive fit",
1370
+ "mcgill_dimension": "sensory"
1371
+ },
1372
+ "Hnyav": {
1373
+ "english": "feel heavy",
1374
+ "mcgill_dimension": "sensory"
1375
+ },
1376
+ "Nyob qis tab sis kho": {
1377
+ "english": "be low but steady",
1378
+ "mcgill_dimension": "sensory"
1379
+ },
1380
+ "Nqaij Khov": {
1381
+ "english": "feels like frostbite",
1382
+ "mcgill_dimension": "sensory"
1383
+ },
1384
+ "Hnov nqaij khaus ntawm ib cheem tsam": {
1385
+ "english": "tingle, feel a twinge in the area",
1386
+ "mcgill_dimension": "sensory"
1387
+ },
1388
+ "Mob, Loog": {
1389
+ "english": "aching, throbbing",
1390
+ "mcgill_dimension": "sensory"
1391
+ },
1392
+ "Muab tswj": {
1393
+ "english": "be twisted",
1394
+ "mcgill_dimension": "sensory"
1395
+ },
1396
+ "Tsw muaj zog": {
1397
+ "english": "pungent",
1398
+ "mcgill_dimension": "sensory"
1399
+ },
1400
+ "Mob": {
1401
+ "english": "throbbing",
1402
+ "mcgill_dimension": "sensory"
1403
+ },
1404
+ "Tev": {
1405
+ "english": "peeling",
1406
+ "mcgill_dimension": "sensory"
1407
+ },
1408
+ "Pleb": {
1409
+ "english": "cracking",
1410
+ "mcgill_dimension": "sensory"
1411
+ },
1412
+ "Nyob ruaj khov": {
1413
+ "english": "be ground",
1414
+ "mcgill_dimension": "sensory"
1415
+ },
1416
+ "Muab Tswj": {
1417
+ "english": "be twisted",
1418
+ "mcgill_dimension": "sensory"
1419
+ },
1420
+ "Txawv": {
1421
+ "english": "out of place",
1422
+ "mcgill_dimension": "sensory"
1423
+ },
1424
+ "Ntog": {
1425
+ "english": "falling out,",
1426
+ "mcgill_dimension": "sensory"
1427
+ },
1428
+ "Suab Tawg": {
1429
+ "english": "creaking",
1430
+ "mcgill_dimension": "sensory"
1431
+ },
1432
+ "Chob": {
1433
+ "english": "poking",
1434
+ "mcgill_dimension": "sensory"
1435
+ },
1436
+ "ci ntsa iab": {
1437
+ "english": "flickering",
1438
+ "mcgill_dimension": "sensory"
1439
+ },
1440
+ "Tshee": {
1441
+ "english": "quivering",
1442
+ "mcgill_dimension": "sensory"
1443
+ },
1444
+ "Co heev": {
1445
+ "english": "pulsing",
1446
+ "mcgill_dimension": "sensory"
1447
+ },
1448
+ "Ntaus": {
1449
+ "english": "pounding",
1450
+ "mcgill_dimension": "sensory"
1451
+ },
1452
+ "Teeb ntsai": {
1453
+ "english": "flashing",
1454
+ "mcgill_dimension": "sensory"
1455
+ },
1456
+ "La la Li": {
1457
+ "english": "boring",
1458
+ "mcgill_dimension": "sensory"
1459
+ },
1460
+ "Tho": {
1461
+ "english": "drilling",
1462
+ "mcgill_dimension": "sensory"
1463
+ },
1464
+ "Nkaug": {
1465
+ "english": "lancinating",
1466
+ "mcgill_dimension": "sensory"
1467
+ },
1468
+ "Hlai": {
1469
+ "english": "lacerating",
1470
+ "mcgill_dimension": "sensory"
1471
+ },
1472
+ "Tom": {
1473
+ "english": "gnawing",
1474
+ "mcgill_dimension": "sensory"
1475
+ },
1476
+ "Mob qaib": {
1477
+ "english": "cramping",
1478
+ "mcgill_dimension": "sensory"
1479
+ },
1480
+ "Tswj": {
1481
+ "english": "wrenching",
1482
+ "mcgill_dimension": "sensory"
1483
+ },
1484
+ "Kub Heev": {
1485
+ "english": "scalding",
1486
+ "mcgill_dimension": "sensory"
1487
+ },
1488
+ "Hlawv": {
1489
+ "english": "searing",
1490
+ "mcgill_dimension": "sensory"
1491
+ },
1492
+ "Mos Mos": {
1493
+ "english": "tender",
1494
+ "mcgill_dimension": "sensory"
1495
+ },
1496
+ "Rub nruj": {
1497
+ "english": "taut",
1498
+ "mcgill_dimension": "sensory"
1499
+ },
1500
+ "Ua pa hnyav": {
1501
+ "english": "rasping",
1502
+ "mcgill_dimension": "sensory"
1503
+ },
1504
+ "Mob heev": {
1505
+ "english": "sickening",
1506
+ "mcgill_dimension": "sensory"
1507
+ },
1508
+ "Tsim Txos": {
1509
+ "english": "grueling",
1510
+ "mcgill_dimension": "sensory"
1511
+ },
1512
+ "Hnyav heev": {
1513
+ "english": "intense",
1514
+ "mcgill_dimension": "sensory"
1515
+ },
1516
+ "Kis log Tuag": {
1517
+ "english": "radiating",
1518
+ "mcgill_dimension": "sensory"
1519
+ },
1520
+ "Rub": {
1521
+ "english": "drawing",
1522
+ "mcgill_dimension": "sensory"
1523
+ },
1524
+ "Thab": {
1525
+ "english": "nagging",
1526
+ "mcgill_dimension": "sensory"
1527
+ },
1528
+ "Tsis zoo siab": {
1529
+ "english": "dreadful",
1530
+ "mcgill_dimension": "sensory"
1531
+ },
1532
+ },
1533
+ }
Backend/scripts/parse_multilingual_data.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Parse multilingual pain descriptor data from xlsx
3
+ Generate Python dictionaries for each language
4
+ """
5
+ import pandas as pd
6
+ import json
7
+
8
+ def categorize_pain_type(english_word):
9
+ """Categorize pain descriptor into neuropathic, nociceptive, or affective"""
10
+ neuropathic_keywords = [
11
+ 'sharp', 'shooting', 'burning', 'tingling', 'numb', 'electric',
12
+ 'stabbing', 'pricking', 'shock', 'sting', 'piercing', 'needle'
13
+ ]
14
+
15
+ nociceptive_keywords = [
16
+ 'aching', 'sore', 'throbbing', 'cramping', 'pressing', 'dull',
17
+ 'heavy', 'tight', 'tender', 'stiff', 'pulling', 'squeezing'
18
+ ]
19
+
20
+ affective_keywords = [
21
+ 'exhausting', 'tiring', 'unbearable', 'miserable', 'annoying',
22
+ 'troublesome', 'depressing', 'frustrating', 'worrying', 'frightening'
23
+ ]
24
+
25
+ word_lower = english_word.lower()
26
+
27
+ # Check each category
28
+ if any(kw in word_lower for kw in neuropathic_keywords):
29
+ return 'neuropathic'
30
+ elif any(kw in word_lower for kw in nociceptive_keywords):
31
+ return 'nociceptive'
32
+ elif any(kw in word_lower for kw in affective_keywords):
33
+ return 'affective'
34
+ else:
35
+ return 'nociceptive' # Default to nociceptive
36
+
37
+ def parse_sheet(xlsx_path, sheet_name, english_col, foreign_col):
38
+ """Parse a specific sheet and return structured data"""
39
+ df = pd.read_excel(xlsx_path, sheet_name=sheet_name)
40
+
41
+ # Special handling for Korean sheet (header is in first row)
42
+ if sheet_name == 'ko-en':
43
+ # First row contains data, not headers
44
+ # Read without header
45
+ df = pd.read_excel(xlsx_path, sheet_name=sheet_name, header=None)
46
+ # Assume column 0 is Korean, column 1 is English
47
+ df.columns = ['Korean', 'English'] + [f'Col{i}' for i in range(len(df.columns) - 2)]
48
+ english_col = 'English'
49
+ foreign_col = 'Korean'
50
+
51
+ # Remove rows with NaN in critical columns
52
+ if english_col not in df.columns or foreign_col not in df.columns:
53
+ print(f"⚠️ Warning: Expected columns not found in {sheet_name}")
54
+ print(f" Available columns: {list(df.columns)}")
55
+ return {'neuropathic': {}, 'nociceptive': {}, 'affective': {}}
56
+
57
+ df = df.dropna(subset=[english_col, foreign_col])
58
+
59
+ pain_dict = {
60
+ 'neuropathic': {},
61
+ 'nociceptive': {},
62
+ 'affective': {}
63
+ }
64
+
65
+ for _, row in df.iterrows():
66
+ english = str(row[english_col]).strip()
67
+ foreign = str(row[foreign_col]).strip()
68
+
69
+ # Skip empty or invalid entries
70
+ if not english or not foreign or english == 'nan' or foreign == 'nan':
71
+ continue
72
+
73
+ # Categorize
74
+ category = categorize_pain_type(english)
75
+
76
+ # Add to dictionary (without snomed_ct)
77
+ pain_dict[category][foreign] = {
78
+ 'english': english,
79
+ 'mcgill_dimension': 'sensory' # Default, can be refined
80
+ }
81
+
82
+ return pain_dict
83
+
84
+ def main():
85
+ xlsx_path = r'c:\Users\ChaCha ship\Documents\Github\PainReport\Backend\data\questionnaire_form.xlsx'
86
+
87
+ # Parse each language sheet with correct column names
88
+ # Format: (sheet_name, english_column, foreign_column)
89
+ languages = {
90
+ 'chinese': ('cn-en', 'English', 'Chinese'),
91
+ 'korean': ('ko-en', 'English', 'Korean'), # Will be handled specially
92
+ 'spanish': ('es-en', 'English', 'Spanish'),
93
+ 'hmong': ('hmong-en', 'English pain words', 'Hmong pain words')
94
+ }
95
+
96
+ results = {}
97
+
98
+ for lang_name, (sheet_name, english_col, foreign_col) in languages.items():
99
+ print(f"\n{'='*60}")
100
+ print(f"Parsing {lang_name.upper()} ({sheet_name})")
101
+ print(f"{'='*60}")
102
+
103
+ pain_dict = parse_sheet(xlsx_path, sheet_name, english_col, foreign_col)
104
+ results[lang_name] = pain_dict
105
+
106
+ # Print statistics
107
+ total = sum(len(pain_dict[cat]) for cat in pain_dict)
108
+ print(f"Total terms: {total}")
109
+ print(f" - Neuropathic: {len(pain_dict['neuropathic'])}")
110
+ print(f" - Nociceptive: {len(pain_dict['nociceptive'])}")
111
+ print(f" - Affective: {len(pain_dict['affective'])}")
112
+
113
+ # Show samples
114
+ if len(pain_dict['neuropathic']) > 0:
115
+ print(f"\nSample neuropathic terms:")
116
+ for i, (foreign, data) in enumerate(list(pain_dict['neuropathic'].items())[:3]):
117
+ print(f" {foreign} -> {data['english']}")
118
+
119
+ # Save to JSON for inspection
120
+ import os
121
+ scripts_dir = os.path.dirname(os.path.abspath(__file__))
122
+ output_path = os.path.join(scripts_dir, 'multilingual_pain_data.json')
123
+
124
+ with open(output_path, 'w', encoding='utf-8') as f:
125
+ json.dump(results, f, ensure_ascii=False, indent=2)
126
+
127
+ print(f"\n\n✅ Multilingual data saved to: {output_path}")
128
+
129
+ return results
130
+
131
+ if __name__ == '__main__':
132
+ main()
Backend/services/__init__.py ADDED
File without changes
Backend/services/conversation_service.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from dotenv import load_dotenv
4
+ import json
5
+ from typing import List, Dict
6
+
7
+ load_dotenv()
8
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
9
+
10
+ PLACEHOLDER_IMAGES = {
11
+ "sharp": "/images/sharp.gif",
12
+ "dull": "/images/dull.jpg",
13
+ "burning": "/images/burning.gif",
14
+ "tingling": "/images/Tingling.jpg",
15
+ "throbbing": "/images/throbbing.jpg",
16
+ "radiating": "/images/radiating.gif",
17
+ "pulsing": "/images/pulsing.gif",
18
+ "pounding": "/images/pounding.gif"
19
+ }
20
+
21
+ def generateFollowUpQuestions(converHistory: List[Dict]) -> dict:
22
+ """Generate bilingual visual pain assessment question using available GIF animations"""
23
+
24
+ # Fixed bilingual question using available GIFs
25
+ result = {
26
+ "question": "Which image best describes your pain sensation? | 哪个图像最能描述您的疼痛感觉?",
27
+ "question_type": "quality",
28
+ "options": [
29
+ {
30
+ "id": "A",
31
+ "text": "Sharp, stabbing pain | 尖锐刺痛感",
32
+ "image_key": "sharp",
33
+ "image_url": "/images/sharp.gif"
34
+ },
35
+ {
36
+ "id": "B",
37
+ "text": "Pulsing, throbbing pain | 搏动性疼痛",
38
+ "image_key": "pulsing",
39
+ "image_url": "/images/pulsing.gif"
40
+ },
41
+ {
42
+ "id": "C",
43
+ "text": "Burning, hot sensation | 灼烧样疼痛",
44
+ "image_key": "burning",
45
+ "image_url": "/images/burning.gif"
46
+ }
47
+ ],
48
+ "round_number": 1
49
+ }
50
+
51
+ return result
52
+
Backend/services/llm_service.py ADDED
@@ -0,0 +1,796 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from dotenv import load_dotenv
4
+ import json
5
+
6
+ load_dotenv()
7
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
8
+
9
+ def analyzePainDescription(text: str) ->dict:
10
+
11
+ system_prompt = """You are an expert Medical Anthropologist specializing in cross-cultural pain expression.
12
+
13
+ Your task is to analyze a patient's transcript by decomposing it into FOUR analytical layers, and then output a structured JSON representation.
14
+
15
+ ⚠️ Constraints:
16
+ - Do NOT act as a medical doctor or provide diagnosis.
17
+ - Do NOT infer beyond the given transcript.
18
+ - If uncertain, explicitly state "unknown" or explain uncertainty.
19
+ - Output MUST be valid JSON only. No extra text.
20
+
21
+ ---
22
+
23
+ ### Analytical Framework (MANDATORY)
24
+
25
+ You MUST analyze the transcript in the following four layers:
26
+
27
+ 1. Linguistic Layer
28
+ - Provide a literal translation preserving the patient's original wording and meaning.
29
+ - Do NOT interpret or simplify.
30
+
31
+ 2. Cultural-Semantic Layer
32
+ - Identify culturally specific metaphors or expressions.
33
+ - Explain what they mean in plain English.
34
+ - Preserve original language for reference.
35
+
36
+ 3. Clinical Abstraction Layer
37
+ - Map the narrative into structured pain descriptors:
38
+ - sensory qualities (e.g., sharp, dull)
39
+ - affective qualities (e.g., tiring, distressing)
40
+ - temporal pattern (e.g., intermittent, constant)
41
+ - possible body location (if mentioned)
42
+ - If unclear, mark as "unknown"
43
+
44
+ 4. Psychosocial Layer
45
+ - Identify:
46
+ - emotional distress
47
+ - under-reporting (stoicism)
48
+ - communication risks
49
+ - Base ONLY on text evidence (no guessing)
50
+
51
+ ---
52
+
53
+ ### Output JSON Schema (STRICT)
54
+
55
+ {
56
+ "literal_translation": "...",
57
+ "metaphor_mapping": [
58
+ {
59
+ "original_phrase": "...",
60
+ "language": "...",
61
+ "literal_meaning": "...",
62
+ "interpreted_meaning": "..."
63
+ }
64
+ ],
65
+ "clinical_abstraction": {
66
+ "sensory": [],
67
+ "affective": [],
68
+ "temporal_pattern": "",
69
+ "body_location": "",
70
+ "intensity_estimate": ""
71
+ },
72
+ "psychological_and_stoicism_flags": {
73
+ "underreporting_risk": true,
74
+ "emotional_distress": true,
75
+ "communication_risk": "low",
76
+ "notes": ""
77
+ },
78
+ "physician_action_note": ""
79
+ }"""
80
+
81
+ try:
82
+ response = client.chat.completions.create(
83
+ model="gpt-5.2", # Use gpt-5.2 for the latest features
84
+ messages=[
85
+ {"role": "system", "content": system_prompt + "\n\n**CRITICAL**: Your response must be ONLY valid JSON. No additional text before or after the JSON."},
86
+ {"role": "user", "content": text}
87
+ ],
88
+ temperature=0.1 # Low temperature for consistency
89
+ )
90
+
91
+ analysis = json.loads(response.choices[0].message.content)
92
+ return analysis
93
+
94
+ except Exception as e:
95
+ raise Exception(f"Error analyzing pain description: {str(e)}")
96
+
97
+
98
+ def match_pain_terms_from_vocabulary(text: str, vocabulary: list, language: str = "Chinese") -> dict:
99
+ """
100
+ Match pain terms from a provided vocabulary list in the patient's text.
101
+
102
+ This replaces the old entity extraction approach. Instead of asking LLM to extract terms,
103
+ we provide a curated vocabulary and ask it to identify which terms are present.
104
+
105
+ Benefits:
106
+ - No hallucination (LLM chooses from given list)
107
+ - No unmapped terms (all terms are in dictionary)
108
+ - Language-specific vocabulary reduces token usage
109
+ - Translation handled by dictionary (100% accurate)
110
+
111
+ Args:
112
+ text: Patient's pain description
113
+ vocabulary: List of pain terms in patient's language (from our dictionary)
114
+ language: Language name for context
115
+
116
+ Returns:
117
+ Dictionary with:
118
+ - matched_terms: List of terms from vocabulary found in text
119
+ - location: Body part mentioned
120
+ - duration_phrase: Time expression
121
+ - intensity: Pain intensity if stated
122
+ - emotion_keywords: Emotional words
123
+ - functional_impact: Activity limitations
124
+
125
+ Example:
126
+ >>> vocab = ["火辣辣的疼", "麻的", "刺痛", "酸痛"]
127
+ >>> result = match_pain_terms_from_vocabulary("腿火辣辣的疼,还有点麻", vocab, "Chinese")
128
+ >>> # Returns: {"matched_terms": ["火辣辣的疼", "麻的"], ...}
129
+ """
130
+
131
+ # Format vocabulary for prompt (limit to prevent token overflow)
132
+ vocab_str = "\n".join([f" - {term}" for term in vocabulary[:200]]) # Max 200 terms
133
+
134
+ system_prompt = f"""You are a medical term matcher for {language} pain descriptions.
135
+
136
+ **YOUR TASK**: Identify which pain terms from the provided vocabulary appear in the patient's text.
137
+
138
+ **VOCABULARY** (pain descriptors in {language}):
139
+ {vocab_str}
140
+
141
+ **MATCHING RULES**:
142
+ 1. **Exact and Fuzzy Matching ONLY**:
143
+ - Exact: If patient says "火辣辣的疼" and it's in vocabulary → MATCH ✅
144
+ - Fuzzy: If patient says "火辣辣的" or "有点麻", match "火辣辣的疼" or "麻的" ✅
145
+ - Core matching: "麻" can match "麻的", "一抽一抽" can match "一抽一抽的痛"
146
+
147
+ 2. **CRITICAL - NO INTERPRETATION**:
148
+ - ❌ DO NOT interpret metaphors (e.g., "蚂蚁在爬" should NOT match "痒的" even if it sounds itchy)
149
+ - ❌ DO NOT infer meaning (e.g., "像被火烧" should NOT match "火辣辣的" unless text contains "火辣")
150
+ - ❌ DO NOT translate descriptions to medical terms
151
+ - ✅ ONLY match if the ACTUAL WORDS appear in text (with fuzzy tolerance for suffixes)
152
+
153
+ 3. **What to Match**:
154
+ - Pain quality words that LITERALLY appear in text
155
+ - DO NOT match: connectors (还有, 而且), fillers (那个, 嗯), modifiers alone (有点, 很)
156
+
157
+ 3. **Additional Extraction** (structured fields):
158
+ - location: Body part (腿, 腰, knee, back, etc.)
159
+ - duration_phrase: Time (四个月, 3 months, 一周)
160
+ - intensity: Numeric (0-10) or qualitative if clearly stated
161
+ - emotion_keywords: Emotional words (郁闷, depressed, 害怕)
162
+ - functional_impact: Activity limitations (睡不着, can't walk, 影响工作)
163
+
164
+ **OUTPUT FORMAT** (JSON only):
165
+ {{
166
+ "matched_terms": ["term1 from vocabulary", "term2 from vocabulary"],
167
+ "location": "body part or 'Not stated'",
168
+ "duration_phrase": "time phrase or 'Not stated'",
169
+ "intensity": "intensity or 'Not stated'",
170
+ "emotion_keywords": ["emotion1", "emotion2"],
171
+ "functional_impact": "impact or null"
172
+ }}
173
+
174
+ **EXAMPLES**:
175
+
176
+ Example 1:
177
+ Input: "我的腿火辣辣的疼,还有点麻"
178
+ Vocabulary contains: ["火辣辣的疼", "麻的", "刺痛"]
179
+ Output: {{
180
+ "matched_terms": ["火辣辣的疼", "麻的"],
181
+ "location": "腿",
182
+ "duration_phrase": "Not stated",
183
+ "intensity": "Not stated",
184
+ "emotion_keywords": [],
185
+ "functional_impact": null
186
+ }}
187
+
188
+ Example 2:
189
+ Input: "腰部到腿部触电一样麻痛,四个月了,睡不着,很郁闷"
190
+ Vocabulary contains: ["触电一样", "麻痛", "郁闷"]
191
+ Output: {{
192
+ "matched_terms": ["触电一样", "麻痛"],
193
+ "location": "腰部到腿部",
194
+ "duration_phrase": "四个月",
195
+ "intensity": "Not stated",
196
+ "emotion_keywords": ["郁闷"],
197
+ "functional_impact": "睡不着"
198
+ }}
199
+
200
+ Example 3 (IMPORTANT - NO INTERPRETATION):
201
+ Input: "腿部火辣辣的疼,好像浑身有蚂蚁在爬"
202
+ Vocabulary contains: ["火辣辣的疼", "麻的", "痒的", "刺痛"]
203
+ Output: {{
204
+ "matched_terms": ["火辣辣的疼"],
205
+ "location": "腿部",
206
+ "duration_phrase": "Not stated",
207
+ "intensity": "Not stated",
208
+ "emotion_keywords": [],
209
+ "functional_impact": null
210
+ }}
211
+ NOTE: "蚂蚁在爬" is a metaphor describing sensation, but "蚂蚁" does not appear in vocabulary.
212
+ DO NOT match "痒的" even though ants crawling sounds itchy - no literal word match!
213
+ The metaphor "蚂蚁在爬" will be handled separately as an unmapped unique description.
214
+ """
215
+
216
+ try:
217
+ response = client.chat.completions.create(
218
+ model="gpt-5.2",
219
+ messages=[
220
+ {"role": "system", "content": system_prompt + "\n\n**CRITICAL**: Return ONLY valid JSON. No extra text."},
221
+ {"role": "user", "content": text}
222
+ ],
223
+ temperature=0.1
224
+ )
225
+
226
+ result = json.loads(response.choices[0].message.content)
227
+ return result
228
+
229
+ except Exception as e:
230
+ # Fallback: return empty matches
231
+ return {
232
+ "matched_terms": [],
233
+ "location": "Not stated",
234
+ "duration_phrase": "Not stated",
235
+ "intensity": "Not stated",
236
+ "emotion_keywords": [],
237
+ "functional_impact": None
238
+ }
239
+
240
+
241
+ def extract_pain_entities_constrained(text: str) -> dict:
242
+ """
243
+ LLM-based Named Entity Recognition for pain descriptions.
244
+
245
+ STRICT CONSTRAINTS:
246
+ - ONLY extract entities present in text
247
+ - NO medical reasoning or diagnosis
248
+ - NO speculation beyond explicit patient statements
249
+ - Output must conform to predefined fields
250
+
251
+ This function is part of the neuro-symbolic architecture where LLM is used
252
+ ONLY for narrow-scope entity extraction, not clinical decision-making.
253
+
254
+ Args:
255
+ text: Raw patient pain description (Chinese or multilingual)
256
+
257
+ Returns:
258
+ Dictionary with extracted entities (not clinical conclusions)
259
+
260
+ Example:
261
+ >>> entities = extract_pain_entities_constrained("Electric shock-like pain in lower back for 4 months")
262
+ >>> # Returns: {"pain_descriptors": ["electric shock-like"], "location": "lower back", ...}
263
+ """
264
+
265
+ system_prompt = """You are a medical NER (Named Entity Recognition) system.
266
+
267
+ **YOUR ONLY TASK**: Extract factual entities from patient text.
268
+
269
+ **STRICT RULES**:
270
+ 1. ONLY extract information explicitly stated in the text
271
+ 2. DO NOT make medical diagnoses or clinical interpretations
272
+ 3. DO NOT infer pain type classifications (e.g., neuropathic vs nociceptive)
273
+ 4. DO NOT add medical reasoning or recommendations
274
+ 5. Mark fields as "Not stated" if not explicitly mentioned
275
+
276
+ **WHAT TO EXTRACT**:
277
+ - **location**: Anatomical body parts mentioned (腿, 腰, knee, back, etc.)
278
+ - **duration_phrase**: Time expressions (四个月, 3 months, 一周, etc.)
279
+ - **intensity**: Numeric scores (0-10) or qualitative terms if clearly stated
280
+ - **emotion_keywords**: Emotional words (郁闷, depressed, 害怕, anxious, etc.)
281
+ - **functional_impact**: Activity limitations (睡不着, can't walk, 影响工作, etc.)
282
+ - **pain_descriptors**: OPTIONAL - only extract if patient uses vivid/unique descriptions not in standard medical terminology
283
+
284
+ **NOTE ON pain_descriptors**:
285
+ • Our system has a comprehensive pain term dictionary (152 Chinese terms, 131 Korean, 74 Spanish)
286
+ • Dictionary matching works directly on original text - no extraction needed for standard terms
287
+ • ONLY extract pain_descriptors if patient uses creative/unique expressions like:
288
+ - "像被火烧一样" (creative metaphor)
289
+ - "说不出来的难受" (hard to describe)
290
+ - "怪怪的感觉" (unusual sensation)
291
+ • For standard terms like "火辣辣的疼", "触电一样", "麻" - leave pain_descriptors EMPTY
292
+ • Dictionary will find them automatically
293
+
294
+ **PROHIBITED**:
295
+ - Medical diagnoses
296
+ - Pain classification
297
+ - Clinical interpretations
298
+ - Recommendations
299
+ - Inferences beyond stated text
300
+
301
+ **OUTPUT FORMAT** (JSON only):
302
+ {
303
+ "pain_descriptors": [], // Usually EMPTY - dictionary handles standard terms
304
+ "location": "anatomical location or 'Not stated'",
305
+ "duration_phrase": "exact time phrase or 'Not stated'",
306
+ "intensity": "numeric value or qualitative term if stated, else 'Not stated'",
307
+ "emotion_keywords": ["emotional words patient used"],
308
+ "functional_impact": "impact on activities if mentioned, else null"
309
+ }
310
+
311
+ **EXAMPLE 1** (Chinese - standard terms, pain_descriptors EMPTY):
312
+ Input: "我的腿火辣辣的疼,还有点麻"
313
+ Output: {
314
+ "pain_descriptors": [],
315
+ "location": "腿",
316
+ "duration_phrase": "Not stated",
317
+ "intensity": "Not stated",
318
+ "emotion_keywords": [],
319
+ "functional_impact": null
320
+ }
321
+ Note: "火辣辣的疼" and "麻" are in dictionary - no need to extract
322
+
323
+ **EXAMPLE 2** (Chinese - extract only unique expressions):
324
+ Input: "腰部到腿部说不出来的难受感觉,四个月了,晚上睡不着,心情很郁闷"
325
+ Output: {
326
+ "pain_descriptors": ["说不出来的难受感觉"],
327
+ "location": "腰部到腿部",
328
+ "duration_phrase": "四个月",
329
+ "intensity": "Not stated",
330
+ "emotion_keywords": ["郁闷"],
331
+ "functional_impact": "晚上睡不着"
332
+ }
333
+ Note: "说不出来的难受感觉" is unique/creative - extract it
334
+
335
+ **EXAMPLE 3** (English - focus on structure):
336
+ Input: "My lower back has been aching for 3 months, I'm exhausted"
337
+ Output: {
338
+ "pain_descriptors": [],
339
+ "location": "lower back",
340
+ "duration_phrase": "3 months",
341
+ "intensity": "Not stated",
342
+ "emotion_keywords": ["exhausted"],
343
+ "functional_impact": null
344
+ }
345
+ Note: "aching" is standard - dictionary handles it
346
+ """
347
+
348
+ try:
349
+ response = client.chat.completions.create(
350
+ model="gpt-5.2", # Use gpt-5.2 for the latest features
351
+ messages=[
352
+ {"role": "system", "content": system_prompt + "\n\n**CRITICAL**: Your response must be ONLY valid JSON. No additional text before or after the JSON."},
353
+ {"role": "user", "content": text}
354
+ ],
355
+ temperature=0.1 # Low temperature for consistency
356
+ )
357
+
358
+ entities = json.loads(response.choices[0].message.content)
359
+ return entities
360
+
361
+ except Exception as e:
362
+ raise Exception(f"Error in LLM entity extraction: {str(e)}")
363
+
364
+
365
+ def normalize_transcription(text: str, language: str = "Chinese") -> dict:
366
+ """
367
+ Normalize speech-to-text transcription using LLM to correct errors and standardize expressions.
368
+
369
+ This preprocessing step improves ontology matching accuracy by:
370
+ 1. Correcting common Whisper transcription errors
371
+ 2. Standardizing colloquial/oral expressions to medical terminology
372
+ 3. Fixing incomplete grammar while preserving original meaning
373
+ 4. Normalizing pain descriptors to match ontology terms
374
+
375
+ Args:
376
+ text: Original transcription from Whisper
377
+ language: Patient's language (Chinese, Korean, Spanish, Hmong, English)
378
+
379
+ Returns:
380
+ Dictionary with:
381
+ - original: Original transcription
382
+ - normalized: Cleaned and standardized text
383
+ - corrections: List of changes made (for transparency)
384
+
385
+ Example:
386
+ >>> result = normalize_transcription("Leg... uh... burning really badly", "English")
387
+ >>> # Returns: {
388
+ >>> "original": "Leg... uh... burning really badly",
389
+ >>> "normalized": "Leg burning pain",
390
+ >>> "corrections": ["removed filler words", "standardized expression"]
391
+ >>> }
392
+ """
393
+
394
+ system_prompt = f"""You are a medical transcription normalization expert for {language} pain descriptions.
395
+
396
+ **YOUR TASK**: Clean and standardize speech-to-text transcription while preserving the patient's original pain descriptors.
397
+
398
+ **NORMALIZATION RULES**:
399
+
400
+ 1. **Fix Transcription Errors**:
401
+ - Correct common Whisper errors (homophones, misheard words)
402
+ - Examples:
403
+ * Chinese: "一揪一揪" → "一抽一抽" (throbbing)
404
+ * Korean: "따금거리다" → "따끔거리다" (stinging)
405
+ * Spanish: "quemasón" → "quemazón" (burning)
406
+
407
+ **SPECIAL: Traditional Chinese → Simplified Chinese Conversion**:
408
+ - Whisper may output Traditional Chinese based on speaker accent (Taiwan/Hong Kong)
409
+ - Our pain dictionary uses ONLY Simplified Chinese - conversion is REQUIRED
410
+ - Convert Traditional characters to Simplified:
411
+ * 還 → 还, 點 → 点, 個 → 个, 頭 → 头, 麻 → 麻 (already same)
412
+ * 癢 → 痒, 脹 → 胀, 緊 → 紧, 軟 → 软, 腫 → 肿
413
+ * 鬱悶 → 郁闷, 難受 → 难受, 嚴重 → 严重
414
+ - Examples:
415
+ * "我的腿火辣辣的疼,還有點麻" → "我的腿火辣辣的疼,还有点麻"
416
+ * "頭很痛" → "头很痛"
417
+ * "感覺很難受" → "感觉很难受"
418
+
419
+ 2. **Standardize Pain Descriptors**:
420
+ - Keep vivid pain terms intact (these are medically valuable)
421
+ - Convert colloquial to standard forms:
422
+ * Chinese: "疼得不行" → "剧烈疼痛"
423
+ * Korean: "너무 아파" → "심한 통증"
424
+ * Spanish: "me duele muchísimo" → "dolor intenso"
425
+
426
+ 3. **Clean Up Grammar**:
427
+ - Remove filler words ("那个", "嗯", "uh", "like")
428
+ - Complete incomplete sentences
429
+ - Fix word order errors
430
+ - But DO NOT change pain descriptors
431
+
432
+ 4. **Preserve Original Meaning**:
433
+ - DO NOT add medical interpretations
434
+ - DO NOT change the severity described
435
+ - DO NOT invent information not in original text
436
+
437
+ 5. **Standardize Body Part Names**:
438
+ - Chinese: "腿" → "腿部", "肚子" → "腹部"
439
+ - Keep other details as-is
440
+
441
+ **OUTPUT FORMAT** (JSON only):
442
+ {{
443
+ "original": "original transcription text",
444
+ "normalized": "cleaned and standardized text",
445
+ "corrections": [
446
+ "fix: description of change made",
447
+ "standardize: description of change"
448
+ ],
449
+ "confidence": "high/medium/low (based on # of corrections)"
450
+ }}
451
+
452
+ **EXAMPLES**:
453
+
454
+ Input: "腿那个...怎么说...火辣辣的,疼死了"
455
+ Output: {{
456
+ "original": "腿那个...怎么说...火辣辣的,疼死了",
457
+ "normalized": "腿部火辣辣的疼",
458
+ "corrections": ["removed filler words '那个', '怎么说'", "standardized '腿' to '腿部'", "converted '疼死了' to '疼'"],
459
+ "confidence": "high"
460
+ }}
461
+
462
+ Input: "我的腿火辣辣的疼,還有點麻"
463
+ Output: {{
464
+ "original": "我的腿火辣辣的疼,還有點麻",
465
+ "normalized": "我的腿火辣辣的疼,还有点麻",
466
+ "corrections": ["converted Traditional Chinese to Simplified: 還→还, 點→点"],
467
+ "confidence": "high"
468
+ }}
469
+
470
+ Input: "頭很痛,感覺很難受"
471
+ Output: {{
472
+ "original": "頭很痛,感覺很難受",
473
+ "normalized": "头很痛,感觉很难受",
474
+ "corrections": ["converted Traditional Chinese: 頭→头, 難→难"],
475
+ "confidence": "high"
476
+ }}
477
+
478
+ Input: "허리가 따금거려요, 너무 아파요"
479
+ Output: {{
480
+ "original": "허리가 따금거려요, 너무 아파요",
481
+ "normalized": "허리가 따끔거리고 심하게 아프다",
482
+ "corrections": ["corrected '따금거려요' to '따끔거리고'", "standardized '너무 아파요' to '심하게 아프다'"],
483
+ "confidence": "high"
484
+ }}
485
+
486
+ Input: "Me duele la espalda, como quemasón"
487
+ Output: {{
488
+ "original": "Me duele la espalda, como quemasón",
489
+ "normalized": "Dolor de espalda con quemazón",
490
+ "corrections": ["standardized sentence structure", "corrected 'quemasón' to 'quemazón'"],
491
+ "confidence": "high"
492
+ }}
493
+ """
494
+
495
+ try:
496
+ response = client.chat.completions.create(
497
+ model="gpt-5.2",
498
+ messages=[
499
+ {"role": "system", "content": system_prompt + "\n\n**CRITICAL**: Return ONLY valid JSON. No extra text."},
500
+ {"role": "user", "content": text}
501
+ ],
502
+ temperature=0.2 # Slightly higher for natural corrections
503
+ )
504
+
505
+ result = json.loads(response.choices[0].message.content)
506
+ return result
507
+
508
+ except Exception as e:
509
+ # Fallback: return original text if normalization fails
510
+ return {
511
+ "original": text,
512
+ "normalized": text,
513
+ "corrections": [f"normalization_failed: {str(e)}"],
514
+ "confidence": "low"
515
+ }
516
+
517
+
518
+ def translate_pain_description(text: str, source_language: str, matched_terms: list, mappings: list) -> str:
519
+ """
520
+ Translate patient's pain description to English using matched term translations as reference.
521
+
522
+ This creates a natural English translation that incorporates the medical terminology
523
+ already mapped from our dictionary. Ensures consistency between term mappings and full sentence.
524
+
525
+ Args:
526
+ text: Patient's pain description (in source language)
527
+ source_language: Detected language name ("Chinese", "Korean", etc.)
528
+ matched_terms: List of pain terms matched from vocabulary
529
+ mappings: List of ontology mappings with original_term and mapped_english
530
+
531
+ Returns:
532
+ English translation string
533
+
534
+ Example:
535
+ >>> text = "我的腿火辣辣的疼,还有点麻"
536
+ >>> matched_terms = ["火辣辣的疼", "麻的"]
537
+ >>> mappings = [
538
+ ... {"original_term": "火辣辣的疼", "mapped_english": "burning"},
539
+ ... {"original_term": "麻的", "mapped_english": "numb"}
540
+ ... ]
541
+ >>> translate_pain_description(text, "Chinese", matched_terms, mappings)
542
+ "My leg has burning pain and feels a bit numb"
543
+ """
544
+
545
+ # Build reference translation dictionary from mappings
546
+ term_translation_ref = {}
547
+ for mapping in mappings:
548
+ original = mapping.get('original_term', '')
549
+ english = mapping.get('mapped_english', '')
550
+ if original and english:
551
+ term_translation_ref[original] = english
552
+
553
+ # Format term references for prompt
554
+ term_refs = "\n".join([f" - '{original}' = '{english}'" for original, english in term_translation_ref.items()])
555
+
556
+ if not term_refs:
557
+ term_refs = "(No pain-specific terms mapped)"
558
+
559
+ system_prompt = f"""You are a medical translator specializing in pain assessment.
560
+
561
+ **YOUR TASK**: Translate the patient's {source_language} pain description into natural medical English.
562
+
563
+ **CRITICAL REQUIREMENT**: You MUST use the provided term translations from our medical dictionary.
564
+ These translations are standardized medical terminology (McGill Pain Questionnaire) and MUST be used exactly.
565
+
566
+ **PROVIDED TERM TRANSLATIONS** (USE THESE EXACTLY):
567
+ {term_refs}
568
+
569
+ **TRANSLATION RULES**:
570
+
571
+ 1. **Use Dictionary Terms Exactly**:
572
+ - When translating matched pain terms, use ONLY the provided English translation
573
+ - Example: If "火辣辣的疼" → "burning", translate as "burning pain" NOT "fiery pain" or "burning hot"
574
+
575
+ 2. **Create Natural English**:
576
+ - Produce fluent, natural medical English
577
+ - Maintain professional but clear tone
578
+ - Use proper medical grammar
579
+
580
+ 3. **Preserve All Information**:
581
+ - Include body location, duration, intensity if mentioned
582
+ - Keep emotional and functional impact details
583
+ - Maintain original meaning and severity
584
+
585
+ 4. **Structure**:
586
+ - Use clear, concise sentences
587
+ - Follow standard medical description format: Location + Pain Quality + Duration + Impact
588
+
589
+ 5. **DO NOT**:
590
+ - Add medical interpretations or diagnoses
591
+ - Change pain severity or characteristics
592
+ - Invent information not in original
593
+
594
+ **OUTPUT**: Return ONLY the English translation text. No JSON, no additional formatting.
595
+
596
+ **EXAMPLES**:
597
+
598
+ Example 1:
599
+ Input ({source_language}): "我的腿火辣辣的疼,还有点麻"
600
+ Term References: "火辣辣的疼"="burning", "麻的"="numb"
601
+ Output: "My leg has burning pain and feels a bit numb"
602
+
603
+ Example 2:
604
+ Input ({source_language}): "腰部到腿部触电一样的痛,四个月了,晚上睡不着,很郁闷"
605
+ Term References: "触电一样"="electric-shock-like"
606
+ Output: "Electric-shock-like pain from lower back to legs for 4 months, can't sleep at night, feeling very depressed"
607
+
608
+ Example 3:
609
+ Input ({source_language}): "膝盖一抽一抽的痛,走路困难"
610
+ Term References: "一抽一抽的痛"="throbbing"
611
+ Output: "Throbbing pain in knee, difficulty walking"
612
+ """
613
+
614
+ try:
615
+ response = client.chat.completions.create(
616
+ model="gpt-5.2",
617
+ messages=[
618
+ {"role": "system", "content": system_prompt},
619
+ {"role": "user", "content": f"Translate: {text}"}
620
+ ],
621
+ temperature=0.2
622
+ )
623
+
624
+ translation = response.choices[0].message.content.strip()
625
+
626
+ # Remove quotes if LLM wrapped the output
627
+ if translation.startswith('"') and translation.endswith('"'):
628
+ translation = translation[1:-1]
629
+
630
+ return translation
631
+
632
+ except Exception as e:
633
+ # Fallback: just return the original text
634
+ return text
635
+
636
+
637
+ def generate_comprehensive_report(
638
+ original_text: str,
639
+ structured_data: dict,
640
+ ontology_mappings: list,
641
+ clinical_recommendations: list,
642
+ detected_language: str = "Chinese", # New parameter: detected language
643
+ semantic_analysis: dict = None # New parameter: semantic distance analysis
644
+ ) -> str:
645
+ """
646
+ Generate comprehensive multilingual clinical report using GPT after rule-based analysis.
647
+
648
+ Called AFTER neuro-symbolic pipeline completes. Supports multiple languages:
649
+ Chinese (中文), Korean (한국어), Spanish (Español), Hmong, English
650
+
651
+ Args:
652
+ original_text: Patient's original pain description
653
+ structured_data: PainOntology data (dict format)
654
+ ontology_mappings: List of term mappings (original_term → mapped_english)
655
+ clinical_recommendations: List of rule-triggered recommendations
656
+ detected_language: Language detected from input (default: "Chinese")
657
+ semantic_analysis: Optional semantic distance analysis for unmapped terms
658
+
659
+ Returns:
660
+ Comprehensive bilingual clinical report (original language + English)
661
+ """
662
+
663
+ # Determine bilingual header format based on language
664
+ language_headers = {
665
+ "Chinese": "中文",
666
+ "Korean": "한국어",
667
+ "Spanish": "Español",
668
+ "Hmong": "Hmoob",
669
+ "English": "English"
670
+ }
671
+
672
+ native_lang = language_headers.get(detected_language, "原语言")
673
+
674
+ # If English input, report is English-only
675
+ is_english_only = (detected_language == "English")
676
+
677
+ system_prompt = f"""You are a medical report writer for multilingual pain assessment.
678
+
679
+ **INPUT LANGUAGE**: {detected_language}
680
+ **OUTPUT FORMAT**: {"English only (no translation needed)" if is_english_only else f"Bilingual ({native_lang} + English)"}
681
+
682
+ **YOUR TASK**: Create a well-structured clinical report with THREE sections:
683
+
684
+ **SECTION 1: Translation & Terminology Mapping {"| 翻译与术语映射" if not is_english_only else ""}**
685
+ - Explain how patient's expressions were mapped to standardized medical terminology
686
+ - {"List Original " + detected_language + " terms → English translations" if not is_english_only else "Show pain descriptors used"}
687
+ - Highlight culturally-specific metaphors if present
688
+ - Be clear and structured
689
+
690
+ **SECTION 2: Clinical Assessment {"| 临床评估" if not is_english_only else ""}**
691
+ - Summarize pain characteristics: type, location, duration, intensity
692
+ - Explain pain classification (neuropathic/nociceptive) in simple terms
693
+ - Describe emotional and functional impacts
694
+ - Be empathetic and clear
695
+
696
+ **SECTION 3: Treatment Recommendations {"| 治疗建议" if not is_english_only else ""}**
697
+ - Explain each recommended intervention and WHY
698
+ - Reference clinical rules or guidelines that triggered recommendations
699
+ - Provide actionable guidance
700
+ - Be supportive
701
+
702
+ **FORMATTING REQUIREMENTS**:
703
+ - {"Bilingual headings (" + native_lang + " | English)" if not is_english_only else "English headings"}
704
+ - Professional but accessible language
705
+ - Concise paragraphs
706
+ - Bullet points for clarity
707
+ - Total: 300-500 words
708
+
709
+ **TONE**: Professional, empathetic, culturally sensitive
710
+
711
+ **IMPORTANT**:
712
+ - Base ENTIRELY on provided data - do NOT invent
713
+ - If no recommendations, provide supportive general guidance
714
+ - Maintain medical accuracy while patient-friendly
715
+ - {"Use both " + detected_language + " and English for key medical terms" if not is_english_only else "Use clear medical English"}"""
716
+
717
+ # Prepare mappings summary (handle different field names)
718
+ mappings_summary = "\n".join([
719
+ f"- '{m.get('original_term', m.get('chinese_input', 'N/A'))}' → '{m.get('mapped_english', 'N/A')}' ({m.get('pain_type', m.get('dimension', 'N/A'))})"
720
+ for m in ontology_mappings
721
+ ]) if ontology_mappings else "No term mappings available"
722
+
723
+ recommendations_summary = "\n".join([
724
+ f"- {rec.get('triggered_by_rule', 'N/A')}: {rec.get('recommendation', 'N/A')}\n Evidence: {rec.get('evidence', {})}"
725
+ for rec in clinical_recommendations
726
+ ]) if clinical_recommendations else "No specific recommendations triggered"
727
+
728
+ # Prepare semantic analysis summary
729
+ semantic_summary = ""
730
+ if semantic_analysis and semantic_analysis.get('unmapped_analysis'):
731
+ semantic_items = []
732
+ for item in semantic_analysis['unmapped_analysis']:
733
+ original = item['original_term']
734
+ matches = item['closest_matches']
735
+ confidence = item['confidence']
736
+ top_match = matches[0]['term'] if matches else 'N/A'
737
+ score = matches[0]['score'] if matches else 0
738
+ semantic_items.append(
739
+ f" • '{original}' → closest: '{top_match}' (similarity: {score:.2f}, confidence: {confidence})"
740
+ )
741
+ semantic_summary = f"\n\n**SEMANTIC ANALYSIS** (Unmapped Terms - AI Interpretation):\n" + "\n".join(semantic_items)
742
+ semantic_summary += "\n Note: These are AI-suggested interpretations based on semantic similarity, not exact matches from the medical dictionary."
743
+
744
+ user_prompt = f"""**PATIENT'S ORIGINAL DESCRIPTION** ({detected_language}):
745
+ {original_text}
746
+
747
+ **STRUCTURED CLINICAL DATA**:
748
+ - Pain Type: {structured_data.get('pain_type', 'N/A')}
749
+ - Location: {structured_data.get('location', 'N/A')}
750
+ - Temporal Pattern: {structured_data.get('temporal_pattern', 'N/A')}
751
+ - Intensity: {structured_data.get('intensity', 'N/A')}
752
+ - Emotional Impact: {structured_data.get('emotion', 'None detected')}
753
+ - Functional Impact: {structured_data.get('functional_impact', 'Not stated')}
754
+
755
+ **TERM MAPPINGS** ({detected_language} → English):
756
+ {mappings_summary}{semantic_summary}
757
+
758
+ **CLINICAL RECOMMENDATIONS** (From rule engine):
759
+ {recommendations_summary}
760
+
761
+ Please generate a comprehensive {"bilingual" if not is_english_only else ""} clinical report."""
762
+
763
+ try:
764
+ response = client.chat.completions.create(
765
+ model="gpt-5.2",
766
+ messages=[
767
+ {"role": "system", "content": system_prompt},
768
+ {"role": "user", "content": user_prompt}
769
+ ],
770
+ temperature=0.3
771
+ )
772
+
773
+ report = response.choices[0].message.content
774
+ return report
775
+
776
+ except Exception as e:
777
+ # Fallback: structured template
778
+ fallback_report = f"""**Clinical Report | 临床报告**
779
+
780
+ **Patient Description** ({detected_language}):
781
+ {original_text}
782
+
783
+ **Assessment | 评估**:
784
+ - Pain Type: {structured_data.get('pain_type', 'N/A')}
785
+ - Location: {structured_data.get('location', 'N/A')}
786
+ - Duration: {structured_data.get('temporal_pattern', 'N/A')}
787
+
788
+ **Term Mappings | 术语映射**:
789
+ {mappings_summary}
790
+
791
+ **Recommendations | 建议**:
792
+ {recommendations_summary}
793
+
794
+ (Note: GPT report generation failed: {str(e)})"""
795
+
796
+ return fallback_report
Backend/services/neuro_symbolic_service.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Neuro-symbolic pain assessment service.
3
+
4
+ Integrates LLM extraction with ontology mapping and rule-based reasoning.
5
+ This is the main entry point for the upgraded pain assessment system.
6
+
7
+ Architecture:
8
+ - Neural: LLM for narrow-scope entity extraction
9
+ - Symbolic: Dictionary-based ontology mapping + rule-based clinical reasoning
10
+ - Output: Fully explainable clinical recommendations with evidence chains
11
+ """
12
+
13
+ import sys
14
+ import os
15
+
16
+ # Add Backend to path for imports
17
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18
+
19
+ from pipeline.pain_assessment_pipeline import PainAssessmentPipeline
20
+ from models.pain_schema import ExplainableReport
21
+ from services.llm_service import (
22
+ extract_pain_entities_constrained,
23
+ normalize_transcription,
24
+ match_pain_terms_from_vocabulary,
25
+ translate_pain_description
26
+ )
27
+ from utils.language_detector import detect_language, get_language_name
28
+
29
+
30
+ # Initialize pipeline (singleton pattern for performance)
31
+ _pipeline = None
32
+
33
+
34
+ def get_pipeline() -> PainAssessmentPipeline:
35
+ """
36
+ Get or create pipeline instance.
37
+
38
+ Uses singleton pattern to avoid reinitializing the rule engine
39
+ on every request (improves performance).
40
+
41
+ Returns:
42
+ PainAssessmentPipeline instance
43
+ """
44
+ global _pipeline
45
+ if _pipeline is None:
46
+ _pipeline = PainAssessmentPipeline(verbose=True)
47
+ return _pipeline
48
+
49
+
50
+ def analyze_pain_neuro_symbolic(patient_text: str) -> dict:
51
+ """
52
+ Main entry point for neuro-symbolic pain assessment.
53
+
54
+ This function replaces the pure LLM analysis from the old system.
55
+ It implements a complete neuro-symbolic hybrid architecture:
56
+
57
+ 1. LLM extracts entities (narrow-scope NER only)
58
+ 2. Ontology mapping converts Chinese to English medical terms
59
+ 3. Data is structured into validated Pydantic model
60
+ 4. Rule engine applies deterministic clinical logic
61
+ 5. Complete reasoning chain is generated for explainability
62
+
63
+ Args:
64
+ patient_text: Raw patient pain description (Chinese or multilingual)
65
+
66
+ Returns:
67
+ Dictionary representation of ExplainableReport containing:
68
+ - structured_data: Normalized pain ontology (PainOntology)
69
+ - ontology_mapping_trace: Chinese→English mapping details
70
+ - clinical_recommendations: Rule-triggered recommendations with evidence
71
+ - reasoning_chain: Step-by-step explanation of decision process
72
+ - physician_summary: Human-readable clinical summary
73
+
74
+ Example:
75
+ >>> result = analyze_pain_neuro_symbolic("Lower back electric-shock pain for 4 months, feeling depressed")
76
+ >>> print(result['structured_data']['pain_type'])
77
+ 'Neuropathic (Electric-shock-like)'
78
+ >>> print(result['clinical_recommendations'][0]['triggered_by_rule'])
79
+ 'RULE_A: Chronic Pain + Depression'
80
+ """
81
+ try:
82
+ # Step 0: Detect language for normalization
83
+ detected_lang = detect_language(patient_text)
84
+ language_name = get_language_name(detected_lang)
85
+
86
+ # Step 1: Normalize transcription (fix Whisper errors & standardize expressions)
87
+ print(f"[Neuro-Symbolic Service] Normalizing {language_name} transcription...")
88
+ normalization_result = normalize_transcription(patient_text, language_name)
89
+
90
+ original_text = normalization_result.get("original", patient_text)
91
+ normalized_text = normalization_result.get("normalized", patient_text)
92
+ corrections = normalization_result.get("corrections", [])
93
+ normalization_confidence = normalization_result.get("confidence", "unknown")
94
+
95
+ print(f"[Neuro-Symbolic Service] Applied {len(corrections)} corrections (confidence: {normalization_confidence})")
96
+
97
+ # Step 2: Load vocabulary for detected language
98
+ print(f"[Neuro-Symbolic Service] Loading {language_name} pain vocabulary...")
99
+ from ontology.pain_mapping_multilingual import LANGUAGE_DESCRIPTORS
100
+
101
+ vocabulary_dict = LANGUAGE_DESCRIPTORS.get(detected_lang, {})
102
+ vocabulary_list = list(vocabulary_dict.keys())
103
+
104
+ if not vocabulary_list:
105
+ print(f"[Neuro-Symbolic Service] WARNING: No vocabulary for {language_name}, using English fallback")
106
+ vocabulary_list = []
107
+ else:
108
+ print(f"[Neuro-Symbolic Service] Loaded {len(vocabulary_list)} terms for {language_name}")
109
+
110
+ # Step 3: LLM term matching (with vocabulary)
111
+ print("[Neuro-Symbolic Service] Matching pain terms from vocabulary...")
112
+
113
+ llm_match_result = match_pain_terms_from_vocabulary(
114
+ normalized_text,
115
+ vocabulary_list,
116
+ language_name
117
+ )
118
+
119
+ matched_terms = llm_match_result.get('matched_terms', [])
120
+ print(f"[Neuro-Symbolic Service] LLM matched {len(matched_terms)} terms: {matched_terms}")
121
+
122
+ # Step 4: Translate matched terms using dictionary
123
+ print("[Neuro-Symbolic Service] Translating matched terms...")
124
+ ontology_mappings = []
125
+
126
+ for term in matched_terms:
127
+ if term in vocabulary_dict:
128
+ term_data = vocabulary_dict[term]
129
+ ontology_mappings.append({
130
+ "original_term": term,
131
+ "matched_text": term,
132
+ "mapped_english": term_data["english"],
133
+ "dimension": term_data.get("dimension", "sensory"),
134
+ "pain_type": term_data.get("pain_type"),
135
+ "confidence": "high", # High confidence because it's from dictionary
136
+ "mcgill_dimension": term_data.get("mcgill_dimension", "sensory"),
137
+ "detected_language": detected_lang
138
+ })
139
+
140
+ print(f"[Neuro-Symbolic Service] Translated {len(ontology_mappings)} terms to English")
141
+
142
+ # Step 4.5: Extract unique/unmapped pain descriptors (creative expressions not in dictionary)
143
+ print("[Neuro-Symbolic Service] Extracting unique pain descriptors...")
144
+ from services.llm_service import extract_pain_entities_constrained
145
+ unique_entities = extract_pain_entities_constrained(normalized_text)
146
+ unique_descriptors = unique_entities.get("pain_descriptors", [])
147
+
148
+ # These are creative/metaphorical descriptions not in our dictionary
149
+ # Examples: "好像蚂蚁在爬", "说不出来的难受", "像被火烧一样"
150
+ # V2: Will use multilingual dictionary (Chinese/Korean/Spanish/Hmong) for semantic matching
151
+ if unique_descriptors:
152
+ print(f"[Neuro-Symbolic Service] Found {len(unique_descriptors)} unique descriptors: {unique_descriptors}")
153
+ print(f"[Neuro-Symbolic Service] → Will match against multilingual pain dictionary ({language_name})")
154
+
155
+ # Prepare LLM entities for pipeline
156
+ llm_entities = {
157
+ "pain_descriptors": unique_descriptors, # Original native language text (for multilingual semantic analysis V2)
158
+ "location": llm_match_result.get("location", "Not stated"),
159
+ "duration_phrase": llm_match_result.get("duration_phrase", "Not stated"),
160
+ "intensity": llm_match_result.get("intensity", "Not stated"),
161
+ "emotion_keywords": llm_match_result.get("emotion_keywords", []),
162
+ "functional_impact": llm_match_result.get("functional_impact")
163
+ }
164
+
165
+ # Step 5: Execute complete pipeline (ontology mapping + rule engine)
166
+ print("[Neuro-Symbolic Service] Executing pipeline...")
167
+ pipeline = get_pipeline()
168
+ report: ExplainableReport = pipeline.execute_with_mappings(
169
+ normalized_text,
170
+ llm_entities,
171
+ ontology_mappings # Pass pre-computed mappings
172
+ )
173
+
174
+ # Step 6: Translate full sentence to English (if not English)
175
+ english_translation = None
176
+ if detected_lang != 'en':
177
+ print(f"[Neuro-Symbolic Service] Translating from {language_name} to English...")
178
+ english_translation = translate_pain_description(
179
+ normalized_text,
180
+ language_name,
181
+ matched_terms,
182
+ ontology_mappings
183
+ )
184
+ print(f"[Neuro-Symbolic Service] Translation: {english_translation}")
185
+
186
+ # Step 7: Convert Pydantic models to dictionary for API response
187
+ return {
188
+ "status": "success",
189
+ "transcription": {
190
+ "original": original_text,
191
+ "normalized": normalized_text,
192
+ "english_translation": english_translation, # NEW: Full sentence translation
193
+ "corrections_applied": corrections,
194
+ "normalization_confidence": normalization_confidence,
195
+ "language_detected": language_name,
196
+ "vocabulary_size": len(vocabulary_list),
197
+ "matched_terms_count": len(matched_terms)
198
+ },
199
+ "structured_data": report.structured_data.model_dump(),
200
+ "ontology_mapping_trace": report.ontology_mapping_trace,
201
+ "clinical_recommendations": [
202
+ rec.model_dump() for rec in report.clinical_recommendations
203
+ ],
204
+ "reasoning_chain": report.reasoning_chain,
205
+ "physician_summary": report.physician_summary
206
+ }
207
+
208
+ except Exception as e:
209
+ # Return error with detailed information for debugging
210
+ import traceback
211
+ return {
212
+ "status": "error",
213
+ "message": f"Pipeline execution failed: {str(e)}",
214
+ "error_type": type(e).__name__,
215
+ "traceback": traceback.format_exc()
216
+ }
217
+
218
+
219
+ def get_system_info() -> dict:
220
+ """
221
+ Get information about the neuro-symbolic system configuration.
222
+
223
+ Useful for debugging, monitoring, and documentation.
224
+
225
+ Returns:
226
+ Dictionary with system metadata
227
+ """
228
+ pipeline = get_pipeline()
229
+
230
+ return {
231
+ "system_name": "Neuro-Symbolic Pain Assessment System",
232
+ "version": "1.0.0",
233
+ "architecture": "Hybrid (Neural + Symbolic)",
234
+ "components": {
235
+ "neural": {
236
+ "llm_model": "GPT-5.2",
237
+ "purpose": "Named Entity Recognition only",
238
+ "temperature": 0.1
239
+ },
240
+ "symbolic": {
241
+ "ontology_mappings": "Chinese↔English pain descriptors",
242
+ "rule_engine": "Deterministic If-Then clinical rules",
243
+ "knowledge_bases": [
244
+ "McGill Pain Questionnaire (SF-MPQ)",
245
+ "SNOMED CT",
246
+ "Wisconsin Medical Examining Board Guidelines"
247
+ ]
248
+ }
249
+ },
250
+ "pipeline_info": pipeline.get_pipeline_info(),
251
+ "capabilities": [
252
+ "Cross-cultural pain assessment (Chinese→English)",
253
+ "Deterministic clinical reasoning",
254
+ "Complete explainability with evidence chains",
255
+ "Neuropathic vs Nociceptive pain classification",
256
+ "Guideline-based clinical recommendations"
257
+ ],
258
+ "limitations": [
259
+ "Ontology coverage limited to defined descriptors",
260
+ "Rule engine contains 4 clinical rules (expandable)",
261
+ "Requires manual review for unmapped terms",
262
+ "Not a diagnostic tool - for triage and assessment only"
263
+ ]
264
+ }
265
+
266
+
267
+ def validate_input(patient_text: str) -> tuple[bool, str]:
268
+ """
269
+ Validate patient input before processing.
270
+
271
+ Args:
272
+ patient_text: Raw patient input
273
+
274
+ Returns:
275
+ Tuple of (is_valid, error_message)
276
+ If valid, error_message is empty string
277
+ """
278
+ if not patient_text or not patient_text.strip():
279
+ return False, "Input text is empty"
280
+
281
+ if len(patient_text) < 5:
282
+ return False, "Input text too short (minimum 5 characters)"
283
+
284
+ if len(patient_text) > 10000:
285
+ return False, "Input text too long (maximum 10000 characters)"
286
+
287
+ return True, ""
288
+
289
+
290
+ # Batch processing support (for future use)
291
+ def analyze_pain_batch(patient_texts: list[str]) -> list[dict]:
292
+ """
293
+ Process multiple patient descriptions in batch.
294
+
295
+ Args:
296
+ patient_texts: List of patient pain descriptions
297
+
298
+ Returns:
299
+ List of analysis results (one per input)
300
+ """
301
+ results = []
302
+ for text in patient_texts:
303
+ results.append(analyze_pain_neuro_symbolic(text))
304
+ return results
Backend/services/semantic_distance_service.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ import numpy as np
3
+ from typing import List, Dict
4
+ import os
5
+
6
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
7
+
8
+ # Embed the dictionary terms once and cache them for future use
9
+ DICTIONARY_EMBEDDINGS_CACHE = None
10
+
11
+ def get_standard_pain_terms():
12
+ """Return a list of standard pain terms for semantic distance calculation."""
13
+ return [
14
+ "burning", "tingling", "shooting", "stabbing", "aching",
15
+ "throbbing", "sharp", "dull", "cramping", "electric-shock-like",
16
+ "pricking", "numb", "crawling", "tight", "heavy"
17
+ ]
18
+
19
+ def precompute_dictionary_embeddings():
20
+ """Precompute and cache dictionary embeddings at system startup."""
21
+ global DICTIONARY_EMBEDDINGS_CACHE
22
+
23
+ terms = get_standard_pain_terms()
24
+ response = client.embeddings.create(
25
+ model="text-embedding-3-small", # inexpensive
26
+ input=terms
27
+ )
28
+
29
+ DICTIONARY_EMBEDDINGS_CACHE = {
30
+ "terms": terms,
31
+ "embeddings": [item.embedding for item in response.data]
32
+ }
33
+ print(f"[Init] Cached {len(terms)} dictionary embeddings")
34
+
35
+ def calculate_semantic_distances(
36
+ unmapped_terms: List[str],
37
+ patient_text: str,
38
+ language: str,
39
+ translated_terms: List[str] = None # Pre-translated terms (optional)
40
+ ) -> Dict:
41
+ """
42
+ Calculate semantic distances only for unmapped terms.
43
+
44
+ Args:
45
+ unmapped_terms: Original pain expressions (any language)
46
+ patient_text: Full patient text (for context)
47
+ language: Detected language name
48
+ translated_terms: Optional pre-translated English versions of unmapped_terms
49
+ If provided, will use these directly instead of translating again
50
+
51
+ Returns:
52
+ Dictionary with unmapped_analysis containing similarity scores
53
+ """
54
+ if not unmapped_terms:
55
+ return None
56
+
57
+ # Ensure dictionary embeddings are loaded
58
+ if DICTIONARY_EMBEDDINGS_CACHE is None:
59
+ precompute_dictionary_embeddings()
60
+
61
+ # Use pre-translated terms if provided, otherwise translate now
62
+ if translated_terms and len(translated_terms) == len(unmapped_terms):
63
+ print(f"[Semantic Distance] Using pre-translated terms")
64
+ terms_to_embed = translated_terms
65
+ elif language != "English":
66
+ print(f"[Semantic Distance] Translating {len(unmapped_terms)} non-English terms to English...")
67
+ translated_terms = []
68
+ for term in unmapped_terms:
69
+ try:
70
+ # Quick translation to English for semantic matching
71
+ response = client.chat.completions.create(
72
+ model="gpt-4o-mini", # Use faster model for translation
73
+ messages=[
74
+ {"role": "system", "content": "Translate pain descriptions to concise medical English. Output ONLY the translation, no explanations."},
75
+ {"role": "user", "content": f"Translate to medical English: {term}"}
76
+ ],
77
+ temperature=0.1,
78
+ max_tokens=50
79
+ )
80
+ translated = response.choices[0].message.content.strip().strip('"\'')
81
+ translated_terms.append(translated)
82
+ print(f"[Semantic Distance] '{term[:40]}...' → '{translated}'")
83
+ except Exception as e:
84
+ print(f"[Semantic Distance] Translation failed for '{term[:40]}...', using original")
85
+ translated_terms.append(term)
86
+
87
+ terms_to_embed = translated_terms
88
+ else:
89
+ terms_to_embed = unmapped_terms
90
+
91
+ # Get embeddings for (translated) unmapped terms
92
+ response = client.embeddings.create(
93
+ model="text-embedding-3-small",
94
+ input=terms_to_embed
95
+ )
96
+ unmapped_embeddings = [item.embedding for item in response.data]
97
+
98
+ # Calculate similarities
99
+ results = []
100
+ for i, original_term in enumerate(unmapped_terms):
101
+ similarities = []
102
+ for j, dict_term in enumerate(DICTIONARY_EMBEDDINGS_CACHE["terms"]):
103
+ score = cosine_similarity(
104
+ unmapped_embeddings[i],
105
+ DICTIONARY_EMBEDDINGS_CACHE["embeddings"][j]
106
+ )
107
+ similarities.append({"term": dict_term, "score": round(score, 3)})
108
+
109
+ # Top 3
110
+ top_matches = sorted(similarities, key=lambda x: x['score'], reverse=True)[:3]
111
+ confidence = "high" if top_matches[0]['score'] > 0.75 else \
112
+ "medium" if top_matches[0]['score'] > 0.60 else "low"
113
+
114
+ results.append({
115
+ "original_term": original_term,
116
+ "translated_term": terms_to_embed[i] if language != "English" else None,
117
+ "closest_matches": top_matches,
118
+ "confidence": confidence
119
+ })
120
+
121
+ return {"unmapped_analysis": results}
122
+
123
+ def cosine_similarity(a, b):
124
+ return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
Backend/services/semantic_distance_service_biolord.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Semantic Distance Service (Version 3 - BioLORD Medical Specialist)
3
+
4
+ Uses BioLORD-2023-M (Medical Language Model) for semantic similarity calculation.
5
+ BioLORD is specifically trained on medical ontologies and achieves SOTA performance
6
+ on medical semantic similarity tasks (MedSTS, EHR-Rel-B).
7
+
8
+ **Why BioLORD-2023-M:**
9
+ - SOTA on MedSTS (Medical Semantic Text Similarity) benchmark
10
+ - Trained on medical concept definitions from AGCT (Auto-Clinical Terminology)
11
+ - 50+ language support including Chinese, Korean, Spanish
12
+ - Understands medical ontology hierarchies
13
+ - 5-6x better accuracy on medical concept similarity vs general models
14
+
15
+ **Architecture (Same-Language Matching):**
16
+ 1. Chinese patient "蚂蚁爬" → BioLORD embedding (768-dim)
17
+ 2. Compare with Chinese McGill translations → Match "蚁爬感"
18
+ 3. Return English StandardTerm: "formication"
19
+
20
+ **Key Difference from System Dictionary:**
21
+ - System dictionary: multilingual_pain_data.json (used by main pipeline)
22
+ - This service: McGill translations (auxiliary/fallback matching)
23
+ - No duplication - serves as complement when system dictionary has no match
24
+
25
+ **Advantages over OpenAI embeddings:**
26
+ - ✅ Medical domain expertise (trained on UMLS/AGCT)
27
+ - ✅ Better understanding of pain terminology nuances
28
+ - ✅ Fully local deployment (no API costs, complete privacy)
29
+ - ✅ Offline capability
30
+ - ✅ Consistent performance (no API rate limits)
31
+
32
+ **Cost:** FREE after initial model download (~1GB one-time)
33
+ """
34
+
35
+ import numpy as np
36
+ from sentence_transformers import SentenceTransformer
37
+ from typing import List, Dict
38
+ import os
39
+
40
+ # Import McGill Pain Questionnaire translations (auxiliary matching, not system dictionary)
41
+ from ontology.mcgill_translations import (
42
+ CHINESE_MCGILL,
43
+ KOREAN_MCGILL,
44
+ SPANISH_MCGILL,
45
+ HMONG_MCGILL
46
+ )
47
+
48
+ # Global model instance (loaded once at startup)
49
+ _biolord_model = None
50
+
51
+ # Cache for McGill multilingual embeddings (same-language medical matching)
52
+ MCGILL_EMBEDDINGS_CACHE = {}
53
+
54
+
55
+ def load_biolord_model():
56
+ """
57
+ Load BioLORD-2023-M model from Hugging Face.
58
+
59
+ Model will be downloaded to ~/.cache/huggingface/ on first run.
60
+ Subsequent runs load from local cache (instant).
61
+
62
+ Model size: ~1GB
63
+ Download time: ~2-5 minutes (one-time, depends on internet speed)
64
+ """
65
+ global _biolord_model
66
+
67
+ if _biolord_model is not None:
68
+ return _biolord_model
69
+
70
+ print("[BioLORD] Loading BioLORD-2023-M model...")
71
+ print("[BioLORD] Model: FremyCompany/BioLORD-2023-M")
72
+ print("[BioLORD] Size: ~1GB (downloads to ~/.cache/huggingface/)")
73
+
74
+ try:
75
+ _biolord_model = SentenceTransformer("FremyCompany/BioLORD-2023-M")
76
+ print("[BioLORD] ✓ Model loaded successfully")
77
+ print(f"[BioLORD] Embedding dimension: {_biolord_model.get_sentence_embedding_dimension()}")
78
+ return _biolord_model
79
+
80
+ except Exception as e:
81
+ print(f"[BioLORD] ❌ Failed to load model: {e}")
82
+ print("[BioLORD] Falling back to OpenAI embeddings...")
83
+ raise
84
+
85
+
86
+ def precompute_dictionary_embeddings():
87
+ """
88
+ Precompute BioLORD embeddings for McGill Pain Questionnaire translations.
89
+
90
+ **Same-Language Medical Matching:**
91
+ - Chinese patient "蚂蚁爬" → Chinese McGill "蚁爬感" → English "formication"
92
+ - Korean patient "개미 감각" → Korean McGill "개미가 기어가는 느낌" → English "formication"
93
+
94
+ **Why McGill translations (not system dictionary):**
95
+ System already uses multilingual_pain_data.json. This serves as auxiliary/fallback
96
+ using standardized McGill medical terminology when primary dictionary has no match.
97
+ """
98
+ global MCGILL_EMBEDDINGS_CACHE
99
+
100
+ # Load BioLORD model
101
+ try:
102
+ model = load_biolord_model()
103
+ except Exception as e:
104
+ print(f"[BioLORD] Cannot precompute embeddings - model load failed: {e}")
105
+ return
106
+
107
+ # Language configurations - McGill translations for auxiliary matching
108
+ language_configs = {
109
+ 'zh': {'name': 'Chinese', 'mcgill': CHINESE_MCGILL},
110
+ 'ko': {'name': 'Korean', 'mcgill': KOREAN_MCGILL},
111
+ 'es': {'name': 'Spanish', 'mcgill': SPANISH_MCGILL},
112
+ 'hmong': {'name': 'Hmong', 'mcgill': HMONG_MCGILL}
113
+ }
114
+
115
+ print(f"[BioLORD] Precomputing McGill translations for {len(language_configs)} languages...")
116
+ print(f"[BioLORD] Source: McGill Pain Questionnaire (auxiliary matching)")
117
+
118
+ for lang_code, config in language_configs.items():
119
+ mcgill_dict = config['mcgill']
120
+ lang_name = config['name']
121
+
122
+ if not mcgill_dict:
123
+ print(f"[BioLORD] ⚠️ {lang_name}: No McGill translations, skipping")
124
+ continue
125
+
126
+ # Extract all McGill terms + aliases in native language
127
+ all_terms = []
128
+ term_metadata = []
129
+
130
+ for native_term, metadata in mcgill_dict.items():
131
+ # Add main McGill term
132
+ all_terms.append(native_term)
133
+ term_metadata.append({
134
+ "native_term": native_term,
135
+ "english": metadata["english"],
136
+ "pain_type": metadata["type"],
137
+ "dimension": metadata["dimension"],
138
+ "is_alias": False
139
+ })
140
+
141
+ # Add aliases
142
+ for alias in metadata.get("aliases", []):
143
+ all_terms.append(alias)
144
+ term_metadata.append({
145
+ "native_term": alias,
146
+ "english": metadata["english"],
147
+ "pain_type": metadata["type"],
148
+ "dimension": metadata["dimension"],
149
+ "is_alias": True,
150
+ "parent_term": native_term
151
+ })
152
+
153
+ if not all_terms:
154
+ print(f"[BioLORD] ⚠️ {lang_name}: No terms extracted, skipping")
155
+ continue
156
+
157
+ print(f"[BioLORD] {lang_name} McGill: Processing {len(all_terms)} terms...")
158
+
159
+ # Generate embeddings using BioLORD (medical-grade within-language understanding)
160
+ try:
161
+ embeddings = model.encode(
162
+ all_terms,
163
+ batch_size=32,
164
+ show_progress_bar=False,
165
+ convert_to_numpy=True
166
+ )
167
+
168
+ MCGILL_EMBEDDINGS_CACHE[lang_code] = {
169
+ "terms": all_terms,
170
+ "embeddings": embeddings,
171
+ "metadata": term_metadata
172
+ }
173
+
174
+ print(f"[BioLORD] ✓ {lang_name}: Cached {len(all_terms)} McGill terms")
175
+
176
+ except Exception as e:
177
+ print(f"[BioLORD] ❌ {lang_name}: Embedding failed - {e}")
178
+
179
+ total_terms = sum(len(cache["terms"]) for cache in MCGILL_EMBEDDINGS_CACHE.values())
180
+ print(f"[BioLORD] ✓ Total: Cached {total_terms} McGill terms across {len(MCGILL_EMBEDDINGS_CACHE)} languages")
181
+ print(f"[BioLORD] 🎯 Same-language medical semantic matching ready (auxiliary service)")
182
+ print(f"[BioLORD] 📋 Complements system dictionary (multilingual_pain_data.json)")
183
+
184
+
185
+ def cosine_similarity(vec1, vec2):
186
+ """Calculate cosine similarity between two vectors."""
187
+ vec1 = np.array(vec1)
188
+ vec2 = np.array(vec2)
189
+ return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
190
+
191
+
192
+ def calculate_semantic_distances(
193
+ unmapped_terms: List[str],
194
+ patient_text: str,
195
+ language: str,
196
+ translated_terms: List[str] = None
197
+ ) -> Dict:
198
+ """
199
+ Calculate semantic distances using BioLORD medical embeddings.
200
+
201
+ **Same-Language Medical Matching:**
202
+ Uses McGill translations for auxiliary matching when system dictionary has no match.
203
+
204
+ Example:
205
+ - Chinese patient: "像蚂蚁在爬"
206
+ - BioLORD → Chinese McGill: "蚁爬感" (formication)
207
+ - Return: English standard term "formication"
208
+
209
+ **Advantage over cross-lingual:**
210
+ BioLORD medical training excels at understanding medical semantics WITHIN each language.
211
+
212
+ Args:
213
+ unmapped_terms: Pain expressions not found in system dictionary
214
+ patient_text: Full patient text (for context)
215
+ language: Detected language name (e.g., "Chinese", "Korean", "Spanish")
216
+ translated_terms: [DEPRECATED] Not used
217
+
218
+ Returns:
219
+ Dictionary with medical-semantic analysis using McGill auxiliary matching
220
+ """
221
+ if not unmapped_terms:
222
+ return None
223
+
224
+ # Map language names to codes
225
+ language_map = {
226
+ "Chinese": "zh",
227
+ "Korean": "ko",
228
+ "Spanish": "es",
229
+ "Hmong": "hmong",
230
+ "English": "en"
231
+ }
232
+
233
+ lang_code = language_map.get(language, "en")
234
+
235
+ # Skip for English or unsupported languages
236
+ if lang_code == "en":
237
+ print(f"[BioLORD] Skipping - English terms already standardized")
238
+ return None
239
+
240
+ # Check if we have McGill translations for this language
241
+ if lang_code not in MCGILL_EMBEDDINGS_CACHE:
242
+ print(f"[BioLORD] ⚠️ No McGill translations cached for {language}")
243
+ return None
244
+
245
+ # Load model if not already loaded
246
+ try:
247
+ model = load_biolord_model()
248
+ except Exception as e:
249
+ print(f"[BioLORD] ❌ Model not available: {e}")
250
+ return None
251
+
252
+ # Get language-specific McGill cache
253
+ lang_cache = MCGILL_EMBEDDINGS_CACHE[lang_code]
254
+
255
+ print(f"[BioLORD] Same-language matching: {len(unmapped_terms)} {language} terms → {language} McGill")
256
+ print(f"[BioLORD] Target: {len(lang_cache['terms'])} McGill translations")
257
+
258
+ # Generate embeddings for patient's terms using BioLORD
259
+ unmapped_embeddings = model.encode(
260
+ unmapped_terms,
261
+ batch_size=16,
262
+ show_progress_bar=False,
263
+ convert_to_numpy=True
264
+ )
265
+
266
+ # Calculate same-language medical-semantic similarities
267
+ results = []
268
+ for i, patient_term in enumerate(unmapped_terms):
269
+ similarities = []
270
+
271
+ # Compare patient's term with same-language McGill translations
272
+ for j, mcgill_data in enumerate(lang_cache["metadata"]):
273
+ score = cosine_similarity(
274
+ unmapped_embeddings[i],
275
+ lang_cache["embeddings"][j]
276
+ )
277
+
278
+ similarities.append({
279
+ "native_term": mcgill_data["native_term"],
280
+ "english": mcgill_data["english"],
281
+ "pain_type": mcgill_data["pain_type"],
282
+ "dimension": mcgill_data["dimension"],
283
+ "is_alias": mcgill_data.get("is_alias", False),
284
+ "score": float(score)
285
+ })
286
+
287
+ # Sort by similarity (highest first)
288
+ similarities.sort(key=lambda x: x["score"], reverse=True)
289
+ top_matches = similarities[:3]
290
+
291
+ # Determine confidence level
292
+ best_score = top_matches[0]["score"]
293
+ if best_score > 0.75:
294
+ confidence = "high"
295
+ elif best_score > 0.60:
296
+ confidence = "medium"
297
+ else:
298
+ confidence = "low"
299
+
300
+ # Same-language result: patient's language → same language McGill → English
301
+ result = {
302
+ "original_term": patient_term, # e.g., "蚂蚁爬"
303
+ "matched_mcgill_native": top_matches[0]["native_term"], # e.g., "蚁爬感"
304
+ "matched_standard_english": top_matches[0]["english"], # e.g., "formication"
305
+ "closest_matches": top_matches,
306
+ "confidence": confidence,
307
+ "language": lang_code,
308
+ "model": "BioLORD-2023-M (same-language)"
309
+ }
310
+
311
+ results.append(result)
312
+
313
+ print(f"[BioLORD] '{patient_term}' → '{top_matches[0]['native_term']}' ({top_matches[0]['english']}) [score: {best_score:.3f}, {confidence}]")
314
+
315
+ return {
316
+ "unmapped_analysis": results,
317
+ "model_used": "BioLORD-2023-M",
318
+ "matching_strategy": "same-language-mcgill",
319
+ "language": language
320
+ }
Backend/services/semantic_distance_service_v2.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Semantic Distance Service (Version 2 - Multilingual)
3
+
4
+ Uses OpenAI embeddings to calculate semantic similarity between patient's
5
+ unmapped pain expressions and our multilingual medical pain dictionary.
6
+
7
+ **Improved Architecture (User-Suggested):**
8
+ 1. Patient expression (any language) → Compare with native language dictionary embeddings
9
+ 2. Find best matching term in native language dictionary (e.g., Korean "따끔거리다")
10
+ 3. Use dictionary's standard English translation (e.g., "Pricking")
11
+ 4. NO GPT translation needed - uses pre-defined medical translations
12
+
13
+ **Supported Languages:**
14
+ - Chinese (中文): 373+ terms from CHINESE_PAIN_DESCRIPTORS
15
+ - Korean (한국어): 131+ terms from KOREAN_PAIN_DESCRIPTORS
16
+ - Spanish (Español): 74+ terms from SPANISH_PAIN_DESCRIPTORS
17
+ - Hmong: Terms from HMONG_PAIN_DESCRIPTORS
18
+ - English: Pass-through (no mapping needed)
19
+
20
+ Advantages:
21
+ - Native language → Native language semantic space (more accurate)
22
+ - Medical-grade English translations (standardized)
23
+ - Faster (no GPT translation API calls)
24
+ - Cost-effective (~$0.02 per 1M tokens for embeddings only)
25
+
26
+ Cost: ~$0.02 per 1M tokens (text-embedding-3-small)
27
+ """
28
+
29
+ import numpy as np
30
+ from openai import OpenAI
31
+ from typing import List, Dict
32
+ import os
33
+ from ontology.pain_mapping_multilingual import (
34
+ CHINESE_PAIN_DESCRIPTORS,
35
+ KOREAN_PAIN_DESCRIPTORS,
36
+ SPANISH_PAIN_DESCRIPTORS,
37
+ HMONG_PAIN_DESCRIPTORS
38
+ )
39
+
40
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
41
+
42
+ # Cache for dictionary embeddings (precomputed at startup)
43
+ # Structure: {language_code: {terms: [...], embeddings: [...], metadata: [...]}}
44
+ DICTIONARY_EMBEDDINGS_CACHE = {}
45
+
46
+
47
+ def precompute_dictionary_embeddings():
48
+ """
49
+ Precompute embeddings for ALL multilingual pain terms at app startup.
50
+
51
+ Processes dictionaries for:
52
+ - Chinese (中文): ~373 terms + aliases
53
+ - Korean (한국어): ~131 terms + aliases
54
+ - Spanish (Español): ~74 terms + aliases
55
+ - Hmong: All defined terms
56
+
57
+ Each language gets its own embedding cache for accurate native-language matching.
58
+ """
59
+ global DICTIONARY_EMBEDDINGS_CACHE
60
+
61
+ # Language configurations
62
+ language_configs = {
63
+ 'zh': {'name': 'Chinese', 'descriptors': CHINESE_PAIN_DESCRIPTORS},
64
+ 'ko': {'name': 'Korean', 'descriptors': KOREAN_PAIN_DESCRIPTORS},
65
+ 'es': {'name': 'Spanish', 'descriptors': SPANISH_PAIN_DESCRIPTORS},
66
+ 'hmong': {'name': 'Hmong', 'descriptors': HMONG_PAIN_DESCRIPTORS}
67
+ }
68
+
69
+ print(f"[Semantic Distance V2] Precomputing embeddings for {len(language_configs)} languages...")
70
+
71
+ for lang_code, config in language_configs.items():
72
+ descriptors = config['descriptors']
73
+ lang_name = config['name']
74
+
75
+ if not descriptors:
76
+ print(f"[Semantic Distance V2] ⚠️ {lang_name}: No descriptors found, skipping")
77
+ continue
78
+
79
+ # Extract all terms + aliases from dictionary
80
+ all_terms = []
81
+ term_metadata = [] # Store: term → English translation + metadata
82
+
83
+ for term_key, metadata in descriptors.items():
84
+ # Add main term
85
+ all_terms.append(term_key)
86
+ term_metadata.append({
87
+ "native_term": term_key,
88
+ "english": metadata.get("english", metadata.get("mapped_english", "Unknown")),
89
+ "pain_type": metadata.get("pain_type", "unknown"),
90
+ "dimension": metadata.get("dimension", "sensory"),
91
+ "is_alias": False
92
+ })
93
+
94
+ # Add aliases if available
95
+ for alias in metadata.get("aliases", []):
96
+ all_terms.append(alias)
97
+ term_metadata.append({
98
+ "native_term": alias,
99
+ "english": metadata.get("english", metadata.get("mapped_english", "Unknown")),
100
+ "pain_type": metadata.get("pain_type", "unknown"),
101
+ "dimension": metadata.get("dimension", "sensory"),
102
+ "is_alias": True,
103
+ "parent_term": term_key
104
+ })
105
+
106
+ if not all_terms:
107
+ print(f"[Semantic Distance V2] ⚠️ {lang_name}: No terms extracted, skipping")
108
+ continue
109
+
110
+ print(f"[Semantic Distance V2] {lang_name}: Processing {len(all_terms)} terms...")
111
+
112
+ # Batch API call to get embeddings for all terms in this language
113
+ try:
114
+ response = client.embeddings.create(
115
+ model="text-embedding-3-small",
116
+ input=all_terms
117
+ )
118
+
119
+ embeddings = [item.embedding for item in response.data]
120
+
121
+ DICTIONARY_EMBEDDINGS_CACHE[lang_code] = {
122
+ "terms": all_terms,
123
+ "embeddings": embeddings,
124
+ "metadata": term_metadata
125
+ }
126
+
127
+ print(f"[Semantic Distance V2] ✓ {lang_name}: Cached {len(all_terms)} term embeddings")
128
+
129
+ except Exception as e:
130
+ print(f"[Semantic Distance V2] ❌ {lang_name}: Embedding failed - {e}")
131
+
132
+ total_terms = sum(len(cache["terms"]) for cache in DICTIONARY_EMBEDDINGS_CACHE.values())
133
+ print(f"[Semantic Distance V2] ✓ Total: Cached {total_terms} terms across {len(DICTIONARY_EMBEDDINGS_CACHE)} languages")
134
+
135
+
136
+ def calculate_semantic_distances(
137
+ unmapped_terms: List[str],
138
+ patient_text: str,
139
+ language: str,
140
+ translated_terms: List[str] = None # Deprecated parameter (backward compatibility)
141
+ ) -> Dict:
142
+ """
143
+ Calculate semantic distances for unmapped pain expressions in ANY supported language.
144
+
145
+ **Multilingual Flow:**
146
+ 1. Chinese patient: "像有成千上万只蚂蚁在皮肤下面爬来爬去"
147
+ → Compare with Chinese dictionary → Match "蚂蚁爬" → Return "Formication (crawling)"
148
+
149
+ 2. Korean patient: "허리가 따끔거리듯이 아프다"
150
+ → Compare with Korean dictionary → Match "따끔거리다" → Return "Pricking"
151
+
152
+ 3. Spanish patient: "dolor punzante en la espalda"
153
+ → Compare with Spanish dictionary → Match "punzante" → Return "Stabbing"
154
+
155
+ Args:
156
+ unmapped_terms: Original pain expressions in patient's native language
157
+ patient_text: Full patient text (for logging)
158
+ language: Detected language name (e.g., "Chinese", "Korean", "Spanish", "Hmong")
159
+ translated_terms: [DEPRECATED] Pre-translated terms (no longer used)
160
+
161
+ Returns:
162
+ Dictionary with unmapped_analysis containing:
163
+ - original_term: Patient's native language expression
164
+ - matched_native_term: Best matching dictionary term (native language)
165
+ - standard_english: Dictionary's medical English translation
166
+ - closest_matches: Top 3 similar dictionary terms
167
+ - confidence: high/medium/low based on similarity score
168
+ - language: Detected language code
169
+ """
170
+ if not unmapped_terms:
171
+ return None
172
+
173
+ # Map language names to codes
174
+ language_map = {
175
+ "Chinese": "zh",
176
+ "Korean": "ko",
177
+ "Spanish": "es",
178
+ "Hmong": "hmong",
179
+ "English": "en"
180
+ }
181
+
182
+ lang_code = language_map.get(language, "en")
183
+
184
+ # Skip semantic analysis for English (already standardized) or unsupported languages
185
+ if lang_code == "en":
186
+ print(f"[Semantic Distance V2] Skipping - English terms already standardized")
187
+ return None
188
+
189
+ # Ensure dictionary embeddings are loaded
190
+ if not DICTIONARY_EMBEDDINGS_CACHE:
191
+ precompute_dictionary_embeddings()
192
+
193
+ # Check if this language is supported
194
+ if lang_code not in DICTIONARY_EMBEDDINGS_CACHE:
195
+ print(f"[Semantic Distance V2] ⚠️ Language '{language}' ({lang_code}) not supported - no dictionary available")
196
+ return None
197
+
198
+ lang_cache = DICTIONARY_EMBEDDINGS_CACHE[lang_code]
199
+
200
+ print(f"[Semantic Distance V2] Analyzing {len(unmapped_terms)} {language} unmapped terms...")
201
+ print(f"[Semantic Distance V2] Using {language} dictionary: {len(lang_cache['terms'])} terms")
202
+
203
+ # Get embeddings for patient's unmapped terms (in their native language)
204
+ response = client.embeddings.create(
205
+ model="text-embedding-3-small",
206
+ input=unmapped_terms
207
+ )
208
+ unmapped_embeddings = [item.embedding for item in response.data]
209
+
210
+ # Calculate similarities between patient terms and dictionary
211
+ results = []
212
+ for i, patient_term in enumerate(unmapped_terms):
213
+ similarities = []
214
+
215
+ # Compare with all dictionary terms in this language
216
+ for j, dict_data in enumerate(lang_cache["metadata"]):
217
+ score = cosine_similarity(
218
+ unmapped_embeddings[i],
219
+ lang_cache["embeddings"][j]
220
+ )
221
+ similarities.append({
222
+ "native_term": dict_data["native_term"],
223
+ "english": dict_data["english"],
224
+ "pain_type": dict_data["pain_type"],
225
+ "dimension": dict_data["dimension"],
226
+ "score": round(score, 3)
227
+ })
228
+
229
+ # Get Top 3 matches
230
+ top_matches = sorted(similarities, key=lambda x: x['score'], reverse=True)[:3]
231
+
232
+ # Determine confidence level
233
+ best_score = top_matches[0]['score']
234
+ if best_score > 0.75:
235
+ confidence = "high"
236
+ elif best_score > 0.60:
237
+ confidence = "medium"
238
+ else:
239
+ confidence = "low"
240
+
241
+ # Log best match
242
+ print(f"[Semantic Distance V2] '{patient_term[:50]}' → '{top_matches[0]['native_term']}' "
243
+ f"({top_matches[0]['english']}) [score: {best_score:.3f}]")
244
+
245
+ results.append({
246
+ "original_term": patient_term, # Patient's native language expression
247
+ "matched_native_term": top_matches[0]['native_term'], # Best dictionary term (native)
248
+ "standard_english": top_matches[0]['english'], # Medical English translation
249
+ "closest_matches": top_matches, # Top 3 for display
250
+ "confidence": confidence,
251
+ "language": lang_code
252
+ })
253
+
254
+ return {"unmapped_analysis": results}
255
+
256
+
257
+ def cosine_similarity(a, b):
258
+ """Calculate cosine similarity between two vectors."""
259
+ return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
Backend/services/whisper_service.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from openai import OpenAI
4
+ from typing import Optional
5
+ from dotenv import load_dotenv
6
+ load_dotenv() # Load environment variables from .env file
7
+
8
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
9
+
10
+ def transcribeAudio(audioBytes: bytes, language: Optional[str] = None) -> dict:
11
+ try:
12
+ audioFile = ("audio.mp3", audioBytes, "audio/mpeg")
13
+
14
+ response = client.audio.transcriptions.create(
15
+ model="whisper-1",
16
+ file=audioFile,
17
+ language=language,
18
+ response_format = "json"
19
+ )
20
+ return {
21
+ "text": response.text,
22
+ "language": language or "auto-detected"
23
+
24
+ }
25
+
26
+ except Exception as e:
27
+ raise Exception(f"Error transcribing audio: {str(e)}")
Backend/test_multilingual_pipeline.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test multilingual pipeline with sample inputs from each language
3
+ """
4
+ import sys
5
+ import os
6
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
+
8
+ from pipeline.pain_assessment_pipeline import PainAssessmentPipeline
9
+
10
+ def test_multilingual_pipeline():
11
+ """Test pipeline with different languages"""
12
+
13
+ pipeline = PainAssessmentPipeline(verbose=True)
14
+
15
+ print("\n" + "=" * 80)
16
+ print("PIPELINE INFORMATION")
17
+ print("=" * 80)
18
+ info = pipeline.get_pipeline_info()
19
+ for key, value in info.items():
20
+ print(f"{key}: {value}")
21
+
22
+ # Test cases for each language
23
+ test_cases = [
24
+ {
25
+ "language": "Chinese",
26
+ "text": "我有火辣辣的疼痛,已经好几个月了,腰部很难受",
27
+ "llm_entities": {
28
+ "pain_descriptors": ["火辣辣"],
29
+ "location": "腰部",
30
+ "duration_phrase": "好几个月",
31
+ "emotion_keywords": ["难受"],
32
+ "functional_impact": None,
33
+ "intensity": "Moderate to severe"
34
+ }
35
+ },
36
+ {
37
+ "language": "Korean",
38
+ "text": "허리가 따끔거리다",
39
+ "llm_entities": {
40
+ "pain_descriptors": ["따끔거리다"],
41
+ "location": "허리",
42
+ "duration_phrase": None,
43
+ "emotion_keywords": [],
44
+ "functional_impact": None,
45
+ "intensity": "Moderate"
46
+ }
47
+ },
48
+ {
49
+ "language": "Spanish",
50
+ "text": "Tengo un dolor agudo y punzante en la espalda",
51
+ "llm_entities": {
52
+ "pain_descriptors": ["agudo", "punzante"],
53
+ "location": "la espalda",
54
+ "duration_phrase": None,
55
+ "emotion_keywords": [],
56
+ "functional_impact": None,
57
+ "intensity": "Severe"
58
+ }
59
+ },
60
+ {
61
+ "language": "Hmong",
62
+ "text": "Kuv mob Kub Heev heev",
63
+ "llm_entities": {
64
+ "pain_descriptors": ["Kub Heev"],
65
+ "location": None,
66
+ "duration_phrase": None,
67
+ "emotion_keywords": [],
68
+ "functional_impact": None,
69
+ "intensity": "Severe"
70
+ }
71
+ }
72
+ ]
73
+
74
+ for i, test_case in enumerate(test_cases, 1):
75
+ print("\n" + "=" * 80)
76
+ print(f"TEST CASE {i}: {test_case['language']}")
77
+ print("=" * 80)
78
+ print(f"Input: {test_case['text']}")
79
+ print()
80
+
81
+ try:
82
+ report = pipeline.execute(
83
+ test_case['text'],
84
+ test_case['llm_entities']
85
+ )
86
+
87
+ print("\n--- STRUCTURED DATA ---")
88
+ print(f"Pain Type: {report.structured_data.pain_type}")
89
+ print(f"Location: {report.structured_data.location}")
90
+ print(f"Temporal Pattern: {report.structured_data.temporal_pattern}")
91
+
92
+ print("\n--- ONTOLOGY MAPPINGS ---")
93
+ for mapping in report.ontology_mapping_trace:
94
+ print(f" {mapping.get('original_term', mapping.get('chinese_input', 'N/A'))} "
95
+ f"→ {mapping['mapped_english']} ({mapping.get('pain_type', 'N/A')})")
96
+
97
+ print("\n--- RECOMMENDATIONS ---")
98
+ if report.clinical_recommendations:
99
+ for rec in report.clinical_recommendations:
100
+ print(f" • {rec.recommendation}")
101
+ else:
102
+ print(" • Standard assessment recommended")
103
+
104
+ print(f"\n✅ {test_case['language']} test PASSED")
105
+
106
+ except Exception as e:
107
+ print(f"\n❌ {test_case['language']} test FAILED: {e}")
108
+ import traceback
109
+ traceback.print_exc()
110
+
111
+ if __name__ == '__main__':
112
+ test_multilingual_pipeline()
Backend/utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Utility functions for the PainReport system"""
Backend/utils/language_detector.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Language Detection Utility
3
+ Detects the language of input text for multilingual pain assessment
4
+ Supports: Chinese, Korean, Spanish, Hmong, English
5
+ """
6
+ import re
7
+ from typing import Literal
8
+
9
+ LanguageCode = Literal['zh', 'ko', 'es', 'hmong', 'en']
10
+
11
+ def detect_language(text: str) -> LanguageCode:
12
+ """
13
+ Detect the primary language of the input text.
14
+
15
+ Uses character-based heuristics:
16
+ - Chinese: CJK Unified Ideographs (U+4E00–U+9FFF)
17
+ - Korean: Hangul Syllables (U+AC00–U+D7A3)
18
+ - Spanish: Spanish-specific characters (ñ, á, é, í, ó, ú, ü, ¿, ¡)
19
+ - Hmong: Latin script with specific patterns
20
+ - English: Default fallback
21
+
22
+ Args:
23
+ text: Input text to detect language from
24
+
25
+ Returns:
26
+ Language code: 'zh', 'ko', 'es', 'hmong', or 'en'
27
+ """
28
+ if not text or not text.strip():
29
+ return 'en'
30
+
31
+ # Count characters by script
32
+ chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text))
33
+ korean_chars = len(re.findall(r'[\uac00-\ud7a3]', text))
34
+ spanish_chars = len(re.findall(r'[ñáéíóúü¿¡]', text, re.IGNORECASE))
35
+
36
+ # Total characters (excluding whitespace)
37
+ total_chars = len(re.findall(r'\S', text))
38
+
39
+ if total_chars == 0:
40
+ return 'en'
41
+
42
+ # Chinese detection (>30% CJK characters)
43
+ if chinese_chars / total_chars > 0.3:
44
+ return 'zh'
45
+
46
+ # Korean detection (>30% Hangul characters)
47
+ if korean_chars / total_chars > 0.3:
48
+ return 'ko'
49
+
50
+ # Spanish detection (Spanish-specific characters OR common Spanish words)
51
+ spanish_keywords = [
52
+ 'tengo', 'dolor', 'muy', 'que', 'para', 'con', 'por',
53
+ 'esta', 'tiene', 'cuando', 'donde', 'como', 'agudo', 'punzante'
54
+ ]
55
+ text_lower = text.lower()
56
+ spanish_word_matches = sum(1 for kw in spanish_keywords if f' {kw} ' in f' {text_lower} ')
57
+
58
+ if spanish_chars > 0 or spanish_word_matches >= 2:
59
+ return 'es'
60
+
61
+ # Hmong detection (heuristic: common Hmong words)
62
+ hmong_keywords = [
63
+ 'mob', 'txoj', 'kev', 'kuv', 'koj', 'nws', 'lawv',
64
+ 'ntawm', 'rau', 'los', 'thiab', 'muaj', 'yog', 'tsis'
65
+ ]
66
+ text_lower = text.lower()
67
+ hmong_matches = sum(1 for kw in hmong_keywords if kw in text_lower)
68
+
69
+ if hmong_matches >= 2: # At least 2 Hmong keywords
70
+ return 'hmong'
71
+
72
+ # Default to English
73
+ return 'en'
74
+
75
+ def get_language_name(code: LanguageCode) -> str:
76
+ """
77
+ Get full language name from language code.
78
+
79
+ Args:
80
+ code: Language code
81
+
82
+ Returns:
83
+ Full language name
84
+ """
85
+ names = {
86
+ 'zh': 'Chinese',
87
+ 'ko': 'Korean',
88
+ 'es': 'Spanish',
89
+ 'hmong': 'Hmong',
90
+ 'en': 'English'
91
+ }
92
+ return names.get(code, 'Unknown')
93
+
94
+ # Test cases
95
+ if __name__ == '__main__':
96
+ test_cases = [
97
+ ("我有火辣辣的疼痛", "zh"),
98
+ ("허리가 따끔거리듯이 아프다", "ko"),
99
+ ("Tengo un dolor agudo y punzante", "es"),
100
+ ("Kuv mob mob heev", "hmong"),
101
+ ("I have a sharp stabbing pain", "en"),
102
+ ("My back hurts so bad", "en"),
103
+ ]
104
+
105
+ print("Language Detection Tests:")
106
+ print("=" * 60)
107
+ for text, expected in test_cases:
108
+ detected = detect_language(text)
109
+ status = "✅" if detected == expected else "❌"
110
+ print(f"{status} '{text}'")
111
+ print(f" Expected: {expected}, Detected: {detected} ({get_language_name(detected)})")
112
+ print()
Backend/utils/report_generator.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Any
2
+ import os
3
+ from openai import OpenAI
4
+
5
+ try:
6
+ from dotenv import load_dotenv
7
+ load_dotenv()
8
+ except ImportError:
9
+ pass # dotenv not available, use system env vars
10
+
11
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
12
+
13
+
14
+ def generate_comprehensive_report(
15
+ original_text: str,
16
+ structured_data: Dict[str, Any],
17
+ ontology_mappings: List[Dict],
18
+ clinical_recommendations: List[Dict],
19
+ detected_language: str,
20
+ semantic_analysis: Dict = None
21
+
22
+ ) -> str:
23
+ """
24
+ Generate comprehensive clinical report using Medical Anthropologist framework.
25
+
26
+ Uses detailed 4-layer analysis (linguistic, cultural, clinical, psychosocial).
27
+ Outputs as Markdown text for frontend display.
28
+
29
+ Args:
30
+ original_text: Patient's original pain description
31
+ structured_data: Structured PainOntology data
32
+ ontology_mappings: Ontology mapping trace
33
+ clinical_recommendations: List of triggered recommendations
34
+ detected_language: Detected language name
35
+ semantic_analysis: Optional semantic distance analysis for unmapped terms
36
+
37
+ Returns:
38
+ Markdown-formatted clinical report for frontend rendering
39
+ """
40
+
41
+ # Prepare ontology mappings summary (MAPPED TERMS ONLY - exact dictionary matches)
42
+ mapped_terms_summary = []
43
+ for mapping in ontology_mappings[:10]: # Limit to first 10
44
+ # Skip suggestions - only show exact/direct mappings
45
+ if mapping.get('is_suggestion') or mapping.get('confidence') == 'suggestion_only':
46
+ continue
47
+
48
+ original = mapping.get('original_term', '')
49
+ english = mapping.get('mapped_english', '')
50
+ pain_type = mapping.get('pain_type', '')
51
+ if original and english:
52
+ mapped_terms_summary.append(f" - '{original}' → {english} ({pain_type})")
53
+
54
+ mappings_text = '\n'.join(mapped_terms_summary) if mapped_terms_summary else " (No direct dictionary mappings found)"
55
+
56
+ # Prepare recommendations summary
57
+ rec_summary = []
58
+ for rec in clinical_recommendations:
59
+ rule = rec.get('triggered_by_rule', 'Clinical Recommendation')
60
+ text = rec.get('recommendation', '')
61
+ rec_summary.append(f" - {rule}: {text}")
62
+
63
+ recs_text = '\n'.join(rec_summary) if rec_summary else " (Standard pain assessment recommended)"
64
+
65
+ # Prepare UNMAPPED terms semantic analysis summary
66
+ unmapped_text = ""
67
+ if semantic_analysis and semantic_analysis.get('unmapped_analysis'):
68
+ semantic_items = []
69
+ for item in semantic_analysis['unmapped_analysis']:
70
+ original = item['original_term']
71
+ matches = item['closest_matches']
72
+ confidence = item['confidence']
73
+ if matches:
74
+ # Show ALL top matches (usually top 3)
75
+ match_list = []
76
+ for i, match in enumerate(matches[:3], 1):
77
+ # V2: Use new field names (native_term + english)
78
+ native = match.get('native_term', match.get('chinese_term', match.get('term', 'Unknown')))
79
+ english = match.get('english', '')
80
+ match_list.append(f" {i}. {native} ({english}) - similarity: {match['score']:.3f}")
81
+
82
+ semantic_items.append(
83
+ f" - Original: '{original}'\n"
84
+ f" Confidence: {confidence}\n"
85
+ f" Top matches:\n" + '\n'.join(match_list)
86
+ )
87
+ if semantic_items:
88
+ unmapped_text = "\n\n===== UNMAPPED TERMS - SEMANTIC DISTANCE ANALYSIS (AI-Assisted Interpretation) =====\n"
89
+ unmapped_text += "These terms were NOT found in the standard medical dictionary. AI semantic analysis suggests possible matches:\n\n"
90
+ unmapped_text += '\n'.join(semantic_items)
91
+ unmapped_text += "\n\n ⚠️ Important: These are AI-generated suggestions based on semantic similarity, NOT exact dictionary matches.\n Scores closer to 1.0 indicate stronger semantic relationship. Always verify with clinical context."
92
+
93
+ prompt = f"""You are an expert Medical Anthropologist specializing in cross-cultural pain expression.
94
+
95
+ Your goal is to translate cultural pain metaphors into structured medical ontologies.
96
+ ⚠️ DO NOT act as a doctor making a final diagnosis.
97
+ ⚠️ DO NOT infer beyond the given information.
98
+
99
+ ===== PATIENT INPUT =====
100
+ Language: {detected_language}
101
+ Original Words: "{original_text}"
102
+
103
+ ===== STRUCTURED CLINICAL DATA (from neuro-symbolic pipeline) =====
104
+ Pain Type: {structured_data.get('pain_type', 'Not specified')}
105
+ Location: {structured_data.get('location', 'Not specified')}
106
+ Temporal Pattern: {structured_data.get('temporal_pattern', 'Not specified')}
107
+ Intensity: {structured_data.get('intensity', 'Not stated')}
108
+ Emotional Impact: {structured_data.get('emotion', 'None noted')}
109
+ Functional Impact: {structured_data.get('functional_impact', 'None noted')}
110
+
111
+ ===== MAPPED TERMS (Direct Matches from Medical Dictionary) =====
112
+ {mappings_text}{unmapped_text}
113
+
114
+ ===== CLINICAL RECOMMENDATIONS (from rule engine) =====
115
+ {recs_text}
116
+
117
+ ===== YOUR TASK =====
118
+ Generate a comprehensive clinical report using this MANDATORY four-layer analytical framework:
119
+
120
+ **Layer 1: Linguistic Layer (Patient's Voice)**
121
+ - Provide literal translation preserving the patient's EXACT wording
122
+ - Keep cultural expressions intact (e.g., "死疼死疼的", "불같이 아파요")
123
+ - Do NOT simplify or standardize the patient's words
124
+
125
+ **Layer 2: Cultural-Semantic Layer**
126
+ - Identify any culturally specific metaphors or expressions
127
+ - Explain their clinical meaning
128
+ - If no cultural metaphors exist, clearly state that
129
+
130
+ **Layer 3: Clinical Abstraction Layer (McGill Pain Questionnaire)**
131
+ - Sensory qualities (e.g., sharp, burning, aching)
132
+ - Affective qualities (e.g., tiring, distressing)
133
+ - Temporal pattern and intensity
134
+ - Body location
135
+
136
+ **Layer 4: Psychosocial Layer**
137
+ - Emotional distress indicators
138
+ - Under-reporting risk (stoicism patterns)
139
+ - Communication considerations
140
+
141
+ **Layer 5: Semantic Distance Analysis (CRITICAL - if applicable)**
142
+ - IF semantic analysis data is provided in the input:
143
+ - Display EACH unmapped term's semantic similarity scores
144
+ - Show the top 3 closest medical terms with similarity scores
145
+ - Include confidence levels (high/medium/low)
146
+ - Explain in plain language what the similarity scores suggest
147
+ - IF no semantic analysis data: skip this layer entirely
148
+
149
+ ===== OUTPUT FORMAT =====
150
+ Generate in clear Markdown format with these sections:
151
+
152
+ **📝 Patient's Description (Literal Translation)**
153
+ [First quote patient's exact words in original language, then provide word-for-word English translation preserving sentence structure and cultural expressions]
154
+ Example format:
155
+ > Original: "死疼死疼的,真的受不了了"
156
+ > English: "Deadly painful, deadly painful, really can't bear it anymore"
157
+
158
+ **🔗 Cultural Expression Analysis**
159
+ [Analyze any cultural metaphors. If none: "No specific cultural metaphors identified."]
160
+
161
+ **🏥 McGill Pain Assessment**
162
+ - **Sensory Qualities:** [list descriptors]
163
+ - **Affective Qualities:** [list descriptors]
164
+ - **Temporal Pattern:** [pattern]
165
+ - **Location:** [body location]
166
+ - **Intensity:** [severity estimate]
167
+
168
+ **🧠 Psychosocial Considerations**
169
+ - **Emotional Distress:** [Yes/No with brief evidence]
170
+ - **Under-reporting Risk:** [Low/Medium/High with reasoning]
171
+ - **Communication Notes:** [any relevant observations]
172
+
173
+ **🔬 Semantic Distance Analysis (AI-Based Interpretation)**
174
+ [ONLY include this section IF semantic analysis data exists in the input]
175
+ For each unmapped creative expression/metaphor:
176
+ - **Original Term:** [patient's exact words]
177
+ - **Top 3 Similar Medical Terms:**
178
+ 1. [term] (similarity: X.XX, confidence: high/medium/low)
179
+ 2. [term] (similarity: X.XX)
180
+ 3. [term] (similarity: X.XX)
181
+ - **Clinical Interpretation:** [1-sentence plain-language explanation of what these similarities suggest about the pain quality]
182
+
183
+ [If NO semantic analysis data provided, completely omit this section]
184
+
185
+ **⚕️ Clinical Action Plan**
186
+ [Synthesize the clinical recommendations above into 2-3 actionable sentences]
187
+
188
+ ===== CRITICAL RULES =====
189
+ 1. Preserve patient's exact words and emotional tone
190
+ 2. Base all assessments on actual evidence - don't speculate
191
+ 3. If no cultural metaphors, state clearly
192
+ 4. Integrate the provided clinical recommendations
193
+ 5. **MANDATORY: If semantic analysis data is provided above, you MUST include the "🔬 Semantic Distance Analysis" section with all similarity scores displayed clearly**
194
+ 6. Keep report professional but comprehensive
195
+
196
+ Generate the report now:"""
197
+
198
+ try:
199
+ response = client.chat.completions.create(
200
+ model='gpt-5.2',
201
+ messages=[
202
+ {"role": "system", "content": "You are a Medical Anthropologist generating clinical reports. Output clear Markdown text. ALWAYS include the Clinical Action Plan section at the end."},
203
+ {"role": "user", "content": prompt}
204
+ ],
205
+ temperature=0.2,
206
+ max_completion_tokens=2500 # Increased for longer reports
207
+ )
208
+
209
+ return response.choices[0].message.content
210
+
211
+ except Exception as e:
212
+ print(f"[Warning] Report generation error: {e}")
213
+ # Fallback to template matching new format
214
+ return f"""**📝 Patient's Description**
215
+ "{original_text[:300]}..."
216
+
217
+ **🔗 Cultural Expression Analysis**
218
+ Unable to analyze cultural expressions at this time.
219
+
220
+ **🏥 McGill Pain Assessment**
221
+ - **Sensory Qualities:** Based on structured data
222
+ - **Pain Type:** {structured_data.get('pain_type', 'Not specified')}
223
+ - **Location:** {structured_data.get('location', 'Not specified')}
224
+ - **Temporal Pattern:** {structured_data.get('temporal_pattern', 'Not specified')}
225
+ - **Intensity:** {structured_data.get('intensity', 'Not stated')}
226
+
227
+ **🧠 Psychosocial Considerations**
228
+ - **Emotional Distress:** {'Yes' if structured_data.get('emotion') else 'Unknown'}
229
+ - **Functional Impact:** {structured_data.get('functional_impact', 'Not noted')}
230
+
231
+ **⚕️ Clinical Action Plan**
232
+ {chr(10).join([f"- {rec.get('recommendation', '')}" for rec in clinical_recommendations[:3]]) if clinical_recommendations else 'Standard pain assessment and management recommended.'}
233
+
234
+ (Note: Full anthropological analysis unavailable. Using template fallback.)
235
+ """
236
+
237
+
238
+ def translate_to_english_simple(text: str) -> str:
239
+ """
240
+ Simple translation utility to convert short phrases to English.
241
+
242
+ Used for translating intensity levels, functional impacts, etc.
243
+ If text is already in English, returns it unchanged.
244
+
245
+ Args:
246
+ text: Short text to translate (e.g., "很痛", "difficulty walking")
247
+
248
+ Returns:
249
+ English translation or original text if already English
250
+ """
251
+ if not text or text.strip() == "":
252
+ return text
253
+
254
+ # Quick check: if text is already mostly English (ASCII), return as-is
255
+ try:
256
+ text.encode('ascii')
257
+ return text # Already English
258
+ except UnicodeEncodeError:
259
+ pass # Contains non-ASCII, needs translation
260
+
261
+ system_prompt = """You are a medical translator. Translate the given text into concise medical English.
262
+
263
+ Rules:
264
+ - Keep it brief and clinical
265
+ - Preserve medical meaning
266
+ - If already English, return unchanged
267
+ - Output ONLY the translation, no explanations"""
268
+
269
+ try:
270
+ response = client.chat.completions.create(
271
+ model="gpt-5.2",
272
+ messages=[
273
+ {"role": "system", "content": system_prompt},
274
+ {"role": "user", "content": text}
275
+ ],
276
+ temperature=0.1,
277
+ max_tokens=50
278
+ )
279
+
280
+ translation = response.choices[0].message.content.strip()
281
+
282
+ # Remove quotes if present
283
+ if translation.startswith('"') and translation.endswith('"'):
284
+ translation = translation[1:-1]
285
+ if translation.startswith("'") and translation.endswith("'"):
286
+ translation = translation[1:-1]
287
+
288
+ return translation
289
+
290
+ except Exception as e:
291
+ # Fallback: return original
292
+ return text
Frontend/demo.html ADDED
@@ -0,0 +1,942 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Neuro-Symbolic Pain Assessment System - Demo</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1400px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ .header {
27
+ text-align: center;
28
+ color: white;
29
+ margin-bottom: 30px;
30
+ }
31
+
32
+ .header h1 {
33
+ font-size: 2.5em;
34
+ margin-bottom: 10px;
35
+ }
36
+
37
+ .header p {
38
+ font-size: 1.2em;
39
+ opacity: 0.9;
40
+ }
41
+
42
+ .modules {
43
+ display: grid;
44
+ grid-template-columns: 1fr 1fr;
45
+ grid-template-rows: auto auto;
46
+ gap: 20px;
47
+ margin-bottom: 20px;
48
+ }
49
+
50
+ .module-1 {
51
+ grid-column: 1;
52
+ grid-row: 1;
53
+ }
54
+
55
+ .module-2 {
56
+ grid-column: 1;
57
+ grid-row: 2;
58
+ }
59
+
60
+ .report-panel {
61
+ grid-column: 2;
62
+ grid-row: 1 / span 2;
63
+ }
64
+
65
+ .module {
66
+ background: white;
67
+ border-radius: 15px;
68
+ padding: 25px;
69
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
70
+ }
71
+
72
+ .module h2 {
73
+ color: #667eea;
74
+ margin-bottom: 15px;
75
+ font-size: 1.5em;
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 10px;
79
+ }
80
+
81
+ .module-badge {
82
+ background: #667eea;
83
+ color: white;
84
+ padding: 5px 12px;
85
+ border-radius: 20px;
86
+ font-size: 0.7em;
87
+ }
88
+
89
+ textarea {
90
+ width: 100%;
91
+ padding: 15px;
92
+ border: 2px solid #e0e0e0;
93
+ border-radius: 8px;
94
+ font-size: 16px;
95
+ resize: vertical;
96
+ min-height: 120px;
97
+ font-family: inherit;
98
+ }
99
+
100
+ textarea:focus {
101
+ outline: none;
102
+ border-color: #667eea;
103
+ }
104
+
105
+ button {
106
+ background: #667eea;
107
+ color: white;
108
+ border: none;
109
+ padding: 12px 30px;
110
+ border-radius: 8px;
111
+ font-size: 16px;
112
+ cursor: pointer;
113
+ transition: all 0.3s;
114
+ margin-top: 10px;
115
+ }
116
+
117
+ button:hover {
118
+ background: #5568d3;
119
+ transform: translateY(-2px);
120
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
121
+ }
122
+
123
+ button:disabled {
124
+ background: #ccc;
125
+ cursor: not-allowed;
126
+ transform: none;
127
+ }
128
+
129
+ .result {
130
+ margin-top: 20px;
131
+ padding: 20px;
132
+ background: #f8f9fa;
133
+ border-radius: 8px;
134
+ border-left: 4px solid #667eea;
135
+ max-height: 600px;
136
+ overflow-y: auto;
137
+ }
138
+
139
+ .result h3 {
140
+ color: #667eea;
141
+ margin-bottom: 10px;
142
+ }
143
+
144
+ .result-section {
145
+ margin-bottom: 15px;
146
+ }
147
+
148
+ .result-section h4 {
149
+ color: #444;
150
+ margin-bottom: 8px;
151
+ font-size: 1.1em;
152
+ }
153
+
154
+ .mapping-item {
155
+ background: white;
156
+ padding: 10px;
157
+ margin: 5px 0;
158
+ border-radius: 5px;
159
+ border-left: 3px solid #28a745;
160
+ }
161
+
162
+ .recommendation-item {
163
+ background: #fff3cd;
164
+ padding: 15px;
165
+ margin: 10px 0;
166
+ border-radius: 5px;
167
+ border-left: 4px solid #ffc107;
168
+ }
169
+
170
+ .reasoning-chain {
171
+ background: white;
172
+ padding: 10px;
173
+ margin: 5px 0;
174
+ border-left: 3px solid #17a2b8;
175
+ font-family: 'Courier New', monospace;
176
+ font-size: 0.9em;
177
+ }
178
+
179
+ .question-options {
180
+ display: grid;
181
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
182
+ gap: 15px;
183
+ margin-top: 15px;
184
+ }
185
+
186
+ .option-card {
187
+ background: white;
188
+ border: 2px solid #e0e0e0;
189
+ border-radius: 10px;
190
+ padding: 15px;
191
+ cursor: pointer;
192
+ transition: all 0.3s;
193
+ text-align: center;
194
+ }
195
+
196
+ .option-card:hover {
197
+ border-color: #667eea;
198
+ transform: translateY(-3px);
199
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
200
+ }
201
+
202
+ .option-card.selected {
203
+ border-color: #667eea;
204
+ background: #f0f4ff;
205
+ }
206
+
207
+ .option-image {
208
+ width: 100%;
209
+ height: 150px;
210
+ object-fit: cover;
211
+ border-radius: 8px;
212
+ margin-bottom: 10px;
213
+ }
214
+
215
+ .loading {
216
+ text-align: center;
217
+ padding: 20px;
218
+ color: #667eea;
219
+ }
220
+
221
+ .spinner {
222
+ border: 4px solid #f3f3f3;
223
+ border-top: 4px solid #667eea;
224
+ border-radius: 50%;
225
+ width: 40px;
226
+ height: 40px;
227
+ animation: spin 1s linear infinite;
228
+ margin: 0 auto;
229
+ }
230
+
231
+ @keyframes spin {
232
+ 0% { transform: rotate(0deg); }
233
+ 100% { transform: rotate(360deg); }
234
+ }
235
+
236
+ .badge {
237
+ display: inline-block;
238
+ padding: 3px 8px;
239
+ border-radius: 12px;
240
+ font-size: 0.8em;
241
+ font-weight: bold;
242
+ margin-left: 5px;
243
+ }
244
+
245
+ .badge-neuropathic {
246
+ background: #dc3545;
247
+ color: white;
248
+ }
249
+
250
+ .badge-nociceptive {
251
+ background: #fd7e14;
252
+ color: white;
253
+ }
254
+
255
+ .badge-affective {
256
+ background: #6f42c1;
257
+ color: white;
258
+ }
259
+
260
+ .example-buttons {
261
+ display: flex;
262
+ gap: 10px;
263
+ margin-top: 10px;
264
+ flex-wrap: wrap;
265
+ }
266
+
267
+ .example-btn {
268
+ background: #f8f9fa;
269
+ color: #667eea;
270
+ border: 1px solid #667eea;
271
+ padding: 8px 15px;
272
+ font-size: 14px;
273
+ }
274
+
275
+ .example-btn:hover {
276
+ background: #667eea;
277
+ color: white;
278
+ }
279
+
280
+ /* New styles for improved readability */
281
+ details {
282
+ cursor: pointer;
283
+ user-select: none;
284
+ }
285
+
286
+ details summary {
287
+ padding: 8px;
288
+ border-radius: 6px;
289
+ background: rgba(0,0,0,0.02);
290
+ transition: background 0.2s;
291
+ }
292
+
293
+ details summary:hover {
294
+ background: rgba(0,0,0,0.05);
295
+ }
296
+
297
+ details[open] summary {
298
+ margin-bottom: 10px;
299
+ border-bottom: 1px solid rgba(0,0,0,0.1);
300
+ }
301
+
302
+ .result-section {
303
+ padding: 20px;
304
+ margin-bottom: 15px;
305
+ border-radius: 12px;
306
+ }
307
+
308
+ /* Smooth scrollbar for results */
309
+ .result::-webkit-scrollbar {
310
+ width: 8px;
311
+ }
312
+
313
+ .result::-webkit-scrollbar-track {
314
+ background: #f1f1f1;
315
+ border-radius: 4px;
316
+ }
317
+
318
+ .result::-webkit-scrollbar-thumb {
319
+ background: #667eea;
320
+ border-radius: 4px;
321
+ }
322
+
323
+ .result::-webkit-scrollbar-thumb:hover {
324
+ background: #5568d3;
325
+ }
326
+
327
+ @media (max-width: 1024px) {
328
+ .modules {
329
+ grid-template-columns: 1fr;
330
+ }
331
+ }
332
+ </style>
333
+ </head>
334
+ <body>
335
+ <div class="container">
336
+ <div class="header">
337
+ <h1>🏥 Neuro-Symbolic Pain Assessment System</h1>
338
+ <p>Hybrid AI Architecture: LLM Entity Extraction + Deterministic Clinical Reasoning</p>
339
+ </div>
340
+
341
+ <div class="modules">
342
+ <!-- MODULE 1: Text + Voice Input (Left Top) -->
343
+ <div class="module module-1">
344
+ <h2>
345
+ <span class="module-badge">Module 1</span>
346
+ Pain Assessment Pipeline
347
+ </h2>
348
+
349
+ <p style="margin-bottom: 15px; color: #666;">
350
+ Enter a pain description to see the complete neuro-symbolic analysis pipeline in action.
351
+ </p>
352
+
353
+ <textarea id="painInput" placeholder="Example: I've had constant electric-shock-like pain in my lower back for 4 months, can't sleep at night, feeling very depressed..."></textarea>
354
+
355
+ <div class="example-buttons">
356
+ <button class="example-btn" onclick="loadExample('neuropathic')">Example: Neuropathic</button>
357
+ <button class="example-btn" onclick="loadExample('chronic')">Example: Chronic Pain</button>
358
+ <button class="example-btn" onclick="loadExample('chinese')">Example: Chinese</button>
359
+ </div>
360
+
361
+ <button id="analyzeBtn" onclick="analyzePain()">🔬 Analyze Pain Description</button>
362
+
363
+ <!-- Voice Recording -->
364
+ <div style="margin-top: 15px; padding: 15px; background: #f0f4ff; border-radius: 8px; border: 2px dashed #667eea;">
365
+ <p style="margin-bottom: 10px; color: #667eea; font-weight: bold;">🎙️ OR Record Your Voice:</p>
366
+ <div style="display: flex; gap: 10px; align-items: center;">
367
+ <button id="recordBtn" onclick="toggleRecording()" style="background: #dc3545;">
368
+ 🎙️ Start Recording
369
+ </button>
370
+ <span id="recordStatus" style="color: #666;">Ready to record</span>
371
+ </div>
372
+ <audio id="audioPlayback" controls style="width: 100%; margin-top: 10px; display: none;"></audio>
373
+ </div>
374
+
375
+ <!-- Structured Data Results -->
376
+ <div id="pipelineResult"></div>
377
+ </div>
378
+
379
+ <!-- MODULE 2: Visual Q&A (Left Bottom) -->
380
+ <div class="module module-2">
381
+ <h2>
382
+ <span class="module-badge" style="background: #28a745;">Module 2</span>
383
+ Visual Follow-up Questions
384
+ </h2>
385
+
386
+ <p style="margin-bottom: 15px; color: #666;">
387
+ Interactive image-based pain assessment for better characterization.
388
+ </p>
389
+
390
+ <button onclick="generateQuestion()">🖼️ Generate Visual Question</button>
391
+
392
+ <div id="questionResult"></div>
393
+ </div>
394
+
395
+ <!-- Right Panel: Clinical Report (Full Height) -->
396
+ <div class="module report-panel">
397
+ <h2>
398
+ <span class="module-badge" style="background: #13547a;">📋 Report</span>
399
+ Physician Summary (Clinical Report)
400
+ </h2>
401
+
402
+ <p style="margin-bottom: 15px; color: #666;">
403
+ Comprehensive medical anthropologist analysis with 4-layer framework.
404
+ </p>
405
+
406
+ <div id="physicianReport"></div>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <script>
412
+ const API_BASE = 'http://localhost:8000';
413
+
414
+ // Example texts
415
+ const examples = {
416
+ neuropathic: "I've had constant electric-shock-like tingling pain in my lower back and legs for 4 months. The pain wakes me up at night, I can't sleep properly. Feeling exhausted and depressed.",
417
+ chronic: "My knees have been aching constantly for several months now. The pain is dull and throbbing, especially during the night. I'm exhausted from dealing with it.",
418
+ chinese: "最近四个月腰部到腿部总是像触电一样的麻痛,晚上痛得睡不着,心情很郁闷"
419
+ };
420
+
421
+ function loadExample(type) {
422
+ document.getElementById('painInput').value = examples[type];
423
+ }
424
+
425
+ async function analyzePain() {
426
+ const text = document.getElementById('painInput').value.trim();
427
+ if (!text) {
428
+ alert('Please enter a pain description');
429
+ return;
430
+ }
431
+
432
+ const resultDiv = document.getElementById('pipelineResult');
433
+ const btn = document.getElementById('analyzeBtn');
434
+
435
+ btn.disabled = true;
436
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div><p>Analyzing pain description...</p></div>';
437
+
438
+ try {
439
+ const response = await fetch(`${API_BASE}/api/analyze-text-neuro-symbolic`, {
440
+ method: 'POST',
441
+ headers: { 'Content-Type': 'application/json' },
442
+ body: JSON.stringify({ text })
443
+ });
444
+
445
+ const data = await response.json();
446
+
447
+ if (data.status === 'success') {
448
+ displayPipelineResult(data);
449
+ } else {
450
+ resultDiv.innerHTML = `<div class="result"><h3>❌ Error</h3><p>${data.message}</p></div>`;
451
+ }
452
+ } catch (error) {
453
+ resultDiv.innerHTML = `<div class="result"><h3>❌ Connection Error</h3><p>Make sure the backend server is running on ${API_BASE}</p></div>`;
454
+ } finally {
455
+ btn.disabled = false;
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Parse bilingual text format: "原文 [English translation]"
461
+ * Returns object with {original, translation} or {original: text} if no translation
462
+ */
463
+ function parseBilingualText(text) {
464
+ if (!text) return { original: '' };
465
+
466
+ // Check for format: "原文 [English translation]"
467
+ const match = text.match(/^(.+?)\s*\[(.+?)\]$/);
468
+ if (match) {
469
+ return {
470
+ original: match[1].trim(),
471
+ translation: match[2].trim()
472
+ };
473
+ }
474
+
475
+ // No translation format, return as-is
476
+ return { original: text };
477
+ }
478
+
479
+ /**
480
+ * Format bilingual text for display: large original + small translation
481
+ */
482
+ function formatBilingualDisplay(text) {
483
+ const parsed = parseBilingualText(text);
484
+
485
+ if (parsed.translation) {
486
+ // Has translation - display original prominently with translation below
487
+ return `
488
+ <div style="font-size: 1.05em; font-weight: 600; margin-top: 4px; line-height: 1.4;">
489
+ ${parsed.original}
490
+ </div>
491
+ <div style="font-size: 0.8em; color: rgba(255,255,255,0.75); margin-top: 4px; font-style: italic;">
492
+ ${parsed.translation}
493
+ </div>
494
+ `;
495
+ } else {
496
+ // No translation - display plain text
497
+ return `<div style="font-size: 1.05em; font-weight: 600; margin-top: 4px;">${parsed.original}</div>`;
498
+ }
499
+ }
500
+
501
+ function displayPipelineResult(data) {
502
+ const resultDiv = document.getElementById('pipelineResult');
503
+
504
+ // Validate data structure
505
+ if (!data || !data.structured_data) {
506
+ resultDiv.innerHTML = `<div class="result"><h3>⚠️ Analysis Issue</h3><p>Unable to process the pain description. Please try again with more details.</p></div>`;
507
+ return;
508
+ }
509
+
510
+ const sd = data.structured_data;
511
+ const mappings = data.ontology_mapping_trace || [];
512
+ const recommendations = data.clinical_recommendations || [];
513
+ const reasoning = data.reasoning_chain || [];
514
+ const transcription = data.transcription || null;
515
+
516
+ let html = '<div class="result">';
517
+ html += '<h3 style="color: #28a745;">✅ Analysis Complete</h3>';
518
+
519
+ // Transcription Normalization (if available)
520
+ if (transcription && transcription.original !== transcription.normalized) {
521
+ html += '<div class="result-section" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">';
522
+ html += '<h4 style="color: white; margin-bottom: 15px;">📝 Speech Recognition & Correction</h4>';
523
+
524
+ html += '<div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 8px; margin-bottom: 12px;">';
525
+ html += '<div style="font-size: 0.85em; opacity: 0.9; margin-bottom: 5px;">🎤 Original Transcription</div>';
526
+ html += `<div style="font-size: 1em; font-style: italic;">${transcription.original}</div>`;
527
+ html += '</div>';
528
+
529
+ html += '<div style="text-align: center; margin: 10px 0; opacity: 0.7;">⬇️</div>';
530
+
531
+ html += '<div style="background: rgba(255,255,255,0.25); padding: 12px; border-radius: 8px; border: 2px solid rgba(255,255,255,0.4);">';
532
+ html += '<div style="font-size: 0.85em; opacity: 0.9; margin-bottom: 5px;">✨ AI-Enhanced</div>';
533
+ html += `<div style="font-size: 1.1em; font-weight: 600;">${transcription.normalized}</div>`;
534
+ html += '</div>';
535
+
536
+ // English Translation (if available)
537
+ if (transcription.english_translation) {
538
+ html += '<div style="text-align: center; margin: 10px 0; opacity: 0.7;">⬇️</div>';
539
+
540
+ html += '<div style="background: rgba(255,255,255,0.35); padding: 12px; border-radius: 8px; border: 2px solid rgba(255,255,255,0.6);">';
541
+ html += '<div style="font-size: 0.85em; opacity: 0.9; margin-bottom: 5px;">🌐 English Translation</div>';
542
+ html += `<div style="font-size: 1.1em; font-weight: 600;">${transcription.english_translation}</div>`;
543
+ html += '</div>';
544
+ }
545
+
546
+ if (transcription.corrections_applied && transcription.corrections_applied.length > 0) {
547
+ html += '<details style="margin-top: 15px; cursor: pointer;">';
548
+ html += '<summary style="font-size: 0.9em; opacity: 0.9;">🔧 Optimization Details (' + transcription.corrections_applied.length + ' items)</summary>';
549
+ html += '<div style="background: rgba(0,0,0,0.1); padding: 10px; border-radius: 6px; margin-top: 8px;">';
550
+ transcription.corrections_applied.forEach(correction => {
551
+ html += `<div style="padding: 4px 0; font-size: 0.85em;">• ${correction}</div>`;
552
+ });
553
+ html += '</div></details>';
554
+ }
555
+ html += '</div>';
556
+ }
557
+
558
+ // Pain Assessment Summary (Main Card)
559
+ html += '<div class="result-section" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">';
560
+ html += '<h4 style="color: white; margin-bottom: 15px;">🩺 Pain Assessment Results</h4>';
561
+
562
+ // Pain Type with visual indicator
563
+ const painTypeColor = sd.pain_type && sd.pain_type.toLowerCase().includes('neuropathic') ? '#ff6b6b' :
564
+ sd.pain_type && sd.pain_type.toLowerCase().includes('nociceptive') ? '#4ecdc4' : '#95e1d3';
565
+ html += '<div style="background: rgba(255,255,255,0.2); padding: 15px; border-radius: 8px; margin-bottom: 12px;">';
566
+ html += `<div style="font-size: 0.9em; opacity: 0.9; margin-bottom: 5px;">Pain Type</div>`;
567
+ html += `<div style="font-size: 1.3em; font-weight: 700; display: flex; align-items: center;">`;
568
+ html += `<span style="background: ${painTypeColor}; width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: 10px;"></span>`;
569
+ html += `${sd.pain_type || 'Not detected'}`;
570
+ html += `</div></div>`;
571
+
572
+ // Grid layout for other info
573
+ html += '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">';
574
+
575
+ if (sd.location && sd.location !== 'Not specified') {
576
+ html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;">';
577
+ html += '<div style="font-size: 0.85em; opacity: 0.9;">📍 Location</div>';
578
+ html += `<div style="font-size: 1.05em; font-weight: 600; margin-top: 4px;">${sd.location}</div>`;
579
+ html += '</div>';
580
+ }
581
+
582
+ if (sd.temporal_pattern && sd.temporal_pattern !== 'Not specified') {
583
+ html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;">';
584
+ html += '<div style="font-size: 0.85em; opacity: 0.9;">⏱️ Temporal Pattern</div>';
585
+ html += `<div style="font-size: 1.05em; font-weight: 600; margin-top: 4px;">${sd.temporal_pattern}</div>`;
586
+ html += '</div>';
587
+ }
588
+
589
+ if (sd.intensity && sd.intensity !== 'Not stated') {
590
+ html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;">';
591
+ html += '<div style="font-size: 0.85em; opacity: 0.9;">💪 Intensity</div>';
592
+ html += formatBilingualDisplay(sd.intensity);
593
+ html += '</div>';
594
+ }
595
+
596
+ if (sd.emotion && sd.emotion !== 'None detected') {
597
+ html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;">';
598
+ html += '<div style="font-size: 0.85em; opacity: 0.9;">😔 Emotional Impact</div>';
599
+ html += `<div style="font-size: 1.05em; font-weight: 600; margin-top: 4px;">${sd.emotion}</div>`;
600
+ html += '</div>';
601
+ }
602
+
603
+ if (sd.functional_impact && sd.functional_impact !== 'Not stated') {
604
+ html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px; grid-column: 1 / -1;">';
605
+ html += '<div style="font-size: 0.85em; opacity: 0.9;">🚶 Functional Impact</div>';
606
+ html += formatBilingualDisplay(sd.functional_impact);
607
+ html += '</div>';
608
+ }
609
+
610
+ html += '</div></div>';
611
+
612
+ // Unmapped/Unique Pain Descriptors (if any)
613
+ // Check if pain_type contains [Unmapped terms: ...]
614
+ if (sd.pain_type && sd.pain_type.includes('[Unmapped terms:')) {
615
+ const unmappedMatch = sd.pain_type.match(/\[Unmapped terms: ([^\]]+)\]/);
616
+ if (unmappedMatch) {
617
+ const unmappedTerms = unmappedMatch[1].split(', ');
618
+ html += '<div class="result-section" style="background: #fff3cd; border-left: 4px solid #ffc107; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">';
619
+ html += '<h4 style="color: #856404; margin-bottom: 15px;">⚠️ Unique Pain Descriptors (Not in Medical Dictionary)</h4>';
620
+ html += '<div style="color: #856404; line-height: 1.6; margin-bottom: 10px;">The patient used creative/metaphorical expressions that are not in our standardized pain terminology database. These should be noted for clinical context:</div>';
621
+ unmappedTerms.forEach(term => {
622
+ html += `<div style="background: #ffffff; padding: 10px 12px; border-radius: 6px; margin-bottom: 8px; border-left: 3px solid #ffc107; font-weight: 500; color: #333;">`;
623
+ html += `📝 "${term.trim()}"`;
624
+ html += `</div>`;
625
+ });
626
+ html += '<div style="font-size: 0.85em; color: #856404; margin-top: 10px; font-style: italic;">💡 Recommendation: Consider asking follow-up questions to understand these expressions in clinical terms.</div>';
627
+ html += '</div>';
628
+ }
629
+ }
630
+
631
+ // Physician Summary moved to right panel - display it there instead
632
+ if (data.physician_summary) {
633
+ displayPhysicianReport(data.physician_summary);
634
+ }
635
+
636
+ // Clinical Recommendations (if available)
637
+ if (recommendations.length > 0) {
638
+ html += '<div class="result-section" style="background: #fff; border-left: 4px solid #ff6b6b; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">';
639
+ html += '<h4 style="color: #ff6b6b; margin-bottom: 15px;">💡 Clinical Recommendations</h4>';
640
+ recommendations.forEach(rec => {
641
+ html += `<div style="background: #fff5f5; padding: 15px; border-radius: 8px; margin-bottom: 10px; border-left: 3px solid #ff6b6b;">`;
642
+ html += `<div style="font-weight: 600; color: #333; margin-bottom: 8px;">${rec.triggered_by_rule || 'Clinical Recommendation'}</div>`;
643
+ html += `<div style="color: #555; line-height: 1.6;">${rec.recommendation}</div>`;
644
+ html += `<div style="margin-top: 8px; font-size: 0.85em; color: #999;">Confidence: ${rec.confidence || 'medium'}</div>`;
645
+ html += `</div>`;
646
+ });
647
+ html += '</div>';
648
+ }
649
+
650
+ // Ontology Mappings (Collapsible Technical Details)
651
+ if (mappings.length > 0) {
652
+ // Separate confirmed mappings from suggestions
653
+ const confirmedMappings = mappings.filter(m => !m.is_suggestion);
654
+ const suggestedMappings = mappings.filter(m => m.is_suggestion);
655
+
656
+ html += '<div class="result-section" style="background: #f8f9fa; border: 1px solid #e9ecef;">';
657
+ html += '<details style="cursor: pointer;">';
658
+ html += '<summary style="font-weight: 600; color: #495057; padding: 5px 0; user-select: none;">🔬 Terminology Mapping Details (' + confirmedMappings.length + ' confirmed';
659
+ if (suggestedMappings.length > 0) {
660
+ html += ', ' + suggestedMappings.length + ' suggested';
661
+ }
662
+ html += ') ▼</summary>';
663
+ html += '<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #dee2e6;">';
664
+
665
+ // Display confirmed mappings
666
+ if (confirmedMappings.length > 0) {
667
+ html += '<div style="margin-bottom: 20px;">';
668
+ html += '<div style="font-size: 0.9em; color: #28a745; font-weight: 600; margin-bottom: 10px;">✅ Confirmed Mappings (Dictionary Matches)</div>';
669
+ confirmedMappings.forEach(m => {
670
+ const badgeClass = m.pain_type === 'neuropathic' ? 'badge-neuropathic' :
671
+ m.pain_type === 'nociceptive' ? 'badge-nociceptive' : 'badge-affective';
672
+ html += `<div style="background: white; padding: 12px; border-radius: 6px; margin-bottom: 8px; border-left: 3px solid #28a745;">`;
673
+ html += `<div style="display: flex; align-items: center; justify-content: space-between;">`;
674
+ html += `<div><strong style="color: #495057;">"${m.original_term || m.chinese_input}"</strong> → <strong style="color: #007bff;">${m.mapped_english}</strong></div>`;
675
+ html += `<span class="badge ${badgeClass}" style="margin-left: 10px;">${m.pain_type || m.dimension}</span>`;
676
+ html += `</div>`;
677
+ if (m.matched_text && m.matched_text !== m.original_term) {
678
+ html += `<div style="margin-top: 5px; font-size: 0.85em; color: #6c757d;">Matched: "${m.matched_text}"</div>`;
679
+ }
680
+ html += `</div>`;
681
+ });
682
+ html += '</div>';
683
+ }
684
+
685
+ // Display suggested mappings (warnings)
686
+ if (suggestedMappings.length > 0) {
687
+ html += '<div style="margin-top: 15px; padding-top: 15px; border-top: 2px dashed #ffc107;">';
688
+ html += '<div style="font-size: 0.9em; color: #856404; font-weight: 600; margin-bottom: 10px;">⚠️ Similarity Suggestions (Not in Dictionary)</div>';
689
+ html += '<div style="background: #fff3cd; padding: 12px; border-radius: 6px; margin-bottom: 10px; font-size: 0.85em; color: #856404;">';
690
+ html += '💡 These terms were not found in our medical dictionary. The system suggests similar terms below, but these are NOT definitive mappings. Clinical review is required.';
691
+ html += '</div>';
692
+
693
+ suggestedMappings.forEach(m => {
694
+ html += `<div style="background: #fff3cd; padding: 12px; border-radius: 6px; margin-bottom: 8px; border-left: 3px dashed #ffc107;">`;
695
+ html += `<div style="display: flex; align-items: center; justify-content: space-between;">`;
696
+ html += `<div><strong style="color: #856404;">"${m.original_term}"</strong> <span style="opacity: 0.7;">→ might be similar to →</span> <strong style="color: #d39e00;">${m.mapped_english}</strong></div>`;
697
+ html += `<span class="badge" style="background: #ffc107; color: #000; margin-left: 10px;">suggestion</span>`;
698
+ html += `</div>`;
699
+ if (m.similarity_reason) {
700
+ html += `<div style="margin-top: 8px; font-size: 0.85em; color: #856404; background: rgba(255,255,255,0.5); padding: 6px 8px; border-radius: 4px;">`;
701
+ html += `🔍 Similarity: ${m.similarity_reason}`;
702
+ html += `</div>`;
703
+ }
704
+ if (m.suggestion_note) {
705
+ html += `<div style="margin-top: 8px; font-size: 0.8em; color: #856404; font-style: italic;">`;
706
+ html += `${m.suggestion_note}`;
707
+ html += `</div>`;
708
+ }
709
+ html += `</div>`;
710
+ });
711
+ html += '</div>';
712
+ }
713
+
714
+ html += '</div></details></div>';
715
+ }
716
+
717
+ // Reasoning Chain (Collapsible Technical Details)
718
+ if (reasoning.length > 0) {
719
+ html += '<div class="result-section" style="background: #f8f9fa; border: 1px solid #e9ecef;">';
720
+ html += '<details style="cursor: pointer;">';
721
+ html += '<summary style="font-weight: 600; color: #495057; padding: 5px 0; user-select: none;">🧠 AI Reasoning Process (' + reasoning.length + ' steps) ▼</summary>';
722
+ html += '<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #dee2e6;">';
723
+ reasoning.forEach((step, index) => {
724
+ // Parse and format reasoning steps
725
+ let formattedStep = step.replace(/===/g, '').replace(/\n\n/g, '<br>');
726
+ const isHeader = step.includes('===') || step.match(/^[A-Z\s]+$/);
727
+ const style = isHeader ?
728
+ 'background: #e7f3ff; padding: 8px 12px; border-radius: 4px; font-weight: 600; color: #0066cc; margin: 10px 0 5px 0;' :
729
+ 'background: white; padding: 10px 12px; border-radius: 4px; color: #495057; margin: 5px 0; border-left: 2px solid #dee2e6; font-family: monospace; font-size: 0.9em; white-space: pre-wrap;';
730
+ html += `<div style="${style}">${formattedStep}</div>`;
731
+ });
732
+ html += '</div></details></div>';
733
+ }
734
+
735
+ html += '</div>';
736
+ resultDiv.innerHTML = html;
737
+ }
738
+
739
+ function displayPhysicianReport(summary) {
740
+ const reportDiv = document.getElementById('physicianReport');
741
+
742
+ let html = '<div style="background: rgba(255,255,255,0.95); color: #333; padding: 20px; border-radius: 8px; line-height: 1.6;">';
743
+
744
+ // Clean up excessive whitespace first
745
+ let cleanedSummary = summary
746
+ .replace(/\n{3,}/g, '\n\n') // Replace 3+ newlines with 2
747
+ .trim();
748
+
749
+ // Enhanced Markdown formatting with better spacing
750
+ let formattedSummary = cleanedSummary
751
+ // Headers (## ) - with proper spacing
752
+ .replace(/^## (.+)$/gm, '<h3 style="color: #667eea; margin: 25px 0 12px 0; font-size: 1.3em; font-weight: 600;">$1</h3>')
753
+ // Horizontal rules (---) - thinner with less margin
754
+ .replace(/^---$/gm, '<hr style="border: none; border-top: 1px solid #e0e0e0; margin: 15px 0;">')
755
+ // Blockquotes (> ) - more compact
756
+ .replace(/^> (.+)$/gm, '<div style="border-left: 3px solid #667eea; padding: 8px 12px; margin: 8px 0; background: #f8f9fa; color: #555; font-style: italic; border-radius: 3px;">$1</div>')
757
+ // Bold (**text**)
758
+ .replace(/\*\*(.+?)\*\*/g, '<strong style="color: #333;">$1</strong>')
759
+ // Unordered lists (- ) - more compact
760
+ .replace(/^- (.+)$/gm, '<li style="margin: 3px 0 3px 20px; line-height: 1.5;">$1</li>')
761
+ // Wrap consecutive <li> in <ul> with tighter spacing
762
+ .replace(/(<li[^>]*>.*?<\/li>\s*)+/g, '<ul style="list-style-type: disc; margin: 8px 0; padding-left: 0;">$&</ul>')
763
+ // Convert remaining double newlines to paragraph breaks (smaller gap)
764
+ .replace(/\n\n/g, '<div style="height: 10px;"></div>')
765
+ // Convert single newlines to line breaks
766
+ .replace(/\n/g, '<br>');
767
+
768
+ html += formattedSummary;
769
+ html += '</div>';
770
+
771
+ reportDiv.innerHTML = html;
772
+ }
773
+
774
+ async function generateQuestion() {
775
+ const resultDiv = document.getElementById('questionResult');
776
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div><p>Generating visual question...</p></div>';
777
+
778
+ try {
779
+ const response = await fetch(`${API_BASE}/api/follow-up`, {
780
+ method: 'POST',
781
+ headers: { 'Content-Type': 'application/json' },
782
+ body: JSON.stringify({ history: [] })
783
+ });
784
+
785
+ const data = await response.json();
786
+
787
+ if (data.status === 'success') {
788
+ displayQuestion(data.followup);
789
+ } else {
790
+ resultDiv.innerHTML = `<div class="result"><h3>❌ Error</h3><p>${data.message}</p></div>`;
791
+ }
792
+ } catch (error) {
793
+ resultDiv.innerHTML = `<div class="result"><h3>❌ Connection Error</h3><p>${error.message}</p></div>`;
794
+ }
795
+ }
796
+
797
+ function displayQuestion(question) {
798
+ const resultDiv = document.getElementById('questionResult');
799
+
800
+ let html = '<div class="result">';
801
+ html += `<h3 style="line-height: 1.6;">❓ ${question.question}</h3>`;
802
+ html += '<div class="question-options">';
803
+
804
+ question.options.forEach(option => {
805
+ // Split bilingual text for better display
806
+ const textParts = option.text.split('|').map(t => t.trim());
807
+ const displayText = textParts.length > 1
808
+ ? `<strong>${option.id}</strong>: ${textParts[0]}<br><span style="color: #666; font-size: 0.9em;">${textParts[1]}</span>`
809
+ : `<strong>${option.id}</strong>: ${option.text}`;
810
+
811
+ html += `<div class="option-card" onclick="selectOption(this, '${option.id}')">`;
812
+ html += `<img src="${API_BASE}${option.image_url}" class="option-image" alt="${option.text}">`;
813
+ html += `<p>${displayText}</p>`;
814
+ html += `</div>`;
815
+ });
816
+
817
+ html += '</div>';
818
+ html += '</div>';
819
+
820
+ resultDiv.innerHTML = html;
821
+ }
822
+
823
+ function selectOption(element, optionId) {
824
+ document.querySelectorAll('.option-card').forEach(card => {
825
+ card.classList.remove('selected');
826
+ });
827
+ element.classList.add('selected');
828
+ console.log('Selected option:', optionId);
829
+ }
830
+
831
+ // Audio Recording Functions
832
+ let mediaRecorder;
833
+ let audioChunks = [];
834
+
835
+ async function toggleRecording() {
836
+ const btn = document.getElementById('recordBtn');
837
+ const status = document.getElementById('recordStatus');
838
+ const audioPlayback = document.getElementById('audioPlayback');
839
+
840
+ if (!mediaRecorder || mediaRecorder.state === 'inactive') {
841
+ // Start recording
842
+ try {
843
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
844
+ mediaRecorder = new MediaRecorder(stream);
845
+ audioChunks = [];
846
+
847
+ mediaRecorder.ondataavailable = (event) => {
848
+ audioChunks.push(event.data);
849
+ };
850
+
851
+ mediaRecorder.onstop = async () => {
852
+ const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
853
+ const audioUrl = URL.createObjectURL(audioBlob);
854
+
855
+ audioPlayback.src = audioUrl;
856
+ audioPlayback.style.display = 'block';
857
+
858
+ status.textContent = 'Processing audio...';
859
+ status.style.color = '#007bff';
860
+
861
+ await analyzeAudio(audioBlob);
862
+ };
863
+
864
+ mediaRecorder.start();
865
+ btn.textContent = '⏹️ Stop Recording';
866
+ btn.style.background = '#dc3545';
867
+ status.textContent = 'Recording... speak now';
868
+ status.style.color = '#dc3545';
869
+ } catch (error) {
870
+ alert('Microphone access denied: ' + error.message);
871
+ }
872
+ } else {
873
+ // Stop recording
874
+ mediaRecorder.stop();
875
+ mediaRecorder.stream.getTracks().forEach(track => track.stop());
876
+ btn.textContent = '🎙️ Start Recording';
877
+ btn.style.background = '#28a745';
878
+ status.textContent = 'Analyzing...';
879
+ status.style.color = '#007bff';
880
+ }
881
+ }
882
+
883
+ async function analyzeAudio(audioBlob) {
884
+ const resultDiv = document.getElementById('pipelineResult');
885
+ const status = document.getElementById('recordStatus');
886
+
887
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div><p>Transcribing and analyzing audio...</p></div>';
888
+
889
+ try {
890
+ const formData = new FormData();
891
+ formData.append('file', audioBlob, 'recording.webm');
892
+
893
+ const response = await fetch(`${API_BASE}/api/analyze-audio-neuro-symbolic`, {
894
+ method: 'POST',
895
+ body: formData
896
+ });
897
+
898
+ const data = await response.json();
899
+
900
+ if (data.status === 'success') {
901
+ // Show normalized transcription in input field (cleaner for display)
902
+ if (data.transcription) {
903
+ // Use normalized text if available, otherwise fall back to original
904
+ const displayText = data.transcription.normalized || data.transcription.original || data.transcription;
905
+ document.getElementById('painInput').value = displayText;
906
+ }
907
+
908
+ // Display results only if structured data exists
909
+ if (data.structured_data) {
910
+ displayPipelineResult(data);
911
+ status.textContent = 'Analysis complete!';
912
+ status.style.color = '#28a745';
913
+ } else {
914
+ // Show transcription but indicate analysis failed
915
+ const transcriptionText = data.transcription?.normalized || data.transcription?.original || data.transcription || 'Unknown';
916
+ resultDiv.innerHTML = `<div class="result">
917
+ <h3>⚠️ Transcription Only</h3>
918
+ <p><strong>Transcribed text:</strong> ${transcriptionText}</p>
919
+ <p style="color: #666; margin-top: 10px;">Analysis could not be completed. The system may not recognize the pain description yet.</p>
920
+ </div>`;
921
+ status.textContent = 'Transcription done, analysis incomplete';
922
+ status.style.color = '#ff9800';
923
+ }
924
+ } else {
925
+ resultDiv.innerHTML = `<div class="result"><h3>❌ Error</h3><p>${data.message || 'Unknown error'}</p></div>`;
926
+ status.textContent = 'Error occurred';
927
+ status.style.color = '#dc3545';
928
+ }
929
+ } catch (error) {
930
+ resultDiv.innerHTML = `<div class="result"><h3>❌ Connection Error</h3><p>${error.message}</p></div>`;
931
+ status.textContent = 'Connection failed';
932
+ status.style.color = '#dc3545';
933
+ }
934
+ }
935
+
936
+ // Load first example on page load
937
+ window.onload = () => {
938
+ loadExample('neuropathic');
939
+ };
940
+ </script>
941
+ </body>
942
+ </html>
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: cd Backend && uvicorn main:app --host 0.0.0.0 --port $PORT
app.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio Demo for Multilingual Pain Assessment System
3
+ Powered by BioLORD-2023-M medical embeddings
4
+ """
5
+
6
+ import gradio as gr
7
+ import requests
8
+ import os
9
+ import sys
10
+
11
+ # Add Backend to path
12
+ sys.path.append("./Backend")
13
+
14
+ # For local testing
15
+ API_URL = os.getenv("API_URL", "http://localhost:8000")
16
+
17
+ def analyze_pain(text, language):
18
+ """Analyze pain description using the backend API."""
19
+ try:
20
+ response = requests.post(
21
+ f"{API_URL}/analyze_pain_text",
22
+ json={"text": text},
23
+ timeout=60
24
+ )
25
+
26
+ if response.status_code == 200:
27
+ result = response.json()
28
+ return result.get("report", "No report generated")
29
+ else:
30
+ return f"Error: {response.status_code} - {response.text}"
31
+
32
+ except Exception as e:
33
+ return f"Error connecting to backend: {str(e)}\n\nMake sure the backend server is running:\ncd Backend && python main.py"
34
+
35
+
36
+ # Example pain descriptions in different languages
37
+ examples = [
38
+ ["腰部和腿部最近一周特别难受。感觉像有成千上万只蚂蚁在皮肤下面爬来爬去,停不下来;有时突然像被针戳了一下,会猛地跳起来。", "Chinese"],
39
+ ["허리와 다리가 최근 일주일 동안 특히 불편합니다. 피부 아래 수천 마리의 개미가 기어다니는 느낌이 들고, 때때로 갑자기 바늘에 찔린 것처럼 아파요.", "Korean"],
40
+ ["La espalda y las piernas han sido especialmente difíciles de soportar esta última semana. Se siente como si hubiera miles de hormigas arrastrándose bajo la piel, sin poder detenerse.", "Spanish"],
41
+ ["My lower back and legs have been especially hard to bear this past week. It feels like thousands of ants crawling under the skin, unable to stop.", "English"]
42
+ ]
43
+
44
+ # Build Gradio interface
45
+ with gr.Blocks(title="Pain Assessment System") as demo:
46
+ gr.Markdown("""
47
+ # 🏥 Multilingual Pain Assessment System
48
+
49
+ Powered by **BioLORD-2023-M** medical embeddings and **GPT-5.2**
50
+
51
+ ### Supported Languages:
52
+ - 🇨🇳 Chinese (中文)
53
+ - 🇰🇷 Korean (한국어)
54
+ - 🇪🇸 Spanish (Español)
55
+ - 🇻🇳 Hmong
56
+ - 🇺🇸 English
57
+
58
+ ### How it works:
59
+ 1. Enter patient's pain description in any supported language
60
+ 2. BioLORD analyzes medical semantics
61
+ 3. GPT-5.2 generates comprehensive clinical report
62
+ """)
63
+
64
+ with gr.Row():
65
+ with gr.Column():
66
+ text_input = gr.Textbox(
67
+ label="Patient's Pain Description",
68
+ placeholder="Enter pain description in any language...",
69
+ lines=8
70
+ )
71
+
72
+ language_input = gr.Dropdown(
73
+ choices=["Chinese", "Korean", "Spanish", "Hmong", "English"],
74
+ label="Language (optional - auto-detected)",
75
+ value="Chinese"
76
+ )
77
+
78
+ submit_btn = gr.Button("Analyze Pain", variant="primary")
79
+
80
+ with gr.Column():
81
+ output = gr.Markdown(
82
+ label="Clinical Report",
83
+ value="*Report will appear here...*"
84
+ )
85
+
86
+ # Examples
87
+ gr.Examples(
88
+ examples=examples,
89
+ inputs=[text_input, language_input],
90
+ outputs=output,
91
+ fn=analyze_pain,
92
+ cache_examples=False
93
+ )
94
+
95
+ # Event handlers
96
+ submit_btn.click(
97
+ fn=analyze_pain,
98
+ inputs=[text_input, language_input],
99
+ outputs=output
100
+ )
101
+
102
+ gr.Markdown("""
103
+ ---
104
+ ### 🔬 Model Information
105
+
106
+ - **Embeddings**: BioLORD-2023-M (SOTA on MedSTS medical semantic similarity)
107
+ - **Report Generation**: GPT-5.2
108
+ - **Dictionary**: 362 multilingual pain terms
109
+ - **Accuracy**: 85-92% on medical synonym matching
110
+
111
+ ### ℹ️ About
112
+ This system maps patient's pain expressions to standardized medical terminology using:
113
+ - **Semantic Distance Analysis**: BioLORD understands medical concepts beyond literal text
114
+ - **Knowledge Graph Integration**: Aligned with medical ontologies (UMLS/AGCT)
115
+ - **Cultural Sensitivity**: Preserves metaphors and cultural expressions
116
+
117
+ **Privacy**: BioLORD embeddings run locally. GPT-5.2 API used for report generation only.
118
+ """)
119
+
120
+ # Launch
121
+ if __name__ == "__main__":
122
+ demo.launch(
123
+ server_name="0.0.0.0",
124
+ server_port=7860,
125
+ share=False
126
+ )
quick_test.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quick Test Script - Direct Pipeline Usage Without Server
3
+ For development testing, no OpenAI API Key required
4
+ "
5
+ import sys
6
+ import os
7
+
8
+ # Add Backend to path
9
+ sys.path.append(os.path.join(os.path.dirname(__file__), 'Backend'))
10
+
11
+ from pipeline.pain_assessment_pipeline import PainAssessmentPipeline
12
+
13
+ def test_quick():
14
+ """Quick test using mocked LLM output"""
15
+
16
+ print("="*70)
17
+ print("🧪 Quick Test (No OpenAI API Required)")
18
+ print("="*70)
19
+
20
+ # Initialize pipeline
21
+ pipeline = PainAssessmentPipeline(verbose=True)
22
+
23
+ # Test cases
24
+ test_cases = [
25
+ {
26
+ "name": "中文 - 慢性神经病理性疼痛",
27
+ "text": "我有火辣辣的疼痛,已经好几个月了,腰部很难受",
28
+ "llm_entities": {
29
+ "pain_descriptors": ["火辣辣的疼痛"],
30
+ "location": "腰部",
31
+ "duration_phrase": "好几个月",
32
+ "emotion_keywords": ["难受"],
33
+ "functional_impact": None,
34
+ "intensity": "Moderate to severe"
35
+ }
36
+ },
37
+ {
38
+ "name": "韩语 - 刺痛",
39
+ "text": "허리가 따끔거리다",
40
+ "llm_entities": {
41
+ "pain_descriptors": ["따끔거리다"],
42
+ "location": "허리",
43
+ "duration_phrase": None,
44
+ "emotion_keywords": [],
45
+ "functional_impact": None,
46
+ "intensity": "Moderate"
47
+ }
48
+ },
49
+ {
50
+ "name": "西班牙语 - 急性疼痛",
51
+ "text": "Tengo un dolor agudo y punzante en la espalda",
52
+ "llm_entities": {
53
+ "pain_descriptors": ["agudo", "punzante"],
54
+ "location": "la espalda",
55
+ "duration_phrase": None,
56
+ "emotion_keywords": [],
57
+ "functional_impact": None,
58
+ "intensity": "Severe"
59
+ }
60
+ },
61
+ {
62
+ "name": "苗族语 - 灼烧痛",
63
+ "text": "Kuv mob Kub Heev heev",
64
+ "llm_entities": {
65
+ "pain_descriptors": ["Kub Heev"],
66
+ "location": None,
67
+ "duration_phrase": None,
68
+ "emotion_keywords": [],
69
+ "functional_impact": None,
70
+ "intensity": "Severe"
71
+ }
72
+ }
73
+ ]
74
+
75
+ passed = 0
76
+ failed = 0
77
+
78
+ for test in test_cases:
79
+ print("\n" + "="*70)
80
+ print(f"Test: {test['name']}")
81
+ print("-"*70)
82
+ print(f"Input: {test['text']}")
83
+
84
+ try:
85
+ # Execute pipeline with mocked LLM output
86
+ report = pipeline.execute(
87
+ test['text'],
88
+ test['llm_entities']
89
+ )
90
+
91
+ print(f"\n✅ Test Passed")
92
+ print(f"\n📋 Analysis Results:")
93
+ print(f" Pain Type: {report.structured_data.pain_type}")
94
+ print(f" Location: {report.structured_data.location}")
95
+ print(f" Temporal Pattern: {report.structured_data.temporal_pattern}")
96
+
97
+ if report.ontology_mapping_trace:
98
+ print(f"\n🔄 Ontology Mappings ({len(report.ontology_mapping_trace)} items):")
99
+ for mapping in report.ontology_mapping_trace:
100
+ lang = mapping.get('detected_language', '?')
101
+ print(f" [{lang}] {mapping['original_term']} → "
102
+ f"{mapping['mapped_english']} ({mapping.get('pain_type', 'N/A')})")
103
+
104
+ if report.clinical_recommendations:
105
+ print(f"\n💊 Clinical Recommendations ({len(report.clinical_recommendations)} items):")
106
+ for i, rec in enumerate(report.clinical_recommendations, 1):
107
+ print(f"\n {i}. {rec.recommendation[:80]}...")
108
+ print(f" Rule: {rec.triggered_by_rule}")
109
+ else:
110
+ print(f"\n💊 Clinical Recommendations: Standard assessment")
111
+
112
+ passed += 1
113
+
114
+ except Exception as e:
115
+ print(f"\n❌ Test Failed: {e}")
116
+ import traceback
117
+ traceback.print_exc()
118
+ failed += 1
119
+
120
+ # Summary
121
+ print("="*70)
122
+ print("📊 Test Summary")
123
+ print("="*70)
124
+ print(f"Passed: {passed}/{passed+failed}")
125
+ print(f"Failed: {failed}/{passed+failed}")
126
+
127
+ if failed == 0:
128
+ print("\n✅ All tests passed!")
129
+ else:
130
+ print(f"\n⚠️ {failed} test(s) failed")
131
+
132
+ return failed == 0
133
+
134
+
135
+ def show_system_info():
136
+ """Display system information"""
137
+ print("\n" + "="*70)
138
+ print("📊 System Information")
139
+ print("="*70)
140
+
141
+ pipeline = PainAssessmentPipeline(verbose=False)
142
+ info = pipeline.get_pipeline_info()
143
+
144
+ print(f"Pipeline Version: {info['pipeline_version']}")
145
+ print(f"Architecture: {info['architecture']}")
146
+ print(f"Supported Languages: {', '.join(info['supported_languages'])}")
147
+ print(f"\nOntology Coverage:")
148
+ print(f" Total: {info['ontology_coverage']['total_descriptors']} terms")
149
+ print(f" Chinese: {info['ontology_coverage']['chinese_terms']}")
150
+ print(f" Korean: {info['ontology_coverage']['korean_terms']}")
151
+ print(f" Spanish: {info['ontology_coverage']['spanish_terms']}")
152
+ print(f" Hmong: {info['ontology_coverage']['hmong_terms']}")
153
+ print(f"\nClinical Rules: {info['rule_count']}")
154
+ print(f" {', '.join(info['active_rules'])}")
155
+
156
+
157
+ if __name__ == "__main__":
158
+ print("="*70)
159
+ print("🚀 Multilingual Pain Assessment - Quick Test")
160
+ print("="*70)
161
+ print("\n💡 Note: This test does not require OpenAI API Key")
162
+ print(" Uses predefined LLM output to simulate complete pipeline")
163
+
164
+ # Display system information
165
+ show_system_info()
166
+
167
+ # Run tests
168
+ success = test_quick()
169
+
170
+ if success:
171
+ print("\n" + "="*70)
172
+ print("🎉 System working properly!")
173
+ print("="*70)
174
+ print("\nNext Steps:")
175
+ print(" 1. Set OPENAI_API_KEY environment variable")
176
+ print(" 2. Start server: python Backend/main.py")
177
+ print(" 3. Test full API: python test_api.py")
178
+ print("\nDetailed docs: HOW_TO_START.md")
179
+
180
+ sys.exit(0 if success else 1)
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.110.0
2
+ uvicorn[standard]==0.27.1
3
+ python-multipart==0.0.9
4
+ openai>=1.30.0
5
+ python-dotenv==1.0.1
6
+ pydantic>=2.0
7
+ numpy>=1.24.0
8
+ faiss-cpu>=1.7.4
9
+ sentence-transformers>=2.3.0
10
+ torch>=2.0.0
11
+ scikit-learn>=1.3.0
12
+ gradio>=4.0.0
start_server.bat ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ========================================
3
+ echo Multilingual Pain Assessment System
4
+ echo ========================================
5
+ echo.
6
+
7
+ REM Check if .env file exists
8
+ if not exist "Backend\.env" (
9
+ echo [WARNING] Backend\.env file not found
10
+ echo [INFO] Creating .env file from template...
11
+ copy Backend\.env.example Backend\.env >nul 2>&1
12
+ echo.
13
+ echo [IMPORTANT] Please edit Backend\.env and add your OpenAI API Key:
14
+ echo 1. Open file: Backend\.env
15
+ echo 2. Replace "your-openai-api-key-here" with your actual API Key
16
+ echo 3. Save the file and run this script again
17
+ echo.
18
+ echo Get API Key at: https://platform.openai.com/api-keys
19
+ echo.
20
+ pause
21
+ notepad Backend\.env
22
+ exit /b 1
23
+ )
24
+
25
+ REM Check if .env has real API Key configured
26
+ findstr /C:"your-openai-api-key-here" Backend\.env >nul
27
+ if %errorlevel% equ 0 (
28
+ echo [ERROR] Please configure OpenAI API Key first
29
+ echo.
30
+ echo Edit Backend\.env file:
31
+ echo OPENAI_API_KEY=sk-your-real-api-key-here
32
+ echo.
33
+ echo Opening editor now...
34
+ notepad Backend\.env
35
+ pause
36
+ exit /b 1
37
+ )
38
+
39
+ echo .env file configured
40
+ echo Starting server...
41
+ echo go http://localhost:8000/docs to access API docs
42
+ echo.
43
+
44
+ cd Backend
45
+ python main.py
46
+
47
+ pause
test_api.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple API Test Script - Multilingual Pain Assessment System Demo
3
+ """
4
+ import requests
5
+ import json
6
+
7
+ BASE_URL = "http://localhost:8000"
8
+
9
+
10
+ def test_health():
11
+ """Check server health status"""
12
+ try:
13
+ response = requests.get(f"{BASE_URL}/health", timeout=5)
14
+ print("✅ Server is running")
15
+ print(f" Version: {response.json()['version']}")
16
+ return True
17
+ except requests.exceptions.ConnectionError:
18
+ print("❌ Server not running")
19
+ print(" Please start server: python Backend/main.py")
20
+ return False
21
+ except Exception as e:
22
+ print(f"❌ Connection error: {e}")
23
+ return False
24
+
25
+
26
+ def test_system_info():
27
+ """Get system information"""
28
+ print("\n" + "="*70)
29
+ print("📊 System Information")
30
+ print("="*70)
31
+
32
+ try:
33
+ response = requests.get(f"{BASE_URL}/api/system-info")
34
+ data = response.json()
35
+
36
+ if data["status"] == "success":
37
+ info = data["system_info"]["pipeline_info"]
38
+ print(f"Version: {info['pipeline_version']}")
39
+ print(f"Architecture: {info['architecture']}")
40
+ print(f"Supported Languages: {', '.join(info['supported_languages'])}")
41
+ print(f"\nOntology Coverage:")
42
+ print(f" - Total: {info['ontology_coverage']['total_descriptors']} terms")
43
+ print(f" - Chinese: {info['ontology_coverage']['chinese_terms']}")
44
+ print(f" - Korean: {info['ontology_coverage']['korean_terms']}")
45
+ print(f" - Spanish: {info['ontology_coverage']['spanish_terms']}")
46
+ print(f" - Hmong: {info['ontology_coverage']['hmong_terms']}")
47
+ except Exception as e:
48
+ print(f"❌ Failed to get system info: {e}")
49
+
50
+
51
+ def test_analysis(language_name, text):
52
+ """Test pain analysis"""
53
+ print("\n" + "="*70)
54
+ print(f"🌐 Testing {language_name}")
55
+ print("="*70)
56
+ print(f"Input: {text}")
57
+
58
+ try:
59
+ response = requests.post(
60
+ f"{BASE_URL}/api/analyze-text-neuro-symbolic",
61
+ json={"text": text},
62
+ timeout=30
63
+ )
64
+
65
+ result = response.json()
66
+
67
+ if result["status"] == "success":
68
+ print("\n✅ Analysis succeeded!")
69
+
70
+ # Display structured data
71
+ data = result["structured_data"]
72
+ print(f"\n📋 Structured Data:")
73
+ print(f" Pain Type: {data['pain_type']}")
74
+ print(f" Location: {data['location']}")
75
+ print(f" Temporal Pattern: {data['temporal_pattern']}")
76
+ print(f" Intensity: {data['intensity']}")
77
+ if data['emotion']:
78
+ print(f" Emotion: {data['emotion']}")
79
+
80
+ # Display ontology mappings
81
+ if result["ontology_mapping_trace"]:
82
+ print(f"\n🔄 Ontology Mappings ({len(result['ontology_mapping_trace'])} items):")
83
+ for mapping in result["ontology_mapping_trace"]:
84
+ lang_code = mapping.get('detected_language', '?')
85
+ print(f" - [{lang_code}] {mapping['original_term']} → "
86
+ f"{mapping['mapped_english']} ({mapping.get('pain_type', 'N/A')})")
87
+
88
+ # Display clinical recommendations
89
+ if result["clinical_recommendations"]:
90
+ print(f"\n💊 Clinical Recommendations ({len(result['clinical_recommendations'])} items):")
91
+ for i, rec in enumerate(result["clinical_recommendations"], 1):
92
+ print(f"\n {i}. {rec['recommendation'][:100]}...")
93
+ print(f" Rule: {rec['triggered_by_rule']}")
94
+ print(f" Evidence: {rec['evidence']}")
95
+ else:
96
+ print("\n💊 Clinical Recommendations: Standard assessment recommended")
97
+
98
+ return True
99
+ else:
100
+ print(f"\n❌ Analysis failed: {result.get('message')}")
101
+ return False
102
+
103
+ except requests.exceptions.Timeout:
104
+ print("\n❌ Request timeout (OpenAI API might be slow)")
105
+ return False
106
+ except Exception as e:
107
+ print(f"\n❌ Error: {e}")
108
+ import traceback
109
+ traceback.print_exc()
110
+ return False
111
+
112
+
113
+ def main():
114
+ """Main test function"""
115
+ print("="*70)
116
+ print("🚀 Multilingual Pain Assessment System - API Test")
117
+ print("="*70)
118
+
119
+ # 1. Check server
120
+ if not test_health():
121
+ return
122
+
123
+ # 2. Get system info
124
+ test_system_info()
125
+
126
+ # 3. Test different languages
127
+ test_cases = [
128
+ {
129
+ "name": "Chinese 🇨🇳",
130
+ "text": "我有火辣辣的疼痛,已经好几个月了,腰部很难受"
131
+ },
132
+ {
133
+ "name": "Korean 🇰🇷",
134
+ "text": "허리가 따끔거리듯이 아프다"
135
+ },
136
+ {
137
+ "name": "Spanish 🇪🇸",
138
+ "text": "Tengo un dolor agudo y punzante en la espalda"
139
+ },
140
+ {
141
+ "name": "Hmong",
142
+ "text": "Kuv mob Kub Heev heev"
143
+ }
144
+ ]
145
+
146
+ passed = 0
147
+ total = len(test_cases)
148
+
149
+ for test_case in test_cases:
150
+ if test_analysis(test_case["name"], test_case["text"]):
151
+ passed += 1
152
+
153
+ # Summary
154
+ print("\n" + "="*70)
155
+ print("📊 Test Summary")
156
+ print("="*70)
157
+ print(f"Passed: {passed}/{total}")
158
+
159
+ if passed == total:
160
+ print("✅ All tests passed!")
161
+ else:
162
+ print(f"⚠️ {total - passed} test(s) failed")
163
+
164
+
165
+ if __name__ == "__main__":
166
+ main()
test_biolord.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BioLORD Model Test Script
3
+ 测试BioLORD-2023-M模型加载和基本功能
4
+ """
5
+
6
+ import os
7
+ os.environ["EMBEDDING_MODEL"] = "biolord"
8
+
9
+ print("=" * 60)
10
+ print("🧪 BioLORD-2023-M 模型测试")
11
+ print("=" * 60)
12
+
13
+ # Test 1: Import test
14
+ print("\n[1/4] 测试导入...")
15
+ try:
16
+ from sentence_transformers import SentenceTransformer
17
+ import numpy as np
18
+ from sklearn.metrics.pairwise import cosine_similarity
19
+ print("✅ 所有依赖导入成功")
20
+ except Exception as e:
21
+ print(f"❌ 导入失败: {e}")
22
+ exit(1)
23
+
24
+ # Test 2: Model loading
25
+ print("\n[2/4] 加载BioLORD-2023-M模型...")
26
+ print("⚠️ 首次运行会下载~1GB模型,需要2-5分钟...")
27
+ try:
28
+ model = SentenceTransformer("FremyCompany/BioLORD-2023-M")
29
+ print(f"✅ 模型加载成功!")
30
+ print(f" - 嵌入维度: {model.get_sentence_embedding_dimension()}")
31
+ print(f" - 最大序列长度: {model.max_seq_length}")
32
+ except Exception as e:
33
+ print(f"❌ 模型加载失败: {e}")
34
+ exit(1)
35
+
36
+ # Test 3: Embedding generation
37
+ print("\n[3/4] 测试多语言疼痛表达嵌入...")
38
+ test_expressions = {
39
+ "中文": "像有成千上万只蚂蚁在皮肤下面爬来爬去",
40
+ "韩文": "허리가 따끔거리듯이 아프다",
41
+ "西班牙语": "dolor punzante en la espalda",
42
+ "英文": "stabbing pain in the lower back"
43
+ }
44
+
45
+ try:
46
+ for lang, text in test_expressions.items():
47
+ embedding = model.encode(text, convert_to_numpy=True)
48
+ print(f"✅ {lang}: 生成 {len(embedding)}-维向量")
49
+ except Exception as e:
50
+ print(f"❌ 嵌入生成失败: {e}")
51
+ exit(1)
52
+
53
+ # Test 4: Medical similarity test
54
+ print("\n[4/4] 测试医学语义相似度...")
55
+ print("比较: '刺痛' vs '钝痛' vs '蚂蚁爬'")
56
+
57
+ try:
58
+ terms = ["刺痛", "钝痛", "像蚂蚁在爬"]
59
+ embeddings = model.encode(terms, convert_to_numpy=True)
60
+
61
+ # Calculate similarity matrix
62
+ similarity_matrix = cosine_similarity(embeddings)
63
+
64
+ print("\n相似度矩阵:")
65
+ print(" 刺痛 钝痛 蚂蚁爬")
66
+ for i, term in enumerate(terms):
67
+ row = f"{term:8s} "
68
+ for j in range(len(terms)):
69
+ row += f"{similarity_matrix[i][j]:.3f} "
70
+ print(row)
71
+
72
+ # Medical dictionary test
73
+ print("\n\n测试与医学词典匹配:")
74
+ patient_expr = "像成千上万只蚂蚁在皮肤下爬"
75
+ medical_terms = {
76
+ "蚊虫叮咬的刺疼": "stinging",
77
+ "蚂蚁爬感": "formication",
78
+ "麻木感": "numbness",
79
+ "刺痛": "stabbing"
80
+ }
81
+
82
+ patient_emb = model.encode([patient_expr], convert_to_numpy=True)[0]
83
+ dict_texts = list(medical_terms.keys())
84
+ dict_embs = model.encode(dict_texts, convert_to_numpy=True)
85
+
86
+ scores = cosine_similarity([patient_emb], dict_embs)[0]
87
+ ranked = sorted(zip(dict_texts, medical_terms.values(), scores), key=lambda x: x[2], reverse=True)
88
+
89
+ print(f"\n患者表达: '{patient_expr}'")
90
+ print("\n最匹配的医学术语:")
91
+ for i, (chinese, english, score) in enumerate(ranked[:3], 1):
92
+ confidence = "HIGH" if score > 0.75 else "MEDIUM" if score > 0.60 else "LOW"
93
+ print(f" {i}. {chinese} ({english})")
94
+ print(f" 相似度: {score:.3f} [{confidence}]")
95
+
96
+ print("\n✅ 医学语义理解测试通过!")
97
+
98
+ except Exception as e:
99
+ print(f"❌ 相似度测试失败: {e}")
100
+ exit(1)
101
+
102
+ # Test 5: Test integration with service
103
+ print("\n\n[5/5] 测试与系统集成...")
104
+ try:
105
+ import sys
106
+ sys.path.append("./Backend")
107
+
108
+ from services.semantic_distance_service_biolord import load_biolord_model, precompute_dictionary_embeddings
109
+
110
+ print("加载BioLORD服务...")
111
+ model = load_biolord_model()
112
+ print("✅ 服务加载成功")
113
+
114
+ print("\n预计算多语言词典嵌入...")
115
+ precompute_dictionary_embeddings()
116
+ print("✅ 词典嵌入预计算完成")
117
+
118
+ except Exception as e:
119
+ print(f"❌ 系统集成测试失败: {e}")
120
+ import traceback
121
+ traceback.print_exc()
122
+ exit(1)
123
+
124
+ # Success summary
125
+ print("\n" + "=" * 60)
126
+ print("🎉 所有测试通过!BioLORD模型已就绪")
127
+ print("=" * 60)
128
+ print("\n下一步:")
129
+ print("1. 启动服务器: cd Backend && python main.py")
130
+ print("2. 测试API: 发送疼痛描述到 /analyze_pain_text")
131
+ print("3. 查看语义分析结果(使用BioLORD医学嵌入)")
132
+ print("\n提示: 环境变量 EMBEDDING_MODEL=biolord 已启用")
test_crosslingual.py ADDED
File without changes
test_mcgill_matching.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test: BioLORD Same-Language McGill Matching
3
+
4
+ Tests the new architecture:
5
+ - Chinese patient terms → Chinese McGill translations → English standard terms
6
+ - Uses auxiliary McGill dictionary (not system's multilingual_pain_data.json)
7
+ """
8
+
9
+ import sys
10
+ import os
11
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'Backend'))
12
+
13
+ from services.semantic_distance_service_biolord import (
14
+ load_biolord_model,
15
+ precompute_dictionary_embeddings,
16
+ calculate_semantic_distances,
17
+ MCGILL_EMBEDDINGS_CACHE
18
+ )
19
+
20
+ def test_mcgill_matching():
21
+ """Test same-language McGill matching with Chinese examples."""
22
+
23
+ print("=" * 80)
24
+ print("BioLORD: Same-Language McGill Matching Test")
25
+ print("=" * 80)
26
+ print()
27
+
28
+ # Load model and precompute McGill embeddings
29
+ print("Step 1: Loading BioLORD model...")
30
+ try:
31
+ model = load_biolord_model()
32
+ print(f"✓ Model loaded: {model.get_sentence_embedding_dimension()}-dim embeddings\n")
33
+ except Exception as e:
34
+ print(f"✗ Failed to load model: {e}")
35
+ return
36
+
37
+ print("Step 2: Precomputing McGill translations...")
38
+ precompute_dictionary_embeddings()
39
+ print()
40
+
41
+ # Check what McGill terms were loaded
42
+ if 'zh' in MCGILL_EMBEDDINGS_CACHE:
43
+ zh_cache = MCGILL_EMBEDDINGS_CACHE['zh']
44
+ print(f"Chinese McGill: {len(zh_cache['terms'])} terms loaded")
45
+ print(f"Sample terms: {zh_cache['terms'][:10]}")
46
+ print()
47
+
48
+ # Test cases: real patient expressions
49
+ test_cases = [
50
+ {
51
+ "term": "蚂蚁爬",
52
+ "description": "Basic formication expression",
53
+ "expected": "formication"
54
+ },
55
+ {
56
+ "term": "像蚂蚁在爬",
57
+ "description": "More detailed formication",
58
+ "expected": "formication"
59
+ },
60
+ {
61
+ "term": "有时候像是蚂蚁爬",
62
+ "description": "Contextual formication phrase",
63
+ "expected": "formication"
64
+ },
65
+ {
66
+ "term": "火辣辣的疼",
67
+ "description": "Burning pain (colloquial)",
68
+ "expected": "burning"
69
+ },
70
+ {
71
+ "term": "麻木",
72
+ "description": "Numbness",
73
+ "expected": "numbness"
74
+ },
75
+ {
76
+ "term": "针扎感",
77
+ "description": "Pins and needles / tingling",
78
+ "expected": "pins and needles"
79
+ },
80
+ {
81
+ "term": "电击一样",
82
+ "description": "Electric shock sensation",
83
+ "expected": "electric shock"
84
+ }
85
+ ]
86
+
87
+ print("=" * 80)
88
+ print("Test Results: Chinese → Chinese McGill → English")
89
+ print("=" * 80)
90
+ print()
91
+
92
+ correct = 0
93
+ total = len(test_cases)
94
+
95
+ for i, test in enumerate(test_cases, 1):
96
+ print(f"Test {i}/{total}: {test['description']}")
97
+ print(f" Patient term: {test['term']}")
98
+ print(f" Expected English: {test['expected']}")
99
+
100
+ # Test using calculate_semantic_distances
101
+ result = calculate_semantic_distances(
102
+ unmapped_terms=[test['term']],
103
+ patient_text=test['term'],
104
+ language="Chinese"
105
+ )
106
+
107
+ if result and 'unmapped_analysis' in result:
108
+ analysis = result['unmapped_analysis'][0]
109
+ matched_english = analysis['matched_standard_english']
110
+ matched_native = analysis.get('matched_mcgill_native', 'N/A')
111
+ confidence = analysis['confidence']
112
+ score = analysis['closest_matches'][0]['score']
113
+
114
+ is_correct = matched_english == test['expected']
115
+ status = "✓ CORRECT" if is_correct else f"✗ WRONG (got: {matched_english})"
116
+
117
+ if is_correct:
118
+ correct += 1
119
+
120
+ print(f" Matched McGill (中文): {matched_native}")
121
+ print(f" Matched English: {matched_english}")
122
+ print(f" Confidence: {confidence} (score: {score:.3f})")
123
+ print(f" {status}")
124
+
125
+ # Show top 3 matches
126
+ print(f" Top 3 matches:")
127
+ for j, match in enumerate(analysis['closest_matches'][:3], 1):
128
+ print(f" {j}. {match['native_term']} ({match['english']}) - {match['score']:.3f}")
129
+ else:
130
+ print(f" ✗ No result returned")
131
+
132
+ print()
133
+
134
+ print("=" * 80)
135
+ print(f"Final Score: {correct}/{total} correct ({correct/total*100:.1f}%)")
136
+ print("=" * 80)
137
+
138
+ # Compare with expected performance
139
+ if correct >= total * 0.8:
140
+ print("✓ EXCELLENT: Same-language matching working as expected!")
141
+ elif correct >= total * 0.6:
142
+ print("⚠ GOOD: Most matches correct, may need tuning")
143
+ else:
144
+ print("✗ NEEDS IMPROVEMENT: Many incorrect matches")
145
+
146
+ if __name__ == "__main__":
147
+ test_mcgill_matching()
test_report.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script to verify report generation is using the new Medical Anthropologist prompt
3
+ """
4
+ import sys
5
+ import os
6
+ sys.path.append('Backend')
7
+
8
+ # Load .env from Backend directory
9
+ try:
10
+ from dotenv import load_dotenv
11
+ load_dotenv('Backend/.env')
12
+ except ImportError:
13
+ print("Note: dotenv not available, using system environment variables")
14
+
15
+ # Now import after dotenv is loaded
16
+ from utils.report_generator import generate_comprehensive_report
17
+
18
+ # Test data
19
+ test_original_text = "我这两天一直觉得肚子钝痛,不是那种特别剧烈的,就是一直隐隐作痛。有时候会觉得按到那个部位的时候会更明显一点,躺着会好一点,还挺烦有点受不了的感觉。"
20
+
21
+ test_structured_data = {
22
+ 'pain_type': 'Nociceptive (aching, dull, tingling, constant, tender)',
23
+ 'location': 'Abdomen',
24
+ 'temporal_pattern': 'Constant',
25
+ 'intensity': '不是那种特别剧烈的 [Not that particularly severe]',
26
+ 'emotion': 'exhausted, depressed',
27
+ 'functional_impact': '走路或者按到那个的时候会更明显一点,躺着会好一点 [more noticeable when walking or pressing the area, better when lying down]'
28
+ }
29
+
30
+ test_ontology_mappings = [
31
+ {'original_term': '钝痛', 'mapped_english': 'dull', 'pain_type': 'nociceptive'},
32
+ {'original_term': '隐隐作痛', 'mapped_english': 'aching', 'pain_type': 'nociceptive'},
33
+ {'original_term': '一直', 'mapped_english': 'constant', 'pain_type': 'temporal'}
34
+ ]
35
+
36
+ test_clinical_recommendations = [
37
+ {
38
+ 'triggered_by_rule': 'Nociceptive Pain Assessment',
39
+ 'recommendation': 'Standard pain assessment and management pathway recommended. Consider detailed clinical interview for further characterization.',
40
+ 'confidence': 'medium'
41
+ }
42
+ ]
43
+
44
+ test_detected_language = 'Chinese'
45
+
46
+ print("=" * 80)
47
+ print("TESTING REPORT GENERATOR")
48
+ print("=" * 80)
49
+ print("\nCalling generate_comprehensive_report()...\n")
50
+
51
+ try:
52
+ report = generate_comprehensive_report(
53
+ original_text=test_original_text,
54
+ structured_data=test_structured_data,
55
+ ontology_mappings=test_ontology_mappings,
56
+ clinical_recommendations=test_clinical_recommendations,
57
+ detected_language=test_detected_language
58
+ )
59
+
60
+ print("=" * 80)
61
+ print("GENERATED REPORT:")
62
+ print("=" * 80)
63
+ print(report)
64
+ print("\n" + "=" * 80)
65
+
66
+ # Check if it's using the new format
67
+ if "📝 Patient's Description" in report:
68
+ print("✅ SUCCESS: Using new Medical Anthropologist format!")
69
+ elif "Patient Presentation:" in report:
70
+ print("❌ FAIL: Still using old template format!")
71
+ print("\nThis means the function is hitting the exception handler.")
72
+ else:
73
+ print("⚠️ UNKNOWN: Cannot determine format")
74
+
75
+ except Exception as e:
76
+ print(f"❌ ERROR: {e}")
77
+ import traceback
78
+ traceback.print_exc()
79
+
80
+ print("=" * 80)