Jay1121 commited on
Commit
d32a4eb
·
verified ·
1 Parent(s): e87fc2b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -371
app.py CHANGED
@@ -1,391 +1,100 @@
1
- # -*- coding: utf-8 -*-
2
- # app.py — 어느 MZ 친구의 느린 DM방 (Blossom 8B GGUF, llama.cpp, Gradio)
3
-
4
- import os
5
- import re
6
- import random
7
- import difflib
8
- from datetime import datetime
9
-
10
- try:
11
- from zoneinfo import ZoneInfo
12
- except Exception:
13
- ZoneInfo = None
14
-
15
  import gradio as gr
16
- from transformers import AutoTokenizer
17
  from huggingface_hub import hf_hub_download
18
  from llama_cpp import Llama
 
19
 
20
- # =========================================================
21
- # 기본 모델 / 토크나이저 / GGUF 경로 설정
22
- # =========================================================
23
-
24
- # 베이스 모델 (토크나이저용)
25
- BASE_MODEL_PATH = "MLP-KTLim/llama-3-Korean-Bllossom-8B"
26
-
27
- # 병합된 GGUF 모델이 올라간 Hugging Face Repo
28
- # 🔹 여기만 blossom_v3로 바꿨다
29
- MODEL_DIR_DEFAULT = "Jay1121/blossom_v3" # repo id
30
- MODEL_DIR = os.environ.get("MODEL_DIR", MODEL_DIR_DEFAULT)
31
-
32
- GGUF_REPO_ID = os.environ.get("GGUF_REPO_ID", MODEL_DIR)
33
- # 새로 만든 4bit GGUF 파일 이름
34
- GGUF_FILENAME = os.environ.get("GGUF_FILENAME", "blossom_v2.Q4_K_M.q4_k_m.gguf")
35
-
36
- # =========================================================
37
- # 환경 변수 / 기본값 설정
38
- # =========================================================
39
-
40
- DICT_PATH = os.environ.get("DICT_PATH", "./dictionaries/korean_words.txt")
41
- PROFANITY_PATH = os.environ.get(
42
- "PROFANITY_PATH",
43
- "./dictionaries/profanity_whitelist.txt"
44
- )
45
- OOV_THRESHOLD = int(os.environ.get("OOV_THRESHOLD", "0"))
46
- OOV_STRIP = os.environ.get("OOV_STRIP", "1") == "1"
47
-
48
- STRICT_MODE = os.environ.get("STRICT_MODE", "0") == "1" # 기본 OFF
49
- SAFETY_ON = os.environ.get("SAFETY_ON", "0") == "1" # 기본 OFF
50
- BAN_JAMO = os.environ.get("BAN_JAMO", "1") == "1"
51
-
52
- STYLE_MODE = os.environ.get("STYLE_MODE", "auto") # auto | deadpan | neutral
53
-
54
- WHITELIST_JAMO = set(
55
- [s.strip() for s in os.environ.get("WHITELIST_JAMO", "ㅎ,ㅋ").split(",") if s.strip()]
56
  )
