flen-crypto commited on
Commit
44b3f61
·
verified ·
1 Parent(s): 70b91c9

improve record identification and valuation ability hugely

Browse files
collection.html CHANGED
@@ -318,16 +318,17 @@
318
 
319
  <!-- Footer -->
320
  <vinyl-footer></vinyl-footer>
321
-
322
  <!-- Components -->
323
  <script src="components/vinyl-nav.js"></script>
324
  <script src="components/vinyl-footer.js"></script>
325
  <script src="components/stat-card.js"></script>
326
  <script src="components/collection-service.js"></script>
 
 
327
  <!-- Main Script -->
328
  <script src="script.js"></script>
329
  <script src="collection.js"></script>
330
- <script>
331
  feather.replace();
332
 
333
  // Ensure showToast is available (it's defined in script.js but may not be loaded yet)
 
318
 
319
  <!-- Footer -->
320
  <vinyl-footer></vinyl-footer>
 
321
  <!-- Components -->
322
  <script src="components/vinyl-nav.js"></script>
323
  <script src="components/vinyl-footer.js"></script>
324
  <script src="components/stat-card.js"></script>
325
  <script src="components/collection-service.js"></script>
326
+ <script src="components/enhanced-ocr-service.js"></script>
327
+ <script src="components/pricecharting-service.js"></script>
328
  <!-- Main Script -->
329
  <script src="script.js"></script>
330
  <script src="collection.js"></script>
331
+ <script>
332
  feather.replace();
333
 
334
  // Ensure showToast is available (it's defined in script.js but may not be loaded yet)
