everydaycats commited on
Commit
4f61e51
Β·
verified Β·
1 Parent(s): ce53861

Update apps/aura_measure.js

Browse files
Files changed (1) hide show
  1. apps/aura_measure.js +306 -0
apps/aura_measure.js CHANGED
@@ -1,4 +1,310 @@
1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  // ─── GET /feed ─────────────────────────────────────────────────────────────────
4
  router.get('/feed', async (req, res) => {
 
1
 
2
+ import express from 'express';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import crypto from 'crypto';
6
+ import { generateCompletion } from '../ai_engine.js';
7
+ import { supabase } from '../config/supabaseClient.js';
8
+
9
+ const router = express.Router();
10
+
11
+ const TEMP_DIR = path.join(process.cwd(), 'tmp_scans');
12
+ const tempScans = new Map();
13
+ fs.mkdir(TEMP_DIR, { recursive: true }).catch(console.error);
14
+
15
+ setInterval(async () => {
16
+ const now = Date.now();
17
+ for (const [tempId, data] of tempScans.entries()) {
18
+ if (now - data.timestamp > 30 * 60 * 1000) {
19
+ try { await fs.unlink(data.filepath); } catch (e) {}
20
+ tempScans.delete(tempId);
21
+ }
22
+ }
23
+ }, 5 * 60 * 1000);
24
+
25
+ const cache = {
26
+ feed_page_1: { data: null, expires: 0 },
27
+ leaderboard_all: { data: null, expires: 0 },
28
+ leaderboard_session: { data: null, expires: 0 },
29
+ leaderboard_grade: { data: null, expires: 0 },
30
+ challenges_active: { data: null, expires: 0 }
31
+ };
32
+ const CACHE_TTL = 60 * 1000;
33
+
34
+ function invalidateCaches(type) {
35
+ if (type === 'feed') {
36
+ cache.feed_page_1.expires = 0;
37
+ cache.feed_page_1.data = null;
38
+ }
39
+ if (type === 'leaderboard') {
40
+ cache.leaderboard_all.expires = 0; cache.leaderboard_all.data = null;
41
+ cache.leaderboard_session.expires = 0; cache.leaderboard_session.data = null;
42
+ cache.leaderboard_grade.expires = 0; cache.leaderboard_grade.data = null;
43
+ }
44
+ if (type === 'challenges') {
45
+ cache.challenges_active.expires = 0; cache.challenges_active.data = null;
46
+ }
47
+ }
48
+
49
+ const BASE_RULES = `
50
+ ## SCORING PHILOSOPHY:
51
+ - Normal selfie: 600-1,100
52
+ - Solid fit check: 1,100-2,200
53
+ - Great outfit + confidence: 2,000-4,000
54
+ - Elite rizz: 2,000-5,000
55
+ - Legendary: 5,000-15,000
56
+ - Minor detail: Β±50-300 | Mid factor: Β±200-800 | Core factor: Β±500-2,500
57
+
58
+ Output ONLY a JSON object with exactly:
59
+ - "score": integer (exact sum of all gains minus all losses)
60
+ - "analysis_markdown": string formatted exactly as:
61
+ ## 🟒 Aura Gained
62
+ * **[Element]**: +[Points] ([Funny reason])
63
+ ## πŸ”΄ Aura Lost
64
+ * **[Element]**: -[Points] ([Honest reason])
65
+ ## πŸ’¬ The Verdict
66
+ [One sharp sentence]
67
+ ## 🎯 The Play
68
+ [One specific tip]
69
+ `;
70
+
71
+ const AURA_PROMPT_STANDALONE = `You are Aura Cat πŸ•ΆοΈπŸˆ, the ultimate judge of social cachet. Evaluate the provided image absolutely. ${BASE_RULES}`;
72
+ const AURA_PROMPT_CONTEXTUAL = `You are Aura Cat πŸ•ΆοΈπŸˆ, the ultimate judge of social cachet.
73
+ You are evaluating a submission to a Challenge or Thread.
74
+ Image 1: The Reference Image
75
+ Image 2: The Submission
76
+ Evaluate Image 2 strictly based on how well it matched the vibe and quality of Image 1. ${BASE_RULES}`;
77
+
78
+ // ─── POST /initialize ──────────────────────────────────────────────────────────
79
+ router.post('/initialize', async (req, res) => {
80
+ try {
81
+ const { device_id, push_token, username, avatar } = req.body;
82
+ if (!device_id) return res.status(400).json({ success: false, error: 'device_id required' });
83
+
84
+ const updateData = { last_login_at: new Date().toISOString() };
85
+ if (push_token) updateData.push_token = push_token;
86
+ if (username) updateData.username = username;
87
+ if (avatar) updateData.avatar = avatar;
88
+
89
+ const { data, error } = await supabase
90
+ .from('aura_profiles')
91
+ .upsert({ device_id, ...updateData }, { onConflict: 'device_id' })
92
+ .select('id, username, avatar, current_grade, cumulative_aura')
93
+ .single();
94
+
95
+ if (error) throw error;
96
+ res.json({ success: true, profile: data });
97
+ } catch (err) {
98
+ console.error('[Initialize] Error:', err);
99
+ res.status(500).json({ success: false, error: err.message });
100
+ }
101
+ });
102
+
103
+ // ─── POST /analyze ─────────────────────────────────────────────────────────────
104
+ router.post('/analyze', async (req, res) => {
105
+ try {
106
+ const {
107
+ device_id, username, avatar, media_base64,
108
+ context_type = 'standalone', context_id = null, reference_data = null
109
+ } = req.body;
110
+
111
+ if (!media_base64 || !device_id) {
112
+ return res.status(400).json({ success: false, error: 'Missing media or device_id.' });
113
+ }
114
+
115
+ const { data: profile, error: profileError } = await supabase
116
+ .from('aura_profiles')
117
+ .upsert({ device_id, username, avatar, last_login_at: new Date().toISOString() }, { onConflict: 'device_id' })
118
+ .select('id')
119
+ .single();
120
+
121
+ if (profileError) throw profileError;
122
+ console.log('[Analyze] Profile ID:', profile.id);
123
+
124
+ let systemPrompt = AURA_PROMPT_STANDALONE;
125
+ let promptText = 'Analyze this image and calculate my Aura. Return JSON.';
126
+ let imagesToProcess = [media_base64];
127
+
128
+ if ((context_type === 'challenge' || context_type === 'thread') && reference_data) {
129
+ systemPrompt = AURA_PROMPT_CONTEXTUAL;
130
+ promptText = `CONTEXT TITLE: ${reference_data.title}\nCONTEXT DESCRIPTION: ${reference_data.description}\nEvaluate Image 2 against Image 1.`;
131
+ imagesToProcess = [reference_data.image_base64, media_base64];
132
+ }
133
+
134
+ console.log('[Analyze] Running AI...');
135
+ const aiResult = await generateCompletion({
136
+ model: 'qwen-vl-max',
137
+ prompt: promptText,
138
+ system_prompt: systemPrompt,
139
+ images: imagesToProcess
140
+ });
141
+
142
+ if (!aiResult.success) throw new Error(aiResult.error);
143
+
144
+ const cleanJsonStr = aiResult.data.replace(/```json/gi, '').replace(/```/gi, '').trim();
145
+ let parsedData;
146
+ try {
147
+ parsedData = JSON.parse(cleanJsonStr);
148
+ } catch {
149
+ throw new Error(`AI returned invalid JSON: ${cleanJsonStr.substring(0, 100)}`);
150
+ }
151
+
152
+ if (typeof parsedData.score !== 'number') {
153
+ throw new Error('AI response missing valid score field.');
154
+ }
155
+
156
+ console.log('[Analyze] AI done. Score:', parsedData.score);
157
+
158
+ const tempId = crypto.randomUUID();
159
+ const imageBuffer = Buffer.from(media_base64, 'base64');
160
+
161
+ tempScans.set(tempId, {
162
+ filepath: path.join(TEMP_DIR, `${tempId}.jpg`),
163
+ timestamp: Date.now(),
164
+ user_id: profile.id,
165
+ session_id: null,
166
+ score: parsedData.score,
167
+ analysis: parsedData.analysis_markdown,
168
+ context_type,
169
+ context_id,
170
+ ready: false,
171
+ });
172
+
173
+ res.json({
174
+ success: true,
175
+ score: parsedData.score,
176
+ analysis: parsedData.analysis_markdown,
177
+ temp_scan_id: tempId,
178
+ profile_id: profile.id,
179
+ });
180
+
181
+ console.log('[Analyze] Response sent. Running async DB writes...');
182
+
183
+ (async () => {
184
+ try {
185
+ await fs.writeFile(path.join(TEMP_DIR, `${tempId}.jpg`), imageBuffer);
186
+
187
+ let { data: session } = await supabase
188
+ .from('aura_sessions')
189
+ .select('id')
190
+ .eq('is_active', true)
191
+ .maybeSingle();
192
+
193
+ if (!session) {
194
+ const start = new Date();
195
+ const end = new Date(start.getTime() + (3 * 24 * 60 * 60 * 1000));
196
+ const { data: newSession, error: sessionError } = await supabase
197
+ .from('aura_sessions')
198
+ .insert({ start_time: start, end_time: end, is_active: true })
199
+ .select('id')
200
+ .single();
201
+ if (sessionError) throw sessionError;
202
+ session = newSession;
203
+ }
204
+
205
+ const { error: rpcError } = await supabase.rpc('aura_increment_points', {
206
+ p_user_id: profile.id,
207
+ p_session_id: session.id,
208
+ p_score_to_add: parsedData.score,
209
+ p_is_challenge: context_type === 'challenge'
210
+ });
211
+ if (rpcError) throw rpcError;
212
+
213
+ const { data: updatedProfile } = await supabase
214
+ .from('aura_profiles')
215
+ .select('cumulative_aura, current_grade')
216
+ .eq('id', profile.id)
217
+ .single();
218
+
219
+ const existing = tempScans.get(tempId);
220
+ if (existing) {
221
+ tempScans.set(tempId, {
222
+ ...existing,
223
+ session_id: session.id,
224
+ ready: true,
225
+ cumulative_aura: updatedProfile?.cumulative_aura ?? 0,
226
+ current_grade: updatedProfile?.current_grade ?? 'Ungraded',
227
+ });
228
+ }
229
+
230
+ invalidateCaches('leaderboard');
231
+ console.log('[Analyze] Async writes complete for', tempId);
232
+ } catch (asyncErr) {
233
+ console.error('[Analyze] Async write error:', asyncErr);
234
+ const existing = tempScans.get(tempId);
235
+ if (existing) tempScans.set(tempId, { ...existing, ready: true, db_error: true });
236
+ }
237
+ })();
238
+
239
+ } catch (err) {
240
+ console.error('[Analyze] Error:', err);
241
+ res.status(500).json({ success: false, error: err.message });
242
+ }
243
+ });
244
+
245
+ // ─── POST /publish ─────────────────────────────────────────────────────────────
246
+ router.post('/publish', async (req, res) => {
247
+ try {
248
+ const { temp_scan_id } = req.body;
249
+
250
+ if (!temp_scan_id || !tempScans.has(temp_scan_id)) {
251
+ return res.status(400).json({ success: false, error: 'Scan expired or not found.' });
252
+ }
253
+
254
+ const scanData = await new Promise((resolve, reject) => {
255
+ const entry = tempScans.get(temp_scan_id);
256
+ if (entry.ready) return resolve(entry);
257
+ let waited = 0;
258
+ const interval = setInterval(() => {
259
+ const current = tempScans.get(temp_scan_id);
260
+ waited += 200;
261
+ if (!current) { clearInterval(interval); reject(new Error('Scan disappeared.')); }
262
+ else if (current.ready) { clearInterval(interval); resolve(current); }
263
+ else if (waited >= 10000) { clearInterval(interval); reject(new Error('Timed out waiting for scan.')); }
264
+ }, 200);
265
+ });
266
+
267
+ if (scanData.db_error || !scanData.user_id) {
268
+ return res.status(500).json({ success: false, error: 'Score was saved but publish failed due to a database error.' });
269
+ }
270
+
271
+ const fileName = `${scanData.user_id}/${Date.now()}_${temp_scan_id}.jpg`;
272
+ const fileBuffer = await fs.readFile(scanData.filepath);
273
+
274
+ const { error: uploadError } = await supabase.storage
275
+ .from('aura_media')
276
+ .upload(fileName, fileBuffer, { contentType: 'image/jpeg' });
277
+ if (uploadError) throw uploadError;
278
+
279
+ const { data: publicUrlData } = supabase.storage.from('aura_media').getPublicUrl(fileName);
280
+
281
+ const { error: insertError } = await supabase.from('aura_public_scans').insert({
282
+ user_id: scanData.user_id,
283
+ session_id: scanData.session_id,
284
+ image_url: publicUrlData.publicUrl,
285
+ score: scanData.score,
286
+ analysis_json: scanData.analysis,
287
+ context_type: scanData.context_type,
288
+ context_id: scanData.context_id
289
+ });
290
+ if (insertError) throw insertError;
291
+
292
+ await fs.unlink(scanData.filepath).catch(() => {});
293
+ tempScans.delete(temp_scan_id);
294
+ invalidateCaches('feed');
295
+
296
+ res.json({
297
+ success: true,
298
+ message: 'Published.',
299
+ cumulative_aura: scanData.cumulative_aura,
300
+ current_grade: scanData.current_grade,
301
+ });
302
+
303
+ } catch (err) {
304
+ console.error('[Publish] Error:', err);
305
+ res.status(500).json({ success: false, error: err.message });
306
+ }
307
+ });
308
 
309
  // ─── GET /feed ─────────────────────────────────────────────────────────────────
310
  router.get('/feed', async (req, res) => {