Ryan Christian D. Deniega commited on
Commit
a98615e
·
1 Parent(s): f934e95

chore: add HF Spaces deployment config and fix frontend API URL

Browse files
Dockerfile CHANGED
@@ -1,6 +1,6 @@
1
- # ── PhilVerify API — Cloud Run Dockerfile ─────────────────────────────────────
2
  # Build: docker build -t philverify-api .
3
- # Run: docker run -p 8080:8080 --env-file .env philverify-api
4
 
5
  FROM python:3.12-slim
6
 
@@ -56,12 +56,12 @@ print('All HuggingFace models cached.'); \
56
  " || true
57
 
58
  # ── Runtime ───────────────────────────────────────────────────────────────────
59
- # Cloud Run sets PORT automatically; default to 8080 for local runs.
60
- ENV PORT=8080
61
  ENV APP_ENV=production
62
  ENV DEBUG=false
63
 
64
- EXPOSE 8080
65
 
66
  # Use exec form so signals (SIGTERM) reach uvicorn directly
67
  CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT} --workers 1 --timeout-keep-alive 75"]
 
1
+ # ── PhilVerify API — Dockerfile (Cloud Run + Hugging Face Spaces) ─────────────
2
  # Build: docker build -t philverify-api .
3
+ # Run: docker run -p 7860:7860 --env-file .env philverify-api
4
 
5
  FROM python:3.12-slim
6
 
 
56
  " || true
57
 
58
  # ── Runtime ───────────────────────────────────────────────────────────────────
59
+ # HF Spaces uses port 7860 by default. Cloud Run overrides PORT via env var.
60
+ ENV PORT=7860
61
  ENV APP_ENV=production
62
  ENV DEBUG=false
63
 
64
+ EXPOSE 7860
65
 
66
  # Use exec form so signals (SIGTERM) reach uvicorn directly
67
  CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT} --workers 1 --timeout-keep-alive 75"]
README.md CHANGED
@@ -1,3 +1,13 @@
 
 
 
 
 
 
 
 
 
 
1
  <p align="center">
2
  <img src="frontend/public/logo.svg" alt="PhilVerify Logo" width="150">
3
  </p>
 
1
+ ---
2
+ title: PhilVerify API
3
+ emoji: 🇵🇭
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
  <p align="center">
12
  <img src="frontend/public/logo.svg" alt="PhilVerify Logo" width="150">
13
  </p>
frontend/.env.production.example ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Copy this to .env.production and fill in your actual HF Space URL
2
+ # HF Space URL format: https://<your-hf-username>-philverify-api.hf.space
3
+ # The /api suffix is required — it must match your FastAPI router prefix
4
+
5
+ VITE_API_BASE_URL=https://YOUR_HF_USERNAME-philverify-api.hf.space/api
frontend/src/api.js CHANGED
@@ -1,5 +1,8 @@
1
- /** PhilVerify API client — proxied through Vite to http://localhost:8000 */
2
- const BASE = '/api'
 
 
 
3
 
