winebox 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
winebox/static/js/app.js CHANGED
@@ -17,8 +17,23 @@ document.addEventListener('DOMContentLoaded', () => {
17
17
  initModals();
18
18
  initAuth();
19
19
  checkAuth();
20
+ loadAppInfo();
20
21
  });
21
22
 
23
+ // Load app info for footer
24
+ async function loadAppInfo() {
25
+ try {
26
+ const response = await fetch('/health');
27
+ const data = await response.json();
28
+ const appInfo = document.getElementById('app-info');
29
+ if (appInfo && data.app_name && data.version) {
30
+ appInfo.innerHTML = `${data.app_name} <span class="version">v${data.version}</span>`;
31
+ }
32
+ } catch (error) {
33
+ console.log('Could not load app info');
34
+ }
35
+ }
36
+
22
37
  // Authentication
23
38
  function initAuth() {
24
39
  // Login form
@@ -26,6 +41,28 @@ function initAuth() {
26
41
 
27
42
  // Logout button
28
43
  document.getElementById('logout-btn').addEventListener('click', handleLogout);
44
+
45
+ // Password toggle
46
+ const passwordToggle = document.querySelector('.password-toggle');
47
+ if (passwordToggle) {
48
+ passwordToggle.addEventListener('click', function() {
49
+ const passwordInput = document.getElementById('login-password');
50
+ const eyeIcon = this.querySelector('.eye-icon');
51
+ const eyeOffIcon = this.querySelector('.eye-off-icon');
52
+
53
+ if (passwordInput.type === 'password') {
54
+ passwordInput.type = 'text';
55
+ eyeIcon.style.display = 'none';
56
+ eyeOffIcon.style.display = 'block';
57
+ this.setAttribute('aria-label', 'Hide password');
58
+ } else {
59
+ passwordInput.type = 'password';
60
+ eyeIcon.style.display = 'block';
61
+ eyeOffIcon.style.display = 'none';
62
+ this.setAttribute('aria-label', 'Show password');
63
+ }
64
+ });
65
+ }
29
66
  }
30
67
 
31
68
  async function checkAuth() {
@@ -177,6 +214,7 @@ function initForms() {
177
214
  checkinForm.addEventListener('reset', () => {
178
215
  document.getElementById('front-preview').innerHTML = 'Tap to take photo or select image';
179
216
  document.getElementById('back-preview').innerHTML = 'Tap to take photo or select image';
217
+ clearRawLabelText();
180
218
  });
181
219
 
182
220
  // Image previews - make clickable to trigger file input
@@ -187,9 +225,11 @@ function initForms() {
187
225
 
188
226
  frontLabel.addEventListener('change', (e) => {
189
227
  previewImage(e.target, 'front-preview');
228
+ scanLabels();
190
229
  });
191
230
  backLabel.addEventListener('change', (e) => {
192
231
  previewImage(e.target, 'back-preview');
232
+ scanLabels();
193
233
  });
194
234
 
195
235
  // Click on preview to trigger file input
@@ -200,6 +240,25 @@ function initForms() {
200
240
  backLabel.click();
201
241
  });
202
242
 
243
+ // Label text collapsible toggle
244
+ const labelTextToggle = document.getElementById('label-text-toggle');
245
+ if (labelTextToggle) {
246
+ labelTextToggle.addEventListener('click', () => {
247
+ const section = document.getElementById('label-text-section');
248
+ const content = document.getElementById('label-text-content');
249
+ const icon = section.querySelector('.collapse-icon');
250
+
251
+ section.classList.toggle('open');
252
+ if (section.classList.contains('open')) {
253
+ content.style.display = 'block';
254
+ icon.textContent = '-';
255
+ } else {
256
+ content.style.display = 'none';
257
+ icon.textContent = '+';
258
+ }
259
+ });
260
+ }
261
+
203
262
  // Search form
204
263
  document.getElementById('search-form').addEventListener('submit', handleSearch);
205
264
 
@@ -227,6 +286,141 @@ function previewImage(input, previewId) {
227
286
  }
228
287
  }
229
288
 
289
+ async function scanLabels() {
290
+ const frontLabel = document.getElementById('front-label');
291
+
292
+ // Only scan if front label is present
293
+ if (!frontLabel.files || !frontLabel.files[0]) {
294
+ return;
295
+ }
296
+
297
+ const backLabel = document.getElementById('back-label');
298
+ const formData = new FormData();
299
+ formData.append('front_label', frontLabel.files[0]);
300
+
301
+ if (backLabel.files && backLabel.files[0]) {
302
+ formData.append('back_label', backLabel.files[0]);
303
+ }
304
+
305
+ // Show scanning indicator
306
+ showScanningIndicator(true);
307
+
308
+ try {
309
+ const response = await fetchWithAuth(`${API_BASE}/wines/scan`, {
310
+ method: 'POST',
311
+ body: formData
312
+ });
313
+
314
+ if (!response.ok) {
315
+ const error = await response.json();
316
+ throw new Error(error.detail || 'Scan failed');
317
+ }
318
+
319
+ const result = await response.json();
320
+ populateFormFromScan(result);
321
+ const methodName = result.method === 'claude_vision' ? 'Claude Vision' : 'Tesseract OCR';
322
+ showToast(`Label scanned with ${methodName}`, 'success');
323
+ } catch (error) {
324
+ showToast(`Scan failed: ${error.message}`, 'error');
325
+ } finally {
326
+ showScanningIndicator(false);
327
+ }
328
+ }
329
+
330
+ function populateFormFromScan(result) {
331
+ const parsed = result.parsed;
332
+
333
+ // Update fields with scanned values (overwrites previous scan results)
334
+ const fields = {
335
+ 'wine-name': parsed.name,
336
+ 'winery': parsed.winery,
337
+ 'vintage': parsed.vintage,
338
+ 'grape-variety': parsed.grape_variety,
339
+ 'region': parsed.region,
340
+ 'country': parsed.country,
341
+ 'alcohol': parsed.alcohol_percentage
342
+ };
343
+
344
+ for (const [fieldId, value] of Object.entries(fields)) {
345
+ const input = document.getElementById(fieldId);
346
+ if (input && value !== null && value !== undefined) {
347
+ input.value = value;
348
+ // Add visual indicator that field was auto-filled
349
+ input.classList.add('auto-filled');
350
+ setTimeout(() => input.classList.remove('auto-filled'), 2000);
351
+ }
352
+ }
353
+
354
+ // Populate raw label text section
355
+ populateRawLabelText(result.ocr, result.method);
356
+ }
357
+
358
+ function populateRawLabelText(ocr, method) {
359
+ const section = document.getElementById('label-text-section');
360
+ const frontText = document.getElementById('raw-front-label-text');
361
+ const backSection = document.getElementById('raw-back-label-section');
362
+ const backText = document.getElementById('raw-back-label-text');
363
+ const header = section.querySelector('h3');
364
+
365
+ // Update header to show scan method
366
+ const methodName = method === 'claude_vision' ? 'Claude Vision' : 'Tesseract OCR';
367
+ header.innerHTML = `Raw Label Text <span class="scan-method-badge">${methodName}</span>`;
368
+
369
+ if (ocr.front_label_text) {
370
+ frontText.textContent = ocr.front_label_text;
371
+ section.style.display = 'block';
372
+ }
373
+
374
+ if (ocr.back_label_text) {
375
+ backText.textContent = ocr.back_label_text;
376
+ backSection.style.display = 'block';
377
+ } else {
378
+ backSection.style.display = 'none';
379
+ }
380
+ }
381
+
382
+ function clearRawLabelText() {
383
+ const section = document.getElementById('label-text-section');
384
+ const frontText = document.getElementById('raw-front-label-text');
385
+ const backSection = document.getElementById('raw-back-label-section');
386
+ const backText = document.getElementById('raw-back-label-text');
387
+
388
+ section.style.display = 'none';
389
+ section.classList.remove('open');
390
+ document.getElementById('label-text-content').style.display = 'none';
391
+ document.querySelector('#label-text-section .collapse-icon').textContent = '+';
392
+ frontText.textContent = '';
393
+ backText.textContent = '';
394
+ backSection.style.display = 'none';
395
+ }
396
+
397
+ function showScanningIndicator(show) {
398
+ const submitBtn = document.querySelector('#checkin-form button[type="submit"]');
399
+ const formNote = document.querySelector('#checkin-form .form-note');
400
+
401
+ if (show) {
402
+ if (submitBtn) {
403
+ submitBtn.disabled = true;
404
+ submitBtn.dataset.originalText = submitBtn.textContent;
405
+ submitBtn.textContent = 'Scanning...';
406
+ }
407
+ if (formNote) {
408
+ formNote.dataset.originalText = formNote.textContent;
409
+ formNote.textContent = 'Analyzing label with Claude Vision...';
410
+ formNote.classList.add('scanning');
411
+ }
412
+ } else {
413
+ if (submitBtn) {
414
+ submitBtn.disabled = false;
415
+ submitBtn.textContent = submitBtn.dataset.originalText || 'Check In Wine';
416
+ }
417
+ if (formNote) {
418
+ formNote.textContent = formNote.dataset.originalText || 'Leave fields blank to use OCR-detected values';
419
+ formNote.classList.remove('scanning');
420
+ }
421
+ }
422
+ }
423
+
230
424
  async function handleCheckin(e) {
231
425
  e.preventDefault();
232
426
  const form = e.target;
@@ -244,14 +438,74 @@ async function handleCheckin(e) {
244
438
  }
245
439
 
246
440
  const wine = await response.json();
247
- showToast(`Successfully checked in: ${wine.name}`, 'success');
441
+ showCheckinConfirmation(wine);
248
442
  form.reset();
249
- navigateTo('cellar');
250
443
  } catch (error) {
251
444
  showToast(error.message, 'error');
252
445
  }
253
446
  }
254
447
 
448
+ function showCheckinConfirmation(wine) {
449
+ const modal = document.getElementById('checkin-confirm-modal');
450
+
451
+ // Set wine name
452
+ document.getElementById('checkin-confirm-name').textContent = wine.name;
453
+
454
+ // Set image
455
+ const imageContainer = document.getElementById('checkin-confirm-image');
456
+ if (wine.front_label_image_path) {
457
+ imageContainer.innerHTML = `<img src="${API_BASE}/images/${wine.front_label_image_path}" alt="Wine label">`;
458
+ } else {
459
+ imageContainer.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);">No image</div>';
460
+ }
461
+
462
+ // Set parsed fields
463
+ const fieldsContainer = document.getElementById('checkin-confirm-fields');
464
+ const fields = [
465
+ { label: 'Winery', value: wine.winery },
466
+ { label: 'Vintage', value: wine.vintage },
467
+ { label: 'Grape Variety', value: wine.grape_variety },
468
+ { label: 'Region', value: wine.region },
469
+ { label: 'Country', value: wine.country },
470
+ { label: 'Alcohol %', value: wine.alcohol_percentage ? `${wine.alcohol_percentage}%` : null },
471
+ { label: 'Quantity', value: wine.inventory?.quantity || 1 }
472
+ ];
473
+
474
+ fieldsContainer.innerHTML = fields.map(field => `
475
+ <div class="checkin-confirm-field">
476
+ <div class="label">${field.label}</div>
477
+ <div class="value ${field.value ? '' : 'empty'}">${field.value || 'Not detected'}</div>
478
+ </div>
479
+ `).join('');
480
+
481
+ // Set OCR text
482
+ document.getElementById('checkin-confirm-front-ocr').textContent = wine.front_label_text || 'No text extracted';
483
+
484
+ const backOcrSection = document.getElementById('checkin-confirm-back-ocr-section');
485
+ if (wine.back_label_text) {
486
+ backOcrSection.style.display = 'block';
487
+ document.getElementById('checkin-confirm-back-ocr').textContent = wine.back_label_text;
488
+ } else {
489
+ backOcrSection.style.display = 'none';
490
+ }
491
+
492
+ // Show modal
493
+ modal.classList.add('active');
494
+
495
+ // Set up button handlers
496
+ document.getElementById('checkin-confirm-done').onclick = () => {
497
+ modal.classList.remove('active');
498
+ navigateTo('cellar');
499
+ };
500
+
501
+ document.getElementById('checkin-confirm-another').onclick = () => {
502
+ modal.classList.remove('active');
503
+ // Reset form previews
504
+ document.getElementById('front-preview').innerHTML = 'Tap to take photo or select image';
505
+ document.getElementById('back-preview').innerHTML = 'Tap to take photo or select image';
506
+ };
507
+ }
508
+
255
509
  async function handleSearch(e) {
256
510
  e.preventDefault();
257
511
  const form = e.target;
@@ -558,9 +812,25 @@ async function showWineDetail(wineId) {
558
812
  ` : ''}
559
813
 
560
814
  ${wine.front_label_text ? `
561
- <div class="wine-detail-field">
562
- <div class="label">OCR Text (Front Label)</div>
563
- <div class="wine-detail-ocr">${wine.front_label_text}</div>
815
+ <div class="wine-detail-label-text collapsible">
816
+ <div class="collapsible-header" onclick="toggleWineDetailLabelText(this)">
817
+ <span class="label">Show Raw Label Text</span>
818
+ <span class="collapse-icon">+</span>
819
+ </div>
820
+ <div class="collapsible-content" style="display: none;">
821
+ <div class="ocr-raw-text">
822
+ <div class="ocr-raw-section">
823
+ <label>Front Label:</label>
824
+ <pre>${escapeHtml(wine.front_label_text)}</pre>
825
+ </div>
826
+ ${wine.back_label_text ? `
827
+ <div class="ocr-raw-section">
828
+ <label>Back Label:</label>
829
+ <pre>${escapeHtml(wine.back_label_text)}</pre>
830
+ </div>
831
+ ` : ''}
832
+ </div>
833
+ </div>
564
834
  </div>
565
835
  ` : ''}
566
836
 
@@ -678,6 +948,30 @@ function formatDate(dateString) {
678
948
  });
679
949
  }
680
950
 
951
+ function escapeHtml(text) {
952
+ const div = document.createElement('div');
953
+ div.textContent = text;
954
+ return div.innerHTML;
955
+ }
956
+
957
+ function toggleWineDetailLabelText(header) {
958
+ const section = header.parentElement;
959
+ const content = section.querySelector('.collapsible-content');
960
+ const icon = header.querySelector('.collapse-icon');
961
+ const label = header.querySelector('.label');
962
+
963
+ section.classList.toggle('open');
964
+ if (section.classList.contains('open')) {
965
+ content.style.display = 'block';
966
+ icon.textContent = '-';
967
+ label.textContent = 'Hide Raw Label Text';
968
+ } else {
969
+ content.style.display = 'none';
970
+ icon.textContent = '+';
971
+ label.textContent = 'Show Raw Label Text';
972
+ }
973
+ }
974
+
681
975
  function showToast(message, type = 'info') {
682
976
  const container = document.getElementById('toast-container');
683
977
  const toast = document.createElement('div');
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: winebox
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Wine Cellar Management Application with OCR label scanning
5
5
  Project-URL: Homepage, https://github.com/jdrumgoole/winebox
6
6
  Project-URL: Repository, https://github.com/jdrumgoole/winebox
@@ -22,6 +22,7 @@ Classifier: Topic :: Home Automation
22
22
  Requires-Python: >=3.11
23
23
  Requires-Dist: aiofiles>=23.0.0
24
24
  Requires-Dist: aiosqlite>=0.19.0
25
+ Requires-Dist: anthropic>=0.40.0
25
26
  Requires-Dist: bcrypt<4.1.0,>=4.0.0
26
27
  Requires-Dist: fastapi>=0.109.0
27
28
  Requires-Dist: jinja2>=3.1.0
@@ -173,6 +174,38 @@ Images are served via the API at `/api/images/{filename}`.
173
174
 
174
175
  **Note:** The `data/` directory is excluded from git (see `.gitignore`). Make sure to back up this directory to preserve your wine collection data.
175
176
 
177
+ ## Label Scanning
178
+
179
+ WineBox uses AI-powered label scanning to extract wine information from photos.
180
+
181
+ ### Claude Vision (Recommended)
182
+
183
+ For best results, configure Claude Vision by setting your Anthropic API key:
184
+
185
+ ```bash
186
+ export ANTHROPIC_API_KEY=your-api-key
187
+ # or
188
+ export WINEBOX_ANTHROPIC_API_KEY=your-api-key
189
+ ```
190
+
191
+ Claude Vision provides intelligent label analysis that:
192
+ - Handles decorative and artistic fonts
193
+ - Understands wine-specific terminology
194
+ - Extracts structured data (winery, vintage, grape variety, region, etc.)
195
+ - Works with curved or angled text
196
+
197
+ ### Tesseract OCR (Fallback)
198
+
199
+ If no Anthropic API key is configured, WineBox falls back to Tesseract OCR. This requires Tesseract to be installed on your system:
200
+
201
+ ```bash
202
+ # macOS
203
+ brew install tesseract
204
+
205
+ # Ubuntu/Debian
206
+ sudo apt-get install tesseract-ocr
207
+ ```
208
+
176
209
  ## Authentication
177
210
 
178
211
  WineBox requires authentication for all API endpoints (except `/health`).
@@ -1,5 +1,5 @@
1
- winebox/__init__.py,sha256=bAd30zPrCJ2BYtbyxoRUvXt6m6Z4ivEOSdT3HD5uyos,75
2
- winebox/config.py,sha256=IJ9NzNyX0_g7ji3EmVxZRRL0csB7N5iGxsYStO25hI8,1007
1
+ winebox/__init__.py,sha256=1MbD6vmUlpuZKB-RvWkszuH5NNsw0MFclRCQgwErdNQ,75
2
+ winebox/config.py,sha256=mxdWkchZdYkPrSpfgn1RV26nZK09_3rql0iGbcAGvM0,1233
3
3
  winebox/database.py,sha256=jTdf9mb48D8644THo6Mx6lHCKDFlAZ18P8Dvuk2eROI,1128
4
4
  winebox/main.py,sha256=wlXrBwMvJQhG3g8mmOGWp-QbEEbR805SaElBryR61t0,2398
5
5
  winebox/cli/__init__.py,sha256=FOUoclklBXVfsfON3ohA8v_coJ9p5LSbVYvTucyXieg,29
@@ -15,20 +15,22 @@ winebox/routers/auth.py,sha256=7rmld-TOIHmCMJfQKuQfkZFoNAAZd0zuj-mKqmAbHeg,2446
15
15
  winebox/routers/cellar.py,sha256=QSyAnPQ2hiNaoojLk-5hJy95OnG8RMHOD0h5LV6bAWM,3404
16
16
  winebox/routers/search.py,sha256=aIqwsjnB9e-D84VVcuye-01V_9WBUjFKzXNsX0gkRak,4971
17
17
  winebox/routers/transactions.py,sha256=K0UDIJbVSTjV7NdI_CS0pvoTflH3Yp89DmneyFyl9ok,2055
18
- winebox/routers/wines.py,sha256=4Jp1IGDCH2_jJfeAvy1ItfaNmmagUv20r_NTNmEYnew,9385
18
+ winebox/routers/wines.py,sha256=muTwU0T21vkihNOyVtnXjQY7Fl6d2BRKBpJoIyyieA4,14238
19
19
  winebox/schemas/__init__.py,sha256=R4j1wdV1F-6kA45rHA8NUP2cqxBmLb2CMklo8tFjzGQ,357
20
20
  winebox/schemas/transaction.py,sha256=GV_AZGeuFp_nOkg902UUhrHGwB1DxcQmGoG82m1UccI,899
21
21
  winebox/schemas/wine.py,sha256=xHWDiV5bvEJuLPN2qWgOhqVMOTdlDSmfan5mtEy3Nvw,2301
22
22
  winebox/services/__init__.py,sha256=rlAd5FBcqZJ7rsrFqR8MwTqGJVerlQHVUleEmoIaz1Y,277
23
23
  winebox/services/auth.py,sha256=RDXP26hL3cGGxKcniXWMZ0sPUP6ew4LLMnfo4dv4P2E,3854
24
24
  winebox/services/image_storage.py,sha256=ftiX54dVuDT0DVLpyBOL9N7zhZkapI0l1TFknfPedno,2393
25
- winebox/services/ocr.py,sha256=X9lk1dkcH7xRrfYU5ArOakW4qavlRHQIkDD9nbPtzz4,3881
25
+ winebox/services/ocr.py,sha256=zFmQGj3a_jv21MBx8RfNkFIenAOnJ0O28JupSrsNC44,4925
26
+ winebox/services/vision.py,sha256=9WKHrDdFK0AmmmnST7vnV50Vgr6UsKR-WUtflKklJLc,9100
26
27
  winebox/services/wine_parser.py,sha256=n5ldv2x2XsTfpK3acYDyMNyZHdJ_8WQU0keOql14MJ0,10456
27
- winebox/static/index.html,sha256=57uWE1XQXIjP5a1lXMLqwZR2l7w5QaVMH5Qpp0DC4A0,12900
28
- winebox/static/css/style.css,sha256=xzDVgl0-zT4yLtHWsuU3ASk0YImMnMGP5a8vKpYK6gA,19561
29
- winebox/static/js/app.js,sha256=GabcNGSn7yQv7_KC7Ew5VjHmdiCjeAWxK2YtTkktVTc,23557
30
- winebox-0.1.1.dist-info/METADATA,sha256=SzuTSWWq3ST0JlGC0aoDVfU0dtCapuvGb4DYnGjzFFU,7150
31
- winebox-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
32
- winebox-0.1.1.dist-info/entry_points.txt,sha256=XY-GMf023m8Iof3rmokkITONECm8gCOVkTX0rCkze30,103
33
- winebox-0.1.1.dist-info/licenses/LICENSE,sha256=3popbhCShFhWVwTLKeQE-JNDYuPOYfmCiz-LoQpizTA,1070
34
- winebox-0.1.1.dist-info/RECORD,,
28
+ winebox/static/favicon.svg,sha256=jYFlQ-dEfhUoHQJepoSEMYQx0jxk8HyuX0wrd4OApzE,1061
29
+ winebox/static/index.html,sha256=7T_bgTKKGOQ0eKoo4scyVAD51GyCfXfNrJMTwlNvbYQ,17065
30
+ winebox/static/css/style.css,sha256=TBVTdw5eEkm2du7GeS-sPTsWiE-_ttzAWozWLXOuBzU,25940
31
+ winebox/static/js/app.js,sha256=RNDdXMaz3uC3emRnsaUPxDCi9Q5SPcUQeuIrTRAmX2U,34741
32
+ winebox-0.1.3.dist-info/METADATA,sha256=UcVbvLKE-F1v3AiS8GISrF379XqEN2tF_B63ra0Je-k,8001
33
+ winebox-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ winebox-0.1.3.dist-info/entry_points.txt,sha256=XY-GMf023m8Iof3rmokkITONECm8gCOVkTX0rCkze30,103
35
+ winebox-0.1.3.dist-info/licenses/LICENSE,sha256=3popbhCShFhWVwTLKeQE-JNDYuPOYfmCiz-LoQpizTA,1070
36
+ winebox-0.1.3.dist-info/RECORD,,