@@ -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 )
336332async 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