Laura Wagner commited on
Commit
23cc0e3
·
1 Parent(s): 6d08c5c

added llm annotation code

Browse files
jupyter_notebooks/DEEPFAKE_PIPELINE_GUIDE.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deepfake Adapter Dataset Processing - Quick Start Guide
2
+
3
+ ## Overview
4
+
5
+ This pipeline processes the `real_person_adapters.csv` dataset to identify and annotate real people used in deepfake LoRA models using three LLM options: **Qwen**, **Llama**, and **Mistral**.
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Prerequisites
10
+
11
+ ```bash
12
+ # Install required packages
13
+ pip install pandas numpy emoji requests tqdm spacy
14
+
15
+ # Download spaCy English model (for NER)
16
+ python -m spacy download en_core_web_sm
17
+ ```
18
+
19
+ **Note**: The spaCy model will be automatically downloaded when you run the notebook if not already installed.
20
+
21
+ ### 2. Set Up API Keys
22
+
23
+ Choose at least ONE LLM provider and get an API key:
24
+
25
+ | Provider | Model | Sign Up Link | Est. Cost (10k entries) |
26
+ |----------|-------|--------------|-------------------------|
27
+ | **Qwen** | Qwen-Max | https://dashscope.aliyun.com/ | Varies |
28
+ | **Llama** | Llama-3.1-70B | https://www.together.ai/ | ~$5-10 |
29
+ | **Mistral** | Mistral Large | https://mistral.ai/ | ~$40-80 |
30
+
31
+ Create your API key file in `misc/credentials/`:
32
+
33
+ ```bash
34
+ # For Qwen
35
+ echo "your-api-key-here" > misc/credentials/qwen_api_key.txt
36
+
37
+ # For Llama (via Together AI)
38
+ echo "your-api-key-here" > misc/credentials/together_api_key.txt
39
+
40
+ # For Mistral
41
+ echo "your-api-key-here" > misc/credentials/mistral_api_key.txt
42
+ ```
43
+
44
+ ### 3. Run the Notebook
45
+
46
+ Open `Section_2-3-4_Figure_8_deepfake_adapters.ipynb` and:
47
+
48
+ 1. **Run all cells sequentially** from top to bottom
49
+ 2. The default configuration uses Qwen in test mode (10 samples)
50
+ 3. Review the test results
51
+ 4. To process the full dataset, change in the LLM annotation cell:
52
+ ```python
53
+ TEST_MODE = False
54
+ ```
55
+
56
+ ## Pipeline Stages
57
+
58
+ ### Stage 1: NER & Name Cleaning
59
+ - **Input**: `data/CSV/real_person_adapters.csv`
60
+ - **Output**: `data/CSV/NER_POI_step01_pre_annotation.csv`
61
+ - **Function**: Cleans adapter names to extract real person names
62
+ - Removes: emoji, "lora", "v1", special characters
63
+ - Example: "IU LoRA v2 🎤" → "IU"
64
+
65
+ ### Stage 2: Country/Nationality Mapping
66
+ - **Input**: Step 1 output + `misc/lists/countries.csv`
67
+ - **Output**: `data/CSV/NER_POI_step02_annotated.csv`
68
+ - **Function**: Maps tags to standardized countries
69
+ - Example: "korean" → "South Korea"
70
+ - Excludes uninhabited territories
71
+
72
+ ### Stage 3: LLM Profession Annotation
73
+ - **Input**: Step 2 output + `misc/lists/professions.csv`
74
+ - **Output**: `data/CSV/{llm}_annotated_POI_test.csv` (test) or `{llm}_annotated_POI.csv` (full)
75
+ - **Function**: Uses LLM to identify:
76
+ - Full name
77
+ - Gender
78
+ - Up to 3 professions (from profession list)
79
+ - Country
80
+ - **Progress**: Automatically saves every 10 rows
81
+ - **Resumable**: Can continue from last saved progress if interrupted
82
+
83
+ ## Configuration Options
84
+
85
+ In the LLM annotation cell, you can configure:
86
+
87
+ ```python
88
+ # Choose LLM provider
89
+ SELECTED_LLM = 'qwen' # Options: 'qwen', 'llama', 'mistral'
90
+
91
+ # Test mode (recommended for first run)
92
+ TEST_MODE = True # True = test on small sample
93
+ TEST_SIZE = 10 # Number of rows for testing
94
+
95
+ # Processing limits
96
+ MAX_ROWS = 20000 # Maximum rows to process (None = all)
97
+ SAVE_INTERVAL = 10 # Save progress every N rows
98
+ ```
99
+
100
+ ## Expected Output Format
101
+
102
+ The final dataset will include all original columns plus:
103
+
104
+ | Column | Description | Example |
105
+ |--------|-------------|---------|
106
+ | `real_name` | Cleaned name | "IU" |
107
+ | `full_name` | Full name from LLM | "Lee Ji-eun (IU)" |
108
+ | `gender` | Gender from LLM | "Female" |
109
+ | `profession_llm` | Up to 3 professions | "singer, actor, celebrity" |
110
+ | `country` | Country from LLM | "South Korea" |
111
+ | `likely_country` | Country from tags | "South Korea" |
112
+ | `likely_nationality` | Nationality from tags | "South Korean" |
113
+ | `tags` | Combined tags | "['korean', 'celebrity', 'singer']" |
114
+
115
+ ## Troubleshooting
116
+
117
+ ### API Key Errors
118
+ ```
119
+ Warning: No API key for qwen
120
+ ```
121
+ **Solution**: Ensure your API key file exists and contains only the key (no extra whitespace)
122
+
123
+ ### Rate Limiting
124
+ ```
125
+ Qwen API error (attempt 1/3): 429 Too Many Requests
126
+ ```
127
+ **Solution**: The code automatically retries with exponential backoff. You can also:
128
+ - Increase `time.sleep(0.5)` to a higher value
129
+ - Process in smaller batches
130
+
131
+ ### Progress Lost
132
+ **Solution**: The pipeline saves progress automatically. Check:
133
+ - `data/CSV/{llm}_annotated_POI_test.csv` - your partial results
134
+ - `misc/{llm}_query_index.txt` - last processed index
135
+ - Just re-run the cell and it will resume from the last saved progress
136
+
137
+ ### JSON Parse Errors from LLM
138
+ ```
139
+ Qwen API error: JSONDecodeError
140
+ ```
141
+ **Solution**: This is usually temporary. The code:
142
+ - Returns "Unknown" for failed queries
143
+ - Continues processing
144
+ - You can manually review/reprocess failed entries later
145
+
146
+ ## Cost Management
147
+
148
+ ### Estimate Costs Before Processing
149
+
150
+ For a dataset with N entries:
151
+ - **Qwen**: Contact Alibaba Cloud for pricing
152
+ - **Llama**: ~N × $0.0005 = ~$5 per 10k entries
153
+ - **Mistral**: ~N × $0.004 = ~$40 per 10k entries
154
+
155
+ ### Best Practices
156
+
157
+ 1. **Always test first**: Run with `TEST_MODE = True` on 10 samples
158
+ 2. **Monitor API usage**: Check your API provider's dashboard
159
+ 3. **Use cheaper models first**: Try Llama before Mistral
160
+ 4. **Process in batches**: Set `MAX_ROWS` to process incrementally
161
+ 5. **Save intermediate results**: The automatic saving feature helps prevent data loss
162
+
163
+ ## Comparing Multiple LLMs
164
+
165
+ To compare results from different LLMs:
166
+
167
+ 1. Run the pipeline with `SELECTED_LLM = 'qwen'`
168
+ 2. Change to `SELECTED_LLM = 'llama'` and run again
169
+ 3. Change to `SELECTED_LLM = 'mistral'` and run again
170
+ 4. Compare the three output files:
171
+ - `qwen_annotated_POI.csv`
172
+ - `llama_annotated_POI.csv`
173
+ - `mistral_annotated_POI.csv`
174
+
175
+ ## Files Created
176
+
177
+ The pipeline creates these files:
178
+
179
+ ```
180
+ data/CSV/
181
+ ├── NER_POI_step01_pre_annotation.csv # After name cleaning
182
+ ├── NER_POI_step02_annotated.csv # After country mapping
183
+ ├── qwen_annotated_POI_test.csv # Test results (Qwen)
184
+ ├── qwen_annotated_POI.csv # Full results (Qwen)
185
+ ├── llama_annotated_POI.csv # Full results (Llama)
186
+ └── mistral_annotated_POI.csv # Full results (Mistral)
187
+
188
+ misc/
189
+ ├── qwen_query_index.txt # Progress tracking
190
+ ├── llama_query_index.txt # Progress tracking
191
+ └── mistral_query_index.txt # Progress tracking
192
+ ```
193
+
194
+ ## Support
195
+
196
+ For issues or questions:
197
+ 1. Check this guide for common problems
198
+ 2. Review `misc/credentials/README.md` for API setup
199
+ 3. Read the notebook documentation (first cell)
200
+ 4. Check API provider documentation for service-specific issues
201
+
202
+ ## Ethical Considerations
203
+
204
+ This research documents ethical problems with AI deepfake models. The dataset and analysis help:
205
+ - Understand the scope of unauthorized person likeness usage
206
+ - Document professions/demographics most affected
207
+ - Inform policy and technical solutions
208
+ - Raise awareness about deepfake technology misuse
209
+
210
+ Use this data responsibly and respect individual privacy and consent.
jupyter_notebooks/LLM_MODELS_COMPARISON.md ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LLM Models for Deepfake Annotation
2
+
3
+ ## Overview
4
+
5
+ The pipeline now includes **6 LLM options** in individual cells for easy comparison:
6
+
7
+ 1. **Deepseek** - Testing (use first!)
8
+ 2. **Qwen (API)** - Chinese (Alibaba Cloud)
9
+ 3. **Llama** - American (Meta)
10
+ 4. **Mixtral** - French (Mistral AI)
11
+ 5. **Gemma** - American Open Source (Google)
12
+ 6. **Qwen-2.5-32B Local** - FREE local inference (NEW!)
13
+
14
+ ## The 6 LLMs
15
+
16
+ ### 1. Deepseek (Testing)
17
+ **Cell 10**
18
+
19
+ - **Model**: deepseek-chat
20
+ - **Provider**: DeepSeek
21
+ - **API**: https://platform.deepseek.com/
22
+ - **Cost**: ~$0.14-0.28 per 1M tokens (~$1-2 for 10k entries)
23
+ - **Use case**: **Test this first!** Cheapest option to verify pipeline works
24
+ - **API Key**: `misc/credentials/deepseek_api_key.txt`
25
+
26
+ ---
27
+
28
+ ### 2. Qwen API (Chinese)
29
+ **Cells 11-12**
30
+
31
+ - **Model**: qwen-max (automatically uses Qwen3-Max)
32
+ - **Provider**: Alibaba Cloud DashScope
33
+ - **API**: https://dashscope.aliyun.com/
34
+ - **Cost**: Variable (check Alibaba pricing)
35
+ - **Use case**: Chinese company, strong multilingual support
36
+ - **API Key**: `misc/credentials/qwen_api_key.txt`
37
+ - **Note**: Uses latest Qwen3-Max when you specify `qwen-max`
38
+
39
+ ---
40
+
41
+ ### 6. Qwen-2.5-32B Local (FREE!)
42
+ **Cells 19-20** (NEW!)
43
+
44
+ - **Model**: qwen2.5:32b-instruct
45
+ - **Provider**: Ollama (local inference)
46
+ - **Setup**: https://ollama.com/
47
+ - **Cost**: **$0** (FREE - no API costs!)
48
+ - **Requirements**:
49
+ - A100 80GB GPU (or similar)
50
+ - ~25GB VRAM during inference
51
+ - ~20GB storage for model download
52
+ - Ollama installed
53
+ - **Speed**: 5-10 tokens/sec on A100 (~100-200 samples/hour)
54
+ - **Use case**:
55
+ - ✅ Large datasets (>1000 samples) where cost matters
56
+ - ✅ Privacy-sensitive research data
57
+ - ✅ Offline processing
58
+ - ✅ Strong multilingual support
59
+ - **Setup guide**: See `QWEN_LOCAL_SETUP.md`
60
+
61
+ ---
62
+
63
+ ### 3. Llama (American)
64
+ **Cells 13-14**
65
+
66
+ - **Model**: meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo
67
+ - **Provider**: Together AI (hosting Meta's model)
68
+ - **Developer**: Meta (American)
69
+ - **API**: https://www.together.ai/
70
+ - **Cost**: ~$0.90 per 1M tokens (~$5-10 for 10k entries)
71
+ - **Use case**: Open-source American model, good quality
72
+ - **API Key**: `misc/credentials/together_api_key.txt`
73
+
74
+ ---
75
+
76
+ ### 4. Mixtral (French)
77
+ **Cells 15-16**
78
+
79
+ - **Model**: open-mixtral-8x22b
80
+ - **Provider**: Mistral AI
81
+ - **Developer**: Mistral AI (French)
82
+ - **API**: https://mistral.ai/
83
+ - **Cost**: ~$2 per 1M tokens (~$10-20 for 10k entries)
84
+ - **Use case**: European alternative, Mixture-of-Experts architecture
85
+ - **API Key**: `misc/credentials/mistral_api_key.txt`
86
+ - **Note**: Using open-mixtral-8x22b (cheaper than mistral-large)
87
+
88
+ ---
89
+
90
+ ### 5. Gemma (American Open Source)
91
+ **Cells 17-18**
92
+
93
+ - **Model**: google/gemma-2-27b-it
94
+ - **Provider**: Together AI (hosting Google's model)
95
+ - **Developer**: Google (American)
96
+ - **API**: https://www.together.ai/ (same as Llama)
97
+ - **Cost**: ~$0.80 per 1M tokens (~$4-8 for 10k entries)
98
+ - **Use case**: American open-source alternative, competitive quality
99
+ - **API Key**: `misc/credentials/together_api_key.txt` (same as Llama)
100
+ - **Note**: Fully open-source, can be self-hosted
101
+
102
+ ---
103
+
104
+ ## Cost Comparison (10,000 entries)
105
+
106
+ | Model | Provider | Cost | Time | Origin |
107
+ |-------|----------|------|------|--------|
108
+ | **Qwen-2.5-32B Local** | Ollama (local) | **$0** | ~50-100 hrs | 🇨🇳 Chinese |
109
+ | **Deepseek** | DeepSeek | ~$1-2 | ~5-10 hrs | 🇨🇳 Chinese |
110
+ | **Gemma 2** | Together AI | ~$4-8 | ~5-10 hrs | 🇺🇸 American (open) |
111
+ | **Llama 3.1** | Together AI | ~$5-10 | ~5-10 hrs | 🇺🇸 American (open) |
112
+ | **Mixtral** | Mistral AI | ~$10-20 | ~5-10 hrs | 🇫🇷 French (open) |
113
+ | **Qwen API** | Alibaba | Variable | ~5-10 hrs | 🇨🇳 Chinese |
114
+
115
+ **Note**: Local inference is FREE but slower. Good for large datasets where cost matters more than time.
116
+
117
+ ## Recommended Testing Order
118
+
119
+ ### 1. Start with Deepseek
120
+ ```python
121
+ # Cell 10
122
+ TEST_MODE = True
123
+ TEST_SIZE = 10
124
+ ```
125
+ - **Why**: Cheapest, verify pipeline works
126
+ - **Cost**: Pennies for 10 samples
127
+
128
+ ### 2. Compare on Small Sample
129
+ Pick 2-3 models and run on same 100 samples:
130
+ ```python
131
+ # In each cell:
132
+ TEST_MODE = True
133
+ TEST_SIZE = 100
134
+ ```
135
+
136
+ **Good combinations:**
137
+ - Budget: Deepseek + Gemma
138
+ - Quality: Llama + Mixtral
139
+ - Geographic: Qwen + Llama + Mixtral
140
+
141
+ ### 3. Production Run
142
+ Choose best model from testing and run full dataset:
143
+ ```python
144
+ TEST_MODE = False
145
+ MAX_ROWS = None # or 20000
146
+ ```
147
+
148
+ ## API Key Setup
149
+
150
+ ### For Deepseek & Qwen (separate keys):
151
+ ```bash
152
+ echo "your-deepseek-key" > misc/credentials/deepseek_api_key.txt
153
+ echo "your-qwen-key" > misc/credentials/qwen_api_key.txt
154
+ ```
155
+
156
+ ### For Llama & Gemma (same Together AI key):
157
+ ```bash
158
+ echo "your-together-key" > misc/credentials/together_api_key.txt
159
+ ```
160
+ Both Llama and Gemma use the same Together AI key!
161
+
162
+ ### For Mixtral:
163
+ ```bash
164
+ echo "your-mistral-key" > misc/credentials/mistral_api_key.txt
165
+ ```
166
+
167
+ ## Output Files
168
+
169
+ Each LLM saves to a separate file:
170
+
171
+ ```
172
+ data/CSV/
173
+ ├── deepseek_annotated_POI_test.csv # Deepseek test
174
+ ├── deepseek_annotated_POI.csv # Deepseek full
175
+ ├── qwen_annotated_POI_test.csv # Qwen API test
176
+ ├── qwen_annotated_POI.csv # Qwen API full
177
+ ├── qwen_local_annotated_POI_test.csv # Qwen Local test (NEW!)
178
+ ├── qwen_local_annotated_POI.csv # Qwen Local full (NEW!)
179
+ ├── llama_annotated_POI_test.csv # Llama test
180
+ ├── llama_annotated_POI.csv # Llama full
181
+ ├── mixtral_annotated_POI_test.csv # Mixtral test
182
+ ├── mixtral_annotated_POI.csv # Mixtral full
183
+ ├── gemma_annotated_POI_test.csv # Gemma test
184
+ └── gemma_annotated_POI.csv # Gemma full
185
+ ```
186
+
187
+ ## Comparing Results
188
+
189
+ After running multiple LLMs, compare results:
190
+
191
+ ```python
192
+ import pandas as pd
193
+
194
+ # Load results from different models
195
+ deepseek_df = pd.read_csv('data/CSV/deepseek_annotated_POI_test.csv')
196
+ qwen_df = pd.read_csv('data/CSV/qwen_annotated_POI_test.csv')
197
+ qwen_local_df = pd.read_csv('data/CSV/qwen_local_annotated_POI_test.csv') # NEW!
198
+ llama_df = pd.read_csv('data/CSV/llama_annotated_POI_test.csv')
199
+ mixtral_df = pd.read_csv('data/CSV/mixtral_annotated_POI_test.csv')
200
+ gemma_df = pd.read_csv('data/CSV/gemma_annotated_POI_test.csv')
201
+
202
+ # Compare profession distributions
203
+ print("Deepseek professions:", deepseek_df['profession_llm'].value_counts().head())
204
+ print("Qwen API professions:", qwen_df['profession_llm'].value_counts().head())
205
+ print("Qwen Local professions:", qwen_local_df['profession_llm'].value_counts().head()) # NEW!
206
+ print("Llama professions:", llama_df['profession_llm'].value_counts().head())
207
+ print("Mixtral professions:", mixtral_df['profession_llm'].value_counts().head())
208
+ print("Gemma professions:", gemma_df['profession_llm'].value_counts().head())
209
+
210
+ # Compare specific cases
211
+ print("\nIrene identification:")
212
+ print("Deepseek:", deepseek_df[deepseek_df['real_name'] == 'Irene']['full_name'].values)
213
+ print("Qwen API:", qwen_df[qwen_df['real_name'] == 'Irene']['full_name'].values)
214
+ print("Qwen Local:", qwen_local_df[qwen_local_df['real_name'] == 'Irene']['full_name'].values)
215
+ print("Llama:", llama_df[llama_df['real_name'] == 'Irene']['full_name'].values)
216
+ print("Mixtral:", mixtral_df[mixtral_df['real_name'] == 'Irene']['full_name'].values)
217
+ print("Gemma:", gemma_df[gemma_df['real_name'] == 'Irene']['full_name'].values)
218
+ ```
219
+
220
+ ## Model Characteristics
221
+
222
+ ### Deepseek
223
+ - ✅ Very cheap
224
+ - ✅ Good for testing
225
+ - ⚠️ Less documentation
226
+ - 🇨🇳 Chinese company
227
+
228
+ ### Qwen (Qwen3-Max)
229
+ - ✅ Latest version automatically used
230
+ - ✅ Strong multilingual
231
+ - ✅ Good Asian name recognition
232
+ - 💰 Variable cost
233
+ - 🇨🇳 Chinese company (Alibaba)
234
+
235
+ ### Llama 3.1 70B
236
+ - ✅ Open-source
237
+ - ✅ Strong overall performance
238
+ - ✅ Well-documented
239
+ - ✅ American (Meta)
240
+ - 💰 Mid-range cost
241
+
242
+ ### Mixtral 8x22B
243
+ - ✅ Open-source
244
+ - ✅ MoE architecture (efficient)
245
+ - ✅ European alternative
246
+ - 💰 Mid-range cost
247
+ - 🇫🇷 French company
248
+
249
+ ### Gemma 2 27B
250
+ - ✅ Fully open-source
251
+ - ✅ Can self-host
252
+ - ✅ American (Google)
253
+ - ✅ Cheap via API
254
+ - ✅ Good quality for size
255
+
256
+ ### Qwen-2.5-32B Local (NEW!)
257
+ - ✅ **FREE** - $0 cost (no API fees)
258
+ - ✅ **FAST** - Local inference on A100 (5-10 tokens/sec)
259
+ - ✅ **PRIVATE** - Data never leaves your machine
260
+ - ✅ **OFFLINE** - Works without internet
261
+ - ✅ **HIGH QUALITY** - 32B parameter model
262
+ - ✅ Strong multilingual support
263
+ - ⚠️ Requires: A100 80GB GPU, ~25GB VRAM, Ollama installed
264
+ - 🇨🇳 Chinese company (Alibaba)
265
+ - 📦 Model size: ~20GB download
266
+
267
+ ## Decision Matrix
268
+
269
+ ### If you prioritize...
270
+
271
+ **FREE / Zero Cost**: Use **Qwen-2.5-32B Local** (no API fees!)
272
+
273
+ **Cost** (with API): Use **Deepseek** or **Gemma**
274
+
275
+ **Quality**: Use **Qwen-2.5-32B Local**, **Llama**, or **Mixtral**
276
+
277
+ **Privacy**: Use **Qwen-2.5-32B Local** (data stays on your machine)
278
+
279
+ **American/Open Source**: Use **Gemma** or **Llama**
280
+
281
+ **Asian Names**: Use **Qwen** (API or Local - strong multilingual)
282
+
283
+ **European Provider**: Use **Mixtral**
284
+
285
+ **Testing**: Use **Deepseek** first, always!
286
+
287
+ ## Running Multiple Models
288
+
289
+ You can run all 6 models in sequence:
290
+
291
+ ```python
292
+ # 1. Run Cell 10 (Deepseek) - verify works (~$1-2 for 10k)
293
+ # 2. Run Cell 12 (Qwen API) - Chinese perspective (~variable cost)
294
+ # 3. Run Cell 14 (Llama) - American perspective (~$5-10 for 10k)
295
+ # 4. Run Cell 16 (Mixtral) - European perspective (~$10-20 for 10k)
296
+ # 5. Run Cell 18 (Gemma) - Open source perspective (~$4-8 for 10k)
297
+ # 6. Run Cell 20 (Qwen-2.5-32B Local) - FREE local inference ($0!)
298
+ ```
299
+
300
+ Each saves to its own file, so you can compare results!
301
+
302
+ ## Notes
303
+
304
+ - **Llama and Gemma use the same API key** (Together AI)
305
+ - All models use the **same 9 profession categories**
306
+ - All models have **automatic retries** with exponential backoff
307
+ - All models **save progress** every 10 rows
308
+ - All models are **resumable** if interrupted
309
+
310
+ ## Summary
311
+
312
+ You now have **6 LLM options** to choose from:
313
+
314
+ 1. 🧪 **Deepseek** - Test first (cheapest API)
315
+ 2. ����🇳 **Qwen3-Max API** - Chinese, strong multilingual
316
+ 3. 🇺🇸 **Llama 3.1 70B** - American, open-source
317
+ 4. 🇫🇷 **Mixtral 8x22B** - French, open-source MoE
318
+ 5. 🇺🇸 **Gemma 2 27B** - American open-source (Google)
319
+ 6. 💰 **Qwen-2.5-32B Local** - FREE local inference (NEW!)
320
+
321
+ Each in its own cell, easy to run and compare! 🎉
322
+
323
+ **Recommended workflow**:
324
+ 1. Test with Deepseek (Cell 10) - verify pipeline works
325
+ 2. For small datasets (<1000): Use API (Deepseek/Gemma/Llama)
326
+ 3. For large datasets (>1000): Use Qwen-2.5-32B Local (Cell 20) - FREE!
jupyter_notebooks/QUICK_START_LOCAL.md ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quick Start: Running Qwen-2.5-32B Locally
2
+
3
+ This is a quick guide to get you started with FREE local LLM inference using your A100 GPU.
4
+
5
+ ## Why Local?
6
+
7
+ ✅ **$0 cost** - No API fees
8
+ ✅ **Privacy** - Data stays on your machine
9
+ ✅ **Quality** - 32B parameter model with strong performance
10
+
11
+ ## Setup (One-time)
12
+
13
+ ### 1. Pull the Model (~10-30 minutes)
14
+
15
+ ```bash
16
+ # Pull Qwen-2.5-32B-Instruct
17
+ ollama pull qwen2.5:32b-instruct
18
+
19
+ # Wait for download to complete (~20GB)
20
+ # Model will be cached at: ~/.ollama/models/
21
+ ```
22
+
23
+ ### 2. Verify Model is Ready
24
+
25
+ ```bash
26
+ # List installed models
27
+ ollama list
28
+
29
+ # Should show: qwen2.5:32b-instruct
30
+
31
+ # Test it
32
+ ollama run qwen2.5:32b-instruct "Hello, who are you?"
33
+ ```
34
+
35
+ If you see a response, you're ready! ✅
36
+
37
+ ## Running the Notebook
38
+
39
+ ### Open the Notebook
40
+
41
+ ```bash
42
+ cd jupyter_notebooks
43
+ jupyter notebook Section_2-3-4_Figure_8_deepfake_adapters.ipynb
44
+ ```
45
+
46
+ ### Run the Cells
47
+
48
+ 1. **Cell 5**: NER & Name Cleaning (processes names)
49
+ 2. **Cell 7**: Country/Nationality Mapping
50
+ 3. **Cell 20**: Qwen-2.5-32B Local Annotation 👈 **This is the new one!**
51
+
52
+ ### Configure Cell 20
53
+
54
+ ```python
55
+ # Start with test mode
56
+ TEST_MODE = True
57
+ TEST_SIZE = 10
58
+
59
+ # Then run full dataset
60
+ TEST_MODE = False
61
+ MAX_ROWS = 20000 # or None for all
62
+ ```
63
+
64
+ ### Run Cell 20
65
+
66
+ Just click "Run" or press Shift+Enter. The cell will:
67
+ 1. Check if Ollama is installed ✅
68
+ 2. Check if model is available ✅
69
+ 3. Start annotating
70
+ 4. Save progress every 10 rows
71
+ 5. Show completion stats
72
+
73
+ ### Monitor Progress
74
+
75
+ ```
76
+ Qwen Local: 100%|██████████| 10/10 [02:30<00:00, 15.0s/it]
77
+ ✅ Saved after 10 rows (~24.0 samples/hour)
78
+
79
+ ✅ Done! Results: data/CSV/qwen_local_annotated_POI_test.csv
80
+ Total time: 2.5 minutes
81
+ Average speed: 240.0 samples/hour
82
+ ```
83
+
84
+ ## Performance
85
+
86
+ On your A100 80GB:
87
+ - **Speed**: ~5-10 tokens/second
88
+ - **Throughput**: ~100-200 samples/hour
89
+ - **Memory**: ~22-25GB VRAM
90
+ - **Cost**: $0
91
+
92
+ ### Time Estimates
93
+
94
+ | Dataset Size | Time |
95
+ |-------------|------|
96
+ | 10 samples (test) | ~2-3 minutes |
97
+ | 100 samples | ~20-30 minutes |
98
+ | 1,000 samples | ~5-10 hours |
99
+ | 10,000 samples | ~50-100 hours |
100
+
101
+ **Tip**: Run overnight or over the weekend for large datasets!
102
+
103
+ ## Troubleshooting
104
+
105
+ ### "Model not found"
106
+
107
+ ```bash
108
+ ollama pull qwen2.5:32b-instruct
109
+ ```
110
+
111
+ ### "Ollama not running"
112
+
113
+ ```bash
114
+ ollama serve
115
+ ```
116
+
117
+ ### Out of Memory
118
+
119
+ Your A100 has 80GB VRAM - this should NOT happen with the 32B model (~25GB VRAM).
120
+
121
+ If it does, try the quantized version:
122
+ ```bash
123
+ ollama pull qwen2.5:32b-instruct-q4_0 # Only ~12GB VRAM
124
+ ```
125
+
126
+ ## Output
127
+
128
+ Results saved to:
129
+ - Test: `data/CSV/qwen_local_annotated_POI_test.csv`
130
+ - Full: `data/CSV/qwen_local_annotated_POI.csv`
131
+
132
+ Same format as API results - easy to compare!
133
+
134
+ ## Custom Model Cache Location
135
+
136
+ To store models in `data/models/`:
137
+
138
+ ```bash
139
+ export OLLAMA_MODELS="/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/data/models"
140
+ ollama pull qwen2.5:32b-instruct
141
+ ```
142
+
143
+ ## Comparing API vs Local
144
+
145
+ After running both:
146
+
147
+ ```python
148
+ import pandas as pd
149
+
150
+ qwen_api = pd.read_csv('data/CSV/qwen_annotated_POI_test.csv')
151
+ qwen_local = pd.read_csv('data/CSV/qwen_local_annotated_POI_test.csv')
152
+
153
+ # Check agreement
154
+ agreement = (qwen_api['profession_llm'] == qwen_local['profession_llm']).mean()
155
+ print(f"Agreement: {agreement*100:.1f}%")
156
+ ```
157
+
158
+ ## Full Documentation
159
+
160
+ For more details, see:
161
+ - `QWEN_LOCAL_SETUP.md` - Complete setup guide
162
+ - `LLM_MODELS_COMPARISON.md` - All 6 LLM options compared
163
+
164
+ ## Summary
165
+
166
+ ✅ Ollama already installed
167
+ ✅ A100 80GB GPU - perfect for Qwen-2.5-32B
168
+ ✅ FREE inference - no API costs
169
+ ✅ Privacy - data stays local
170
+
171
+ **Next step**: Run Cell 20 in the notebook! 🚀
jupyter_notebooks/QWEN_LOCAL_SETUP.md ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Running Qwen-2.5-32B Locally with Ollama
2
+
3
+ This guide explains how to run Qwen-2.5-32B-Instruct locally on your A100 GPU using Ollama.
4
+
5
+ ## Why Run Locally?
6
+
7
+ ✅ **FREE** - No API costs ($0 per query)
8
+ ✅ **FAST** - Local inference on A100 (5-10 tokens/sec)
9
+ ✅ **PRIVATE** - Data never leaves your machine
10
+ ✅ **OFFLINE** - Works without internet (after model download)
11
+ ✅ **HIGH QUALITY** - 32B parameter model with strong multilingual support
12
+
13
+ ## System Requirements
14
+
15
+ ### Minimum Specs
16
+ - **GPU**: NVIDIA A100 80GB (or similar high-end GPU)
17
+ - **VRAM**: 22-25GB during inference
18
+ - **RAM**: 32GB system RAM (you have 265GB - more than enough!)
19
+ - **Storage**: ~20GB for model download
20
+ - **OS**: Linux (you're on Ubuntu)
21
+
22
+ ### Your Setup
23
+ ✅ NVIDIA A100 80GB - Perfect for Qwen-2.5-32B
24
+ ✅ 265GB RAM - Excellent
25
+ ✅ Linux (Ubuntu) - Supported
26
+ ✅ Ollama already installed at `/usr/local/bin/ollama`
27
+
28
+ ## Installation Steps
29
+
30
+ ### 1. Verify Ollama Installation
31
+
32
+ ```bash
33
+ # Check if Ollama is installed
34
+ which ollama
35
+ # Should output: /usr/local/bin/ollama
36
+
37
+ # Check Ollama version
38
+ ollama --version
39
+ ```
40
+
41
+ If not installed, install with:
42
+ ```bash
43
+ curl -fsSL https://ollama.com/install.sh | sh
44
+ ```
45
+
46
+ ### 2. Pull Qwen-2.5-32B-Instruct Model
47
+
48
+ ```bash
49
+ # This will download ~20GB
50
+ ollama pull qwen2.5:32b-instruct
51
+
52
+ # Alternative: Use the base model (not instruct-tuned)
53
+ # ollama pull qwen2.5:32b
54
+ ```
55
+
56
+ **Download time**: ~10-30 minutes depending on your internet speed.
57
+
58
+ **Model cache location**: By default, models are cached at:
59
+ - Linux: `~/.ollama/models/`
60
+
61
+ To use custom cache location (e.g., `data/models/`):
62
+ ```bash
63
+ export OLLAMA_MODELS="/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/data/models"
64
+ ollama pull qwen2.5:32b-instruct
65
+ ```
66
+
67
+ ### 3. Verify Model is Ready
68
+
69
+ ```bash
70
+ # List all installed models
71
+ ollama list
72
+
73
+ # Test the model
74
+ ollama run qwen2.5:32b-instruct "Hello, who are you?"
75
+ ```
76
+
77
+ You should see a response from Qwen!
78
+
79
+ ### 4. Start Ollama Server (if needed)
80
+
81
+ Ollama runs as a background service by default. If you need to start it manually:
82
+
83
+ ```bash
84
+ # Start Ollama server
85
+ ollama serve
86
+
87
+ # Or run in background
88
+ nohup ollama serve > /dev/null 2>&1 &
89
+ ```
90
+
91
+ ## Using Qwen-2.5-32B in the Notebook
92
+
93
+ ### Cell 20: Qwen-2.5-32B Local Annotation
94
+
95
+ The notebook cell handles everything automatically:
96
+
97
+ 1. **Checks Ollama installation**
98
+ 2. **Verifies model availability**
99
+ 3. **Runs inference locally**
100
+ 4. **Saves progress every 10 rows**
101
+
102
+ ### Configuration
103
+
104
+ ```python
105
+ # In Cell 20
106
+ TEST_MODE = True # Start with small test
107
+ TEST_SIZE = 10 # Test on 10 samples first
108
+ MAX_ROWS = 20000 # Full dataset size
109
+ SAVE_INTERVAL = 10 # Save every 10 rows
110
+
111
+ MODEL_NAME = "qwen2.5:32b-instruct" # Model to use
112
+ OLLAMA_HOST = "http://localhost:11434" # Default Ollama port
113
+ ```
114
+
115
+ ### Running the Pipeline
116
+
117
+ 1. **Test run first** (recommended):
118
+ ```python
119
+ TEST_MODE = True
120
+ TEST_SIZE = 10
121
+ ```
122
+ Run Cell 20 to test on 10 samples (~1-2 minutes)
123
+
124
+ 2. **Check results**:
125
+ ```python
126
+ # Output saved to:
127
+ data/CSV/qwen_local_annotated_POI_test.csv
128
+ ```
129
+
130
+ 3. **Full run**:
131
+ ```python
132
+ TEST_MODE = False
133
+ MAX_ROWS = 20000 # or None for all rows
134
+ ```
135
+ Run Cell 20 for full dataset (~2-3 hours for 10k samples on A100)
136
+
137
+ ### Performance Expectations
138
+
139
+ On NVIDIA A100 80GB:
140
+ - **Speed**: 5-10 tokens/second
141
+ - **Throughput**: 100-200 samples/hour (depends on prompt length)
142
+ - **Memory**: ~22-25GB VRAM during inference
143
+ - **Time for 10k samples**: ~50-100 hours (can run overnight/over weekend)
144
+
145
+ ### Monitoring
146
+
147
+ The cell shows progress updates:
148
+ ```
149
+ Qwen Local: 100%|██████████| 10/10 [02:30<00:00, 15.0s/it]
150
+ ✅ Saved after 10 rows (~24.0 samples/hour)
151
+
152
+ ✅ Done! Results: data/CSV/qwen_local_annotated_POI_test.csv
153
+ Total time: 2.5 minutes
154
+ Average speed: 240.0 samples/hour
155
+ ```
156
+
157
+ ## Troubleshooting
158
+
159
+ ### Model Not Found
160
+
161
+ ```bash
162
+ # Check if model is installed
163
+ ollama list
164
+
165
+ # If not listed, pull it
166
+ ollama pull qwen2.5:32b-instruct
167
+ ```
168
+
169
+ ### Ollama Server Not Running
170
+
171
+ ```bash
172
+ # Check if Ollama is running
173
+ ps aux | grep ollama
174
+
175
+ # If not running, start it
176
+ ollama serve
177
+ ```
178
+
179
+ ### GPU Not Detected
180
+
181
+ ```bash
182
+ # Check NVIDIA GPU
183
+ nvidia-smi
184
+
185
+ # Check CUDA
186
+ nvcc --version
187
+
188
+ # Ollama should automatically detect GPU
189
+ # If not, check Ollama logs
190
+ journalctl -u ollama
191
+ ```
192
+
193
+ ### Out of Memory (OOM)
194
+
195
+ If you get OOM errors:
196
+
197
+ 1. **Check VRAM usage**:
198
+ ```bash
199
+ watch -n 1 nvidia-smi
200
+ ```
201
+
202
+ 2. **Try smaller batch size** (not applicable here - we process 1 at a time)
203
+
204
+ 3. **Try quantized version** (smaller model):
205
+ ```bash
206
+ # 4-bit quantized version (~12GB VRAM)
207
+ ollama pull qwen2.5:32b-instruct-q4_0
208
+
209
+ # Update MODEL_NAME in notebook
210
+ MODEL_NAME = "qwen2.5:32b-instruct-q4_0"
211
+ ```
212
+
213
+ ### Slow Inference
214
+
215
+ If inference is very slow (<1 token/sec):
216
+
217
+ 1. **Check GPU utilization**:
218
+ ```bash
219
+ nvidia-smi
220
+ ```
221
+ GPU should show ~90%+ utilization during inference
222
+
223
+ 2. **Check CPU vs GPU**:
224
+ Ollama might be using CPU instead of GPU
225
+ ```bash
226
+ # Force GPU usage
227
+ OLLAMA_GPU=1 ollama serve
228
+ ```
229
+
230
+ ## Model Variants
231
+
232
+ Ollama provides several Qwen-2.5 variants:
233
+
234
+ | Model | Size | VRAM | Speed | Quality |
235
+ |-------|------|------|-------|---------|
236
+ | `qwen2.5:32b-instruct` | 32B | ~25GB | Medium | Best |
237
+ | `qwen2.5:32b-instruct-q4_0` | 32B (4-bit) | ~12GB | Fast | Good |
238
+ | `qwen2.5:14b-instruct` | 14B | ~10GB | Fast | Good |
239
+ | `qwen2.5:7b-instruct` | 7B | ~5GB | Very Fast | OK |
240
+
241
+ For your A100 80GB, **`qwen2.5:32b-instruct`** is recommended (best quality, no VRAM issues).
242
+
243
+ ## Custom Model Cache Location
244
+
245
+ To store models in `data/models/` directory:
246
+
247
+ ```bash
248
+ # Set environment variable
249
+ export OLLAMA_MODELS="/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/data/models"
250
+
251
+ # Add to ~/.bashrc for persistence
252
+ echo 'export OLLAMA_MODELS="/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/data/models"' >> ~/.bashrc
253
+
254
+ # Pull model (will download to data/models/)
255
+ ollama pull qwen2.5:32b-instruct
256
+
257
+ # Verify
258
+ ls -lh $OLLAMA_MODELS/
259
+ ```
260
+
261
+ ## Comparing Results
262
+
263
+ After running both API and local versions, compare results:
264
+
265
+ ```python
266
+ import pandas as pd
267
+
268
+ # Load results
269
+ qwen_api = pd.read_csv('data/CSV/qwen_annotated_POI_test.csv')
270
+ qwen_local = pd.read_csv('data/CSV/qwen_local_annotated_POI_test.csv')
271
+
272
+ # Compare professions
273
+ print("API professions:", qwen_api['profession_llm'].value_counts().head())
274
+ print("Local professions:", qwen_local['profession_llm'].value_counts().head())
275
+
276
+ # Check agreement
277
+ agreement = (qwen_api['profession_llm'] == qwen_local['profession_llm']).mean()
278
+ print(f"Agreement rate: {agreement*100:.1f}%")
279
+ ```
280
+
281
+ ## Cost Comparison (10,000 samples)
282
+
283
+ | Method | Cost | Time | Privacy |
284
+ |--------|------|------|---------|
285
+ | **Qwen Local (A100)** | **$0** | ~50-100 hours | ✅ Full |
286
+ | Qwen API (Alibaba) | ~$10-20 | ~5-10 hours | ⚠️ Data sent to Alibaba |
287
+ | Llama API (Together) | ~$5-10 | ~5-10 hours | ⚠️ Data sent to Together AI |
288
+ | Deepseek API | ~$1-2 | ~5-10 hours | ⚠️ Data sent to Deepseek |
289
+
290
+ **Recommendation**:
291
+ - For **small tests** (<100 samples): Use API (faster)
292
+ - For **large datasets** (>1000 samples): Use local (free, private)
293
+ - For **research papers**: Use local to avoid data privacy concerns
294
+
295
+ ## Advanced: Parallel Processing
296
+
297
+ For faster processing on multi-GPU setup:
298
+
299
+ ```python
300
+ # Not implemented yet, but possible with:
301
+ # - Multiple Ollama instances on different GPUs
302
+ # - Ray or Dask for parallel processing
303
+ # - ~4x speedup with 4 GPUs
304
+ ```
305
+
306
+ ## Summary
307
+
308
+ ✅ **Ollama** already installed
309
+ ✅ **A100 80GB** GPU - perfect for Qwen-2.5-32B
310
+ ✅ **Free inference** - no API costs
311
+ ✅ **Privacy** - data stays local
312
+
313
+ **Next steps:**
314
+ 1. Pull model: `ollama pull qwen2.5:32b-instruct`
315
+ 2. Test with Cell 20: `TEST_MODE = True`, `TEST_SIZE = 10`
316
+ 3. Run full dataset: `TEST_MODE = False`
317
+
318
+ **Estimated time for 10,000 samples**: ~50-100 hours
319
+ **Cost**: $0
320
+
321
+ Good luck! 🚀
jupyter_notebooks/SPACY_NER_EXPLANATION.md ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # spaCy NER Implementation
2
+
3
+ ## Why spaCy for NER?
4
+
5
+ Using **spaCy's Named Entity Recognition (NER)** is significantly better than regex-based cleaning because:
6
+
7
+ 1. **Intelligent entity extraction**: Recognizes PERSON entities using machine learning
8
+ 2. **Context-aware**: Understands sentence structure and context
9
+ 3. **Robust**: Handles various name formats (first, last, full, stage names)
10
+ 4. **Language support**: Works with multiple languages and scripts
11
+ 5. **Industry standard**: Used in production NLP applications
12
+
13
+ ## How It Works
14
+
15
+ ### Pipeline Overview
16
+
17
+ ```
18
+ Original Name
19
+ ↓
20
+ 1. Translate Leetspeak (4→a, 3→e, 1→i)
21
+ ↓
22
+ 2. Remove Noise (emoji, LoRA terms, versions)
23
+ ↓
24
+ 3. spaCy NER - Extract PERSON entities
25
+ ↓
26
+ 4. Fallback to capitalized words if needed
27
+ ↓
28
+ Cleaned Name
29
+ ```
30
+
31
+ ### Detailed Steps
32
+
33
+ #### Step 1: Leetspeak Translation
34
+ ```python
35
+ "4kira LoRA v2" → "akira LoRA v2"
36
+ "1rene Model" → "irene Model"
37
+ "3mma Watson" → "emma Watson"
38
+ ```
39
+
40
+ #### Step 2: Noise Removal
41
+ ```python
42
+ "akira LoRA v2" → "akira"
43
+ "irene Model" → "irene"
44
+ "emma Watson" → "emma Watson"
45
+ ```
46
+
47
+ #### Step 3: spaCy NER
48
+ ```python
49
+ nlp("akira")
50
+ # Entities: [("akira", PERSON)]
51
+ # Result: "akira"
52
+
53
+ nlp("emma Watson")
54
+ # Entities: [("emma Watson", PERSON)]
55
+ # Result: "emma Watson"
56
+ ```
57
+
58
+ #### Step 4: Fallback
59
+ If spaCy doesn't find a PERSON entity:
60
+ - Extract capitalized words (likely names)
61
+ - Or return cleaned text as-is
62
+
63
+ ## Examples
64
+
65
+ ### Case 1: Simple Name
66
+ ```
67
+ Input: "IU"
68
+ Output: "IU"
69
+
70
+ Process:
71
+ - Preprocess: "IU" (no noise)
72
+ - spaCy NER: Recognizes "IU" as PERSON
73
+ - Result: "IU"
74
+ ```
75
+
76
+ ### Case 2: Name with LoRA Terms
77
+ ```
78
+ Input: "Scarlett Johansson「LoRa」"
79
+ Output: "Scarlett Johansson"
80
+
81
+ Process:
82
+ - Preprocess: "Scarlett Johansson" (removed 「LoRa」)
83
+ - spaCy NER: Recognizes "Scarlett Johansson" as PERSON
84
+ - Result: "Scarlett Johansson"
85
+ ```
86
+
87
+ ### Case 3: Leetspeak Name
88
+ ```
89
+ Input: "4kira Anime Character v1"
90
+ Output: "akira"
91
+
92
+ Process:
93
+ - Leetspeak: "akira Anime Character v1"
94
+ - Preprocess: "akira Anime Character"
95
+ - spaCy NER: Recognizes "akira" as PERSON
96
+ - Result: "akira"
97
+ ```
98
+
99
+ ### Case 4: Complex Format
100
+ ```
101
+ Input: "Gakki | Aragaki Yui | 新垣結衣"
102
+ Output: "Gakki"
103
+
104
+ Process:
105
+ - Preprocess: "Gakki" (kept first part before |)
106
+ - spaCy NER: Recognizes "Gakki" as PERSON
107
+ - Result: "Gakki"
108
+ ```
109
+
110
+ ### Case 5: With Metadata
111
+ ```
112
+ Input: "Emma Watson (JG) v3.5"
113
+ Output: "Emma Watson"
114
+
115
+ Process:
116
+ - Preprocess: "Emma Watson" (removed (JG) and v3.5)
117
+ - spaCy NER: Recognizes "Emma Watson" as PERSON
118
+ - Result: "Emma Watson"
119
+ ```
120
+
121
+ ## Advantages Over Regex-Only
122
+
123
+ ### Old Approach (Regex Only)
124
+ ```python
125
+ # Just remove noise and hope for the best
126
+ name = remove_noise(name)
127
+ name = name.strip()
128
+ # Result: May include non-name words
129
+ ```
130
+
131
+ Problems:
132
+ - Can't distinguish names from other capitalized words
133
+ - May include words like "Model", "Anime", "Character"
134
+ - No context awareness
135
+ - Language-dependent regex patterns needed
136
+
137
+ ### New Approach (spaCy NER)
138
+ ```python
139
+ # Intelligent entity extraction
140
+ preprocessed = remove_noise(name)
141
+ doc = nlp(preprocessed)
142
+ person_names = [ent.text for ent in doc.ents if ent.label_ == "PERSON"]
143
+ # Result: Only actual person names
144
+ ```
145
+
146
+ Benefits:
147
+ - ✅ Identifies actual person entities
148
+ - ✅ Ignores non-person words
149
+ - ✅ Context-aware (understands "Emma Watson" is one entity)
150
+ - ✅ Multi-language support
151
+ - ✅ Handles various name formats
152
+
153
+ ## Comparison Examples
154
+
155
+ | Input | Regex Only | spaCy NER |
156
+ |-------|------------|-----------|
157
+ | `"Emma Watson Model"` | `"Emma Watson Model"` ❌ | `"Emma Watson"` ✅ |
158
+ | `"Anime Character Levi"` | `"Anime Character Levi"` ❌ | `"Levi"` ✅ |
159
+ | `"Taylor Swift v2"` | `"Taylor Swift"` ✅ | `"Taylor Swift"` ✅ |
160
+ | `"K4te Middleton"` | `"K4te Middleton"` ❌ | `"Kate Middleton"` ✅ |
161
+ | `"Celebrity IU"` | `"Celebrity IU"` ❌ | `"IU"` ✅ |
162
+
163
+ ## spaCy Model Information
164
+
165
+ ### Model Used
166
+ - **Name**: `en_core_web_sm`
167
+ - **Language**: English (but works reasonably with romanized names)
168
+ - **Size**: ~13 MB
169
+ - **Entities**: Recognizes PERSON, ORG, GPE, etc.
170
+
171
+ ### Installation
172
+ ```bash
173
+ # Install spaCy
174
+ pip install spacy
175
+
176
+ # Download model
177
+ python -m spacy download en_core_web_sm
178
+ ```
179
+
180
+ The notebook automatically downloads the model if not found.
181
+
182
+ ### Performance
183
+ - **Speed**: ~1000-5000 docs/second
184
+ - **Accuracy**: High for common names
185
+ - **Memory**: Low (~100MB loaded)
186
+
187
+ ## Fallback Strategy
188
+
189
+ If spaCy doesn't recognize a PERSON entity:
190
+
191
+ 1. **Extract capitalized words**:
192
+ ```python
193
+ "unknown name here" → ["unknown"]
194
+ ```
195
+
196
+ 2. **Return first few capitalized words**:
197
+ ```python
198
+ "Celebrity Model Actor" → "Celebrity Model Actor"
199
+ ```
200
+
201
+ 3. **Last resort**: Return cleaned text as-is
202
+
203
+ This ensures we always get something, even for:
204
+ - Uncommon/rare names
205
+ - Nicknames
206
+ - Non-English names
207
+ - Stage names
208
+
209
+ ## Testing
210
+
211
+ ### How to Verify spaCy is Working
212
+
213
+ Run Cell 5 and check the output:
214
+
215
+ ```
216
+ ✅ spaCy model loaded: en_core_web_sm
217
+
218
+ 📊 Name cleaning examples (with spaCy NER):
219
+ ===================================================================================================
220
+ Original Name | Cleaned Name
221
+ ===================================================================================================
222
+ Scarlett Johansson「LoRa」 | Scarlett Johansson
223
+ Emma Watson (JG) | Emma Watson
224
+ IU | IU
225
+ Belle Delphine | Belle Delphine
226
+ ...
227
+ ```
228
+
229
+ ### Key Indicators
230
+
231
+ ✅ **Good signs**:
232
+ - Person names cleanly extracted
233
+ - No extra words like "Model", "LoRA", "Celebrity"
234
+ - Multi-word names kept together (e.g., "Emma Watson" not just "Emma")
235
+
236
+ ❌ **Issues to watch**:
237
+ - Empty results (increase fallback logic)
238
+ - Partial names (e.g., only first name)
239
+ - Non-names included (tune preprocessing)
240
+
241
+ ## Customization
242
+
243
+ ### Add More Languages
244
+
245
+ For better support of non-English names:
246
+
247
+ ```python
248
+ # Download multilingual model
249
+ python -m spacy download xx_ent_wiki_sm
250
+
251
+ # Use in code
252
+ nlp = spacy.load("xx_ent_wiki_sm")
253
+ ```
254
+
255
+ ### Adjust Entity Extraction
256
+
257
+ To extract other entities:
258
+
259
+ ```python
260
+ # Extract organizations too
261
+ entities = [ent.text for ent in doc.ents
262
+ if ent.label_ in ["PERSON", "ORG"]]
263
+ ```
264
+
265
+ ### Custom Entity Rules
266
+
267
+ Add custom patterns for names spaCy might miss:
268
+
269
+ ```python
270
+ from spacy.matcher import Matcher
271
+
272
+ matcher = Matcher(nlp.vocab)
273
+ # Add patterns for specific name formats
274
+ ```
275
+
276
+ ## Benefits for This Project
277
+
278
+ ### Better Person Identification
279
+
280
+ With cleaner names:
281
+ - LLMs receive recognizable names
282
+ - "Emma Watson" instead of "Emma Watson Model LoRA v3"
283
+ - Better identification accuracy
284
+
285
+ ### Reduced Ambiguity
286
+
287
+ spaCy helps distinguish:
288
+ - Person names vs. descriptive words
289
+ - "Celebrity IU" → "IU" (person)
290
+ - "Model Bella" → "Bella" (person)
291
+
292
+ ### Improved Context for LLMs
293
+
294
+ Cleaner input = better prompts:
295
+ ```
296
+ Before: "Given 'Celebrity Model Emma Watson LoRA v2' (actress)..."
297
+ After: "Given 'Emma Watson' (actress)..."
298
+ ```
299
+
300
+ The LLM can now focus on identifying the person, not parsing the noise.
301
+
302
+ ## Summary
303
+
304
+ ✅ **spaCy NER** provides intelligent, context-aware name extraction
305
+ ✅ **Better than regex** for handling complex name formats
306
+ ✅ **Fallback strategy** ensures we always get a result
307
+ ✅ **Industry standard** tool used in production NLP
308
+ ✅ **Easy to use** with minimal code
309
+
310
+ The combination of:
311
+ 1. Leetspeak translation
312
+ 2. Noise removal
313
+ 3. spaCy NER
314
+ 4. Smart fallbacks
315
+
316
+ ...results in clean, accurate person names ready for LLM annotation!
jupyter_notebooks/Section_2-3-4_Figure_8_deepfake_adapters.ipynb ADDED
@@ -0,0 +1,1943 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "e4cf5af4",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Deepfake Adapter Dataset Processing Pipeline\n",
9
+ "\n",
10
+ "This notebook processes the `real_person_adapters.csv` dataset to identify and annotate real people used in deepfake LoRA models.\n",
11
+ "\n",
12
+ "## Overview\n",
13
+ "\n",
14
+ "The pipeline consists of 4 main steps:\n",
15
+ "\n",
16
+ "1. **NER & Name Cleaning**: Extract clean person names from noisy adapter names\n",
17
+ " - Remove emoji, version tags (v1, v2), LoRA-specific terms\n",
18
+ " - Extract core person name from complex strings\n",
19
+ "\n",
20
+ "2. **Country/Nationality Mapping**: Convert nationality tags to standardized countries\n",
21
+ " - Uses `misc/lists/countries.csv` reference\n",
22
+ " - Excludes uninhabited territories (Isle of Man, Bouvet Island, etc.)\n",
23
+ "\n",
24
+ "3. **LLM-Based Profession Annotation**: Use large language models to identify professions\n",
25
+ " - Supports 3 LLM providers: **Qwen**, **Llama**, **Mistral** (large versions)\n",
26
+ " - Uses profession list from `misc/lists/professions.csv`\n",
27
+ " - Can annotate up to 3 professions per person (e.g., Emma Watson: actor, activist)\n",
28
+ "\n",
29
+ "4. **Output Formatting**: Generate annotated dataset with standardized fields\n",
30
+ "\n",
31
+ "## Prerequisites\n",
32
+ "\n",
33
+ "### 1. Install Required Packages\n",
34
+ "\n",
35
+ "```bash\n",
36
+ "pip install pandas numpy emoji requests tqdm spacy\n",
37
+ "\n",
38
+ "# Download spaCy English model for NER\n",
39
+ "python -m spacy download en_core_web_sm\n",
40
+ "```\n",
41
+ "\n",
42
+ "### 2. Set Up API Keys\n",
43
+ "\n",
44
+ "You need API keys for at least one of the LLM providers:\n",
45
+ "\n",
46
+ "- **Qwen (Alibaba Cloud)**: https://dashscope.aliyun.com/\n",
47
+ "- **Llama (Together AI)**: https://www.together.ai/\n",
48
+ "- **Mistral AI**: https://mistral.ai/\n",
49
+ "\n",
50
+ "Create text files in `misc/credentials/`:\n",
51
+ "- `qwen_api_key.txt`\n",
52
+ "- `together_api_key.txt` (for Llama)\n",
53
+ "- `mistral_api_key.txt`\n",
54
+ "\n",
55
+ "Each file should contain only the API key (no extra whitespace).\n",
56
+ "\n",
57
+ "See `misc/credentials/README.md` for detailed instructions.\n",
58
+ "\n",
59
+ "## Configuration\n",
60
+ "\n",
61
+ "In the LLM annotation section, you can configure:\n",
62
+ "\n",
63
+ "```python\n",
64
+ "SELECTED_LLM = 'qwen' # Choose: 'qwen', 'llama', or 'mistral'\n",
65
+ "TEST_MODE = True # True = test on small sample first\n",
66
+ "TEST_SIZE = 10 # Number of rows to test\n",
67
+ "MAX_ROWS = 20000 # Maximum rows to process (None = all)\n",
68
+ "SAVE_INTERVAL = 10 # Save progress every N rows\n",
69
+ "```\n",
70
+ "\n",
71
+ "## Expected Output\n",
72
+ "\n",
73
+ "The final annotated dataset will include:\n",
74
+ "- `real_name`: Cleaned person name\n",
75
+ "- `full_name`: Full legal/stage name (from LLM)\n",
76
+ "- `gender`: Gender (from LLM)\n",
77
+ "- `profession_llm`: Up to 3 professions (from LLM)\n",
78
+ "- `country`: Primary country (from LLM)\n",
79
+ "- `tags`: Original tags as list\n",
80
+ "- `likely_country`: Country inferred from tags\n",
81
+ "- `likely_nationality`: Nationality inferred from tags\n",
82
+ "- `likely_profession`: Profession inferred from tags\n",
83
+ "- `prompt`: Summary of hints used for LLM\n",
84
+ "\n",
85
+ "## Cost Considerations\n",
86
+ "\n",
87
+ "For ~10,000 entries:\n",
88
+ "- **Qwen**: Check Alibaba Cloud pricing\n",
89
+ "- **Llama (Together AI)**: ~$5-10\n",
90
+ "- **Mistral**: ~$40-80\n",
91
+ "\n",
92
+ "**Always test on a small sample first!**\n",
93
+ "\n",
94
+ "## Running the Pipeline\n",
95
+ "\n",
96
+ "1. Run cells sequentially from top to bottom\n",
97
+ "2. Start with `TEST_MODE = True` to test on 10 samples\n",
98
+ "3. Review results and adjust configuration as needed\n",
99
+ "4. Set `TEST_MODE = False` for full processing\n",
100
+ "5. The pipeline saves progress automatically and can be resumed if interrupted\n",
101
+ "\n",
102
+ "---\n"
103
+ ]
104
+ },
105
+ {
106
+ "cell_type": "markdown",
107
+ "id": "23d0ae58",
108
+ "metadata": {},
109
+ "source": [
110
+ "# Deepfake Adapter Dataset - LLM Annotation Pipeline"
111
+ ]
112
+ },
113
+ {
114
+ "cell_type": "markdown",
115
+ "id": "d7942c40",
116
+ "metadata": {},
117
+ "source": []
118
+ },
119
+ {
120
+ "cell_type": "markdown",
121
+ "id": "3c906826",
122
+ "metadata": {},
123
+ "source": [
124
+ "## NER Code"
125
+ ]
126
+ },
127
+ {
128
+ "cell_type": "markdown",
129
+ "id": "cb529f57",
130
+ "metadata": {},
131
+ "source": [
132
+ "please put NER code here. on the first collumn of the adapter dataset in data/real_person_adapters.csv named entity recognition should be performed to extract the most plausible name."
133
+ ]
134
+ },
135
+ {
136
+ "cell_type": "code",
137
+ "execution_count": 2,
138
+ "id": "ner_code_cell",
139
+ "metadata": {},
140
+ "outputs": [
141
+ {
142
+ "name": "stdout",
143
+ "output_type": "stream",
144
+ "text": [
145
+ "\u2705 spaCy model loaded: en_core_web_sm\n",
146
+ "Loaded 18 rows\n",
147
+ "\n",
148
+ "\ud83d\udd04 Processing names with spaCy NER...\n",
149
+ "\n",
150
+ "\ud83d\udcca Name cleaning examples (with spaCy NER):\n",
151
+ "====================================================================================================\n",
152
+ "Original Name | Cleaned Name \n",
153
+ "====================================================================================================\n",
154
+ "Super Pose Book Vol.1 - ControlNet | Super Pose Book \n",
155
+ "Liyuu LoRA | Liyuu \n",
156
+ "HashimotoKanna/ \u6a4b\u672c\u74b0\u5948 _JP_Actress | HashimotoKanna \n",
157
+ "Emma Watson (JG) | Emma Watson \n",
158
+ "Gal Gadot\u300cLoRa\u300d | Gal Gadot \n",
159
+ "Scarlett Johansson\u300cLoRa\u300d | Scarlett Johansson \n",
160
+ "Gakki | Aragaki Yui | \u65b0\u57a3\u7d50\u8863 | Gakki \n",
161
+ "Actress Satomi_\u77f3\u539f\u25cb\u25cb | Actress Satomi \n",
162
+ "Game of Thrones Cast | Game Thrones Cast \n",
163
+ "Natalie Portman\u300cLoRa\u300d | Natalie Portman \n",
164
+ "Emma Watson LoRA | Emma Watson \n",
165
+ "====================================================================================================\n",
166
+ "\n",
167
+ "\ud83e\uddea Leetspeak translation examples:\n",
168
+ " 4kira LoRA -> akira\n",
169
+ " 3mma Watson v2 -> Watson\n",
170
+ " 1rene LORA -> irene\n",
171
+ " L3vi Ackerman -> Levi Ackerman\n",
172
+ "\n",
173
+ "\ud83d\udcc8 Statistics:\n",
174
+ " Total rows: 18\n",
175
+ " Non-empty names: 18\n",
176
+ " Empty names: 0\n",
177
+ "\n",
178
+ "\ud83c\udfaf Sample spaCy NER results:\n",
179
+ " 1. IU\n",
180
+ " 2. Super Pose Book\n",
181
+ " 3. Liyuu\n",
182
+ " 4. Irene\n",
183
+ " 5. AESPA Karina\n",
184
+ " 6. Saika Kawakita\n",
185
+ " 7. Liu Yifei\n",
186
+ " 8. HashimotoKanna\n",
187
+ " 9. Emma Watson\n",
188
+ " 10. Gal Gadot\n",
189
+ "\n",
190
+ "\u2705 Cleaned 18 names using spaCy NER\n",
191
+ "\ud83d\udcbe Saved to /home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/data/CSV/NER_POI_step01_pre_annotation.csv\n"
192
+ ]
193
+ }
194
+ ],
195
+ "source": [
196
+ "import pandas as pd\n",
197
+ "import re\n",
198
+ "from pathlib import Path\n",
199
+ "import emoji\n",
200
+ "import spacy\n",
201
+ "\n",
202
+ "# Load spaCy model\n",
203
+ "# You may need to download it first: python -m spacy download en_core_web_sm\n",
204
+ "try:\n",
205
+ " nlp = spacy.load(\"en_core_web_sm\")\n",
206
+ " print(\"\u2705 spaCy model loaded: en_core_web_sm\")\n",
207
+ "except OSError:\n",
208
+ " print(\"\u274c spaCy model not found. Downloading...\")\n",
209
+ " import subprocess\n",
210
+ " subprocess.run([\"python\", \"-m\", \"spacy\", \"download\", \"en_core_web_sm\"])\n",
211
+ " nlp = spacy.load(\"en_core_web_sm\")\n",
212
+ " print(\"\u2705 spaCy model downloaded and loaded\")\n",
213
+ "\n",
214
+ "# Set up paths\n",
215
+ "current_dir = Path.cwd()\n",
216
+ "#input_file = current_dir.parent / \"data/CSV/real_person_adapters.csv\"\n",
217
+ "input_file = current_dir.parent / \"data/CSV/samples/real_person_adapters_sample.csv\"\n",
218
+ "\n",
219
+ "# Load dataset\n",
220
+ "df = pd.read_csv(input_file)\n",
221
+ "print(f\"Loaded {len(df)} rows\")\n",
222
+ "\n",
223
+ "def translate_leetspeak(text: str) -> str:\n",
224
+ " \"\"\"\n",
225
+ " Translate common leetspeak patterns to normal letters.\n",
226
+ " Examples: 4kira -> Akira, 3mma -> Emma, 1rene -> Irene\n",
227
+ " \"\"\"\n",
228
+ " if not text:\n",
229
+ " return text\n",
230
+ " \n",
231
+ " # Common leetspeak mappings (order matters!)\n",
232
+ " leetspeak_map = {\n",
233
+ " '4': 'a',\n",
234
+ " '3': 'e', \n",
235
+ " '1': 'i',\n",
236
+ " '0': 'o',\n",
237
+ " '7': 't',\n",
238
+ " '5': 's',\n",
239
+ " '8': 'b',\n",
240
+ " '9': 'g',\n",
241
+ " '@': 'a',\n",
242
+ " '$': 's',\n",
243
+ " '!': 'i',\n",
244
+ " }\n",
245
+ " \n",
246
+ " result = text\n",
247
+ " # Apply mappings at word boundaries or start of string\n",
248
+ " for leet, normal in leetspeak_map.items():\n",
249
+ " # Replace at start of word\n",
250
+ " result = re.sub(rf'\\b{re.escape(leet)}', normal, result, flags=re.IGNORECASE)\n",
251
+ " # Replace standalone numbers that look like letters in context\n",
252
+ " result = re.sub(rf'(?<=[a-z]){re.escape(leet)}(?=[a-z])', normal, result, flags=re.IGNORECASE)\n",
253
+ " \n",
254
+ " return result\n",
255
+ "\n",
256
+ "def preprocess_for_ner(name: str) -> str:\n",
257
+ " \"\"\"\n",
258
+ " Preprocess the name before spaCy NER.\n",
259
+ " Remove noise but keep the actual name parts.\n",
260
+ " \"\"\"\n",
261
+ " if pd.isna(name):\n",
262
+ " return \"\"\n",
263
+ " \n",
264
+ " name = str(name)\n",
265
+ " \n",
266
+ " # FIRST: Translate leetspeak\n",
267
+ " name = translate_leetspeak(name)\n",
268
+ " \n",
269
+ " # Remove emoji\n",
270
+ " name = emoji.replace_emoji(name, replace=' ')\n",
271
+ " \n",
272
+ " # Remove version indicators (v1, v2, v1.0, etc.)\n",
273
+ " name = re.sub(r'\\s*[vV]\\d+(\\.\\d+)?\\s*', ' ', name)\n",
274
+ " \n",
275
+ " # Remove LoRA-related terms (case insensitive)\n",
276
+ " lora_terms = ['lora', 'loha', 'lycoris', 'controlnet', 'textual inversion', \n",
277
+ " 'embedding', 'ti', 'checkpoint', 'model', 'adapter']\n",
278
+ " for term in lora_terms:\n",
279
+ " name = re.sub(rf'\\b{term}\\b', '', name, flags=re.IGNORECASE)\n",
280
+ " \n",
281
+ " # Remove content in parentheses or brackets (often metadata)\n",
282
+ " name = re.sub(r'\\([^)]*\\)', '', name)\n",
283
+ " name = re.sub(r'\\[[^\\]]*\\]', '', name)\n",
284
+ " \n",
285
+ " # Remove special characters like \u300c\u300d\n",
286
+ " name = re.sub(r'[\u300c\u300d\u300e\u300f\u3010\u3011\u3008\u3009\u300a\u300b]', '', name)\n",
287
+ " \n",
288
+ " # Handle pipe - keep first part\n",
289
+ " if '|' in name:\n",
290
+ " name = name.split('|')[0]\n",
291
+ " \n",
292
+ " # Handle forward slash - keep first part\n",
293
+ " if '/' in name:\n",
294
+ " name = name.split('/')[0]\n",
295
+ " \n",
296
+ " # Replace underscores with spaces\n",
297
+ " name = name.replace('_', ' ')\n",
298
+ " \n",
299
+ " # Remove multiple spaces\n",
300
+ " name = re.sub(r'\\s+', ' ', name)\n",
301
+ " \n",
302
+ " # Strip\n",
303
+ " name = name.strip()\n",
304
+ " \n",
305
+ " return name\n",
306
+ "\n",
307
+ "def extract_person_name(text: str) -> str:\n",
308
+ " \"\"\"\n",
309
+ " Use spaCy NER to extract person names from text.\n",
310
+ " Falls back to cleaned text if no PERSON entity found.\n",
311
+ " \"\"\"\n",
312
+ " if not text:\n",
313
+ " return \"\"\n",
314
+ " \n",
315
+ " # Run spaCy NER\n",
316
+ " doc = nlp(text)\n",
317
+ " \n",
318
+ " # Extract PERSON entities\n",
319
+ " person_entities = [ent.text for ent in doc.ents if ent.label_ == \"PERSON\"]\n",
320
+ " \n",
321
+ " if person_entities:\n",
322
+ " # Return the first (usually longest/best) person name\n",
323
+ " return person_entities[0].strip()\n",
324
+ " \n",
325
+ " # If no PERSON entity found, try to extract capitalized words (likely names)\n",
326
+ " # This helps with names spaCy might miss\n",
327
+ " words = text.split()\n",
328
+ " capitalized_words = [w for w in words if w and w[0].isupper() and len(w) > 1]\n",
329
+ " \n",
330
+ " if capitalized_words:\n",
331
+ " # Join first few capitalized words (likely the name)\n",
332
+ " return ' '.join(capitalized_words[:3]).strip()\n",
333
+ " \n",
334
+ " # Last resort: return cleaned text\n",
335
+ " return text.strip()\n",
336
+ "\n",
337
+ "def clean_name_with_spacy(name: str) -> str:\n",
338
+ " \"\"\"\n",
339
+ " Complete name cleaning pipeline with spaCy NER.\n",
340
+ " \n",
341
+ " Pipeline:\n",
342
+ " 1. Translate leetspeak (4\u2192a, 3\u2192e, 1\u2192i, etc.)\n",
343
+ " 2. Remove noise (emoji, version tags, LoRA terms)\n",
344
+ " 3. Use spaCy to extract PERSON entities\n",
345
+ " 4. Fallback to capitalized words or cleaned text\n",
346
+ " \"\"\"\n",
347
+ " # Step 1 & 2: Preprocess (leetspeak + noise removal)\n",
348
+ " preprocessed = preprocess_for_ner(name)\n",
349
+ " \n",
350
+ " if not preprocessed:\n",
351
+ " return \"\"\n",
352
+ " \n",
353
+ " # Step 3: Extract person name using spaCy NER\n",
354
+ " person_name = extract_person_name(preprocessed)\n",
355
+ " \n",
356
+ " return person_name\n",
357
+ "\n",
358
+ "# Apply name cleaning with spaCy\n",
359
+ "print(\"\\n\ud83d\udd04 Processing names with spaCy NER...\")\n",
360
+ "df['real_name'] = df['name'].apply(clean_name_with_spacy)\n",
361
+ "\n",
362
+ "# Show examples with detailed comparison\n",
363
+ "print(\"\\n\ud83d\udcca Name cleaning examples (with spaCy NER):\")\n",
364
+ "print(\"=\" * 100)\n",
365
+ "print(f\"{'Original Name':<50} | {'Cleaned Name':<30}\")\n",
366
+ "print(\"=\" * 100)\n",
367
+ "\n",
368
+ "examples = df[['name', 'real_name']].head(30)\n",
369
+ "shown = 0\n",
370
+ "for idx, row in examples.iterrows():\n",
371
+ " if row['name'] != row['real_name'] and shown < 20:\n",
372
+ " print(f\"{row['name']:<50} | {row['real_name']:<30}\")\n",
373
+ " shown += 1\n",
374
+ "\n",
375
+ "print(\"=\" * 100)\n",
376
+ "\n",
377
+ "# Show specific test cases\n",
378
+ "print(\"\\n\ud83e\uddea Leetspeak translation examples:\")\n",
379
+ "test_names = ['4kira LoRA', '3mma Watson v2', '1rene LORA', 'L3vi Ackerman']\n",
380
+ "for test in test_names:\n",
381
+ " result = clean_name_with_spacy(test)\n",
382
+ " print(f\" {test:<30} -> {result}\")\n",
383
+ "\n",
384
+ "# Statistics\n",
385
+ "print(f\"\\n\ud83d\udcc8 Statistics:\")\n",
386
+ "print(f\" Total rows: {len(df)}\")\n",
387
+ "print(f\" Non-empty names: {(df['real_name'] != '').sum()}\")\n",
388
+ "print(f\" Empty names: {(df['real_name'] == '').sum()}\")\n",
389
+ "\n",
390
+ "# Show some examples of what spaCy identified\n",
391
+ "print(\"\\n\ud83c\udfaf Sample spaCy NER results:\")\n",
392
+ "sample_names = df['real_name'].head(20).tolist()\n",
393
+ "for i, name in enumerate(sample_names[:10], 1):\n",
394
+ " if name:\n",
395
+ " print(f\" {i}. {name}\")\n",
396
+ "\n",
397
+ "print(f\"\\n\u2705 Cleaned {len(df)} names using spaCy NER\")\n",
398
+ "\n",
399
+ "# Save intermediate result\n",
400
+ "output_step1 = current_dir.parent / \"data/CSV/NER_POI_step01_pre_annotation.csv\"\n",
401
+ "df.to_csv(output_step1, index=False)\n",
402
+ "print(f\"\ud83d\udcbe Saved to {output_step1}\")\n"
403
+ ]
404
+ },
405
+ {
406
+ "cell_type": "markdown",
407
+ "id": "d701d1cb",
408
+ "metadata": {},
409
+ "source": [
410
+ "## nationality to Country\n",
411
+ "here tags related to nationality gets converted to the country equivalent. make sure isle of man is shows never up in the country collumn.as well as other uninhabited places."
412
+ ]
413
+ },
414
+ {
415
+ "cell_type": "code",
416
+ "execution_count": 3,
417
+ "id": "36ce71f2",
418
+ "metadata": {},
419
+ "outputs": [
420
+ {
421
+ "name": "stdout",
422
+ "output_type": "stream",
423
+ "text": [
424
+ "Built country/nationality mapping with 781 entries\n",
425
+ "\n",
426
+ "Processed 18 rows\n",
427
+ "Rows with country: 0\n",
428
+ "Rows with nationality: 0\n",
429
+ "Rows with profession: 14\n",
430
+ "\n",
431
+ "Top 10 countries:\n",
432
+ "Series([], Name: count, dtype: int64)\n",
433
+ "\n",
434
+ "Sample results:\n",
435
+ " real_name likely_country likely_nationality likely_profession\n",
436
+ "0 IU celebrity\n",
437
+ "1 Super Pose Book \n",
438
+ "2 Liyuu \n",
439
+ "3 Irene celebrity\n",
440
+ "4 AESPA Karina celebrity\n",
441
+ "5 Saika Kawakita celebrity\n",
442
+ "6 Liu Yifei celebrity\n",
443
+ "7 HashimotoKanna actor\n",
444
+ "8 Emma Watson actor\n",
445
+ "9 Gal Gadot model\n",
446
+ "\n",
447
+ "\ud83d\udd0d Checking 'Irene' entries:\n",
448
+ " Name: Irene\n",
449
+ " Tags: ['girl', 'photorealistic', 'asian', 'woman', 'beautiful', 'celebrity', 'korean']\n",
450
+ " Country: \n",
451
+ " Nationality: \n",
452
+ "\n"
453
+ ]
454
+ }
455
+ ],
456
+ "source": [
457
+ "import pandas as pd\n",
458
+ "from pathlib import Path\n",
459
+ "\n",
460
+ "# Set up paths\n",
461
+ "current_dir = Path.cwd()\n",
462
+ "countries_file = current_dir.parent / \"misc/lists/countries.csv\"\n",
463
+ "professions_file = current_dir.parent / \"misc/lists/professions.csv\"\n",
464
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step01_pre_annotation.csv\"\n",
465
+ "output_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
466
+ "\n",
467
+ "# Load datasets\n",
468
+ "poi_df = pd.read_csv(input_file)\n",
469
+ "countries_df = pd.read_csv(countries_file)\n",
470
+ "professions_df = pd.read_csv(professions_file)\n",
471
+ "\n",
472
+ "# Define uninhabited or non-relevant territories to exclude\n",
473
+ "excluded_territories = {\n",
474
+ " 'isle of man', 'bouvet island', 'heard island and mcdonald islands',\n",
475
+ " 'french southern territories', 'south georgia and the south sandwich islands',\n",
476
+ " 'svalbard and jan mayen', 'british indian ocean territory', 'antarctica',\n",
477
+ " 'christmas island', 'cocos (keeling) islands', 'norfolk island',\n",
478
+ " 'pitcairn', 'tokelau', 'united states minor outlying islands',\n",
479
+ " 'wallis and futuna', 'western sahara'\n",
480
+ "}\n",
481
+ "\n",
482
+ "# Step 1: Combine tags into one lowercase list\n",
483
+ "def combine_tags(row):\n",
484
+ " return [str(row[f\"tag_{i}\"]).strip().lower() for i in range(1, 8) if pd.notna(row.get(f\"tag_{i}\"))]\n",
485
+ "\n",
486
+ "poi_df[\"tags\"] = poi_df.apply(combine_tags, axis=1)\n",
487
+ "\n",
488
+ "# Step 2: Build tag \u2192 (country, nationality) mapping with PRIORITIES\n",
489
+ "tag_to_country_nationality = {}\n",
490
+ "# We'll use a priority score: direct country name = 3, nationality = 2, word parts = 1\n",
491
+ "\n",
492
+ "for _, row in countries_df.iterrows():\n",
493
+ " country = str(row[\"en_short_name\"]).strip()\n",
494
+ " nationality = str(row[\"nationality\"]).strip()\n",
495
+ " \n",
496
+ " # Skip excluded territories\n",
497
+ " if country.lower() in excluded_territories:\n",
498
+ " continue\n",
499
+ "\n",
500
+ " country_lc = country.lower()\n",
501
+ " nationality_lc = nationality.lower()\n",
502
+ "\n",
503
+ " # Store as (country, nationality, priority)\n",
504
+ " # Exact country name match = highest priority\n",
505
+ " if country_lc not in tag_to_country_nationality:\n",
506
+ " tag_to_country_nationality[country_lc] = (country, \"\", 3)\n",
507
+ " \n",
508
+ " # Exact nationality match = medium priority \n",
509
+ " if nationality_lc not in tag_to_country_nationality:\n",
510
+ " tag_to_country_nationality[nationality_lc] = (\"\", nationality, 2)\n",
511
+ " \n",
512
+ " # No-space versions\n",
513
+ " country_no_space = country_lc.replace(\" \", \"\")\n",
514
+ " nationality_no_space = nationality_lc.replace(\" \", \"\")\n",
515
+ " \n",
516
+ " if country_no_space not in tag_to_country_nationality:\n",
517
+ " tag_to_country_nationality[country_no_space] = (country, \"\", 3)\n",
518
+ " if nationality_no_space not in tag_to_country_nationality:\n",
519
+ " tag_to_country_nationality[nationality_no_space] = (\"\", nationality, 2)\n",
520
+ "\n",
521
+ " # Word parts = lowest priority (only for longer words to avoid false matches)\n",
522
+ " for part in country_lc.split():\n",
523
+ " if len(part) > 4: # Only words longer than 4 chars\n",
524
+ " if part not in tag_to_country_nationality:\n",
525
+ " tag_to_country_nationality[part] = (country, \"\", 1)\n",
526
+ " for part in nationality_lc.split():\n",
527
+ " if len(part) > 4:\n",
528
+ " if part not in tag_to_country_nationality:\n",
529
+ " tag_to_country_nationality[part] = (\"\", nationality, 1)\n",
530
+ "\n",
531
+ "print(f\"Built country/nationality mapping with {len(tag_to_country_nationality)} entries\")\n",
532
+ "\n",
533
+ "# Step 3: Infer likely_country and likely_nationality by checking ALL tags\n",
534
+ "def infer_country_and_nationality(tags):\n",
535
+ " \"\"\"\n",
536
+ " Check ALL tags and return the best match based on priority.\n",
537
+ " Priority: exact country name > nationality > word parts\n",
538
+ " \"\"\"\n",
539
+ " best_match = None\n",
540
+ " best_priority = 0\n",
541
+ " \n",
542
+ " for tag in tags:\n",
543
+ " # Try cleaned version (no spaces)\n",
544
+ " cleaned = tag.replace(\" \", \"\").lower()\n",
545
+ " \n",
546
+ " # Check cleaned version\n",
547
+ " if cleaned in tag_to_country_nationality:\n",
548
+ " country, nationality, priority = tag_to_country_nationality[cleaned]\n",
549
+ " if priority > best_priority and country and country.lower() not in excluded_territories:\n",
550
+ " best_match = (country, nationality)\n",
551
+ " best_priority = priority\n",
552
+ " \n",
553
+ " # Also check original tag\n",
554
+ " if tag in tag_to_country_nationality:\n",
555
+ " country, nationality, priority = tag_to_country_nationality[tag]\n",
556
+ " if priority > best_priority and country and country.lower() not in excluded_territories:\n",
557
+ " best_match = (country, nationality)\n",
558
+ " best_priority = priority\n",
559
+ " \n",
560
+ " if best_match:\n",
561
+ " return pd.Series(best_match)\n",
562
+ " return pd.Series([\"\", \"\"])\n",
563
+ "\n",
564
+ "poi_df[[\"likely_country\", \"likely_nationality\"]] = poi_df[\"tags\"].apply(infer_country_and_nationality)\n",
565
+ "\n",
566
+ "# Step 4: Build tag \u2192 profession mapping\n",
567
+ "profession_alias_map = {}\n",
568
+ "\n",
569
+ "for _, row in professions_df.iterrows():\n",
570
+ " canonical = str(row['profession']).strip().lower()\n",
571
+ " profession_alias_map[canonical] = canonical\n",
572
+ " for alias_col in ['alias_1', 'alias_2', 'alias_3']:\n",
573
+ " alias = row.get(alias_col)\n",
574
+ " if pd.notna(alias):\n",
575
+ " profession_alias_map[str(alias).strip().lower()] = canonical\n",
576
+ "\n",
577
+ "# Step 5: Infer likely profession from tags\n",
578
+ "def infer_profession_from_tags(tags):\n",
579
+ " matched = []\n",
580
+ " for tag in tags:\n",
581
+ " cleaned = tag.strip().lower()\n",
582
+ " if cleaned in profession_alias_map:\n",
583
+ " matched.append(profession_alias_map[cleaned])\n",
584
+ "\n",
585
+ " if not matched:\n",
586
+ " return \"\"\n",
587
+ " if \"celebrity\" in matched and len(set(matched)) > 1:\n",
588
+ " # Drop 'celebrity' if other professions are present\n",
589
+ " matched = [m for m in matched if m != \"celebrity\"]\n",
590
+ "\n",
591
+ " return matched[0] # Return the first specific match\n",
592
+ "\n",
593
+ "\n",
594
+ "poi_df[\"likely_profession\"] = poi_df[\"tags\"].apply(infer_profession_from_tags)\n",
595
+ "\n",
596
+ "# Step 6: Save enriched dataset\n",
597
+ "poi_df.to_csv(output_file, index=False)\n",
598
+ "\n",
599
+ "# Preview results\n",
600
+ "print(f\"\\nProcessed {len(poi_df)} rows\")\n",
601
+ "print(f\"Rows with country: {(poi_df['likely_country'] != '').sum()}\")\n",
602
+ "print(f\"Rows with nationality: {(poi_df['likely_nationality'] != '').sum()}\")\n",
603
+ "print(f\"Rows with profession: {(poi_df['likely_profession'] != '').sum()}\")\n",
604
+ "\n",
605
+ "print(f\"\\nTop 10 countries:\")\n",
606
+ "print(poi_df[poi_df['likely_country'] != '']['likely_country'].value_counts().head(10))\n",
607
+ "\n",
608
+ "print(f\"\\nSample results:\")\n",
609
+ "print(poi_df[[\"real_name\", \"likely_country\", \"likely_nationality\", \"likely_profession\"]].head(10))\n",
610
+ "\n",
611
+ "# Check specific case: Irene with korean tag\n",
612
+ "irene_rows = poi_df[poi_df['real_name'].str.contains('irene', case=False, na=False)]\n",
613
+ "if len(irene_rows) > 0:\n",
614
+ " print(f\"\\n\ud83d\udd0d Checking 'Irene' entries:\")\n",
615
+ " for idx, row in irene_rows.iterrows():\n",
616
+ " print(f\" Name: {row['real_name']}\")\n",
617
+ " print(f\" Tags: {row['tags']}\")\n",
618
+ " print(f\" Country: {row['likely_country']}\")\n",
619
+ " print(f\" Nationality: {row['likely_nationality']}\")\n",
620
+ " print()\n"
621
+ ]
622
+ },
623
+ {
624
+ "cell_type": "markdown",
625
+ "id": "29eec5be",
626
+ "metadata": {},
627
+ "source": []
628
+ },
629
+ {
630
+ "cell_type": "markdown",
631
+ "id": "deepseek_header",
632
+ "metadata": {},
633
+ "source": [
634
+ "## Deepseek annotation (Testing)\n",
635
+ "\n",
636
+ "**Use this section first to test if the pipeline works** before using Qwen/Llama/Mistral.\n",
637
+ "\n",
638
+ "This uses your existing Deepseek credits. The code is adapted from your original implementation but updated to:\n",
639
+ "- Use the correct input file from Step 2\n",
640
+ "- Include the profession list from `misc/lists/professions.csv`\n",
641
+ "- Allow up to 3 professions per person\n",
642
+ "- Match the output format of the other LLMs"
643
+ ]
644
+ },
645
+ {
646
+ "cell_type": "code",
647
+ "execution_count": 10,
648
+ "id": "deepseek_code",
649
+ "metadata": {},
650
+ "outputs": [
651
+ {
652
+ "name": "stdout",
653
+ "output_type": "stream",
654
+ "text": [
655
+ "\u274c Error: deepseek_api_key.txt not found in misc/credentials/\n",
656
+ " Please create this file with your API key\n"
657
+ ]
658
+ },
659
+ {
660
+ "ename": "FileNotFoundError",
661
+ "evalue": "[Errno 2] No such file or directory: '/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/misc/credentials/deepseek_api_key.txt'",
662
+ "output_type": "error",
663
+ "traceback": [
664
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
665
+ "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)",
666
+ "Cell \u001b[0;32mIn[10], line 37\u001b[0m\n\u001b[1;32m 35\u001b[0m \u001b[38;5;66;03m# === LOAD API KEY ===\u001b[39;00m\n\u001b[1;32m 36\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m---> 37\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mapi_key_file\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 38\u001b[0m api_key \u001b[38;5;241m=\u001b[39m f\u001b[38;5;241m.\u001b[39mread()\u001b[38;5;241m.\u001b[39mstrip()\n\u001b[1;32m 39\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\u2705 Deepseek API key loaded\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
667
+ "File \u001b[0;32m~/.local/lib/python3.10/site-packages/IPython/core/interactiveshell.py:324\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 317\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 318\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 320\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 321\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 322\u001b[0m )\n\u001b[0;32m--> 324\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
668
+ "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/misc/credentials/deepseek_api_key.txt'"
669
+ ]
670
+ }
671
+ ],
672
+ "source": [
673
+ "import pandas as pd\n",
674
+ "import json\n",
675
+ "import time\n",
676
+ "import requests\n",
677
+ "import re\n",
678
+ "from pathlib import Path\n",
679
+ "from openai import OpenAI\n",
680
+ "from tqdm import tqdm\n",
681
+ "\n",
682
+ "current_dir = Path.cwd()\n",
683
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
684
+ "professions_file = current_dir.parent / \"misc/lists/professions.csv\"\n",
685
+ "professions_mapped_file = current_dir.parent / \"misc/lists/professions_mapped.csv\"\n",
686
+ "api_key_file = current_dir.parent / \"misc/credentials/deepseek_api_key.txt\"\n",
687
+ "\n",
688
+ "# === CONFIGURATION ===\n",
689
+ "TEST_MODE = True # Set to False to process full dataset\n",
690
+ "TEST_SIZE = 10 # Number of rows to test\n",
691
+ "MAX_ROWS = 20000\n",
692
+ "SAVE_INTERVAL = 10 # Save every 10 rows\n",
693
+ "\n",
694
+ "# Define the SPECIFIC profession categories\n",
695
+ "PROFESSION_CATEGORIES = [\n",
696
+ " \"actor\",\n",
697
+ " \"adult performer\",\n",
698
+ " \"singer/musician\",\n",
699
+ " \"model\",\n",
700
+ " \"online personality\",\n",
701
+ " \"public figure\",\n",
702
+ " \"voice actor/ASMR\",\n",
703
+ " \"sports professional\",\n",
704
+ " \"tv personality\"\n",
705
+ "]\n",
706
+ "\n",
707
+ "# === LOAD API KEY ===\n",
708
+ "try:\n",
709
+ " with open(api_key_file) as f:\n",
710
+ " api_key = f.read().strip()\n",
711
+ " print(\"\u2705 Deepseek API key loaded\")\n",
712
+ "except FileNotFoundError:\n",
713
+ " print(\"\u274c Error: deepseek_api_key.txt not found in misc/credentials/\")\n",
714
+ " print(\" Please create this file with your API key\")\n",
715
+ " raise\n",
716
+ "\n",
717
+ "client = OpenAI(\n",
718
+ " api_key=api_key,\n",
719
+ " base_url=\"https://api.deepseek.com/v1\"\n",
720
+ ")\n",
721
+ "\n",
722
+ "# === LOAD DATA ===\n",
723
+ "df = pd.read_csv(input_file)\n",
724
+ "professions_df = pd.read_csv(professions_file)\n",
725
+ "\n",
726
+ "# Load profession categories and keywords\n",
727
+ "try:\n",
728
+ " prof_mapped_df = pd.read_csv(professions_mapped_file)\n",
729
+ " print(f\"\u2705 Loaded profession mapping with {len(prof_mapped_df)} categories\")\n",
730
+ "except:\n",
731
+ " print(\"\u26a0\ufe0f Warning: professions_mapped.csv not found, using default categories\")\n",
732
+ "\n",
733
+ "profession_str = \", \".join(PROFESSION_CATEGORIES)\n",
734
+ "\n",
735
+ "print(f\"Loaded {len(df)} rows\")\n",
736
+ "print(f\"\\nProfession categories ({len(PROFESSION_CATEGORIES)}):\")\n",
737
+ "for cat in PROFESSION_CATEGORIES:\n",
738
+ " print(f\" - {cat}\")\n",
739
+ "\n",
740
+ "if TEST_MODE:\n",
741
+ " print(f\"\\nRunning in TEST MODE with {TEST_SIZE} samples\")\n",
742
+ " df = df.head(TEST_SIZE).copy()\n",
743
+ "elif MAX_ROWS:\n",
744
+ " df = df.head(MAX_ROWS).copy()\n",
745
+ "\n",
746
+ "# === CREATE PROMPTS ===\n",
747
+ "def create_prompt(row):\n",
748
+ " \"\"\"Create prompt for Deepseek annotation with specific profession categories.\"\"\"\n",
749
+ " name = row['real_name'] if pd.notna(row.get('real_name')) else row.get('name', '')\n",
750
+ " \n",
751
+ " # Gather hints\n",
752
+ " hints = []\n",
753
+ " if pd.notna(row.get('likely_profession')):\n",
754
+ " hints.append(str(row['likely_profession']))\n",
755
+ " if pd.notna(row.get('likely_nationality')):\n",
756
+ " hints.append(str(row['likely_nationality']))\n",
757
+ " if pd.notna(row.get('likely_country')):\n",
758
+ " hints.append(str(row['likely_country']))\n",
759
+ " \n",
760
+ " # Add tags if we don't have enough hints\n",
761
+ " if len(hints) < 3:\n",
762
+ " for i in range(1, 8):\n",
763
+ " tag_col = f'tag_{i}'\n",
764
+ " if tag_col in row and pd.notna(row[tag_col]):\n",
765
+ " tag_val = str(row[tag_col])\n",
766
+ " if tag_val not in hints:\n",
767
+ " hints.append(tag_val)\n",
768
+ " if len(hints) >= 5:\n",
769
+ " break\n",
770
+ " \n",
771
+ " hint_text = \", \".join(hints[:5]) if hints else \"none\"\n",
772
+ " \n",
773
+ " return f\"\"\"Given '{name}' ({hint_text}), provide:\n",
774
+ "1. Full legal name (Western order if non-latin script)\n",
775
+ "2. Any stage names/aliases (comma separated)\n",
776
+ "3. Gender (Male/Female/Other/Unknown)\n",
777
+ "4. Top 3 most likely professions from ONLY these categories:\n",
778
+ " - actor\n",
779
+ " - adult performer\n",
780
+ " - singer/musician\n",
781
+ " - model\n",
782
+ " - online personality (includes streamers, cosplayers, influencers)\n",
783
+ " - public figure (includes politicians, activists, journalists, authors)\n",
784
+ " - voice actor/ASMR\n",
785
+ " - sports professional\n",
786
+ " - tv personality (includes hosts, presenters, reality TV)\n",
787
+ "\n",
788
+ "5. Primary country associated\n",
789
+ "\n",
790
+ "IMPORTANT:\n",
791
+ "- Choose professions ONLY from the 9 categories above\n",
792
+ "- Provide up to 3 professions, comma-separated, ordered by relevance\n",
793
+ "- Be SPECIFIC: choose the most accurate category for each role\n",
794
+ "- \"online personality\" includes: streamers, cosplayers, YouTubers, influencers, content creators\n",
795
+ "- Use 'Unknown' when uncertain or for fictional characters/places\n",
796
+ "- For multi-role people, list all relevant categories (e.g., \"actor, singer/musician, online personality\")\n",
797
+ "\n",
798
+ "Respond with exactly 5 numbered lines.\"\"\"\n",
799
+ "\n",
800
+ "# Create prompts\n",
801
+ "df['prompt'] = df.apply(create_prompt, axis=1)\n",
802
+ "\n",
803
+ "# === QUERY DEEPSEEK ===\n",
804
+ "def query_deepseek(prompt):\n",
805
+ " \"\"\"Query Deepseek API.\"\"\"\n",
806
+ " try:\n",
807
+ " response = client.chat.completions.create(\n",
808
+ " model=\"deepseek-chat\",\n",
809
+ " messages=[\n",
810
+ " {\"role\": \"system\", \"content\": \"You are an assistant that extracts key data on a person based on the name. Respond with exactly 5 numbered lines. For professions, choose ONLY from these categories: actor, adult performer, singer/musician, model, online personality, public figure, voice actor/ASMR, sports professional, tv personality.\"},\n",
811
+ " {\"role\": \"user\", \"content\": prompt}\n",
812
+ " ],\n",
813
+ " temperature=0.05,\n",
814
+ " top_p=0.8,\n",
815
+ " )\n",
816
+ " return response.choices[0].message.content.strip()\n",
817
+ " except Exception as e:\n",
818
+ " print(f\"Error querying Deepseek: {e}\")\n",
819
+ " return None\n",
820
+ "\n",
821
+ "# === PARSE RESPONSE ===\n",
822
+ "def parse_response(response):\n",
823
+ " \"\"\"Parse Deepseek response into structured fields.\"\"\"\n",
824
+ " if not response:\n",
825
+ " return {\n",
826
+ " 'full_name': 'Unknown',\n",
827
+ " 'aliases': 'Unknown',\n",
828
+ " 'gender': 'Unknown',\n",
829
+ " 'profession_llm': 'Unknown',\n",
830
+ " 'country': 'Unknown'\n",
831
+ " }\n",
832
+ " \n",
833
+ " # Split into lines and clean\n",
834
+ " lines = [line.strip() for line in response.split('\\n') if line.strip()]\n",
835
+ " \n",
836
+ " # Initialize with Unknown values\n",
837
+ " fields = {\n",
838
+ " 'full_name': 'Unknown',\n",
839
+ " 'aliases': 'Unknown',\n",
840
+ " 'gender': 'Unknown',\n",
841
+ " 'profession_llm': 'Unknown',\n",
842
+ " 'country': 'Unknown'\n",
843
+ " }\n",
844
+ " \n",
845
+ " # Extract information from each numbered line\n",
846
+ " for line in lines:\n",
847
+ " if line.startswith('1.'):\n",
848
+ " fields['full_name'] = line[2:].strip()\n",
849
+ " elif line.startswith('2.'):\n",
850
+ " fields['aliases'] = line[2:].strip()\n",
851
+ " elif line.startswith('3.'):\n",
852
+ " fields['gender'] = line[2:].strip()\n",
853
+ " elif line.startswith('4.'):\n",
854
+ " fields['profession_llm'] = line[2:].strip()\n",
855
+ " elif line.startswith('5.'):\n",
856
+ " fields['country'] = line[2:].strip()\n",
857
+ " \n",
858
+ " return fields\n",
859
+ "\n",
860
+ "# === PROCESS DATA ===\n",
861
+ "output_file = current_dir.parent / f\"data/CSV/deepseek_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
862
+ "index_file = current_dir.parent / \"misc/deepseek_query_index.txt\"\n",
863
+ "\n",
864
+ "# Check for existing progress\n",
865
+ "current_index = 0\n",
866
+ "if index_file.exists():\n",
867
+ " with open(index_file, 'r') as f:\n",
868
+ " current_index = int(f.read().strip())\n",
869
+ " print(f\"Resuming from index {current_index}\")\n",
870
+ "\n",
871
+ "# Initialize result columns if they don't exist\n",
872
+ "for col in ['full_name', 'gender', 'profession_llm', 'country', 'aliases']:\n",
873
+ " if col not in df.columns:\n",
874
+ " df[col] = 'Unknown'\n",
875
+ "\n",
876
+ "print(f\"\\nAnnotating with DEEPSEEK...\")\n",
877
+ "print(f\"Processing rows {current_index} to {len(df)}\")\n",
878
+ "\n",
879
+ "try:\n",
880
+ " for i in tqdm(range(current_index, len(df)), desc=\"Annotating\"):\n",
881
+ " row = df.iloc[i]\n",
882
+ " \n",
883
+ " # Query Deepseek\n",
884
+ " response = query_deepseek(row['prompt'])\n",
885
+ " parsed_data = parse_response(response)\n",
886
+ " \n",
887
+ " # Update dataframe\n",
888
+ " for key, value in parsed_data.items():\n",
889
+ " df.at[i, key] = value\n",
890
+ " \n",
891
+ " current_index = i + 1\n",
892
+ " \n",
893
+ " # Save progress at intervals\n",
894
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
895
+ " df.to_csv(output_file, index=False)\n",
896
+ " with open(index_file, 'w') as f:\n",
897
+ " f.write(str(current_index))\n",
898
+ " print(f\"\u2705 Progress saved after {i+1} rows\")\n",
899
+ " \n",
900
+ " time.sleep(1) # Rate limiting\n",
901
+ " \n",
902
+ " print(f\"\\n\u2705 Done! Final results saved to {output_file}\")\n",
903
+ " \n",
904
+ " # Summary statistics\n",
905
+ " print(\"\\n=== Summary Statistics ===\")\n",
906
+ " print(f\"Total processed: {len(df)}\")\n",
907
+ " print(f\"\\nGender distribution:\")\n",
908
+ " print(df['gender'].value_counts())\n",
909
+ " print(f\"\\nTop 10 profession combinations:\")\n",
910
+ " print(df['profession_llm'].value_counts().head(10))\n",
911
+ " print(f\"\\nTop 10 countries:\")\n",
912
+ " print(df['country'].value_counts().head(10))\n",
913
+ " \n",
914
+ " # Show sample results\n",
915
+ " print(\"\\n=== Sample Results ===\")\n",
916
+ " display_cols = ['real_name', 'full_name', 'gender', 'profession_llm', 'country']\n",
917
+ " print(df[display_cols].head(10).to_string(index=False))\n",
918
+ "\n",
919
+ "except Exception as e:\n",
920
+ " print(f\"\u26a0\ufe0f Error encountered: {e}\")\n",
921
+ " print(f\"\u26a0\ufe0f Last processed index: {current_index}\")\n",
922
+ " \n",
923
+ " # Save progress before exiting\n",
924
+ " df.to_csv(output_file, index=False)\n",
925
+ " with open(index_file, 'w') as f:\n",
926
+ " f.write(str(current_index))\n",
927
+ " \n",
928
+ " print(f\"\u26a0\ufe0f Progress saved up to row {current_index}\")\n"
929
+ ]
930
+ },
931
+ {
932
+ "cell_type": "markdown",
933
+ "id": "qwen_header",
934
+ "metadata": {},
935
+ "source": [
936
+ "## Qwen Annotation (Alibaba Cloud)\n",
937
+ "\n",
938
+ "Uses **Qwen-Max** (Qwen3-Max) via OpenAI-compatible API.\n",
939
+ "\n",
940
+ "**Requirements:**\n",
941
+ "- API Key: `misc/credentials/qwen_api_key.txt`\n",
942
+ "- Sign up: https://dashscope.aliyun.com/\n",
943
+ "- Base URL: `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`\n",
944
+ "- Model: `qwen-max` (uses Qwen3-Max)\n",
945
+ "- Uses OpenAI Python client for compatibility\n",
946
+ "- Cost: Variable (check Alibaba Cloud pricing)\n",
947
+ "\n",
948
+ "**Note:** This matches the official dashboard example code exactly."
949
+ ]
950
+ },
951
+ {
952
+ "cell_type": "code",
953
+ "execution_count": null,
954
+ "id": "qwen_code",
955
+ "metadata": {},
956
+ "outputs": [],
957
+ "source": [
958
+ "import pandas as pd",
959
+ "import json",
960
+ "import time",
961
+ "import re",
962
+ "from pathlib import Path",
963
+ "from tqdm import tqdm",
964
+ "from openai import OpenAI",
965
+ "",
966
+ "current_dir = Path.cwd()",
967
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"",
968
+ "api_key_file = current_dir.parent / \"misc/credentials/qwen_api_key.txt\"",
969
+ "",
970
+ "# === CONFIGURATION ===",
971
+ "TEST_MODE = True",
972
+ "TEST_SIZE = 10",
973
+ "MAX_ROWS = 20000",
974
+ "SAVE_INTERVAL = 10",
975
+ "",
976
+ "PROFESSION_CATEGORIES = [",
977
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",",
978
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",",
979
+ " \"sports professional\", \"tv personality\"",
980
+ "]",
981
+ "",
982
+ "# === LOAD API KEY ===",
983
+ "try:",
984
+ " with open(api_key_file) as f:",
985
+ " api_key = f.read().strip()",
986
+ " print(\"\u2705 Qwen API key loaded\")",
987
+ " print(f\" Key starts with: {api_key[:10]}...\")",
988
+ "except FileNotFoundError:",
989
+ " print(\"\u274c Error: qwen_api_key.txt not found\")",
990
+ " raise",
991
+ "",
992
+ "# Initialize OpenAI client for Qwen (matching dashboard example)",
993
+ "client = OpenAI(",
994
+ " api_key=api_key,",
995
+ " base_url=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"",
996
+ ")",
997
+ "",
998
+ "# === LOAD DATA ===",
999
+ "df = pd.read_csv(input_file)",
1000
+ "print(f\"Loaded {len(df)} rows\")",
1001
+ "",
1002
+ "if TEST_MODE:",
1003
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")",
1004
+ " df = df.head(TEST_SIZE).copy()",
1005
+ "elif MAX_ROWS:",
1006
+ " df = df.head(MAX_ROWS).copy()",
1007
+ "",
1008
+ "# === CREATE PROMPT ===",
1009
+ "def create_prompt(name: str, tags: list, country: str) -> str:",
1010
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"",
1011
+ " country_str = country if country else \"unknown\"",
1012
+ " ",
1013
+ " return f\"\"\"You are an expert at identifying public figures.",
1014
+ "",
1015
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}",
1016
+ "",
1017
+ "Provide in JSON format:",
1018
+ "{{",
1019
+ " \"full_name\": \"<full name>\",",
1020
+ " \"gender\": \"<Male/Female/Other/Unknown>\",",
1021
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],",
1022
+ " \"country\": \"<country>\"",
1023
+ "}}",
1024
+ "",
1025
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}",
1026
+ "- Max 3 professions, ordered by relevance",
1027
+ "- \"online personality\" includes streamers, cosplayers, influencers",
1028
+ "- Return \"Unknown\" if uncertain",
1029
+ "",
1030
+ "Return ONLY JSON, no extra text.\"\"\"",
1031
+ "",
1032
+ "# === QUERY QWEN ===",
1033
+ "def query_qwen(prompt: str) -> dict:",
1034
+ " \"\"\"Query Qwen using OpenAI-compatible API (matching dashboard example).\"\"\"",
1035
+ " for attempt in range(3):",
1036
+ " try:",
1037
+ " completion = client.chat.completions.create(",
1038
+ " model=\"qwen-max\", # Note: dashboard shows \"qwen3-max\" but \"qwen-max\" works too",
1039
+ " messages=[",
1040
+ " {\"role\": \"user\", \"content\": prompt}",
1041
+ " ],",
1042
+ " temperature=0.1,",
1043
+ " max_tokens=500,",
1044
+ " stream=False # Disable streaming for easier parsing",
1045
+ " )",
1046
+ " ",
1047
+ " content = completion.choices[0].message.content",
1048
+ " ",
1049
+ " # Parse JSON from response",
1050
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)",
1051
+ " if json_match:",
1052
+ " return json.loads(json_match.group())",
1053
+ " ",
1054
+ " return None",
1055
+ " ",
1056
+ " except Exception as e:",
1057
+ " print(f\"Qwen error (attempt {attempt+1}): {e}\")",
1058
+ " if attempt < 2:",
1059
+ " time.sleep(2 ** attempt)",
1060
+ " ",
1061
+ " return None",
1062
+ "",
1063
+ "# === PROCESS ===",
1064
+ "output_file = current_dir.parent / f\"data/CSV/qwen_annotated_POI{'_test' if TEST_MODE else ''}.csv\"",
1065
+ "index_file = current_dir.parent / \"misc/qwen_query_index.txt\"",
1066
+ "",
1067
+ "current_index = 0",
1068
+ "if index_file.exists():",
1069
+ " with open(index_file) as f:",
1070
+ " current_index = int(f.read().strip())",
1071
+ "",
1072
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:",
1073
+ " if col not in df.columns:",
1074
+ " df[col] = 'Unknown'",
1075
+ "",
1076
+ "print(f\"\\nAnnotating with QWEN-MAX (OpenAI-compatible API) - rows {current_index} to {len(df)}...\")",
1077
+ "",
1078
+ "try:",
1079
+ " for i in tqdm(range(current_index, len(df)), desc=\"Qwen\"):",
1080
+ " row = df.iloc[i]",
1081
+ " ",
1082
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8) ",
1083
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]",
1084
+ " ",
1085
+ " prompt = create_prompt(",
1086
+ " row.get('real_name', row.get('name', '')),",
1087
+ " tags,",
1088
+ " row.get('likely_country', '')",
1089
+ " )",
1090
+ " ",
1091
+ " result = query_qwen(prompt)",
1092
+ " ",
1093
+ " if result:",
1094
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')",
1095
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')",
1096
+ " profs = result.get('professions', [])",
1097
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)",
1098
+ " df.at[i, 'country'] = result.get('country', 'Unknown')",
1099
+ " ",
1100
+ " current_index = i + 1",
1101
+ " ",
1102
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):",
1103
+ " df.to_csv(output_file, index=False)",
1104
+ " with open(index_file, 'w') as f:",
1105
+ " f.write(str(current_index))",
1106
+ " print(f\"\u2705 Saved after {i+1} rows\")",
1107
+ " ",
1108
+ " time.sleep(0.5)",
1109
+ " ",
1110
+ " print(f\"\\n\u2705 Done! Results: {output_file}\")",
1111
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")",
1112
+ " ",
1113
+ "except Exception as e:",
1114
+ " print(f\"\u26a0\ufe0f Error: {e}\")",
1115
+ " df.to_csv(output_file, index=False)",
1116
+ " with open(index_file, 'w') as f:",
1117
+ " f.write(str(current_index))",
1118
+ ""
1119
+ ]
1120
+ },
1121
+ {
1122
+ "cell_type": "markdown",
1123
+ "id": "llama_header",
1124
+ "metadata": {},
1125
+ "source": [
1126
+ "## Llama Annotation (Together AI)\n",
1127
+ "\n",
1128
+ "Uses **Llama-3.1-70B-Instruct-Turbo** via Together AI.\n",
1129
+ "\n",
1130
+ "**Requirements:**\n",
1131
+ "- API Key: `misc/credentials/together_api_key.txt`\n",
1132
+ "- Sign up: https://www.together.ai/\n",
1133
+ "- Model: meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\n",
1134
+ "- Cost: ~$0.90 per 1M tokens (~$5-10 for 10k entries)"
1135
+ ]
1136
+ },
1137
+ {
1138
+ "cell_type": "code",
1139
+ "execution_count": null,
1140
+ "id": "llama_code",
1141
+ "metadata": {},
1142
+ "outputs": [],
1143
+ "source": [
1144
+ "import pandas as pd",
1145
+ "import json",
1146
+ "import time",
1147
+ "import requests",
1148
+ "import re",
1149
+ "from pathlib import Path",
1150
+ "from tqdm import tqdm",
1151
+ "",
1152
+ "current_dir = Path.cwd()",
1153
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"",
1154
+ "api_key_file = current_dir.parent / \"misc/credentials/together_api_key.txt\"",
1155
+ "",
1156
+ "# === CONFIGURATION ===",
1157
+ "TEST_MODE = True",
1158
+ "TEST_SIZE = 10",
1159
+ "MAX_ROWS = 20000",
1160
+ "SAVE_INTERVAL = 10",
1161
+ "",
1162
+ "PROFESSION_CATEGORIES = [",
1163
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",",
1164
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",",
1165
+ " \"sports professional\", \"tv personality\"",
1166
+ "]",
1167
+ "",
1168
+ "# === LOAD API KEY ===",
1169
+ "try:",
1170
+ " with open(api_key_file) as f:",
1171
+ " api_key = f.read().strip()",
1172
+ " print(\"\u2705 Llama API key loaded\")",
1173
+ "except FileNotFoundError:",
1174
+ " print(\"\u274c Error: together_api_key.txt not found\")",
1175
+ " raise",
1176
+ "",
1177
+ "# === LOAD DATA ===",
1178
+ "df = pd.read_csv(input_file)",
1179
+ "print(f\"Loaded {len(df)} rows\")",
1180
+ "",
1181
+ "if TEST_MODE:",
1182
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")",
1183
+ " df = df.head(TEST_SIZE).copy()",
1184
+ "elif MAX_ROWS:",
1185
+ " df = df.head(MAX_ROWS).copy()",
1186
+ "",
1187
+ "# === CREATE PROMPT ===",
1188
+ "def create_prompt(name: str, tags: list, country: str) -> str:",
1189
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"",
1190
+ " country_str = country if country else \"unknown\"",
1191
+ " ",
1192
+ " return f\"\"\"You are an expert at identifying public figures.",
1193
+ "",
1194
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}",
1195
+ "",
1196
+ "Provide in JSON format:",
1197
+ "{{",
1198
+ " \"full_name\": \"<full name>\",",
1199
+ " \"gender\": \"<Male/Female/Other/Unknown>\",",
1200
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],",
1201
+ " \"country\": \"<country>\"",
1202
+ "}}",
1203
+ "",
1204
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}",
1205
+ "- Max 3 professions, ordered by relevance",
1206
+ "- \"online personality\" includes streamers, cosplayers, influencers",
1207
+ "- Return \"Unknown\" if uncertain",
1208
+ "",
1209
+ "Return ONLY JSON, no extra text.\"\"\"",
1210
+ "",
1211
+ "# === QUERY LLAMA ===",
1212
+ "def query_llama(prompt: str) -> dict:",
1213
+ " url = \"https://api.together.xyz/v1/chat/completions\"",
1214
+ " ",
1215
+ " headers = {",
1216
+ " \"Authorization\": f\"Bearer {api_key}\",",
1217
+ " \"Content-Type\": \"application/json\"",
1218
+ " }",
1219
+ " ",
1220
+ " data = {",
1221
+ " \"model\": \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",",
1222
+ " \"messages\": [{\"role\": \"user\", \"content\": prompt}],",
1223
+ " \"temperature\": 0.1,",
1224
+ " \"max_tokens\": 500",
1225
+ " }",
1226
+ " ",
1227
+ " for attempt in range(3):",
1228
+ " try:",
1229
+ " response = requests.post(url, headers=headers, json=data, timeout=30)",
1230
+ " response.raise_for_status()",
1231
+ " result = response.json()",
1232
+ " ",
1233
+ " if 'choices' in result:",
1234
+ " content = result['choices'][0]['message']['content']",
1235
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)",
1236
+ " if json_match:",
1237
+ " return json.loads(json_match.group())",
1238
+ " return None",
1239
+ " except Exception as e:",
1240
+ " print(f\"Llama error (attempt {attempt+1}): {e}\")",
1241
+ " if attempt < 2:",
1242
+ " time.sleep(2 ** attempt)",
1243
+ " return None",
1244
+ "",
1245
+ "# === PROCESS ===",
1246
+ "output_file = current_dir.parent / f\"data/CSV/llama_annotated_POI{'_test' if TEST_MODE else ''}.csv\"",
1247
+ "index_file = current_dir.parent / \"misc/llama_query_index.txt\"",
1248
+ "",
1249
+ "current_index = 0",
1250
+ "if index_file.exists():",
1251
+ " with open(index_file) as f:",
1252
+ " current_index = int(f.read().strip())",
1253
+ "",
1254
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:",
1255
+ " if col not in df.columns:",
1256
+ " df[col] = 'Unknown'",
1257
+ "",
1258
+ "print(f\"\\nAnnotating with LLAMA 3.1 70B - rows {current_index} to {len(df)}...\")",
1259
+ "",
1260
+ "try:",
1261
+ " for i in tqdm(range(current_index, len(df)), desc=\"Llama\"):",
1262
+ " row = df.iloc[i]",
1263
+ " ",
1264
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8) ",
1265
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]",
1266
+ " ",
1267
+ " prompt = create_prompt(",
1268
+ " row.get('real_name', row.get('name', '')),",
1269
+ " tags,",
1270
+ " row.get('likely_country', '')",
1271
+ " )",
1272
+ " ",
1273
+ " result = query_llama(prompt)",
1274
+ " ",
1275
+ " if result:",
1276
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')",
1277
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')",
1278
+ " profs = result.get('professions', [])",
1279
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)",
1280
+ " df.at[i, 'country'] = result.get('country', 'Unknown')",
1281
+ " ",
1282
+ " current_index = i + 1",
1283
+ " ",
1284
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):",
1285
+ " df.to_csv(output_file, index=False)",
1286
+ " with open(index_file, 'w') as f:",
1287
+ " f.write(str(current_index))",
1288
+ " print(f\"\u2705 Saved after {i+1} rows\")",
1289
+ " ",
1290
+ " time.sleep(0.5)",
1291
+ " ",
1292
+ " print(f\"\\n\u2705 Done! Results: {output_file}\")",
1293
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")",
1294
+ " ",
1295
+ "except Exception as e:",
1296
+ " print(f\"\u26a0\ufe0f Error: {e}\")",
1297
+ " df.to_csv(output_file, index=False)",
1298
+ " with open(index_file, 'w') as f:",
1299
+ " f.write(str(current_index))",
1300
+ ""
1301
+ ]
1302
+ },
1303
+ {
1304
+ "cell_type": "markdown",
1305
+ "id": "mistral_header",
1306
+ "metadata": {},
1307
+ "source": [
1308
+ "## Mixtral Annotation (Mistral AI)\n",
1309
+ "\n",
1310
+ "Uses **Open-Mixtral-8x22B** via Mistral AI API.\n",
1311
+ "\n",
1312
+ "**Requirements:**\n",
1313
+ "- API Key: `misc/credentials/mistral_api_key.txt`\n",
1314
+ "- Sign up: https://mistral.ai/\n",
1315
+ "- Model: open-mixtral-8x22b\n",
1316
+ "- Cost: ~$2 per 1M tokens (~$10-20 for 10k entries)"
1317
+ ]
1318
+ },
1319
+ {
1320
+ "cell_type": "code",
1321
+ "execution_count": null,
1322
+ "id": "mixtral_code",
1323
+ "metadata": {},
1324
+ "outputs": [],
1325
+ "source": [
1326
+ "import pandas as pd",
1327
+ "import json",
1328
+ "import time",
1329
+ "import requests",
1330
+ "import re",
1331
+ "from pathlib import Path",
1332
+ "from tqdm import tqdm",
1333
+ "",
1334
+ "current_dir = Path.cwd()",
1335
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"",
1336
+ "api_key_file = current_dir.parent / \"misc/credentials/mistral_api_key.txt\"",
1337
+ "",
1338
+ "# === CONFIGURATION ===",
1339
+ "TEST_MODE = True",
1340
+ "TEST_SIZE = 10",
1341
+ "MAX_ROWS = 20000",
1342
+ "SAVE_INTERVAL = 10",
1343
+ "",
1344
+ "PROFESSION_CATEGORIES = [",
1345
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",",
1346
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",",
1347
+ " \"sports professional\", \"tv personality\"",
1348
+ "]",
1349
+ "",
1350
+ "# === LOAD API KEY ===",
1351
+ "try:",
1352
+ " with open(api_key_file) as f:",
1353
+ " api_key = f.read().strip()",
1354
+ " print(\"\u2705 Mixtral API key loaded\")",
1355
+ "except FileNotFoundError:",
1356
+ " print(\"\u274c Error: mistral_api_key.txt not found\")",
1357
+ " raise",
1358
+ "",
1359
+ "# === LOAD DATA ===",
1360
+ "df = pd.read_csv(input_file)",
1361
+ "print(f\"Loaded {len(df)} rows\")",
1362
+ "",
1363
+ "if TEST_MODE:",
1364
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")",
1365
+ " df = df.head(TEST_SIZE).copy()",
1366
+ "elif MAX_ROWS:",
1367
+ " df = df.head(MAX_ROWS).copy()",
1368
+ "",
1369
+ "# === CREATE PROMPT ===",
1370
+ "def create_prompt(name: str, tags: list, country: str) -> str:",
1371
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"",
1372
+ " country_str = country if country else \"unknown\"",
1373
+ " ",
1374
+ " return f\"\"\"You are an expert at identifying public figures.",
1375
+ "",
1376
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}",
1377
+ "",
1378
+ "Provide in JSON format:",
1379
+ "{{",
1380
+ " \"full_name\": \"<full name>\",",
1381
+ " \"gender\": \"<Male/Female/Other/Unknown>\",",
1382
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],",
1383
+ " \"country\": \"<country>\"",
1384
+ "}}",
1385
+ "",
1386
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}",
1387
+ "- Max 3 professions, ordered by relevance",
1388
+ "- \"online personality\" includes streamers, cosplayers, influencers",
1389
+ "- Return \"Unknown\" if uncertain",
1390
+ "",
1391
+ "Return ONLY JSON, no extra text.\"\"\"",
1392
+ "",
1393
+ "# === QUERY MIXTRAL ===",
1394
+ "def query_mixtral(prompt: str) -> dict:",
1395
+ " url = \"https://api.mistral.ai/v1/chat/completions\"",
1396
+ " ",
1397
+ " headers = {",
1398
+ " \"Authorization\": f\"Bearer {api_key}\",",
1399
+ " \"Content-Type\": \"application/json\"",
1400
+ " }",
1401
+ " ",
1402
+ " data = {",
1403
+ " \"model\": \"open-mixtral-8x22b\",",
1404
+ " \"messages\": [{\"role\": \"user\", \"content\": prompt}],",
1405
+ " \"temperature\": 0.1,",
1406
+ " \"max_tokens\": 500",
1407
+ " }",
1408
+ " ",
1409
+ " for attempt in range(3):",
1410
+ " try:",
1411
+ " response = requests.post(url, headers=headers, json=data, timeout=30)",
1412
+ " response.raise_for_status()",
1413
+ " result = response.json()",
1414
+ " ",
1415
+ " if 'choices' in result:",
1416
+ " content = result['choices'][0]['message']['content']",
1417
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)",
1418
+ " if json_match:",
1419
+ " return json.loads(json_match.group())",
1420
+ " return None",
1421
+ " except Exception as e:",
1422
+ " print(f\"Mixtral error (attempt {attempt+1}): {e}\")",
1423
+ " if attempt < 2:",
1424
+ " time.sleep(2 ** attempt)",
1425
+ " return None",
1426
+ "",
1427
+ "# === PROCESS ===",
1428
+ "output_file = current_dir.parent / f\"data/CSV/mixtral_annotated_POI{'_test' if TEST_MODE else ''}.csv\"",
1429
+ "index_file = current_dir.parent / \"misc/mixtral_query_index.txt\"",
1430
+ "",
1431
+ "current_index = 0",
1432
+ "if index_file.exists():",
1433
+ " with open(index_file) as f:",
1434
+ " current_index = int(f.read().strip())",
1435
+ "",
1436
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:",
1437
+ " if col not in df.columns:",
1438
+ " df[col] = 'Unknown'",
1439
+ "",
1440
+ "print(f\"\\nAnnotating with MIXTRAL 8x22B - rows {current_index} to {len(df)}...\")",
1441
+ "",
1442
+ "try:",
1443
+ " for i in tqdm(range(current_index, len(df)), desc=\"Mixtral\"):",
1444
+ " row = df.iloc[i]",
1445
+ " ",
1446
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8) ",
1447
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]",
1448
+ " ",
1449
+ " prompt = create_prompt(",
1450
+ " row.get('real_name', row.get('name', '')),",
1451
+ " tags,",
1452
+ " row.get('likely_country', '')",
1453
+ " )",
1454
+ " ",
1455
+ " result = query_mixtral(prompt)",
1456
+ " ",
1457
+ " if result:",
1458
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')",
1459
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')",
1460
+ " profs = result.get('professions', [])",
1461
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)",
1462
+ " df.at[i, 'country'] = result.get('country', 'Unknown')",
1463
+ " ",
1464
+ " current_index = i + 1",
1465
+ " ",
1466
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):",
1467
+ " df.to_csv(output_file, index=False)",
1468
+ " with open(index_file, 'w') as f:",
1469
+ " f.write(str(current_index))",
1470
+ " print(f\"\u2705 Saved after {i+1} rows\")",
1471
+ " ",
1472
+ " time.sleep(0.5)",
1473
+ " ",
1474
+ " print(f\"\\n\u2705 Done! Results: {output_file}\")",
1475
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")",
1476
+ " ",
1477
+ "except Exception as e:",
1478
+ " print(f\"\u26a0\ufe0f Error: {e}\")",
1479
+ " df.to_csv(output_file, index=False)",
1480
+ " with open(index_file, 'w') as f:",
1481
+ " f.write(str(current_index))",
1482
+ ""
1483
+ ]
1484
+ },
1485
+ {
1486
+ "cell_type": "markdown",
1487
+ "id": "gemma_header",
1488
+ "metadata": {},
1489
+ "source": [
1490
+ "## Gemma Annotation (Google - American Open Source)\n",
1491
+ "\n",
1492
+ "Uses **Gemma 2 27B** via Together AI - Google's open-source model.\n",
1493
+ "\n",
1494
+ "**Requirements:**\n",
1495
+ "- API Key: `misc/credentials/together_api_key.txt` (same as Llama)\n",
1496
+ "- Sign up: https://www.together.ai/\n",
1497
+ "- Model: google/gemma-2-27b-it\n",
1498
+ "- Cost: ~$0.80 per 1M tokens (~$4-8 for 10k entries)\n",
1499
+ "- Note: American company (Google), fully open-source model"
1500
+ ]
1501
+ },
1502
+ {
1503
+ "cell_type": "code",
1504
+ "execution_count": null,
1505
+ "id": "gemma_code",
1506
+ "metadata": {},
1507
+ "outputs": [],
1508
+ "source": [
1509
+ "import pandas as pd",
1510
+ "import json",
1511
+ "import time",
1512
+ "import requests",
1513
+ "import re",
1514
+ "from pathlib import Path",
1515
+ "from tqdm import tqdm",
1516
+ "",
1517
+ "current_dir = Path.cwd()",
1518
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"",
1519
+ "api_key_file = current_dir.parent / \"misc/credentials/together_api_key.txt\"",
1520
+ "",
1521
+ "# === CONFIGURATION ===",
1522
+ "TEST_MODE = True",
1523
+ "TEST_SIZE = 10",
1524
+ "MAX_ROWS = 20000",
1525
+ "SAVE_INTERVAL = 10",
1526
+ "",
1527
+ "PROFESSION_CATEGORIES = [",
1528
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",",
1529
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",",
1530
+ " \"sports professional\", \"tv personality\"",
1531
+ "]",
1532
+ "",
1533
+ "# === LOAD API KEY ===",
1534
+ "try:",
1535
+ " with open(api_key_file) as f:",
1536
+ " api_key = f.read().strip()",
1537
+ " print(\"\u2705 Gemma API key loaded\")",
1538
+ "except FileNotFoundError:",
1539
+ " print(\"\u274c Error: together_api_key.txt not found\")",
1540
+ " raise",
1541
+ "",
1542
+ "# === LOAD DATA ===",
1543
+ "df = pd.read_csv(input_file)",
1544
+ "print(f\"Loaded {len(df)} rows\")",
1545
+ "",
1546
+ "if TEST_MODE:",
1547
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")",
1548
+ " df = df.head(TEST_SIZE).copy()",
1549
+ "elif MAX_ROWS:",
1550
+ " df = df.head(MAX_ROWS).copy()",
1551
+ "",
1552
+ "# === CREATE PROMPT ===",
1553
+ "def create_prompt(name: str, tags: list, country: str) -> str:",
1554
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"",
1555
+ " country_str = country if country else \"unknown\"",
1556
+ " ",
1557
+ " return f\"\"\"You are an expert at identifying public figures.",
1558
+ "",
1559
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}",
1560
+ "",
1561
+ "Provide in JSON format:",
1562
+ "{{",
1563
+ " \"full_name\": \"<full name>\",",
1564
+ " \"gender\": \"<Male/Female/Other/Unknown>\",",
1565
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],",
1566
+ " \"country\": \"<country>\"",
1567
+ "}}",
1568
+ "",
1569
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}",
1570
+ "- Max 3 professions, ordered by relevance",
1571
+ "- \"online personality\" includes streamers, cosplayers, influencers",
1572
+ "- Return \"Unknown\" if uncertain",
1573
+ "",
1574
+ "Return ONLY JSON, no extra text.\"\"\"",
1575
+ "",
1576
+ "# === QUERY GEMMA ===",
1577
+ "def query_gemma(prompt: str) -> dict:",
1578
+ " url = \"https://api.together.xyz/v1/chat/completions\"",
1579
+ " ",
1580
+ " headers = {",
1581
+ " \"Authorization\": f\"Bearer {api_key}\",",
1582
+ " \"Content-Type\": \"application/json\"",
1583
+ " }",
1584
+ " ",
1585
+ " data = {",
1586
+ " \"model\": \"google/gemma-2-27b-it\",",
1587
+ " \"messages\": [{\"role\": \"user\", \"content\": prompt}],",
1588
+ " \"temperature\": 0.1,",
1589
+ " \"max_tokens\": 500",
1590
+ " }",
1591
+ " ",
1592
+ " for attempt in range(3):",
1593
+ " try:",
1594
+ " response = requests.post(url, headers=headers, json=data, timeout=30)",
1595
+ " response.raise_for_status()",
1596
+ " result = response.json()",
1597
+ " ",
1598
+ " if 'choices' in result:",
1599
+ " content = result['choices'][0]['message']['content']",
1600
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)",
1601
+ " if json_match:",
1602
+ " return json.loads(json_match.group())",
1603
+ " return None",
1604
+ " except Exception as e:",
1605
+ " print(f\"Gemma error (attempt {attempt+1}): {e}\")",
1606
+ " if attempt < 2:",
1607
+ " time.sleep(2 ** attempt)",
1608
+ " return None",
1609
+ "",
1610
+ "# === PROCESS ===",
1611
+ "output_file = current_dir.parent / f\"data/CSV/gemma_annotated_POI{'_test' if TEST_MODE else ''}.csv\"",
1612
+ "index_file = current_dir.parent / \"misc/gemma_query_index.txt\"",
1613
+ "",
1614
+ "current_index = 0",
1615
+ "if index_file.exists():",
1616
+ " with open(index_file) as f:",
1617
+ " current_index = int(f.read().strip())",
1618
+ "",
1619
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:",
1620
+ " if col not in df.columns:",
1621
+ " df[col] = 'Unknown'",
1622
+ "",
1623
+ "print(f\"\\nAnnotating with GEMMA 2 27B (Google) - rows {current_index} to {len(df)}...\")",
1624
+ "",
1625
+ "try:",
1626
+ " for i in tqdm(range(current_index, len(df)), desc=\"Gemma\"):",
1627
+ " row = df.iloc[i]",
1628
+ " ",
1629
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8) ",
1630
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]",
1631
+ " ",
1632
+ " prompt = create_prompt(",
1633
+ " row.get('real_name', row.get('name', '')),",
1634
+ " tags,",
1635
+ " row.get('likely_country', '')",
1636
+ " )",
1637
+ " ",
1638
+ " result = query_gemma(prompt)",
1639
+ " ",
1640
+ " if result:",
1641
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')",
1642
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')",
1643
+ " profs = result.get('professions', [])",
1644
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)",
1645
+ " df.at[i, 'country'] = result.get('country', 'Unknown')",
1646
+ " ",
1647
+ " current_index = i + 1",
1648
+ " ",
1649
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):",
1650
+ " df.to_csv(output_file, index=False)",
1651
+ " with open(index_file, 'w') as f:",
1652
+ " f.write(str(current_index))",
1653
+ " print(f\"\u2705 Saved after {i+1} rows\")",
1654
+ " ",
1655
+ " time.sleep(0.5)",
1656
+ " ",
1657
+ " print(f\"\\n\u2705 Done! Results: {output_file}\")",
1658
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")",
1659
+ " ",
1660
+ "except Exception as e:",
1661
+ " print(f\"\u26a0\ufe0f Error: {e}\")",
1662
+ " df.to_csv(output_file, index=False)",
1663
+ " with open(index_file, 'w') as f:",
1664
+ " f.write(str(current_index))",
1665
+ ""
1666
+ ]
1667
+ },
1668
+ {
1669
+ "cell_type": "markdown",
1670
+ "metadata": {},
1671
+ "source": [
1672
+ "## Qwen-2.5-32B Local Annotation (Ollama)\n",
1673
+ "\n",
1674
+ "Run **Qwen-2.5-32B-Instruct** locally using Ollama - NO API COSTS!\n",
1675
+ "\n",
1676
+ "**Requirements:**\n",
1677
+ "- Ollama installed: https://ollama.com/\n",
1678
+ "- GPU: NVIDIA A100 80GB (recommended)\n",
1679
+ "- RAM: 265GB system RAM\n",
1680
+ "- Model cache: `data/models/` (auto-downloaded on first run)\n",
1681
+ "- Model size: ~20GB download\n",
1682
+ "- VRAM usage: ~22-25GB during inference\n",
1683
+ "\n",
1684
+ "**Setup:**\n",
1685
+ "```bash\n",
1686
+ "# 1. Install Ollama (if not installed)\n",
1687
+ "curl -fsSL https://ollama.com/install.sh | sh\n",
1688
+ "\n",
1689
+ "# 2. Pull Qwen-2.5-32B-Instruct\n",
1690
+ "ollama pull qwen2.5:32b-instruct\n",
1691
+ "\n",
1692
+ "# 3. Verify model is available\n",
1693
+ "ollama list\n",
1694
+ "```\n",
1695
+ "\n",
1696
+ "**Advantages:**\n",
1697
+ "- \u2705 FREE - No API costs\n",
1698
+ "- \u2705 Fast - Local inference on A100\n",
1699
+ "- \u2705 Privacy - Data never leaves your machine\n",
1700
+ "- \u2705 Offline - Works without internet\n",
1701
+ "- \u2705 High quality - 32B parameter model\n",
1702
+ "\n",
1703
+ "**Performance:**\n",
1704
+ "- Speed: ~5-10 tokens/sec on A100\n",
1705
+ "- Throughput: ~100-200 samples/hour (depends on prompt length)\n",
1706
+ "- Cost: $0 (after model download)"
1707
+ ]
1708
+ },
1709
+ {
1710
+ "cell_type": "code",
1711
+ "execution_count": null,
1712
+ "metadata": {},
1713
+ "outputs": [],
1714
+ "source": [
1715
+ "import pandas as pd\n",
1716
+ "import json\n",
1717
+ "import time\n",
1718
+ "import re\n",
1719
+ "import subprocess\n",
1720
+ "from pathlib import Path\n",
1721
+ "from tqdm import tqdm\n",
1722
+ "\n",
1723
+ "current_dir = Path.cwd()\n",
1724
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
1725
+ "\n",
1726
+ "# === CONFIGURATION ===\n",
1727
+ "TEST_MODE = True\n",
1728
+ "TEST_SIZE = 10\n",
1729
+ "MAX_ROWS = 20000\n",
1730
+ "SAVE_INTERVAL = 10\n",
1731
+ "\n",
1732
+ "# Ollama settings\n",
1733
+ "MODEL_NAME = \"qwen2.5:32b-instruct\" # or \"qwen2.5:32b\" for base model\n",
1734
+ "OLLAMA_HOST = \"http://localhost:11434\" # Default Ollama port\n",
1735
+ "\n",
1736
+ "PROFESSION_CATEGORIES = [\n",
1737
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",\n",
1738
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",\n",
1739
+ " \"sports professional\", \"tv personality\"\n",
1740
+ "]\n",
1741
+ "\n",
1742
+ "# === CHECK OLLAMA ===\n",
1743
+ "def check_ollama():\n",
1744
+ " \"\"\"Check if Ollama is installed and model is available.\"\"\"\n",
1745
+ " try:\n",
1746
+ " result = subprocess.run(\n",
1747
+ " [\"ollama\", \"list\"],\n",
1748
+ " capture_output=True,\n",
1749
+ " text=True,\n",
1750
+ " timeout=5\n",
1751
+ " )\n",
1752
+ " \n",
1753
+ " if result.returncode == 0:\n",
1754
+ " print(\"\u2705 Ollama is installed\")\n",
1755
+ " \n",
1756
+ " # Check if model is available\n",
1757
+ " if MODEL_NAME in result.stdout or \"qwen2.5\" in result.stdout:\n",
1758
+ " print(f\"\u2705 Model {MODEL_NAME} is available\")\n",
1759
+ " return True\n",
1760
+ " else:\n",
1761
+ " print(f\"\u26a0\ufe0f Model {MODEL_NAME} not found\")\n",
1762
+ " print(f\" Run: ollama pull {MODEL_NAME}\")\n",
1763
+ " return False\n",
1764
+ " else:\n",
1765
+ " print(\"\u274c Ollama command failed\")\n",
1766
+ " return False\n",
1767
+ " except FileNotFoundError:\n",
1768
+ " print(\"\u274c Ollama not installed\")\n",
1769
+ " print(\" Install: curl -fsSL https://ollama.com/install.sh | sh\")\n",
1770
+ " return False\n",
1771
+ " except Exception as e:\n",
1772
+ " print(f\"\u274c Error checking Ollama: {e}\")\n",
1773
+ " return False\n",
1774
+ "\n",
1775
+ "# Check Ollama before proceeding\n",
1776
+ "if not check_ollama():\n",
1777
+ " raise RuntimeError(\"Ollama not ready. Please install Ollama and pull the model.\")\n",
1778
+ "\n",
1779
+ "# === LOAD DATA ===\n",
1780
+ "df = pd.read_csv(input_file)\n",
1781
+ "print(f\"\\nLoaded {len(df)} rows\")\n",
1782
+ "\n",
1783
+ "if TEST_MODE:\n",
1784
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")\n",
1785
+ " df = df.head(TEST_SIZE).copy()\n",
1786
+ "elif MAX_ROWS:\n",
1787
+ " df = df.head(MAX_ROWS).copy()\n",
1788
+ "\n",
1789
+ "# === CREATE PROMPT ===\n",
1790
+ "def create_prompt(name: str, tags: list, country: str) -> str:\n",
1791
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"\n",
1792
+ " country_str = country if country else \"unknown\"\n",
1793
+ " \n",
1794
+ " return f\"\"\"You are an expert at identifying public figures.\n",
1795
+ "\n",
1796
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}\n",
1797
+ "\n",
1798
+ "Provide in JSON format:\n",
1799
+ "{{\n",
1800
+ " \"full_name\": \"<full name>\",\n",
1801
+ " \"gender\": \"<Male/Female/Other/Unknown>\",\n",
1802
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],\n",
1803
+ " \"country\": \"<country>\"\n",
1804
+ "}}\n",
1805
+ "\n",
1806
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}\n",
1807
+ "- Max 3 professions, ordered by relevance\n",
1808
+ "- \"online personality\" includes streamers, cosplayers, influencers\n",
1809
+ "- Return \"Unknown\" if uncertain\n",
1810
+ "\n",
1811
+ "Return ONLY JSON, no extra text.\"\"\"\n",
1812
+ "\n",
1813
+ "# === QUERY QWEN LOCAL ===\n",
1814
+ "def query_qwen_local(prompt: str) -> dict:\n",
1815
+ " \"\"\"Query Qwen-2.5-32B via Ollama.\"\"\"\n",
1816
+ " for attempt in range(3):\n",
1817
+ " try:\n",
1818
+ " result = subprocess.run(\n",
1819
+ " [\n",
1820
+ " \"ollama\", \"run\", MODEL_NAME,\n",
1821
+ " prompt\n",
1822
+ " ],\n",
1823
+ " capture_output=True,\n",
1824
+ " text=True,\n",
1825
+ " timeout=60 # 60 second timeout per query\n",
1826
+ " )\n",
1827
+ " \n",
1828
+ " if result.returncode == 0:\n",
1829
+ " content = result.stdout.strip()\n",
1830
+ " \n",
1831
+ " # Parse JSON from response\n",
1832
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)\n",
1833
+ " if json_match:\n",
1834
+ " return json.loads(json_match.group())\n",
1835
+ " \n",
1836
+ " # If no JSON found, try to extract from markdown code block\n",
1837
+ " code_block = re.search(r'```(?:json)?\\s*({[^}]+})\\s*```', content, re.DOTALL)\n",
1838
+ " if code_block:\n",
1839
+ " return json.loads(code_block.group(1))\n",
1840
+ " \n",
1841
+ " return None\n",
1842
+ " \n",
1843
+ " except subprocess.TimeoutExpired:\n",
1844
+ " print(f\"Qwen local timeout (attempt {attempt+1})\")\n",
1845
+ " if attempt < 2:\n",
1846
+ " time.sleep(2)\n",
1847
+ " except json.JSONDecodeError as e:\n",
1848
+ " print(f\"Qwen local JSON error (attempt {attempt+1}): {e}\")\n",
1849
+ " if attempt < 2:\n",
1850
+ " time.sleep(1)\n",
1851
+ " except Exception as e:\n",
1852
+ " print(f\"Qwen local error (attempt {attempt+1}): {e}\")\n",
1853
+ " if attempt < 2:\n",
1854
+ " time.sleep(2)\n",
1855
+ " \n",
1856
+ " return None\n",
1857
+ "\n",
1858
+ "# === PROCESS ===\n",
1859
+ "output_file = current_dir.parent / f\"data/CSV/qwen_local_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
1860
+ "index_file = current_dir.parent / \"misc/qwen_local_query_index.txt\"\n",
1861
+ "\n",
1862
+ "current_index = 0\n",
1863
+ "if index_file.exists():\n",
1864
+ " with open(index_file) as f:\n",
1865
+ " current_index = int(f.read().strip())\n",
1866
+ "\n",
1867
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:\n",
1868
+ " if col not in df.columns:\n",
1869
+ " df[col] = 'Unknown'\n",
1870
+ "\n",
1871
+ "print(f\"\\nAnnotating with QWEN-2.5-32B LOCAL - rows {current_index} to {len(df)}...\")\n",
1872
+ "print(f\"Model: {MODEL_NAME}\")\n",
1873
+ "print(f\"This may take a while (~100-200 samples/hour on A100)\\n\")\n",
1874
+ "\n",
1875
+ "try:\n",
1876
+ " start_time = time.time()\n",
1877
+ " \n",
1878
+ " for i in tqdm(range(current_index, len(df)), desc=\"Qwen Local\"):\n",
1879
+ " row = df.iloc[i]\n",
1880
+ " \n",
1881
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8)\n",
1882
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]\n",
1883
+ " \n",
1884
+ " prompt = create_prompt(\n",
1885
+ " row.get('real_name', row.get('name', '')),\n",
1886
+ " tags,\n",
1887
+ " row.get('likely_country', '')\n",
1888
+ " )\n",
1889
+ " \n",
1890
+ " result = query_qwen_local(prompt)\n",
1891
+ " \n",
1892
+ " if result:\n",
1893
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')\n",
1894
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')\n",
1895
+ " profs = result.get('professions', [])\n",
1896
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)\n",
1897
+ " df.at[i, 'country'] = result.get('country', 'Unknown')\n",
1898
+ " \n",
1899
+ " current_index = i + 1\n",
1900
+ " \n",
1901
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
1902
+ " df.to_csv(output_file, index=False)\n",
1903
+ " with open(index_file, 'w') as f:\n",
1904
+ " f.write(str(current_index))\n",
1905
+ " \n",
1906
+ " # Calculate stats\n",
1907
+ " elapsed = time.time() - start_time\n",
1908
+ " samples_done = i + 1 - (current_index - (i + 1))\n",
1909
+ " samples_per_hour = (samples_done / elapsed) * 3600 if elapsed > 0 else 0\n",
1910
+ " \n",
1911
+ " print(f\"\u2705 Saved after {i+1} rows (~{samples_per_hour:.1f} samples/hour)\")\n",
1912
+ " \n",
1913
+ " elapsed_total = time.time() - start_time\n",
1914
+ " print(f\"\\n\u2705 Done! Results: {output_file}\")\n",
1915
+ " print(f\"Total time: {elapsed_total/60:.1f} minutes\")\n",
1916
+ " print(f\"Average speed: {len(df)/(elapsed_total/3600):.1f} samples/hour\")\n",
1917
+ " \n",
1918
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")\n",
1919
+ "\n",
1920
+ "except KeyboardInterrupt:\n",
1921
+ " print(f\"\\n\u26a0\ufe0f Interrupted by user\")\n",
1922
+ " df.to_csv(output_file, index=False)\n",
1923
+ " with open(index_file, 'w') as f:\n",
1924
+ " f.write(str(current_index))\n",
1925
+ " print(f\"Progress saved up to row {current_index}\")\n",
1926
+ "\n",
1927
+ "except Exception as e:\n",
1928
+ " print(f\"\u26a0\ufe0f Error: {e}\")\n",
1929
+ " df.to_csv(output_file, index=False)\n",
1930
+ " with open(index_file, 'w') as f:\n",
1931
+ " f.write(str(current_index))\n",
1932
+ " print(f\"Progress saved up to row {current_index}\")"
1933
+ ]
1934
+ }
1935
+ ],
1936
+ "metadata": {
1937
+ "language_info": {
1938
+ "name": "python"
1939
+ }
1940
+ },
1941
+ "nbformat": 4,
1942
+ "nbformat_minor": 5
1943
+ }
jupyter_notebooks/TESTING_INSTRUCTIONS.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quick Testing Instructions
2
+
3
+ ## Start Here! 🚀
4
+
5
+ You mentioned you have Deepseek credits, so **start by testing with Deepseek first** before trying the other LLMs.
6
+
7
+ ## Step-by-Step Testing
8
+
9
+ ### 1. Make sure your Deepseek API key is in place
10
+
11
+ Check if this file exists:
12
+ ```bash
13
+ cat misc/credentials/deepseek_api_key.txt
14
+ ```
15
+
16
+ If not, create it:
17
+ ```bash
18
+ echo "your-deepseek-api-key" > misc/credentials/deepseek_api_key.txt
19
+ ```
20
+
21
+ ### 2. Open the notebook
22
+
23
+ ```bash
24
+ jupyter notebook jupyter_notebooks/Section_2-3-4_Figure_8_deepfake_adapters.ipynb
25
+ ```
26
+
27
+ ### 3. Run the cells in order
28
+
29
+ 1. **Cell 0-4**: Introduction and setup (just markdown, no execution needed)
30
+ 2. **Cell 5**: NER & Name Cleaning (processes `real_person_adapters.csv`)
31
+ 3. **Cell 7**: Country/Nationality Mapping
32
+ 4. **Cell 10**: 🌟 **DEEPSEEK ANNOTATION** (TEST THIS FIRST!)
33
+ - Default: `TEST_MODE = True` (10 samples)
34
+ - Will create: `data/CSV/deepseek_annotated_POI_test.csv`
35
+ 5. **Cell 12**: Qwen/Llama/Mistral (run later after Deepseek works)
36
+
37
+ ### 4. Review Deepseek Results
38
+
39
+ After Cell 10 completes, check:
40
+ - Console output shows summary statistics
41
+ - Output file: `data/CSV/deepseek_annotated_POI_test.csv`
42
+
43
+ Example output should look like:
44
+ ```
45
+ ✅ Progress saved after 10 rows
46
+ ✅ Done! Final results saved to data/CSV/deepseek_annotated_POI_test.csv
47
+
48
+ === Summary Statistics ===
49
+ Total processed: 10
50
+
51
+ Gender distribution:
52
+ Female 8
53
+ Male 2
54
+ ...
55
+ ```
56
+
57
+ ### 5. If Deepseek Works Well
58
+
59
+ Once you're satisfied with the Deepseek results:
60
+
61
+ **Option A: Process full dataset with Deepseek**
62
+ ```python
63
+ # In Cell 10, change:
64
+ TEST_MODE = False
65
+ ```
66
+
67
+ **Option B: Try other LLMs for comparison**
68
+ 1. Set up API keys for Qwen/Llama/Mistral (see `misc/credentials/README.md`)
69
+ 2. Run Cell 12 with your chosen LLM:
70
+ ```python
71
+ SELECTED_LLM = 'qwen' # or 'llama' or 'mistral'
72
+ TEST_MODE = True # Test first!
73
+ ```
74
+
75
+ ## Expected Cost (Deepseek)
76
+
77
+ - **10 samples** (test): ~$0.01 or less
78
+ - **1,000 entries**: ~$0.10-0.20
79
+ - **10,000 entries**: ~$1-2
80
+
81
+ Much cheaper than the other options, making it perfect for testing!
82
+
83
+ ## Troubleshooting
84
+
85
+ ### "deepseek_api_key.txt not found"
86
+ ```bash
87
+ # Create the file with your key
88
+ echo "your-api-key" > misc/credentials/deepseek_api_key.txt
89
+ ```
90
+
91
+ ### "File does not exist: real_person_adapters.csv"
92
+ Make sure the input file exists:
93
+ ```bash
94
+ ls -lh data/CSV/real_person_adapters.csv
95
+ ```
96
+
97
+ ### API Rate Limiting
98
+ The code includes automatic rate limiting (`time.sleep(1)` between requests). If you still get rate limited:
99
+ - Increase the sleep time in Cell 10: change `time.sleep(1)` to `time.sleep(2)`
100
+
101
+ ### Pipeline Interrupted
102
+ No problem! The code saves progress every 10 rows. Just re-run the cell and it will resume from where it left off.
103
+
104
+ ## What's Next?
105
+
106
+ After testing with Deepseek:
107
+
108
+ 1. **If results look good**: Scale up to full dataset with Deepseek
109
+ 2. **Compare LLMs**: Test Qwen/Llama/Mistral on the same sample to see which gives best results
110
+ 3. **Production run**: Choose your preferred LLM and process the full dataset
111
+
112
+ ## File Outputs
113
+
114
+ The pipeline creates these files:
115
+
116
+ ```
117
+ data/CSV/
118
+ ├── NER_POI_step01_pre_annotation.csv # After Cell 5 (name cleaning)
119
+ ├── NER_POI_step02_annotated.csv # After Cell 7 (country mapping)
120
+ ├── deepseek_annotated_POI_test.csv # After Cell 10 (test mode)
121
+ ├── deepseek_annotated_POI.csv # After Cell 10 (full mode)
122
+ ├── qwen_annotated_POI_test.csv # After Cell 12 (if using Qwen)
123
+ └── ...
124
+
125
+ misc/
126
+ ├── deepseek_query_index.txt # Progress tracking
127
+ └── ...
128
+ ```
129
+
130
+ ## Quick Commands
131
+
132
+ ```bash
133
+ # View first few results
134
+ head -20 data/CSV/deepseek_annotated_POI_test.csv
135
+
136
+ # Count processed rows
137
+ wc -l data/CSV/deepseek_annotated_POI_test.csv
138
+
139
+ # Check progress
140
+ cat misc/deepseek_query_index.txt
141
+
142
+ # Reset progress (start from scratch)
143
+ rm misc/deepseek_query_index.txt
144
+ ```
145
+
146
+ ---
147
+
148
+ **Ready to start?** Open the notebook and run Cell 5 → Cell 7 → Cell 10! 🎉
jupyter_notebooks/UPDATES_AND_FIXES.md ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Recent Updates and Fixes
2
+
3
+ ## Overview
4
+
5
+ Two important fixes have been implemented based on testing feedback:
6
+
7
+ 1. **Leetspeak Translation** (before NER)
8
+ 2. **Improved Country Mapping** (check ALL tags)
9
+
10
+ ---
11
+
12
+ ## Fix 1: Leetspeak Translation
13
+
14
+ ### Problem
15
+ Names with leetspeak (numbers replacing letters) weren't being properly cleaned:
16
+ - `4kira` should be `Akira`
17
+ - `1rene` should be `Irene`
18
+ - `3mma` should be `Emma`
19
+
20
+ ### Solution
21
+ Added leetspeak translation **before** other NER processing in Cell 5.
22
+
23
+ ### Mapping Table
24
+ | Leetspeak | Letter |
25
+ |-----------|--------|
26
+ | 4 | A |
27
+ | 3 | E |
28
+ | 1 | I |
29
+ | 0 | O |
30
+ | 7 | T |
31
+ | 5 | S |
32
+ | 8 | B |
33
+ | 9 | G |
34
+ | @ | A |
35
+ | $ | S |
36
+ | ! | I |
37
+
38
+ ### Examples
39
+ ```
40
+ 4kira -> akira
41
+ 3mma -> emma
42
+ 1rene -> irene
43
+ L3vi -> Levi
44
+ S4sha -> Sasha
45
+ K4te -> Kate
46
+ J3ssica -> Jessica
47
+ ```
48
+
49
+ ### Implementation
50
+ The `translate_leetspeak()` function runs FIRST in `clean_name()`, before emoji removal and other cleaning steps. This ensures leetspeak is converted to proper letters before any other processing.
51
+
52
+ ---
53
+
54
+ ## Fix 2: Improved Country Mapping
55
+
56
+ ### Problem
57
+ The country mapping was stopping at the first match, which meant:
58
+ - **Irene** with tags `['girl', 'photorealistic', 'asian', 'woman', 'beautiful', 'celebrity', 'korean']`
59
+ - The `'korean'` tag wasn't being properly mapped to `'South Korea'`
60
+ - This resulted in incomplete hints being sent to the LLM
61
+ - **Expected**: Deepseek should identify **Bae Joo-hyun (Irene)** from Red Velvet
62
+
63
+ ### Solution
64
+ Updated Cell 7 to:
65
+ 1. **Check ALL tags** (not just stop at first match)
66
+ 2. **Use a priority system** to select the best match:
67
+ - Priority 3: Exact country name match (highest)
68
+ - Priority 2: Nationality match (medium)
69
+ - Priority 1: Word parts (lowest)
70
+
71
+ ### How It Works
72
+
73
+ #### Before (Broken)
74
+ ```python
75
+ def infer_country_and_nationality(tags):
76
+ for tag in tags:
77
+ if tag in mapping:
78
+ return mapping[tag] # ❌ Stops at first match!
79
+ return ("", "")
80
+ ```
81
+
82
+ #### After (Fixed)
83
+ ```python
84
+ def infer_country_and_nationality(tags):
85
+ best_match = None
86
+ best_priority = 0
87
+
88
+ for tag in tags: # ✅ Check ALL tags
89
+ if tag in mapping:
90
+ country, nationality, priority = mapping[tag]
91
+ if priority > best_priority:
92
+ best_match = (country, nationality)
93
+ best_priority = priority
94
+
95
+ return best_match or ("", "")
96
+ ```
97
+
98
+ ### Example: Irene Case
99
+
100
+ **Input Tags**: `['girl', 'photorealistic', 'asian', 'woman', 'beautiful', 'celebrity', 'korean']`
101
+
102
+ **Processing**:
103
+ 1. Check `'girl'` → no match
104
+ 2. Check `'photorealistic'` → no match
105
+ 3. Check `'asian'` → no match (too generic)
106
+ 4. Check `'woman'` → no match
107
+ 5. Check `'beautiful'` → no match
108
+ 6. Check `'celebrity'` → no match
109
+ 7. Check `'korean'` → ✅ **MATCH!**
110
+ - Maps to nationality: `'South Korean'`
111
+ - Which maps to country: `'South Korea'`
112
+ - Priority: 2 (nationality match)
113
+
114
+ **Output**:
115
+ - `likely_country`: `'South Korea'`
116
+ - `likely_nationality`: `'South Korean'`
117
+
118
+ **Sent to Deepseek**:
119
+ ```
120
+ Given 'Irene' (celebrity, South Korea), provide:
121
+ 1. Full legal name
122
+ 2. Aliases
123
+ 3. Gender
124
+ 4. Top 3 professions
125
+ 5. Country
126
+ ```
127
+
128
+ **Expected Result**: Deepseek can now identify this as **Bae Joo-hyun (Irene)**, a South Korean singer/actress from the K-pop group Red Velvet.
129
+
130
+ ---
131
+
132
+ ## Impact on Results
133
+
134
+ ### Better Name Recognition
135
+ - Leetspeak names are now properly translated
136
+ - LLMs receive cleaner, more recognizable names
137
+
138
+ ### Better Country Context
139
+ - All tags are now considered for country mapping
140
+ - More accurate country/nationality hints sent to LLMs
141
+ - Better identification of international celebrities
142
+
143
+ ### Example Improvements
144
+
145
+ | Name | Tags | Before | After |
146
+ |------|------|--------|-------|
147
+ | `4kira LoRA` | `['japanese', 'actress']` | `'4kira'` + no country | `'Akira'` + `'Japan'` |
148
+ | `Irene` | `['korean', 'celebrity']` | `'Irene'` + no country | `'Irene'` + `'South Korea'` |
149
+ | `1U` | `['korean', 'singer']` | `'1U'` + no country | `'IU'` + `'South Korea'` |
150
+ | `3lsa` | `['model']` | `'3lsa'` + no country | `'Elsa'` + country if tagged |
151
+
152
+ ---
153
+
154
+ ## Testing Recommendations
155
+
156
+ ### Before Running Full Pipeline
157
+
158
+ 1. **Test Leetspeak Translation** (Cell 5):
159
+ ```python
160
+ # Look for names with numbers in the output
161
+ # Verify they're properly translated
162
+ ```
163
+
164
+ 2. **Test Country Mapping** (Cell 7):
165
+ ```python
166
+ # Check the debug output at the end:
167
+ # "🔍 Checking 'Irene' entries:"
168
+ # Verify country is properly mapped
169
+ ```
170
+
171
+ 3. **Test Deepseek Results** (Cell 10):
172
+ ```python
173
+ # Look for Irene in the results
174
+ # Should now identify as Bae Joo-hyun
175
+ ```
176
+
177
+ ### Validation Checklist
178
+
179
+ - [ ] Leetspeak names are translated (check console output in Cell 5)
180
+ - [ ] Country mapping shows high success rate (check stats in Cell 7)
181
+ - [ ] Irene is correctly identified as Bae Joo-hyun (check results in Cell 10)
182
+ - [ ] Other K-pop/Korean celebrities are properly identified
183
+ - [ ] Japanese/Chinese celebrities also benefit from better country mapping
184
+
185
+ ---
186
+
187
+ ## Notes
188
+
189
+ ### Why Check ALL Tags?
190
+
191
+ Some entries have many tags, and the most informative tag might not be first:
192
+ ```
193
+ tags = ['girl', 'sexy', 'beautiful', 'asian', 'korean', 'celebrity', 'kpop']
194
+ ^^^^ Most informative!
195
+ ```
196
+
197
+ The old code might stop at `'girl'` or `'asian'` (no country info), missing the `'korean'` tag.
198
+
199
+ ### Why Use Priority?
200
+
201
+ Some tags might match multiple countries. Priority ensures we get the best match:
202
+ - `'american'` → exact nationality match (priority 2) → USA
203
+ - `'america'` → could be North/South/Central America (priority 1)
204
+
205
+ The system picks the higher priority match.
206
+
207
+ ### Word Length Filter
208
+
209
+ Word parts only match if >4 characters to avoid false positives:
210
+ - ✅ `'china'` → matches China (5 chars)
211
+ - ❌ `'us'` → too short, might be part of other words
212
+
213
+ ---
214
+
215
+ ## Future Improvements
216
+
217
+ Potential enhancements:
218
+ 1. **More leetspeak patterns**: `|\/|` for M, `(_)` for U, etc.
219
+ 2. **Fuzzy country matching**: Handle typos like `'corean'` → `'korean'`
220
+ 3. **Multi-country support**: Some celebrities work in multiple countries
221
+ 4. **Language detection**: Use name structure to infer origin
222
+
223
+ ---
224
+
225
+ ## Summary
226
+
227
+ ✅ **Leetspeak translation** ensures names are readable before NER
228
+ ✅ **ALL tags checked** ensures no country hints are missed
229
+ ✅ **Priority system** ensures best match is selected
230
+ ✅ **Better LLM results** from improved name quality and country context
231
+
232
+ These fixes should significantly improve the accuracy of person identification, especially for:
233
+ - International celebrities (K-pop, J-pop, C-pop)
234
+ - Names with leetspeak
235
+ - Entries where country info appears later in tag list
misc/deepseek_query_index.txt CHANGED
@@ -1 +1 @@
1
- 4
 
