File size: 11,351 Bytes
7fb5a6e
 
e7523dc
7fb5a6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d813b7b
f05c8ae
d813b7b
 
 
 
 
 
 
 
 
 
7fb5a6e
d813b7b
 
 
 
 
 
7fb5a6e
d813b7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b819049
d813b7b
 
 
 
7fb5a6e
d813b7b
7fb5a6e
 
d813b7b
7fb5a6e
707131e
7fb5a6e
 
707131e
b819049
707131e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7fb5a6e
 
707131e
 
 
 
7fb5a6e
 
e3a6895
b819049
707131e
 
450a53e
707131e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450a53e
e3a6895
450a53e
b819049
7fb5a6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e7523dc
7fb5a6e
 
 
 
 
 
 
 
 
a72c7dd
7fb5a6e
 
a72c7dd
 
7fb5a6e
a72c7dd
 
 
7fb5a6e
a72c7dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e7523dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import gradio as gr
import json
import time  # ← ADD THIS LINE
from pathlib import Path
from typing import List, Dict, Any, Optional
from llama_cpp import Llama
from huggingface_hub import hf_hub_download
import os
import re

# ====================================
# CONFIGURATION
# ====================================
MODEL_REPO = "xubayer/LlamaPIE-GGUF"
SMALL_MODEL_FILENAME = "llamapie-1b-q8_0.gguf"
LARGE_MODEL_FILENAME = "llamapie-8b-q8_0.gguf"

# ====================================
# MODEL LOADING
# ====================================
print("⏳ Downloading LlamaPIE models...")
small_model_path = hf_hub_download(repo_id=MODEL_REPO, filename=SMALL_MODEL_FILENAME)
large_model_path = hf_hub_download(repo_id=MODEL_REPO, filename=LARGE_MODEL_FILENAME)

print("πŸš€ Loading Classifier (1B)...")
classifier = Llama(small_model_path, n_ctx=2048, n_threads=2, verbose=False)
print("πŸš€ Loading Generator (8B)...")
generator = Llama(large_model_path, n_ctx=2048, n_threads=2, verbose=False)

# ====================================
# CORE INFERENCE LOGIC
# ====================================
class LlamaPIEInference:
    def __init__(self, classifier, generator):
        self.classifier = classifier
        self.generator = generator
        
    def parse_dialogue(self, dialogue: str) -> List[Dict]:
        """Parse dialogue into segments ending with |SILENCE > markers."""
        # Split by |SILENCE > markers
        silence_positions = [m.end() for m in re.finditer(r'\|\s*SILENCE\s*\>', dialogue)]
        segments = []
        
        start = 0
        for pos in silence_positions:
            segment = dialogue[start:pos].strip()
            if segment:
                segments.append({
                    "text": segment,
                    "end_pos": pos,
                    "context": dialogue[max(0, pos-200):pos]  # Last 200 chars
                })
            start = pos
        return segments

    def classify_silence_position(self, context: str, confidence_threshold: float = 0.6) -> Dict:
        """
        Stricter Classifier: Force model to choose between specific outcomes if possible,
        or use a better heuristic for open-ended generation.
        """
        # PROMPT ENGINEERING: 
        # We explicitly tell the 1B model what to do.
        # If your model was trained with a specific prompt wrapper, use it here.
        # Assuming raw completion:
        prompt = context + " |SILENCE >"
        
        output = self.classifier(
            prompt,
            max_tokens=5, # Generate a bit more to see intent
            temperature=0.1,
            top_p=0.9,
            # CRITICAL: Stop generating if it starts a new turn
            stop=["User:", "Speaker", "\n", "|"] 
        )
        
        generated = output['choices'][0]['text'].strip()
        
        # DEBUG: Print what it actually generated to help you debug
        print(f"DEBUG Classifier [Pos ?]: '{generated}'")

        # LOGIC FIX:
        # If it generates "(whisper)" or purely structural tokens, that might actually be its way of saying "I have nothing to add" 
        # unless your training data explicitly used "(whisper)" as the positive label.
        
        # New Heuristic:
        # 1. If it generates nothing or just whitespace -> SILENT
        # 2. If it generates brackets like (silence) or (music) -> SILENT
        # 3. If it generates actual words -> WHISPER
        
        is_silence = False
        if not generated:
            is_silence = True
        elif generated.lower() in ["(silence)", "(no)", "no", "."]:
            is_silence = True
        elif len(generated) < 2:
            is_silence = True
            
        # FORCE CONFIDENCE SCORE
        # We can't get true logits easily in the high-level API without 'logprobs=True'
        # So we simulate confidence based on clarity.
        
        if is_silence:
            decision = "SILENT"
            confidence = 0.1
        else:
            decision = "WHISPER"
            confidence = 0.95 # It produced text, so it's confident it wants to speak
            

        return {
            "decision": decision,
            "confidence": confidence,
            "generated_preview": generated,
            "threshold_met": decision == "WHISPER"
        }
         
    
    def generate_whisper(self, context: str, memory: Optional[str] = None) -> str:
        """Generate concise, helpful whispers with better prompting."""
        
        # Structured prompt for consistent output
        if memory:
            prompt = f"""You are a helpful AI assistant that whispers short, useful hints (1-3 words).
    
    Memory: {memory}
    
    Conversation: {context} |SILENCE >
    
    Whisper a short, helpful hint (1-3 words): """
        else:
            prompt = f"""You are a helpful AI assistant that whispers short, useful hints (1-3 words).
    
    Conversation: {context} |SILENCE >
    
    Whisper a short, helpful hint (1-3 words): """
    
        output = self.generator(
            prompt,
            max_tokens=8,  # Enough for 1-3 words
            temperature=0.3,  # Lower for more consistent output
            top_p=0.8,
            stop=["\n", ".", "|", "Conversation:", "User:", "Speaker"]
        )
        
        raw_text = output['choices'][0]['text'].strip()
        
        # Clean and validate the whisper
        whisper = raw_text.split('\n')[0]  # Take only first line
        
        # Remove quotes and unwanted characters
        whisper = re.sub(r'["\',;]', '', whisper)
    
        # Ensure it's 1-3 words
        words = whisper.split()
        if len(words) == 0:
            return "Continue"  # Default fallback
        elif len(words) > 3:
            # Take key words: usually verb + object
            if len(words) >= 3:
                whisper = " ".join(words[:3])
            else:
                whisper = " ".join(words)
        
        # Make sure whisper is action-oriented
        whisper = whisper.strip()
        if not whisper or whisper.lower() in ["the", "and", "or", "but", "then"]:
            whisper = "Go on"  # Safe default
        
        return whisper        
            



    def process_conversation(self, dialogue: str, memory: str = "", 
                           confidence_threshold: float = 0.6) -> Dict[str, Any]:
        """Main pipeline: Process full conversation."""
        start_time = time.time()
        
        segments = self.parse_dialogue(dialogue)
        whispers = []
        
        for i, segment in enumerate(segments):
            classification = self.classify_silence_position(segment["context"], confidence_threshold)
            
            whisper_result = None
            if classification["threshold_met"]:
                whisper_result = self.generate_whisper(segment["context"], memory)
                
                whispers.append({
                    "position": i + 1,
                    "text": whisper_result,
                    "confidence": classification["confidence"],
                    "context": segment["context"][-100:],  # Last 100 chars
                    "preview_token": classification["generated_preview"]
                })
        
        # Reconstruct annotated dialogue
        annotated = dialogue
        for whisper in whispers:
            annotated = annotated.replace("|SILENCE >", f" Agent: {whisper['text']} |SILENCE >", 1)
        
        return {
            "input": {
                "dialogue": dialogue,
                "memory": memory,
                "confidence_threshold": confidence_threshold
            },
            "whispers": whispers,
            "annotated_dialogue": annotated,
            "stats": {
                "total_silence_positions": len(segments),
                "whisper_decisions": len(whispers),
                "processing_time_ms": int((time.time() - start_time) * 1000),
                "whisper_frequency": f"{len(whispers)/max(1, len(segments))*100:.1f}%"
            }
        }