57
- KEEP_REPEATS = os.environ.get("KEEP_REPEATS", "0") == "1"
58
- ANTI_SMALLTALK = os.environ.get("ANTI_SMALLTALK", "0") == "1"
59
- SMALLTALK_TRIES = int(os.environ.get("SMALLTALK_TRIES", "1"))
60
-
61
- META_BANS = ["AI", "인공지능", "챗봇", "도와줄게", "역할"]
62
-
63
- DEFAULT_PROFANITY = {
64
- "씨발", "시발", "ㅅㅂ", "좆", "좆같", "개같", "개새끼", "개새", "개소리",
65
- "지랄", "병신", "븅신", "병쉰", "병1신", "염병", "닥쳐", "꺼져", "닥치",
66
- "ㅄ", "ㅗ", "", "ㅈ같", "개지랄", "싫다", "빡친", "개빡", "개빡침",
67
- "등신", "존나", "미친"
68
- }
69
-
70
- # =========================================================
71
- # GGUF 로더 (llama.cpp)
72
- # =========================================================
73
-
74
- def load_model_for_chat(model_repo: str):
75
- """
76
- GGUF + llama.cpp 로드.
77
- - model_repo: Hugging Face repo id (예: 'Jay1121/blossom_v3')
78
- - GGUF_REPO_ID / GGUF_FILENAME 환경변수로 오버라이드 가능
79
- """
80
- repo_id = os.environ.get("GGUF_REPO_ID", model_repo)
81
- filename = os.environ.get("GGUF_FILENAME", GGUF_FILENAME)
82
-
83
- print(f"📥 GGUF 다운로드: {repo_id}/{filename}")
84
- model_path = hf_hub_download(
85
- repo_id=repo_id,
86
- filename=filename,
87
- )
88
-
89
- n_threads = int(os.environ.get("N_THREADS", str(os.cpu_count() or 4)))
90
- n_ctx = int(os.environ.get("N_CTX", "2048"))
91
-
92
- print(f"🧠 llama.cpp 초기화 (n_threads={n_threads}, n_ctx={n_ctx})")
93
- llm = Llama(
94
- model_path=model_path,
95
- n_ctx=n_ctx,
96
- n_threads=n_threads,
97
- logits_all=False,
98
- seed=0,
99
- )
100
- print("✅ GGUF 모델 로드 완료!")
101
- return llm
102
-
103
- # =========================================================
104
- # 사전 / 욕설
105
- # =========================================================
106
-
107
- def load_dictionary(path=DICT_PATH):
108
- if os.path.exists(path):
109
- with open(path, "r", encoding="utf-8") as f:
110
- words = set(w.strip() for w in f if w.strip())
111
- print(f"📚 사전 로드: {path} (단어 {len(words)}개)")
112
- return words
113
- print(f"📚 사전 없음: {path} (OOV 검사 약화)")
114
- return set()
115
-
116
-
117
- def load_profanity(path=PROFANITY_PATH):
118
- prof = set(DEFAULT_PROFANITY)
119
- if path and os.path.exists(path):
120
- with open(path, "r", encoding="utf-8") as f:
121
- for line in f:
122
- w = line.strip()
123
- if w:
124
- prof.add(w)
125
- print(f"📝 욕설 화이트리스트 추가 로드: {path}")
126
- return prof
127
-
128
- # =========================================================
129
- # 전처리 / 검사
130
- # =========================================================
131
-
132
- RE_LAUGH = re.compile(r"(ㅋ|ㅎ|ㅠ|ㅜ)\1{2,}")
133
- RE_EN = re.compile(r"[A-Za-z]+")
134
- RE_WORDS = re.compile(r"[가-힣]{2,}")
135
-
136
- def _is_jamo(ch: str) -> bool:
137
- code = ord(ch)
138
- return (0x1100 <= code <= 0x11FF) or (0x3130 <= code <= 0x318F)
139
-
140
-
141
- def _strip_jamo(text: str) -> str:
142
- if not BAN_JAMO:
143
- return text
144
- out_chars = []
145
- for ch in text:
146
- if _is_jamo(ch) and (ch not in WHITELIST_JAMO):
147
- continue
148
- out_chars.append(ch)
149
- return "".join(out_chars)
150
-
151
-
152
- def clean_text(txt: str):
153
- # 1) ㅋㅋㅋㅋ/ㅠㅠㅠ 등 줄이기
154
- if not KEEP_REPEATS:
155
- txt = RE_LAUGH.sub(lambda m: m.group(1) * 2, txt)
156
- # 2) 영문 제거
157
- txt = RE_EN.sub("", txt)
158
- # 3) prompt template 섞인 경우 잘라내기
159
- cut = txt.split("### User:")[0]
160
- txt = cut.strip()
161
- # 4) 메타 단어 제거
162
- for banned in META_BANS:
163
- txt = txt.replace(banned, "")
164
- # 5) 자모 제거 (화이트리스트 제외)
165
- txt = _strip_jamo(txt)
166
- return txt.strip()
167
-
168
-
169
- def count_oov(txt: str, dictionary, allowlist):
170
- words = RE_WORDS.findall(txt)
171
- oov = [w for w in words if (w not in dictionary and w not in allowlist)]
172
- return len(oov), oov
173
-
174
-
175
- def strip_oov(txt: str, dictionary, allowlist):
176
- kept, i = [], 0
177
- while i < len(txt):
178
- m = RE_WORDS.search(txt, i)
179
- if not m:
180
- kept.append(txt[i:])
181
- break
182
- kept.append(txt[i:m.start()])
183
- w = m.group(0)
184
- if (w in dictionary) or (w in allowlist):
185
- kept.append(w)
186
- i = m.end()
187
- out = "".join(kept)
188
- out = re.sub(r"\s{2,}", " ", out).strip()
189
- return out
190
-
191
- SMALLTALK_PATTERNS = [
192
- r"오늘\s*날씨",
193
- r"\b날씨\s*(가|는)?\s*(좋|괜찮|별로|따뜻|쌀쌀|시원|선선)",
194
- r"(하늘|기온|미세먼지)\s*(이|가)?\s*(좋|맑|깨끗|나쁨|흐림)",
195
- r"(더워|추워)\b",
196
- r"비(\s*가)?\s*(온|와|왔|올)\b",
197
- ]
198
- SMALLTALK_REGEXES = [re.compile(p) for p in SMALLTALK_PATTERNS]
199
-
200
- def normalize_for_sim(s: str):
201
- s = re.sub(r"\s+", "", s)
202
- s = re.sub(r"[.!?~…]+", "", s)
203
- s = re.sub(r"(.)\1{2,}", r"\1\1", s)
204
- return s
205
-
206
-
207
- def looks_smalltalk(text: str):
208
- t = normalize_for_sim(text)
209
- if "오늘날씨좋았어" in t:
210
- return True
211
- return any(rx.search(text) for rx in SMALLTALK_REGEXES)
212
-
213
-
214
- def too_similar_to_history(text: str, history_texts, thresh=0.86):
215
- t1 = normalize_for_sim(text)
216
- for h in history_texts:
217
- t2 = normalize_for_sim(h)
218
- if difflib.SequenceMatcher(None, t1, t2).ratio() >= thresh:
219
- return True
220
- return False
221
-
222
- # =========================================================
223
- # 데드팬 스타일
224
- # =========================================================
225
-
226
- DEADPAN_TRIGGERS = [
227
- "심심", "귀찮", "짜증", "싫", "하..", "휴", "후", "지루",
228
- "그만", "피곤", "죽였어", "개소리", "뭐래", "에휴", "흥미없",
229
- "아...", "음....", ";;;;", "어쩌라고", "그건 본인 사정이죠", "그건 니사정이지"
230
- ]
231
-
232
- def should_deadpan(user_text: str):
233
- mode = STYLE_MODE
234
- if mode == "deadpan":
235
- return True
236
- if mode == "neutral":
237
- return False
238
- return any(k in user_text for k in DEADPAN_TRIGGERS)
239
-
240
-
241
- def postprocess_deadpan(reply: str):
242
- reply = reply.replace("!", ".")
243
- reply = re.sub(r"[~…]+", "...", reply)
244
- if len(reply) > 120:
245
- cut = re.split(r"([.다]\s)", reply, maxsplit=1)
246
- if cut and len("".join(cut[:2])) > 0:
247
- reply = "".join(cut[:2]).strip()
248
- reply = reply[:120].rstrip() + "..."
249
- if not reply.startswith(("음", "아니", "흠", "글쎄")):
250
- reply = random.choice(["음.. ", "아니.. ", "흠.. ", "글쎄.. "]) + reply
251
- if random.random() < 0.3 and not reply.endswith(("..", "...", ".")):
252
- reply = reply + "..."
253
- return reply.strip()
254
-
255
- # =========================================================
256
- # 디코딩 (llama.cpp 사용)
257
- # =========================================================
258
-
259
- def decode_once(model, prompt: str, *, deadpan: bool = False) -> str:
260
- """llama.cpp로 한 번 디코딩."""
261
- if deadpan:
262
- temperature = 0.25
263
- top_p = 0.85
264
- max_tokens = 48
265
- elif STRICT_MODE:
266
- temperature = 0.35
267
- top_p = 0.88
268
- max_tokens = 56
269
  else:
