File size: 13,828 Bytes
9c94650
 
a75112b
 
 
 
 
 
 
9945bd1
a75112b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9945bd1
a75112b
 
 
 
9945bd1
a75112b
9945bd1
9c94650
a75112b
 
9945bd1
a75112b
 
 
9945bd1
 
 
 
 
a75112b
 
9945bd1
a75112b
9945bd1
 
a75112b
 
9945bd1
 
a75112b
9945bd1
 
 
 
 
 
 
a75112b
 
20ef58c
f05e4f5
 
 
 
20ef58c
f05e4f5
9945bd1
20ef58c
 
 
 
 
a75112b
 
20ef58c
 
 
 
a75112b
f05e4f5
a75112b
20ef58c
f05e4f5
a75112b
f05e4f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a75112b
f05e4f5
a75112b
9945bd1
f05e4f5
9c94650
 
 
f05e4f5
9c94650
 
 
 
 
 
 
 
9945bd1
a75112b
 
 
9945bd1
a75112b
 
9945bd1
a75112b
 
9945bd1
 
 
 
 
a75112b
9945bd1
 
a75112b
9945bd1
 
 
 
 
a75112b
9945bd1
f05e4f5
 
a75112b
 
 
 
 
9c94650
 
 
f05e4f5
 
9945bd1
9c94650
f05e4f5
9c94650
a75112b
 
 
9945bd1
9c94650
 
 
a75112b
 
9c94650
a75112b
9c94650
9945bd1
9c94650
 
a75112b
9c94650
 
 
 
9945bd1
9c94650
 
 
 
 
 
 
 
 
 
f05e4f5
 
9c94650
 
 
f05e4f5
 
9c94650
 
 
 
 
a75112b
 
 
9c94650
 
 
 
a75112b
9c94650
a75112b
9c94650
 
f05e4f5
a75112b
 
 
9c94650
 
 
 
 
 
 
 
 
a75112b
 
 
 
9945bd1
a75112b
 
 
 
 
 
9945bd1
a75112b
 
9945bd1
 
a75112b
 
 
f05e4f5
9c94650
 
 
a75112b
 
9c94650
 
 
9945bd1
f05e4f5
 
 
9c94650
a75112b
9c94650
9945bd1
9c94650
 
 
 
 
 
 
 
 
 
 
20ef58c
9c94650
 
a75112b
 
 
9945bd1
a75112b
9c94650
 
 
 
a75112b
9c94650
 
 
 
 
 
f05e4f5
9c94650
 
 
 
 
 
 
 
a75112b
 
 
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
import matplotlib
matplotlib.use('Agg')
import gradio as gr
import pandas as pd
import joblib
import matplotlib.pyplot as plt
from wordcloud import WordCloud
from src.feature_extraction import FeatureExtractor
import os
import numpy as np

# Load Model
MODEL_PATH = 'best_svm_model.joblib'
try:
    model = joblib.load(MODEL_PATH)
    print(f"Model loaded from {MODEL_PATH}")
except Exception as e:
    print(f"Error loading model: {e}")
    print("Please run src/train_deployment_model.py first.")
    model = None

feature_extractor = FeatureExtractor()

def predict_summary(title, text):
    if not title or not text:
        return "Please provide both Title and Content.", pd.DataFrame()
    
    # 1. Extract Features
    features_list = feature_extractor.calculate_features(title, text)
    if not features_list:
        return "Could not extract sentences.", pd.DataFrame()
    
    df_features = pd.DataFrame(features_list)
    
    # 2. Prepare X for prediction
    feature_cols = ['f1_lead', 'f2_judul_sim', 'f3_freq_word', 'f4_sim_sent', 'f5_len_norm', 'f6_overlap']
    X_pred = df_features[feature_cols]
    
    # 3. Predict
    if model:
        predictions = model.predict(X_pred)
        # Get probabilities for fallback
        if hasattr(model, "predict_proba"):
            probs = model.predict_proba(X_pred)[:, 1] # Probability of class 1
        else:
            probs = model.decision_function(X_pred)
    else:
        return "Model not loaded.", pd.DataFrame()
    
    df_features['prediction'] = predictions
    df_features['probability'] = probs
    
    # 4. Generate Summary Text
    summary_sentences = df_features[df_features['prediction'] == 1]['teks_kalimat'].tolist()
    
    # Fallback Logic: If no sentences selected
    if not summary_sentences:
        best_idx = np.argmax(probs)
        summary_sentences = [df_features.iloc[best_idx]['teks_kalimat']]
        df_features.at[best_idx, 'prediction'] = 1
        
    summary_text = " ".join(summary_sentences)
        
    return summary_text, df_features[['teks_kalimat', 'prediction', 'probability'] + feature_cols]


