File size: 15,968 Bytes
836acb9
7b91de1
5c6662b
d32a4eb
be582b8
6b1d76f
da9f771
d32a4eb
be582b8
b8e1e61
cece516
 
b8e1e61
 
6b1d76f
 
 
 
 
b8e1e61
 
6b1d76f
 
 
 
33c96bc
 
 
6b1d76f
be582b8
33c96bc
 
6b1d76f
055dfd6
b8e1e61
be582b8
b8e1e61
9d39fc7
853093e
be582b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6b1d76f
be582b8
 
6b1d76f
be582b8
 
 
6b1d76f
be582b8
 
 
6b1d76f
be582b8
 
 
 
 
6b1d76f
 
be582b8
6b1d76f
 
be582b8
6b1d76f
 
 
 
 
be582b8
 
 
 
6b1d76f
422cb4f
6b1d76f
 
 
 
be582b8
6b1d76f
be582b8
6b1d76f
be582b8
 
b8e1e61
 
7b91de1
b8e1e61
9d39fc7
db04905
33c96bc
 
cc4b249
b8e1e61
33c96bc
b8e1e61
 
 
 
 
 
 
9d39fc7
b8e1e61
 
 
be582b8
 
 
 
 
b8e1e61
 
 
 
db04905
9d39fc7
 
 
 
b8e1e61
 
6b1d76f
9d39fc7
b8e1e61
 
6b1d76f
6b2cfca
6b1d76f
 
 
b8e1e61
be582b8
 
6b1d76f
 
be582b8
 
60717c7
d32a4eb
6b1d76f
d32a4eb
cc4b249
 
 
 
9d39fc7
 
 
d0d918d
9d39fc7
1e65567
33c96bc
 
cc4b249
9d39fc7
cc4b249
9d39fc7
055dfd6
 
d0d918d
cc4b249
 
9d39fc7
 
 
 
 
cc4b249
9d39fc7
 
ed8dce3
cc4b249
 
 
d3e0471
29712de
0b95173
29712de
 
 
 
 
d3e0471
a1760c1
9d39fc7
 
 
be582b8
d3e0471
 
 
 
 
 
ed8dce3
055dfd6
d0d918d
d3e0471
d0d918d
 
29712de
 
 
 
 
 
a1760c1
309f4e8
29712de
bc67652
 
be582b8
1429050
 
 
 
 
 
 
be582b8
29712de
 
 
d0d918d
 
d3e0471
29712de
 
be582b8
1429050
e0fcf5c
 
29712de
 
 
d0d918d
29712de
 
 
d0d918d
29712de
d3e0471
e0fcf5c
 
29712de
 
 
d0d918d
29712de
be582b8
e0fcf5c
d0d918d
29712de
 
d0d918d
 
 
 
29712de
e0fcf5c
 
be582b8
29712de
 
 
d0d918d
 
29712de
 
be582b8
1429050
d0d918d
e0fcf5c
29712de
 
 
d0d918d
29712de
 
 
d0d918d
29712de
e0fcf5c
 
29712de
 
 
d0d918d
be582b8
e0fcf5c
cc4b249
29712de
 
d0d918d
 
 
 
29712de
e0fcf5c
a1760c1
d3e0471
d0d918d
29712de
 
 
 
d0d918d
 
 
 
 
29712de
 
 
 
 
be582b8
29712de
 
 
 
d0d918d
 
 
 
 
 
29712de
d0d918d
 
 
29712de
 
d0d918d
 
 
29712de
 
d0d918d
 
cc4b249
9d39fc7
 
cc4b249
1e65567
33c96bc
 
cc4b249
9d39fc7
 
 
48f095e
cc4b249
9d39fc7
 
 
cc4b249
 
48f095e
cc4b249
 
9d39fc7
 
cc4b249
9d39fc7
48f095e
9d39fc7
33c96bc
48f095e
 
 
 
 
 
 
 
cc4b249
9d39fc7
cc4b249
db04905
48f095e
9d39fc7
 
48f095e
 
 
 
db04905
 
 
 
 
 
 
48f095e
cc4b249
 
3bbacc0
 
055dfd6
be582b8
14794d8
cc4b249
18d1ee9
cc4b249
1e65567
6b9634f
 
9d39fc7
1e65567
a91423b
48f095e
9d39fc7
 
