roseluo commited on
Commit
068b4ac
Β·
verified Β·
1 Parent(s): 18dd133

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +788 -0
  2. main.py +15 -1
app.py ADDED
@@ -0,0 +1,788 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WaveSign β€” Invisible File Authentication
3
+ UI matching wavesign_merged.html: cool blue, dot-grid, card layout, JetBrains Mono
4
+ """
5
+
6
+ import streamlit as st
7
+ from PIL import Image
8
+ import json, io, zipfile
9
+ from core import sign_image, embed_watermark, verify_image, detect_mode
10
+ from pdf_utils import sign_pdf, verify_pdf, get_pdf_page_count
11
+
12
+ st.set_page_config(
13
+ page_title="WaveSign β€” Invisible File Authentication",
14
+ page_icon="πŸ”", layout="wide",
15
+ initial_sidebar_state="collapsed"
16
+ )
17
+
18
+ CSS = """
19
+ <style>
20
+ @import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
21
+
22
+ :root {
23
+ --bg: #f2f4f8;
24
+ --white: #ffffff;
25
+ --surface: #f8f9fc;
26
+ --border: #dde2ec;
27
+ --border2: #c4ccda;
28
+ --accent: #1a6cff;
29
+ --accent-lt: #eaf0ff;
30
+ --accent-dk: #0f4fd4;
31
+ --ink: #141c2e;
32
+ --ink2: #3d4f6a;
33
+ --muted: #7a8fa8;
34
+ --muted2: #aab8cc;
35
+ --ok: #00986a;
36
+ --ok-bg: #e6f6f1;
37
+ --err: #d42050;
38
+ --err-bg: #fce9ef;
39
+ --warm: #f5f0e8;
40
+ --warm-bd: #e8dfc8;
41
+ --r: 8px;
42
+ --rs: 5px;
43
+ }
44
+
45
+ html, body, [class*="css"] {
46
+ font-family: 'Source Sans 3', sans-serif !important;
47
+ background: var(--bg) !important;
48
+ color: var(--ink);
49
+ font-size: 15px;
50
+ line-height: 1.6;
51
+ }
52
+
53
+ .stApp { background: var(--bg) !important; }
54
+ #MainMenu, footer, header { visibility: hidden; }
55
+ .block-container { padding: 2rem 48px 4rem !important; max-width: 960px !important; margin: 0 auto !important; }
56
+
57
+ /* dot grid */
58
+ .stApp::before {
59
+ content: '';
60
+ position: fixed; inset: 0;
61
+ background-image: radial-gradient(circle, rgba(26,108,255,0.06) 1px, transparent 1px);
62
+ background-size: 28px 28px;
63
+ pointer-events: none; z-index: 0;
64
+ }
65
+
66
+ .ws-wrap {
67
+ position: relative; z-index: 1;
68
+ max-width: 100%; margin: 0 auto;
69
+ padding: 0;
70
+ }
71
+
72
+ /* header */
73
+ .ws-hdr {
74
+ display: flex; align-items: center; justify-content: space-between;
75
+ margin-bottom: 36px; padding-bottom: 18px;
76
+ border-bottom: 1px solid var(--border);
77
+ }
78
+ .logo { font-size: 24px; font-weight: 700; letter-spacing: -0.5px; color: var(--ink); }
79
+ .logo span { color: var(--accent); }
80
+ .logo-tag {
81
+ display: inline-block; margin-left: 8px;
82
+ font-size: 10px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase;
83
+ color: var(--muted); border: 1px solid var(--border2); background: var(--white);
84
+ padding: 2px 7px; border-radius: 3px; vertical-align: middle; position: relative; top: -2px;
85
+ }
86
+ .hdr-status { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); font-weight: 500; }
87
+ .sdot {
88
+ width: 7px; height: 7px; border-radius: 50%; background: var(--ok);
89
+ box-shadow: 0 0 0 2px rgba(0,152,106,0.2); display: inline-block;
90
+ animation: pulse 2.4s ease-in-out infinite;
91
+ }
92
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
93
+
94
+ /* hero */
95
+ .hero {
96
+ background: var(--white); border: 1px solid var(--border); border-radius: var(--r);
97
+ padding: 40px 44px 36px; margin-bottom: 28px; text-align: center;
98
+ box-shadow: 0 1px 4px rgba(20,28,46,0.06); position: relative; overflow: hidden;
99
+ }
100
+ .hero::before {
101
+ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
102
+ background: linear-gradient(90deg, var(--accent), #55aaff 50%, var(--accent));
103
+ }
104
+ .hero-eye {
105
+ font-size: 11px; font-weight: 600; letter-spacing: 3px; text-transform: uppercase;
106
+ color: var(--accent); margin-bottom: 14px;
107
+ display: flex; align-items: center; justify-content: center; gap: 8px;
108
+ }
109
+ .hero-eye::before, .hero-eye::after {
110
+ content: ''; display: block; width: 28px; height: 1px; background: var(--accent); opacity: 0.4;
111
+ }
112
+ .hero-title { font-size: 32px; font-weight: 700; letter-spacing: -0.5px; color: var(--ink); line-height: 1.2; margin-bottom: 14px; }
113
+ .hero-div { width: 40px; height: 2px; background: linear-gradient(90deg, var(--accent), #55aaff); border-radius: 2px; margin: 0 auto 16px; }
114
+ .hero-sub { font-size: 16px; color: var(--ink2); max-width: 520px; margin: 0 auto; line-height: 1.7; }
115
+ .trust-row { display: flex; justify-content: center; gap: 10px; margin-top: 22px; flex-wrap: wrap; }
116
+ .trust-pill {
117
+ display: flex; align-items: center; gap: 6px;
118
+ font-size: 12px; font-weight: 600; color: var(--ink2);
119
+ background: var(--surface); border: 1px solid var(--border); border-radius: 20px; padding: 5px 13px;
120
+ }
121
+
122
+ /* tabs */
123
+ .stTabs [data-baseweb="tab-list"] {
124
+ background: var(--white) !important; border: 1px solid var(--border) !important;
125
+ border-radius: var(--r) !important; padding: 5px !important; gap: 4px !important;
126
+ box-shadow: 0 1px 3px rgba(20,28,46,0.05) !important; margin-bottom: 24px !important;
127
+ }
128
+ .stTabs [data-baseweb="tab"] {
129
+ font-family: 'Source Sans 3', sans-serif !important;
130
+ font-size: 13px !important; font-weight: 600 !important; color: var(--muted) !important;
131
+ border-radius: 6px !important; padding: 10px 16px !important; background: transparent !important; border: none !important;
132
+ }
133
+ .stTabs [aria-selected="true"] {
134
+ color: var(--accent) !important; background: var(--accent-lt) !important; font-weight: 700 !important;
135
+ }
136
+ .stTabs [data-baseweb="tab-panel"] { padding-top: 0 !important; }
137
+
138
+ /* step card */
139
+ .sc {
140
+ background: var(--white); border: 1px solid var(--border); border-radius: var(--r);
141
+ overflow: hidden; box-shadow: 0 1px 3px rgba(20,28,46,0.05); margin-bottom: 14px;
142
+ transition: box-shadow 0.2s, border-color 0.2s;
143
+ }
144
+ .sc:hover { box-shadow: 0 3px 14px rgba(26,108,255,0.09); border-color: #b3caff; }
145
+ .sc-hd {
146
+ display: flex; align-items: center; gap: 12px;
147
+ padding: 13px 18px; border-bottom: 1px solid var(--border); background: var(--surface);
148
+ }
149
+ .sc-badge {
150
+ width: 24px; height: 24px; border-radius: 50%; background: var(--accent-lt);
151
+ border: 1.5px solid #b3caff; display: inline-flex; align-items: center; justify-content: center;
152
+ font-size: 11px; font-weight: 700; color: var(--accent); flex-shrink: 0;
153
+ }
154
+ .sc-title { font-size: 13px; font-weight: 700; color: var(--ink); letter-spacing: 0.2px; text-transform: uppercase; }
155
+ .sc-body { padding: 20px; }
156
+
157
+ /* info box */
158
+ .ibox {
159
+ background: var(--warm); border: 1px solid var(--warm-bd); border-radius: var(--rs);
160
+ padding: 11px 14px; font-size: 13px; color: var(--ink2); margin-bottom: 14px; line-height: 1.55;
161
+ }
162
+
163
+ /* field label */
164
+ .flabel { font-size: 12px; font-weight: 700; color: var(--ink2); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px; }
165
+
166
+ /* format badges */
167
+ .fmt-row { display: flex; gap: 6px; justify-content: center; margin-top: 14px; }
168
+ .fmt-badge {
169
+ font-size: 10px; font-weight: 700; letter-spacing: 1px; color: var(--ink2);
170
+ border: 1px solid var(--border2); background: var(--white); padding: 3px 9px; border-radius: 3px;
171
+ }
172
+
173
+ /* detect badge */
174
+ .dbadge { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; border-radius: 20px; padding: 4px 12px; margin-top: 8px; }
175
+ .db-doc { background: var(--accent-lt); color: var(--accent); border: 1px solid #b3caff; }
176
+ .db-img { background: #fff8ec; color: #b06010; border: 1px solid #f0d090; }
177
+ .db-pdf { background: var(--ok-bg); color: var(--ok); border: 1px solid #90d8c0; }
178
+
179
+ /* inputs */
180
+ .stTextInput > div > div > input {
181
+ background: var(--bg) !important; border: 1.5px solid var(--border2) !important;
182
+ border-radius: var(--rs) !important; padding: 12px 14px !important;
183
+ font-family: 'JetBrains Mono', monospace !important;
184
+ font-size: 13px !important; color: var(--ink) !important; letter-spacing: 1px !important;
185
+ }
186
+ .stTextInput > div > div > input:focus {
187
+ border-color: var(--accent) !important; box-shadow: 0 0 0 3px rgba(26,108,255,0.1) !important; background: var(--white) !important;
188
+ }
189
+
190
+ /* file uploader */
191
+ div[data-testid="stFileUploader"] { background: var(--surface) !important; border: 1.5px dashed var(--border2) !important; border-radius: var(--rs) !important; }
192
+ div[data-testid="stFileUploader"]:hover { border-color: var(--accent) !important; background: var(--accent-lt) !important; }
193
+
194
+ /* buttons */
195
+ .stButton > button {
196
+ background: var(--accent) !important; color: white !important; border: none !important;
197
+ border-radius: var(--rs) !important; font-family: 'Source Sans 3', sans-serif !important;
198
+ font-size: 15px !important; font-weight: 700 !important; padding: 15px 20px !important;
199
+ width: 100% !important; min-height: 50px !important;
200
+ box-shadow: 0 2px 8px rgba(26,108,255,0.28) !important; transition: all 0.15s !important;
201
+ }
202
+ .stButton > button:hover { background: var(--accent-dk) !important; box-shadow: 0 4px 18px rgba(26,108,255,0.38) !important; transform: translateY(-1px) !important; }
203
+ .stDownloadButton > button {
204
+ background: var(--accent) !important; color: white !important; border: none !important;
205
+ border-radius: var(--rs) !important; font-size: 14px !important; font-weight: 700 !important;
206
+ width: 100% !important; min-height: 46px !important; box-shadow: 0 2px 8px rgba(26,108,255,0.28) !important;
207
+ }
208
+ .stDownloadButton > button:hover { background: var(--accent-dk) !important; }
209
+
210
+ /* side cards */
211
+ .scard { background: var(--white); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; box-shadow: 0 1px 3px rgba(20,28,46,0.05); margin-bottom: 14px; }
212
+ .scard-hd { padding: 11px 16px; border-bottom: 1px solid var(--border); background: var(--surface); font-size: 11px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); }
213
+ .scard-bd { padding: 14px 16px; }
214
+
215
+ /* metrics */
216
+ .mrow { display: flex; align-items: center; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid var(--border); }
217
+ .mrow:last-child { border-bottom: none; }
218
+ .mlbl { font-size: 12px; color: var(--muted); font-weight: 500; }
219
+ .mval { font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 500; padding: 2px 9px; border-radius: 4px; }
220
+ .mv-ok { color: var(--ok); background: var(--ok-bg); }
221
+ .mv-err { color: var(--err); background: var(--err-bg); }
222
+ .mv-neu { color: var(--muted); background: var(--surface); border: 1px solid var(--border); }
223
+
224
+ /* results */
225
+ .res-ok { background: var(--ok-bg); border: 1.5px solid #90d8c0; border-radius: var(--r); padding: 20px; text-align: center; margin-bottom: 14px; }
226
+ .res-err { background: var(--err-bg); border: 1.5px solid #f0a0b8; border-radius: var(--r); padding: 20px; text-align: center; margin-bottom: 14px; }
227
+ .res-icon { font-size: 28px; margin-bottom: 8px; }
228
+ .res-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
229
+ .res-sub { font-family: 'JetBrains Mono', monospace; font-size: 11px; opacity: 0.75; }
230
+
231
+ /* ready */
232
+ .ready { display: flex; flex-direction: column; align-items: center; padding: 28px 10px; gap: 10px; text-align: center; }
233
+ .ready-lbl { font-size: 12px; font-weight: 600; color: var(--muted2); }
234
+
235
+ /* sig hash */
236
+ .sighash { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--accent); background: var(--accent-lt); border: 1px solid #b3caff; border-radius: var(--rs); padding: 8px 12px; word-break: break-all; line-height: 1.7; margin: 8px 0; }
237
+
238
+ /* how it works */
239
+ .how-step { display: flex; gap: 16px; align-items: flex-start; padding: 16px 0; border-bottom: 1px solid var(--border); }
240
+ .how-step:last-child { border-bottom: none; }
241
+ .how-num { font-size: 11px; font-weight: 700; color: var(--accent); background: var(--accent-lt); border: 1.5px solid #b3caff; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 2px; }
242
+ .how-title { font-size: 14px; font-weight: 700; color: var(--ink); margin-bottom: 4px; }
243
+ .how-desc { font-size: 13px; color: var(--ink2); line-height: 1.55; }
244
+
245
+ .ucard { background: var(--surface); border: 1px solid var(--border); border-radius: var(--rs); padding: 12px 14px; margin-bottom: 8px; display: flex; gap: 12px; align-items: flex-start; }
246
+ .uicon { font-size: 18px; line-height: 1; margin-top: 1px; }
247
+ .utitle { font-size: 13px; font-weight: 700; color: var(--ink); margin-bottom: 3px; }
248
+ .udesc { font-size: 12px; color: var(--muted); line-height: 1.45; }
249
+
250
+ /* footer */
251
+ .ws-ftr { margin-top: 52px; padding-top: 18px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--muted); }
252
+ .ftr-links { display: flex; gap: 18px; }
253
+ .ftr-links a { color: var(--muted); text-decoration: none; font-weight: 500; }
254
+ .ftr-links a:hover { color: var(--ink2); }
255
+
256
+ @media(max-width:680px){
257
+ .hero { padding: 28px 20px 24px; }
258
+ .hero-title { font-size: 24px; }
259
+ }
260
+ </style>
261
+ """
262
+
263
+ st.markdown(CSS, unsafe_allow_html=True)
264
+
265
+ # ── wrapper + header ──────────────────────────────────────────────────
266
+ st.markdown("""
267
+ <div class="ws-wrap">
268
+ <div class="ws-hdr">
269
+ <div>
270
+ <span class="logo">Wave<span>Sign</span></span>
271
+ <span class="logo-tag">Beta</span>
272
+ </div>
273
+ <div class="hdr-status"><span class="sdot"></span> System online</div>
274
+ </div>
275
+
276
+ <div class="hero">
277
+ <div class="hero-eye">Invisible File Signing</div>
278
+ <div class="hero-title">Protect your files with an invisible signature</div>
279
+ <div class="hero-div"></div>
280
+ <div class="hero-sub">Upload your file, set a secret key, and get a signed copy you can share.
281
+ Anyone with the key can instantly verify the file is authentic and untampered.</div>
282
+ <div class="trust-row">
283
+ <div class="trust-pill">πŸ” No accounts needed</div>
284
+ <div class="trust-pill">πŸ”‘ Key never stored</div>
285
+ <div class="trust-pill">⚑ Instant verification</div>
286
+ <div class="trust-pill">πŸ–Ό Images &amp; PDFs</div>
287
+ </div>
288
+ </div>
289
+ """, unsafe_allow_html=True)
290
+
291
+ tab1, tab2, tab3, tab4 = st.tabs(["πŸ” Sign a File", "πŸ” Verify a File", "❓ How It Works", "⚑ API"])
292
+
293
+
294
+ # ══════════════════════════════════════════════════════════════════════
295
+ # TAB 1 β€” SIGN
296
+ # ══════════════════════════════════════════════════════════════════════
297
+ with tab1:
298
+ L, R = st.columns([1, 0.56], gap="large")
299
+
300
+ with L:
301
+ # Step 1
302
+ st.markdown("""
303
+ <div class="sc"><div class="sc-hd">
304
+ <span class="sc-badge">1</span>
305
+ <span class="sc-title">Upload Your File</span>
306
+ </div><div class="sc-body">
307
+ <div class="ibox">Supports images (PNG, JPG, WEBP) and documents (PDF).</div>
308
+ """, unsafe_allow_html=True)
309
+
310
+ uploaded = st.file_uploader("file", type=["png","jpg","jpeg","webp","pdf"],
311
+ key="su", label_visibility="collapsed")
312
+
313
+ _mode = None
314
+ if uploaded:
315
+ mb = uploaded.size / 1048576
316
+ if mb > 20: st.warning(f"Large file ({mb:.1f} MB) β€” may be slow on free hosting.")
317
+ elif mb > 8: st.info(f"{mb:.1f} MB β€” will take a few extra seconds.")
318
+
319
+ if uploaded.name.lower().endswith(".pdf"):
320
+ n = get_pdf_page_count(uploaded.getvalue())
321
+ st.markdown(f'<span class="dbadge db-pdf">πŸ“‘ PDF β€” {n} page{"s" if n!=1 else ""}</span>', unsafe_allow_html=True)
322
+ _mode = "document"
323
+ else:
324
+ _m = detect_mode(Image.open(io.BytesIO(uploaded.getvalue())))
325
+ _mode = _m
326
+ if _m == "color":
327
+ st.markdown('<span class="dbadge db-img">πŸ–ΌοΈ Color image</span>', unsafe_allow_html=True)
328
+ else:
329
+ st.markdown('<span class="dbadge db-doc">πŸ“„ Document</span>', unsafe_allow_html=True)
330
+
331
+ st.markdown("""
332
+ <div class="fmt-row">
333
+ <span class="fmt-badge">PNG</span><span class="fmt-badge">JPG</span>
334
+ <span class="fmt-badge">WEBP</span><span class="fmt-badge">PDF</span>
335
+ </div>
336
+ </div></div>
337
+ """, unsafe_allow_html=True)
338
+
339
+ # Step 2
340
+ st.markdown("""
341
+ <div class="sc"><div class="sc-hd">
342
+ <span class="sc-badge">2</span>
343
+ <span class="sc-title">Create Your Secret Key</span>
344
+ </div><div class="sc-body">
345
+ <div class="ibox">Choose any passphrase. You will need the same key to verify this file later. Keep it safe.</div>
346
+ <div class="flabel">Passphrase</div>
347
+ """, unsafe_allow_html=True)
348
+
349
+ secret = st.text_input("key", placeholder="e.g. my-company-2024",
350
+ key="sk", type="password", label_visibility="collapsed")
351
+ st.markdown("""
352
+ <div style="font-size:12px;color:var(--muted);margin-top:8px">
353
+ Your key is never sent to any server. It stays on your device only.
354
+ </div>
355
+ </div></div>
356
+ """, unsafe_allow_html=True)
357
+
358
+ sign_btn = st.button("Sign File β†’", key="sb")
359
+
360
+ with R:
361
+ if sign_btn and uploaded and secret:
362
+ is_pdf = uploaded.name.lower().endswith(".pdf")
363
+ base = uploaded.name.rsplit(".", 1)[0]
364
+
365
+ if is_pdf:
366
+ with st.spinner("Signing all pages…"):
367
+ spdf, sigs, np_ = sign_pdf(uploaded.getvalue(), secret, strength=0.03)
368
+
369
+ st.markdown(f"""
370
+ <div class="scard"><div class="scard-hd">Signed Document</div><div class="scard-bd">
371
+ <div class="res-ok">
372
+ <div class="res-icon">πŸ“‘</div>
373
+ <div class="res-title" style="color:var(--ok)">{np_} Page{"s" if np_!=1 else ""} Signed</div>
374
+ <div class="res-sub">Invisible signature embedded</div>
375
+ </div>
376
+ </div></div>""", unsafe_allow_html=True)
377
+
378
+ from pdf2image import convert_from_bytes as _cfb
379
+ pv = _cfb(spdf, dpi=72, first_page=1, last_page=min(2, np_))
380
+ st.markdown('<div class="scard"><div class="scard-hd">Page Preview</div><div class="scard-bd">', unsafe_allow_html=True)
381
+ pc = st.columns(len(pv))
382
+ for i,(col,pg) in enumerate(zip(pc,pv)):
383
+ with col:
384
+ st.markdown(f'<div style="text-align:center;font-size:10px;color:var(--muted);margin-bottom:2px">Page {i+1}</div>', unsafe_allow_html=True)
385
+ st.image(pg, use_container_width=True)
386
+ st.markdown('</div></div>', unsafe_allow_html=True)
387
+
388
+ cn,_ = st.columns([3,1])
389
+ with cn: fname = st.text_input("Name", value=f"wavesign_{base}", key="dn", label_visibility="visible")
390
+ fname = (fname or f"wavesign_{base}").strip()
391
+ zb = io.BytesIO()
392
+ with zipfile.ZipFile(zb,"w",zipfile.ZIP_DEFLATED) as zf:
393
+ zf.writestr(f"{fname}.pdf", spdf)
394
+ zf.writestr(f"{fname}.json", json.dumps(sigs, indent=2))
395
+ zb.seek(0)
396
+ st.download_button("⬇ Download Signed Package", zb.getvalue(),
397
+ file_name=f"{fname}.zip", mime="application/zip", use_container_width=True)
398
+ st.markdown(f'<div style="font-size:11px;color:var(--muted);margin-top:6px;text-align:center">Contains: <code>{fname}.pdf</code> + verification file</div>', unsafe_allow_html=True)
399
+
400
+ else:
401
+ with st.spinner("Signing your file…"):
402
+ img = Image.open(uploaded)
403
+ m = detect_mode(img)
404
+ wm = embed_watermark(img, secret, strength=0.015 if m=="color" else 0.03, mode=m)
405
+ sig = sign_image(wm, secret, mode=m)
406
+
407
+ st.markdown('<div class="scard"><div class="scard-hd">Your Signed File</div><div class="scard-bd">', unsafe_allow_html=True)
408
+ c1,c2 = st.columns(2)
409
+ with c1:
410
+ st.markdown('<div style="text-align:center;font-size:10px;font-weight:600;text-transform:uppercase;color:var(--muted);margin-bottom:3px">Original</div>', unsafe_allow_html=True)
411
+ st.image(img, use_container_width=True)
412
+ with c2:
413
+ st.markdown('<div style="text-align:center;font-size:10px;font-weight:600;text-transform:uppercase;color:var(--accent);margin-bottom:3px">Signed</div>', unsafe_allow_html=True)
414
+ st.image(wm, use_container_width=True)
415
+ st.markdown(f'<div class="sighash">{sig["sig_hash"][:48]}…</div>', unsafe_allow_html=True)
416
+ st.markdown('</div></div>', unsafe_allow_html=True)
417
+
418
+ cn,_ = st.columns([3,1])
419
+ with cn: fname = st.text_input("Name", value=f"wavesign_{base}", key="dn", label_visibility="visible", placeholder="e.g. contract_signed")
420
+ fname = (fname or f"wavesign_{base}").strip()
421
+ zb = io.BytesIO()
422
+ with zipfile.ZipFile(zb,"w",zipfile.ZIP_DEFLATED) as zf:
423
+ ib = io.BytesIO(); wm.save(ib, format="PNG")
424
+ zf.writestr(f"{fname}.png", ib.getvalue())
425
+ zf.writestr(f"{fname}.json", json.dumps(sig, indent=2))
426
+ zb.seek(0)
427
+ st.download_button("⬇ Download Signed Package", zb.getvalue(),
428
+ file_name=f"{fname}.zip", mime="application/zip", use_container_width=True)
429
+ st.markdown(f'<div style="font-size:11px;color:var(--muted);margin-top:6px;text-align:center">Contains: <code>{fname}.png</code> + verification file</div>', unsafe_allow_html=True)
430
+
431
+ elif sign_btn:
432
+ st.warning("Please upload a file and enter a secret key.")
433
+ else:
434
+ st.markdown("""
435
+ <div class="scard"><div class="scard-hd">Your Signed File</div><div class="scard-bd">
436
+ <div class="ready"><div style="font-size:26px;color:var(--muted2)">✦</div>
437
+ <div class="ready-lbl">Ready to sign</div></div>
438
+ </div></div>
439
+
440
+ """, unsafe_allow_html=True)
441
+
442
+
443
+ # ══════════════════════════════════════════════════════════════════════
444
+ # TAB 2 β€” VERIFY
445
+ # ══════════════════════════════════════════════════════════════════════
446
+ with tab2:
447
+ L, R = st.columns([1, 0.56], gap="large")
448
+
449
+ with L:
450
+ st.markdown("""
451
+ <div class="sc"><div class="sc-hd">
452
+ <span class="sc-badge">1</span>
453
+ <span class="sc-title">Upload Signed File</span>
454
+ </div><div class="sc-body">
455
+ <div class="ibox">Upload the signed image or PDF from your WaveSign package β€” not the original.</div>
456
+ """, unsafe_allow_html=True)
457
+ vf = st.file_uploader("Signed file", type=["png","jpg","jpeg","webp","pdf"],
458
+ key="vu", label_visibility="collapsed")
459
+ st.markdown('</div></div>', unsafe_allow_html=True)
460
+
461
+ st.markdown("""
462
+ <div class="sc"><div class="sc-hd">
463
+ <span class="sc-badge">2</span>
464
+ <span class="sc-title">Upload Verification File</span>
465
+ </div><div class="sc-body">
466
+ <div class="ibox">Upload the <strong>.json</strong> verification file from your WaveSign package. Keep it safe β€” it cannot be recovered if lost.</div>
467
+ """, unsafe_allow_html=True)
468
+ sf = st.file_uploader("Verification file", type=["json"],
469
+ key="sfu", label_visibility="collapsed")
470
+ st.markdown('</div></div>', unsafe_allow_html=True)
471
+
472
+ st.markdown("""
473
+ <div class="sc"><div class="sc-hd">
474
+ <span class="sc-badge">3</span>
475
+ <span class="sc-title">Enter Your Secret Key</span>
476
+ </div><div class="sc-body">
477
+ <div class="flabel">Passphrase</div>
478
+ """, unsafe_allow_html=True)
479
+ vk = st.text_input("vkey", placeholder="the key used when signing…",
480
+ key="vk", type="password", label_visibility="collapsed")
481
+ st.markdown('</div></div>', unsafe_allow_html=True)
482
+
483
+ vbtn = st.button("Check Authenticity β†’", key="vb")
484
+
485
+ with R:
486
+ if vbtn and vf and sf and vk:
487
+ try:
488
+ sd = json.loads(sf.getvalue().decode("utf-8"))
489
+ is_pdf = vf.name.lower().endswith(".pdf")
490
+ is_pdf_s = isinstance(sd, list)
491
+
492
+ if is_pdf and is_pdf_s:
493
+ with st.spinner("Checking all pages…"):
494
+ res = verify_pdf(vf.getvalue(), vk, sd)
495
+ ok = all(r["is_valid"] for r in res)
496
+ tot = len(res)
497
+ fail = [str(r["page_index"]+1) for r in res if not r["is_valid"]]
498
+
499
+ if ok:
500
+ st.markdown(f"""<div class="res-ok">
501
+ <div class="res-icon">βœ…</div>
502
+ <div class="res-title" style="color:var(--ok)">Document Authentic</div>
503
+ <div class="res-sub">All {tot} pages verified β€” no changes detected</div>
504
+ </div>""", unsafe_allow_html=True)
505
+ else:
506
+ st.markdown(f"""<div class="res-err">
507
+ <div class="res-icon">❌</div>
508
+ <div class="res-title" style="color:var(--err)">Document Modified</div>
509
+ <div class="res-sub">Page{"s" if len(fail)>1 else ""} {", ".join(fail)} failed</div>
510
+ </div>""", unsafe_allow_html=True)
511
+
512
+ if tot > 1:
513
+ st.markdown('<div class="scard"><div class="scard-hd">Page Results</div><div class="scard-bd">', unsafe_allow_html=True)
514
+ for r in res:
515
+ ic = "βœ…" if r["is_valid"] else "❌"
516
+ cls = "mv-ok" if r["is_valid"] else "mv-err"
517
+ lbl = "Authentic" if r["is_valid"] else "Modified"
518
+ st.markdown(f'<div class="mrow"><span class="mlbl">Page {r["page_index"]+1}</span><span class="mval {cls}">{ic} {lbl}</span></div>', unsafe_allow_html=True)
519
+ st.markdown('</div></div>', unsafe_allow_html=True)
520
+
521
+ elif not is_pdf and not is_pdf_s:
522
+ image = Image.open(vf)
523
+ with st.spinner("Checking your file…"):
524
+ r = verify_image(image, vk, sd)
525
+
526
+ if r["is_valid"]:
527
+ st.markdown("""<div class="res-ok">
528
+ <div class="res-icon">βœ…</div>
529
+ <div class="res-title" style="color:var(--ok)">File is Authentic</div>
530
+ <div class="res-sub">Signature verified β€” no changes detected</div>
531
+ </div>""", unsafe_allow_html=True)
532
+ else:
533
+ reason = "File modified after signing" if not r.get("spatial_hash_match", True) else "Wrong key or different signing account"
534
+ st.markdown(f"""<div class="res-err">
535
+ <div class="res-icon">❌</div>
536
+ <div class="res-title" style="color:var(--err)">Verification Failed</div>
537
+ <div class="res-sub">{reason}</div>
538
+ </div>""", unsafe_allow_html=True)
539
+
540
+ st.image(image, use_container_width=True)
541
+ else:
542
+ st.error("Verification file doesn't match the uploaded file type.")
543
+
544
+ except Exception as e:
545
+ st.error(f"Error: {e}")
546
+
547
+ elif vbtn:
548
+ st.warning("Please complete all three steps.")
549
+ else:
550
+ st.markdown("""
551
+ <div class="scard"><div class="scard-hd">Verification Result</div><div class="scard-bd">
552
+ <div class="ready">
553
+ <div style="font-size:26px;color:var(--muted2)">πŸ”</div>
554
+ <div class="ready-lbl">Awaiting verification</div>
555
+ </div>
556
+ </div></div>""", unsafe_allow_html=True)
557
+
558
+
559
+ # ══════════════════════════════════════════════════════════════════════
560
+ # TAB 3 β€” HOW IT WORKS
561
+ # ══════════════════════════════════════════════════════════════════════
562
+ with tab3:
563
+ C1, C2 = st.columns([3, 2], gap="large")
564
+
565
+ with C1:
566
+ st.markdown("""
567
+ <div class="sc"><div class="sc-hd">
568
+ <span class="sc-badge">✍</span>
569
+ <span class="sc-title">Signing a File</span>
570
+ </div><div class="sc-body">""", unsafe_allow_html=True)
571
+
572
+ for n, t, d in [
573
+ ("1", "Upload your file", "Choose any image or PDF to protect. Format is detected automatically."),
574
+ ("2", "Set a secret key", "Your passphrase locks the signature. Only someone with the same key can verify it. It never leaves your device."),
575
+ ("3", "Download your package", "A ZIP with two files: the signed file (visually identical to the original) and a small verification file. Keep both together."),
576
+ ]:
577
+ st.markdown(f'<div class="how-step"><div class="how-num">{n}</div><div><div class="how-title">{t}</div><div class="how-desc">{d}</div></div></div>', unsafe_allow_html=True)
578
+ st.markdown('</div></div>', unsafe_allow_html=True)
579
+
580
+ st.markdown("""
581
+ <div class="sc" style="margin-top:14px"><div class="sc-hd">
582
+ <span class="sc-badge">πŸ”</span>
583
+ <span class="sc-title">Verifying a File</span>
584
+ </div><div class="sc-body">""", unsafe_allow_html=True)
585
+
586
+ for n, t, d in [
587
+ ("1", "Upload the signed file", "Use the signed copy from your package β€” not the original."),
588
+ ("2", "Upload the verification file", "The small .json file from your package. Without it, verification isn't possible."),
589
+ ("3", "Enter your key", "WaveSign instantly tells you whether the file is authentic or has been changed since signing."),
590
+ ]:
591
+ st.markdown(f'<div class="how-step"><div class="how-num">{n}</div><div><div class="how-title">{t}</div><div class="how-desc">{d}</div></div></div>', unsafe_allow_html=True)
592
+ st.markdown('</div></div>', unsafe_allow_html=True)
593
+
594
+ with C2:
595
+ st.markdown('<div style="font-size:11px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);margin-bottom:10px">Use Cases</div>', unsafe_allow_html=True)
596
+ for ico, t, d in [
597
+ ("πŸ“„", "Contracts & Documents", "Share a signed PDF β€” any edit is instantly caught on verification"),
598
+ ("πŸ–ΌοΈ", "Images & Photos", "Prove your image hasn't been cropped, filtered, or altered"),
599
+ ("🎨", "Creative Work", "Sign before publishing β€” verify origin and integrity later"),
600
+ ("πŸ”", "Sensitive Files", "Any modification after signing invalidates the signature"),
601
+ ]:
602
+ st.markdown(f'<div class="ucard"><div class="uicon">{ico}</div><div><div class="utitle">{t}</div><div class="udesc">{d}</div></div></div>', unsafe_allow_html=True)
603
+
604
+ st.markdown('<div class="scard" style="margin-top:14px"><div class="scard-hd">Important to Know</div><div class="scard-bd">', unsafe_allow_html=True)
605
+ for tip in [
606
+ "Always share the <strong>signed file</strong>, not the original",
607
+ "Keep your <strong>verification file</strong> β€” it cannot be recovered",
608
+ "Use the <strong>same key</strong> to sign and verify",
609
+ "Signed files look <strong>identical</strong> to the original",
610
+ ]:
611
+ st.markdown(f'<div style="display:flex;gap:8px;padding:7px 0;border-bottom:1px solid var(--border);font-size:13px;color:var(--ink2);align-items:flex-start"><span style="color:var(--accent);font-weight:700;flex-shrink:0">β†’</span><span>{tip}</span></div>', unsafe_allow_html=True)
612
+ st.markdown('</div></div>', unsafe_allow_html=True)
613
+
614
+ # ══════════════════════════════════════════════════════════════════════
615
+ # TAB 4 β€” API ACCESS
616
+ # ══════════════════════════════════════════════════════════════════════
617
+ with tab4:
618
+ AC, BC = st.columns([3, 2], gap="large")
619
+
620
+ with AC:
621
+ # Hero intro card
622
+ st.markdown("""
623
+ <div class="sc"><div class="sc-hd">
624
+ <span class="sc-badge">⚑</span>
625
+ <span class="sc-title">REST API β€” Workflow Integration</span>
626
+ </div><div class="sc-body">
627
+ <div class="ibox">
628
+ The WaveSign REST API lets you embed invisible file signing and verification
629
+ directly into your pipelines, scripts, and applications β€” no browser required.
630
+ Every request is authenticated with your personal <strong>API token</strong>.
631
+ Files are processed by the same proprietary signing engine that powers this app.
632
+ </div>
633
+ <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:4px">
634
+ <div class="trust-pill">πŸ” Token-authenticated</div>
635
+ <div class="trust-pill">πŸ“‚ multipart/form-data</div>
636
+ <div class="trust-pill">⚑ JSON responses</div>
637
+ <div class="trust-pill">πŸ–Ό Images &amp; PDFs</div>
638
+ </div>
639
+ </div></div>
640
+ """, unsafe_allow_html=True)
641
+
642
+ # POST /sign card
643
+ st.markdown("""
644
+ <div class="sc"><div class="sc-hd">
645
+ <span class="sc-badge">1</span>
646
+ <span class="sc-title">POST /sign</span>
647
+ </div><div class="sc-body">
648
+ <div class="flabel">Endpoint</div>
649
+ <div style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px;margin-bottom:14px;word-break:break-all;">
650
+ POST &nbsp;https://roseluo-wavesign-api.hf.space/sign
651
+ </div>
652
+ <div class="flabel">Request β€” multipart/form-data</div>
653
+ <div style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px;margin-bottom:14px;line-height:1.9">
654
+ <span style="color:var(--accent)">file</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;image/PDF to sign (PNG Β· JPG Β· WEBP Β· PDF)<br>
655
+ <span style="color:var(--accent)">key</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;your secret passphrase<br>
656
+ <span style="color:var(--accent)">Authorization</span> &nbsp;Bearer &lt;your-api-token&gt;
657
+ </div>
658
+ <div class="flabel">Response β€” application/json</div>
659
+ <div style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px;margin-bottom:14px;line-height:1.9">
660
+ <span style="color:var(--ok)">signed_file</span> &nbsp;&nbsp;base64-encoded signed file<br>
661
+ <span style="color:var(--ok)">sig_file</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;base64-encoded signature file (JSON)<br>
662
+ <span style="color:var(--ok)">file_format</span> &nbsp;&nbsp;detected format (png Β· pdf Β· …)<br>
663
+ <span style="color:var(--ok)">status</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"ok"
664
+ </div>
665
+ <div class="flabel">curl example</div>
666
+ <div style="font-family:'JetBrains Mono',monospace;font-size:11.5px;background:var(--ink);color:#e2eaff;border-radius:var(--rs);padding:14px 16px;line-height:1.85;white-space:pre;overflow-x:auto">curl -X POST https://roseluo-wavesign-api.hf.space/sign \\
667
+ -H "Authorization: Bearer &lt;your-api-token&gt;" \\
668
+ -F "file=@document.pdf" \\
669
+ -F "key=my-secret-passphrase" \\
670
+ -o response.json</div>
671
+ </div></div>
672
+ """, unsafe_allow_html=True)
673
+
674
+ # POST /verify card
675
+ st.markdown("""
676
+ <div class="sc"><div class="sc-hd">
677
+ <span class="sc-badge">2</span>
678
+ <span class="sc-title">POST /verify</span>
679
+ </div><div class="sc-body">
680
+ <div class="flabel">Endpoint</div>
681
+ <div style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px;margin-bottom:14px;word-break:break-all;">
682
+ POST &nbsp;https://roseluo-wavesign-api.hf.space/verify
683
+ </div>
684
+ <div class="flabel">Request β€” multipart/form-data</div>
685
+ <div style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px;margin-bottom:14px;line-height:1.9">
686
+ <span style="color:var(--accent)">file</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;the signed image or PDF<br>
687
+ <span style="color:var(--accent)">sig_file</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;the .json signature file from signing<br>
688
+ <span style="color:var(--accent)">key</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;your secret passphrase<br>
689
+ <span style="color:var(--accent)">Authorization</span> &nbsp;Bearer &lt;your-api-token&gt;
690
+ </div>
691
+ <div class="flabel">Response β€” application/json</div>
692
+ <div style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px;margin-bottom:14px;line-height:1.9">
693
+ <span style="color:var(--ok)">is_valid</span> &nbsp;&nbsp;&nbsp;true / false<br>
694
+ <span style="color:var(--ok)">status</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"authentic" or "modified"<br>
695
+ <span style="color:var(--ok)">detail</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;human-readable verification message
696
+ </div>
697
+ <div class="flabel">curl example</div>
698
+ <div style="font-family:'JetBrains Mono',monospace;font-size:11.5px;background:var(--ink);color:#e2eaff;border-radius:var(--rs);padding:14px 16px;line-height:1.85;white-space:pre;overflow-x:auto">curl -X POST https://roseluo-wavesign-api.hf.space/verify \\
699
+ -H "Authorization: Bearer &lt;your-api-token&gt;" \\
700
+ -F "file=@document_signed.pdf" \\
701
+ -F "sig_file=@document_signed.json" \\
702
+ -F "key=my-secret-passphrase"</div>
703
+ </div></div>
704
+ """, unsafe_allow_html=True)
705
+
706
+ with BC:
707
+ # Python example card
708
+ st.markdown("""
709
+ <div class="scard"><div class="scard-hd">Python Example</div><div class="scard-bd">
710
+ <div style="font-family:'JetBrains Mono',monospace;font-size:11px;background:var(--ink);color:#e2eaff;border-radius:var(--rs);padding:14px 16px;line-height:1.85;white-space:pre;overflow-x:auto">import requests, base64
711
+
712
+ API_TOKEN = "&lt;your-api-token&gt;"
713
+ BASE_URL = "https://roseluo-wavesign-api.hf.space"
714
+ HEADERS = {"Authorization": f"Bearer {API_TOKEN}"}
715
+
716
+ # ── sign ─────────────────────────────
717
+ with open("document.pdf", "rb") as f:
718
+ r = requests.post(
719
+ f"{BASE_URL}/sign",
720
+ headers=HEADERS,
721
+ files={"file": f},
722
+ data={"key": "my-secret"},
723
+ )
724
+ resp = r.json()
725
+ signed = base64.b64decode(resp["signed_file"])
726
+ sig = base64.b64decode(resp["sig_file"])
727
+
728
+ with open("document_signed.pdf", "wb") as f:
729
+ f.write(signed)
730
+ with open("document_signed.json", "wb") as f:
731
+ f.write(sig)
732
+
733
+ # ── verify ───────────────────────────
734
+ with (open("document_signed.pdf","rb") as pf,
735
+ open("document_signed.json","rb") as sf):
736
+ r = requests.post(
737
+ f"{BASE_URL}/verify",
738
+ headers=HEADERS,
739
+ files={"file": pf, "sig_file": sf},
740
+ data={"key": "my-secret"},
741
+ )
742
+ print(r.json())
743
+ # {"is_valid": true, "status": "authentic", ...}</div>
744
+ </div></div>
745
+ """, unsafe_allow_html=True)
746
+
747
+ # Request access card
748
+ st.markdown("""
749
+ <div class="scard" style="margin-top:14px"><div class="scard-hd">Request API Access</div><div class="scard-bd">
750
+ <div style="font-size:13px;color:var(--ink2);line-height:1.65;margin-bottom:14px">
751
+ The WaveSign API is currently available on request.
752
+ Reach out with a brief description of your use case and we'll send you an
753
+ <strong>authentication token</strong> along with full integration documentation.
754
+ </div>
755
+ <div class="flabel">Contact</div>
756
+ <div style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--accent-lt);border:1px solid #b3caff;border-radius:var(--rs);padding:10px 14px;color:var(--accent);font-weight:600;word-break:break-all;">
757
+ api@your-domain.com
758
+ </div>
759
+ <div style="font-size:12px;color:var(--muted);margin-top:10px;line-height:1.5">
760
+ Please include your intended request volume and a short description of your
761
+ integration so we can set the right rate limits for your token.
762
+ </div>
763
+ </div></div>
764
+ """, unsafe_allow_html=True)
765
+
766
+ # Key facts card
767
+ st.markdown('<div class="scard" style="margin-top:14px"><div class="scard-hd">Key Facts</div><div class="scard-bd">', unsafe_allow_html=True)
768
+ for lbl, val, cls in [
769
+ ("Auth method", "Bearer token", "mv-neu"),
770
+ ("File transfer", "multipart/form-data", "mv-neu"),
771
+ ("Response", "JSON", "mv-neu"),
772
+ ("Max file size", "20 MB", "mv-neu"),
773
+ ("Formats", "PNG Β· JPG Β· PDF", "mv-neu"),
774
+ ]:
775
+ st.markdown(f'<div class="mrow"><span class="mlbl">{lbl}</span><span class="mval {cls}">{val}</span></div>', unsafe_allow_html=True)
776
+ st.markdown('</div></div>', unsafe_allow_html=True)
777
+
778
+
779
+ # ── footer + close wrapper ────────────────────────────────────────────
780
+ st.markdown("""
781
+ <div class="ws-ftr">
782
+ <div class="ftr-links">
783
+ <a href="https://github.com/roseluo/WaveSign" target="_blank">GitHub</a>
784
+ </div>
785
+ <div>WaveSign Β· Invisible File Authentication Β· Β© 2026 roseluo</div>
786
+ </div>
787
+ </div>
788
+ """, unsafe_allow_html=True)
main.py CHANGED
@@ -23,7 +23,7 @@ app = FastAPI(title="WaveSign API", docs_url=None, redoc_url=None)
23
  _API_KEY: str = os.environ.get("WS_API_KEY", "")
24
 
25
  # Routes that do not require authentication
26
- _PUBLIC_ROUTES = {"/health"}
27
 
28
 
29
  # ---------------------------------------------------------------------------
@@ -75,6 +75,20 @@ def _build_zip(files: dict) -> io.BytesIO:
75
  # Endpoints
76
  # ---------------------------------------------------------------------------
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  @app.get("/health")
79
  async def health():
80
  return {"status": "ok", "service": "WaveSign API"}
 
23
  _API_KEY: str = os.environ.get("WS_API_KEY", "")
24
 
25
  # Routes that do not require authentication
26
+ _PUBLIC_ROUTES = {"/health", "/"}
27
 
28
 
29
  # ---------------------------------------------------------------------------
 
75
  # Endpoints
76
  # ---------------------------------------------------------------------------
77
 
78
+ @app.get("/")
79
+ async def root():
80
+ return {
81
+ "service": "WaveSign API",
82
+ "version": "1.0",
83
+ "endpoints": {
84
+ "POST /sign": "Sign an image or PDF. Params: file, key. Returns ZIP.",
85
+ "POST /verify": "Verify a signed file. Params: file, sig_file, key. Returns JSON.",
86
+ "GET /health": "Service health check.",
87
+ },
88
+ "auth": "Bearer token required for /sign and /verify.",
89
+ }
90
+
91
+
92
  @app.get("/health")
93
  async def health():
94
  return {"status": "ok", "service": "WaveSign API"}