def process_batch_file(file_path):
    # Initialize returns for consistent tuple size (8 items: 7 data + 1 status message)
    # output_csv, ui_df, df_results, fig1, fig2, fig3, fig4, status_msg
    empty_returns = (None, None, None, None, None, None, None, "No file uploaded.")
    
    if file_path is None:
        return empty_returns
    
    if hasattr(file_path, 'name'):
        file_path_str = file_path.name
    else:
        file_path_str = str(file_path)

    # Read File
    try:
        if file_path_str.endswith('.csv'):
            df = pd.read_csv(file_path_str)
        elif file_path_str.endswith('.xlsx'):
            df = pd.read_excel(file_path_str)
        else:
            return None, None, None, None, None, None, None, "Invalid file format. Please upload .csv or .xlsx"
    except Exception as e:
        print(f"Error reading file: {e}")
        return None, None, None, None, None, None, None, f"Error reading file: {str(e)}"

    # --- Column Detection & Logic ---
    cols = [c.lower() for c in df.columns]
    
    # Case A: Sentence-level data (raw corpus) -> Needs Aggregation
    # Check for ID columns AND Sentence columns
    is_sentence_level = False
    id_col = next((c for c in df.columns if c.lower() in ['id_berita', 'id', 'article_id']), None)
    sent_col = next((c for c in df.columns if c.lower() in ['teks_kalimat', 'sentence', 'kalimat', 'text']), None)
    
    if id_col and sent_col:
        is_sentence_level = True
    
    df_processed = pd.DataFrame(columns=['title', 'content'])
    
    if is_sentence_level:
        # Check for title column in raw data
        title_col = next((c for c in df.columns if c.lower() in ['id_judul', 'judul', 'title', 'headline']), None)
        
        # Aggregate
        # Group by ID
        grouped = df.groupby(id_col)
        processed_rows = []
        
        for name, group in grouped:
            # Extract Title (first non-null)
            title_text = "Untitled"
            if title_col:
                possible_titles = group[title_col].dropna().unique()
                if len(possible_titles) > 0:
                    title_text = possible_titles[0]
            
            # Combine sentences
            sentences = group[sent_col].dropna().astype(str).tolist()
            content_text = " ".join(sentences)
            
            processed_rows.append({'title': title_text, 'content': content_text, 'original_id': name})
            
        df_processed = pd.DataFrame(processed_rows)
        status_msg = f"Detected sentence-level data. Aggregated {len(df)} sentences into {len(df_processed)} articles."
        
    else:
        # Case B: Article-level data -> Direct Mapping
        title_col = next((c for c in df.columns if c.lower() in ['title', 'judul', 'headline']), None)
        content_col = next((c for c in df.columns if c.lower() in ['content', 'isi', 'text', 'full_text', 'artikel']), None)
        
        if not title_col or not content_col:
            found_cols = ", ".join(df.columns)
            return None, None, None, None, None, None, None, f"Could not find required columns. Found: {found_cols}. Need 'title'/'judul' and 'content'/'isi'/'teks_kalimat'."
            
        df_processed = df.rename(columns={title_col: 'title', content_col: 'content'})
        status_msg = f"Processed {len(df_processed)} articles."

    # --- Summarization Logic ---
    all_results = []
    
    for idx, row in df_processed.iterrows():
        title_raw = row['title']
        content_raw = row['content']
        
        # Validation
        if pd.isna(title_raw) or pd.isna(content_raw):
            continue
        
        title = str(title_raw).strip()
        content = str(content_raw).strip()
        
        if not title or not content or content.lower() == 'nan':
            continue
        
        features_list = feature_extractor.calculate_features(title, content)
        if not features_list:
            continue
            
        df_feat = pd.DataFrame(features_list)
        X_pred = df_feat[['f1_lead', 'f2_judul_sim', 'f3_freq_word', 'f4_sim_sent', 'f5_len_norm', 'f6_overlap']]
        
        if model:
            preds = model.predict(X_pred)
            if hasattr(model, "predict_proba"):
                probs = model.predict_proba(X_pred)[:, 1]
            else:
                probs = model.decision_function(X_pred)
                
            df_feat['prediction'] = preds
            df_feat['probability'] = probs
            
            summary_sents = df_feat[df_feat['prediction'] == 1]['teks_kalimat'].tolist()
            
            if not summary_sents:
                best_idx = np.argmax(probs)
                summary_sents = [df_feat.iloc[best_idx]['teks_kalimat']]
            
            summary_text = " ".join(summary_sents)
            
            result_row = {
                'article_id': row.get('original_id', idx),
                'title': title,
                'original_content': content,
                'generated_summary': summary_text,
                'original_length': len(content.split()),
                'summary_length': len(summary_text.split()),
                'compression_ratio': len(summary_text.split()) / len(content.split()) if len(content.split()) > 0 else 0,
                'num_sentences_original': len(features_list),
                'num_sentences_summary': len(summary_sents)
            }
            all_results.append(result_row)
            
    if not all_results:
        return None, pd.DataFrame(), pd.DataFrame(), None, None, None, None, status_msg + " But no valid rows processed."

    df_results = pd.DataFrame(all_results)
    output_csv = "batch_results.csv"
    df_results.to_csv(output_csv, index=False)
    
    # Analysis Plots
    # 1. Ratio Distribution
    fig1, ax1 = plt.subplots(figsize=(6, 4))
    ax1.hist(df_results['compression_ratio'], bins=10, color='skyblue', edgecolor='black')
    ax1.set_title('Distribution of Compression Ratios')
    ax1.set_xlabel('Ratio')
    ax1.set_ylabel('Frequency')
    plt.tight_layout()
    
    # 2. Length Scatter
    fig2, ax2 = plt.subplots(figsize=(6, 4))
    ax2.scatter(df_results['original_length'], df_results['summary_length'], alpha=0.5)
    ax2.set_title('Original vs Summary Length (Words)')
    ax2.set_xlabel('Original Length')
    ax2.set_ylabel('Summary Length')
    plt.tight_layout()
    
    # 3. Sentence Count Comparison
    fig3, ax3 = plt.subplots(figsize=(6, 4))
    indices = range(len(df_results))
    if len(df_results) > 20:
        plot_df = df_results.head(20)
        indices = range(20)
    else:
        plot_df = df_results
        
    width = 0.35
    # Use string casting for x-axis if article_ids are weird strings
    x_labels = [str(x) for x in plot_df['article_id']]
    ax3.bar(indices, plot_df['num_sentences_original'], width, label='Original')
    ax3.bar([i + width for i in indices], plot_df['num_sentences_summary'], width, label='Summary')
    ax3.set_title('Sentence Count Comparison (First 20)')
    ax3.set_xticks([i + width/2 for i in indices])
    ax3.set_xticklabels(x_labels, rotation=45, ha='right')
    ax3.set_ylabel('Count')
    ax3.legend()
    plt.tight_layout()

    # 4. Word Cloud
    all_summary_text = " ".join(df_results['generated_summary'].tolist())
    if all_summary_text.strip():
        wordcloud = WordCloud(width=800, height=400, background_color='white').generate(all_summary_text)
        fig4, ax4 = plt.subplots(figsize=(10, 5))
        ax4.imshow(wordcloud, interpolation='bilinear')
        ax4.axis('off')
        ax4.set_title('Word Cloud of Summaries')
    else:
        fig4 = None

    ui_df = df_results[['article_id', 'title', 'generated_summary', 'compression_ratio', 'num_sentences_original', 'num_sentences_summary']]
    
    return output_csv, ui_df, df_results, fig1, fig2, fig3, fig4, status_msg

