Theowise / apps /_shadowing.py
hoon1018's picture
Rename apps/shadowing.py to apps/_shadowing.py
4035457 verified
Raw
History Blame Contribute Delete
11.9 kB
import os
import json
import httpx
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional
router = APIRouter()
@router.get("/health")
async def health():
return {"ok": True}
# ── 곡톡 헬퍼 ──────────────────────────────────────────────────────────
def _upstash_headers():
token = os.environ.get("UPSTASH_REDIS_REST_TOKEN", "")
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
def _upstash_url():
return os.environ.get("UPSTASH_REDIS_REST_URL", "")
def _safe_email(email: str) -> str:
return email.replace("@", "_").replace(".", "_")
# ── μš”μ²­ λͺ¨λΈ ──────────────────────────────────────────────────────────
class AnalyzeRequest(BaseModel):
text: str
class ProgressRequest(BaseModel):
key: str
track: str
position: float
playCount: int
listenSeconds: int
updatedAt: str
trackIdx: Optional[int] = None
class SessionRequest(ProgressRequest):
pass
# 읽기 μ „μš© μš”μ²­ λͺ¨λΈ (μ΄λ©”μΌλ§Œ ν•„μš”)
class EmailRequest(BaseModel):
email: str
# ── 뢄석 ───────────────────────────────────────────────────────────────
@router.post("/api/analyze")
async def analyze_text(req: AnalyzeRequest):
groq_key = os.environ.get("GROQ_API_KEY")
if not groq_key:
return {"reply": "μ„œλ²„μ— Groq API Keyκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. (HF Secrets 확인 ν•„μš”)"}
prompt = f"""You are a British English language coach. Analyze this text for a Korean learner practicing shadowing:
"{req.text}"
Provide in this EXACT format (Korean labels, English explanations):
**μ–΄νœ˜**: Key words/phrases explained simply
**발음 팁**: British pronunciation notes (specific sounds, stress)
**문법**: Grammar structure if notable
**μœ μ‚¬ ν‘œν˜„**: 1-2 similar natural alternatives
Be concise. Max 120 words total."""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": f"Bearer {groq_key}"},
json={
"model": "llama-3.3-70b-versatile",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 300,
"temperature": 0.4
},
timeout=10.0
)
if response.status_code != 200:
return {"reply": f"Groq API 연동 였λ₯˜ λ°œμƒ: {response.text}"}
data = response.json()
return {"reply": data["choices"][0]["message"]["content"]}
# ── 진도 μ €μž₯ ──────────────────────────────────────────────────────────
@router.post("/api/progress")
async def save_progress(req: ProgressRequest):
url, headers = _upstash_url(), _upstash_headers()
if not url:
return {"status": "error", "message": "Upstash 인증 정보가 μ—†μŠ΅λ‹ˆλ‹€."}
payload = req.model_dump() if hasattr(req, "model_dump") else req.dict()
async with httpx.AsyncClient() as client:
r = await client.post(url + "/", headers=headers,
json=["SET", req.key, json.dumps(payload)], timeout=10.0)
return {"status": "success" if r.status_code == 200 else "error"}
# ── μ„Έμ…˜ μ €μž₯ ──────────────────────────────────────────────────────────
@router.post("/api/session")
async def save_session(req: SessionRequest):
url, headers = _upstash_url(), _upstash_headers()
if not url:
return {"status": "error", "message": "Upstash 인증 정보가 μ—†μŠ΅λ‹ˆλ‹€."}
payload = req.model_dump() if hasattr(req, "model_dump") else req.dict()
async with httpx.AsyncClient() as client:
r = await client.post(url + "/", headers=headers,
json=["SET", req.key, json.dumps(payload)], timeout=10.0)
return {"status": "success" if r.status_code == 200 else "error"}
# ── μ„Έμ…˜ 쑰회 (κΈ°μ‘΄ GET β€” ν•˜μœ„ ν˜Έν™˜ μœ μ§€) ──────────────────────────────
@router.get("/api/session/{key}")
async def get_session(key: str):
url, headers = _upstash_url(), {"Authorization": _upstash_headers()["Authorization"]}
if not url:
return {"status": "error", "message": "Upstash 인증 정보 μ—†μŒ"}
async with httpx.AsyncClient() as client:
r = await client.get(f"{url}/get/{key}", headers=headers)
if r.status_code == 200:
d = r.json()
if d.get("result"):
return json.loads(d["result"])
return {"status": "error", "message": "데이터λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}
# ── β˜… μ‹ κ·œ: λ§ˆμ§€λ§‰ μ„Έμ…˜ 쑰회 (이메일 기반, cfg λΆˆν•„μš”) ────────────────
@router.post("/api/session/last")
async def get_last_session(req: EmailRequest):
"""
ν΄λΌμ΄μ–ΈνŠΈκ°€ cfg.upstashToken 없이도 호좜 κ°€λŠ₯.
μƒˆ λΈŒλΌμš°μ € / λͺ¨λ°”μΌμ—μ„œ 둜그인 직후 λ§ˆμ§€λ§‰ μ„Έμ…˜μ„ κ°€μ Έμ˜¬ λ•Œ μ‚¬μš©.
"""
url = _upstash_url()
headers = {"Authorization": _upstash_headers()["Authorization"]}
if not url:
return {"status": "error", "message": "Upstash 인증 정보 μ—†μŒ"}
safe = _safe_email(req.email)
key = f"shadowing:{safe}:_last_session"
async with httpx.AsyncClient() as client:
r = await client.get(f"{url}/get/{key}", headers=headers)
if r.status_code == 200:
d = r.json()
if d.get("result"):
return json.loads(d["result"])
return {"status": "not_found"}
# ── β˜… μ‹ κ·œ: 전체 νŠΈλž™ μ™„μ£Ό 횟수 쑰회 (이메일 기반) ────────────────────
@router.post("/api/counts")
async def get_all_counts(req: EmailRequest):
"""
ν•΄λ‹Ή μœ μ €μ˜ shadowing:{email}:* ν‚€λ₯Ό λͺ¨λ‘ μŠ€μΊ”ν•΄
{ "comprehension_drill": 3, "news_drill": 1, ... } ν˜•νƒœλ‘œ λ°˜ν™˜.
ν΄λΌμ΄μ–ΈνŠΈλŠ” manifest labelκ³Ό λ§€μΉ­ν•΄μ„œ Γ—N ν‘œμ‹œμ— ν™œμš©.
"""
url = _upstash_url()
headers_auth = {"Authorization": _upstash_headers()["Authorization"],
"Content-Type": "application/json"}
if not url:
return {"counts": {}}
safe = _safe_email(req.email)
pattern = f"shadowing:{safe}:*"
counts = {}
async with httpx.AsyncClient() as client:
# SCAN으둜 ν•΄λ‹Ή μœ μ €μ˜ λͺ¨λ“  ν‚€ μˆ˜μ§‘
cursor = "0"
all_keys = []
while True:
r = await client.post(
f"{url}/",
headers=headers_auth,
json=["SCAN", cursor, "MATCH", pattern, "COUNT", "200"],
timeout=10.0
)
result = r.json().get("result", ["0", []])
cursor = result[0]
all_keys.extend(result[1])
if cursor == "0":
break
# λ‚΄λΆ€ 메타 ν‚€ μ œμ™Έ (_last_session, _meta, _consent)
track_keys = [k for k in all_keys
if not any(k.endswith(s) for s in ["_last_session", "_meta", "_consent"])]
if not track_keys:
return {"counts": {}}
# pipeline으둜 ν•œ λ²ˆμ— 쑰회
pipeline = [["GET", k] for k in track_keys]
pr = await client.post(f"{url}/pipeline",
headers=headers_auth,
json=pipeline,
timeout=10.0)
results = pr.json()
for key, res in zip(track_keys, results):
if res.get("result"):
try:
d = json.loads(res["result"])
# key ν˜•μ‹: shadowing:{safe_email}:{safeTitle}:{trackKey}
# 예: shadowing:hoon1018_knou_ac_kr:comprehension_drill:bm_christopher_03_1
parts = key.split(":")
if len(parts) >= 4:
# {safeTitle}:{trackKey} λ³΅ν•©ν‚€λ‘œ λ°˜ν™˜
composite = f"{parts[-2]}:{parts[-1]}"
else:
composite = parts[-1]
counts[composite] = d.get("playCount", 0)
except Exception:
pass
return {"counts": counts}
# ── β˜… μ‹ κ·œ: λ™μ˜ μ €μž₯ ─────────────────────────────────────────────────
class ConsentRequest(BaseModel):
email: str
agreed: bool
@router.post("/api/consent")
async def save_consent(req: ConsentRequest):
url, headers = _upstash_url(), _upstash_headers()
if not url:
return {"status": "error"}
safe = _safe_email(req.email)
key = f"shadowing:{safe}:_consent"
payload = {"email": req.email, "agreed": req.agreed}
async with httpx.AsyncClient() as client:
r = await client.post(url + "/", headers=headers,
json=["SET", key, json.dumps(payload)], timeout=5.0)
return {"status": "success" if r.status_code == 200 else "error"}
@router.post("/api/consent/get")
async def get_consent(req: EmailRequest):
url = _upstash_url()
headers = {"Authorization": _upstash_headers()["Authorization"]}
if not url:
return {"status": "error"}
safe = _safe_email(req.email)
key = f"shadowing:{safe}:_consent"
async with httpx.AsyncClient() as client:
r = await client.get(f"{url}/get/{key}", headers=headers)
if r.status_code == 200:
d = r.json()
if d.get("result"):
return json.loads(d["result"])
return {"status": "not_found"}
# ── β˜… SUB-NOTE: νŠΈλž™λ³„ νŽΈμ§‘ λ…ΈνŠΈ μ €μž₯/쑰회 ──────────────────
class SubNoteRequest(BaseModel):
email: str
trackKey: str
content: dict # TipTap JSON
class SubNoteGetRequest(BaseModel):
email: str
trackKey: str
@router.post("/api/subnote/set")
async def set_subnote(req: SubNoteRequest):
url, headers = _upstash_url(), _upstash_headers()
if not url:
return {"status": "error", "message": "Upstash 인증 정보 μ—†μŒ"}
safe = _safe_email(req.email)
key = f"shadowing:{safe}:subnote:{req.trackKey}"
payload = {
"email": req.email,
"trackKey": req.trackKey,
"content": req.content,
"updatedAt": __import__('datetime').datetime.utcnow().isoformat()
}
async with httpx.AsyncClient() as client:
r = await client.post(url + "/", headers=headers,
json=["SET", key, json.dumps(payload)], timeout=10.0)
return {"status": "success" if r.status_code == 200 else "error"}
@router.post("/api/subnote/get")
async def get_subnote(req: SubNoteGetRequest):
url = _upstash_url()
headers = {"Authorization": _upstash_headers()["Authorization"]}
if not url:
return {"status": "error"}
safe = _safe_email(req.email)
key = f"shadowing:{safe}:subnote:{req.trackKey}"
async with httpx.AsyncClient() as client:
r = await client.get(f"{url}/get/{key}", headers=headers)
if r.status_code == 200:
d = r.json()
if d.get("result"):
return json.loads(d["result"])
return {"status": "not_found", "content": None}