{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 웹소설 → 웹툰 그림 서버 (무료 Colab)\n",
    "**순서**: 런타임 → 런타임 유형 변경 → **T4 GPU** → 아래 셀 3개를 위에서부터 ▶ 실행\n",
    "마지막 셀이 `https://....trycloudflare.com` 주소를 출력해요. 그 주소를 `novel.html` 4번 칸의 **Colab 주소**에 붙여넣으면 끝이에요.\n",
    "- 이 탭(Colab)은 작업하는 동안 **계속 열어 두세요**. 닫거나 오래 두면 주소가 죽고, 다시 실행하면 **새 주소**가 나와요.\n",
    "- 무료 Colab은 GPU 사용 시간이 하루 몇 시간으로 제한돼요."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "#@title 1. 설치 (2~3분)\n",
    "!pip -q install -U diffusers transformers accelerate flask flask-cors\n",
    "!wget -q -O cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 && chmod +x cloudflared\n",
    "print(\"설치 완료\")"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "#@title 2. 모델 불러오기 (처음 한 번, 5분 안팎)\n",
    "import torch\n",
    "from diffusers import StableDiffusionPipeline, UniPCMultistepScheduler\n",
    "BASE = \"Linaqruf/anything-v3-1\" #@param {type:\"string\"}\n",
    "try:\n",
    "    pipe = StableDiffusionPipeline.from_pretrained(BASE, torch_dtype=torch.float16, safety_checker=None)\n",
    "except Exception as e:\n",
    "    print(\"기본 모델을 못 불러와 SD1.5로 대체해요:\", str(e)[:120])\n",
    "    pipe = StableDiffusionPipeline.from_pretrained(\"Comfy-Org/stable-diffusion-v1-5\", torch_dtype=torch.float16, safety_checker=None)\n",
    "pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config)\n",
    "pipe.load_ip_adapter(\"h94/IP-Adapter\", subfolder=\"models\", weight_name=\"ip-adapter-plus_sd15.bin\")\n",
    "pipe.to(\"cuda\")\n",
    "print(\"준비 완료\")"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "#@title 3. 서버 켜기 (실행하면 주소가 나와요)\n",
    "CHARACTER_STRENGTH = 0.8 #@param {type:\"slider\", min:0.3, max:1.0, step:0.05}\n",
    "STEPS = 26 #@param {type:\"integer\"}\n",
    "import io, re, base64, threading, subprocess, time\n",
    "from PIL import Image\n",
    "from flask import Flask, request, Response, jsonify\n",
    "from flask_cors import CORS\n",
    "\n",
    "W, H = 576, 768\n",
    "NEG = \"lowres, bad anatomy, bad hands, extra fingers, worst quality, blurry, text, watermark, multiple people, two people\"\n",
    "STYLE = \"korean webtoon style, manhwa, soft cel shading, sharp lineart, masterpiece, best quality\"\n",
    "lock = threading.Lock()\n",
    "app = Flask(__name__); CORS(app)\n",
    "\n",
    "def png(im):\n",
    "    b = io.BytesIO(); im.save(b, \"PNG\"); return Response(b.getvalue(), mimetype=\"image/png\")\n",
    "\n",
    "def gen(prompt, ref, scale):\n",
    "    pipe.set_ip_adapter_scale(scale)\n",
    "    with lock:\n",
    "        return pipe(prompt=prompt, negative_prompt=NEG, ip_adapter_image=ref, num_inference_steps=int(STEPS),\n",
    "                    guidance_scale=7, height=H, width=W).images[0]\n",
    "\n",
    "@app.post(\"/actor\")\n",
    "def actor():\n",
    "    d = request.get_json(force=True)\n",
    "    ref = Image.open(io.BytesIO(base64.b64decode(d[\"image\"].split(\",\")[1]))).convert(\"RGB\")\n",
    "    part = \"upper body, close-up\" if d.get(\"shot\") == \"close-up\" else \"full body, standing, head to toe\"\n",
    "    p = f\"solo, {part}, {d.get('pose','')}, simple flat green background, {STYLE}\"\n",
    "    return png(gen(p, ref, CHARACTER_STRENGTH))\n",
    "\n",
    "@app.post(\"/bg\")\n",
    "def bg():\n",
    "    d = request.get_json(force=True)\n",
    "    blank = Image.new(\"RGB\", (224, 224), (128, 128, 128))\n",
    "    p = f\"scenery, no humans, empty room, {d.get('text','')}, background illustration, {STYLE}\"\n",
    "    return png(gen(p, blank, 0.0))\n",
    "\n",
    "@app.get(\"/ping\")\n",
    "def ping(): return jsonify(ok=True)\n",
    "\n",
    "threading.Thread(target=lambda: app.run(port=5000, threaded=True), daemon=True).start()\n",
    "time.sleep(2)\n",
    "proc = subprocess.Popen([\"./cloudflared\", \"tunnel\", \"--url\", \"http://localhost:5000\", \"--no-autoupdate\"],\n",
    "                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n",
    "url = None\n",
    "t0 = time.time()\n",
    "while time.time() - t0 < 60 and not url:\n",
    "    line = proc.stdout.readline()\n",
    "    m = re.search(r\"https://[a-z0-9-]+\\.trycloudflare\\.com\", line or \"\")\n",
    "    if m: url = m.group(0)\n",
    "print(\"\\n\" + \"=\" * 50)\n",
    "print(\"Colab 주소 (novel.html에 붙여넣기):\" if url else \"주소를 못 받았어요. 이 셀을 다시 실행해 보세요.\")\n",
    "print(url or \"\")\n",
    "print(\"=\" * 50)\n",
    "print(\"이 탭을 닫지 말고 열어 두세요. (이 셀이 계속 도는 게 정상이에요)\")\n",
    "while True: time.sleep(60)"
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}