Skip to content

Commit 98bea69

Browse files
committed
1027 1640
1 parent 6b49f8d commit 98bea69

File tree

2 files changed

+106
-54
lines changed

2 files changed

+106
-54
lines changed

logs/hf_summary.log

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
[2025-10-27T04:49:15.827939] ▶ HF Request to Qwen2.5-7B-Instruct
3+
Payload preview: 다음 텍스트를 자세하고 명확하게 한국어로 요약해주세요. 결과는 Markdown 형식으로 작성하고, '## 요약', '## 핵심 요점', '## 슬라이드 요약', '## 상세 설명' 섹션을 반드시 포함하세요.
4+
5+
# 슬라이드1
6+
7+
학생 여러분! 안녕하세요 건국대학교 컴퓨터공학과 이철원 교수입니다 자, 이번 시간에는 사 다시 일주차 파이썬 프로그래밍 리스트의 이해와 활용이라는 주제로 여러분을 만나뵙게 되었습니다 지금까지 우리는 변수와 기본적인 자료형, 각종 연산자들에 대해 배웠죠? 이제 파이썬의 꽃이라고 할 수 있는 '자료 구조' 중 하나...
8+
Response status: 404
9+
Response text preview: Not Found
10+
--------------------------------------------------------------------------------

routers/note.py