4
  function _detailToString(detail, status) {
5
  if (!detail) return `HTTP ${status}`
 
1
+ /** PhilVerify API client
2
+ * Dev: Vite proxies /api → http://localhost:8000
3
+ * Production: Set VITE_API_BASE_URL to your backend URL (e.g. HF Space)
4
+ */
5
+ const BASE = (import.meta.env.VITE_API_BASE_URL ?? '').replace(/\/$/, '') || '/api'
6
 
7
  function _detailToString(detail, status) {
8
  if (!detail) return `HTTP ${status}`
frontend/src/pages/VerifyPage.jsx CHANGED
@@ -237,6 +237,7 @@ export default function VerifyPage() {
237
  const [tab, setTab] = useState(persisted?.tab ?? 'text')
238
  const [input, setInput] = useState(persisted?.input ?? '')
239
  const [file, setFile] = useState(null)
 
240
  const [dragOver, setDragOver] = useState(false)
241
  const [loading, setLoading] = useState(false)
242
  const [result, setResult] = useState(persisted?.result ?? null)
@@ -257,6 +258,14 @@ export default function VerifyPage() {
257
  }
258
  }, [submittedInput])
259
 
 
 
 
 
 
 
 
 
260
  /* Persist result + input to sessionStorage so state survives navigation/refresh */
261
  useEffect(() => {
262
  if (result) {
@@ -321,12 +330,13 @@ export default function VerifyPage() {
321
  }
322
 
323
  function handleTabChange(id) {
324
- setTab(id); setInput(''); setFile(null); setResult(null); setError(null); setSubmittedInput(null); setUrlPreview(null)
325
  sessionStorage.removeItem(STORAGE_KEY)
326
  }
327
 
328
  function handleVerifyAgain() {
329
  setResult(null); setError(null); setExtractedTextOpen(false)
 
330
  sessionStorage.removeItem(STORAGE_KEY)
331
  // Smooth-scroll back to the input panel
332
  requestAnimationFrame(() => {
@@ -482,11 +492,45 @@ export default function VerifyPage() {
482
  name={tab === 'image' ? 'media-image' : 'media-video'}
483
  accept={tab === 'image' ? 'image/*' : 'video/*,audio/*'}
484
  onChange={e => setFile(e.target.files[0])} />
485
- <Upload size={18} aria-hidden="true"
486
- style={{ margin: '0 auto 8px', color: file ? 'var(--accent-red)' : 'var(--text-muted)' }} />
487
- {file
488
- ? <p className="text-sm font-semibold" style={{ color: 'var(--accent-red)', fontFamily: 'var(--font-display)' }}>{file.name}</p>
489
- : <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  <p className="text-sm" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }}>
491
  Drop or click to upload {tab === 'image' ? 'image' : 'video / audio'}
492
  </p>
@@ -494,7 +538,7 @@ export default function VerifyPage() {
494
  or press <kbd style={{ background: 'var(--bg-hover)', border: '1px solid var(--border)', borderRadius: 2, padding: '1px 5px', fontFamily: 'var(--font-mono)', fontSize: 10 }}>Ctrl+V</kbd> to paste from clipboard
495
  </p>
496
  </>
497
- }
498
  </div>
499
  </div>
500
  )}
@@ -525,13 +569,33 @@ export default function VerifyPage() {
525
  <div className="card p-4 fade-up" style={{ borderLeft: '3px solid var(--accent-red)' }}>
526
  <p className="text-xs font-semibold uppercase tracking-widest mb-2"
527
  style={{ fontFamily: 'var(--font-display)', color: 'var(--text-muted)', letterSpacing: '0.15em' }}>
528
- Verified Input
 
 
 
529
  </p>
530
  {submittedInput.type === 'url' && (
531
  <div className="space-y-2">
532
- {/* Rich article card if preview is available */}
533
- {submittedInput.preview
534
- ? <URLPreviewCard preview={submittedInput.preview} loading={false} url={submittedInput.text} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  : (
536
  <a href={submittedInput.text} target="_blank" rel="noreferrer"
537
  className="flex items-center gap-2 text-sm"
@@ -560,45 +624,67 @@ export default function VerifyPage() {
560
  </p>
561
  )}
562
  {submittedInput.type === 'image' && (
563
- <div className="flex items-start gap-3">
564
- {submittedInput.fileUrl && (
565
- <img
566
- src={submittedInput.fileUrl}
567
- alt="Submitted image preview"
568
- style={{
569
- width: 72, height: 72,
570
- objectFit: 'cover',
571
- borderRadius: 2,
572
- border: '1px solid var(--border)',
573
- flexShrink: 0,
574
- }} />
575
- )}
576
- <div>
577
- <p className="text-sm font-semibold"
578
- style={{ color: 'var(--text-primary)', fontFamily: 'var(--font-display)' }}>
579
- {submittedInput.file?.name}
580
- </p>
581
- <p className="text-xs mt-0.5"
582
- style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
583
- {submittedInput.file ? (submittedInput.file.size / 1024).toFixed(1) + ' KB' : ''}
584
- </p>
585
- </div>
586
- </div>
 
 
 
 
 
 
 
587
  )}
588
  {submittedInput.type === 'video' && (
589
- <div className="flex items-center gap-2">
590
- <Video size={15} style={{ color: 'var(--text-muted)', flexShrink: 0 }} aria-hidden="true" />
591
- <div>
592
- <p className="text-sm font-semibold"
593
- style={{ color: 'var(--text-primary)', fontFamily: 'var(--font-display)' }}>
594
- {submittedInput.file?.name}
595
- </p>
596
- <p className="text-xs mt-0.5"
597
- style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
598
- {submittedInput.file ? (submittedInput.file.size / (1024 * 1024)).toFixed(2) + ' MB' : ''}
599
- </p>
600
- </div>
601
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  )}
603
  </div>
604
  )}
@@ -726,86 +812,6 @@ export default function VerifyPage() {
726
  </p>
727
  </div>
728
 
729
- {/* Row 2½: Extracted Text (collapsible) */}
730
- {result.extracted_text && (
731
- <div className="card fade-up-3" style={{ overflow: 'hidden' }}>
732
- <button
733
- onClick={() => setExtractedTextOpen(o => !o)}
734
- className="w-full flex items-center justify-between px-5 py-3 transition-colors"
735
- style={{
736
- background: 'none',
737
- border: 'none',
738
- cursor: 'pointer',
739
- borderBottom: extractedTextOpen ? '1px solid var(--border)' : 'none',
740
- }}
741
- onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
742
- onMouseLeave={e => e.currentTarget.style.background = 'none'}
743
- aria-expanded={extractedTextOpen}
744
- aria-controls="extracted-text-panel"
745
- >
746
- <div className="flex items-center gap-2">
747
- <span className="text-xs font-semibold uppercase tracking-widest"
748
- style={{ fontFamily: 'var(--font-display)', color: 'var(--text-muted)', letterSpacing: '0.15em' }}>
749
- {result.input_type === 'image' ? 'OCR Extracted Text'
750
- : result.input_type === 'video' ? 'Transcribed Text'
751
- : result.input_type === 'url' ? 'Scraped Text'
752
- : 'Analyzed Text'}
753
- </span>
754
- <span className="text-xs tabular"
755
- style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', fontSize: 10 }}>
756
- {result.extracted_text.length} chars
757
- </span>
758
- </div>
759
- <ChevronRight size={13}
760
- style={{
761
- color: 'var(--text-muted)',
762
- transform: extractedTextOpen ? 'rotate(90deg)' : 'rotate(0deg)',
763
- transition: 'transform 200ms ease',
764
- flexShrink: 0,
765
- }}
766
- aria-hidden="true"
767
- />
768
- </button>
769
- {extractedTextOpen && (
770
- <div id="extracted-text-panel" className="px-5 py-4">
771
- {result.input_type === 'url' && (
772
- <p className="text-xs mb-3" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)', lineHeight: 1.5 }}>
773
- This is the text our scraper extracted from the URL. If it looks wrong or incomplete, the page may have been partially blocked.
774
- </p>
775
- )}
776
- {result.input_type === 'image' && (
777
- <p className="text-xs mb-3" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)', lineHeight: 1.5 }}>
778
- This is the text read from your image using OCR. Poor image quality may cause errors.
779
- </p>
780
- )}
781
- {result.input_type === 'video' && (
782
- <p className="text-xs mb-3" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)', lineHeight: 1.5 }}>
783
- This is the speech transcribed from your video/audio file.
784
- </p>
785
- )}
786
- <pre
787
- style={{
788
- whiteSpace: 'pre-wrap',
789
- wordBreak: 'break-word',
790
- fontFamily: 'var(--font-mono)',
791
- fontSize: 12,
792
- lineHeight: 1.7,
793
- color: 'var(--text-secondary)',
794
- background: 'var(--bg-elevated)',
795
- border: '1px solid var(--border)',
796
- borderRadius: 2,
797
- padding: '12px 14px',
798
- maxHeight: 280,
799
- overflowY: 'auto',
800
- }}
801
- >
802
- {result.extracted_text}
803
- </pre>
804
- </div>
805
- )}
806
- </div>
807
- )}
808
-
809
  {/* Row 3: Layer cards (2 col, collapses to 1 on mobile) */}
810
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 fade-up-4">
811
  {/* Layer 1 */}
 
237
  const [tab, setTab] = useState(persisted?.tab ?? 'text')
238
  const [input, setInput] = useState(persisted?.input ?? '')
239
  const [file, setFile] = useState(null)
240
+ const [fileObjectUrl, setFileObjectUrl] = useState(null)
241
  const [dragOver, setDragOver] = useState(false)
242
  const [loading, setLoading] = useState(false)
243
  const [result, setResult] = useState(persisted?.result ?? null)
 
258
  }
259
  }, [submittedInput])
260
 
261
+ /* Create/revoke object URL for in-form file preview */
262
+ useEffect(() => {
263
+ if (!file) { setFileObjectUrl(null); return }
264
+ const url = URL.createObjectURL(file)
265
+ setFileObjectUrl(url)
266
+ return () => URL.revokeObjectURL(url)
267
+ }, [file])
268
+
269
  /* Persist result + input to sessionStorage so state survives navigation/refresh */
270
  useEffect(() => {
271
  if (result) {
 
330
  }
331
 
332
  function handleTabChange(id) {
333
+ setTab(id); setInput(''); setFile(null); setFileObjectUrl(null); setResult(null); setError(null); setSubmittedInput(null); setUrlPreview(null)
334
  sessionStorage.removeItem(STORAGE_KEY)
335
  }
336
 
337
  function handleVerifyAgain() {
338
  setResult(null); setError(null); setExtractedTextOpen(false)
339
+ setFile(null); setFileObjectUrl(null); setUrlPreview(null); setSubmittedInput(null)
340
  sessionStorage.removeItem(STORAGE_KEY)
341
  // Smooth-scroll back to the input panel
342
  requestAnimationFrame(() => {
 
492
  name={tab === 'image' ? 'media-image' : 'media-video'}
493
  accept={tab === 'image' ? 'image/*' : 'video/*,audio/*'}
494
  onChange={e => setFile(e.target.files[0])} />
495
+ {file && fileObjectUrl && tab === 'image' ? (
496
+ <div className="flex flex-col items-center gap-2">
497
+ <img
498
+ src={fileObjectUrl}
499
+ alt={file.name}
500
+ style={{
501
+ maxHeight: 200,
502
+ maxWidth: '100%',
503
+ borderRadius: 2,
504
+ border: '1px solid var(--border)',
505
+ objectFit: 'contain',
506
+ }}
507
+ />
508
+ <p className="text-xs" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
509
+ {file.name} · {(file.size / 1024).toFixed(1)} KB
510
+ </p>
511
+ </div>
512
+ ) : file && fileObjectUrl && tab === 'video' ? (
513
+ <div className="flex flex-col items-center gap-2">
514
+ <video
515
+ src={fileObjectUrl}
516
+ controls
517
+ onClick={e => e.stopPropagation()}
518
+ style={{
519
+ maxHeight: 200,
520
+ maxWidth: '100%',
521
+ borderRadius: 2,
522
+ border: '1px solid var(--border)',
523
+ background: '#000',
524
+ }}
525
+ />
526
+ <p className="text-xs" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
527
+ {file.name} · {(file.size / (1024 * 1024)).toFixed(2)} MB
528
+ </p>
529
+ </div>
530
+ ) : (
531
+ <>
532
+ <Upload size={18} aria-hidden="true"
533
+ style={{ margin: '0 auto 8px', color: 'var(--text-muted)' }} />
534
  <p className="text-sm" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }}>
535
  Drop or click to upload {tab === 'image' ? 'image' : 'video / audio'}
536
  </p>
 
538
  or press <kbd style={{ background: 'var(--bg-hover)', border: '1px solid var(--border)', borderRadius: 2, padding: '1px 5px', fontFamily: 'var(--font-mono)', fontSize: 10 }}>Ctrl+V</kbd> to paste from clipboard
539
  </p>
540
  </>
541
+ )}
542
  </div>
543
  </div>
544
  )}
 
569
  <div className="card p-4 fade-up" style={{ borderLeft: '3px solid var(--accent-red)' }}>
570
  <p className="text-xs font-semibold uppercase tracking-widest mb-2"
571
  style={{ fontFamily: 'var(--font-display)', color: 'var(--text-muted)', letterSpacing: '0.15em' }}>
572
+ {submittedInput.type === 'url' ? 'Scraped Text'
573
+ : submittedInput.type === 'image' ? 'OCR Extracted Text'
574
+ : submittedInput.type === 'video' ? 'Transcribed Text'
575
+ : 'Verified Input'}
576
  </p>
577
  {submittedInput.type === 'url' && (
578
  <div className="space-y-2">
579
+ {result?.extracted_text
580
+ ? (
581
+ <pre style={{
582
+ whiteSpace: 'pre-wrap',
583
+ wordBreak: 'break-word',
584
+ fontFamily: 'var(--font-mono)',
585
+ fontSize: 12,
586
+ lineHeight: 1.7,
587
+ color: 'var(--text-secondary)',
588
+ background: 'var(--bg-elevated)',
589
+ border: '1px solid var(--border)',
590
+ borderRadius: 2,
591
+ padding: '12px 14px',
592
+ maxHeight: 280,
593
+ overflowY: 'auto',
594
+ margin: 0,
595
+ }}>
596
+ {result.extracted_text}
597
+ </pre>
598
+ )
599
  : (
600
  <a href={submittedInput.text} target="_blank" rel="noreferrer"
601
  className="flex items-center gap-2 text-sm"
 
624
  </p>
625
  )}
626
  {submittedInput.type === 'image' && (
627
+ result?.extracted_text
628
+ ? (
629
+ <pre style={{
630
+ whiteSpace: 'pre-wrap',
631
+ wordBreak: 'break-word',
632
+ fontFamily: 'var(--font-mono)',
633
+ fontSize: 12,
634
+ lineHeight: 1.7,
635
+ color: 'var(--text-secondary)',
636
+ background: 'var(--bg-elevated)',
637
+ border: '1px solid var(--border)',
638
+ borderRadius: 2,
639
+ padding: '12px 14px',
640
+ maxHeight: 280,
641
+ overflowY: 'auto',
642
+ margin: 0,
643
+ }}>
644
+ {result.extracted_text}
645
+ </pre>
646
+ )
647
+ : (
648
+ <div className="flex items-center gap-3">
649
+ {submittedInput.fileUrl && (
650
+ <img src={submittedInput.fileUrl} alt=""
651
+ style={{ width: 48, height: 48, objectFit: 'cover', borderRadius: 2, border: '1px solid var(--border)', flexShrink: 0 }} />
652
+ )}
653
+ <p className="text-xs" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
654
+ {submittedInput.file?.name}
655
+ </p>
656
+ </div>
657
+ )
658
  )}
659
  {submittedInput.type === 'video' && (
660
+ result?.extracted_text
661
+ ? (
662
+ <pre style={{
663
+ whiteSpace: 'pre-wrap',
664
+ wordBreak: 'break-word',
665
+ fontFamily: 'var(--font-mono)',
666
+ fontSize: 12,
667
+ lineHeight: 1.7,
668
+ color: 'var(--text-secondary)',
669
+ background: 'var(--bg-elevated)',
670
+ border: '1px solid var(--border)',
671
+ borderRadius: 2,
672
+ padding: '12px 14px',
673
+ maxHeight: 280,
674
+ overflowY: 'auto',
675
+ margin: 0,
676
+ }}>
677
+ {result.extracted_text}
678
+ </pre>
679
+ )
680
+ : (
681
+ <div className="flex items-center gap-2">
682
+ <Video size={15} style={{ color: 'var(--text-muted)', flexShrink: 0 }} aria-hidden="true" />
683
+ <p className="text-xs" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
684
+ {submittedInput.file?.name}
685
+ </p>
686
+ </div>
687
+ )
688
  )}
689
  </div>
690
  )}
 
812
  </p>
813
  </div>
814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  {/* Row 3: Layer cards (2 col, collapses to 1 on mobile) */}
816
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 fade-up-4">
817
  {/* Layer 1 */}