Spaces:
Sleeping
Sleeping
Fixed missing folders issue
Browse files- audio_server +0 -1
- audio_server/README.md +10 -0
- audio_server/app.py +147 -0
- audio_server/assets/intro.mp3 +0 -0
- audio_server/assets/outro.mp3 +0 -0
- biologic +0 -1
- biologic/README.md +11 -0
- biologic/index.js +551 -0
- cipher_sticker +0 -1
- cipher_sticker/README.md +10 -0
- cipher_sticker/app.py +192 -0
- math_scan +0 -1
- math_scan/README.md +12 -0
- math_scan/app.py +82 -0
- math_scan/math_solver.py +87 -0
- math_scan/scan.py +34 -0
audio_server
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Subproject commit 87d300defcef4c2b8e8b55db7135aefff9e84f31
|
|
|
|
|
|
audio_server/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Audio Server
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
audio_server/app.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Install requirements:
|
| 2 |
+
# pip install fastapi uvicorn python-multipart ffmpeg-python
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import subprocess
|
| 6 |
+
import math
|
| 7 |
+
from fastapi import FastAPI, File, UploadFile, Form
|
| 8 |
+
from fastapi.responses import FileResponse
|
| 9 |
+
import shutil
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
app = FastAPI()
|
| 13 |
+
|
| 14 |
+
# 📂 ෆයිල් ටික තියාගන්න තැන්j
|
| 15 |
+
UPLOAD_FOLDER = "uploads"
|
| 16 |
+
PROCESSED_FOLDER = "processed"
|
| 17 |
+
ASSETS_FOLDER = "assets" # මෙතන intro.mp3 සහ outro.mp3 තියෙන්න ඕනේ
|
| 18 |
+
|
| 19 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 20 |
+
os.makedirs(PROCESSED_FOLDER, exist_ok=True)
|
| 21 |
+
os.makedirs(ASSETS_FOLDER, exist_ok=True)
|
| 22 |
+
|
| 23 |
+
# 🛠️ 1. DURATION FINDER (සින්දුවේ දිග හොයාගන්න - Outro එකට ඕන වෙනවා)
|
| 24 |
+
def get_audio_duration(file_path):
|
| 25 |
+
cmd = [
|
| 26 |
+
"ffprobe", "-v", "error", "-show_entries", "format=duration",
|
| 27 |
+
"-of", "default=noprint_wrappers=1:nokey=1", file_path
|
| 28 |
+
]
|
| 29 |
+
try:
|
| 30 |
+
output = subprocess.check_output(cmd).decode("utf-8").strip()
|
| 31 |
+
return float(output)
|
| 32 |
+
except:
|
| 33 |
+
return 0
|
| 34 |
+
|
| 35 |
+
# 🎛️ 2. THE ULTIMATE FILTER GENERATOR (මෙන්න මැජික් එක)
|
| 36 |
+
def get_filter_complex(effect_type, level, duration):
|
| 37 |
+
|
| 38 |
+
# DJ Drop Logic (Intro & Outro Mixing)
|
| 39 |
+
# Intro එක පටන් ගද්දිම (delay 0), Outro එක සින්දුව ඉවර වෙන්න තත්පර 5කට කලින්
|
| 40 |
+
outro_start = max(0, (duration * 1000) - 5000) # milliseconds
|
| 41 |
+
|
| 42 |
+
# මූලික මික්සින් කෝඩ් එක (Watermark)
|
| 43 |
+
# [0] = Song, [1] = Intro, [2] = Outro
|
| 44 |
+
# amix පාවිච්චි කරලා සින්දුවයි වොයිස් කට් එකයි ලස්සනට අමුනනවා
|
| 45 |
+
dj_mix_code = (
|
| 46 |
+
f"[0:a]volume=1.0[main];"
|
| 47 |
+
f"[1:a]volume=0.8,adelay=0|0[in_v];"
|
| 48 |
+
f"[2:a]volume=0.8,adelay={outro_start}|{outro_start}[out_v];"
|
| 49 |
+
f"[main][in_v][out_v]amix=inputs=3:duration=first:dropout_transition=3[mixed];"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# දැන් අපි Effect එක තෝරමු (16GB RAM Optimized Filters)
|
| 53 |
+
effects = {
|
| 54 |
+
# --- BASS SERIES ---
|
| 55 |
+
'bass_studio': f"asplit=2[b][t];[b]lowpass=f=100,asubboost=dry=0.6:wet=0.8,bass=g={level}:f=60:w=0.5[bass_out];[t]highpass=f=100[treble_out];[bass_out][treble_out]amix=inputs=2:duration=first,volume=2dB",
|
| 56 |
+
'bass_edm': f"asplit=2[b][t];[b]lowpass=f=120,asubboost=dry=0.5:wet=1.0,bass=g={level}:f=80:w=0.6,equalizer=f=60:width_type=h:width=50:g=5[bass_out];[t]highpass=f=120[treble_out];[bass_out][treble_out]amix=inputs=2:duration=first,volume=3dB",
|
| 57 |
+
'bass_car': f"asplit=2[b][t];[b]lowpass=f=80,asubboost=dry=0.5:wet=1.0,bass=g={level}:f=50:w=1.0[bass_out];[t]highpass=f=80,volume=0.8[treble_out];[bass_out][treble_out]amix=inputs=2:duration=first,volume=4dB",
|
| 58 |
+
'bass_slap': f"bass=g={level}:f=150:w=1.0,treble=g=3", # House Music වලට
|
| 59 |
+
|
| 60 |
+
# --- SPATIAL & 8D ---
|
| 61 |
+
'8d_audio': "apulsator=hz=0.125", # ඔලුව වටේ කැරකෙනවා (8s per cycle)
|
| 62 |
+
'3d_surround': "apulsator=hz=0.5,stereowiden=level=0.4",
|
| 63 |
+
'wide_stereo': "stereowiden=delay=20:feedback=0.3:crossfeed=0.3:drymix=0.8",
|
| 64 |
+
'hall_reverb': "aecho=0.8:0.9:1000:0.3", # ලොකු හෝල් එකක වගේ
|
| 65 |
+
|
| 66 |
+
# --- VOCAL & EQ ---
|
| 67 |
+
'vocal_boost': "treble=g=5,equalizer=f=1000:width_type=h:width=200:g=4", # කටහඩ මතු කරන්න
|
| 68 |
+
'crystal_clear': "treble=g=4,bass=g=-2,equalizer=f=3000:width_type=h:width=1000:g=2",
|
| 69 |
+
'lofi_chill': "aresample=22050,asetrate=22050,lowpass=f=3000,noise=alls=20:allf=t+u", # පරණ රේඩියෝ වගේ
|
| 70 |
+
|
| 71 |
+
# --- FUN & STYLE ---
|
| 72 |
+
'nightcore': "asetrate=44100*1.25,aresample=44100,equalizer=f=60:width_type=h:width=100:g=5", # වේගෙන් සහ හීන් හඩින්
|
| 73 |
+
'slow_reverb': "asetrate=44100*0.85,aresample=44100,aecho=0.8:0.9:1000:0.3",
|
| 74 |
+
'vaporwave': "asetrate=44100*0.8,aresample=44100,lowpass=f=2000",
|
| 75 |
+
'reverse': "areverse",
|
| 76 |
+
'underwater': "lowpass=f=300,volume=2dB",
|
| 77 |
+
|
| 78 |
+
# --- MASTERING TOOLS ---
|
| 79 |
+
'compressor': "acompressor=threshold=-20dB:ratio=4:attack=5:release=50", # Professional Compression
|
| 80 |
+
'limiter': "alimiter=limit=0.95:level=1.0", # සද්දෙ පැලෙන්නේ නැති වෙන්න හදනවා
|
| 81 |
+
'loudness': "loudnorm=I=-16:TP=-1.5:LRA=11", # TV/Radio Quality Volume
|
| 82 |
+
'concert': "aecho=0.8:0.88:60:0.4,stereowiden=level=0.4"
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
selected_effect = effects.get(effect_type, effects['bass_studio']) # Default to studio
|
| 86 |
+
|
| 87 |
+
# Final Chain: Mixed Audio -> Effect -> Limiter (Safety)
|
| 88 |
+
return f"{dj_mix_code}[mixed]{selected_effect},alimiter=limit=0.99[out]"
|
| 89 |
+
|
| 90 |
+
# 🚀 3. API ENDPOINT
|
| 91 |
+
@app.post("/process")
|
| 92 |
+
async def process_audio(
|
| 93 |
+
file: UploadFile = File(...),
|
| 94 |
+
effect: str = Form("bass_studio"),
|
| 95 |
+
level: int = Form(10)
|
| 96 |
+
):
|
| 97 |
+
timestamp = int(time.time())
|
| 98 |
+
input_path = os.path.join(UPLOAD_FOLDER, f"in_{timestamp}_{file.filename}")
|
| 99 |
+
output_path = os.path.join(PROCESSED_FOLDER, f"out_{timestamp}.mp3")
|
| 100 |
+
|
| 101 |
+
# 1. සින්දුව සේව් කරගන්නවා
|
| 102 |
+
with open(input_path, "wb") as buffer:
|
| 103 |
+
shutil.copyfileobj(file.file, buffer)
|
| 104 |
+
|
| 105 |
+
# 2. අවශ්ය ෆයිල් තියෙනවද බලනවා (Intro/Outro)
|
| 106 |
+
intro_path = os.path.join(ASSETS_FOLDER, "intro.mp3")
|
| 107 |
+
outro_path = os.path.join(ASSETS_FOLDER, "outro.mp3")
|
| 108 |
+
|
| 109 |
+
if not os.path.exists(intro_path) or not os.path.exists(outro_path):
|
| 110 |
+
return {"error": "Missing intro.mp3 or outro.mp3 in assets folder!"}
|
| 111 |
+
|
| 112 |
+
# 3. සින්දුවේ දිග හොයනවා (Outro එක හරියට තියන්න)
|
| 113 |
+
duration = get_audio_duration(input_path)
|
| 114 |
+
|
| 115 |
+
# 4. Filter Chain එක හදාගන්නවා
|
| 116 |
+
filter_complex = get_filter_complex(effect, level, duration)
|
| 117 |
+
|
| 118 |
+
# 5. FFmpeg Command එක ගහනවා (16GB RAM Power 🔥)
|
| 119 |
+
# -threads 0 : CPU එකේ ඔක්කොම කෝර් ටික පාවිච්චි කරනවා
|
| 120 |
+
# -preset ultrafast : කොලිටි එක අඩුවෙන්නේ නෑ Audio වලට, හැබැයි වේගවත්
|
| 121 |
+
cmd = [
|
| 122 |
+
"ffmpeg", "-y",
|
| 123 |
+
"-i", input_path, # [0] Main Song
|
| 124 |
+
"-i", intro_path, # [1] Intro
|
| 125 |
+
"-i", outro_path, # [2] Outro
|
| 126 |
+
"-filter_complex", filter_complex,
|
| 127 |
+
"-map", "[out]",
|
| 128 |
+
"-b:a", "320k", # 320kbps High Quality Audio
|
| 129 |
+
"-threads", "0",
|
| 130 |
+
output_path
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
print(f"🎛️ Processing: {effect} | Level: {level}")
|
| 134 |
+
|
| 135 |
+
process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 136 |
+
|
| 137 |
+
if process.returncode != 0:
|
| 138 |
+
print("Error:", process.stderr.decode())
|
| 139 |
+
return {"error": "FFmpeg processing failed"}
|
| 140 |
+
|
| 141 |
+
# 6. හදපු ෆයිල් එක යවනවා
|
| 142 |
+
return FileResponse(output_path, media_type="audio/mpeg", filename=f"Cipher_{effect}.mp3")
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
# Server එක රන් කරන්න
|
| 146 |
+
import uvicorn
|
| 147 |
+
uvicorn.run(app, host="0.0.0.0", port=7862)
|
audio_server/assets/intro.mp3
ADDED
|
Binary file (42.6 kB). View file
|
|
|
audio_server/assets/outro.mp3
ADDED
|
Binary file (67.2 kB). View file
|
|
|
biologic
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Subproject commit 412049530afc5413f7adf70348c7094fd496338e
|
|
|
|
|
|
biologic/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Market Chart Gen
|
| 3 |
+
emoji: 📈
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Market Chart Generator
|
biologic/index.js
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const { Canvas, loadImage } = require('skia-canvas');
|
| 3 |
+
const app = express();
|
| 4 |
+
const multer = require('multer'); // ෆයිල් ගන්න
|
| 5 |
+
const { Sticker, StickerTypes } = require('wa-sticker-formatter');
|
| 6 |
+
const upload = multer({ storage: multer.memoryStorage() });
|
| 7 |
+
app.use(express.json({ limit: '50mb' }));
|
| 8 |
+
|
| 9 |
+
app.get('/', (req, res) => res.send("🚀 Cipher-MD ULTRA-SHARP ENGINE"));
|
| 10 |
+
|
| 11 |
+
// ============================================================
|
| 12 |
+
// 🎨 1. WELCOME IMAGE GENERATOR (Exact Custom Design)
|
| 13 |
+
// ============================================================
|
| 14 |
+
app.post('/welcome', upload.single('image'), async (req, res) => {
|
| 15 |
+
try {
|
| 16 |
+
// ෆොටෝ එකක් නැත්නම් එරර් එකක්
|
| 17 |
+
if (!req.file) return res.status(400).send("No Profile Pic Uploaded");
|
| 18 |
+
|
| 19 |
+
// බොට්ගෙන් එන ඩේටා (gName = title විදිහට ගන්නවා)
|
| 20 |
+
const { title, count } = req.body;
|
| 21 |
+
const gName = title || "Group Name"; // නම හිස් නම් default එකක්
|
| 22 |
+
|
| 23 |
+
const size = 1024;
|
| 24 |
+
const canvas = new Canvas(size, size);
|
| 25 |
+
const ctx = canvas.getContext('2d');
|
| 26 |
+
const watermark = "CIPHER-MD VISUAL ENGINE v2.0";
|
| 27 |
+
|
| 28 |
+
// Style 1 හෝ 3 විතරයි පාවිච්චි කරන්නේ
|
| 29 |
+
const styleId = Math.random() > 0.5 ? 1 : 3;
|
| 30 |
+
|
| 31 |
+
// 1. Image Load (Buffer එකෙන් කෙලින්ම ගන්නවා)
|
| 32 |
+
const img = await loadImage(req.file.buffer);
|
| 33 |
+
|
| 34 |
+
// 2. Background Blur
|
| 35 |
+
ctx.filter = 'blur(50px) brightness(0.5)';
|
| 36 |
+
ctx.drawImage(img, -50, -50, size + 100, size + 100);
|
| 37 |
+
ctx.filter = 'none';
|
| 38 |
+
|
| 39 |
+
// 3. Styles Logic (1 & 3)
|
| 40 |
+
if (styleId === 1) {
|
| 41 |
+
ctx.strokeStyle = 'rgba(0, 242, 255, 0.1)';
|
| 42 |
+
for (let i = 0; i < size; i += 60) {
|
| 43 |
+
ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, size); ctx.stroke();
|
| 44 |
+
ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(size, i); ctx.stroke();
|
| 45 |
+
}
|
| 46 |
+
} else {
|
| 47 |
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
|
| 48 |
+
ctx.lineWidth = 1;
|
| 49 |
+
for (let i = 0; i < 20; i++) {
|
| 50 |
+
ctx.beginPath(); ctx.moveTo(Math.random() * size, Math.random() * size);
|
| 51 |
+
ctx.lineTo(Math.random() * size, Math.random() * size); ctx.stroke();
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// 4. Highlight Beam (උඹේ කෝඩ් එකේ තිබ්බ අලුත් කෑල්ල)
|
| 56 |
+
ctx.save();
|
| 57 |
+
const beamGrad = ctx.createLinearGradient(0, 0, size, size);
|
| 58 |
+
beamGrad.addColorStop(0, 'rgba(0, 242, 255, 0)');
|
| 59 |
+
beamGrad.addColorStop(0.5, 'rgba(0, 242, 255, 0.15)');
|
| 60 |
+
beamGrad.addColorStop(1, 'rgba(0, 242, 255, 0)');
|
| 61 |
+
ctx.fillStyle = beamGrad; ctx.rotate(-15 * Math.PI / 180);
|
| 62 |
+
ctx.fillRect(-size, size / 3, size * 2, 150); ctx.restore();
|
| 63 |
+
|
| 64 |
+
// 5. ✅ AVATAR CENTERED (පොටෝ එක හරි මැදට)
|
| 65 |
+
const centerX = size / 2;
|
| 66 |
+
const centerY = size / 2;
|
| 67 |
+
const radius = 250;
|
| 68 |
+
|
| 69 |
+
ctx.save();
|
| 70 |
+
ctx.strokeStyle = '#00f2ff'; ctx.lineWidth = 12; ctx.shadowBlur = 40; ctx.shadowColor = '#00f2ff';
|
| 71 |
+
ctx.beginPath(); ctx.arc(centerX, centerY, radius + 15, 0, Math.PI * 2); ctx.stroke(); ctx.restore();
|
| 72 |
+
|
| 73 |
+
ctx.save();
|
| 74 |
+
ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.clip();
|
| 75 |
+
ctx.drawImage(img, centerX - radius, centerY - radius, radius * 2, radius * 2); ctx.restore();
|
| 76 |
+
|
| 77 |
+
// 6. ✅ TEXT LAYOUT
|
| 78 |
+
ctx.textAlign = 'center';
|
| 79 |
+
|
| 80 |
+
// "W E L C O M E" Label
|
| 81 |
+
ctx.font = 'bold 60px sans-serif';
|
| 82 |
+
ctx.strokeStyle = 'rgba(0, 242, 255, 0.8)';
|
| 83 |
+
ctx.lineWidth = 3;
|
| 84 |
+
ctx.strokeText("W E L C O M E", centerX, centerY + radius + 120);
|
| 85 |
+
|
| 86 |
+
// Group Name & Count
|
| 87 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
| 88 |
+
ctx.font = '35px sans-serif';
|
| 89 |
+
// Emoji Safe Regex එකත් දාලා තියෙන්නේ ආරක්ෂාවට
|
| 90 |
+
const safeTitle = gName.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, '');
|
| 91 |
+
|
| 92 |
+
ctx.fillText(`${safeTitle} • MEMBER #${count}`, centerX, centerY + radius + 190);
|
| 93 |
+
|
| 94 |
+
// Watermark
|
| 95 |
+
ctx.globalAlpha = 0.3;
|
| 96 |
+
ctx.font = 'bold 25px sans-serif';
|
| 97 |
+
ctx.fillStyle = '#ffffff';
|
| 98 |
+
ctx.fillText(watermark, centerX, size - 30);
|
| 99 |
+
|
| 100 |
+
// යැවීම
|
| 101 |
+
const buffer = await canvas.toBuffer('png');
|
| 102 |
+
res.set('Content-Type', 'image/png');
|
| 103 |
+
res.send(buffer);
|
| 104 |
+
|
| 105 |
+
} catch (e) {
|
| 106 |
+
console.error(e);
|
| 107 |
+
res.status(500).send("Welcome Gen Error: " + e.message);
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
// ============================================================
|
| 112 |
+
// 🖼️ 2. WATERMARK (Only for .img gen)
|
| 113 |
+
// ============================================================
|
| 114 |
+
app.post('/watermark', upload.single('image'), async (req, res) => {
|
| 115 |
+
try {
|
| 116 |
+
if (!req.file) return res.status(400).send("No image");
|
| 117 |
+
const img = await loadImage(req.file.buffer);
|
| 118 |
+
const canvas = new Canvas(img.width, img.height);
|
| 119 |
+
const ctx = canvas.getContext('2d');
|
| 120 |
+
|
| 121 |
+
ctx.drawImage(img, 0, 0);
|
| 122 |
+
|
| 123 |
+
// Add Watermark
|
| 124 |
+
const fontSize = Math.floor(img.width * 0.02);
|
| 125 |
+
ctx.font = `bold ${fontSize}px sans-serif`;
|
| 126 |
+
ctx.textAlign = 'right';
|
| 127 |
+
ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
|
| 128 |
+
ctx.shadowBlur = 5; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 3;
|
| 129 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
| 130 |
+
ctx.fillText('CIPHER-MD GEN ENGINE', img.width - 20, img.height - 20);
|
| 131 |
+
|
| 132 |
+
res.set('Content-Type', 'image/png');
|
| 133 |
+
res.send(await canvas.toBuffer('png'));
|
| 134 |
+
} catch (e) { res.status(500).send("WM Error"); }
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
// ============================================================
|
| 138 |
+
// 🃏 3. STICKER MAKER
|
| 139 |
+
// ============================================================
|
| 140 |
+
app.post('/sticker', upload.single('media'), async (req, res) => {
|
| 141 |
+
try {
|
| 142 |
+
if (!req.file) return res.status(400).send("No media");
|
| 143 |
+
const { pack, author } = req.body;
|
| 144 |
+
|
| 145 |
+
const sticker = new Sticker(req.file.buffer, {
|
| 146 |
+
pack: pack || 'Cipher-MD',
|
| 147 |
+
author: author || 'Bot',
|
| 148 |
+
type: StickerTypes.FULL,
|
| 149 |
+
categories: ['🤩', '🎉'],
|
| 150 |
+
quality: 40,
|
| 151 |
+
background: 'transparent'
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
res.set('Content-Type', 'image/webp');
|
| 155 |
+
res.send(await sticker.toBuffer());
|
| 156 |
+
} catch (e) { res.status(500).send("Sticker Error"); }
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
// ============================================================
|
| 161 |
+
// ✨ 4. ENHANCE ROUTE (Server 2 -> Python Server 3)
|
| 162 |
+
// ============================================================
|
| 163 |
+
app.post('/enhance', upload.single('image'), async (req, res) => {
|
| 164 |
+
try {
|
| 165 |
+
if (!req.file) return res.status(400).send("No image");
|
| 166 |
+
|
| 167 |
+
// 1. Python Server එකට කනෙක්ට් වෙනවා
|
| 168 |
+
const { Client } = await import("@gradio/client");
|
| 169 |
+
const client = await Client.connect("https://sh4lu-z-image-generate.hf.space");
|
| 170 |
+
|
| 171 |
+
// 2. හරියටම පරාමිතීන් (Settings) යවනවා
|
| 172 |
+
// (උඹ එවපු Python Code එකේ පිළිවෙළටම)
|
| 173 |
+
const result = await client.predict("/inference", [
|
| 174 |
+
new Blob([req.file.buffer]), // 1. ෆොටෝ එක
|
| 175 |
+
"GFPGANv1.4.pth", // 2. Face Model
|
| 176 |
+
"SRVGG, realesr-general-x4v3.pth", // 3. Upscale Model (මේක තමයි කලින් මිස් වුණේ)
|
| 177 |
+
2, // 4. Scale (2x හෝ 4x දාන්න පුළුවන්)
|
| 178 |
+
"retinaface_resnet50", // 5. Face Detection
|
| 179 |
+
10, // 6. Threshold
|
| 180 |
+
false, // 7. Only Center Face?
|
| 181 |
+
true, // 8. Output Name?
|
| 182 |
+
true // 9. Save as PNG?
|
| 183 |
+
]);
|
| 184 |
+
|
| 185 |
+
// 3. හරියටම 4 වෙනි ෆොටෝ එක (Final Result) තෝරාගැනීම
|
| 186 |
+
// Python Server එකෙන් එන result එකේ [0] වෙනි array එකේ [3] වෙනි අයිතමය
|
| 187 |
+
const target = result.data[0][3] || result.data[0];
|
| 188 |
+
const enhancedUrl = target.image ? target.image.url : target.url;
|
| 189 |
+
|
| 190 |
+
// 4. ෆොටෝ එක බාගෙන Bot ට යවනවා
|
| 191 |
+
const imgBuffer = await axios.get(enhancedUrl, { responseType: 'arraybuffer' }).then(r => r.data);
|
| 192 |
+
res.set('Content-Type', 'image/png');
|
| 193 |
+
res.send(imgBuffer);
|
| 194 |
+
|
| 195 |
+
} catch (e) {
|
| 196 |
+
console.error("Enhance Error:", e);
|
| 197 |
+
res.status(500).send("Enhance Failed: " + e.message);
|
| 198 |
+
}
|
| 199 |
+
});
|
| 200 |
+
|
| 201 |
+
// ============================================================
|
| 202 |
+
// 🛠️ HELPER: ROUNDED RECTANGLE
|
| 203 |
+
// ============================================================
|
| 204 |
+
function roundedRect(ctx, x, y, width, height, radius) {
|
| 205 |
+
ctx.beginPath();
|
| 206 |
+
ctx.moveTo(x + radius, y);
|
| 207 |
+
ctx.lineTo(x + width - radius, y);
|
| 208 |
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
| 209 |
+
ctx.lineTo(x + width, y + height - radius);
|
| 210 |
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
| 211 |
+
ctx.lineTo(x + radius, y + height);
|
| 212 |
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
| 213 |
+
ctx.lineTo(x, y + radius);
|
| 214 |
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
| 215 |
+
ctx.closePath();
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// ============================================================
|
| 219 |
+
// 📈 ENGINE 1: ULTRA-SHARP LINE CHART (Gold, P2P, CSE)
|
| 220 |
+
// ============================================================
|
| 221 |
+
async function drawLineChart(title, subTitle, history, colorTheme) {
|
| 222 |
+
const width = 2400;
|
| 223 |
+
const height = 1500;
|
| 224 |
+
const canvas = new Canvas(width, height);
|
| 225 |
+
const ctx = canvas.getContext('2d');
|
| 226 |
+
|
| 227 |
+
ctx.scale(2, 2);
|
| 228 |
+
// 1. Background (Solid Dark)
|
| 229 |
+
ctx.fillStyle = '#131722';
|
| 230 |
+
ctx.fillRect(0, 0, width, height);
|
| 231 |
+
|
| 232 |
+
// 📐 Precise Layout Calculation
|
| 233 |
+
// යට ඉඩ (Bottom Padding) වැඩි කළා අකුරු ගෑවෙන්නෙ නැති වෙන්න
|
| 234 |
+
const chartX = 20;
|
| 235 |
+
const chartY = 120; // Title area height
|
| 236 |
+
const chartW = 1050;
|
| 237 |
+
const chartH = 530; // Chart height reduced slightly to keep bottom clear
|
| 238 |
+
|
| 239 |
+
// Data Min/Max
|
| 240 |
+
const values = history.map(h => (h.price || h.rate || h.value));
|
| 241 |
+
const minVal = Math.min(...values) * 0.9995;
|
| 242 |
+
const maxVal = Math.max(...values) * 1.0005;
|
| 243 |
+
const range = maxVal - minVal;
|
| 244 |
+
|
| 245 |
+
const getY = (val) => chartY + chartH - ((val - minVal) / range) * chartH;
|
| 246 |
+
const getX = (i) => chartX + (i / (values.length - 1)) * chartW;
|
| 247 |
+
|
| 248 |
+
// 2. GRID LINES (Very Subtle)
|
| 249 |
+
ctx.lineWidth = 1;
|
| 250 |
+
ctx.strokeStyle = '#2a2e39';
|
| 251 |
+
|
| 252 |
+
// Horizontal Grids
|
| 253 |
+
const rows = 6;
|
| 254 |
+
for (let i = 0; i <= rows; i++) {
|
| 255 |
+
const y = chartY + (chartH / rows) * i;
|
| 256 |
+
ctx.beginPath(); ctx.moveTo(chartX, y); ctx.lineTo(chartX + chartW, y); ctx.stroke();
|
| 257 |
+
|
| 258 |
+
// Price Label (Sharp Font)
|
| 259 |
+
const price = maxVal - (range / rows) * i;
|
| 260 |
+
ctx.fillStyle = '#787b86';
|
| 261 |
+
ctx.font = '13px Consolas, monospace'; // Monospace for alignment
|
| 262 |
+
ctx.textAlign = 'left';
|
| 263 |
+
ctx.fillText(price.toLocaleString(undefined, {minimumFractionDigits: 2}), chartX + chartW + 12, y + 8);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
// Vertical Grids
|
| 267 |
+
const cols = 8;
|
| 268 |
+
for (let i = 0; i <= cols; i++) {
|
| 269 |
+
const x = chartX + (chartW / cols) * i;
|
| 270 |
+
ctx.beginPath(); ctx.moveTo(x, chartY); ctx.lineTo(x, chartY + chartH + 60); ctx.stroke();
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// 3. THE CHART AREA (Clean Gradient)
|
| 274 |
+
ctx.save();
|
| 275 |
+
ctx.beginPath();
|
| 276 |
+
ctx.moveTo(getX(0), getY(values[0]));
|
| 277 |
+
for (let i = 0; i < values.length; i++) ctx.lineTo(getX(i), getY(values[i]));
|
| 278 |
+
|
| 279 |
+
// Close path to bottom of chart area
|
| 280 |
+
ctx.lineTo(getX(values.length - 1), chartY + chartH + 60);
|
| 281 |
+
ctx.lineTo(getX(0), chartY + chartH + 60);
|
| 282 |
+
ctx.closePath();
|
| 283 |
+
|
| 284 |
+
// Sharp Gradient (No blurring at bottom)
|
| 285 |
+
const gradient = ctx.createLinearGradient(0, chartY, 0, chartY + chartH + 60);
|
| 286 |
+
gradient.addColorStop(0, `${colorTheme}55`); // Top: Semi-transparent
|
| 287 |
+
gradient.addColorStop(1, `${colorTheme}00`); // Bottom: Completely transparent
|
| 288 |
+
ctx.fillStyle = gradient;
|
| 289 |
+
ctx.fill();
|
| 290 |
+
ctx.restore();
|
| 291 |
+
|
| 292 |
+
// 4. THE MAIN LINE (Thin & Sharp)
|
| 293 |
+
ctx.beginPath();
|
| 294 |
+
ctx.lineWidth = 3; // Thinner line for "Pro" look (was 5)
|
| 295 |
+
ctx.strokeStyle = colorTheme;
|
| 296 |
+
ctx.lineJoin = 'round';
|
| 297 |
+
// Removed shadowBlur here to avoid "smudge" look
|
| 298 |
+
for (let i = 0; i < values.length; i++) {
|
| 299 |
+
if(i===0) ctx.moveTo(getX(i), getY(values[i]));
|
| 300 |
+
else ctx.lineTo(getX(i), getY(values[i]));
|
| 301 |
+
}
|
| 302 |
+
ctx.stroke();
|
| 303 |
+
|
| 304 |
+
// 5. CURRENT PRICE BADGE (Right Axis)
|
| 305 |
+
const lastVal = values[values.length - 1];
|
| 306 |
+
const lastY = getY(lastVal);
|
| 307 |
+
|
| 308 |
+
// Dotted line to axis
|
| 309 |
+
ctx.setLineDash([2, 2]);
|
| 310 |
+
ctx.lineWidth = 1;
|
| 311 |
+
ctx.beginPath(); ctx.moveTo(getX(values.length-1), lastY); ctx.lineTo(chartX + chartW, lastY); ctx.stroke();
|
| 312 |
+
ctx.setLineDash([]);
|
| 313 |
+
|
| 314 |
+
// Badge
|
| 315 |
+
ctx.fillStyle = colorTheme;
|
| 316 |
+
roundedRect(ctx, chartX + chartW + 5, lastY - 11, 115, 22, 3);
|
| 317 |
+
ctx.fill();
|
| 318 |
+
|
| 319 |
+
ctx.fillStyle = '#ffffff';
|
| 320 |
+
ctx.font = 'bold 13px Consolas';
|
| 321 |
+
ctx.textAlign = 'left';
|
| 322 |
+
ctx.fillText(lastVal.toLocaleString(undefined, {minimumFractionDigits: 2}), chartX + chartW + 10, lastY + 5);
|
| 323 |
+
|
| 324 |
+
// Pulsing Dot (Sharp)
|
| 325 |
+
const lastX = getX(values.length - 1);
|
| 326 |
+
ctx.beginPath(); ctx.arc(lastX, lastY, 5, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill();
|
| 327 |
+
ctx.beginPath(); ctx.arc(lastX, lastY, 12, 0, Math.PI * 2); ctx.strokeStyle = `${colorTheme}66`; ctx.lineWidth = 2; ctx.stroke();
|
| 328 |
+
|
| 329 |
+
// 6. HEADER (Aligned)
|
| 330 |
+
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px sans-serif'; ctx.textAlign = 'left';
|
| 331 |
+
ctx.fillText(title, 30, 60);
|
| 332 |
+
|
| 333 |
+
ctx.fillStyle = colorTheme; ctx.font = 'bold 32px sans-serif';
|
| 334 |
+
ctx.fillText(subTitle, 30, 100);
|
| 335 |
+
|
| 336 |
+
// 7. WATERMARK (Safe Distance)
|
| 337 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right';
|
| 338 |
+
ctx.fillText('⚡ Cipher-MD Analytics (4K)', (width / 2) - 20, (height / 2) - 20);
|
| 339 |
+
|
| 340 |
+
return canvas.toBuffer('png');
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// ============================================================
|
| 344 |
+
// 🕯️ ENGINE 2: CANDLESTICK CHART (Fixed Sharpness)
|
| 345 |
+
// ============================================================
|
| 346 |
+
app.post('/chart/candle', async (req, res) => {
|
| 347 |
+
try {
|
| 348 |
+
const { candles, symbol, change, currentPrice } = req.body;
|
| 349 |
+
const width = 2400;
|
| 350 |
+
const height = 1500;
|
| 351 |
+
const canvas = new Canvas(width, height);
|
| 352 |
+
const ctx = canvas.getContext('2d');
|
| 353 |
+
|
| 354 |
+
ctx.scale(2, 2);
|
| 355 |
+
ctx.fillStyle = '#131722'; ctx.fillRect(0, 0, width, height);
|
| 356 |
+
|
| 357 |
+
const chartX = 20;
|
| 358 |
+
const chartY = 60;
|
| 359 |
+
const chartW = 1050;
|
| 360 |
+
const chartH = 530;
|
| 361 |
+
|
| 362 |
+
const minPrice = Math.min(...candles.map(c => c.low));
|
| 363 |
+
const maxPrice = Math.max(...candles.map(c => c.high));
|
| 364 |
+
const range = maxPrice - minPrice;
|
| 365 |
+
const maxVol = Math.max(...candles.map(c => c.volume));
|
| 366 |
+
const getY = (p) => chartY + chartH - ((p - minPrice) / range) * chartH;
|
| 367 |
+
|
| 368 |
+
// Grid
|
| 369 |
+
ctx.lineWidth = 1; ctx.strokeStyle = '#2a2e39';
|
| 370 |
+
const rows = 8;
|
| 371 |
+
for(let i=0; i<=rows; i++) {
|
| 372 |
+
const y = chartY + (chartH/rows)*i;
|
| 373 |
+
ctx.beginPath(); ctx.moveTo(chartX, y); ctx.lineTo(chartX+chartW, y); ctx.stroke();
|
| 374 |
+
const p = maxPrice - (range/rows)*i;
|
| 375 |
+
ctx.fillStyle = '#787b86'; ctx.textAlign = 'left'; ctx.font = '13px Consolas';
|
| 376 |
+
ctx.fillText(p.toFixed(2), chartX+chartW+10, y+4);
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
const candleW = (chartW / candles.length) * 0.7;
|
| 380 |
+
const spacing = chartW / candles.length;
|
| 381 |
+
|
| 382 |
+
candles.forEach((c, i) => {
|
| 383 |
+
const x = chartX + (i * spacing) + (spacing/2);
|
| 384 |
+
const isUp = c.close >= c.open;
|
| 385 |
+
const color = isUp ? '#089981' : '#f23645';
|
| 386 |
+
|
| 387 |
+
// Volume (Sharp, low opacity)
|
| 388 |
+
const volH = (c.volume / maxVol) * 120;
|
| 389 |
+
ctx.fillStyle = isUp ? 'rgba(8, 153, 129, 0.2)' : 'rgba(242, 54, 69, 0.2)';
|
| 390 |
+
ctx.fillRect(x - candleW/2, chartY + chartH - volH + 150, candleW, volH);
|
| 391 |
+
|
| 392 |
+
// Wick (Thin line)
|
| 393 |
+
ctx.strokeStyle = color; ctx.lineWidth = 1.5;
|
| 394 |
+
ctx.beginPath(); ctx.moveTo(x, getY(c.high)); ctx.lineTo(x, getY(c.low)); ctx.stroke();
|
| 395 |
+
|
| 396 |
+
// Body
|
| 397 |
+
ctx.fillStyle = color;
|
| 398 |
+
const yOpen = getY(c.open);
|
| 399 |
+
const yClose = getY(c.close);
|
| 400 |
+
ctx.fillRect(x - candleW/2, Math.min(yOpen, yClose), candleW, Math.max(Math.abs(yOpen - yClose), 1));
|
| 401 |
+
});
|
| 402 |
+
|
| 403 |
+
// Price Badge
|
| 404 |
+
const lastC = candles[candles.length-1];
|
| 405 |
+
const lastY = getY(lastC.close);
|
| 406 |
+
const lastColor = lastC.close >= lastC.open ? '#089981' : '#f23645';
|
| 407 |
+
|
| 408 |
+
ctx.strokeStyle = '#ffffff'; ctx.setLineDash([2,2]); ctx.lineWidth = 1;
|
| 409 |
+
ctx.beginPath(); ctx.moveTo(0, lastY); ctx.lineTo(chartX+chartW, lastY); ctx.stroke();
|
| 410 |
+
ctx.setLineDash([]);
|
| 411 |
+
|
| 412 |
+
ctx.fillStyle = lastColor;
|
| 413 |
+
roundedRect(ctx, chartX+chartW+5, lastY-12, 115, 24, 3); ctx.fill();
|
| 414 |
+
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 13px Consolas';
|
| 415 |
+
ctx.fillText(parseFloat(currentPrice).toFixed(2), chartX+chartW+10, lastY+5);
|
| 416 |
+
|
| 417 |
+
// Header
|
| 418 |
+
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px sans-serif'; ctx.textAlign = 'left';
|
| 419 |
+
ctx.fillText(`${symbol} / USDT`, 30, 60);
|
| 420 |
+
ctx.fillStyle = parseFloat(change) >= 0 ? '#089981' : '#f23645'; ctx.font = 'bold 32px sans-serif';
|
| 421 |
+
ctx.fillText(`${change}%`, 30, 100);
|
| 422 |
+
|
| 423 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right';
|
| 424 |
+
ctx.fillText('⚡ Cipher-MD Analytics', (width / 2) - 20, (height / 2) - 20);
|
| 425 |
+
|
| 426 |
+
res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png'));
|
| 427 |
+
} catch (e) { res.status(500).send(e.message); }
|
| 428 |
+
});
|
| 429 |
+
|
| 430 |
+
// ============================================================
|
| 431 |
+
// 💎 ENGINE 3: NFT & ARB (Retained Fixed Version)
|
| 432 |
+
// ============================================================
|
| 433 |
+
app.post('/chart/nft', async (req, res) => {
|
| 434 |
+
try {
|
| 435 |
+
const { data } = req.body;
|
| 436 |
+
const canvas = new Canvas(800, 400); const ctx = canvas.getContext('2d');
|
| 437 |
+
ctx.fillStyle = '#0a0b0d'; roundedRect(ctx, 0, 0, 800, 400, 25); ctx.fill();
|
| 438 |
+
|
| 439 |
+
if (data.image_url) {
|
| 440 |
+
try {
|
| 441 |
+
const img = await loadImage(data.image_url);
|
| 442 |
+
ctx.save(); ctx.beginPath(); roundedRect(ctx, 40, 40, 200, 200, 20); ctx.clip();
|
| 443 |
+
ctx.drawImage(img, 40, 40, 200, 200); ctx.restore();
|
| 444 |
+
} catch (e) {}
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 36px sans-serif';
|
| 448 |
+
ctx.fillText(data.name.substring(0, 18), 270, 70);
|
| 449 |
+
|
| 450 |
+
ctx.fillStyle = '#8a939b'; ctx.font = '24px sans-serif';
|
| 451 |
+
ctx.fillText(`Chain: ${data.chain.toUpperCase()}`, 270, 110);
|
| 452 |
+
|
| 453 |
+
ctx.fillStyle = '#2081e2'; ctx.font = 'bold 55px sans-serif';
|
| 454 |
+
ctx.fillText(`${data.floor_price} ${data.floor_price_symbol}`, 270, 190);
|
| 455 |
+
|
| 456 |
+
ctx.fillStyle = '#ffffff'; ctx.font = '18px sans-serif';
|
| 457 |
+
ctx.fillText('FLOOR PRICE', 270, 220);
|
| 458 |
+
|
| 459 |
+
ctx.fillStyle = '#161b22'; roundedRect(ctx, 270, 260, 480, 80, 15); ctx.fill();
|
| 460 |
+
|
| 461 |
+
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 20px sans-serif';
|
| 462 |
+
ctx.textAlign = 'left'; ctx.fillText(`Vol: ${Math.round(data.total_volume)}`, 290, 308);
|
| 463 |
+
ctx.textAlign = 'right'; ctx.fillText(`Owners: ${data.owner_count}`, 730, 308);
|
| 464 |
+
|
| 465 |
+
ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(255,255,255,0.2)';
|
| 466 |
+
ctx.fillText('Cipher-MD NFT', 750, 380);
|
| 467 |
+
|
| 468 |
+
res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png'));
|
| 469 |
+
} catch (e) { res.status(500).send(e.message); }
|
| 470 |
+
});
|
| 471 |
+
|
| 472 |
+
app.post('/chart/arb', async (req, res) => {
|
| 473 |
+
try {
|
| 474 |
+
const { coin, exData } = req.body;
|
| 475 |
+
const canvas = new Canvas(900, 600); const ctx = canvas.getContext('2d');
|
| 476 |
+
ctx.fillStyle = '#131722'; ctx.fillRect(0,0,900,600);
|
| 477 |
+
|
| 478 |
+
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 40px sans-serif';
|
| 479 |
+
ctx.fillText(`${coin} Arbitrage Monitor`, 40, 60);
|
| 480 |
+
|
| 481 |
+
let y = 120;
|
| 482 |
+
const avg = exData.reduce((a, b) => a + b.price, 0) / exData.length;
|
| 483 |
+
|
| 484 |
+
exData.forEach(ex => {
|
| 485 |
+
const diff = (((ex.price - avg) / avg) * 100);
|
| 486 |
+
const color = diff >= 0 ? '#089981' : '#f23645';
|
| 487 |
+
|
| 488 |
+
ctx.fillStyle = '#1e222d'; roundedRect(ctx, 40, y, 820, 70, 10); ctx.fill();
|
| 489 |
+
|
| 490 |
+
ctx.fillStyle = '#b2b5be'; ctx.textAlign = 'left'; ctx.font = 'bold 24px sans-serif';
|
| 491 |
+
ctx.fillText(ex.name, 70, y + 45);
|
| 492 |
+
|
| 493 |
+
ctx.fillStyle = '#ffffff'; ctx.textAlign = 'right';
|
| 494 |
+
ctx.fillText(`$${ex.price.toLocaleString()}`, 600, y + 45);
|
| 495 |
+
|
| 496 |
+
ctx.fillStyle = color;
|
| 497 |
+
ctx.fillText(`${diff > 0 ? '+' : ''}${diff.toFixed(2)}%`, 830, y + 45);
|
| 498 |
+
y += 85;
|
| 499 |
+
});
|
| 500 |
+
|
| 501 |
+
res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png'));
|
| 502 |
+
} catch (e) { res.status(500).send(e.message); }
|
| 503 |
+
});
|
| 504 |
+
|
| 505 |
+
// ============================================================
|
| 506 |
+
// ⚡ ROUTERS
|
| 507 |
+
// ============================================================
|
| 508 |
+
app.post('/chart/p2p', async (req, res) => {
|
| 509 |
+
try {
|
| 510 |
+
const { history, currentRate } = req.body;
|
| 511 |
+
const buf = await drawLineChart("USDT / LKR (P2P)", `Rs. ${currentRate}`, history, '#2962ff');
|
| 512 |
+
res.set('Content-Type', 'image/png'); res.send(buf);
|
| 513 |
+
} catch (e) { res.status(500).send(e.message); }
|
| 514 |
+
});
|
| 515 |
+
|
| 516 |
+
app.post('/chart/forex', async (req, res) => {
|
| 517 |
+
try {
|
| 518 |
+
const { history, currentRate } = req.body;
|
| 519 |
+
const buf = await drawLineChart("USD / LKR (Official)", `Rs. ${currentRate}`, history, '#089981');
|
| 520 |
+
res.set('Content-Type', 'image/png'); res.send(buf);
|
| 521 |
+
} catch (e) { res.status(500).send(e.message); }
|
| 522 |
+
});
|
| 523 |
+
|
| 524 |
+
app.post('/chart/gold', async (req, res) => {
|
| 525 |
+
try {
|
| 526 |
+
const { history, currentRate } = req.body;
|
| 527 |
+
const buf = await drawLineChart("Gold (24K)", `Rs. ${currentRate}`, history, '#f2a900');
|
| 528 |
+
res.set('Content-Type', 'image/png'); res.send(buf);
|
| 529 |
+
} catch (e) { res.status(500).send(e.message); }
|
| 530 |
+
});
|
| 531 |
+
|
| 532 |
+
app.post('/chart/cse', async (req, res) => {
|
| 533 |
+
try {
|
| 534 |
+
const { history, currentRate } = req.body;
|
| 535 |
+
const buf = await drawLineChart("CSE ASPI", `${currentRate}`, history, '#a300cc');
|
| 536 |
+
res.set('Content-Type', 'image/png'); res.send(buf);
|
| 537 |
+
} catch (e) { res.status(500).send(e.message); }
|
| 538 |
+
});
|
| 539 |
+
|
| 540 |
+
app.post('/chart/fear', async (req, res) => {
|
| 541 |
+
try {
|
| 542 |
+
const { history } = req.body;
|
| 543 |
+
const last = history[history.length-1].value;
|
| 544 |
+
const color = last > 50 ? '#089981' : '#f23645';
|
| 545 |
+
const buf = await drawLineChart("Fear & Greed", `${last}/100`, history, color);
|
| 546 |
+
res.set('Content-Type', 'image/png'); res.send(buf);
|
| 547 |
+
} catch (e) { res.status(500).send(e.message); }
|
| 548 |
+
});
|
| 549 |
+
|
| 550 |
+
const port = 7861;
|
| 551 |
+
app.listen(port, () => console.log(`🚀 ULTRA-SHARP ENGINE running on port ${port}`));
|
cipher_sticker
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Subproject commit bccc38b266995e4478b5f1380e0e16384366a6eb
|
|
|
|
|
|
cipher_sticker/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Cipher Sticker Api
|
| 3 |
+
emoji: 🏢
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
cipher_sticker/app.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, send_file, jsonify
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
import uuid
|
| 5 |
+
import shutil
|
| 6 |
+
import zipfile
|
| 7 |
+
import math
|
| 8 |
+
|
| 9 |
+
app = Flask(__name__)
|
| 10 |
+
UPLOAD_FOLDER = "uploads"
|
| 11 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 12 |
+
|
| 13 |
+
# වීඩියෝ එකේ කාලය (Duration) ගන්න ෆන්ෂන් එක
|
| 14 |
+
def get_video_duration(input_path):
|
| 15 |
+
try:
|
| 16 |
+
result = subprocess.run(
|
| 17 |
+
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", input_path],
|
| 18 |
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
| 19 |
+
)
|
| 20 |
+
return float(result.stdout)
|
| 21 |
+
except:
|
| 22 |
+
return 0.0
|
| 23 |
+
|
| 24 |
+
# 🎨 UNIVERSAL CONVERTER ENGINE (මේක තමයි හැම වැඩේම කරන්නේ)
|
| 25 |
+
def convert_segment(input_path, output_path, conf, start_time=0):
|
| 26 |
+
duration = conf['t']
|
| 27 |
+
fps = conf['fps']
|
| 28 |
+
quality = conf['q']
|
| 29 |
+
scale_size = conf['s']
|
| 30 |
+
|
| 31 |
+
# 🌟 Blur Filter Logic (Smart Resize එක්ක)
|
| 32 |
+
# 512px, 384px, 320px ඕන එකකට හරියන්න මේක හැදෙනවා
|
| 33 |
+
filter_complex = (
|
| 34 |
+
f"split[bg][fg];"
|
| 35 |
+
f"[bg]scale={scale_size}:{scale_size}:force_original_aspect_ratio=increase,crop={scale_size}:{scale_size},boxblur=20:10[bg_blurred];"
|
| 36 |
+
f"[fg]scale={scale_size}:{scale_size}:force_original_aspect_ratio=decrease[fg_scaled];"
|
| 37 |
+
f"[bg_blurred][fg_scaled]overlay=(W-w)/2:(H-h)/2"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
command = [
|
| 41 |
+
"ffmpeg", "-y",
|
| 42 |
+
"-ss", str(start_time), # පටන් ගන්න තැන (Pack එකට ඕන වෙනවා)
|
| 43 |
+
"-i", input_path,
|
| 44 |
+
"-vf", f"{filter_complex},fps={fps}",
|
| 45 |
+
"-vcodec", "libwebp",
|
| 46 |
+
"-lossless", "0",
|
| 47 |
+
"-compression_level", "6", # Ultra Compression
|
| 48 |
+
"-preset", "default",
|
| 49 |
+
"-q:v", str(quality),
|
| 50 |
+
"-loop", "0",
|
| 51 |
+
"-an",
|
| 52 |
+
"-t", str(duration), # කොච්චර වෙලා කපනවද
|
| 53 |
+
output_path
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
subprocess.run(command, check=True)
|
| 58 |
+
return True
|
| 59 |
+
except subprocess.CalledProcessError as e:
|
| 60 |
+
print(f"FFmpeg Error: {e}")
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
@app.route("/")
|
| 64 |
+
def home():
|
| 65 |
+
return "🔥 Cipher_MD Smart Engine (Guaranteed < 1MB) Running!"
|
| 66 |
+
|
| 67 |
+
# ================================================================
|
| 68 |
+
# 1. SINGLE STICKER ROUTE (SMART LOOP ENABLED ✅)
|
| 69 |
+
# ================================================================
|
| 70 |
+
@app.route("/sticker", methods=["POST"])
|
| 71 |
+
def make_single_sticker():
|
| 72 |
+
if 'file' not in request.files:
|
| 73 |
+
return jsonify({"error": "No file uploaded"}), 400
|
| 74 |
+
|
| 75 |
+
file = request.files['file']
|
| 76 |
+
ext = file.filename.split('.')[-1]
|
| 77 |
+
filename = f"{uuid.uuid4()}.{ext}"
|
| 78 |
+
input_path = os.path.join(UPLOAD_FOLDER, filename)
|
| 79 |
+
output_path = os.path.join(UPLOAD_FOLDER, f"{uuid.uuid4()}.webp")
|
| 80 |
+
|
| 81 |
+
file.save(input_path)
|
| 82 |
+
|
| 83 |
+
is_animated = ext.lower() in ['mp4', 'gif', 'mov', 'avi', 'mkv', 'webm']
|
| 84 |
+
success = False
|
| 85 |
+
|
| 86 |
+
if is_animated:
|
| 87 |
+
original_duration = get_video_duration(input_path)
|
| 88 |
+
target_duration = min(original_duration, 10.0)
|
| 89 |
+
|
| 90 |
+
# 🧠 SMART LOOP (Priority List)
|
| 91 |
+
# එකින් එක ට්රයි කරලා 1MB ට අඩු එකක් හම්බෙනකම් දුවනවා
|
| 92 |
+
attempts = [
|
| 93 |
+
{"s": 512, "q": 50, "fps": 15, "t": target_duration}, # Attempt 1
|
| 94 |
+
{"s": 384, "q": 45, "fps": 12, "t": target_duration}, # Attempt 2 (Resize)
|
| 95 |
+
{"s": 320, "q": 40, "fps": 12, "t": min(target_duration, 7.0)}, # Attempt 3 (Time Cut)
|
| 96 |
+
{"s": 256, "q": 30, "fps": 10, "t": min(target_duration, 5.0)} # Emergency (Low Quality)
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
for i, conf in enumerate(attempts):
|
| 100 |
+
print(f"🔄 Single Attempt {i+1}: Size={conf['s']}px...")
|
| 101 |
+
if convert_segment(input_path, output_path, conf, start_time=0):
|
| 102 |
+
size = os.path.getsize(output_path)
|
| 103 |
+
print(f"📦 Size: {size/1024:.2f} KB")
|
| 104 |
+
|
| 105 |
+
if size < 980000: # 980KB Safe Limit
|
| 106 |
+
success = True
|
| 107 |
+
break
|
| 108 |
+
else:
|
| 109 |
+
# Static Image Logic
|
| 110 |
+
cmd = ["ffmpeg", "-y", "-i", input_path, "-vf", "scale='if(gt(iw,ih),512,-1)':'if(gt(iw,ih),-1,512)',scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=#00000000", "-vcodec", "libwebp", "-lossless", "0", "-q:v", "80", output_path]
|
| 111 |
+
try:
|
| 112 |
+
subprocess.run(cmd, check=True)
|
| 113 |
+
success = True
|
| 114 |
+
except:
|
| 115 |
+
success = False
|
| 116 |
+
|
| 117 |
+
if os.path.exists(input_path): os.remove(input_path)
|
| 118 |
+
|
| 119 |
+
if success and os.path.exists(output_path):
|
| 120 |
+
return send_file(output_path, mimetype='image/webp')
|
| 121 |
+
else:
|
| 122 |
+
return jsonify({"error": "Failed to generate valid sticker"}), 500
|
| 123 |
+
|
| 124 |
+
# ================================================================
|
| 125 |
+
# 2. HEAVY PACK ROUTE (SMART LOOP FOR EVERY SEGMENT ✅)
|
| 126 |
+
# ================================================================
|
| 127 |
+
@app.route("/pack", methods=["POST"])
|
| 128 |
+
def make_pack():
|
| 129 |
+
if 'file' not in request.files:
|
| 130 |
+
return jsonify({"error": "No file uploaded"}), 400
|
| 131 |
+
|
| 132 |
+
file = request.files['file']
|
| 133 |
+
request_id = str(uuid.uuid4())
|
| 134 |
+
|
| 135 |
+
pack_folder = os.path.join(UPLOAD_FOLDER, request_id)
|
| 136 |
+
os.makedirs(pack_folder, exist_ok=True)
|
| 137 |
+
|
| 138 |
+
input_path = os.path.join(pack_folder, "video.mp4")
|
| 139 |
+
file.save(input_path)
|
| 140 |
+
|
| 141 |
+
duration = get_video_duration(input_path)
|
| 142 |
+
if duration > 130:
|
| 143 |
+
shutil.rmtree(pack_folder)
|
| 144 |
+
return jsonify({"error": "Video too long (Max 2 mins)"}), 400
|
| 145 |
+
|
| 146 |
+
sticker_files = []
|
| 147 |
+
num_segments = math.ceil(duration / 10)
|
| 148 |
+
|
| 149 |
+
# 🔄 හැම කෑල්ලක්ම Loop එකක් ඇතුලේ Process කරනවා
|
| 150 |
+
for i in range(num_segments):
|
| 151 |
+
start_time = i * 10
|
| 152 |
+
output_name = f"{i+1}.webp"
|
| 153 |
+
output_full_path = os.path.join(pack_folder, output_name)
|
| 154 |
+
|
| 155 |
+
segment_duration = min(10, duration - start_time)
|
| 156 |
+
if segment_duration < 1: continue
|
| 157 |
+
|
| 158 |
+
# 🧠 PACK SMART LOOP (මේක තමයි කලින් මිස් වුනේ!)
|
| 159 |
+
# හැම Sticker කෑල්ලක්ම 1MB ට අඩු වෙනකම් Resize කරනවා
|
| 160 |
+
attempts = [
|
| 161 |
+
{"s": 512, "q": 50, "fps": 15, "t": segment_duration},
|
| 162 |
+
{"s": 384, "q": 45, "fps": 12, "t": segment_duration},
|
| 163 |
+
{"s": 320, "q": 40, "fps": 10, "t": segment_duration},
|
| 164 |
+
{"s": 256, "q": 30, "fps": 10, "t": segment_duration}
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
converted = False
|
| 168 |
+
for conf in attempts:
|
| 169 |
+
print(f"📦 Pack Segment {i+1}: Trying Size={conf['s']}...")
|
| 170 |
+
if convert_segment(input_path, output_full_path, conf, start_time=start_time):
|
| 171 |
+
if os.path.getsize(output_full_path) < 980000:
|
| 172 |
+
sticker_files.append(output_full_path)
|
| 173 |
+
converted = True
|
| 174 |
+
break # හරි ගියොත් ඊළඟ කෑල්ලට යනවා
|
| 175 |
+
|
| 176 |
+
# කොහොමවත් බැරි වුනොත් අන්තිම Output එක හරි ගන්නවා (Fail නොවෙන්න)
|
| 177 |
+
if not converted and os.path.exists(output_full_path):
|
| 178 |
+
sticker_files.append(output_full_path)
|
| 179 |
+
|
| 180 |
+
# ZIP File එක හදනවා
|
| 181 |
+
zip_path = os.path.join(UPLOAD_FOLDER, f"{request_id}.zip")
|
| 182 |
+
with zipfile.ZipFile(zip_path, 'w') as zipf:
|
| 183 |
+
for sticker in sticker_files:
|
| 184 |
+
zipf.write(sticker, arcname=os.path.basename(sticker))
|
| 185 |
+
|
| 186 |
+
# Cleanup
|
| 187 |
+
shutil.rmtree(pack_folder)
|
| 188 |
+
|
| 189 |
+
return send_file(zip_path, mimetype='application/zip', as_attachment=True, download_name=f"{request_id}.zip")
|
| 190 |
+
|
| 191 |
+
if __name__ == "__main__":
|
| 192 |
+
app.run(host="0.0.0.0", port=7863)
|
math_scan
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Subproject commit 89245243e6391011953a5a4db92287f51a37ef7b
|
|
|
|
|
|
math_scan/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Math Scan Processor
|
| 3 |
+
emoji: 🏃
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.5.1
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
math_scan/app.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, Form
|
| 2 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 3 |
+
import shutil
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from io import StringIO
|
| 7 |
+
import contextlib
|
| 8 |
+
|
| 9 |
+
# ඔයාගේ original ෆයිල් දෙක මෙතනට import කරනවා
|
| 10 |
+
import math_solver
|
| 11 |
+
import scan # අපි ඔයාගේ app.py එක scan.py කියලා rename කළානේ
|
| 12 |
+
|
| 13 |
+
app = FastAPI()
|
| 14 |
+
|
| 15 |
+
@app.get("/")
|
| 16 |
+
def home():
|
| 17 |
+
return {"message": "Cipher-MD Python Engine is Running! 🚀"}
|
| 18 |
+
|
| 19 |
+
# =================================================================
|
| 20 |
+
# 1️⃣ MATH SOLVER ENDPOINT
|
| 21 |
+
# =================================================================
|
| 22 |
+
@app.post("/math")
|
| 23 |
+
async def solve_math(image: UploadFile = File(...), prompt: str = Form("")):
|
| 24 |
+
temp_filename = f"temp_math_{image.filename}"
|
| 25 |
+
|
| 26 |
+
# 1. Image එක Save කරගන්නවා
|
| 27 |
+
with open(temp_filename, "wb") as buffer:
|
| 28 |
+
shutil.copyfileobj(image.file, buffer)
|
| 29 |
+
|
| 30 |
+
# 2. ඔයාගේ math_solver එකේ Output එක (Print වෙන ඒවා) අල්ලාගන්නවා
|
| 31 |
+
output_capture = StringIO()
|
| 32 |
+
try:
|
| 33 |
+
# Stdout එක capture කරනවා (Console එකට print වෙන දේ variable එකකට ගන්නවා)
|
| 34 |
+
with contextlib.redirect_stdout(output_capture):
|
| 35 |
+
# ඔයාගේ original function එක call කරනවා
|
| 36 |
+
math_solver.solve_math_problem(temp_filename, prompt)
|
| 37 |
+
|
| 38 |
+
result = output_capture.getvalue()
|
| 39 |
+
except Exception as e:
|
| 40 |
+
result = f"Error: {str(e)}"
|
| 41 |
+
finally:
|
| 42 |
+
# කුණු ෆයිල් අස් කරනවා
|
| 43 |
+
if os.path.exists(temp_filename):
|
| 44 |
+
os.remove(temp_filename)
|
| 45 |
+
|
| 46 |
+
# 3. JSON විදිහට උත්තරේ යවනවා
|
| 47 |
+
return {"result": result}
|
| 48 |
+
|
| 49 |
+
# =================================================================
|
| 50 |
+
# 2️⃣ SCAN / REVEAL ENDPOINT
|
| 51 |
+
# =================================================================
|
| 52 |
+
@app.post("/scan")
|
| 53 |
+
async def scan_watermark(image: UploadFile = File(...)):
|
| 54 |
+
in_file = f"in_{image.filename}"
|
| 55 |
+
out_file = f"out_{image.filename}"
|
| 56 |
+
|
| 57 |
+
# 1. Image එක Save කරගන්නවා
|
| 58 |
+
with open(in_file, "wb") as buffer:
|
| 59 |
+
shutil.copyfileobj(image.file, buffer)
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
# 2. ඔයාගේ scan script (reveal_watermark) එක run කරනවා
|
| 63 |
+
scan.reveal_watermark(in_file, out_file)
|
| 64 |
+
|
| 65 |
+
# 3. හැදුනු ෆොටෝ එක ආපහු යවනවා
|
| 66 |
+
if os.path.exists(out_file):
|
| 67 |
+
return FileResponse(out_file)
|
| 68 |
+
else:
|
| 69 |
+
return {"error": "Processing failed"}
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
return {"error": str(e)}
|
| 73 |
+
|
| 74 |
+
finally:
|
| 75 |
+
# Input ෆයිල් එක විතරක් මකනවා (Output එක යැව්වට පස්සේ මැකෙන විදිහට HuggingFace එකේ auto clean වෙනවා)
|
| 76 |
+
if os.path.exists(in_file):
|
| 77 |
+
os.remove(in_file)
|
| 78 |
+
|
| 79 |
+
# Docker එකේ Run වෙන්න ඕන නිසා
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
import uvicorn
|
| 82 |
+
uvicorn.run(app, host="0.0.0.0", port=7864)
|
math_scan/math_solver.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import base64
|
| 3 |
+
import requests
|
| 4 |
+
import json
|
| 5 |
+
import sys
|
| 6 |
+
import io
|
| 7 |
+
|
| 8 |
+
# UTF-8 Encoding අනිවාර්යයි (නැත්නම් x² අකුර Print වෙද්දි Error එනවා)
|
| 9 |
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
| 10 |
+
|
| 11 |
+
def solve_math_problem(image_path, user_prompt=""):
|
| 12 |
+
img = cv2.imread(image_path)
|
| 13 |
+
if img is None:
|
| 14 |
+
print("❌ Error: Image not found!")
|
| 15 |
+
return
|
| 16 |
+
|
| 17 |
+
# Image Resize Logic
|
| 18 |
+
height, width = img.shape[:2]
|
| 19 |
+
max_width = 768
|
| 20 |
+
if width > max_width:
|
| 21 |
+
ratio = max_width / width
|
| 22 |
+
new_height = int(height * ratio)
|
| 23 |
+
img = cv2.resize(img, (max_width, new_height), interpolation=cv2.INTER_AREA)
|
| 24 |
+
|
| 25 |
+
_, buffer = cv2.imencode('.jpg', img)
|
| 26 |
+
base64_image = base64.b64encode(buffer).decode('utf-8')
|
| 27 |
+
data_url = f"data:image/jpeg;base64,{base64_image}"
|
| 28 |
+
|
| 29 |
+
if not user_prompt.strip():
|
| 30 |
+
final_instruction = "Solve this accurately."
|
| 31 |
+
else:
|
| 32 |
+
final_instruction = f"User Request: {user_prompt}"
|
| 33 |
+
|
| 34 |
+
# 🔥 විශේෂ Prompt එක (Formatting Rules)
|
| 35 |
+
prompt_text = f"""You are a math tutor on WhatsApp.
|
| 36 |
+
|
| 37 |
+
CRITICAL FORMATTING RULES (Follow these strictly):
|
| 38 |
+
1. 🚫 NEVER use the caret symbol (^).
|
| 39 |
+
- CORRECT: x², y³, 10⁵
|
| 40 |
+
- WRONG: x^2, y^3
|
| 41 |
+
2. 🚫 NEVER use asterisk (*) for multiplication.
|
| 42 |
+
- CORRECT: 3x × 2y
|
| 43 |
+
- WRONG: 3x * 2y
|
| 44 |
+
3. 🚫 NEVER use standard slash (/) for division steps if confusing.
|
| 45 |
+
- Use '÷' or write clearly.
|
| 46 |
+
4. Write algebraic terms naturally (e.g., 3x² + 2x + 5).
|
| 47 |
+
|
| 48 |
+
LAYOUT:
|
| 49 |
+
🎯 **Answer:** [Final Result in Bold]
|
| 50 |
+
──────────────────
|
| 51 |
+
📝 **Steps:**
|
| 52 |
+
[Use Emojis 🔹 for bullet points. Keep sentences short.]
|
| 53 |
+
──────────────────
|
| 54 |
+
|
| 55 |
+
INSTRUCTION: {final_instruction}"""
|
| 56 |
+
|
| 57 |
+
url = "https://text.pollinations.ai/"
|
| 58 |
+
payload = {
|
| 59 |
+
"messages": [
|
| 60 |
+
{
|
| 61 |
+
"role": "user",
|
| 62 |
+
"content": [
|
| 63 |
+
{"type": "text", "text": prompt_text},
|
| 64 |
+
{"type": "image_url", "image_url": {"url": data_url}}
|
| 65 |
+
]
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"model": "gemini",
|
| 69 |
+
"jsonMode": False
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
response = requests.post(url, json=payload, headers={"Content-Type": "application/json"})
|
| 74 |
+
response.encoding = 'utf-8'
|
| 75 |
+
|
| 76 |
+
if response.status_code == 200:
|
| 77 |
+
print(response.text)
|
| 78 |
+
else:
|
| 79 |
+
print("❌ AI Error.")
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"❌ Connection Error: {str(e)}")
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
img_file = sys.argv[1] if len(sys.argv) > 1 else "math.png"
|
| 86 |
+
u_prompt = sys.argv[2] if len(sys.argv) > 2 else ""
|
| 87 |
+
solve_math_problem(img_file, u_prompt)
|
math_scan/scan.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
#sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
|
| 5 |
+
def reveal_watermark(input_path, output_path):
|
| 6 |
+
try:
|
| 7 |
+
# 1. පින්තූරය කියවගන්නවා
|
| 8 |
+
img = cv2.imread(input_path)
|
| 9 |
+
if img is None:
|
| 10 |
+
print("Error: Image not found.")
|
| 11 |
+
return
|
| 12 |
+
|
| 13 |
+
# 2. රහස් හොයන ෆිල්ටර් එක (Laplacian)
|
| 14 |
+
laplacian = cv2.Laplacian(img, cv2.CV_64F)
|
| 15 |
+
|
| 16 |
+
# 3. අහුවුණු දේවල් තදින් පෙන්නනවා (Amplify x20)
|
| 17 |
+
revealed = cv2.convertScaleAbs(laplacian, alpha=20)
|
| 18 |
+
|
| 19 |
+
# 4. Save කරනවා (Window එකේ පෙන්නන්නේ නැතුව)
|
| 20 |
+
cv2.imwrite(output_path, revealed)
|
| 21 |
+
print("Success")
|
| 22 |
+
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"Error: {e}")
|
| 25 |
+
|
| 26 |
+
if __name__ == "__main__":
|
| 27 |
+
# බොට් එකෙන් එවන නම් දෙක මෙතනින් ගන්නවා (Input & Output)
|
| 28 |
+
if len(sys.argv) > 2:
|
| 29 |
+
input_file = sys.argv[1]
|
| 30 |
+
output_file = sys.argv[2]
|
| 31 |
+
reveal_watermark(input_file, output_file)
|
| 32 |
+
else:
|
| 33 |
+
|
| 34 |
+
print("Error: Arguments missing")
|