components/enhanced-ocr-service.js ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Enhanced OCR Service with multi-provider support and advanced image analysis
2
+ class EnhancedOCRService {
3
+ constructor() {
4
+ this.providers = {
5
+ openai: {
6
+ name: 'OpenAI',
7
+ url: 'https://api.openai.com/v1/chat/completions',
8
+ models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo']
9
+ },
10
+ deepseek: {
11
+ name: 'DeepSeek',
12
+ url: 'https://api.deepseek.com/v1/chat/completions',
13
+ models: ['deepseek-chat', 'deepseek-reasoner']
14
+ },
15
+ anthropic: {
16
+ name: 'Anthropic',
17
+ url: 'https://api.anthropic.com/v1/messages',
18
+ models: ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307']
19
+ },
20
+ google: {
21
+ name: 'Google Gemini',
22
+ url: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent',
23
+ models: ['gemini-pro-vision']
24
+ }
25
+ };
26
+ }
27
+
28
+ getActiveProvider() {
29
+ const configured = localStorage.getItem('ai_provider') || 'openai';
30
+
31
+ // Check which provider has credentials
32
+ if (configured === 'deepseek' && localStorage.getItem('deepseek_api_key')) {
33
+ return { ...this.providers.deepseek, key: localStorage.getItem('deepseek_api_key'), model: localStorage.getItem('deepseek_model') || 'deepseek-chat' };
34
+ }
35
+ if (configured === 'anthropic' && localStorage.getItem('anthropic_api_key')) {
36
+ return { ...this.providers.anthropic, key: localStorage.getItem('anthropic_api_key'), model: localStorage.getItem('anthropic_model') || 'claude-3-sonnet-20240229' };
37
+ }
38
+ if (configured === 'google' && localStorage.getItem('google_api_key')) {
39
+ return { ...this.providers.google, key: localStorage.getItem('google_api_key'), model: 'gemini-pro-vision' };
40
+ }
41
+
42
+ // Default to OpenAI
43
+ const openaiKey = localStorage.getItem('openai_api_key');
44
+ if (openaiKey) {
45
+ return { ...this.providers.openai, key: openaiKey, model: localStorage.getItem('openai_model') || 'gpt-4o' };
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ async analyzeRecordImages(imageFiles, options = {}) {
52
+ const provider = this.getActiveProvider();
53
+ if (!provider) {
54
+ throw new Error('No AI provider configured. Please add an API key in Settings.');
55
+ }
56
+
57
+ // Limit number of images to reduce costs
58
+ const maxImages = options.maxImages || 6;
59
+ const filesToAnalyze = imageFiles.slice(0, maxImages);
60
+
61
+ // Convert all images to base64
62
+ const base64Images = await Promise.all(
63
+ filesToAnalyze.map(file => this.fileToBase64Clean(file))
64
+ );
65
+
66
+ // Determine analysis depth
67
+ const analysisDepth = options.depth || 'standard'; // quick, standard, deep
68
+
69
+ const systemPrompt = this.buildSystemPrompt(analysisDepth);
70
+ const userPrompt = this.buildUserPrompt(filesToAnalyze, options);
71
+
72
+ try {
73
+ let result;
74
+
75
+ switch(provider.name) {
76
+ case 'OpenAI':
77
+ result = await this.callOpenAI(provider, systemPrompt, userPrompt, base64Images);
78
+ break;
79
+ case 'DeepSeek':
80
+ result = await this.callDeepSeek(provider, systemPrompt, userPrompt, base64Images);
81
+ break;
82
+ case 'Anthropic':
83
+ result = await this.callAnthropic(provider, systemPrompt, userPrompt, base64Images);
84
+ break;
85
+ case 'Google':
86
+ result = await this.callGoogle(provider, systemPrompt, userPrompt, base64Images);
87
+ break;
88
+ default:
89
+ throw new Error('Unknown provider');
90
+ }
91
+
92
+ // Validate and enhance result
93
+ return this.validateAndEnhanceResult(result, filesToAnalyze.length);
94
+
95
+ } catch (error) {
96
+ console.error(`${provider.name} Analysis Error:`, error);
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ buildSystemPrompt(depth) {
102
+ const basePrompt = `You are an expert vinyl record identifier and grader with 20+ years experience in record collecting and sales.
103
+
104
+ Your task is to analyze record photos and extract ALL identifiable information with maximum accuracy.
105
+
106
+ Return ONLY a JSON object with this exact structure:`;
107
+
108
+ const structures = {
109
+ quick: `{
110
+ "artist": "string or null",
111
+ "title": "string or null",
112
+ "catalogueNumber": "string or null",
113
+ "label": "string or null",
114
+ "year": "number or null",
115
+ "confidence": "high|medium|low"
116
+ }`,
117
+
118
+ standard: `{
119
+ "artist": "string or null",
120
+ "title": "string or null",
121
+ "catalogueNumber": "string or null",
122
+ "label": "string or null",
123
+ "year": "number or null",
124
+ "country": "string or null",
125
+ "format": "string or null (LP, 12\", 7\", 10\")",
126
+ "genre": "string or null",
127
+ "style": "string or null",
128
+ "conditionEstimate": {
129
+ "vinyl": "M|NM|VG+|VG|G+|G|F|P",
130
+ "sleeve": "M|NM|VG+|VG|G+|G|F|P",
131
+ "confidence": "high|medium|low",
132
+ "notes": "specific observations about condition"
133
+ },
134
+ "pressingInfo": {
135
+ "matrixA": "string or null",
136
+ "matrixB": "string or null",
137
+ "stampers": "string or null",
138
+ "mother": "string or null",
139
+ "notes": "other pressing identifiers"
140
+ },
141
+ "identifiers": {
142
+ "barcode": "string or null",
143
+ "runout": "string or null",
144
+ "labelCode": "string or null"
145
+ },
146
+ "visualFeatures": {
147
+ "gatefold": boolean,
148
+ "innerSleeve": boolean,
149
+ "inserts": boolean,
150
+ "poster": boolean,
151
+ "obi": boolean,
152
+ "hypeSticker": boolean,
153
+ "shrinkWrap": boolean,
154
+ "sealed": boolean
155
+ },
156
+ "confidence": "high|medium|low",
157
+ "notes": ["array of observation strings"]
158
+ }`,
159
+
160
+ deep: `{
161
+ "artist": "string or null - full artist name as shown",
162
+ "title": "string or null - full album title",
163
+ "fullTitle": "string - complete title including subtitles",
164
+ "catalogueNumber": "string or null - exactly as printed",
165
+ "label": "string or null - record label name",
166
+ "labelCode": "string or null - LC code if visible",
167
+ "year": "number or null - copyright or release year",
168
+ "country": "string or null - manufacturing country",
169
+ "format": "string or null - LP/12\"/7\"/10\" etc",
170
+ "speed": "33|45|78|null",
171
+ "genre": ["array of genres"],
172
+ "style": ["array of styles"],
173
+ "conditionEstimate": {
174
+ "vinyl": "M|NM|VG+|VG|G+|G|F|P",
175
+ "sleeve": "M|NM|VG+|VG|G+|G|F|P",
176
+ "overall": "M|NM|VG+|VG|G+|G|F|P",
177
+ "confidence": "high|medium|low",
178
+ "surfaceNoise": "none|minimal|light|moderate|heavy",
179
+ "visualDefects": ["list of visible issues"],
180
+ "playGradeEstimate": "string description"
181
+ },
182
+ "pressingInfo": {
183
+ "matrixA": "complete side A matrix/runout",
184
+ "matrixB": "complete side B matrix/runout",
185
+ "stampers": "stamper codes if visible",
186
+ "mother": "mother codes",
187
+ "father": "father codes",
188
+ "lacquerCut": "cutting engineer initials",
189
+ "pressingPlant": "identified plant if possible",
190
+ "notes": "any other pressing details"
191
+ },
192
+ "identifiers": {
193
+ "barcode": "UPC/EAN if visible",
194
+ "runout": "full runout etchings",
195
+ "labelCode": "LC-xxxx",
196
+ "rightsSociety": "BIEM, GEMA, ASCAP etc",
197
+ "priceCode": "inner sleeve price code"
198
+ },
199
+ "tracklist": {
200
+ "sideA": ["inferred or visible tracks"],
201
+ "sideB": ["inferred or visible tracks"],
202
+ "totalTracks": number
203
+ },
204
+ "visualFeatures": {
205
+ "gatefold": boolean,
206
+ "booklet": boolean,
207
+ "innerSleeve": {
208
+ "present": boolean,
209
+ "original": boolean,
210
+ "printed": boolean,
211
+ "condition": "description"
212
+ },
213
+ "inserts": [{
214
+ "type": "lyric sheet/poster/etc",
215
+ "condition": "description"
216
+ }],
217
+ "poster": {
218
+ "present": boolean,
219
+ "size": "description",
220
+ "condition": "description"
221
+ },
222
+ "obi": {
223
+ "present": boolean,
224
+ "condition": "description",
225
+ "text": "transliterated if Japanese"
226
+ },
227
+ "hypeSticker": {
228
+ "present": boolean,
229
+ "text": "full sticker text"
230
+ },
231
+ "shrinkWrap": boolean,
232
+ "sealed": boolean,
233
+ "promo": boolean,
234
+ "whiteLabel": boolean,
235
+ "testPressing": boolean
236
+ },
237
+ "photoAnalysis": {
238
+ "shotTypes": ["detected photo types: front/back/label_a/label_b/deadwax/etc"],
239
+ "missingShots": ["recommended additional photos"],
240
+ "quality": "assessment of photo clarity"
241
+ },
242
+ "confidence": "high|medium|low",
243
+ "reasoning": "brief explanation of identification process",
244
+ "notes": ["detailed observations about each photo"]
245
+ }`
246
+ };
247
+
248
+ const instructions = {
249
+ quick: "Be quick but accurate. Only include clearly readable information.",
250
+ standard: "Be thorough. Examine all text carefully. For condition, look for scratches, scuffs, ring wear, seam splits. For pressing info, read deadwax/matrix carefully.",
251
+ deep: "Examine every detail meticulously. Read all small print. Identify press variants by matrix differences. Note all visual features. Assess condition from multiple angles."
252
+ };
253
+
254
+ return `${basePrompt}
255
+
256
+ ${structures[depth]}
257
+
258
+ INSTRUCTIONS:
259
+ ${instructions[depth]}
260
+
261
+ ACCURACY RULES:
262
+ - Only include information you can clearly read from the images
263
+ - If text is blurry or unclear, use null - do not guess
264
+ - For matrix numbers, transcribe EXACTLY as etched including spaces and symbols
265
+ - For condition, be conservative - better to under-grade than over-grade
266
+ - Confidence should reflect your certainty in the primary identification (artist/title)
267
+
268
+ CONDITION GRADING GUIDE:
269
+ M (Mint): Perfect, no flaws, possibly sealed
270
+ NM (Near Mint): Like new, minimal signs of handling
271
+ VG+ (Very Good Plus): Minor wear, light surface marks, clean playback
272
+ VG (Very Good): Visible wear, surface marks, some noise
273
+ G+ (Good Plus): Significant wear, audible defects
274
+ G (Good): Major defects, plays through
275
+ F (Fair): Severe damage, barely playable
276
+ P (Poor): Damaged, collectible only`;
277
+ }
278
+
279
+ buildUserPrompt(files, options) {
280
+ const contexts = {
281
+ collection: "This record is being catalogued for a personal collection. Accurate identification is important for insurance and organization.",
282
+ sale: "This record is being prepared for sale. Accurate grading is critical to avoid returns. Conservative estimates preferred.",
283
+ purchase: "Considering purchasing this record. Need to verify authenticity and assess true condition before buying.",
284
+ research: "Researching this release for pressing variations and historical details."
285
+ };
286
+
287
+ const context = contexts[options.context] || contexts.collection;
288
+
289
+ return `Please analyze these ${files.length} record photo(s).
290
+
291
+ CONTEXT: ${context}
292
+
293
+ ${options.knownInfo ? `KNOWN INFORMATION (verify against photos): ${JSON.stringify(options.knownInfo)}` : ''}
294
+
295
+ Analyze each photo carefully and provide complete identification.`;
296
+ }
297
+
298
+ async callOpenAI(provider, systemPrompt, userPrompt, base64Images) {
299
+ const messages = [
300
+ { role: 'system', content: systemPrompt },
301
+ {
302
+ role: 'user',
303
+ content: [
304
+ { type: 'text', text: userPrompt },
305
+ ...base64Images.map(base64 => ({
306
+ type: 'image_url',
307
+ image_url: {
308
+ url: `data:image/jpeg;base64,${base64}`,
309
+ detail: 'high'
310
+ }
311
+ }))
312
+ ]
313
+ }
314
+ ];
315
+
316
+ const response = await fetch(provider.url, {
317
+ method: 'POST',
318
+ headers: {
319
+ 'Content-Type': 'application/json',
320
+ 'Authorization': `Bearer ${provider.key}`
321
+ },
322
+ body: JSON.stringify({
323
+ model: provider.model,
324
+ messages: messages,
325
+ max_tokens: 4000,
326
+ temperature: 0.1
327
+ })
328
+ });
329
+
330
+ if (!response.ok) {
331
+ const error = await response.json();
332
+ throw new Error(error.error?.message || 'OpenAI request failed');
333
+ }
334
+
335
+ const data = await response.json();
336
+ return this.extractJSON(data.choices[0].message.content);
337
+ }
338
+
339
+ async callDeepSeek(provider, systemPrompt, userPrompt, base64Images) {
340
+ // DeepSeek may have different image handling - adjust as needed
341
+ const messages = [
342
+ { role: 'system', content: systemPrompt },
343
+ {
344
+ role: 'user',
345
+ content: [
346
+ { type: 'text', text: userPrompt },
347
+ ...base64Images.map(base64 => ({
348
+ type: 'image_url',
349
+ image_url: {
350
+ url: `data:image/jpeg;base64,${base64}`,
351
+ detail: 'high'
352
+ }
353
+ }))
354
+ ]
355
+ }
356
+ ];
357
+
358
+ const response = await fetch(provider.url, {
359
+ method: 'POST',
360
+ headers: {
361
+ 'Content-Type': 'application/json',
362
+ 'Authorization': `Bearer ${provider.key}`
363
+ },
364
+ body: JSON.stringify({
365
+ model: provider.model,
366
+ messages: messages,
367
+ max_tokens: 4000,
368
+ temperature: 0.1
369
+ })
370
+ });
371
+
372
+ if (!response.ok) {
373
+ const error = await response.json();
374
+ throw new Error(error.error?.message || 'DeepSeek request failed');
375
+ }
376
+
377
+ const data = await response.json();
378
+ return this.extractJSON(data.choices[0].message.content);
379
+ }
380
+
381
+ async callAnthropic(provider, systemPrompt, userPrompt, base64Images) {
382
+ const content = [
383
+ { type: 'text', text: userPrompt },
384
+ ...base64Images.map(base64 => ({
385
+ type: 'image',
386
+ source: {
387
+ type: 'base64',
388
+ media_type: 'image/jpeg',
389
+ data: base64
390
+ }
391
+ }))
392
+ ];
393
+
394
+ const response = await fetch(provider.url, {
395
+ method: 'POST',
396
+ headers: {
397
+ 'Content-Type': 'application/json',
398
+ 'x-api-key': provider.key,
399
+ 'anthropic-version': '2023-06-01'
400
+ },
401
+ body: JSON.stringify({
402
+ model: provider.model,
403
+ max_tokens: 4000,
404
+ system: systemPrompt,
405
+ messages: [{ role: 'user', content }]
406
+ })
407
+ });
408
+
409
+ if (!response.ok) {
410
+ const error = await response.json();
411
+ throw new Error(error.error?.message || 'Anthropic request failed');
412
+ }
413
+
414
+ const data = await response.json();
415
+ return this.extractJSON(data.content[0].text);
416
+ }
417
+
418
+ async callGoogle(provider, systemPrompt, userPrompt, base64Images) {
419
+ const parts = [
420
+ { text: `${systemPrompt}\n\n${userPrompt}` },
421
+ ...base64Images.map(base64 => ({
422
+ inline_data: {
423
+ mime_type: 'image/jpeg',
424
+ data: base64
425
+ }
426
+ }))
427
+ ];
428
+
429
+ const response = await fetch(`${provider.url}?key=${provider.key}`, {
430
+ method: 'POST',
431
+ headers: {
432
+ 'Content-Type': 'application/json'
433
+ },
434
+ body: JSON.stringify({
435
+ contents: [{ parts }],
436
+ generationConfig: {
437
+ temperature: 0.1,
438
+ maxOutputTokens: 4000
439
+ }
440
+ })
441
+ });
442
+
443
+ if (!response.ok) {
444
+ const error = await response.json();
445
+ throw new Error(error.error?.message || 'Google API request failed');
446
+ }
447
+
448
+ const data = await response.json();
449
+ return this.extractJSON(data.candidates[0].content.parts[0].text);
450
+ }
451
+
452
+ extractJSON(content) {
453
+ // Try to find JSON in various formats
454
+ const patterns = [
455
+ /
components/pricecharting-service.js ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // PriceCharting API Service for comprehensive market data
2
+ class PriceChartingService {
3
+ constructor() {
4
+ this.baseUrl = 'https://www.pricecharting.com/api';
5
+ // Note: PriceCharting requires an API key for full access
6
+ // Free tier allows limited requests per day
7
+ }
8
+
9
+ async searchGame(title, console) {
10
+ try {
11
+ const response = await fetch(`${this.baseUrl}/product?t=${encodeURIComponent(title)}&c=${encodeURIComponent(console)}`);
12
+ if (!response.ok) return null;
13
+ return await response.json();
14
+ } catch (e) {
15
+ console.error('PriceCharting search failed:', e);
16
+ return null;
17
+ }
18
+ }
19
+
20
+ // Fallback: Use web scraping approach via CORS proxy for vinyl data
21
+ async searchVinyl(artist, title, catalogNumber) {
22
+ // Build search query
23
+ const query = encodeURIComponent(`${artist} ${title} ${catalogNumber || ''} vinyl`.trim());
24
+
25
+ try {
26
+ // Try multiple data sources in parallel
27
+ const [discogsData, ebayData, musicBrainzData] = await Promise.allSettled([
28
+ this.fetchDiscogsMarketData(artist, title, catalogNumber),
29
+ this.fetchEbaySoldData(artist, title, catalogNumber),
30
+ this.fetchMusicBrainzData(artist, title, catalogNumber)
31
+ ]);
32
+
33
+ return this.aggregateMarketData({
34
+ discogs: discogsData.status === 'fulfilled' ? discogsData.value : null,
35
+ ebay: ebayData.status === 'fulfilled' ? ebayData.value : null,
36
+ musicBrainz: musicBrainzData.status === 'fulfilled' ? musicBrainzData.value : null
37
+ });
38
+ } catch (e) {
39
+ console.error('Market data aggregation failed:', e);
40
+ return null;
41
+ }
42
+ }
43
+
44
+ async fetchDiscogsMarketData(artist, title, catalogNumber) {
45
+ if (!window.discogsService?.key) return null;
46
+
47
+ try {
48
+ // Search for release
49
+ let query = `${artist} ${title}`;
50
+ if (catalogNumber) query += ` ${catalogNumber}`;
51
+
52
+ const searchResponse = await fetch(
53
+ `https://api.discogs.com/database/search?q=${encodeURIComponent(query)}&type=release&per_page=10`,
54
+ {
55
+ headers: {
56
+ 'Authorization': `Discogs key=${window.discogsService.key}, secret=${window.discogsService.secret}`,
57
+ 'User-Agent': 'VinylVaultPro/1.0'
58
+ }
59
+ }
60
+ );
61
+
62
+ if (!searchResponse.ok) return null;
63
+ const searchData = await searchResponse.json();
64
+
65
+ if (!searchData.results?.length) return null;
66
+
67
+ // Get detailed info for top match
68
+ const topMatch = searchData.results[0];
69
+ const detailsResponse = await fetch(
70
+ `https://api.discogs.com/releases/${topMatch.id}`,
71
+ {
72
+ headers: {
73
+ 'Authorization': `Discogs key=${window.discogsService.key}, secret=${window.discogsService.secret}`,
74
+ 'User-Agent': 'VinylVaultPro/1.0'
75
+ }
76
+ }
77
+ );
78
+
79
+ if (!detailsResponse.ok) return null;
80
+ const details = await detailsResponse.json();
81
+
82
+ // Fetch marketplace listings for pricing
83
+ const marketplaceResponse = await fetch(
84
+ `https://api.discogs.com/marketplace/listings?release_id=${topMatch.id}&per_page=20&sort=price&sort_order=asc`,
85
+ {
86
+ headers: {
87
+ 'Authorization': `Discogs key=${window.discogsService.key}, secret=${window.discogsService.secret}`,
88
+ 'User-Agent': 'VinylVaultPro/1.0'
89
+ }
90
+ }
91
+ );
92
+
93
+ let marketplaceData = null;
94
+ if (marketplaceResponse.ok) {
95
+ marketplaceData = await marketplaceResponse.json();
96
+ }
97
+
98
+ // Calculate statistics from listings
99
+ const prices = marketplaceData?.listings
100
+ ?.filter(l => l.original_price?.value)
101
+ ?.map(l => parseFloat(l.original_price.value)) || [];
102
+
103
+ return {
104
+ source: 'discogs',
105
+ releaseId: topMatch.id,
106
+ uri: details.uri,
107
+ title: details.title,
108
+ year: details.year,
109
+ country: details.country,
110
+ label: details.labels?.[0]?.name,
111
+ format: details.formats?.[0]?.name,
112
+ genre: details.genres?.[0],
113
+ style: details.styles?.[0],
114
+ tracklist: details.tracklist?.map(t => ({ position: t.position, title: t.title, duration: t.duration })),
115
+ images: details.images?.map(i => ({ uri: i.uri, type: i.type })),
116
+ community: {
117
+ have: details.community?.have,
118
+ want: details.community?.want,
119
+ rating: details.community?.rating?.average,
120
+ votes: details.community?.rating?.count
121
+ },
122
+ marketplace: {
123
+ lowestPrice: prices.length > 0 ? Math.min(...prices) : null,
124
+ highestPrice: prices.length > 0 ? Math.max(...prices) : null,
125
+ medianPrice: prices.length > 0 ? this.calculateMedian(prices) : null,
126
+ averagePrice: prices.length > 0 ? prices.reduce((a, b) => a + b, 0) / prices.length : null,
127
+ listingCount: prices.length,
128
+ priceDistribution: this.calculateDistribution(prices)
129
+ },
130
+ identifiers: details.identifiers?.map(i => ({ type: i.type, value: i.value })),
131
+ companies: details.companies?.map(c => ({ name: c.name, entity_type: c.entity_type_name }))
132
+ };
133
+ } catch (e) {
134
+ console.error('Discogs market data fetch failed:', e);
135
+ return null;
136
+ }
137
+ }
138
+
139
+ async fetchEbaySoldData(artist, title, catalogNumber) {
140
+ // Since we can't directly scrape eBay without CORS issues,
141
+ // we'll use a simulated approach based on Discogs data patterns
142
+ // In production, you'd use eBay's Partner Network API or Terapeak
143
+
144
+ try {
145
+ // Check if we have cached eBay data in localStorage
146
+ const cacheKey = `ebay_${artist}_${title}_${catalogNumber}`.replace(/[^a-z0-9]/gi, '_');
147
+ const cached = localStorage.getItem(cacheKey);
148
+
149
+ if (cached) {
150
+ const parsed = JSON.parse(cached);
151
+ // Cache valid for 24 hours
152
+ if (Date.now() - parsed.timestamp < 86400000) {
153
+ return parsed.data;
154
+ }
155
+ }
156
+
157
+ // Return placeholder structure - real implementation would need eBay API
158
+ return {
159
+ source: 'ebay_estimated',
160
+ note: 'Connect eBay API for real sold data',
161
+ estimatedSoldRange: { low: null, high: null, median: null }
162
+ };
163
+ } catch (e) {
164
+ return null;
165
+ }
166
+ }
167
+
168
+ async fetchMusicBrainzData(artist, title, catalogNumber) {
169
+ try {
170
+ const query = encodeURIComponent(`artist:${artist} AND recording:${title}`);
171
+ const response = await fetch(
172
+ `https://musicbrainz.org/ws/2/release/?query=${query}&fmt=json&limit=5`,
173
+ {
174
+ headers: {
175
+ 'User-Agent': 'VinylVaultPro/1.0 (contact@example.com)'
176
+ }
177
+ }
178
+ );
179
+
180
+ if (!response.ok) return null;
181
+ const data = await response.json();
182
+
183
+ if (!data.releases?.length) return null;
184
+
185
+ const release = data.releases[0];
186
+
187
+ // Fetch detailed release info
188
+ const detailResponse = await fetch(
189
+ `https://musicbrainz.org/ws/2/release/${release.id}?inc=artists+labels+recordings&fmt=json`,
190
+ {
191
+ headers: {
192
+ 'User-Agent': 'VinylVaultPro/1.0 (contact@example.com)'
193
+ }
194
+ }
195
+ );
196
+
197
+ let details = null;
198
+ if (detailResponse.ok) {
199
+ details = await detailResponse.json();
200
+ }
201
+
202
+ return {
203
+ source: 'musicbrainz',
204
+ id: release.id,
205
+ title: release.title,
206
+ date: release.date,
207
+ country: release.country,
208
+ barcode: release.barcode,
209
+ status: release.status,
210
+ quality: release.quality,
211
+ packaging: release.packaging,
212
+ label: release['label-info']?.[0]?.label?.name,
213
+ catalogNumber: release['label-info']?.[0]?.['catalog-number'],
214
+ trackCount: details?.media?.[0]?.tracks?.length,
215
+ formats: details?.media?.map(m => m.format)
216
+ };
217
+ } catch (e) {
218
+ console.error('MusicBrainz fetch failed:', e);
219
+ return null;
220
+ }
221
+ }
222
+
223
+ aggregateMarketData(sources) {
224
+ const result = {
225
+ sources: [],
226
+ confidence: 'low',
227
+ unified: {}
228
+ };
229
+
230
+ // Aggregate basic info with priority: Discogs > MusicBrainz > eBay
231
+ if (sources.discogs) {
232
+ result.sources.push('discogs');
233
+ result.unified = {
234
+ ...result.unified,
235
+ artist: sources.discogs.title?.split(' - ')[0],
236
+ title: sources.discogs.title?.split(' - ')[1] || sources.discogs.title,
237
+ year: sources.discogs.year,
238
+ country: sources.discogs.country,
239
+ label: sources.discogs.label,
240
+ format: sources.discogs.format,
241
+ genre: sources.discogs.genre,
242
+ style: sources.discogs.style,
243
+ tracklist: sources.discogs.tracklist,
244
+ images: sources.discogs.images,
245
+ communityData: sources.discogs.community,
246
+ identifiers: sources.discogs.identifiers
247
+ };
248
+
249
+ // Pricing from Discogs marketplace
250
+ if (sources.discogs.marketplace?.listingCount > 0) {
251
+ result.pricing = {
252
+ source: 'discogs_marketplace',
253
+ currency: 'USD', // Discogs uses USD by default
254
+ currentListings: {
255
+ lowest: sources.discogs.marketplace.lowestPrice,
256
+ highest: sources.discogs.marketplace.highestPrice,
257
+ median: sources.discogs.marketplace.medianPrice,
258
+ average: sources.discogs.marketplace.averagePrice,
259
+ count: sources.discogs.marketplace.listingCount
260
+ },
261
+ distribution: sources.discogs.marketplace.priceDistribution
262
+ };
263
+
264
+ // Estimate sold prices (typically 15-25% below asking)
265
+ const discountFactor = 0.8; // Assume 20% negotiation
266
+ result.pricing.estimatedSold = {
267
+ low: result.pricing.currentListings.lowest * discountFactor,
268
+ median: result.pricing.currentListings.median * discountFactor,
269
+ high: result.pricing.currentListings.highest * discountFactor
270
+ };
271
+ }
272
+ }
273
+
274
+ if (sources.musicBrainz) {
275
+ result.sources.push('musicbrainz');
276
+ // Fill gaps with MusicBrainz data
277
+ if (!result.unified.barcode && sources.musicBrainz.barcode) {
278
+ result.unified.barcode = sources.musicBrainz.barcode;
279
+ }
280
+ if (!result.unified.catalogNumber && sources.musicBrainz.catalogNumber) {
281
+ result.unified.catalogNumber = sources.musicBrainz.catalogNumber;
282
+ }
283
+ }
284
+
285
+ if (sources.ebay) {
286
+ result.sources.push('ebay');
287
+ }
288
+
289
+ // Calculate confidence based on data richness
290
+ if (result.sources.includes('discogs') && result.pricing?.currentListings?.count > 5) {
291
+ result.confidence = 'high';
292
+ } else if (result.sources.includes('discogs')) {
293
+ result.confidence = 'medium';
294
+ }
295
+
296
+ return result;
297
+ }
298
+
299
+ calculateMedian(values) {
300
+ if (!values.length) return null;
301
+ const sorted = [...values].sort((a, b) => a - b);
302
+ const mid = Math.floor(sorted.length / 2);
303
+ return sorted.length % 2 !== 0
304
+ ? sorted[mid]
305
+ : (sorted[mid - 1] + sorted[mid]) / 2;
306
+ }
307
+
308
+ calculateDistribution(prices) {
309
+ if (!prices.length) return null;
310
+
311
+ const min = Math.min(...prices);
312
+ const max = Math.max(...prices);
313
+ const range = max - min;
314
+
315
+ if (range === 0) return { singlePrice: min };
316
+
317
+ // Create 5 buckets
318
+ const buckets = [];
319
+ for (let i = 0; i < 5; i++) {
320
+ const bucketMin = min + (range * i / 5);
321
+ const bucketMax = min + (range * (i + 1) / 5);
322
+ const count = prices.filter(p => p >= bucketMin && p < bucketMax).length;
323
+ buckets.push({
324
+ range: `£${bucketMin.toFixed(2)} - £${bucketMax.toFixed(2)}`,
325
+ count,
326
+ percentage: (count / prices.length * 100).toFixed(1)
327
+ });
328
+ }
329
+
330
+ return { min, max, range, buckets };
331
+ }
332
+
333
+ // Advanced valuation algorithm
334
+ calculateValuation(recordData, marketData, userCondition) {
335
+ if (!marketData?.pricing?.estimatedSold) {
336
+ return null;
337
+ }
338
+
339
+ const baseValue = marketData.pricing.estimatedSold.median;
340
+
341
+ // Condition multipliers (industry standard approximations)
342
+ const conditionMultipliers = {
343
+ 'M': 1.5, // Mint - sealed or perfect
344
+ 'NM': 1.3, // Near Mint - played once or twice
345
+ 'VG+': 1.0, // Very Good Plus - our baseline
346
+ 'VG': 0.7, // Very Good - visible wear, plays well
347
+ 'G+': 0.5, // Good Plus - significant wear
348
+ 'G': 0.35, // Good - major defects
349
+ 'F': 0.2, // Fair - barely playable
350
+ 'P': 0.1 // Poor - mostly for rare items only
351
+ };
352
+
353
+ const vinylMult = conditionMultipliers[userCondition.vinyl] || 0.7;
354
+ const sleeveMult = conditionMultipliers[userCondition.sleeve] || 0.7;
355
+
356
+ // Weighted condition adjustment (vinyl matters more than sleeve)
357
+ const conditionAdjust = (vinylMult * 0.65) + (sleeveMult * 0.35);
358
+
359
+ // Demand factor from community data
360
+ let demandFactor = 1.0;
361
+ if (marketData.unified.communityData) {
362
+ const have = marketData.unified.communityData.have || 1;
363
+ const want = marketData.unified.communityData.want || 0;
364
+ const ratio = want / have;
365
+
366
+ if (ratio > 2) demandFactor = 1.3; // High demand
367
+ else if (ratio > 1) demandFactor = 1.15; // Above average
368
+ else if (ratio < 0.3) demandFactor = 0.85; // Low demand
369
+ }
370
+
371
+ // Rarity estimation based on listing count
372
+ let rarityFactor = 1.0;
373
+ const listingCount = marketData.pricing?.currentListings?.count;
374
+ if (listingCount !== undefined) {
375
+ if (listingCount < 3) rarityFactor = 1.4; // Very rare
376
+ else if (listingCount < 10) rarityFactor = 1.2; // Uncommon
377
+ else if (listingCount > 50) rarityFactor = 0.9; // Common
378
+ }
379
+
380
+ // Year premium for vintage
381
+ let vintageFactor = 1.0;
382
+ const year = marketData.unified.year;
383
+ if (year && year < 1980) vintageFactor = 1.2;
384
+ else if (year && year < 1990) vintageFactor = 1.1;
385
+
386
+ // Calculate final valuation
387
+ const adjustedValue = baseValue * conditionAdjust * demandFactor * rarityFactor * vintageFactor;
388
+
389
+ // Calculate confidence interval
390
+ const volatility = marketData.pricing.currentListings
391
+ ? (marketData.pricing.currentListings.highest - marketData.pricing.currentListings.lowest)
392
+ / marketData.pricing.currentListings.median
393
+ : 0.5;
394
+
395
+ const confidenceInterval = {
396
+ low: adjustedValue * (1 - volatility * 0.5),
397
+ high: adjustedValue * (1 + volatility * 0.5)
398
+ };
399
+
400
+ return {
401
+ estimatedValue: Math.round(adjustedValue),
402
+ confidenceInterval: {
403
+ low: Math.round(confidenceInterval.low),
404
+ high: Math.round(confidenceInterval.high)
405
+ },
406
+ factors: {
407
+ condition: conditionAdjust,
408
+ demand: demandFactor,
409
+ rarity: rarityFactor,
410
+ vintage: vintageFactor
411
+ },
412
+ methodology: {
413
+ baseSource: marketData.pricing.source,
414
+ baseValue: Math.round(baseValue),
415
+ confidence: marketData.confidence
416
+ }
417
+ };
418
+ }
419
+ }
420
+
421
+ window.priceChartingService = new PriceChartingService();
index.html CHANGED
@@ -368,8 +368,10 @@
368
  <script src="components/vinyl-footer.js"></script>
369
  <script src="components/stat-card.js"></script>
370
  <script src="components/ocr-service.js"></script>
 
371
  <script src="components/discogs-service.js"></script>
372
  <script src="components/deepseek-service.js"></script>
 
373
 
374
  <!-- Main Script -->
375
  <script src="script.js"></script>
 
368
  <script src="components/vinyl-footer.js"></script>
369
  <script src="components/stat-card.js"></script>
370
  <script src="components/ocr-service.js"></script>
371
+ <script src="components/enhanced-ocr-service.js"></script>
372
  <script src="components/discogs-service.js"></script>
373
  <script src="components/deepseek-service.js"></script>
374
+ <script src="components/pricecharting-service.js"></script>
375
 
376
  <!-- Main Script -->
377
  <script src="script.js"></script>
script.js CHANGED
@@ -333,7 +333,6 @@ function stopAnalysisProgress() {
333
  }
334
  updateAnalysisProgress('Complete!', 100);
335
  }
336
-
337
  async function analyzePhotosWithOCR() {
338
  const spinner = document.getElementById('uploadSpinner');
339
  const dropZone = document.getElementById('dropZone');
@@ -345,67 +344,422 @@ async function analyzePhotosWithOCR() {
345
  // Start progress simulation
346
  startAnalysisProgressSimulation();
347
 
348
- // Determine which AI service to use
349
- const provider = localStorage.getItem('ai_provider') || 'openai';
350
- const service = getAIService();
351
-
352
- // Update API keys
353
- if (provider === 'openai') {
354
- const apiKey = localStorage.getItem('openai_api_key');
355
- if (!apiKey) throw new Error('OpenAI API key not configured');
356
- window.ocrService.updateApiKey(apiKey);
357
- } else {
358
- const apiKey = localStorage.getItem('deepseek_api_key');
359
- if (!apiKey) throw new Error('DeepSeek API key not configured');
360
- window.deepseekService.updateApiKey(apiKey);
361
- window.deepseekService.updateModel(localStorage.getItem('deepseek_model') || 'deepseek-chat');
362
- }
363
-
364
- const result = await service.analyzeRecordImages(uploadedPhotos);
365
 
366
  // Complete the progress bar
367
  stopAnalysisProgress();
368
- populateFieldsFromOCR(result);
369
 
370
- // Try to fetch additional data from Discogs if available
371
- if (result.artist && result.title && window.discogsService) {
372
- try {
373
- const discogsData = await window.discogsService.searchRelease(
374
- result.artist,
375
- result.title,
376
- result.catalogueNumber
377
- );
378
- if (discogsData) {
379
- populateFieldsFromDiscogs(discogsData);
380
- }
381
- } catch (e) {
382
- console.log('Discogs lookup failed:', e);
383
- }
384
- }
385
 
386
- const confidenceMsg = result.confidence === 'high' ? 'Record identified!' :
387
- result.confidence === 'medium' ? 'Record found (verify details)' :
388
- 'Partial match found';
 
 
 
389
  showToast(confidenceMsg, result.confidence === 'high' ? 'success' : 'warning');
390
  } catch (error) {
391
- console.error('OCR Error:', error);
392
- if (error.message.includes('API key') || error.message.includes('not configured')) {
 
 
 
393
  const provider = localStorage.getItem('ai_provider') || 'openai';
394
- showToast(`Please configure ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'} API key in Settings`, 'error');
395
- } else {
396
- showToast(`Analysis failed: ${error.message}`, 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  }
398
- } finally {
399
  stopAnalysisProgress();
400
- // Small delay to show 100% before hiding
401
  setTimeout(() => {
402
  spinner.classList.add('hidden');
403
  dropZone.classList.remove('pointer-events-none');
404
- // Reset progress for next time
405
  updateAnalysisProgress('Initializing...', 0);
406
  }, 300);
407
  }
408
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  function populateFieldsFromDiscogs(discogsData) {
410
  if (!discogsData) return;
411
 
 
333
  }
334
  updateAnalysisProgress('Complete!', 100);
335
  }
 
336
  async function analyzePhotosWithOCR() {
337
  const spinner = document.getElementById('uploadSpinner');
338
  const dropZone = document.getElementById('dropZone');
 
344
  // Start progress simulation
345
  startAnalysisProgressSimulation();
346
 
347
+ // Use enhanced OCR service with deep analysis
348
+ const result = await window.enhancedOCRService.analyzeRecordImages(uploadedPhotos, {
349
+ depth: 'deep',
350
+ context: 'sale',
351
+ maxImages: 6
352
+ });
 
 
 
 
 
 
 
 
 
 
 
353
 
354
  // Complete the progress bar
355
  stopAnalysisProgress();
 
356
 
357
+ // Populate form fields with enhanced data
358
+ populateFieldsFromEnhancedOCR(result);
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
+ // Fetch comprehensive market data
361
+ await fetchComprehensiveMarketData(result);
362
+
363
+ const confidenceMsg = result.confidence === 'high' ? 'Record fully identified with market data!' :
364
+ result.confidence === 'medium' ? 'Record identified - verify details & pricing' :
365
+ 'Partial identification - manual review needed';
366
  showToast(confidenceMsg, result.confidence === 'high' ? 'success' : 'warning');
367
  } catch (error) {
368
+ console.error('Enhanced OCR Error:', error);
369
+
370
+ // Fallback to basic OCR service
371
+ try {
372
+ showToast('Trying fallback identification...', 'warning');
373
  const provider = localStorage.getItem('ai_provider') || 'openai';
374
+ const service = getAIService();
375
+
376
+ if (provider === 'openai') {
377
+ const apiKey = localStorage.getItem('openai_api_key');
378
+ if (apiKey) window.ocrService.updateApiKey(apiKey);
379
+ } else {
380
+ const apiKey = localStorage.getItem('deepseek_api_key');
381
+ if (apiKey) {
382
+ window.deepseekService.updateApiKey(apiKey);
383
+ window.deepseekService.updateModel(localStorage.getItem('deepseek_model') || 'deepseek-chat');
384
+ }
385
+ }
386
+
387
+ const result = await service.analyzeRecordImages(uploadedPhotos);
388
+ populateFieldsFromOCR(result);
389
+ showToast('Basic identification complete', 'success');
390
+ } catch (fallbackError) {
391
+ if (error.message.includes('API key') || error.message.includes('not configured')) {
392
+ showToast('Please configure an AI API key in Settings', 'error');
393
+ } else {
394
+ showToast(`Analysis failed: ${error.message}`, 'error');
395
+ }
396
  }
397
+ } finally {
398
  stopAnalysisProgress();
 
399
  setTimeout(() => {
400
  spinner.classList.add('hidden');
401
  dropZone.classList.remove('pointer-events-none');
 
402
  updateAnalysisProgress('Initializing...', 0);
403
  }, 300);
404
  }
405
  }
406
+
407
+ async function fetchComprehensiveMarketData(ocrResult) {
408
+ if (!ocrResult.artist || !ocrResult.title) return;
409
+
410
+ showToast('Fetching market data...', 'success');
411
+
412
+ try {
413
+ // Use PriceCharting service for comprehensive market data
414
+ const marketData = await window.priceChartingService.searchVinyl(
415
+ ocrResult.artist,
416
+ ocrResult.title,
417
+ ocrResult.catalogueNumber
418
+ );
419
+
420
+ if (marketData && marketData.confidence !== 'low') {
421
+ populateMarketData(marketData, ocrResult);
422
+ } else {
423
+ // Fallback to basic Discogs lookup
424
+ if (window.discogsService?.key && ocrResult.artist && ocrResult.title) {
425
+ const discogsData = await window.discogsService.searchRelease(
426
+ ocrResult.artist,
427
+ ocrResult.title,
428
+ ocrResult.catalogueNumber
429
+ );
430
+ if (discogsData) {
431
+ populateFieldsFromDiscogs(discogsData);
432
+ }
433
+ }
434
+ }
435
+ } catch (e) {
436
+ console.log('Market data fetch failed:', e);
437
+ }
438
+ }
439
+
440
+ function populateFieldsFromEnhancedOCR(data) {
441
+ // Basic fields
442
+ const fields = {
443
+ 'artistInput': data.artist,
444
+ 'titleInput': data.title,
445
+ 'catInput': data.catalogueNumber || data.identifiers?.labelCode,
446
+ 'yearInput': data.year
447
+ };
448
+
449
+ let populatedCount = 0;
450
+
451
+ Object.entries(fields).forEach(([fieldId, value]) => {
452
+ const field = document.getElementById(fieldId);
453
+ if (field && value && value !== 'null' && value !== 'undefined') {
454
+ if (!field.value || field.dataset.userModified !== 'true') {
455
+ field.value = value;
456
+ field.classList.add('border-green-500', 'bg-green-500/10');
457
+ setTimeout(() => {
458
+ field.classList.remove('border-green-500', 'bg-green-500/10');
459
+ }, 3000);
460
+ populatedCount++;
461
+ }
462
+ }
463
+ });
464
+
465
+ // Set condition dropdowns if detected
466
+ if (data.conditionEstimate?.vinyl) {
467
+ const vinylSelect = document.getElementById('vinylConditionInput');
468
+ if (vinylSelect && !vinylSelect.dataset.userModified) {
469
+ const grades = ['M', 'NM', 'VG+', 'VG', 'G+', 'G', 'F', 'P'];
470
+ const detected = data.conditionEstimate.vinyl.toUpperCase();
471
+ if (grades.includes(detected)) {
472
+ vinylSelect.value = detected;
473
+ }
474
+ }
475
+ }
476
+
477
+ if (data.conditionEstimate?.sleeve) {
478
+ const sleeveSelect = document.getElementById('sleeveConditionInput');
479
+ if (sleeveSelect && !sleeveSelect.dataset.userModified) {
480
+ const grades = ['M', 'NM', 'VG+', 'VG', 'G+', 'G', 'F', 'P'];
481
+ const detected = data.conditionEstimate.sleeve.toUpperCase();
482
+ if (grades.includes(detected)) {
483
+ sleeveSelect.value = detected;
484
+ }
485
+ }
486
+ }
487
+
488
+ // Store all detected data globally
489
+ window.detectedData = data;
490
+ window.detectedLabel = data.label;
491
+ window.detectedCountry = data.country;
492
+ window.detectedFormat = data.format;
493
+ window.detectedGenre = Array.isArray(data.genre) ? data.genre[0] : data.genre;
494
+ window.detectedStyle = Array.isArray(data.style) ? data.style[0] : data.style;
495
+ window.detectedCondition = data.conditionEstimate;
496
+ window.detectedPressingInfo = data.pressingInfo;
497
+ window.detectedVisualFeatures = data.visualFeatures;
498
+ window.detectedMatrix = data.matrixNotes || data.pressingInfo?.matrixA;
499
+
500
+ // Update UI with comprehensive info
501
+ updateEnhancedDetectedPanel(data);
502
+
503
+ // Scroll to details
504
+ if (populatedCount > 0) {
505
+ const quickDetailsSection = document.querySelector('.md\\\\:w-80');
506
+ if (quickDetailsSection) {
507
+ setTimeout(() => {
508
+ quickDetailsSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
509
+ }, 100);
510
+ }
511
+ }
512
+ }
513
+
514
+ function populateMarketData(marketData, ocrData) {
515
+ if (!marketData?.unified) return;
516
+
517
+ const unified = marketData.unified;
518
+ const pricing = marketData.pricing;
519
+
520
+ // Update any missing fields from market data
521
+ const fields = {
522
+ 'artistInput': unified.artist,
523
+ 'titleInput': unified.title,
524
+ 'catInput': unified.catalogNumber || unified.catalogueNumber,
525
+ 'yearInput': unified.year
526
+ };
527
+
528
+ Object.entries(fields).forEach(([fieldId, value]) => {
529
+ const field = document.getElementById(fieldId);
530
+ if (field && value && (!field.value || field.value === '[Verify]')) {
531
+ field.value = value;
532
+ field.classList.add('border-blue-500', 'bg-blue-500/10');
533
+ setTimeout(() => {
534
+ field.classList.remove('border-blue-500', 'bg-blue-500/10');
535
+ }, 3000);
536
+ }
537
+ });
538
+
539
+ // Store market pricing for later use
540
+ window.marketPricing = pricing;
541
+ window.marketConfidence = marketData.confidence;
542
+
543
+ // Show market data panel
544
+ updateMarketDataPanel(marketData, ocrData);
545
+ }
546
+
547
+ function updateEnhancedDetectedPanel(data) {
548
+ let panel = document.getElementById('detectedInfoPanel');
549
+ const parent = document.querySelector('#dropZone')?.parentNode;
550
+ if (!parent) return;
551
+
552
+ if (!panel) {
553
+ panel = document.createElement('div');
554
+ panel.id = 'detectedInfoPanel';
555
+ parent.appendChild(panel);
556
+ }
557
+
558
+ const confidenceColor = data.confidence === 'high' ? 'text-green-400 border-green-500/30 bg-green-500/10' :
559
+ data.confidence === 'medium' ? 'text-yellow-400 border-yellow-500/30 bg-yellow-500/10' :
560
+ 'text-orange-400 border-orange-500/30 bg-orange-500/10';
561
+
562
+ // Build info sections
563
+ const basicInfo = [];
564
+ if (data.label) basicInfo.push(`<span class="text-purple-400">Label:</span> ${data.label}`);
565
+ if (data.country) basicInfo.push(`<span class="text-purple-400">Country:</span> ${data.country}`);
566
+ if (data.format) basicInfo.push(`<span class="text-purple-400">Format:</span> ${data.format}${data.speed ? ` @ ${data.speed}rpm` : ''}`);
567
+ if (data.year) basicInfo.push(`<span class="text-purple-400">Year:</span> ${data.year}`);
568
+
569
+ const genreInfo = [];
570
+ if (Array.isArray(data.genre)) {
571
+ genreInfo.push(`<span class="text-cyan-400">Genre:</span> ${data.genre.join(', ')}`);
572
+ } else if (data.genre) {
573
+ genreInfo.push(`<span class="text-cyan-400">Genre:</span> ${data.genre}`);
574
+ }
575
+ if (Array.isArray(data.style)) {
576
+ genreInfo.push(`<span class="text-cyan-400">Style:</span> ${data.style.join(', ')}`);
577
+ } else if (data.style) {
578
+ genreInfo.push(`<span class="text-cyan-400">Style:</span> ${data.style}`);
579
+ }
580
+
581
+ const conditionInfo = [];
582
+ if (data.conditionEstimate) {
583
+ const ce = data.conditionEstimate;
584
+ conditionInfo.push(`<span class="text-${ce.vinyl === 'NM' || ce.vinyl === 'M' ? 'green' : ce.vinyl === 'VG+' ? 'blue' : 'yellow'}-400">Vinyl:</span> ${ce.vinyl}`);
585
+ conditionInfo.push(`<span class="text-${ce.sleeve === 'NM' || ce.sleeve === 'M' ? 'green' : ce.sleeve === 'VG+' ? 'blue' : 'yellow'}-400">Sleeve:</span> ${ce.sleeve}`);
586
+ if (ce.surfaceNoise) {
587
+ conditionInfo.push(`<span class="text-gray-400">Surface noise:</span> ${ce.surfaceNoise}`);
588
+ }
589
+ }
590
+
591
+ const pressingInfo = [];
592
+ if (data.pressingInfo?.matrixA) pressingInfo.push(`<span class="text-pink-400">Side A:</span> ${data.pressingInfo.matrixA}`);
593
+ if (data.pressingInfo?.matrixB) pressingInfo.push(`<span class="text-pink-400">Side B:</span> ${data.pressingInfo.matrixB}`);
594
+ if (data.pressingInfo?.stampers) pressingInfo.push(`<span class="text-pink-400">Stampers:</span> ${data.pressingInfo.stampers}`);
595
+ if (data.pressingInfo?.lacquerCut) pressingInfo.push(`<span class="text-pink-400">Cut by:</span> ${data.pressingInfo.lacquerCut}`);
596
+
597
+ const features = [];
598
+ if (data.visualFeatures) {
599
+ const vf = data.visualFeatures;
600
+ if (vf.gatefold) features.push('🏛️ Gatefold');
601
+ if (vf.innerSleeve) features.push('📄 Inner Sleeve');
602
+ if (vf.inserts) features.push('📋 Inserts');
603
+ if (vf.poster) features.push('🖼️ Poster');
604
+ if (vf.obi) features.push('🇯🇵 OBI Strip');
605
+ if (vf.hypeSticker) features.push('⭐ Hype Sticker');
606
+ if (vf.shrinkWrap) features.push('📦 Shrink');
607
+ if (vf.sealed) features.push('🔒 Sealed');
608
+ }
609
+
610
+ panel.className = `mt-4 p-4 rounded-lg border ${confidenceColor}`;
611
+
612
+ panel.innerHTML = `
613
+ <div class="flex items-center justify-between mb-3">
614
+ <div class="flex items-center gap-2">
615
+ <i data-feather="cpu" class="w-5 h-5"></i>
616
+ <span class="font-medium">AI Deep Analysis (${data.confidence} confidence)</span>
617
+ </div>
618
+ ${data._meta?.photosAnalyzed ? `<span class="text-xs opacity-70">${data._meta.photosAnalyzed} photos analyzed</span>` : ''}
619
+ </div>
620
+
621
+ ${basicInfo.length ? `
622
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm mb-3">
623
+ ${basicInfo.map(item => `<div class="text-gray-300">${item}</div>`).join('')}
624
+ </div>
625
+ ` : ''}
626
+
627
+ ${genreInfo.length ? `
628
+ <div class="flex flex-wrap gap-4 text-sm mb-3 pb-3 border-b border-current/20">
629
+ ${genreInfo.map(item => `<span class="text-gray-300">${item}</span>`).join('')}
630
+ </div>
631
+ ` : ''}
632
+
633
+ ${conditionInfo.length ? `
634
+ <div class="mb-3 pb-3 border-b border-current/20">
635
+ <p class="text-xs uppercase tracking-wide opacity-70 mb-2">Condition Assessment</p>
636
+ <div class="flex flex-wrap gap-4 text-sm">
637
+ ${conditionInfo.map(item => `<span class="text-gray-300">${item}</span>`).join('')}
638
+ </div>
639
+ ${data.conditionEstimate?.notes ? `<p class="text-xs text-gray-400 mt-2 italic">${data.conditionEstimate.notes}</p>` : ''}
640
+ </div>
641
+ ` : ''}
642
+
643
+ ${pressingInfo.length ? `
644
+ <div class="mb-3 pb-3 border-b border-current/20">
645
+ <p class="text-xs uppercase tracking-wide opacity-70 mb-2">Pressing Information</p>
646
+ <div class="space-y-1 text-sm font-mono">
647
+ ${pressingInfo.map(item => `<div class="text-gray-300">${item}</div>`).join('')}
648
+ </div>
649
+ </div>
650
+ ` : ''}
651
+
652
+ ${features.length ? `
653
+ <div class="mb-3">
654
+ <p class="text-xs uppercase tracking-wide opacity-70 mb-2">Included Features</p>
655
+ <div class="flex flex-wrap gap-2">
656
+ ${features.map(f => `<span class="px-2 py-1 bg-surface rounded text-sm">${f}</span>`).join('')}
657
+ </div>
658
+ </div>
659
+ ` : ''}
660
+
661
+ ${data.notes?.length ? `
662
+ <div class="mt-3 pt-3 border-t border-current/20">
663
+ <p class="text-xs uppercase tracking-wide opacity-70 mb-2">Observations</p>
664
+ <ul class="text-xs text-gray-400 space-y-1 list-disc list-inside">
665
+ ${data.notes.map(n => `<li>${n}</li>`).join('')}
666
+ </ul>
667
+ </div>
668
+ ` : ''}
669
+ `;
670
+
671
+ feather.replace();
672
+ }
673
+
674
+ function updateMarketDataPanel(marketData, ocrData) {
675
+ // Find or create market data panel
676
+ let panel = document.getElementById('marketDataPanel');
677
+ const detectedPanel = document.getElementById('detectedInfoPanel');
678
+
679
+ if (!panel && detectedPanel) {
680
+ panel = document.createElement('div');
681
+ panel.id = 'marketDataPanel';
682
+ detectedPanel.parentNode.insertBefore(panel, detectedPanel.nextSibling);
683
+ }
684
+
685
+ if (!panel) return;
686
+
687
+ const pricing = marketData.pricing;
688
+ const unified = marketData.unified;
689
+
690
+ const confidenceClass = marketData.confidence === 'high' ? 'border-green-500/30 bg-green-500/5' :
691
+ marketData.confidence === 'medium' ? 'border-yellow-500/30 bg-yellow-500/5' :
692
+ 'border-gray-500/30 bg-gray-500/5';
693
+
694
+ let pricingHtml = '';
695
+ if (pricing?.estimatedSold) {
696
+ pricingHtml = `
697
+ <div class="grid grid-cols-3 gap-3 mb-3">
698
+ <div class="text-center p-2 bg-surface rounded">
699
+ <p class="text-xs text-gray-500">Low Sold</p>
700
+ <p class="text-lg font-bold text-red-400">£${Math.round(pricing.estimatedSold.low)}</p>
701
+ </div>
702
+ <div class="text-center p-2 bg-surface rounded border border-primary/30">
703
+ <p class="text-xs text-primary">Median Sold</p>
704
+ <p class="text-lg font-bold text-primary">£${Math.round(pricing.estimatedSold.median)}</p>
705
+ </div>
706
+ <div class="text-center p-2 bg-surface rounded">
707
+ <p class="text-xs text-gray-500">High Sold</p>
708
+ <p class="text-lg font-bold text-green-400">£${Math.round(pricing.estimatedSold.high)}</p>
709
+ </div>
710
+ </div>
711
+ `;
712
+ }
713
+
714
+ let listingsHtml = '';
715
+ if (pricing?.currentListings?.count > 0) {
716
+ listingsHtml = `
717
+ <div class="mb-3">
718
+ <p class="text-xs uppercase tracking-wide text-gray-500 mb-2">Current Market (${pricing.currentListings.count} listings)</p>
719
+ <div class="flex justify-between text-sm">
720
+ <span class="text-gray-400">Range: £${Math.round(pricing.currentListings.lowest)} - £${Math.round(pricing.currentListings.highest)}</span>
721
+ <span class="text-gray-400">Avg: £${Math.round(pricing.currentListings.average || pricing.currentListings.median)}</span>
722
+ </div>
723
+ </div>
724
+ `;
725
+ }
726
+
727
+ let demandHtml = '';
728
+ if (unified.communityData) {
729
+ const cd = unified.communityData;
730
+ const ratio = cd.want && cd.have ? (cd.want / cd.have).toFixed(2) : '?';
731
+ demandHtml = `
732
+ <div class="flex gap-4 text-sm mb-3">
733
+ <span class="text-gray-400"><span class="text-blue-400 font-bold">${cd.have || '?'}</span> have</span>
734
+ <span class="text-gray-400"><span class="text-green-400 font-bold">${cd.want || '?'}</span> want</span>
735
+ <span class="text-gray-400">Ratio: <span class="${ratio > 1 ? 'text-green-400' : 'text-yellow-400'} font-bold">${ratio}</span></span>
736
+ ${cd.rating ? `<span class="text-gray-400">Rating: ⭐ ${cd.rating}/5 (${cd.votes})</span>` : ''}
737
+ </div>
738
+ `;
739
+ }
740
+
741
+ panel.className = `mt-4 p-4 rounded-lg border ${confidenceClass}`;
742
+ panel.innerHTML = `
743
+ <div class="flex items-center gap-2 mb-3">
744
+ <i data-feather="trending-up" class="w-5 h-5 text-primary"></i>
745
+ <span class="font-medium text-gray-200">Market Intelligence</span>
746
+ <span class="ml-auto text-xs px-2 py-1 rounded bg-surface text-gray-400">${marketData.sources.join(' + ')}</span>
747
+ </div>
748
+
749
+ ${pricingHtml}
750
+ ${listingsHtml}
751
+ ${demandHtml}
752
+
753
+ ${unified.tracklist ? `
754
+ <div class="mt-3 pt-3 border-t border-gray-700">
755
+ <p class="text-xs uppercase tracking-wide text-gray-500 mb-2">Tracklist Verified</p>
756
+ <p class="text-xs text-gray-400">${unified.tracklist.length} tracks from ${unified.source || 'database'}</p>
757
+ </div>
758
+ ` : ''}
759
+ `;
760
+
761
+ feather.replace();
762
+ }
763
  function populateFieldsFromDiscogs(discogsData) {
764
  if (!discogsData) return;
765