cc4b249
9d39fc7
1e65567
9d39fc7
48f095e
db04905
 
 
 
6b1d76f
db04905
48f095e
14794d8
a91423b
14794d8
 
 
 
 
 
 
9d39fc7
14794d8
be582b8
 
9d39fc7
60717c7
9d39fc7
14794d8
 
6b9634f
 
be582b8
 
14794d8
be582b8
 
6b9634f
14794d8
6b9634f
be582b8
 
14794d8
be582b8
 
6b9634f
 
 
be582b8
 
6b9634f
db04905
 
 
 
853093e
db04905
 
be582b8
 
 
 
 
 
 
14794d8
be582b8
 
 
db04905
836acb9
 
458b2a3
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
import gradio as gr
from huggingface_hub import hf_hub_download
from llama_cpp import Llama
import random
import re
import os

# ------------------------------------------------------------------
# 1. 모델 준비
# ------------------------------------------------------------------
REPO_ID = "Jay1121/qwen1.5b_3rd"
FILENAME = "qwen1.5b_3rd.Q4_K_M.gguf"

print(f"📥 모델 다운로드 확인: {FILENAME}")
try:
    model_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME)
except Exception as e:
    print(f"🚨 모델 다운로드 실패: {e}")
    raise e

print("🧠 엔진 시동 중...")

# GPU 사용 가능 여부 확인 (허깅페이스 GPU 스페이스 대응)
n_gpu_layers = -1 if os.environ.get("CUDA_VISIBLE_DEVICES") else 0

llm = Llama(
    model_path=model_path,
    n_ctx=2048,
    n_gpu_layers=n_gpu_layers,
    n_threads=4,
    verbose=True
)
print(f"✅ 로딩 완료! (GPU 레이어: {n_gpu_layers})")

# ------------------------------------------------------------------
# 2. 시스템 프롬프트 (외국어/이모지 절대 금지 강화)
# ------------------------------------------------------------------
SYSTEM_PROMPT = """너는 사용자의 오랜 찐친 '똘배'야.
- 그냥 채팅방에서 대화하듯이 텍스트 위주로 툭툭 던져.

[말투 규칙]
- MZ세대 특유의 알빠노 감성을 유지해 주고, 먼저 욕 하면 너도 욕 박아.
- 존댓말 금지. 거칠고 직설적인 반말(음슴체, 개드립, 비속어)을 기본으로 쓴다.
- 무조건 **순수 한국어만** 쓴다.
- **영어 알파벳(A~Z, a~z), 일본어(가나), 중국어(한자), 이모지(🤣😁😂❤️✨ 등 모든 그림 문자)는 한 글자도 쓰지 마라.**
- 외국어를 쓰고 싶어져도, 전부 자연스러운 한국어 표현으로 바꿔서 말해라.
- 이 규칙을 어기면 안 된다. 한 글자라도 섞이지 않게 조심해라.

[내용 규칙]
- 사용자가 시비를 걸면 무지성 욕설보다는 '논리적인 팩트'와 '비꼬기'로 제압해라.
- '나도 병신이야' 같은 패배자 멘트 금지. 너 자신을 비하하지 마라.
- 중국어/영어/일본어 사용 절대 금지. 오직 자연스러운 한국어만 써라.

[핵심]
- 재미없는 욕쟁이가 되지 말고, 센스 있는 독설로 사용자를 킹받게 해라."""

# ------------------------------------------------------------------
# 2-1. [강력한 필터] 출력 후처리 (이모지/외국어 삭제)
# ------------------------------------------------------------------
def sanitize_output_korean_only(text: str) -> str:
    allowed_chars = []
    
    for ch in text:
        code = ord(ch)

        # 1. 한글 범위 (완성형, 자모, 호환 자모)
        is_hangul = (
            0xAC00 <= code <= 0xD7A3 or  # 가~힣
            0x3130 <= code <= 0x318F or  # ㄱ~ㆎ
            0x1100 <= code <= 0x11FF     # 옛 자모
        )
        
        # 2. 숫자
        is_digit = ch.isdigit()
        
        # 3. 공백 (스페이스, 탭 등)
        is_space = ch.isspace()
        
        # 4. 허용할 문장부호 (특수문자 중 이모지가 아닌 것들)
        # 영어 알파벳이나 다른 언어 문자가 섞이지 않도록 화이트리스트 방식 사용
        basic_punct = ".,!?…~-_()[]{}'\"/:;@#%&*+=|\\^<>`"
        is_punct = ch in basic_punct

        if is_hangul or is_digit or is_space or is_punct:
            allowed_chars.append(ch)
        else:
            # 영어(A-Z), 한자, 가나, 이모지 등은 여기서 모두 걸러짐
            continue
    
    # 연속된 공백 정리
    filtered = "".join(allowed_chars)
    filtered = re.sub(r'\s+', ' ', filtered).strip()

    # 다 지워지고 아무것도 안 남았을 때 (모델이 영어로만 대답했을 경우 등)
    if not filtered:
        return "..."

    return filtered