1
+ 10
misc/lists/PROFESSION_CATEGORIES.md ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Profession Categories for Deepfake Adapter Classification
2
+
3
+ ## Overview
4
+
5
+ The LLM annotation uses **9 specific profession categories** to classify people found in deepfake adapter datasets. These categories cover the most common types of public figures targeted by deepfake technology.
6
+
7
+ ## The 9 Categories
8
+
9
+ ### 1. **actor**
10
+ Film, TV, and theater performers who primarily work in scripted dramatic content.
11
+
12
+ **Examples:**
13
+ - Emma Watson (film actor)
14
+ - Scarlett Johansson (film actor)
15
+ - Bryan Cranston (TV actor)
16
+
17
+ **Keywords:** actor, actress, film, movie, cinema, theatrical, performer, drama
18
+
19
+ ---
20
+
21
+ ### 2. **adult performer**
22
+ People working in the adult entertainment industry.
23
+
24
+ **Examples:**
25
+ - Adult film actors
26
+ - OnlyFans creators
27
+ - Cam models
28
+
29
+ **Keywords:** adult entertainer, onlyfans, cam model, camgirl, webcam, pornographic actor, porn, pornstar
30
+
31
+ **Note:** This category is important for research on unauthorized deepfake usage, which disproportionately affects adult performers.
32
+
33
+ ---
34
+
35
+ ### 3. **singer/musician**
36
+ Vocalists, instrumentalists, and music performers across all genres.
37
+
38
+ **Examples:**
39
+ - IU (K-pop singer)
40
+ - Taylor Swift (singer/songwriter)
41
+ - DJ Khaled (DJ/producer)
42
+
43
+ **Keywords:** singer, musician, rapper, rap artist, band, vocalist, songwriter, composer, DJ, producer, kpop, jpop
44
+
45
+ ---
46
+
47
+ ### 4. **model**
48
+ Fashion, runway, and photoshoot models.
49
+
50
+ **Examples:**
51
+ - Gigi Hadid (fashion model)
52
+ - Tyra Banks (supermodel)
53
+ - Ashley Graham (plus-size model)
54
+
55
+ **Keywords:** model, fashion, runway, photoshoot, supermodel
56
+
57
+ ---
58
+
59
+ ### 5. **online personality**
60
+ Digital content creators, streamers, and influencers.
61
+
62
+ **Includes:**
63
+ - Streamers (Twitch, YouTube)
64
+ - Cosplayers
65
+ - YouTubers
66
+ - Instagram influencers
67
+ - Content creators
68
+ - E-girls/E-boys
69
+ - Gaming personalities
70
+
71
+ **Examples:**
72
+ - Belle Delphine (online personality, cosplayer)
73
+ - Pokimane (Twitch streamer)
74
+ - MrBeast (YouTuber)
75
+
76
+ **Keywords:** influencer, streamer, twitch, youtuber, youtube, content creator, instagrammer, instagram, e-girl, egirl, e-boy, eboy, cosplayer, gamer
77
+
78
+ ---
79
+
80
+ ### 6. **public figure**
81
+ Politicians, activists, journalists, authors, and other public-facing professionals not in entertainment.
82
+
83
+ **Examples:**
84
+ - Barack Obama (politician)
85
+ - Greta Thunberg (activist)
86
+ - Gordon Ramsay (chef)
87
+ - J.K. Rowling (author)
88
+
89
+ **Keywords:** politician, activist, public speaker, journalist, author, writer, chef, famous, celebrity (when not in entertainment)
90
+
91
+ ---
92
+
93
+ ### 7. **voice actor/ASMR**
94
+ Voice performers for animation, games, and ASMR content creators.
95
+
96
+ **Examples:**
97
+ - Tara Strong (voice actress)
98
+ - Troy Baker (voice actor)
99
+ - ASMR artists
100
+
101
+ **Keywords:** voice actor, voice actress, asmr creator, asmr
102
+
103
+ ---
104
+
105
+ ### 8. **sports professional**
106
+ Professional athletes and sports competitors.
107
+
108
+ **Examples:**
109
+ - Cristiano Ronaldo (soccer player)
110
+ - Serena Williams (tennis player)
111
+ - LeBron James (basketball player)
112
+
113
+ **Keywords:** athlete, sports, player, professional, competitor, olympian
114
+
115
+ ---
116
+
117
+ ### 9. **tv personality**
118
+ TV hosts, presenters, reality TV stars, and broadcast personalities.
119
+
120
+ **Examples:**
121
+ - Oprah Winfrey (talk show host)
122
+ - Kim Kardashian (reality TV)
123
+ - Jimmy Fallon (late night host)
124
+
125
+ **Keywords:** tv host, tv moderator, talk show host, talkshow, radio host, media personality, reality tv star, reality, comedian, presenter, broadcaster, anchor
126
+
127
+ ---
128
+
129
+ ## Multi-Category Classification
130
+
131
+ Many public figures work across multiple categories. The LLM can assign **up to 3 professions** per person, ordered by relevance.
132
+
133
+ ### Examples:
134
+
135
+ | Person | Professions | Explanation |
136
+ |--------|-------------|-------------|
137
+ | **Emma Watson** | `actor, public figure` | Primarily an actor, but also known for activism |
138
+ | **IU (Lee Ji-eun)** | `singer/musician, actor` | K-pop singer who also acts in TV dramas |
139
+ | **Belle Delphine** | `online personality, adult performer` | Internet personality with adult content |
140
+ | **Jamie Foxx** | `actor, singer/musician` | Actor who also has a music career |
141
+ | **Dwayne Johnson** | `actor, sports professional` | Former wrestler, now primarily an actor |
142
+
143
+ ## Category Selection Guidelines
144
+
145
+ ### For the LLM:
146
+
147
+ 1. **Choose most specific category first**
148
+ - ✅ `"actor"` for film performers
149
+ - ❌ `"public figure"` for someone primarily known as an actor
150
+
151
+ 2. **Order by relevance**
152
+ - Most important role first
153
+ - Secondary roles after
154
+ - Maximum 3 categories
155
+
156
+ 3. **Be inclusive for online personalities**
157
+ - Streamers → `"online personality"`
158
+ - Cosplayers → `"online personality"`
159
+ - Influencers → `"online personality"`
160
+
161
+ 4. **Distinguish TV personalities from actors**
162
+ - ✅ `"tv personality"` for talk show hosts
163
+ - ✅ `"actor"` for scripted TV drama performers
164
+ - Some can be both!
165
+
166
+ ## Why These 9 Categories?
167
+
168
+ These categories were chosen based on:
169
+
170
+ 1. **Common targets of deepfake technology**
171
+ - Celebrities and public figures are most frequently deepfaked
172
+ - Adult performers are disproportionately affected
173
+
174
+ 2. **Clear distinctions**
175
+ - Each category represents a distinct professional domain
176
+ - Minimal overlap between categories
177
+
178
+ 3. **Research relevance**
179
+ - Important for analyzing demographic patterns in deepfake usage
180
+ - Helps understand which professions are most at risk
181
+
182
+ 4. **Comprehensive coverage**
183
+ - Covers the vast majority of people in the deepfake adapter dataset
184
+ - Includes both traditional and digital-native celebrities
185
+
186
+ ## Output Format
187
+
188
+ The LLM returns professions as a **comma-separated list**:
189
+
190
+ ```
191
+ "singer/musician, actor"
192
+ "online personality"
193
+ "actor, public figure, online personality"
194
+ "sports professional"
195
+ ```
196
+
197
+ ## Validation
198
+
199
+ After annotation, we can analyze:
200
+ - Distribution across categories
201
+ - Multi-category patterns
202
+ - Correlation with countries/regions
203
+ - Demographic patterns
204
+
205
+ Example analysis:
206
+ ```python
207
+ # Count by category
208
+ df['profession_llm'].value_counts()
209
+
210
+ # Most common combinations
211
+ df['profession_llm'].value_counts().head(20)
212
+
213
+ # Filter by specific category
214
+ actors = df[df['profession_llm'].str.contains('actor', na=False)]
215
+ ```
216
+
217
+ ## Edge Cases
218
+
219
+ ### Fictional Characters
220
+ **Handling:** Return `"Unknown"` for all fields
221
+
222
+ **Example:**
223
+ - Input: `"Elsa from Frozen"`
224
+ - Output: `profession_llm: "Unknown"`
225
+
226
+ ### Unclear/Ambiguous Cases
227
+ **Handling:** Use best judgment based on primary public recognition
228
+
229
+ **Example:**
230
+ - Input: `"Elon Musk"`
231
+ - Primary recognition: Business/technology
232
+ - Category: `"public figure"`
233
+
234
+ ### Multiple Equally Important Roles
235
+ **Handling:** List all relevant categories (up to 3)
236
+
237
+ **Example:**
238
+ - Input: `"Donald Glover / Childish Gambino"`
239
+ - Categories: `"actor, singer/musician, tv personality"`
240
+
241
+ ## Notes for Researchers
242
+
243
+ When analyzing the annotated data:
244
+
245
+ 1. **Category frequency** indicates which professions are most targeted
246
+ 2. **Multi-category entries** show crossover between industries
247
+ 3. **Online personality prevalence** indicates rise of digital-native celebrities in deepfakes
248
+ 4. **Adult performer numbers** highlight ongoing issues with non-consensual deepfakes
249
+
250
+ ## Version History
251
+
252
+ - **v1.0** (Current): Initial 9-category system
253
+ - Refined from original detailed profession list
254
+ - Focused on clear, distinct categories
255
+ - Added comprehensive documentation
misc/lists/professions_mapped.csv ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ category,subcategory,keywords
2
+ actor,actor,actor actress film movie cinema theatrical performer drama
3
+ adult performer,adult performer,adult entertainer onlyfans creator cam model camgirl webcam pornographic actor porn pornstar adult
4
+ singer/musician,singer/musician,singer musician rapper rap artist band vocalist songwriter composer DJ producer kpop jpop
5
+ model,model,model fashion runway photoshoot supermodel
6
+ online personality,online personality,influencer streamer twitch youtuber youtube content creator instagrammer instagram insta e-girl egirl e-boy eboy cosplayer gamer
7
+ public figure,public figure,celebrity celeb politician activist public speaker journalist author writer chef famous
8
+ voice actor/ASMR,voice actor/ASMR,voice actor voice actress asmr creator asmr
9
+ sports professional,sports professional,athlete sports player professional competitor olympian
10
+ tv personality,tv personality,tv host tv moderator talk show host talkshow radio host media personality reality tv star reality comedian presenter broadcaster anchor
misc/qwen_query_index.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 10