Phoe2004 commited on
Commit
a077412
·
verified ·
1 Parent(s): 6175b82

Upload 11 files

Browse files
Files changed (9) hide show
  1. Dockerfile +28 -21
  2. manifest.json +24 -0
  3. packages.txt +1 -0
  4. payment.html +225 -0
  5. payment_history.html +88 -0
  6. privacy.html +138 -0
  7. requirements.txt +16 -2
  8. sw.js +64 -0
  9. terms.html +113 -0
Dockerfile CHANGED
@@ -24,38 +24,33 @@ RUN pip install --no-cache-dir \
24
  COPY requirements.txt .
25
  RUN pip install --no-cache-dir -r requirements.txt
26
 
27
- # yt-dlp latest
28
- RUN pip install --no-cache-dir -U "yt-dlp[default]"
29
- RUN pip install faster-whisper
30
- RUN pip install -U "yt-dlp[default,curl-cffi]"
31
 
32
- # Copy all application files (login.html ဖယ်ထုတ်ပြီ — index.html ထဲ င်းထည့်ပြီးပြီ)
33
  COPY app.py .
34
- COPY run_bot.py .
35
- COPY bot.py .
36
- COPY recap_tg_bot.py .
37
  COPY index.html .
38
- COPY payment.html .
39
- COPY payment_history.html .
40
  COPY privacy.html .
41
  COPY terms.html .
42
  COPY NotoSansMyanmar-Bold.ttf .
43
- COPY m_youtube_com_cookies.txt .
44
- COPY start.sh .
45
  COPY manifest.json .
46
  COPY sw.js .
47
- RUN chmod +x start.sh
48
 
49
- # Create necessary directories
 
 
 
 
50
  RUN mkdir -p outputs slips temp_prev temp_thumb temp_
51
 
52
- # Install NotoSansMyanmar to system fonts + build isolated fonts.conf
53
  RUN mkdir -p /usr/local/share/fonts/myanmar \
54
  && cp /app/NotoSansMyanmar-Bold.ttf /usr/local/share/fonts/myanmar/ \
55
- && fc-cache -fv \
56
- && fc-list | grep -i myanmar || true
57
 
58
- # Write isolated fonts.conf at build time (only Myanmar font dir)
59
  RUN mkdir -p /app/fc_conf && \
60
  printf '<?xml version="1.0"?>\n\
61
  <!DOCTYPE fontconfig SYSTEM "fonts.dtd">\n\
@@ -66,10 +61,22 @@ RUN mkdir -p /app/fc_conf && \
66
  <config><rescan><int>30</int></rescan></config>\n\
67
  </fontconfig>\n' > /app/fc_conf/fonts.conf
68
 
69
- # Pre-download Whisper 'tiny' model at build time → no delay on first request
70
- RUN python -c "import whisper; whisper.load_model('tiny', device='cpu'); print('Whisper tiny model cached')"
71
 
 
72
  RUN deno --version && yt-dlp --version && python -c "import numpy; print('numpy', numpy.__version__)"
73
 
74
  EXPOSE 7860
75
- CMD ["./start.sh"]
 
 
 
 
 
 
 
 
 
 
 
 
24
  COPY requirements.txt .
25
  RUN pip install --no-cache-dir -r requirements.txt
26
 
27
+ # yt-dlp + faster-whisper
28
+ RUN pip install --no-cache-dir -U "yt-dlp[default,curl-cffi]"
29
+ RUN pip install --no-cache-dir faster-whisper
 
30
 
31
+ # App files (bot polling app.py ထဲပါပြီ — သးသန့် bot file မလို)
32
  COPY app.py .
 
 
 
33
  COPY index.html .
 
 
34
  COPY privacy.html .
35
  COPY terms.html .
36
  COPY NotoSansMyanmar-Bold.ttf .
 
 
37
  COPY manifest.json .
38
  COPY sw.js .
39
+ COPY m_youtube_com_cookies.txt* ./
40
 
41
+ # Optional static files (မရှိရင် build မပျက်)
42
+ COPY payment.html* ./
43
+ COPY payment_history.html* ./
44
+
45
+ # Directories
46
  RUN mkdir -p outputs slips temp_prev temp_thumb temp_
47
 
48
+ # Myanmar font install
49
  RUN mkdir -p /usr/local/share/fonts/myanmar \
50
  && cp /app/NotoSansMyanmar-Bold.ttf /usr/local/share/fonts/myanmar/ \
51
+ && fc-cache -fv
 
52
 
53
+ # Isolated fonts.conf (Myanmar font only)
54
  RUN mkdir -p /app/fc_conf && \
55
  printf '<?xml version="1.0"?>\n\
56
  <!DOCTYPE fontconfig SYSTEM "fonts.dtd">\n\
 
61
  <config><rescan><int>30</int></rescan></config>\n\
62
  </fontconfig>\n' > /app/fc_conf/fonts.conf
63
 
64
+ # Pre-cache Whisper tiny model
65
+ RUN python -c "import whisper; whisper.load_model('tiny', device='cpu'); print('Whisper tiny cached')"
66
 
67
+ # Sanity check
68
  RUN deno --version && yt-dlp --version && python -c "import numpy; print('numpy', numpy.__version__)"
69
 
70
  EXPOSE 7860