# Instantiate pipeline
pipeline = LlamaPIEInference(classifier, generator)

# ====================================
# GRADIO UI
# ====================================
css = """
.gradio-container { max-width: 900px !important; }
.output-box { background: #f8f9fa; border-radius: 8px; padding: 12px; }
"""

def run_inference(dialogue: str, memory: str, confidence_threshold: float):
    """Gradio inference wrapper."""
    try:
        result = pipeline.process_conversation(dialogue, memory, confidence_threshold)
        
        # Format output for display
        whisper_lines = [
            f"β€’ Pos {w['position']}: '{w['text']}' (conf: {w['confidence']:.2f})"
            for w in result["whispers"]
        ]
        whispers_text = "\n".join(whisper_lines) if whisper_lines else "No whispers triggered"
        
        annotated = result["annotated_dialogue"]
        if len(annotated) > 1000:
            annotated = annotated[:1000] + "..."
        
        output = (
            f"### πŸ“Š **Results** ({result['stats']['processing_time_ms']}ms)\n"
            f"**Whispers Found:** {result['stats']['whisper_frequency']}\n\n"
            f"**Whispers:**\n"
            f"```\n{whispers_text}\n```\n\n"
            f"**Annotated Dialogue Preview:**\n"
            f"```\n{annotated}\n```\n\n"
            f"**Full Stats:**\n"
            f"- Total silence positions: {result['stats']['total_silence_positions']}\n"
            f"- Whispers triggered: {result['stats']['whisper_decisions']}"
        )
        
        return output, json.dumps(result, indent=2)
        
    except Exception as e:
        return f"❌ Error: {str(e)}", ""

# ====================================
# GRADIO INTERFACE
# ====================================
with gr.Blocks(css=css) as demo:
    gr.Markdown("# πŸ€– LlamaPIE Whisper Inference")
    
    with gr.Row():
        with gr.Column():
            dialogue_input = gr.Textbox(
                label="Dialogue (with |SILENCE > markers)",
                placeholder="User: Hello |SILENCE > Agent: Hi there |SILENCE >",
                lines=5
            )
            memory_input = gr.Textbox(
                label="Memory Context (optional)",
                placeholder="Previous conversation summary...",
                lines=2
            )
            confidence_slider = gr.Slider(
                minimum=0.0,
                maximum=1.0,
                value=0.6,
                step=0.1,
                label="Confidence Threshold"
            )
            submit_btn = gr.Button("πŸš€ Generate Whispers", variant="primary")
    
    with gr.Row():
        with gr.Column():
            output_display = gr.Markdown(label="Results")
            json_output = gr.JSON(label="Full JSON Output")
    
    submit_btn.click(
        fn=run_inference,
        inputs=[dialogue_input, memory_input, confidence_slider],
        outputs=[output_display, json_output]
    )
    
    gr.Examples(
        examples=[
            ["User: What's the weather? |SILENCE > Agent: It's sunny today. |SILENCE >", "", 0.6],
            ["Speaker A: I'm thinking about... |SILENCE > Speaker B: Go ahead. |SILENCE >", "Previous: discussing plans", 0.7],
        ],
        inputs=[dialogue_input, memory_input, confidence_slider]
    )

if __name__ == "__main__":
    demo.launch()