270
- temperature = 0.6
271
- top_p = 0.9
272
- max_tokens = 64
273
 
274
- out = model(
275
- prompt,
276
- max_tokens=max_tokens,
277
- temperature=temperature,
278
- top_p=top_p,
279
- stop=["</s>", "User:", "Assistant:", "### User:"],
280
- )
281
- gen = out["choices"][0]["text"]
282
- return clean_text(gen)
283
-
284
- # =========================================================
285
- # 시스템 프롬프트
286
- # =========================================================
287
-
288
- SYSTEM_PROMPT = (
289
- "너는 사용자의 가장 친한 친구야. 20~30대 MZ 말투 섞인 편안한 한국어 구어체로 말해. "
290
- f"영문/불필요한 낱자 자모 금지(허용: {','.join(sorted(WHITELIST_JAMO))}). "
291
- "메타 단어('AI','인공지능','챗봇','도와줄게','역할') 금지. "
292
- "가끔 시크하게 한 줄만 대답해도 되고, 너무 설교하지 말고 현실 친구처럼 얘기해.\n\n"
293
- "--- 대화 예시 ---\n"
294
- "User: 넌 누구야?\n"
295
- "Assistant: 사람친구..\n"
296
- "User: 무슨 일 해?\n"
297
- "Assistant: 별 건 안해.. 그냥 먹고 살려고\n"
298
- "User: 심심하다\n"
299
- "Assistant: 심심해? 개부럽누..\n"
300
- "--- 여기까지 예시 ---\n\n"
301
- )
302
-
303
- # =========================================================
304
- # 전역 초기화
305
- # =========================================================
306
-
307
- print("🚀 모델 로드 중 (GGUF + llama.cpp)...")
308
- model = load_model_for_chat(MODEL_DIR)
309
-
310
- print("🔤 토크나이저 로드 중...")
311
- tokenizer = AutoTokenizer.from_pretrained(
312
- BASE_MODEL_PATH,
313
- trust_remote_code=True,
314
- use_fast=True,
315
- )
316
- if tokenizer.pad_token is None:
317
- tokenizer.pad_token = tokenizer.eos_token
318
 
