{
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "colab": {
   "provenance": [],
   "gpuType": "T4"
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "name": "python"
  },
  "accelerator": "GPU"
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# 실습 02b · 타로 해석 웹앱 — 로컬 오픈 모델\n\n> **연계 강의자료:** M2 「생성형 AI와 LLM의 이해」 · 3주차\n> **목표:** 역할 부여 프롬프트(프롬프트 엔지니어링)와 Gradio UI로 로컬 LLM 응용 앱을 만든다. 02c(API판)와 응답 품질을 비교하는 것이 핵심이다.\n>\n> 카드 이미지를 표시하려면 `tarot_data/` 폴더가 필요하다(구자료/실습 참조). 이미지가 없어도 해석 텍스트는 정상적으로 생성된다.\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "Zg4a-qUc8s9r"
   },
   "outputs": [],
   "source": [
    "!pip install gradio"
   ]
  },
  {
   "cell_type": "code",
   "source": [
    "import os\n",
    "os.environ[\"HUGGING_FACE_HUB_TOKEN\"] = \"자신의 토큰을 넣으세요\""
   ],
   "metadata": {
    "id": "cDoYOOJ185sQ"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "source": [
    "import random\n",
    "import gradio as gr\n",
    "from transformers import AutoTokenizer, AutoModelForCausalLM\n",
    "import torch\n",
    "from PIL import Image\n",
    "import os\n",
    "\n",
    "# 타로 카드 데이터 (메이저 아르카나 22장)\n",
    "# Tarot card data (22 Major Arcana cards)\n",
    "tarot_cards = [\n",
    "    {\"name\": \"The Fool\", \"image\": \"fool.jpg\", \"description\": \"새로운 시작, 모험, 순수함, 자유로운 정신\"},\n",
    "    {\"name\": \"The Magician\", \"image\": \"magician.jpg\", \"description\": \"창의성, 기술, 의지력, 집중력\"},\n",
    "    {\"name\": \"The High Priestess\", \"image\": \"high_priestess.jpg\", \"description\": \"직관, 신비, 잠재의식, 내면의 지혜\"},\n",
    "    {\"name\": \"The Empress\", \"image\": \"empress.jpg\", \"description\": \"풍요, 창의성, 모성, 자연\"},\n",
    "    {\"name\": \"The Emperor\", \"image\": \"emperor.jpg\", \"description\": \"권위, 구조, 리더십, 안정\"},\n",
    "    {\"name\": \"The Hierophant\", \"image\": \"hierophant.jpg\", \"description\": \"전통, 교육, 신념 체계, 조언\"},\n",
    "    {\"name\": \"The Lovers\", \"image\": \"lovers.jpg\", \"description\": \"사랑, 조화, 관계, 가치관\"},\n",
    "    {\"name\": \"The Chariot\", \"image\": \"chariot.jpg\", \"description\": \"의지력, 결단력, 승리, 자제력\"},\n",
    "    {\"name\": \"Strength\", \"image\": \"strength.jpg\", \"description\": \"용기, 내면의 힘, 인내, 자신감\"},\n",
    "    {\"name\": \"The Hermit\", \"image\": \"hermit.jpg\", \"description\": \"내면의 성찰, 고독, 지혜, 안내\"},\n",
    "    {\"name\": \"Wheel of Fortune\", \"image\": \"wheel_of_fortune.jpg\", \"description\": \"운명, 기회, 변화, 주기\"},\n",
    "    {\"name\": \"Justice\", \"image\": \"justice.jpg\", \"description\": \"정의, 균형, 진실, 인과관계\"},\n",
    "    {\"name\": \"The Hanged Man\", \"image\": \"hanged_man.jpg\", \"description\": \"희생, 새로운 관점, 중단, 항복\"},\n",
    "    {\"name\": \"Death\", \"image\": \"death.jpg\", \"description\": \"끝, 변화, 변형, 새로운 시작\"},\n",
    "    {\"name\": \"Temperance\", \"image\": \"temperance.jpg\", \"description\": \"균형, 절제, 조화, 중용\"},\n",
    "    {\"name\": \"The Devil\", \"image\": \"devil.jpg\", \"description\": \"속박, 중독, 물질주의, 그림자 자아\"},\n",
    "    {\"name\": \"The Tower\", \"image\": \"tower.jpg\", \"description\": \"급격한 변화, 혼란, 계시, 해방\"},\n",
    "    {\"name\": \"The Star\", \"image\": \"star.jpg\", \"description\": \"희망, 영감, 평온, 갱신\"},\n",
    "    {\"name\": \"The Moon\", \"image\": \"moon.jpg\", \"description\": \"환상, 불확실성, 직관, 잠재의식\"},\n",
    "    {\"name\": \"The Sun\", \"image\": \"sun.jpg\", \"description\": \"기쁨, 성공, 활력, 명료함\"},\n",
    "    {\"name\": \"Judgement\", \"image\": \"judgement.jpg\", \"description\": \"심판, 부활, 내면의 소리, 깨달음\"},\n",
    "    {\"name\": \"The World\", \"image\": \"world.jpg\", \"description\": \"완성, 통합, 성취, 여행\"}\n",
    "]\n",
    "# LLM 모델 및 토크나이저 초기화\n",
    "try:\n",
    "    tokenizer = AutoTokenizer.from_pretrained(\"google/gemma-2b-it\")\n",
    "    model = AutoModelForCausalLM.from_pretrained(\"google/gemma-2b-it\", device_map=\"auto\", torch_dtype=torch.float16)\n",
    "    print(\"LLM 모델이 성공적으로 로드되었습니다.\")\n",
    "except Exception as e:\n",
    "    print(f\"LLM 모델 로딩 중 오류 발생: {e}\")\n",
    "    print(\"간단한 규칙 기반 해석을 사용합니다.\")\n",
    "    model = None\n",
    "    tokenizer = None\n",
    "\n",
    "def get_image_path(image_name):\n",
    "    return os.path.join('tarot_data', image_name)\n",
    "\n",
    "def load_and_resize_image(image_path, is_upright):\n",
    "    try:\n",
    "        img = Image.open(image_path)\n",
    "        img = img.resize((150, 280))  # 이미지 크기 조정\n",
    "        if not is_upright:\n",
    "            img = img.rotate(180)\n",
    "        return img\n",
    "    except Exception as e:\n",
    "        print(f\"이미지 로딩 중 오류 발생: {e}\")\n",
    "        return None\n",
    "\n",
    "def get_llm_interpretation(cards, orientations, interest):\n",
    "\n",
    "    prompt = f\"\"\"\n",
    "    당신은 타로점을 보는 최고의 점술사입니다. 과거, 현재, 미래의 카드점을 보는 중이며, 다음 카드들이 선택되었습니다.\n",
    "    과거: {cards[0]['name']} ({orientations[0]})\n",
    "    기본 의미: {cards[0]['description']}\n",
    "    현재: {cards[1]['name']} ({orientations[1]})\n",
    "    기본 의미: {cards[1]['description']}\n",
    "    미래: {cards[2]['name']} ({orientations[2]})\n",
    "    기본 의미: {cards[2]['description']}\n",
    "    관심사: {interest}\n",
    "    카드가 역방향이면 좀더 주의해야 할점에 대해서 다루면 됩니다.\n",
    "    위 정보를 바탕으로 사용자의 관심사인 '{interest}'에 대한 질문자의 과거, 현재, 미래의 상황을 각각 200자 내외로  이야기하세요.\n",
    "    마지막으로 각 상황을 연결하여 하나의 이야기로 풀어내주세요.\n",
    "    답변을 반복하지 마시오.\n",
    "    \"\"\"\n",
    "    '''\n",
    "    prompt = f\"\"\"\n",
    "    You are the best tarot reader. You are doing a past, present, and future card reading, and the following cards have been selected:\n",
    "\n",
    "    Past: {cards[0]['name']} ({orientations[0]})\n",
    "    Basic meaning: {cards[0]['description']}\n",
    "\n",
    "    Present: {cards[1]['name']} ({orientations[1]})\n",
    "    Basic meaning: {cards[1]['description']}\n",
    "\n",
    "    Future: {cards[2]['name']} ({orientations[2]})\n",
    "    Basic meaning: {cards[2]['description']}\n",
    "\n",
    "    Area of interest: {interest}\n",
    "\n",
    "    If the card is reversed, we'll cover some of the things to watch out for.\n",
    "\n",
    "    Based on the above information, please describe the querent's past, present, and future situations regarding their area of interest '{interest}' in about 200 characters each.\n",
    "\n",
    "    Finally, connect these situations and weave them into one cohesive story.\n",
    "\n",
    "    Do not repeat your answer.\n",
    "    \"\"\"\n",
    "    '''\n",
    "    try:\n",
    "        inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n",
    "        outputs = model.generate(**inputs, max_new_tokens=800, temperature=0.9)\n",
    "        interpretation = tokenizer.decode(outputs[0], skip_special_tokens=True)\n",
    "        #return interpretation.split(\"\\n\")[-1].strip()\n",
    "        return interpretation\n",
    "    except Exception as e:\n",
    "        print(f\"LLM 해석 생성 중 오류 발생: {e}\")\n",
    "        return f\"과거: {cards[0]['name']} ({orientations[0]}): {cards[0]['description']}.\\n\\n\" \\\n",
    "               f\"현재: {cards[1]['name']} ({orientations[1]}): {cards[1]['description']}.\\n\\n\" \\\n",
    "               f\"미래: {cards[2]['name']} ({orientations[2]}): {cards[2]['description']}.\\n\\n\" \\\n",
    "               f\"이는 당신의 '{interest}'에 대해 시간의 흐름에 따른 변화를 보여줍니다.\"\n",
    "\n",
    "def get_tarot_reading(interest):\n",
    "    if not interest:\n",
    "        return \"관심사나 고민을 입력해주세요.\", None, None, None\n",
    "\n",
    "    selected_cards = random.sample(tarot_cards, 3)\n",
    "    orientations = [random.choice([\"default\", \"reversed\"]) for _ in range(3)]\n",
    "\n",
    "    if model and tokenizer:\n",
    "        interpretation = get_llm_interpretation(selected_cards, orientations, interest)\n",
    "    else:\n",
    "        interpretation = \"\\n\\n\".join([\n",
    "            f\"{time}: {card['name']} ({orientation}): {card['description']}\"\n",
    "            for time, card, orientation in zip([\"과거\", \"현재\", \"미래\"], selected_cards, orientations)\n",
    "        ])\n",
    "        interpretation += f\"\\n\\n이는 당신의 '{interest}'에 대해 시간의 흐름에 따른 변화를 보여줍니다.\"\n",
    "\n",
    "    images = [load_and_resize_image(get_image_path(card['image']), orientation == \"default\")\n",
    "              for card, orientation in zip(selected_cards, orientations)]\n",
    "\n",
    "    return interpretation, images[0], images[1], images[2]\n",
    "\n",
    "# Gradio 인터페이스 설정\n",
    "with gr.Blocks() as iface:\n",
    "    gr.Markdown(\"# LLM 기반 타로 카드 해석\")\n",
    "    gr.Markdown(\"당신의 관심사나 고민을 입력하면, 3장의 카드를 자동으로 선택하여 해석해드립니다.\")\n",
    "\n",
    "    interest = gr.Textbox(label=\"당신의 관심사나 고민을 입력해주세요\")\n",
    "    submit_button = gr.Button(\"타로 해석 받기\")\n",
    "\n",
    "    with gr.Row():\n",
    "        past_card = gr.Image(label=\"과거\")\n",
    "        present_card = gr.Image(label=\"현재\")\n",
    "        future_card = gr.Image(label=\"미래\")\n",
    "\n",
    "    interpretation = gr.Textbox(label=\"타로 해석\", lines=10)\n",
    "\n",
    "    submit_button.click(\n",
    "        get_tarot_reading,\n",
    "        inputs=[interest],\n",
    "        outputs=[interpretation, past_card, present_card, future_card],\n",
    "    )\n",
    "\n",
    "iface.launch(share=True)"
   ],
   "metadata": {
    "id": "R-RF5aHL8-2G"
   },
   "execution_count": null,
   "outputs": []
  }
 ]
}