def get_template():
    df = pd.DataFrame([{'title': 'Judul Berita', 'content': 'Isi berita lengkap...'}])
    output_path = 'template.xlsx'
    df.to_excel(output_path, index=False)
    return output_path

def show_details(evt: gr.SelectData, df):
    if df is None or evt.index[0] >= len(df):
        return "", "", ""
    row = df.iloc[evt.index[0]]
    return row['title'], row['original_content'], row['generated_summary']

# Gradio Interface
with gr.Blocks(title="Balinese News Summarizer") as demo:
    gr.Markdown("# πŸ“° Balinese News Summarizer (SVM)")
    
    with gr.Tab("πŸ“ Manual Input"):
        with gr.Row():
            with gr.Column():
                input_title = gr.Textbox(label="News Title", placeholder="Enter title here...")
                input_content = gr.Textbox(label="News Content", placeholder="Enter full news text here...", lines=10)
                btn_predict = gr.Button("Summarize", variant="primary")
            
            with gr.Column():
                output_summary = gr.Textbox(label="Generated Summary", lines=5, interactive=False)
                output_df = gr.Dataframe(label="Sentence Analysis", headers=['teks_kalimat', 'prediction', 'probability', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6'])
        
        btn_predict.click(fn=predict_summary, inputs=[input_title, input_content], outputs=[output_summary, output_df])

    with gr.Tab("πŸ“‚ Batch Analysis"):
        gr.Markdown("Upload a CSV/Excel file. Supports both **Article-level** (columns: title, content) and **Sentence-level** (columns: id_berita, id_judul, teks_kalimat) formats.")
        
        batch_results_state = gr.State()
        
        with gr.Row():
            file_upload = gr.File(label="Upload File", file_types=['.csv', '.xlsx'])
            with gr.Column():
                btn_template = gr.Button("Download Excel Template")
                btn_process = gr.Button("Process Batch", variant="primary")
        
        # Status Message
        status_output = gr.Textbox(label="Status", interactive=False)
        
        # Output Section
        with gr.Row():
            download_result = gr.File(label="Download Results (CSV)")
        
        gr.Markdown("### Batch Results Preview (Select a row to view details below)")
        batch_output_df = gr.Dataframe(label="Results Table", interactive=False)
        
        # Detailed View
        with gr.Group():
            gr.Markdown("### Selected Article Details")
            with gr.Row():
                detail_title = gr.Textbox(label="Title", interactive=False)
            with gr.Row():
                detail_original = gr.Textbox(label="Original Content", lines=5, interactive=False)
                detail_summary = gr.Textbox(label="Generated Summary", lines=5, interactive=False)

        # Plots
        gr.Markdown("### Analysis Visualization")
        with gr.Row():
            plot1 = gr.Plot(label="Compression Ratio Distribution")
            plot2 = gr.Plot(label="Length Comparison")
        
        with gr.Row():
            plot3 = gr.Plot(label="Sentence Count Comparison")
        
        with gr.Row():
            plot4 = gr.Plot(label="Word Cloud")

        # Event Wiring
        btn_template.click(fn=get_template, inputs=None, outputs=file_upload)
        
        btn_process.click(
            fn=process_batch_file, 
            inputs=file_upload, 
            outputs=[download_result, batch_output_df, batch_results_state, plot1, plot2, plot3, plot4, status_output]
        )
        
        # Row Selection Event
        batch_output_df.select(
            fn=show_details,
            inputs=[batch_results_state],
            outputs=[detail_title, detail_original, detail_summary]
        )

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