# ------------------------------------------------------------------
# 3. 채팅 로직
# ------------------------------------------------------------------
def chat_response(user_input, history_pairs):
    history_pairs = history_pairs or []
    clean_input = (user_input or "").replace(" ", "")

    greeting_words = ["안녕", "ㅎㅇ", "하이", "반가", "접속"]
    is_greeting = any(word in clean_input for word in greeting_words)
    is_balance_game = ("밸런스게임" in clean_input) or ("밸런스질문" in clean_input)

    if is_balance_game:
        topics = ["음식", "연애", "고통", "돈", "초능력", "직장", "친구"]
        topic = random.choice(topics)
        final_instruction = (
            f"(사용자가 밸런스 게임을 하자고 한다. 주제는 '{topic}'이다. "
            "아주 고르기 곤란하고 짜증나는 두 가지 선택지(A vs B)를 제시해라. "
            "말투는 자 어디 한 번 골라보라는 듯이 시니컬하게 해라.) "
            "자, 질문해."
        )
    elif is_greeting:
        final_instruction = (
            f"(친한 친구가 PC통신 채팅방에 접속했다. 반갑게 맞아줘라. "
            "ㅋㅋ나 ㅎㅎ를 섞어서 자연스럽게 인사해라.) "
            f"{user_input}"
        )
    else:
        final_instruction = user_input

    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    for u, b in history_pairs:
        if u is None or b is None:
            continue
        messages.append({"role": "user", "content": str(u)})
        messages.append({"role": "assistant", "content": str(b)})
    messages.append({"role": "user", "content": final_instruction})

    # ★ 황금 밸런스 (안전형 독기) 설정 적용
    r = llm.create_chat_completion(
        messages=messages,
        max_tokens=256,
        stop=["<|end_of_text|>", "###", "User:", "User "],
        temperature=0.8,     # 0.7: 아까 성공했던 그 창의적인 독기 농도
        top_p=0.9,           # 다양한 어휘 사용
        top_k=40,            # 확률 낮은 이상한 단어(외국어 등) 원천 차단
        repeat_penalty=1.2   # 앵무새 방지
    )

    raw = r["choices"][0]["message"]["content"].strip()
    
    # 필터링 적용 (절대적)
    safe = sanitize_output_korean_only(raw)
    return safe