319
- dictionary = load_dictionary()
320
- profanity = load_profanity()
321
- print("✅ 초기화 완료")
322
 
323
- # =========================================================
324
- # Gradio 챗 함수
325
- # =========================================================
326
 
327
- def chat_fn(user_input, history):
328
- # history: 리스트 [(user, bot), ...]
329
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
330
 
331
- # 속도 위해 최근 2턴만 유지
332
- for u, b in history[-2:]:
333
- messages.append({"role": "user", "content": u})
334
- messages.append({"role": "assistant", "content": b})
335
- messages.append({"role": "user", "content": user_input})
336
 
337
- # 원래 쓰던 chat_template 그대로 활용 (토크나이저만 사용)
338
- prompt = tokenizer.apply_chat_template(
339
- messages,
340
- tokenize=False,
341
- add_generation_prompt=True,
 
 
 
 
342
  )
 
 
343
 
344
- deadpan = should_deadpan(user_input)
345
- reply = decode_once(model, prompt, deadpan=deadpan)
346
-
347
- oov_cnt, _ = count_oov(reply, dictionary, profanity)
348
- if OOV_STRIP and oov_cnt > 0:
349
- reply = strip_oov(reply, dictionary, profanity)
350
-
351
- if deadpan:
352
- reply = postprocess_deadpan(reply)
353
-
354
- return reply
355
-
356
- # =========================================================
357
- # Gradio UI
358
- # =========================================================
359
-
360
  CUSTOM_CSS = """
361
- .gradio-container {
362
- font-family: "Noto Sans KR", system-ui, sans-serif;
363
- }
364
-
365
- /* 유저 메시지 텍스트를 진한 검정으로 */
366
- .message.user,
367
- .user .message,
368
- .chat-message.user,
369
- .gr-chatbot .message.user,
370
- .gr-chatbot .user {
371
- color: #111111 !important;
372
- }
373
  """
374
 
375
- demo = gr.ChatInterface(
376
- fn=chat_fn,
377
- title="어느 MZ 친구의 느린 DM방",
378
- description=(
379
- "어떤 MZ의 말투를 따라하는 한국어 친구 챗봇입니다.\n"
380
- "(⚠️ 개 느림주의: 대답 늦어도 서운해하지 말 것)"
381
- ),
382
- examples=[
383
- " 나 오늘 개피곤하다",
384
- "이직할까 말까 고민중이야",
385
- "나 좀 칭찬해줘",
386
- ],
387
- css=CUSTOM_CSS,
388
- )
389
 
390
  if __name__ == "__main__":
391
  demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
2
  from huggingface_hub import hf_hub_download
3
  from llama_cpp import Llama
4
+ import random
5
 