Lines changed: 96 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -325,81 +325,123 @@ def toggle_favorite(
325325
base_url = os.getenv("BASE_API_URL") or str(request.base_url).rstrip('/')
326326
return serialize_note(db, note, base_url)
327327

328-
329-
# ─────────────────────────────────────────────
330-
# 요약 (동기, 긴 문서 완전 지원)
331-
# ─────────────────────────────────────────────
332328
# ─────────────────────────────────────────────
333-
# 요약 (HF 비활성 환경 대응 - TextRank 기반)
329+
# 요약 (로컬 Qwen 모델 기반, ChatGPT 스타일 자연요약)
334330
# ─────────────────────────────────────────────
335331
@router.post("/notes/{note_id}/summarize_sync", response_model=NoteResponse)
336332
async def summarize_sync(
337333
note_id: int,
338-
domain: str | None = Query(default=None, description="요약 도메인"),
339-
longdoc: bool = Query(default=True, description="긴 문서 모드"),
340334
db: Session = Depends(get_db),
341-
user = Depends(get_current_user)
335+
user=Depends(get_current_user)
342336
):
343337
"""
344-
✅ HF_DISABLED 환경에서도 작동하는 진짜 요약 버전.
345-
- TextRank 기반 문장 중요도 요약
346-
- TL;DR, 핵심 요점, 슬라이드 구조 유지
347-
- 기존 CRUD, 퀴즈 등 기능 영향 없음
338+
✅ ChatGPT 스타일 요약 + 요약 완료 후 메모리 해제
348339
"""
340+
import torch
349341
import numpy as np
342+
import gc
343+
from transformers import AutoTokenizer, AutoModelForCausalLM
350344
from sklearn.feature_extraction.text import TfidfVectorizer
351345
from sklearn.metrics.pairwise import cosine_similarity
352346

353-
# 1️⃣ 노트 조회
354347
note = db.query(Note).filter(Note.id == note_id, Note.user_id == user.u_id).first()
355348
if not note or not (note.content or "").strip():
356349
raise HTTPException(status_code=404, detail="요약 대상 없음")
357350

358-
text = (note.content or "").strip()
359-
if len(text) < 100:
351+
source = note.content.strip()
352+
if len(source) < 50:
360353
raise HTTPException(status_code=400, detail="본문이 너무 짧습니다.")
361354

362-
# 2️⃣ 문장 분리
363-
sentences = re.split(r"(?<=[.!?。])\s+|\n+", text)
364-
sentences = [s.strip() for s in sentences if len(s.strip()) > 10]
365-
if len(sentences) < 3:
366-
final_summary = _fallback_extractive_summary(text)
367-
else:
355+
full_summary = ""
356+
failed = False
357+
358+
try:
359+
print("[summarize_sync] 🚀 Qwen2.5-7B-Instruct 로드 중...")
360+
model_name = "Qwen/Qwen2.5-7B-Instruct"
361+
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
362+
model = AutoModelForCausalLM.from_pretrained(
363+
model_name,
364+
torch_dtype=torch.float16,
365+
device_map="auto",
366+
trust_remote_code=True
367+
)
368+
369+
messages = [
370+
{
371+
"role": "system",
372+
"content": (
373+
"당신은 전문적인 과학기술 문서 요약가입니다. "
374+
"텍스트를 자연스럽고 명확하게 요약하세요. "
375+
"결과는 Markdown 형식으로 작성하고, 다음 구조를 유지하세요:\n\n"
376+
"## 요약\n\n"
377+
"## 핵심 요점\n\n"
378+
"## 상세 설명\n"
379+
),
380+
},
381+
{
382+
"role": "user",
383+
"content": f"아래 내용을 ChatGPT처럼 깔끔하고 자연스럽게 요약해줘:\n\n{source}",
384+
},
385+
]
386+
387+
inputs = tokenizer.apply_chat_template(
388+
messages,
389+
tokenize=True,
390+
add_generation_prompt=True,
391+
return_tensors="pt",
392+
return_dict=True,
393+
).to(model.device)
394+
395+
print("[summarize_sync] 🧠 요약 생성 중...")
396+
with torch.no_grad():
397+
outputs = model.generate(**inputs, max_new_tokens=1500, temperature=0.4, top_p=0.9)
398+
generated = tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
399+
full_summary = generated.strip()
400+
401+
print("[summarize_sync] ✅ 요약 완료")
402+
403+
except Exception as e:
404+
print(f"[summarize_sync] ❌ 모델 요약 실패: {e}")
405+
failed = True
406+
407+
finally:
408+
# ✅ 메모리 해제
368409
try:
369-
# 3️⃣ TextRank 요약 수행
370-
vectorizer = TfidfVectorizer()
371-
tfidf = vectorizer.fit_transform(sentences)
372-
sim = cosine_similarity(tfidf)
373-
scores = np.sum(sim, axis=1)
374-
top_n = max(3, int(len(sentences) * 0.15))
375-
top_idx = np.argsort(scores)[-top_n:]
376-
top_idx = sorted(top_idx)
377-
key_sents = [sentences[i] for i in top_idx]
378-
379-
# 4️⃣ 섹션 구성
380-
tldr = " ".join(key_sents[:3])
381-
bullets = "\n".join(f"- {s}" for s in key_sents[:8])
382-
slides = []
383-
for i, s in enumerate(key_sents, 1):
384-
slides.append(f"### 슬라이드 {i}\n- {s}")
385-
386-
final_summary = f"""## TL;DR
387-
{tldr}
388-
389-
## 핵심 요점
390-
{bullets}
391-
392-
## 슬라이드 요약
393-
{chr(10).join(slides)}
394-
395-
## 상세 설명
396-
이 요약은 HuggingFace API 없이 TextRank 기반 TF-IDF 알고리즘으로 생성되었습니다.
397-
중복 문장은 제거되었고, 중요한 문장만 남겨 핵심을 압축했습니다.
398-
"""
410+
del model
411+
del tokenizer
412+
gc.collect()
413+
if torch.cuda.is_available():
414+
torch.cuda.empty_cache()
415+
print("[summarize_sync] 🧹 모델 메모리 해제 완료")
416+
except Exception as e:
417+
print(f"[summarize_sync] ⚠️ 메모리 해제 실패: {e}")
418+
419+
# ───────────────
420+
# Fallback (TextRank)
421+
# ───────────────
422+
if failed or not full_summary:
423+
print("[summarize_sync] ⚠️ TextRank 백업 사용")
424+
try:
425+
sents = re.split(r"(?<=[.!?。])\s+|\n+", source)
426+
sents = [s.strip() for s in sents if len(s.strip()) > 10]
427+
if len(sents) < 3:
428+
full_summary = _fallback_extractive_summary(source)
429+
else:
430+
vec = TfidfVectorizer()
431+
tfidf = vec.fit_transform(sents)
432+
sim = cosine_similarity(tfidf)
433+
scores = np.sum(sim, axis=1)
434+
top_n = max(3, int(len(sents) * 0.2))
435+
top_idx = np.argsort(scores)[-top_n:]
436+
key_sents = [sents[i] for i in sorted(top_idx)]
437+
bullets = "\n".join(f"- {s}" for s in key_sents[:5])
438+
full_summary = f"## 요약\n{' '.join(key_sents[:2])}\n\n## 핵심 요점\n{bullets}\n\n## 상세 설명\n이 요약은 TextRank 기반 로컬 요약입니다."
399439
except Exception:
400-
final_summary = _fallback_extractive_summary(text)
440+
full_summary = _fallback_extractive_summary(source)
401441

402-
# 5️⃣ 저장
442+
# ───────────────
443+
# DB 저장
444+
# ───────────────
403445
title = (note.title or "").strip() + " — 요약"
404446
if len(title) > 255:
405447
title = title[:255]
@@ -408,7 +450,7 @@ async def summarize_sync(
408450
user_id=user.u_id,
409451
folder_id=note.folder_id,
410452
title=title,
411-
content=final_summary,
453+
content=full_summary,
412454
)
413455
db.add(new_note)
414456
db.commit()

0 commit comments

Comments
 (0)