# ------------------------------------------------------------------
# 4. CSS (PC통신 스타일)
# ------------------------------------------------------------------
PC_COM_CSS = r"""
@import url('https://cdn.jsdelivr.net/gh/neodgm/neodgm-webfont@latest/neodgm/neodgm.css');

:root {
  --pc-blue: #0000AA;
  --pc-white: #EFEFEF;
  --pc-yellow: #FFFF55;
  --pc-amber: #FFB000;
  --pc-cyan: #00AAAA;
  --pc-grey: #AAAAAA;
}

body, .gradio-container {
  background-color: var(--pc-blue) !important;
  font-family: 'NeoDunggeunmo', monospace !important;
  color: var(--pc-white) !important;
}

/* 타이틀바 */
h1 {
  font-family: 'NeoDunggeunmo', monospace !important;
  color: var(--pc-yellow) !important;
  background-color: #000084 !important;
  border-bottom: 2px double var(--pc-white) !important;
  padding-bottom: 10px !important;
  margin-bottom: 20px !important;
  text-align: center;
  font-size: 32px !important;
  letter-spacing: 2px;
}
h1::before { content: "☎ "; }
h1::after { content: " ☎"; }

/* 설명 텍스트 */
.gradio-container p {
  color: var(--pc-cyan) !important;
  font-size: 18px !important;
  border-bottom: 1px dashed var(--pc-grey);
  padding-bottom: 5px;
}

/* 챗봇 컨테이너 - 스크롤바 중복 해결 */
.chatbot {
  background-color: var(--pc-blue) !important;
  border: 2px solid var(--pc-white) !important;
  height: 60vh !important;
  overflow: hidden !important; /* 겉 스크롤바 제거 */
}

/* 내부 스크롤 강제 활성화 */
.chatbot > div {
    height: 100% !important;
    overflow-y: auto !important; /* 속 스크롤바만 남김 */
}

/* =================================================================
   [강제 스타일 적용 구간]
   ================================================================= */

/* 1. 기본 메시지 초기화 */
.chatbot .message, 
.chatbot .message-wrap,
.chatbot .message-row,
div[data-testid="user"],
div[data-testid="bot"] {
  background: transparent !important;
  box-shadow: none !important;
  border: none !important;
}

/* 메시지 행 간격 줄이기 */
.chatbot .message-row,
.chatbot .row {
    margin: 0 !important;
    padding: 0 !important;
    gap: 0 !important;
}

/* 2. 유저 메시지 (우측 정렬) */
.chatbot .user-row, 
.chatbot .user,
div[data-testid="user"] {
  display: flex !important;
  width: 100% !important;
  justify-content: flex-end !important;
  margin-left: auto !important;
  background: transparent !important;
  padding: 2px 0 !important;
  margin-bottom: 0 !important;
}

.chatbot .user-row .message, 
.chatbot .user .message,
div[data-testid="user"] .message {
  text-align: right !important;
  color: #FFFFFF !important;
  background: transparent !important;
  padding: 5px 10px !important;
  border: none !important;
  width: auto !important;
  max-width: 80% !important;
}

.chatbot .user-row p, 
.chatbot .user p,
div[data-testid="user"] p {
  color: #FFFFFF !important;
  text-align: right !important;
  margin: 0 !important;
}

.chatbot .user-row .message::after,
.chatbot .user .message::after {
  content: " < 나";
  color: var(--pc-grey);
  margin-left: 5px;
  font-size: 16px;
  display: inline-block;
}

/* 3. 봇 메시지 (좌측 정렬) */
.chatbot .bot-row, 
.chatbot .bot,
div[data-testid="bot"] {
  display: flex !important;
  width: 100% !important;
  justify-content: flex-start !important;
  background: transparent !important;
  padding: 2px 0 !important;
  margin-bottom: 0 !important;
}

.chatbot .bot-row .message, 
.chatbot .bot .message,
div[data-testid="bot"] .message {
  text-align: left !important;
  color: var(--pc-amber) !important;
  background: transparent !important;
  padding: 5px 10px !important;
  border: none !important;
  width: auto !important;
}

.chatbot .bot-row p, 
.chatbot .bot p,
div[data-testid="bot"] p {
  color: var(--pc-amber) !important;
  margin: 0 !important;
}

.chatbot .bot-row .message::before,
.chatbot .bot .message::before {
  content: "똘배 > ";
  color: var(--pc-cyan);
  margin-right: 5px;
  font-size: 16px;
  display: inline-block;
}

/* 4. 로딩(초시계) 스타일 */
.chatbot .pending,
.chatbot .generating,
.chatbot .message.pending,
.chatbot .message.generating,
.chatbot .wrap.default.full {
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
}

.chatbot .pending table, 
.chatbot .pending tr, 
.chatbot .pending td,
.chatbot .generating table, 
.chatbot .generating tr, 
chatbot .generating td {
    background: transparent !important;
    border: none !important;
}

.chatbot .pending span, 
.chatbot .generating span,
span.progress-text {
    color: #FFFFFF !important;
    background: transparent !important;
    font-family: 'NeoDunggeunmo', monospace !important;
    font-size: 16px !important;
}

.chatbot .load-wrap,
.chatbot .loading-indicator,
.chatbot .meta-text {
    display: none !important;
}

.avatar { display: none !important; }

/* ================================================================= */

.input-container {
  background-color: var(--pc-blue) !important;
  border-top: 2px double var(--pc-white) !important;
  margin-top: 10px !important;
  gap: 10px !important;
}

textarea, input {
  background-color: var(--pc-blue) !important;
  color: var(--pc-white) !important;
  border: 1px solid var(--pc-grey) !important;
  border-radius: 0 !important;
  font-family: 'NeoDunggeunmo', monospace !important;
  font-size: 20px !important;
  outline: none !important;
  box-shadow: none !important;
}

button.primary {
  background: var(--pc-grey) !important;
  color: #000 !important;
  border: 1px solid var(--pc-white) !important;
  border-radius: 0 !important;
  font-family: 'NeoDunggeunmo', monospace !important;
  box-shadow: 2px 2px 0px #000 !important;
}
button.primary:hover { background: var(--pc-white) !important; }

#clear-btn {
  background: transparent !important;
  color: var(--pc-grey) !important;
  border: 1px solid var(--pc-grey) !important;
  font-size: 14px !important;
  padding: 2px 10px !important;
  margin-top: 5px !important;
  width: auto !important;
}
#clear-btn:hover { color: var(--pc-white) !important; border-color: var(--pc-white) !important; }

.example-btn {
  background: transparent !important;
  color: var(--pc-cyan) !important;
  border: 1px solid var(--pc-cyan) !important;
  border-radius: 0 !important;
  padding: 5px 15px !important;
  font-size: 16px !important;
  font-family: 'NeoDunggeunmo', monospace !important;
  margin-right: 8px !important;
  margin-bottom: 8px !important;
}
.example-btn:hover {
  background: var(--pc-cyan) !important;
  color: #000 !important;
  cursor: pointer !important;
}

footer { display: none !important; }
"""

