Upload 4 files
Browse files- Dockerfile +12 -20
- app.py +0 -170
- recap_tg_bot.py +225 -0
- start.sh +48 -6
Dockerfile
CHANGED
|
@@ -13,7 +13,7 @@ ENV PATH="$DENO_INSTALL/bin:$PATH"
|
|
| 13 |
|
| 14 |
WORKDIR /app
|
| 15 |
|
| 16 |
-
# numpy
|
| 17 |
RUN pip install --no-cache-dir "numpy<2"
|
| 18 |
|
| 19 |
# torch CPU only
|
|
@@ -24,33 +24,36 @@ RUN pip install --no-cache-dir \
|
|
| 24 |
COPY requirements.txt .
|
| 25 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 26 |
|
| 27 |
-
# yt-dlp + openai-whisper
|
| 28 |
RUN pip install --no-cache-dir -U "yt-dlp[default,curl-cffi]"
|
| 29 |
RUN pip install --no-cache-dir openai-whisper
|
| 30 |
|
| 31 |
-
# App files
|
| 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
|
|
|
|
| 40 |
|
| 41 |
-
# Optional
|
| 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
|
| 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
|
| 54 |
RUN mkdir -p /app/fc_conf && \
|
| 55 |
printf '<?xml version="1.0"?>\n\
|
| 56 |
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">\n\
|
|
@@ -62,21 +65,10 @@ RUN mkdir -p /app/fc_conf && \
|
|
| 62 |
</fontconfig>\n' > /app/fc_conf/fonts.conf
|
| 63 |
|
| 64 |
# Pre-cache Whisper tiny model
|
| 65 |
-
RUN python -c "import whisper;
|
| 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", "-"]
|
|
|
|
| 13 |
|
| 14 |
WORKDIR /app
|
| 15 |
|
| 16 |
+
# numpy first (before torch)
|
| 17 |
RUN pip install --no-cache-dir "numpy<2"
|
| 18 |
|
| 19 |
# torch CPU only
|
|
|
|
| 24 |
COPY requirements.txt .
|
| 25 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 26 |
|
| 27 |
+
# yt-dlp + openai-whisper
|
| 28 |
RUN pip install --no-cache-dir -U "yt-dlp[default,curl-cffi]"
|
| 29 |
RUN pip install --no-cache-dir openai-whisper
|
| 30 |
|
| 31 |
+
# App files
|
| 32 |
COPY app.py .
|
| 33 |
+
COPY recap_tg_bot.py .
|
| 34 |
COPY index.html .
|
| 35 |
COPY privacy.html .
|
| 36 |
COPY terms.html .
|
| 37 |
COPY NotoSansMyanmar-Bold.ttf .
|
| 38 |
COPY manifest.json .
|
| 39 |
COPY sw.js .
|
| 40 |
+
COPY start.sh .
|
| 41 |
+
RUN chmod +x start.sh
|
| 42 |
|
| 43 |
+
# Optional files (build won't fail if missing)
|
| 44 |
COPY payment.html* ./
|
| 45 |
COPY payment_history.html* ./
|
| 46 |
+
COPY m_youtube_com_cookies.txt* ./
|
| 47 |
|
| 48 |
# Directories
|
| 49 |
RUN mkdir -p outputs slips temp_prev temp_thumb temp_
|
| 50 |
|
| 51 |
+
# Myanmar font
|
| 52 |
RUN mkdir -p /usr/local/share/fonts/myanmar \
|
| 53 |
&& cp /app/NotoSansMyanmar-Bold.ttf /usr/local/share/fonts/myanmar/ \
|
| 54 |
&& fc-cache -fv
|
| 55 |
|
| 56 |
+
# Isolated fonts.conf
|
| 57 |
RUN mkdir -p /app/fc_conf && \
|
| 58 |
printf '<?xml version="1.0"?>\n\
|
| 59 |
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">\n\
|
|
|
|
| 65 |
</fontconfig>\n' > /app/fc_conf/fonts.conf
|
| 66 |
|
| 67 |
# Pre-cache Whisper tiny model
|
| 68 |
+
RUN python -c "import whisper; whisper.load_model('tiny', device='cpu'); print('Whisper cached')"
|
| 69 |
|
| 70 |
# Sanity check
|
| 71 |
RUN deno --version && yt-dlp --version && python -c "import numpy; print('numpy', numpy.__version__)"
|
| 72 |
|
| 73 |
EXPOSE 7860
|
| 74 |
+
CMD ["./start.sh"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1093,178 +1093,8 @@ def run_gemini_preview(voice_name, out_path):
|
|
| 1093 |
# ═════════════════════════════════════════════════════════════
|
| 1094 |
# TELEGRAM BOT — inline polling (runs as daemon thread inside app.py)
|
| 1095 |
# ═════════════════════════════════════════════════════════════
|
| 1096 |
-
import os, json, time, threading
|
| 1097 |
-
import requests as _rq
|
| 1098 |
-
|
| 1099 |
-
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
|
| 1100 |
-
ADMIN_TELEGRAM_CHAT_ID = os.getenv('ADMIN_TELEGRAM_CHAT_ID', '')
|
| 1101 |
-
|
| 1102 |
-
def _bot_tg(method, **kw):
|
| 1103 |
-
try:
|
| 1104 |
-
r = _rq.post(
|
| 1105 |
-
'https://api.telegram.org/bot' + TELEGRAM_BOT_TOKEN + '/' + method,
|
| 1106 |
-
json=kw, timeout=30
|
| 1107 |
-
)
|
| 1108 |
-
return r.json()
|
| 1109 |
-
except Exception as e:
|
| 1110 |
-
print('[BOT] ' + method + ' error: ' + str(e))
|
| 1111 |
-
return {'ok': False}
|
| 1112 |
-
|
| 1113 |
-
def _bot_send(chat_id, text, reply_markup=None, parse_mode='HTML'):
|
| 1114 |
-
kw = {'chat_id': chat_id, 'text': text, 'parse_mode': parse_mode,
|
| 1115 |
-
'disable_web_page_preview': True}
|
| 1116 |
-
if reply_markup:
|
| 1117 |
-
kw['reply_markup'] = reply_markup
|
| 1118 |
-
r = _bot_tg('sendMessage', **kw)
|
| 1119 |
-
return r.get('result', {}).get('message_id')
|
| 1120 |
-
|
| 1121 |
-
def _bot_inline_kb():
|
| 1122 |
-
wu = os.getenv('RECAP_WEBAPP_URL', 'https://recap.psonline.shop')
|
| 1123 |
-
return {'inline_keyboard': [[
|
| 1124 |
-
{'text': '\U0001f3ac Recap Studio \u1016\u103d\u1004\u103a\u1019\u100a\u103a', 'web_app': {'url': wu}}
|
| 1125 |
-
]]}
|
| 1126 |
-
|
| 1127 |
-
def _bot_reply_kb():
|
| 1128 |
-
wu = os.getenv('RECAP_WEBAPP_URL', 'https://recap.psonline.shop')
|
| 1129 |
-
return {
|
| 1130 |
-
'keyboard': [[{'text': '\U0001f3ac Recap Studio', 'web_app': {'url': wu}}]],
|
| 1131 |
-
'resize_keyboard': True, 'persistent': True,
|
| 1132 |
-
}
|
| 1133 |
-
|
| 1134 |
-
def _bot_dispatch(update, load_db_fn):
|
| 1135 |
-
msg = update.get('message')
|
| 1136 |
-
if not msg:
|
| 1137 |
-
return
|
| 1138 |
-
text = (msg.get('text') or '').strip()
|
| 1139 |
-
chat_id = msg['chat']['id']
|
| 1140 |
-
fname = msg.get('from', {}).get('first_name', 'User')
|
| 1141 |
-
tg_id = str(msg.get('from', {}).get('id', ''))
|
| 1142 |
-
if not text:
|
| 1143 |
-
return
|
| 1144 |
-
cmd = text.split()[0].split('@')[0].lower() if text.startswith('/') else ''
|
| 1145 |
-
print('[BOT] chat=' + str(chat_id) + ' cmd=' + repr(cmd))
|
| 1146 |
-
|
| 1147 |
-
if cmd == '/start':
|
| 1148 |
-
body = (
|
| 1149 |
-
'\U0001f44b \u1019\u1004\u103a\u1039\u1002\u101c\u102c\u1015\u102b <b>' + fname + '</b>!\n\n'
|
| 1150 |
-
'\U0001f3ac <b>Recap Studio</b> \u1019\u103e \u1000\u103c\u102d\u102f\u1006\u102d\u102f\u1015\u102b\u101e\u100a\u103a\u1015\u103e\u1004\u103a\u101e\u100a\u103a\u104b\n\n'
|
| 1151 |
-
'AI \u1016\u103c\u1004\u103a\u103d \u1019\u103c\u1014\u103a\u1019\u102c\u1018\u102c\u101e Movie Recap \u1017\u102e\u1012\u102e\u101a\u102c\u1038\u1019\u103b\u102c\u1038 '
|
| 1152 |
-
'\u1021\u101c\u102d\u102f\u1021\u101c\u103b\u1031\u102c\u1000\u103a \u1011\u102f\u1010\u103a\u101c\u102f\u1015\u103a\u1015\u1031\u1038\u101e\u1031\u102c app \u1016\u103c\u1005\u103a\u101e\u100a\u103a\u104b\n\n'
|
| 1153 |
-
'\u2b07\ufe0f \u1021\u1031\u102c\u1000\u103a\u1015\u102b\u1038 button \u1014\u103e\u102d\u1015\u103a\u1014\u103e\u102d\u1015\u103a \u1016\u103d\u1004\u103a\u1015\u102b\u104b'
|
| 1154 |
-
)
|
| 1155 |
-
_bot_send(chat_id, body, reply_markup=_bot_reply_kb())
|
| 1156 |
-
|
| 1157 |
-
elif cmd == '/help':
|
| 1158 |
-
body = (
|
| 1159 |
-
'<b>\U0001f4d6 Recap Studio \u2014 \u101e\u102f\u1038\u1014\u100a\u103a\u1038</b>\n\n'
|
| 1160 |
-
'1\ufe0f\u20e3 /start \u1014\u103e\u102d\u1015\u103a\u1015\u102b\n'
|
| 1161 |
-
'2\ufe0f\u20e3 <b>\U0001f3ac Recap Studio</b> button \u1014\u103e\u102d\u1015\u103a\u1015\u102b\n'
|
| 1162 |
-
'3\ufe0f\u20e3 Video URL \u1011\u100a\u103a\u103e\u1015\u102b\n'
|
| 1163 |
-
'4\ufe0f\u20e3 Settings \u1001\u103b\u1031\u1014\u103e\u102d\u1015\u103a\u1015\u103c\u102e\u1038 <b>Auto Process</b> \u1014\u103e\u102d\u1015\u103a\u1015\u102b\n'
|
| 1164 |
-
'5\ufe0f\u20e3 App \u1015\u102d\u1010\u103a\u1004\u103a progress + video \u1023 chat \u1011\u1032 \u101b\u1031\u102c\u1000\u103a\u1019\u100a\u103a\n\n'
|
| 1165 |
-
'<b>\U0001f4b0 Coins:</b>\n'
|
| 1166 |
-
'• Process \u1010\u1005\u103a\u1001\u102f = 1 Coin\n'
|
| 1167 |
-
'• App \u1011\u1032 Buy Coins \u1014\u103e\u102d\u1015\u103a\u1010\u100a\u103a\u101d\u101a\u1015\u102b\n\n'
|
| 1168 |
-
'<b>Commands:</b>\n'
|
| 1169 |
-
'/start \u2014 App \u1016\u103d\u1004\u103a\u1019\u100a\u103a\n'
|
| 1170 |
-
'/coins \u2014 Coin \u101c\u1000\u103a\u1000\u103b\u1014\u103a \u1005\u1005\u103a\u1019\u100a\u103a\n'
|
| 1171 |
-
'/help \u2014 \u1023 message'
|
| 1172 |
-
)
|
| 1173 |
-
_bot_send(chat_id, body, reply_markup=_bot_inline_kb())
|
| 1174 |
-
|
| 1175 |
-
elif cmd == '/coins':
|
| 1176 |
-
ukey = 'user_' + tg_id
|
| 1177 |
-
db = load_db_fn()
|
| 1178 |
-
udata = db['users'].get(ukey, {})
|
| 1179 |
-
if udata:
|
| 1180 |
-
body = (
|
| 1181 |
-
'\U0001fa99 <b>' + ukey + '</b>\n\n'
|
| 1182 |
-
'\u101c\u1000\u103a\u1000\u103b\u1014\u103a Coins: <b>' + str(udata.get('coins', 0)) + '</b>\n\n'
|
| 1183 |
-
'Coins \u1000\u103a\u101d\u101a\u1000\u103a App \u1016\u103d\u1004\u103a\u1015\u102b \U0001f447'
|
| 1184 |
-
)
|
| 1185 |
-
else:
|
| 1186 |
-
body = '\u274c Account \u1019\u1010\u103d\u1031\u1037\u1015\u102b\u104b\n/start \u1014\u103e\u102d\u1015\u103a\u1014\u103e\u102d\u1015\u103a app \u1000\u1014\u1031 login \u1000\u103d\u1004\u103a\u1015\u102b\u104b'
|
| 1187 |
-
_bot_send(chat_id, body, reply_markup=_bot_inline_kb())
|
| 1188 |
-
|
| 1189 |
-
elif cmd == '/broadcast' and str(chat_id) == str(ADMIN_TELEGRAM_CHAT_ID):
|
| 1190 |
-
parts = text.split(None, 1)
|
| 1191 |
-
if len(parts) < 2:
|
| 1192 |
-
_bot_send(chat_id, '\u274c Usage: /broadcast <message>')
|
| 1193 |
-
return
|
| 1194 |
-
btext = parts[1]
|
| 1195 |
-
db = load_db_fn()
|
| 1196 |
-
tids = [str(v.get('tg_chat_id') or v.get('telegram_id',''))
|
| 1197 |
-
for v in db['users'].values()
|
| 1198 |
-
if v.get('tg_chat_id') or v.get('telegram_id')]
|
| 1199 |
-
sent = 0
|
| 1200 |
-
for tid in tids:
|
| 1201 |
-
if tid and _bot_send(tid, '\U0001f4e2 <b>Recap Studio</b>\n\n' + btext):
|
| 1202 |
-
sent += 1
|
| 1203 |
-
time.sleep(0.05)
|
| 1204 |
-
_bot_send(chat_id, '\u2705 Sent to ' + str(sent) + '/' + str(len(tids)) + ' users.')
|
| 1205 |
-
|
| 1206 |
-
else:
|
| 1207 |
-
_bot_send(chat_id, '\U0001f3ac Recap Studio \u1016\u103d\u1004\u103a\u101b\u1014\u103a:',
|
| 1208 |
-
reply_markup=_bot_inline_kb())
|
| 1209 |
-
|
| 1210 |
-
def _bot_polling_loop(load_db_fn):
|
| 1211 |
-
if not TELEGRAM_BOT_TOKEN:
|
| 1212 |
-
print('[BOT] No TELEGRAM_BOT_TOKEN — bot disabled')
|
| 1213 |
-
return
|
| 1214 |
-
|
| 1215 |
-
# Clear webhook — use requests (urllib SSL fails on HuggingFace)
|
| 1216 |
-
for attempt in range(1, 4):
|
| 1217 |
-
try:
|
| 1218 |
-
r = _rq.post(
|
| 1219 |
-
'https://api.telegram.org/bot' + TELEGRAM_BOT_TOKEN + '/deleteWebhook',
|
| 1220 |
-
json={'drop_pending_updates': True}, timeout=15
|
| 1221 |
-
)
|
| 1222 |
-
data = r.json()
|
| 1223 |
-
if data.get('ok'):
|
| 1224 |
-
print('[BOT] Webhook cleared (attempt ' + str(attempt) + ')')
|
| 1225 |
-
break
|
| 1226 |
-
print('[BOT] deleteWebhook not ok: ' + str(data))
|
| 1227 |
-
except Exception as e:
|
| 1228 |
-
print('[BOT] deleteWebhook attempt ' + str(attempt) + ' failed: ' + str(e))
|
| 1229 |
-
time.sleep(3)
|
| 1230 |
-
|
| 1231 |
-
time.sleep(2)
|
| 1232 |
-
print('[BOT] Polling loop started')
|
| 1233 |
-
offset = 0
|
| 1234 |
-
|
| 1235 |
-
while True:
|
| 1236 |
-
try:
|
| 1237 |
-
r = _rq.get(
|
| 1238 |
-
'https://api.telegram.org/bot' + TELEGRAM_BOT_TOKEN + '/getUpdates',
|
| 1239 |
-
params={'offset': offset, 'timeout': 25,
|
| 1240 |
-
'allowed_updates': ['message']},
|
| 1241 |
-
timeout=30
|
| 1242 |
-
)
|
| 1243 |
-
data = r.json()
|
| 1244 |
-
|
| 1245 |
-
if not data.get('ok'):
|
| 1246 |
-
desc = data.get('description', '')
|
| 1247 |
-
print('[BOT] getUpdates not ok: ' + desc)
|
| 1248 |
-
time.sleep(15 if 'conflict' in desc.lower() else 5)
|
| 1249 |
-
continue
|
| 1250 |
-
|
| 1251 |
-
for upd in data.get('result', []):
|
| 1252 |
-
offset = upd['update_id'] + 1
|
| 1253 |
-
try:
|
| 1254 |
-
_bot_dispatch(upd, load_db_fn)
|
| 1255 |
-
except Exception as e:
|
| 1256 |
-
print('[BOT] dispatch error: ' + str(e))
|
| 1257 |
-
|
| 1258 |
-
except Exception as e:
|
| 1259 |
-
err = str(e)
|
| 1260 |
-
if 'timed out' not in err.lower() and 'timeout' not in err.lower():
|
| 1261 |
-
print('[BOT] polling error: ' + err)
|
| 1262 |
-
time.sleep(5)
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
# ── PULL DB ON START ──
|
| 1266 |
threading.Thread(target=pull_db, daemon=True).start()
|
| 1267 |
-
threading.Thread(target=_bot_polling_loop, args=(load_db,), daemon=True).start()
|
| 1268 |
whisper_model = None
|
| 1269 |
|
| 1270 |
# ════════════════════════════════════════
|
|
|
|
| 1093 |
# ═════════════════════════════════════════════════════════════
|
| 1094 |
# TELEGRAM BOT — inline polling (runs as daemon thread inside app.py)
|
| 1095 |
# ═════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
# ── PULL DB ON START ──
|
| 1097 |
threading.Thread(target=pull_db, daemon=True).start()
|
|
|
|
| 1098 |
whisper_model = None
|
| 1099 |
|
| 1100 |
# ════════════════════════════════════════
|
recap_tg_bot.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
recap_tg_bot.py — Recap Studio Telegram Bot
|
| 3 |
+
Uses requests library (urllib SSL fails on HuggingFace)
|
| 4 |
+
|
| 5 |
+
Env vars:
|
| 6 |
+
TELEGRAM_BOT_TOKEN — @BotFather
|
| 7 |
+
RECAP_WEBAPP_URL — https://recap.psonline.shop
|
| 8 |
+
ADMIN_TELEGRAM_CHAT_ID — numeric Telegram ID
|
| 9 |
+
ADMIN_USERNAME — backend admin username
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import os, json, time, logging, requests
|
| 13 |
+
|
| 14 |
+
logging.basicConfig(level=logging.INFO,
|
| 15 |
+
format='%(asctime)s [%(levelname)s] %(message)s')
|
| 16 |
+
log = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
|
| 19 |
+
WEBAPP_URL = os.getenv('RECAP_WEBAPP_URL', 'https://recap.psonline.shop')
|
| 20 |
+
ADMIN_ID = os.getenv('ADMIN_TELEGRAM_CHAT_ID', '')
|
| 21 |
+
ADMIN_U = os.getenv('ADMIN_USERNAME', '')
|
| 22 |
+
API_BASE = f'https://api.telegram.org/bot{BOT_TOKEN}'
|
| 23 |
+
|
| 24 |
+
if not BOT_TOKEN:
|
| 25 |
+
raise RuntimeError('TELEGRAM_BOT_TOKEN not set!')
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ── API helpers ───────────────────────────────────────────────────────────────
|
| 29 |
+
def _tg(method, **kw):
|
| 30 |
+
try:
|
| 31 |
+
r = requests.post(f'{API_BASE}/{method}', json=kw, timeout=30)
|
| 32 |
+
data = r.json()
|
| 33 |
+
if not data.get('ok'):
|
| 34 |
+
log.warning('[BOT] %s not ok: %s', method, data.get('description', ''))
|
| 35 |
+
return data
|
| 36 |
+
except Exception as e:
|
| 37 |
+
log.error('[BOT] %s error: %s', method, e)
|
| 38 |
+
return {'ok': False}
|
| 39 |
+
|
| 40 |
+
def send_msg(chat_id, text, markup=None, parse_mode='HTML'):
|
| 41 |
+
kw = {'chat_id': chat_id, 'text': text, 'parse_mode': parse_mode,
|
| 42 |
+
'disable_web_page_preview': True}
|
| 43 |
+
if markup:
|
| 44 |
+
kw['reply_markup'] = markup
|
| 45 |
+
r = _tg('sendMessage', **kw)
|
| 46 |
+
return r.get('result', {}).get('message_id')
|
| 47 |
+
|
| 48 |
+
def edit_msg(chat_id, msg_id, text, parse_mode='HTML'):
|
| 49 |
+
_tg('editMessageText', chat_id=chat_id, message_id=msg_id,
|
| 50 |
+
text=text, parse_mode=parse_mode)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ── Keyboards ─────────────────────────────────────────────────────────────────
|
| 54 |
+
def inline_kb():
|
| 55 |
+
return {'inline_keyboard': [[
|
| 56 |
+
{'text': '🎬 Recap Studio ဖွင့်မည်', 'web_app': {'url': WEBAPP_URL}}
|
| 57 |
+
]]}
|
| 58 |
+
|
| 59 |
+
def reply_kb():
|
| 60 |
+
return {
|
| 61 |
+
'keyboard': [[{'text': '🎬 Recap Studio', 'web_app': {'url': WEBAPP_URL}}]],
|
| 62 |
+
'resize_keyboard': True, 'persistent': True,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ── Handlers ──────────────────────────────────────────────────────────────────
|
| 67 |
+
def on_start(msg):
|
| 68 |
+
chat_id = msg['chat']['id']
|
| 69 |
+
fname = msg['from'].get('first_name', 'User')
|
| 70 |
+
send_msg(chat_id,
|
| 71 |
+
f'👋 မင်္ဂလာပါ <b>{fname}</b>!\n\n'
|
| 72 |
+
'🎬 <b>Recap Studio</b> မှ ကြိုဆိုပါသည်။\n\n'
|
| 73 |
+
'AI ဖြင့် မြန်မာဘာသာ Movie Recap ဗီဒီယိုများ '
|
| 74 |
+
'အလိုအလျောက် ထုတ်လုပ်ပေးသော app ဖြစ်သည်။\n\n'
|
| 75 |
+
'⬇️ Button နှိပ်၍ ဖွင့်ပါ။',
|
| 76 |
+
markup=reply_kb()
|
| 77 |
+
)
|
| 78 |
+
log.info('/start chat=%s user=%s', chat_id, msg['from'].get('username', fname))
|
| 79 |
+
|
| 80 |
+
def on_help(msg):
|
| 81 |
+
send_msg(msg['chat']['id'],
|
| 82 |
+
'<b>📖 Recap Studio — သုံးနည်း</b>\n\n'
|
| 83 |
+
'1️⃣ /start နှိပ်ပါ\n'
|
| 84 |
+
'2️⃣ <b>🎬 Recap Studio</b> button နှိပ်ပါ\n'
|
| 85 |
+
'3️⃣ Video URL ထည့်ပါ\n'
|
| 86 |
+
'4️⃣ Settings ချိန်ညှိပြီး <b>Auto Process</b> နှိပ်ပါ\n'
|
| 87 |
+
'5️⃣ App ပိတ်၍ progress + video ဤ chat ထဲ ရောက်မည်\n\n'
|
| 88 |
+
'<b>💰 Coins:</b>\n'
|
| 89 |
+
'• Process တစ်ခု = 1 Coin\n'
|
| 90 |
+
'• App ထဲ Buy Coins နှိပ်ဝယ်ပါ\n\n'
|
| 91 |
+
'<b>Commands:</b>\n'
|
| 92 |
+
'/start — App ဖွင့်မည်\n'
|
| 93 |
+
'/coins — Coin လက်ကျန် စစ်မည်\n'
|
| 94 |
+
'/help — ဤ message',
|
| 95 |
+
markup=inline_kb()
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
def on_coins(msg):
|
| 99 |
+
chat_id = msg['chat']['id']
|
| 100 |
+
tg_id = str(msg['from']['id'])
|
| 101 |
+
try:
|
| 102 |
+
r = requests.get(f'{WEBAPP_URL}/api/coins_by_tgid',
|
| 103 |
+
params={'tg_id': tg_id}, timeout=10)
|
| 104 |
+
d = r.json()
|
| 105 |
+
except Exception:
|
| 106 |
+
d = {'ok': False}
|
| 107 |
+
|
| 108 |
+
if d.get('ok'):
|
| 109 |
+
send_msg(chat_id,
|
| 110 |
+
f"🪙 <b>{d.get('username','')}</b>\n\n"
|
| 111 |
+
f"လက်ကျန် Coins: <b>{d.get('coins', 0)}</b>\n\n"
|
| 112 |
+
'Coins ဝယ်ရန် App ဖွင့်ပါ 👇',
|
| 113 |
+
markup=inline_kb()
|
| 114 |
+
)
|
| 115 |
+
else:
|
| 116 |
+
send_msg(chat_id,
|
| 117 |
+
'❌ Account မတွေ့ပါ။\n/start နှိပ်၍ app ကနေ login ဝင်ပါ။',
|
| 118 |
+
markup=inline_kb()
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
def on_broadcast(msg):
|
| 122 |
+
chat_id = msg['chat']['id']
|
| 123 |
+
if str(chat_id) != str(ADMIN_ID):
|
| 124 |
+
send_msg(chat_id, '❌ Admin only.')
|
| 125 |
+
return
|
| 126 |
+
parts = (msg.get('text') or '').split(None, 1)
|
| 127 |
+
if len(parts) < 2:
|
| 128 |
+
send_msg(chat_id, '❌ Usage: /broadcast <message>')
|
| 129 |
+
return
|
| 130 |
+
try:
|
| 131 |
+
r = requests.get(f'{WEBAPP_URL}/api/admin/tg_users',
|
| 132 |
+
params={'caller': ADMIN_U}, timeout=15)
|
| 133 |
+
tg_ids = r.json().get('tg_ids', [])
|
| 134 |
+
except Exception:
|
| 135 |
+
tg_ids = []
|
| 136 |
+
sent = 0
|
| 137 |
+
for tid in tg_ids:
|
| 138 |
+
if send_msg(tid, f'📢 <b>Recap Studio</b>\n\n{parts[1]}'):
|
| 139 |
+
sent += 1
|
| 140 |
+
time.sleep(0.05)
|
| 141 |
+
send_msg(chat_id, f'✅ Sent to {sent}/{len(tg_ids)} users.')
|
| 142 |
+
|
| 143 |
+
def on_unknown(msg):
|
| 144 |
+
send_msg(msg['chat']['id'],
|
| 145 |
+
'🎬 Recap Studio ဖွင့်ရန်:',
|
| 146 |
+
markup=inline_kb())
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ── Dispatcher ────────────────────────────────────────────────────────────────
|
| 150 |
+
def dispatch(update):
|
| 151 |
+
msg = update.get('message')
|
| 152 |
+
if not msg:
|
| 153 |
+
return
|
| 154 |
+
text = (msg.get('text') or '').strip()
|
| 155 |
+
if not text:
|
| 156 |
+
return
|
| 157 |
+
cmd = text.split()[0].split('@')[0].lower() if text.startswith('/') else ''
|
| 158 |
+
log.info('MSG chat=%s cmd=%r', msg['chat']['id'], cmd or text[:20])
|
| 159 |
+
|
| 160 |
+
if cmd == '/start': on_start(msg)
|
| 161 |
+
elif cmd == '/help': on_help(msg)
|
| 162 |
+
elif cmd == '/coins': on_coins(msg)
|
| 163 |
+
elif cmd == '/broadcast': on_broadcast(msg)
|
| 164 |
+
else: on_unknown(msg)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
# ── Startup: clear webhook ────────────────────────────────────────────────────
|
| 168 |
+
def clear_webhook():
|
| 169 |
+
for attempt in range(1, 4):
|
| 170 |
+
try:
|
| 171 |
+
r = requests.post(f'{API_BASE}/deleteWebhook',
|
| 172 |
+
json={'drop_pending_updates': True}, timeout=15)
|
| 173 |
+
if r.json().get('ok'):
|
| 174 |
+
log.info('Webhook cleared (attempt %d)', attempt)
|
| 175 |
+
return
|
| 176 |
+
log.warning('deleteWebhook not ok: %s', r.json())
|
| 177 |
+
except Exception as e:
|
| 178 |
+
log.warning('deleteWebhook attempt %d failed: %s', attempt, e)
|
| 179 |
+
time.sleep(3)
|
| 180 |
+
log.error('Could not clear webhook — polling may conflict')
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# ── Polling loop ──────────────────────────────────────────────────────────────
|
| 184 |
+
def run_polling():
|
| 185 |
+
log.info('Recap Studio Bot — clearing webhook...')
|
| 186 |
+
clear_webhook()
|
| 187 |
+
time.sleep(2)
|
| 188 |
+
log.info('Recap Studio Bot — polling started')
|
| 189 |
+
|
| 190 |
+
offset = 0
|
| 191 |
+
while True:
|
| 192 |
+
try:
|
| 193 |
+
r = requests.get(
|
| 194 |
+
f'{API_BASE}/getUpdates',
|
| 195 |
+
params={'offset': offset, 'timeout': 25,
|
| 196 |
+
'allowed_updates': ['message']},
|
| 197 |
+
timeout=30
|
| 198 |
+
)
|
| 199 |
+
data = r.json()
|
| 200 |
+
|
| 201 |
+
if not data.get('ok'):
|
| 202 |
+
desc = data.get('description', '')
|
| 203 |
+
log.warning('getUpdates not ok: %s', desc)
|
| 204 |
+
time.sleep(15 if 'conflict' in desc.lower() else 5)
|
| 205 |
+
continue
|
| 206 |
+
|
| 207 |
+
for upd in data.get('result', []):
|
| 208 |
+
offset = upd['update_id'] + 1
|
| 209 |
+
try:
|
| 210 |
+
dispatch(upd)
|
| 211 |
+
except Exception as e:
|
| 212 |
+
log.error('dispatch error: %s', e)
|
| 213 |
+
|
| 214 |
+
except requests.exceptions.ReadTimeout:
|
| 215 |
+
pass
|
| 216 |
+
except requests.exceptions.ConnectionError as e:
|
| 217 |
+
log.error('Connection error: %s — retry 10s', e)
|
| 218 |
+
time.sleep(10)
|
| 219 |
+
except Exception as e:
|
| 220 |
+
log.error('Polling error: %s', e)
|
| 221 |
+
time.sleep(5)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
if __name__ == '__main__':
|
| 225 |
+
run_polling()
|
start.sh
CHANGED
|
@@ -1,11 +1,53 @@
|
|
| 1 |
#!/bin/bash
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
python app.py &
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
#!/bin/bash
|
| 2 |
+
# Recap Studio — start Gunicorn + Telegram Bot
|
| 3 |
|
| 4 |
+
PORT=${PORT:-7860}
|
|
|
|
| 5 |
|
| 6 |
+
# Auto-detect RECAP_WEBAPP_URL
|
| 7 |
+
if [ -z "$RECAP_WEBAPP_URL" ]; then
|
| 8 |
+
if [ -n "$RENDER_EXTERNAL_URL" ]; then
|
| 9 |
+
export RECAP_WEBAPP_URL="$RENDER_EXTERNAL_URL"
|
| 10 |
+
elif [ -n "$RAILWAY_PUBLIC_DOMAIN" ]; then
|
| 11 |
+
export RECAP_WEBAPP_URL="https://$RAILWAY_PUBLIC_DOMAIN"
|
| 12 |
+
elif [ -n "$SPACE_HOST" ]; then
|
| 13 |
+
export RECAP_WEBAPP_URL="https://$SPACE_HOST"
|
| 14 |
+
else
|
| 15 |
+
export RECAP_WEBAPP_URL="https://recap.psonline.shop"
|
| 16 |
+
fi
|
| 17 |
+
echo "RECAP_WEBAPP_URL=$RECAP_WEBAPP_URL"
|
| 18 |
+
fi
|
| 19 |
|
| 20 |
+
# Kill stale bot processes
|
| 21 |
+
pkill -f "python.*recap_tg_bot.py" 2>/dev/null || true
|
| 22 |
+
sleep 1
|
| 23 |
|
| 24 |
+
# Start Telegram bot (background)
|
| 25 |
+
if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
|
| 26 |
+
echo "🤖 Starting Telegram bot..."
|
| 27 |
+
python recap_tg_bot.py &
|
| 28 |
+
BOT_PID=$!
|
| 29 |
+
echo " Bot PID: $BOT_PID"
|
| 30 |
+
else
|
| 31 |
+
echo "⚠️ TELEGRAM_BOT_TOKEN not set — bot skipped"
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
# Trap for clean shutdown
|
| 35 |
+
cleanup() {
|
| 36 |
+
echo "🛑 Shutting down..."
|
| 37 |
+
[ -n "$BOT_PID" ] && kill $BOT_PID 2>/dev/null
|
| 38 |
+
exit 0
|
| 39 |
+
}
|
| 40 |
+
trap cleanup SIGTERM SIGINT
|
| 41 |
+
|
| 42 |
+
# Start Gunicorn (foreground — main process)
|
| 43 |
+
echo "🌐 Starting Gunicorn on port $PORT..."
|
| 44 |
+
exec gunicorn app:app \
|
| 45 |
+
--bind "0.0.0.0:$PORT" \
|
| 46 |
+
--workers 1 \
|
| 47 |
+
--threads 8 \
|
| 48 |
+
--worker-class gthread \
|
| 49 |
+
--timeout 120 \
|
| 50 |
+
--keep-alive 5 \
|
| 51 |
+
--log-level info \
|
| 52 |
+
--access-logfile - \
|
| 53 |
+
--error-logfile -
|