71
+
72
+ # Gunicorn တိုက်ရိုက် run — start.sh မလို
73
+ CMD ["gunicorn", "app:app", \
74
+ "--bind", "0.0.0.0:7860", \
75
+ "--workers", "1", \
76
+ "--threads", "8", \
77
+ "--worker-class", "gthread", \
78
+ "--timeout", "120", \
79
+ "--keep-alive", "5", \
80
+ "--log-level", "info", \
81
+ "--access-logfile", "-", \
82
+ "--error-logfile", "-"]
manifest.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Recap Studio",
3
+ "short_name": "Recap Studio",
4
+ "description": "AI-powered video recap generator",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#1a1a2e",
8
+ "theme_color": "#1a1a2e",
9
+ "orientation": "portrait",
10
+ "icons": [
11
+ {
12
+ "src": "https://raw.githubusercontent.com/Shangyi69/psonlineshop/main/logo.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "https://raw.githubusercontent.com/Shangyi69/psonlineshop/main/logo.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "any maskable"
22
+ }
23
+ ]
24
+ }
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
payment.html ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="my">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Coins ဝယ်ယူပါ — Recap Studio</title>
7
+ <style>
8
+ *{box-sizing:border-box;margin:0;padding:0}
9
+ body{font-family:'Segoe UI',sans-serif;background:#0f0f1a;color:#e0e0ff;min-height:100vh;padding:20px}
10
+ .container{max-width:520px;margin:0 auto}
11
+ h1{text-align:center;font-size:1.5rem;margin-bottom:6px;color:#a78bfa}
12
+ .subtitle{text-align:center;color:#888;font-size:.85rem;margin-bottom:24px}
13
+ .back-btn{display:inline-flex;align-items:center;gap:6px;color:#a78bfa;text-decoration:none;font-size:.85rem;margin-bottom:18px}
14
+ .back-btn:hover{color:#c4b5fd}
15
+
16
+ /* Packages */
17
+ .packages{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px}
18
+ .pkg{border:2px solid #2a2a4a;border-radius:12px;padding:16px;cursor:pointer;transition:.2s;background:#16162a;text-align:center;position:relative}
19
+ .pkg:hover{border-color:#7c3aed;transform:translateY(-2px)}
20
+ .pkg.selected{border-color:#a78bfa;background:#1e1440}
21
+ .pkg.popular::before{content:'🔥 Popular';position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:#7c3aed;color:#fff;font-size:.7rem;padding:2px 10px;border-radius:20px;white-space:nowrap}
22
+ .pkg-coins{font-size:1.6rem;font-weight:700;color:#a78bfa}
23
+ .pkg-unit{font-size:.8rem;color:#888;margin-bottom:4px}
24
+ .pkg-price{font-size:.95rem;color:#fbbf24;font-weight:600}
25
+
26
+ /* KBZ Pay info */
27
+ .kbz-card{background:#1a1a2e;border:1px solid #2a2a4a;border-radius:12px;padding:18px;margin-bottom:20px}
28
+ .kbz-title{font-size:.85rem;color:#888;margin-bottom:10px}
29
+ .kbz-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
30
+ .kbz-label{font-size:.8rem;color:#888}
31
+ .kbz-val{font-size:1rem;font-weight:700;color:#e0e0ff;letter-spacing:.5px}
32
+ .copy-btn{background:#2a2a4a;border:none;color:#a78bfa;font-size:.75rem;padding:4px 10px;border-radius:6px;cursor:pointer;transition:.2s}
33
+ .copy-btn:hover{background:#3a3a6a}
34
+ .selected-pkg-info{text-align:center;font-size:.9rem;color:#fbbf24;margin-bottom:12px;min-height:22px}
35
+
36
+ /* Slip upload */
37
+ .slip-section{margin-bottom:20px}
38
+ .slip-label{font-size:.85rem;color:#888;margin-bottom:8px;display:block}
39
+ .slip-drop{border:2px dashed #3a3a6a;border-radius:12px;padding:30px;text-align:center;cursor:pointer;transition:.2s;position:relative;overflow:hidden}
40
+ .slip-drop:hover,.slip-drop.drag{border-color:#a78bfa;background:#1a1440}
41
+ .slip-drop input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
42
+ .slip-preview{width:100%;max-height:200px;object-fit:contain;border-radius:8px;display:none;margin-top:10px}
43
+ .slip-icon{font-size:2rem;margin-bottom:8px}
44
+ .slip-text{font-size:.85rem;color:#888}
45
+
46
+ /* Submit */
47
+ .submit-btn{width:100%;padding:14px;border:none;border-radius:12px;background:linear-gradient(135deg,#7c3aed,#a855f7);color:#fff;font-size:1rem;font-weight:700;cursor:pointer;transition:.2s}
48
+ .submit-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 20px rgba(124,58,237,.5)}
49
+ .submit-btn:disabled{opacity:.5;cursor:not-allowed}
50
+
51
+ /* Toast */
52
+ .toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#1e1440;border:1px solid #7c3aed;color:#e0e0ff;padding:12px 24px;border-radius:12px;font-size:.9rem;opacity:0;transition:.3s;pointer-events:none;z-index:999;white-space:nowrap}
53
+ .toast.show{opacity:1}
54
+ .toast.error{border-color:#ef4444;background:#2a0a0a}
55
+ .toast.success{border-color:#22c55e;background:#0a2a0a}
56
+
57
+ .history-link{display:block;text-align:center;color:#a78bfa;font-size:.85rem;margin-top:16px;text-decoration:none}
58
+ .history-link:hover{text-decoration:underline}
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <div class="container">
63
+ <a href="/app" class="back-btn">← App သို့ပြန်သွားရန်</a>
64
+ <h1>🪙 Coins ဝယ်ယူပါ</h1>
65
+ <p class="subtitle">KBZ Pay ဖြင့် ငွေပေးချေပြီး Slip တင်ပေးပါ</p>
66
+
67
+ <!-- Packages -->
68
+ <div class="packages" id="packages"></div>
69
+ <div class="selected-pkg-info" id="selectedInfo">Package တစ်ခု ရွေးချယ်ပါ</div>
70
+
71
+ <!-- KBZ Pay Info -->
72
+ <div class="kbz-card">
73
+ <div class="kbz-title">💳 KBZ Pay ငွေလွှဲရမည့် အချက်အလက်</div>
74
+ <div class="kbz-row">
75
+ <span class="kbz-label">Name</span>
76
+ <div style="display:flex;align-items:center;gap:8px">
77
+ <span class="kbz-val" id="kbzName">—</span>
78
+ <button class="copy-btn" onclick="copyText('kbzName')">Copy</button>
79
+ </div>
80
+ </div>
81
+ <div class="kbz-row">
82
+ <span class="kbz-label">Phone</span>
83
+ <div style="display:flex;align-items:center;gap:8px">
84
+ <span class="kbz-val" id="kbzNum">—</span>
85
+ <button class="copy-btn" onclick="copyText('kbzNum')">Copy</button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Slip Upload -->
91
+ <div class="slip-section">
92
+ <span class="slip-label">📸 ငွေလွှဲ Slip ပုံ တင်ပေးပါ</span>
93
+ <div class="slip-drop" id="slipDrop">
94
+ <input type="file" accept="image/*" id="slipInput" onchange="handleSlip(this)">
95
+ <div class="slip-icon">📎</div>
96
+ <div class="slip-text">Slip ပုံကို ထိုနေရာတွင် ထည့်ပါ သို့မဟုတ် နှိပ်ပါ</div>
97
+ <img id="slipPreview" class="slip-preview" alt="Slip Preview">
98
+ </div>
99
+ </div>
100
+
101
+ <button class="submit-btn" id="submitBtn" onclick="submitPayment()" disabled>
102
+ 💰 Payment တင်ပေးပါ
103
+ </button>
104
+ <a href="/payment-history" class="history-link">📋 ငွေပေးချေမှု မှတ်တမ်း ကြည့်ရန်</a>
105
+ </div>
106
+ <div class="toast" id="toast"></div>
107
+
108
+ <script>
109
+ let username = '';
110
+ let selectedPkg = null;
111
+ let slipBase64 = null;
112
+ const packages = [];
113
+
114
+ async function init() {
115
+ // Read from same sessionStorage key used by index.html
116
+ try {
117
+ const saved = sessionStorage.getItem('recap_user');
118
+ if (!saved) { window.location.href = '/login'; return; }
119
+ const sess = JSON.parse(saved);
120
+ username = sess.u || '';
121
+ } catch(e) { window.location.href = '/login'; return; }
122
+ if (!username) { window.location.href = '/login'; return; }
123
+ const r = await fetch('/api/payment/packages').then(r => r.json());
124
+ r.packages.forEach((p, i) => packages.push(p));
125
+ document.getElementById('kbzName').textContent = r.kbz_name;
126
+ document.getElementById('kbzNum').textContent = r.kbz_number;
127
+ renderPackages();
128
+ }
129
+
130
+ function renderPackages() {
131
+ const el = document.getElementById('packages');
132
+ el.innerHTML = packages.map((p, i) => `
133
+ <div class="pkg ${i===1?'popular':''}" id="pkg${i}" onclick="selectPkg(${i})">
134
+ <div class="pkg-coins">${p.coins}</div>
135
+ <div class="pkg-unit">Coins</div>
136
+ <div class="pkg-price">${p.price}</div>
137
+ </div>
138
+ `).join('');
139
+ }
140
+
141
+ function selectPkg(i) {
142
+ document.querySelectorAll('.pkg').forEach(el => el.classList.remove('selected'));
143
+ document.getElementById('pkg'+i).classList.add('selected');
144
+ selectedPkg = packages[i];
145
+ document.getElementById('selectedInfo').textContent =
146
+ `✅ ${selectedPkg.coins} Coins — ${selectedPkg.price} ရွေးချယ်ထားသည်`;
147
+ checkReady();
148
+ }
149
+
150
+ function handleSlip(input) {
151
+ const file = input.files[0];
152
+ if (!file) return;
153
+ if (file.size > 5 * 1024 * 1024) { showToast('❌ ပုံဆိုဒ် 5MB ထက် မကြီးပါနှင့်', 'error'); return; }
154
+ const reader = new FileReader();
155
+ reader.onload = e => {
156
+ slipBase64 = e.target.result;
157
+ const img = document.getElementById('slipPreview');
158
+ img.src = slipBase64;
159
+ img.style.display = 'block';
160
+ document.querySelector('.slip-icon').style.display = 'none';
161
+ document.querySelector('.slip-text').style.display = 'none';
162
+ checkReady();
163
+ };
164
+ reader.readAsDataURL(file);
165
+ }
166
+
167
+ function checkReady() {
168
+ document.getElementById('submitBtn').disabled = !(selectedPkg && slipBase64);
169
+ }
170
+
171
+ async function submitPayment() {
172
+ if (!selectedPkg || !slipBase64) return;
173
+ const btn = document.getElementById('submitBtn');
174
+ btn.disabled = true;
175
+ btn.textContent = '⏳ တင်နေသည်…';
176
+ try {
177
+ const r = await fetch('/api/payment/submit', {
178
+ method: 'POST',
179
+ headers: {'Content-Type':'application/json'},
180
+ body: JSON.stringify({
181
+ username, coins: selectedPkg.coins,
182
+ price: selectedPkg.price, slip_image: slipBase64
183
+ })
184
+ }).then(r => r.json());
185
+ if (r.ok) {
186
+ showToast(r.msg, 'success');
187
+ setTimeout(() => window.location.href = '/payment-history', 2000);
188
+ } else {
189
+ showToast(r.msg, 'error');
190
+ btn.disabled = false;
191
+ btn.textContent = '💰 Payment တင်ပေးပါ';
192
+ }
193
+ } catch(e) {
194
+ showToast('❌ Error: ' + e.message, 'error');
195
+ btn.disabled = false;
196
+ btn.textContent = '💰 Payment တင်ပေးပါ';
197
+ }
198
+ }
199
+
200
+ function copyText(id) {
201
+ const t = document.getElementById(id).textContent;
202
+ navigator.clipboard.writeText(t).then(() => showToast('✅ Copied!', 'success'));
203
+ }
204
+
205
+ function showToast(msg, type='') {
206
+ const t = document.getElementById('toast');
207
+ t.textContent = msg;
208
+ t.className = 'toast show ' + type;
209
+ setTimeout(() => t.className = 'toast', 3000);
210
+ }
211
+
212
+ // Drag over
213
+ const drop = document.getElementById('slipDrop');
214
+ drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('drag'); });
215
+ drop.addEventListener('dragleave', () => drop.classList.remove('drag'));
216
+ drop.addEventListener('drop', e => {
217
+ e.preventDefault(); drop.classList.remove('drag');
218
+ const f = e.dataTransfer.files[0];
219
+ if (f) { document.getElementById('slipInput').files = e.dataTransfer.files; handleSlip({files:[f]}); }
220
+ });
221
+
222
+ init();
223
+ </script>
224
+ </body>
225
+ </html>
payment_history.html ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="my">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Payment မှတ်တမ်း — Recap Studio</title>
7
+ <style>
8
+ *{box-sizing:border-box;margin:0;padding:0}
9
+ body{font-family:'Segoe UI',sans-serif;background:#0f0f1a;color:#e0e0ff;min-height:100vh;padding:20px}
10
+ .container{max-width:600px;margin:0 auto}
11
+ h1{text-align:center;font-size:1.5rem;margin-bottom:6px;color:#a78bfa}
12
+ .subtitle{text-align:center;color:#888;font-size:.85rem;margin-bottom:24px}
13
+ .back-btn{display:inline-flex;align-items:center;gap:6px;color:#a78bfa;text-decoration:none;font-size:.85rem;margin-bottom:18px}
14
+ .back-btn:hover{color:#c4b5fd}
15
+
16
+ .card{background:#16162a;border:1px solid #2a2a4a;border-radius:12px;padding:16px;margin-bottom:12px}
17
+ .card-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px}
18
+ .card-coins{font-size:1.3rem;font-weight:700;color:#a78bfa}
19
+ .badge{font-size:.75rem;padding:3px 10px;border-radius:20px;font-weight:600}
20
+ .badge.pending{background:#3a2a00;color:#fbbf24;border:1px solid #854d0e}
21
+ .badge.approved{background:#052e16;color:#22c55e;border:1px solid #166534}
22
+ .badge.rejected{background:#2a0a0a;color:#ef4444;border:1px solid #7f1d1d}
23
+ .card-price{font-size:.9rem;color:#fbbf24;margin-bottom:4px}
24
+ .card-meta{font-size:.78rem;color:#666;display:flex;gap:12px;flex-wrap:wrap}
25
+ .card-note{margin-top:8px;font-size:.8rem;color:#888;font-style:italic}
26
+ .empty{text-align:center;padding:60px 0;color:#555;font-size:.9rem}
27
+ .buy-btn{display:block;margin:20px auto 0;padding:12px 32px;background:linear-gradient(135deg,#7c3aed,#a855f7);border:none;border-radius:12px;color:#fff;font-size:.95rem;font-weight:700;cursor:pointer;text-decoration:none;text-align:center}
28
+ .refresh-btn{display:block;text-align:center;color:#a78bfa;font-size:.8rem;margin-bottom:16px;cursor:pointer;background:none;border:none}
29
+ .refresh-btn:hover{text-decoration:underline}
30
+ .loading{text-align:center;padding:40px;color:#666}
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <div class="container">
35
+ <a href="/app" class="back-btn">← App သို့ပြန်သွားရန်</a>
36
+ <h1>📋 Payment မှတ်တမ်း</h1>
37
+ <p class="subtitle">ကိုယ်ပိုင် ငွေပေးချေမှု မှတ်တမ်းများ</p>
38
+
39
+ <button class="refresh-btn" onclick="loadHistory()">🔄 Refresh</button>
40
+ <div id="list"><div class="loading">⏳ Loading…</div></div>
41
+ <a href="/payment" class="buy-btn">🪙 Coins ထပ်ဝယ်ရန်</a>
42
+ </div>
43
+
44
+ <script>
45
+ let username = '';
46
+ try {
47
+ const saved = sessionStorage.getItem('recap_user');
48
+ if (saved) username = JSON.parse(saved).u || '';
49
+ } catch(e) {}
50
+ const STATUS_LABEL = { pending: '⏳ Pending', approved: '✅ Approved', rejected: '❌ Rejected' };
51
+
52
+ async function loadHistory() {
53
+ if (!username) { window.location.href = '/login'; return; }
54
+ const el = document.getElementById('list');
55
+ el.innerHTML = '<div class="loading">⏳ Loading…</div>';
56
+ try {
57
+ const r = await fetch(`/api/payment/history?username=${encodeURIComponent(username)}`).then(r => r.json());
58
+ if (!r.ok) { el.innerHTML = `<div class="empty">❌ ${r.msg}</div>`; return; }
59
+ if (!r.payments.length) {
60
+ el.innerHTML = '<div class="empty">📭 Payment မှတ်တမ်း မရှိသေးပါ</div>';
61
+ return;
62
+ }
63
+ el.innerHTML = r.payments.map(p => `
64
+ <div class="card">
65
+ <div class="card-top">
66
+ <div>
67
+ <div class="card-coins">🪙 ${p.coins} Coins</div>
68
+ <div class="card-price">${p.price}</div>
69
+ </div>
70
+ <span class="badge ${p.status}">${STATUS_LABEL[p.status] || p.status}</span>
71
+ </div>
72
+ <div class="card-meta">
73
+ <span>🆔 ${p.id}</span>
74
+ <span>📅 ${p.created_at.slice(0,16).replace('T',' ')}</span>
75
+ ${p.updated_at !== p.created_at ? `<span>🔄 ${p.updated_at.slice(0,16).replace('T',' ')}</span>` : ''}
76
+ </div>
77
+ ${p.admin_note ? `<div class="card-note">📝 ${p.admin_note}</div>` : ''}
78
+ </div>
79
+ `).join('');
80
+ } catch(e) {
81
+ el.innerHTML = `<div class="empty">❌ Error: ${e.message}</div>`;
82
+ }
83
+ }
84
+
85
+ loadHistory();
86
+ </script>
87
+ </body>
88
+ </html>
privacy.html ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Privacy Policy — Recap Studio</title>
7
+ <style>
8
+ *{box-sizing:border-box;margin:0;padding:0}
9
+ body{font-family:'Segoe UI',sans-serif;background:#0f0f1a;color:#e0e0ff;min-height:100vh;padding:0 0 60px}
10
+ .header{background:#16162a;border-bottom:1px solid #2a2a4a;padding:16px 20px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
11
+ .logo{width:32px;height:32px;background:linear-gradient(135deg,#7c3aed,#a855f7);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.85rem;flex-shrink:0}
12
+ .brand{font-weight:800;font-size:1rem}
13
+ .back{margin-left:auto;color:#a78bfa;text-decoration:none;font-size:.82rem;padding:6px 14px;border:1px solid #3a2a6a;border-radius:8px}
14
+ .back:hover{background:rgba(124,58,237,.15)}
15
+ .container{max-width:720px;margin:0 auto;padding:32px 20px}
16
+ h1{font-size:1.6rem;font-weight:800;margin-bottom:6px;color:#fff}
17
+ .meta{font-size:.8rem;color:#666;margin-bottom:32px}
18
+ h2{font-size:1rem;font-weight:700;color:#a78bfa;margin:28px 0 10px;padding-bottom:6px;border-bottom:1px solid #2a2a4a}
19
+ p{font-size:.88rem;color:#b0b0cc;line-height:1.75;margin-bottom:12px}
20
+ ul{margin:8px 0 12px 18px}
21
+ li{font-size:.88rem;color:#b0b0cc;line-height:1.75;margin-bottom:4px}
22
+ a{color:#a78bfa}
23
+ table{width:100%;border-collapse:collapse;margin:12px 0;font-size:.84rem}
24
+ th{text-align:left;padding:9px 12px;background:#1e1e35;color:#a78bfa;font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;border:1px solid #2a2a4a}
25
+ td{padding:9px 12px;color:#b0b0cc;border:1px solid #2a2a4a;vertical-align:top;line-height:1.6}
26
+ .highlight{background:rgba(124,58,237,.1);border:1px solid rgba(167,139,250,.25);border-radius:10px;padding:14px 16px;margin:16px 0;font-size:.85rem;color:#c4b5fd;line-height:1.7}
27
+ .badge{display:inline-block;background:rgba(52,211,153,.12);border:1px solid rgba(52,211,153,.3);color:#34d399;font-size:.72rem;padding:2px 9px;border-radius:20px;font-weight:600}
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <div class="header">
32
+ <div class="logo">🎬</div>
33
+ <span class="brand">Recap Studio</span>
34
+ <a href="/" class="back">← Back</a>
35
+ </div>
36
+ <div class="container">
37
+ <h1>Privacy Policy</h1>
38
+ <p class="meta">Last updated: March 2025 &nbsp;·&nbsp; Effective immediately</p>
39
+
40
+ <div class="highlight">
41
+ Your privacy matters to us. This policy explains what data we collect, why we collect it, and how you can control it. Recap Studio does not sell your personal data.
42
+ </div>
43
+
44
+ <h2>1. Information We Collect</h2>
45
+
46
+ <table>
47
+ <tr><th>Data Type</th><th>Source</th><th>Purpose</th></tr>
48
+ <tr><td>Email address</td><td>Google OAuth (if used)</td><td>Account identification and login</td></tr>
49
+ <tr><td>Display name</td><td>Google OAuth (if used)</td><td>Personalizing your experience</td></tr>
50
+ <tr><td>Google Account ID</td><td>Google OAuth (if used)</td><td>Linking your Google account to your profile</td></tr>
51
+ <tr><td>Username</td><td>Manual registration</td><td>Account identification</td></tr>
52
+ <tr><td>Password (hashed)</td><td>Manual registration</td><td>Authentication — never stored in plaintext</td></tr>
53
+ <tr><td>Coin balance &amp; usage history</td><td>In-app activity</td><td>Service billing and usage tracking</td></tr>
54
+ <tr><td>Payment slips (images)</td><td>User-uploaded</td><td>Payment verification by admin</td></tr>
55
+ <tr><td>Video URLs &amp; uploaded files</td><td>User-submitted</td><td>Processing your video recap requests</td></tr>
56
+ <tr><td>Telegram Chat ID</td><td>Telegram Bot (optional)</td><td>Sending payment notifications</td></tr>
57
+ <tr><td>Usage stats (video count)</td><td>In-app activity</td><td>Account dashboard display</td></tr>
58
+ </table>
59
+
60
+ <h2>2. How We Use Your Information</h2>
61
+ <ul>
62
+ <li><strong>To provide the Service</strong> — process your video requests, generate scripts, render videos</li>
63
+ <li><strong>To manage your account</strong> — authenticate you, track your coin balance</li>
64
+ <li><strong>To process payments</strong> — verify KBZ Pay transfers and credit your account</li>
65
+ <li><strong>To communicate</strong> — send payment approval/rejection notifications via Telegram (if linked)</li>
66
+ <li><strong>To improve the Service</strong> — understand usage patterns and fix issues</li>
67
+ </ul>
68
+ <p>We do <strong>not</strong> use your data for advertising, profiling, or selling to third parties.</p>
69
+
70
+ <h2>3. Google OAuth &amp; Data Usage</h2>
71
+ <p>If you sign in with Google, we receive the following from Google's OAuth 2.0 service:</p>
72
+ <ul>
73
+ <li>Your <strong>email address</strong> (used as your username/identifier)</li>
74
+ <li>Your <strong>display name</strong> (shown in your account profile)</li>
75
+ <li>Your <strong>Google Account ID</strong> (used to link future logins)</li>
76
+ </ul>
77
+ <p>We do <strong>not</strong> request access to your Gmail, Drive, calendar, or any other Google services. We only use the <code>openid email profile</code> scope — the minimum required for authentication.</p>
78
+ <p>You can revoke Recap Studio's access to your Google account at any time via <a href="https://myaccount.google.com/permissions" target="_blank">Google Account Permissions</a>.</p>
79
+ <p>Our use of Google user data complies with the <a href="https://developers.google.com/terms/api-services-user-data-policy" target="_blank">Google API Services User Data Policy</a>, including the Limited Use requirements.</p>
80
+
81
+ <h2>4. Data Storage &amp; Security</h2>
82
+ <ul>
83
+ <li>User data is stored in JSON format, synced to a private Hugging Face dataset repository</li>
84
+ <li>Passwords are hashed using SHA-256 — plaintext passwords are never stored</li>
85
+ <li>Payment slip images are stored as base64 in the payments database and accessible only to admin</li>
86
+ <li>All data is stored on servers within our hosting infrastructure</li>
87
+ <li>We use HTTPS for all data transmission</li>
88
+ </ul>
89
+ <p>While we implement reasonable security measures, no system is 100% secure. Please use a strong, unique password.</p>
90
+
91
+ <h2>5. Data Sharing</h2>
92
+ <p>We share data with third parties only as necessary to operate the Service:</p>
93
+ <table>
94
+ <tr><th>Third Party</th><th>Data Shared</th><th>Purpose</th></tr>
95
+ <tr><td>Google (Gemini AI)</td><td>Video transcript text</td><td>AI script generation</td></tr>
96
+ <tr><td>Microsoft (Edge TTS)</td><td>Generated script text</td><td>Text-to-speech audio generation</td></tr>
97
+ <tr><td>Hugging Face</td><td>User database (encrypted)</td><td>Database storage and sync</td></tr>
98
+ <tr><td>Telegram</td><td>Admin chat ID, payment notifications</td><td>Payment alerts to admin</td></tr>
99
+ </table>
100
+ <p>We do <strong>not</strong> sell, rent, or trade your personal information to any third party.</p>
101
+
102
+ <h2>6. Data Retention</h2>
103
+ <ul>
104
+ <li><strong>Account data</strong>: Retained while your account is active. Deleted upon account deletion request.</li>
105
+ <li><strong>Payment records</strong>: Retained for 12 months for financial record-keeping</li>
106
+ <li><strong>Payment slip images</strong>: Deleted after payment verification (within 30 days)</li>
107
+ <li><strong>Processed videos</strong>: Stored temporarily on-server and automatically purged</li>
108
+ </ul>
109
+
110
+ <h2>7. Your Rights</h2>
111
+ <p>You have the right to:</p>
112
+ <ul>
113
+ <li><span class="badge">Access</span> — Request a copy of the data we hold about you</li>
114
+ <li><span class="badge">Correction</span> — Request correction of inaccurate data</li>
115
+ <li><span class="badge">Deletion</span> — Request deletion of your account and associated data</li>
116
+ <li><span class="badge">Portability</span> — Request your data in a portable format</li>
117
+ <li><span class="badge">Withdrawal</span> — Revoke Google OAuth access at any time</li>
118
+ </ul>
119
+ <p>To exercise these rights, contact us via Telegram: <a href="https://t.me/PhoeShan2001">@PhoeShan2001</a></p>
120
+
121
+ <h2>8. Cookies &amp; Local Storage</h2>
122
+ <p>Recap Studio uses <code>sessionStorage</code> (browser session storage) to maintain your login session. This data is cleared when you close your browser tab. We do not use third-party tracking cookies or analytics.</p>
123
+
124
+ <h2>9. Children's Privacy</h2>
125
+ <p>Recap Studio is not intended for users under the age of 13. We do not knowingly collect personal information from children. If you believe a child has provided us with personal information, please contact us immediately.</p>
126
+
127
+ <h2>10. Changes to This Policy</h2>
128
+ <p>We may update this Privacy Policy from time to time. The "Last updated" date at the top will reflect changes. We encourage you to review this policy periodically. Continued use of the Service after changes constitutes acceptance.</p>
129
+
130
+ <h2>11. Contact Us</h2>
131
+ <p>For privacy-related inquiries, data requests, or concerns:</p>
132
+ <ul>
133
+ <li>Telegram: <a href="https://t.me/PhoeShan2001">@PhoeShan2001</a></li>
134
+ <li>Website: <a href="https://ai.psonline.shop">ai.psonline.shop</a></li>
135
+ </ul>
136
+ </div>
137
+ </body>
138
+ </html>
requirements.txt CHANGED
@@ -1,4 +1,18 @@
1
- flask>=2.3.0
 
 
 
 
 
 
 
 
 
2
  requests>=2.31.0
3
  feedparser>=6.0.0
4
- yt-dlp>=2024.1.0
 
 
 
 
 
 
1
+ flask==3.1.1
2
+ openai>=1.82.0
3
+ openai-whisper>=20240930
4
+ edge-tts>=6.1.18
5
+ google-genai>=1.16.1
6
+ huggingface_hub>=0.30.2
7
+ yt-dlp[default]>=2025.11.12
8
+ yt-dlp-ejs
9
+ gunicorn
10
+ qrcode[pil]
11
  requests>=2.31.0
12
  feedparser>=6.0.0
13
+ tiktok-uploader
14
+ playwright
15
+ pydub
16
+ # ── Telegram Bot (added) ──
17
+ python-telegram-bot[job-queue]>=21.0.0
18
+ aiofiles>=23.2.1
sw.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Recap Studio — Service Worker (PWA)
2
+ const CACHE = 'recap-v1';
3
+ const STATIC = [
4
+ '/',
5
+ '/manifest.json',
6
+ ];
7
+
8
+ // Install: cache static shell
9
+ self.addEventListener('install', e => {
10
+ e.waitUntil(
11
+ caches.open(CACHE).then(c => c.addAll(STATIC))
12
+ );
13
+ self.skipWaiting();
14
+ });
15
+
16
+ // Activate: clean old caches
17
+ self.addEventListener('activate', e => {
18
+ e.waitUntil(
19
+ caches.keys().then(keys =>
20
+ Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
21
+ )
22
+ );
23
+ self.clients.claim();
24
+ });
25
+
26
+ // Fetch: network-first for API, cache-first for shell
27
+ self.addEventListener('fetch', e => {
28
+ const url = new URL(e.request.url);
29
+
30
+ // API calls — always network, no cache
31
+ if (url.pathname.startsWith('/api/') ||
32
+ url.pathname.startsWith('/outputs/') ||
33
+ url.pathname.startsWith('/auth/')) {
34
+ return;
35
+ }
36
+
37
+ // Navigation (HTML pages) — network first, fallback to cache
38
+ if (e.request.mode === 'navigate') {
39
+ e.respondWith(
40
+ fetch(e.request)
41
+ .then(r => {
42
+ const clone = r.clone();
43
+ caches.open(CACHE).then(c => c.put(e.request, clone));
44
+ return r;
45
+ })
46
+ .catch(() => caches.match('/'))
47
+ );
48
+ return;
49
+ }
50
+
51
+ // Static assets — cache first
52
+ e.respondWith(
53
+ caches.match(e.request).then(cached => {
54
+ if (cached) return cached;
55
+ return fetch(e.request).then(r => {
56
+ if (r && r.status === 200 && r.type !== 'opaque') {
57
+ const clone = r.clone();
58
+ caches.open(CACHE).then(c => c.put(e.request, clone));
59
+ }
60
+ return r;
61
+ });
62
+ })
63
+ );
64
+ });
terms.html ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Terms of Service — Recap Studio</title>
7
+ <style>
8
+ *{box-sizing:border-box;margin:0;padding:0}
9
+ body{font-family:'Segoe UI',sans-serif;background:#0f0f1a;color:#e0e0ff;min-height:100vh;padding:0 0 60px}
10
+ .header{background:#16162a;border-bottom:1px solid #2a2a4a;padding:16px 20px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
11
+ .logo{width:32px;height:32px;background:linear-gradient(135deg,#7c3aed,#a855f7);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.85rem;flex-shrink:0}
12
+ .brand{font-weight:800;font-size:1rem}
13
+ .back{margin-left:auto;color:#a78bfa;text-decoration:none;font-size:.82rem;padding:6px 14px;border:1px solid #3a2a6a;border-radius:8px}
14
+ .back:hover{background:rgba(124,58,237,.15)}
15
+ .container{max-width:720px;margin:0 auto;padding:32px 20px}
16
+ h1{font-size:1.6rem;font-weight:800;margin-bottom:6px;color:#fff}
17
+ .meta{font-size:.8rem;color:#666;margin-bottom:32px}
18
+ h2{font-size:1rem;font-weight:700;color:#a78bfa;margin:28px 0 10px;padding-bottom:6px;border-bottom:1px solid #2a2a4a}
19
+ p{font-size:.88rem;color:#b0b0cc;line-height:1.75;margin-bottom:12px}
20
+ ul{margin:8px 0 12px 18px}
21
+ li{font-size:.88rem;color:#b0b0cc;line-height:1.75;margin-bottom:4px}
22
+ a{color:#a78bfa}
23
+ .card{background:#16162a;border:1px solid #2a2a4a;border-radius:12px;padding:20px;margin-bottom:12px}
24
+ .highlight{background:rgba(124,58,237,.1);border:1px solid rgba(167,139,250,.25);border-radius:10px;padding:14px 16px;margin:16px 0;font-size:.85rem;color:#c4b5fd;line-height:1.7}
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <div class="header">
29
+ <div class="logo">🎬</div>
30
+ <span class="brand">Recap Studio</span>
31
+ <a href="/" class="back">← Back</a>
32
+ </div>
33
+ <div class="container">
34
+ <h1>Terms of Service</h1>
35
+ <p class="meta">Last updated: March 2025 &nbsp;·&nbsp; Effective immediately</p>
36
+
37
+ <div class="highlight">
38
+ By accessing or using Recap Studio, you agree to these Terms of Service. Please read them carefully before using the service.
39
+ </div>
40
+
41
+ <h2>1. Acceptance of Terms</h2>
42
+ <p>By creating an account or using Recap Studio ("Service", "we", "us", "our"), you agree to be bound by these Terms of Service and all applicable laws and regulations. If you do not agree with any of these terms, you are prohibited from using this Service.</p>
43
+
44
+ <h2>2. Description of Service</h2>
45
+ <p>Recap Studio is an AI-powered video processing platform that allows users to:</p>
46
+ <ul>
47
+ <li>Download and process videos from supported platforms (YouTube, TikTok, Facebook, Instagram)</li>
48
+ <li>Generate AI-written scripts and voiceovers in Myanmar, Thai, and English</li>
49
+ <li>Create recap videos with custom overlays, watermarks, and branding</li>
50
+ <li>Manage and download processed video content</li>
51
+ </ul>
52
+
53
+ <h2>3. User Accounts</h2>
54
+ <p>You may register using a username/password or via Google OAuth. You are responsible for:</p>
55
+ <ul>
56
+ <li>Maintaining the confidentiality of your account credentials</li>
57
+ <li>All activities that occur under your account</li>
58
+ <li>Providing accurate information during registration</li>
59
+ <li>Notifying us immediately of any unauthorized account use</li>
60
+ </ul>
61
+ <p>We reserve the right to suspend or terminate accounts that violate these terms.</p>
62
+
63
+ <h2>4. Coin System and Payments</h2>
64
+ <p>Recap Studio uses a coin-based credit system:</p>
65
+ <ul>
66
+ <li>Coins are purchased via KBZ Pay and are non-refundable once processed</li>
67
+ <li>Transcript generation costs 1 coin; full video processing costs 2 coins</li>
68
+ <li>Coin balances do not expire but have no cash value</li>
69
+ <li>We reserve the right to adjust coin prices or costs with reasonable notice</li>
70
+ <li>Payments are manually reviewed and approved by our admin team within 24 hours</li>
71
+ </ul>
72
+
73
+ <h2>5. Acceptable Use</h2>
74
+ <p>You agree NOT to use Recap Studio to:</p>
75
+ <ul>
76
+ <li>Process or distribute content that infringes copyright or intellectual property rights</li>
77
+ <li>Create content that is illegal, harmful, defamatory, or misleading</li>
78
+ <li>Violate the Terms of Service of any third-party platform (YouTube, TikTok, etc.)</li>
79
+ <li>Attempt to reverse-engineer, hack, or disrupt the Service</li>
80
+ <li>Use automated scripts or bots to access the Service beyond normal use</li>
81
+ <li>Resell or redistribute the Service without express written permission</li>
82
+ </ul>
83
+
84
+ <h2>6. Intellectual Property</h2>
85
+ <p>You retain ownership of content you submit. By using the Service, you grant us a limited license to process your content solely for the purpose of providing the Service. We do not claim ownership of your videos or AI-generated scripts.</p>
86
+ <p>The Recap Studio platform, branding, and underlying technology are our intellectual property. You may not copy, modify, or distribute them without permission.</p>
87
+
88
+ <h2>7. Third-Party Services</h2>
89
+ <p>Recap Studio integrates with third-party services including Google (OAuth), Gemini AI, Microsoft Edge TTS, and Hugging Face. Your use of these services is also subject to their respective terms and privacy policies. We are not responsible for third-party service availability or accuracy.</p>
90
+
91
+ <h2>8. Content Disclaimer</h2>
92
+ <p>AI-generated scripts and summaries may contain inaccuracies. You are responsible for reviewing content before publishing. We do not guarantee the accuracy, completeness, or legality of AI-generated output.</p>
93
+
94
+ <h2>9. Service Availability</h2>
95
+ <p>We strive to maintain high availability but do not guarantee uninterrupted access. We may perform maintenance, updates, or suspend the Service at any time without prior notice. We are not liable for losses resulting from Service downtime.</p>
96
+
97
+ <h2>10. Limitation of Liability</h2>
98
+ <p>To the maximum extent permitted by law, Recap Studio and its operators shall not be liable for any indirect, incidental, special, or consequential damages arising from your use of the Service, including but not limited to loss of data, revenue, or profits.</p>
99
+
100
+ <h2>11. Termination</h2>
101
+ <p>We may terminate or suspend your access immediately, without prior notice, for conduct that we believe violates these Terms or is harmful to other users, the Service, or third parties.</p>
102
+
103
+ <h2>12. Changes to Terms</h2>
104
+ <p>We reserve the right to modify these terms at any time. Continued use of the Service after changes constitutes acceptance of the new terms. We will make reasonable efforts to notify users of significant changes.</p>
105
+
106
+ <h2>13. Governing Law</h2>
107
+ <p>These Terms shall be governed by applicable law. Any disputes shall be resolved through good-faith negotiation before pursuing legal remedies.</p>
108
+
109
+ <h2>14. Contact</h2>
110
+ <p>For questions about these Terms, contact us via Telegram: <a href="https://t.me/PhoeShan2001">@PhoeShan2001</a> or through the in-app support channel.</p>
111
+ </div>
112
+ </body>
113
+ </html>