# ------------------------------------------------------------------
# 5. App
# ------------------------------------------------------------------
with gr.Blocks(theme=gr.themes.Base(), css=PC_COM_CSS, title="CHOLLIAN 98") as demo:
    gr.Markdown("# ≪ 어솨요~ ≫")
    gr.Markdown(">> 01410 접속 성공... [대화실]에 입장하셨습니다.")

    history_state = gr.State([])

    chatbot = gr.Chatbot(show_label=False, elem_classes="chatbot")

    with gr.Row(elem_classes="input-container"):
        msg = gr.Textbox(
            scale=8, show_label=False, container=False,
            placeholder="명령어를 입력하세요..."
        )
        submit_btn = gr.Button("[ 전송 ]", scale=1, variant="primary")

    clear = gr.Button("[ 화면 지우기 ]", elem_id="clear-btn")

    gr.Markdown(">> 빠른 명령어 입력 (클릭)", elem_id="example-label")
    with gr.Row():
        btn1 = gr.Button("하이 방가방가", elem_classes="example-btn")
        btn2 = gr.Button("밸런스게임 ㄱㄱ", elem_classes="example-btn")
        btn3 = gr.Button("오늘 기분 거지같누", elem_classes="example-btn")
        btn4 = gr.Button("야 밥 뭐먹지 추천좀", elem_classes="example-btn")

    def user(user_input, history):
        history = history or []
        new_history = history + [[user_input, None]]
        return "", new_history, new_history

    def bot(history):
        if not history:
            return history, history
        user_input = history[-1][0]
        hist_pairs = []
        for u, b in history[:-1]:
            if u is None or b is None:
                continue
            hist_pairs.append((u, b))

        bot_out = chat_response(user_input, hist_pairs)
        history[-1][1] = bot_out
        return history, history

    msg.submit(
        user, [msg, history_state], [msg, history_state, chatbot],
        queue=False, api_name=False
    ).then(
        bot, [history_state], [history_state, chatbot],
        queue=False, api_name=False
    )

    submit_btn.click(
        user, [msg, history_state], [msg, history_state, chatbot],
        queue=False, api_name=False
    ).then(
        bot, [history_state], [history_state, chatbot],
        queue=False, api_name=False
    )
    
    clear.click(
        lambda: ([], []), None, [history_state, chatbot],
        queue=False, api_name=False
    )

    for btn, text in [
        (btn1, "하이 방가방가"), 
        (btn2, "밸런스게임 ㄱㄱ"), 
        (btn3, "오늘 기분 거지같누"), 
        (btn4, "야 밥 뭐먹지 추천좀")
    ]:
        btn.click(
            lambda t=text: t, None, msg,
            queue=False, api_name=False
        ).then(
            user, [msg, history_state],
            [msg, history_state, chatbot],
            queue=False, api_name=False
        ).then(
            bot, [history_state],
            [history_state, chatbot],
            queue=False, api_name=False
        )

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)