Spaces:
Sleeping
Sleeping
rm oracle DB loggic
Browse files- app.py +2 -98
- requirements.txt +0 -1
- start.py +16 -89
- start_server.sh +2 -2
app.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
| 1 |
import os
|
| 2 |
-
import json
|
| 3 |
from typing import List, Optional
|
| 4 |
from fastapi import FastAPI, HTTPException
|
| 5 |
-
from pydantic import BaseModel, Field
|
| 6 |
-
import oracledb
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
import pandas as pd
|
| 9 |
from datetime import datetime
|
|
@@ -12,46 +10,13 @@ from datasets import Dataset, DatasetDict, load_dataset
|
|
| 12 |
# λ‘컬 κ°λ°: .env νμΌ λ‘λ (μμΌλ©΄)
|
| 13 |
load_dotenv()
|
| 14 |
|
| 15 |
-
# ----- νκ²½ λ³μ -----
|
| 16 |
-
DB_USER = os.environ["DB_USER"]
|
| 17 |
-
DB_PASSWORD = os.environ["DB_PASSWORD"]
|
| 18 |
-
WALLET_DIR = os.environ["WALLET_DIR"]
|
| 19 |
-
WALLET_PASSWORD = os.environ["WALLET_PASSWORD"]
|
| 20 |
-
|
| 21 |
-
# tnsnames.ora μμ alias νμΈ (λ³΄ν΅ *_high)
|
| 22 |
-
TNS_ALIAS = os.environ["DB_TNS_ALIAS"]
|
| 23 |
-
|
| 24 |
# Hugging Face μ€μ
|
| 25 |
HF_DATA_REPO_ID = os.getenv("HF_DATA_REPO_ID")
|
| 26 |
HF_DATA_TOKEN = os.getenv("HF_DATA_TOKEN")
|
| 27 |
|
| 28 |
-
|
| 29 |
-
# μ λ νΈμΆνμ§ λ§μΈμ: oracledb.init_oracle_client() # (Thickλ‘ λΉ μ Έμ μ€ν¨ κ°λ₯)
|
| 30 |
-
pool: oracledb.ConnectionPool = oracledb.create_pool(
|
| 31 |
-
user=DB_USER,
|
| 32 |
-
password=DB_PASSWORD,
|
| 33 |
-
dsn=TNS_ALIAS, # Wallet tnsnames.oraμ alias
|
| 34 |
-
config_dir=WALLET_DIR,
|
| 35 |
-
wallet_location=WALLET_DIR,
|
| 36 |
-
wallet_password=WALLET_PASSWORD,
|
| 37 |
-
min=1, max=4, increment=1,
|
| 38 |
-
homogeneous=True,
|
| 39 |
-
timeout=60,
|
| 40 |
-
retry_count=6, retry_delay=2
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
app = FastAPI(title="MuscleCare Hybrid Server (mTLS)")
|
| 44 |
|
| 45 |
# ----- λͺ¨λΈ -----
|
| 46 |
-
class StatePayload(BaseModel):
|
| 47 |
-
model_config = ConfigDict(protected_namespaces=())
|
| 48 |
-
|
| 49 |
-
user_id: str
|
| 50 |
-
rms_base: Optional[float] = None
|
| 51 |
-
freq_base: Optional[float] = None
|
| 52 |
-
user_emb: Optional[List[float]] = Field(default=None, description="length=12")
|
| 53 |
-
model_version: Optional[str] = None
|
| 54 |
-
|
| 55 |
class LogUploadPayload(BaseModel):
|
| 56 |
user_id: str
|
| 57 |
session_id: str
|
|
@@ -84,10 +49,6 @@ class BatchLogsPayload(BaseModel):
|
|
| 84 |
batch_data: List[BatchLogItem]
|
| 85 |
|
| 86 |
|
| 87 |
-
# ----- μ νΈ -----
|
| 88 |
-
def clob_json(obj) -> str:
|
| 89 |
-
return json.dumps(obj, separators=(",", ":"), ensure_ascii=False)
|
| 90 |
-
|
| 91 |
# ----- μλν¬μΈνΈ -----
|
| 92 |
@app.get("/")
|
| 93 |
def root():
|
|
@@ -98,9 +59,7 @@ def root():
|
|
| 98 |
"version": "1.0.0",
|
| 99 |
"endpoints": {
|
| 100 |
"health": "/health (λΉ λ₯Έ 체ν¬)",
|
| 101 |
-
"health_db": "/health/db (DB μ°κ²° 체ν¬)",
|
| 102 |
"docs": "/docs",
|
| 103 |
-
"upload_state": "/upload_state",
|
| 104 |
"upload_logs": "/upload_logs (κ°λ³ λ‘κ·Έ λ°μ΄ν°)",
|
| 105 |
"user_dataset": "/user_dataset/{user_id}"
|
| 106 |
}
|
|
@@ -119,61 +78,6 @@ def health():
|
|
| 119 |
except Exception as e:
|
| 120 |
return {"ok": False, "error": str(e)}
|
| 121 |
|
| 122 |
-
@app.get("/health/db")
|
| 123 |
-
def health_db():
|
| 124 |
-
"""DB μ°κ²°μ ν¬ν¨ν μμΈ health 체ν¬"""
|
| 125 |
-
try:
|
| 126 |
-
with pool.acquire() as conn:
|
| 127 |
-
with conn.cursor() as cur:
|
| 128 |
-
cur.execute("SELECT 1 FROM DUAL")
|
| 129 |
-
v = cur.fetchone()[0]
|
| 130 |
-
return {"ok": True, "db": v, "server": "running"}
|
| 131 |
-
except Exception as e:
|
| 132 |
-
return {"ok": False, "db": "error", "error": str(e)}
|
| 133 |
-
|
| 134 |
-
@app.post("/upload_state")
|
| 135 |
-
def upload_state(p: StatePayload):
|
| 136 |
-
# MERGE INTO MuscleCare.user_state
|
| 137 |
-
try:
|
| 138 |
-
emb_json = None
|
| 139 |
-
if p.user_emb is not None:
|
| 140 |
-
if len(p.user_emb) != 12:
|
| 141 |
-
raise HTTPException(400, "user_emb must have length=12")
|
| 142 |
-
emb_json = clob_json(p.user_emb)
|
| 143 |
-
|
| 144 |
-
with pool.acquire() as conn:
|
| 145 |
-
with conn.cursor() as cur:
|
| 146 |
-
cur.execute("""
|
| 147 |
-
MERGE INTO user_state t
|
| 148 |
-
USING (
|
| 149 |
-
SELECT :user_id AS user_id FROM dual
|
| 150 |
-
) s
|
| 151 |
-
ON (t.user_id = s.user_id)
|
| 152 |
-
WHEN MATCHED THEN UPDATE SET
|
| 153 |
-
rms_base = :rms_base,
|
| 154 |
-
freq_base = :freq_base,
|
| 155 |
-
user_emb = :user_emb,
|
| 156 |
-
model_version = :model_version,
|
| 157 |
-
last_sync = CURRENT_TIMESTAMP
|
| 158 |
-
WHEN NOT MATCHED THEN INSERT
|
| 159 |
-
(user_id, rms_base, freq_base, user_emb, model_version, last_sync)
|
| 160 |
-
VALUES
|
| 161 |
-
(:user_id, :rms_base, :freq_base, :user_emb, :model_version, CURRENT_TIMESTAMP)
|
| 162 |
-
""", dict(
|
| 163 |
-
user_id=p.user_id,
|
| 164 |
-
rms_base=p.rms_base,
|
| 165 |
-
freq_base=p.freq_base,
|
| 166 |
-
user_emb=emb_json,
|
| 167 |
-
model_version=p.model_version
|
| 168 |
-
))
|
| 169 |
-
conn.commit()
|
| 170 |
-
return {"ok": True}
|
| 171 |
-
except HTTPException:
|
| 172 |
-
raise
|
| 173 |
-
except Exception as e:
|
| 174 |
-
raise HTTPException(500, f"upload_state failed: {e}")
|
| 175 |
-
|
| 176 |
-
|
| 177 |
@app.post("/upload_logs")
|
| 178 |
async def upload_logs(payload: LogUploadPayload):
|
| 179 |
"""κ°λ³ λ‘κ·Έ λ°μ΄ν°λ₯Ό Hugging Face Hubλ‘ νΈμ"""
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
from typing import List, Optional
|
| 3 |
from fastapi import FastAPI, HTTPException
|
| 4 |
+
from pydantic import BaseModel, Field
|
|
|
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
import pandas as pd
|
| 7 |
from datetime import datetime
|
|
|
|
| 10 |
# λ‘컬 κ°λ°: .env νμΌ λ‘λ (μμΌλ©΄)
|
| 11 |
load_dotenv()
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# Hugging Face μ€μ
|
| 14 |
HF_DATA_REPO_ID = os.getenv("HF_DATA_REPO_ID")
|
| 15 |
HF_DATA_TOKEN = os.getenv("HF_DATA_TOKEN")
|
| 16 |
|
| 17 |
+
app = FastAPI(title="MuscleCare FastAPI Server")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# ----- λͺ¨λΈ -----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
class LogUploadPayload(BaseModel):
|
| 21 |
user_id: str
|
| 22 |
session_id: str
|
|
|
|
| 49 |
batch_data: List[BatchLogItem]
|
| 50 |
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
# ----- μλν¬μΈνΈ -----
|
| 53 |
@app.get("/")
|
| 54 |
def root():
|
|
|
|
| 59 |
"version": "1.0.0",
|
| 60 |
"endpoints": {
|
| 61 |
"health": "/health (λΉ λ₯Έ 체ν¬)",
|
|
|
|
| 62 |
"docs": "/docs",
|
|
|
|
| 63 |
"upload_logs": "/upload_logs (κ°λ³ λ‘κ·Έ λ°μ΄ν°)",
|
| 64 |
"user_dataset": "/user_dataset/{user_id}"
|
| 65 |
}
|
|
|
|
| 78 |
except Exception as e:
|
| 79 |
return {"ok": False, "error": str(e)}
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
@app.post("/upload_logs")
|
| 82 |
async def upload_logs(payload: LogUploadPayload):
|
| 83 |
"""κ°λ³ λ‘κ·Έ λ°μ΄ν°λ₯Ό Hugging Face Hubλ‘ νΈμ"""
|
requirements.txt
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
fastapi
|
| 2 |
uvicorn
|
| 3 |
python-multipart
|
| 4 |
-
oracledb>=2.3
|
| 5 |
pydantic
|
| 6 |
python-dotenv
|
| 7 |
datasets
|
|
|
|
| 1 |
fastapi
|
| 2 |
uvicorn
|
| 3 |
python-multipart
|
|
|
|
| 4 |
pydantic
|
| 5 |
python-dotenv
|
| 6 |
datasets
|
start.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
|
|
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
|
| 5 |
# .env νμΌ λ‘λ (λ‘컬 κ°λ°μ©)
|
|
@@ -12,88 +13,21 @@ def setup_environment():
|
|
| 12 |
- Hugging Face Space: νκ²½λ³μμμ μλ μ€μ
|
| 13 |
"""
|
| 14 |
print("π§ νκ²½λ³μ μ€μ μ€...")
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
if not os.getenv("HF_DATA_TOKEN"):
|
| 22 |
-
os.environ["HF_DATA_TOKEN"] = os.getenv("HF_DATA_TOKEN")
|
| 23 |
-
print("π HF_DATA_TOKEN κΈ°λ³Έκ° μ€μ ")
|
| 24 |
-
|
| 25 |
-
# Oracle DB μ€μ νμΈ
|
| 26 |
-
required_vars = ["DB_USER", "DB_PASSWORD", "WALLET_DIR", "WALLET_PASSWORD"]
|
| 27 |
-
missing_vars = []
|
| 28 |
-
|
| 29 |
-
for var in required_vars:
|
| 30 |
-
if not os.getenv(var):
|
| 31 |
-
missing_vars.append(var)
|
| 32 |
-
|
| 33 |
-
if missing_vars:
|
| 34 |
-
print(f"β οΈ λλ½λ νκ²½λ³μ: {', '.join(missing_vars)}")
|
| 35 |
-
print("π‘ .env νμΌμ νμΈνκ±°λ νκ²½λ³μλ₯Ό μ€μ ν΄μ£ΌμΈμ.")
|
| 36 |
-
|
| 37 |
-
# λ‘컬 κ°λ°μ μν κΈ°λ³Έκ° μ€μ (κ²½κ³ μ ν¨κ»)
|
| 38 |
-
if not os.getenv("DB_USER"):
|
| 39 |
-
os.environ["DB_USER"] = "ADMIN"
|
| 40 |
-
print("π DB_USER κΈ°λ³Έκ° μ€μ : ADMIN")
|
| 41 |
-
if not os.getenv("DB_PASSWORD"):
|
| 42 |
-
os.environ["DB_PASSWORD"] = "YourDBPassword123!"
|
| 43 |
-
print("π DB_PASSWORD κΈ°λ³Έκ° μ€μ : YourDBPassword123!")
|
| 44 |
-
if not os.getenv("WALLET_PASSWORD"):
|
| 45 |
-
os.environ["WALLET_PASSWORD"] = "YourWalletPassword123!"
|
| 46 |
-
print("π WALLET_PASSWORD κΈ°λ³Έκ° μ€μ : YourWalletPassword123!")
|
| 47 |
else:
|
| 48 |
-
print("
|
| 49 |
-
|
| 50 |
-
print("π§ νκ²½λ³μ μ€μ μλ£")
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
""
|
| 58 |
-
# λ‘컬 κ°λ°: WALLET_DIRμ΄ μ΄λ―Έ μ€μ λμ΄ μλ κ²½μ°
|
| 59 |
-
if "WALLET_DIR" in os.environ:
|
| 60 |
-
wallet_dir = os.environ["WALLET_DIR"]
|
| 61 |
-
if Path(wallet_dir).exists():
|
| 62 |
-
print(f"π λ‘컬 Wallet μ¬μ©: {wallet_dir}")
|
| 63 |
-
return wallet_dir
|
| 64 |
-
|
| 65 |
-
# λ°°ν¬ νκ²½: WALLET_ZIP_B64μμ λμ½λ©
|
| 66 |
-
if "WALLET_ZIP_B64" not in os.environ:
|
| 67 |
-
raise RuntimeError(
|
| 68 |
-
"β WALLET_ZIP_B64 λλ WALLET_DIR νκ²½ λ³μκ° νμν©λλ€.\n"
|
| 69 |
-
" λ‘컬: .env νμΌμ WALLET_DIR μ€μ \n"
|
| 70 |
-
" λ°°ν¬: Hugging Face Secretsμ WALLET_ZIP_B64 μ€μ "
|
| 71 |
-
)
|
| 72 |
-
|
| 73 |
-
print("π Wallet Base64 λμ½λ© μμ...")
|
| 74 |
-
b64 = os.environ["WALLET_ZIP_B64"]
|
| 75 |
-
wallet_password = os.environ["WALLET_PASSWORD"]
|
| 76 |
-
|
| 77 |
-
# walletμ λ©λͺ¨λ¦¬->μμλλ ν λ¦¬λ‘ λ³΅μ
|
| 78 |
-
wallet_dir = tempfile.mkdtemp(prefix="wallet_")
|
| 79 |
-
print(f"π μμ λλ ν 리: {wallet_dir}")
|
| 80 |
-
|
| 81 |
-
zip_bytes = base64.b64decode(b64)
|
| 82 |
-
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as z:
|
| 83 |
-
z.extractall(wallet_dir)
|
| 84 |
-
|
| 85 |
-
# μμ μ μν΄ κΆν μΆμ
|
| 86 |
-
for root, _, files in os.walk(wallet_dir):
|
| 87 |
-
for f in files:
|
| 88 |
-
os.chmod(os.path.join(root, f), 0o600)
|
| 89 |
-
|
| 90 |
-
print("β
Wallet μμΆ ν΄μ μλ£")
|
| 91 |
-
|
| 92 |
-
# μ§κ° λΉλ°λ²νΈλ oracledbμμ μ¬μ©
|
| 93 |
-
os.environ["WALLET_DIR"] = wallet_dir
|
| 94 |
-
os.environ["WALLET_PASSWORD"] = wallet_password
|
| 95 |
-
|
| 96 |
-
return wallet_dir
|
| 97 |
|
| 98 |
if __name__ == "__main__":
|
| 99 |
print("=" * 60)
|
|
@@ -103,13 +37,6 @@ if __name__ == "__main__":
|
|
| 103 |
# νκ²½λ³μ μλ μ€μ
|
| 104 |
setup_environment()
|
| 105 |
|
| 106 |
-
# Wallet μ€λΉ
|
| 107 |
-
try:
|
| 108 |
-
prepare_wallet_dir()
|
| 109 |
-
except Exception as e:
|
| 110 |
-
print(f"β Wallet μ€λΉ μ€ν¨: {e}")
|
| 111 |
-
sys.exit(1)
|
| 112 |
-
|
| 113 |
# Uvicorn κΈ°λ
|
| 114 |
print("\nπ FastAPI μλ² μ€ν...")
|
| 115 |
print("=" * 60)
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import subprocess
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
|
| 6 |
# .env νμΌ λ‘λ (λ‘컬 κ°λ°μ©)
|
|
|
|
| 13 |
- Hugging Face Space: νκ²½λ³μμμ μλ μ€μ
|
| 14 |
"""
|
| 15 |
print("π§ νκ²½λ³μ μ€μ μ€...")
|
| 16 |
+
|
| 17 |
+
hf_repo = os.getenv("HF_DATA_REPO_ID")
|
| 18 |
+
hf_token = os.getenv("HF_DATA_TOKEN")
|
| 19 |
+
|
| 20 |
+
if hf_repo:
|
| 21 |
+
print("β
HF_DATA_REPO_ID μ€μ μλ£")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
else:
|
| 23 |
+
print("βΉοΈ HF_DATA_REPO_IDκ° μ€μ λμ§ μμμ΅λλ€. Hugging Face μ
λ‘λ κΈ°λ₯μ΄ μ νλ μ μμ΅λλ€.")
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
if hf_token:
|
| 26 |
+
print("β
HF_DATA_TOKEN μ€μ μλ£")
|
| 27 |
+
else:
|
| 28 |
+
print("βΉοΈ HF_DATA_TOKENμ΄ μ€μ λμ§ μμμ΅λλ€. Hugging Face μ
λ‘λ κΈ°λ₯μ΄ μ νλ μ μμ΅λλ€.")
|
| 29 |
+
|
| 30 |
+
print("π§ νκ²½λ³μ μ€μ μλ£")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
if __name__ == "__main__":
|
| 33 |
print("=" * 60)
|
|
|
|
| 37 |
# νκ²½λ³μ μλ μ€μ
|
| 38 |
setup_environment()
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
# Uvicorn κΈ°λ
|
| 41 |
print("\nπ FastAPI μλ² μ€ν...")
|
| 42 |
print("=" * 60)
|
start_server.sh
CHANGED
|
@@ -19,7 +19,7 @@ echo "π§ νκ²½λ³μλ start.pyμμ μλμΌλ‘ μ€μ λ©λλ€."
|
|
| 19 |
|
| 20 |
# Python μμ‘΄μ± νμΈ λ° μ€μΉ
|
| 21 |
echo "π¦ Python μμ‘΄μ± νμΈ μ€..."
|
| 22 |
-
if ! python3 -c "import fastapi, uvicorn
|
| 23 |
echo "β νμ Python ν¨ν€μ§κ° μ€μΉλμ§ μμμ΅λλ€."
|
| 24 |
echo "π§ μλ μ€μΉλ₯Ό μμν©λλ€..."
|
| 25 |
|
|
@@ -38,7 +38,7 @@ if ! python3 -c "import fastapi, uvicorn, oracledb" 2>/dev/null; then
|
|
| 38 |
fi
|
| 39 |
|
| 40 |
# μ€μΉ ν λ€μ νμΈ
|
| 41 |
-
if ! python3 -c "import fastapi, uvicorn
|
| 42 |
echo "β ν¨ν€μ§ μ€μΉμ μ€ν¨νμ΅λλ€."
|
| 43 |
echo "π‘ μλμΌλ‘ μ€μΉν΄μ£ΌμΈμ:"
|
| 44 |
echo " pip3 install -r requirements.txt"
|
|
|
|
| 19 |
|
| 20 |
# Python μμ‘΄μ± νμΈ λ° μ€μΉ
|
| 21 |
echo "π¦ Python μμ‘΄μ± νμΈ μ€..."
|
| 22 |
+
if ! python3 -c "import fastapi, uvicorn" 2>/dev/null; then
|
| 23 |
echo "β νμ Python ν¨ν€μ§κ° μ€μΉλμ§ μμμ΅λλ€."
|
| 24 |
echo "π§ μλ μ€μΉλ₯Ό μμν©λλ€..."
|
| 25 |
|
|
|
|
| 38 |
fi
|
| 39 |
|
| 40 |
# μ€μΉ ν λ€μ νμΈ
|
| 41 |
+
if ! python3 -c "import fastapi, uvicorn" 2>/dev/null; then
|
| 42 |
echo "β ν¨ν€μ§ μ€μΉμ μ€ν¨νμ΅λλ€."
|
| 43 |
echo "π‘ μλμΌλ‘ μ€μΉν΄μ£ΌμΈμ:"
|
| 44 |
echo " pip3 install -r requirements.txt"
|