6
+ # ------------------------------------------------------------------
7
+ # 1. 모델 준비
8
+ # ------------------------------------------------------------------
9
+ REPO_ID = "Jay1121/blossom_lab1"
10
+ FILENAME = "kakao_merged.Q4_K_M.gguf"
11
+
12
+ print(f"📥 모델 다운로드 요청: {REPO_ID}")
13
+ # 스페이스가 시작될 모델을 받아옵니다.
14
+ model_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME)
15
+
16
+ print("🧠 Llama 엔진 초기화 (Llama-3 호환)")
17
+ # 허깅페이스 무료 CPU에서는 thread 4개 정도가 적당합니다.
18
+ llm = Llama(
19
+ model_path=model_path,
20
+ n_ctx=2048,
21
+ n_threads=4,
22
+ verbose=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  )
24
+ print(" 로딩 완료!")
25
+
26
+ # ------------------------------------------------------------------
27
+ # 2. 채팅 로직 (밸런스 게임 + 인사 필터)
28
+ # ------------------------------------------------------------------
29
+ def chat_response(user_input, history):
30
+ clean_input = user_input.replace(" ", "")
31
+
32
+ # 감지 키워드
33
+ greeting_words = ["안녕", "ㅎㅇ", "하이", "반가", "좋은아침"]
34
+ is_greeting = any(word in clean_input for word in greeting_words)
35
+ is_balance_game = "밸런스게임" in clean_input or "밸런스질문" in clean_input
36
+
37
+ # 시스템 지시문(Context) 주입
38
+ if is_balance_game:
39
+ topics = ["음식", "연애", "고통", "돈", "초능력", "직장", "친구"]
40
+ topic = random.choice(topics)
41
+ final_instruction = (
42
+ f"(사용자가 밸런스 게임을 하자고 한다. 주제는 '{topic}'이다. "
43
+ "아주 고르기 곤란하고 짜증나는 두 가지 선택지(A vs B)를 제시해라. "
44
+ "말투는 여전히 거칠고 시비조로 해라. 예시: '똥맛 카레 vs 카레맛 똥. 골라봐 이 XX야') "
45
+ "자, 질문해."
46
+ )
47
+ elif is_greeting:
48
+ final_instruction = f"(친한 친구가 인사를 건넨다. 욕하지 말고 무심하게 받아준다) {user_input}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  else:
50
+ final_instruction = user_input
 
 
51
 
52
+ # 프롬프트 포맷
53
+ prompt_template = f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ ### Instruction:
56
+ {final_instruction}
 
57
 
58
+ ### Input:
 
 
59
 
 
 
 
60
 
61
+ ### Response:
62
+ <DDOLBAE>"""
 
 
 
63
 
64
+ # 생성
65
+ output = llm(
66
+ prompt_template,
67
+ max_tokens=256,
68
+ stop=["<|end_of_text|>", "###", "User:", "### Instruction:"],
69
+ echo=False,
70
+ temperature=0.7 if is_balance_game else 0.5,
71
+ top_p=0.9,
72
+ repeat_penalty=1.2
73
  )
74
+
75
+ return output['choices'][0]['text'].strip()
76
 
77
+ # ------------------------------------------------------------------
78
+ # 3. UI 구성
79
+ # ------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  CUSTOM_CSS = """
81
+ .gradio-container { font-family: "Noto Sans KR", system-ui, sans-serif; }
82
+ #chatbot { height: 500px; overflow: auto; }
83
+ .message.user, .user .message { color: #111111 !important; font-weight: bold; }
 
 
 
 
 
 
 
 
 
84
  """
85
 
86
+ with gr.Blocks(theme=gr.themes.Soft(), css=CUSTOM_CSS) as demo:
87
+ gr.Markdown("# 🔥 똘배 채팅방")
88
+ gr.Markdown("가비아 도메인 연결용 서버 (Llama-3 GGUF)")
89
+
90
+ chatbot = gr.ChatInterface(
91
+ fn=chat_response,
92
+ retry_btn=None,
93
+ undo_btn=None,
94
+ clear_btn="기록 삭제",
95
+ examples=["밸런스 게임 문제 내봐", "안녕", "돈 좀 빌려줘"],
96
+ )
 
 
 
97
 
98
  if __name__ == "__main__":
99
  demo.launch()
100
+