Spaces:
Running
Running
| from fastapi import FastAPI | |
| from fastapi.responses import StreamingResponse, FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from PIL import Image | |
| import requests | |
| from io import BytesIO | |
| import urllib3 | |
| import json | |
| import base64 | |
| import os | |
| import uuid | |
| import time | |
| from pydantic import BaseModel | |
| from typing import Optional | |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
| app = FastAPI() | |
| # ========================= | |
| # CONFIG | |
| # ========================= | |
| BOT_TOKEN = "MTUwMTk1OTI1Njk4MTc2NjE4NA.Gcshyg.YHzk_9k39AVEgLq2IHXtKeLHYMl6AT7GbnUpwE" | |
| CHANNEL_ID = "1470082028518113342" | |
| MINESKIN_TOKEN = "msk_SBLac6yF_ApAeLAIAUGjlX9FQkiZ2itwwSmuzQ3sBM8KryyVHcD1HrW4KqPCcRGgEfIFPAVSY" | |
| # Создаем папку для сохранения скинов | |
| os.makedirs("skins", exist_ok=True) | |
| # Монтируем статические файлы | |
| app.mount("/skins", StaticFiles(directory="skins"), name="skins") | |
| # ========================= | |
| # MODEL | |
| # ========================= | |
| class SkinRequest(BaseModel): | |
| Playername: str | |
| HeadUrl: str | |
| BodyUrl: str | |
| ArmLeft: Optional[str] = None | |
| ArmRight: Optional[str] = None | |
| LegLeft: Optional[str] = None | |
| LegRight: Optional[str] = None | |
| # ========================= | |
| # DOWNLOAD SKIN | |
| # ========================= | |
| def download_skin(url: str): | |
| headers = {"User-Agent": "Mozilla/5.0"} | |
| r = requests.get(url, headers=headers, timeout=30, verify=False) | |
| r.raise_for_status() | |
| return Image.open(BytesIO(r.content)).convert("RGBA") | |
| # ========================= | |
| # PARSE SKIN INPUT | |
| # ========================= | |
| def parse_skin_input(value: str): | |
| if not value: | |
| return None | |
| if value.startswith("http"): | |
| return value | |
| try: | |
| decoded = base64.b64decode(value).decode("utf-8") | |
| data = json.loads(decoded) | |
| return data["textures"]["SKIN"]["url"] | |
| except: | |
| return value | |
| # ========================= | |
| # MINECRAFT SKIN PARTS (ИЗ ПЕРВОГО КОДА) | |
| # ========================= | |
| HEAD = [ | |
| (0, 0, 32, 16), | |
| (32, 0, 64, 16) | |
| ] | |
| BODY = [ | |
| (16, 16, 40, 32), | |
| (16, 32, 40, 48) | |
| ] | |
| RIGHT_ARM = [ | |
| (40, 16, 56, 32), | |
| (40, 32, 56, 48) | |
| ] | |
| LEFT_ARM = [ | |
| (32, 48, 48, 64), | |
| (48, 48, 64, 64) | |
| ] | |
| RIGHT_LEG = [ | |
| (0, 16, 16, 32), | |
| (0, 32, 16, 48) | |
| ] | |
| LEFT_LEG = [ | |
| (16, 48, 32, 64), | |
| (0, 48, 16, 64) | |
| ] | |
| # ========================= | |
| # CLEAR AREA (ИЗ ПЕРВОГО КОДА) | |
| # ========================= | |
| def clear_area(img, box): | |
| x1, y1, x2, y2 = box | |
| empty = Image.new("RGBA", (x2 - x1, y2 - y1), (0, 0, 0, 0)) | |
| img.paste(empty, (x1, y1)) | |
| # ========================= | |
| # COPY PART (ИЗ ПЕРВОГО КОДА) | |
| # ========================= | |
| def copy(img_from, img_to, box): | |
| x1, y1, x2, y2 = box | |
| part = img_from.crop((x1, y1, x2, y2)) | |
| img_to.paste(part, (x1, y1), part) | |
| # ========================= | |
| # BUILD SKIN (ИЗ ПЕРВОГО КОДА) | |
| # ========================= | |
| def create_merged_skin(req: SkinRequest): | |
| body = download_skin(parse_skin_input(req.BodyUrl)) | |
| head = download_skin(parse_skin_input(req.HeadUrl)) | |
| arm_l = download_skin(parse_skin_input(req.ArmLeft)) if req.ArmLeft else None | |
| arm_r = download_skin(parse_skin_input(req.ArmRight)) if req.ArmRight else None | |
| leg_l = download_skin(parse_skin_input(req.LegLeft)) if req.LegLeft else None | |
| leg_r = download_skin(parse_skin_input(req.LegRight)) if req.LegRight else None | |
| # CLEAN BASE | |
| result = Image.new("RGBA", (64, 64), (0, 0, 0, 0)) | |
| # BODY FIRST | |
| for b in BODY: | |
| copy(body, result, b) | |
| # HEAD | |
| for b in HEAD: | |
| copy(head, result, b) | |
| # ARMS (optional override) | |
| if arm_r: | |
| for b in RIGHT_ARM: | |
| clear_area(result, b) | |
| copy(arm_r, result, b) | |
| if arm_l: | |
| for b in LEFT_ARM: | |
| clear_area(result, b) | |
| copy(arm_l, result, b) | |
| # LEGS | |
| if leg_r: | |
| for b in RIGHT_LEG: | |
| clear_area(result, b) | |
| copy(leg_r, result, b) | |
| if leg_l: | |
| for b in LEFT_LEG: | |
| clear_area(result, b) | |
| copy(leg_l, result, b) | |
| return result | |
| # ========================= | |
| # SAVE SKIN | |
| # ========================= | |
| def save_skin(image, playername: str): | |
| safe_name = "".join(c for c in playername if c.isalnum() or c in ('_', '-')) | |
| filename = f"{safe_name}_{uuid.uuid4().hex[:8]}_{int(time.time())}.png" | |
| filepath = os.path.join("skins", filename) | |
| image.save(filepath, format="PNG") | |
| space_url = os.getenv("SPACE_HOST", "dedlepexa-discordapiuse.hf.space") | |
| public_url = f"https://{space_url}/skins/{filename}" | |
| return public_url, filename | |
| # ========================= | |
| # ENDPOINTS (ИЗ ВТОРОГО КОДА) | |
| # ========================= | |
| def merge_to_link(request: SkinRequest): | |
| try: | |
| print(f"🎮 Игрок: {request.Playername}") | |
| merged_skin = create_merged_skin(request) | |
| public_url, filename = save_skin(merged_skin, request.Playername) | |
| print(f"🔗 URL: {public_url}") | |
| return { | |
| "success": True, | |
| "Playername": request.Playername, | |
| "url": public_url, | |
| "filename": filename | |
| } | |
| except Exception as e: | |
| print(f"❌ Ошибка: {str(e)}") | |
| return { | |
| "success": False, | |
| "error": str(e) | |
| } | |
| def merge_png(head_skin: str, body_skin: str): | |
| req = SkinRequest(Playername="test", HeadUrl=head_skin, BodyUrl=body_skin) | |
| img = create_merged_skin(req) | |
| buf = BytesIO() | |
| img.save(buf, "PNG") | |
| buf.seek(0) | |
| return StreamingResponse(buf, media_type="image/png") | |
| def skin_file(file: str): | |
| return FileResponse(os.path.join("skins", file)) | |
| # ========================= | |
| # GET/HEAD для Mineskin | |
| # ========================= | |
| def merge_to_mineskin(request: SkinRequest): | |
| try: | |
| print(f"🎮 Игрок: {request.Playername}") | |
| merged_skin = create_merged_skin(request) | |
| public_url, filename = save_skin( | |
| merged_skin, | |
| request.Playername | |
| ) | |
| print(f"🔗 Skin URL: {public_url}") | |
| headers = { | |
| "Content-Type": "application/json", | |
| "Accept": "application/json", | |
| "User-Agent": "SkinMerger/1.0", | |
| "Authorization": f"Bearer {MINESKIN_TOKEN}" | |
| } | |
| payload = { | |
| "variant": "classic", | |
| "name": request.Playername, | |
| "visibility": "public", | |
| "url": public_url | |
| } | |
| response = requests.post( | |
| "https://api.mineskin.org/v2/generate", | |
| headers=headers, | |
| json=payload, | |
| timeout=60, | |
| verify=False | |
| ) | |
| mineskin_response = response.json() | |
| return { | |
| "success": True, | |
| "Playername": request.Playername, | |
| "skin_url": public_url, | |
| "filename": filename, | |
| "mineskin_response": mineskin_response | |
| } | |
| except Exception as e: | |
| return { | |
| "success": False, | |
| "error": str(e) | |
| } | |
| def root(): | |
| return { | |
| "status": "ok", | |
| "service": "Skin Merger - Fixed", | |
| "endpoints": { | |
| "POST /merge-to-link": "Создать скин и получить ссылку", | |
| "POST /merge-to-mineskin": "Создать скин и загрузить в MineSkin", | |
| "GET /merge.png": "Быстрое тестирование", | |
| "GET /skins/{filename}": "Доступ к сохраненным скинам" | |
| } | |
| } | |
| # ========================= | |
| # RUN | |
| # ========================= | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |