Spaces:
Sleeping
Sleeping
Initial commit from PainReport
Browse files- .env.example +12 -0
- .gitattributes +2 -35
- .gitignore +14 -0
- Backend/data/questionnaire_form.xlsx +0 -0
- Backend/images/Tingling.jpg +0 -0
- Backend/images/burning.jpg +0 -0
- Backend/images/dull.jpg +0 -0
- Backend/images/sharp.png +0 -0
- Backend/images/throbbing.jpg +0 -0
- Backend/inference/__init__.py +6 -0
- Backend/inference/rule_engine.py +355 -0
- Backend/main.py +214 -0
- Backend/models/__init__.py +6 -0
- Backend/models/pain_schema.py +192 -0
- Backend/ontology/__init__.py +20 -0
- Backend/ontology/mcgill_translations.py +268 -0
- Backend/ontology/pain_mapping.py +449 -0
- Backend/ontology/pain_mapping_multilingual.py +320 -0
- Backend/pipeline/__init__.py +6 -0
- Backend/pipeline/pain_assessment_pipeline.py +644 -0
- Backend/read_xlsx.py +33 -0
- Backend/scripts/format_pain_descriptors.py +85 -0
- Backend/scripts/multilingual_pain_data.json +1525 -0
- Backend/scripts/pain_descriptors_formatted.py +1533 -0
- Backend/scripts/parse_multilingual_data.py +132 -0
- Backend/services/__init__.py +0 -0
- Backend/services/conversation_service.py +52 -0
- Backend/services/llm_service.py +796 -0
- Backend/services/neuro_symbolic_service.py +304 -0
- Backend/services/semantic_distance_service.py +124 -0
- Backend/services/semantic_distance_service_biolord.py +320 -0
- Backend/services/semantic_distance_service_v2.py +259 -0
- Backend/services/whisper_service.py +27 -0
- Backend/test_multilingual_pipeline.py +112 -0
- Backend/utils/__init__.py +1 -0
- Backend/utils/language_detector.py +112 -0
- Backend/utils/report_generator.py +292 -0
- Frontend/demo.html +942 -0
- Procfile +1 -0
- app.py +126 -0
- quick_test.py +180 -0
- requirements.txt +12 -0
- start_server.bat +47 -0
- test_api.py +166 -0
- test_biolord.py +132 -0
- test_crosslingual.py +0 -0
- test_mcgill_matching.py +147 -0
- 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 |
-
|
| 2 |
-
*
|
| 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)
|