Update app.py
Browse files
app.py
CHANGED
|
@@ -9,16 +9,16 @@ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
| 9 |
_last_segments = []
|
| 10 |
|
| 11 |
SPEAKER_COLORS = [
|
| 12 |
-
("#4F46E5", "#EEF2FF"),
|
| 13 |
-
("#059669", "#ECFDF5"),
|
| 14 |
-
("#DC2626", "#FEF2F2"),
|
| 15 |
-
("#D97706", "#FFFBEB"),
|
| 16 |
-
("#7C3AED", "#F5F3FF"),
|
| 17 |
-
("#0891B2", "#ECFEFF"),
|
| 18 |
-
("#DB2777", "#FDF2F8"),
|
| 19 |
-
("#65A30D", "#F7FEE7"),
|
| 20 |
-
("#EA580C", "#FFF7ED"),
|
| 21 |
-
("#0284C7", "#F0F9FF"),
|
| 22 |
]
|
| 23 |
|
| 24 |
def make_conversation_html(segments):
|
|
@@ -27,7 +27,6 @@ def make_conversation_html(segments):
|
|
| 27 |
speaker_list = list(dict.fromkeys(s.speaker for s in segments))
|
| 28 |
color_map = {spk: SPEAKER_COLORS[i % len(SPEAKER_COLORS)] for i, spk in enumerate(speaker_list)}
|
| 29 |
|
| 30 |
-
# Legend
|
| 31 |
legend_items = "".join(
|
| 32 |
f"<span style='display:inline-flex;align-items:center;gap:6px;margin-right:16px'>"
|
| 33 |
f"<span style='width:12px;height:12px;border-radius:50%;background:{color_map[spk][0]}'></span>"
|
|
@@ -36,92 +35,97 @@ def make_conversation_html(segments):
|
|
| 36 |
)
|
| 37 |
legend = f"<div style='padding:12px 16px;border-bottom:1px solid #e5e7eb;display:flex;flex-wrap:wrap;gap:4px'>{legend_items}</div>"
|
| 38 |
|
| 39 |
-
# Conversation bubbles
|
| 40 |
bubbles = ""
|
| 41 |
for seg in segments:
|
| 42 |
fg, bg = color_map[seg.speaker]
|
| 43 |
align = "flex-end" if speaker_list.index(seg.speaker) % 2 == 1 else "flex-start"
|
|
|
|
|
|
|
|
|
|
| 44 |
bubbles += f"""
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
</div>
|
| 57 |
-
</div>
|
| 58 |
-
</div>"""
|
| 59 |
|
| 60 |
return f"""
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
</div>"""
|
| 68 |
|
| 69 |
def make_table_html(segments):
|
| 70 |
if not segments:
|
| 71 |
return ""
|
|
|
|
| 72 |
speaker_list = list(dict.fromkeys(s.speaker for s in segments))
|
| 73 |
color_map = {spk: SPEAKER_COLORS[i % len(SPEAKER_COLORS)] for i, spk in enumerate(speaker_list)}
|
|
|
|
| 74 |
rows = "".join(
|
| 75 |
f"<tr style='border-bottom:1px solid #f3f4f6'>"
|
| 76 |
-
f"<td style='padding:8px 12px'>"
|
| 77 |
-
f" <span style='background:{color_map[s.speaker][1]};color:{color_map[s.speaker][0]};"
|
| 78 |
-
f" padding:3px 10px;border-radius:99px;font-weight:700;font-size:13px'> {s.speaker}</span></td>"
|
| 79 |
f"<td style='padding:8px 12px;color:#6b7280;font-size:13px;white-space:nowrap'>{s.start_fmt}</td>"
|
| 80 |
f"<td style='padding:8px 12px;color:#6b7280;font-size:13px;white-space:nowrap'>{s.end_fmt}</td>"
|
| 81 |
f"<td style='padding:8px 12px;font-size:14px;color:#111827'>{s.text}</td>"
|
| 82 |
f"</tr>"
|
| 83 |
for s in segments
|
| 84 |
)
|
|
|
|
| 85 |
return f"""
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
|
| 100 |
def process_audio(audio_path, num_speakers):
|
| 101 |
global _last_segments
|
|
|
|
| 102 |
if audio_path is None:
|
| 103 |
return "⚠️ Please upload an audio file first.", "", ""
|
|
|
|
| 104 |
try:
|
| 105 |
pipeline = get_pipeline()
|
| 106 |
n = int(num_speakers) if num_speakers and int(num_speakers) > 0 else 0
|
| 107 |
segments = pipeline.process(audio_path, num_speakers=n)
|
| 108 |
except Exception as e:
|
| 109 |
return f"❌ Error: {e}\n{traceback.format_exc()}", "", ""
|
|
|
|
| 110 |
if not segments:
|
| 111 |
return "⚠️ No speech detected.", "", ""
|
|
|
|
| 112 |
_last_segments = segments
|
| 113 |
unique = len(set(s.speaker for s in segments))
|
| 114 |
status = f"✅ Done — {len(segments)} segments · {unique} speaker(s) detected"
|
| 115 |
return status, make_conversation_html(segments), make_table_html(segments)
|
| 116 |
|
| 117 |
def export_json():
|
| 118 |
-
if not _last_segments:
|
|
|
|
| 119 |
out = str(UPLOAD_DIR / "annotation.json")
|
| 120 |
to_json(_last_segments, out)
|
| 121 |
return out
|
| 122 |
|
| 123 |
def export_csv():
|
| 124 |
-
if not _last_segments:
|
|
|
|
| 125 |
out = str(UPLOAD_DIR / "annotation.csv")
|
| 126 |
to_csv(_last_segments, out)
|
| 127 |
return out
|
|
@@ -132,37 +136,39 @@ footer { display: none !important; }
|
|
| 132 |
"""
|
| 133 |
|
| 134 |
with gr.Blocks(title="Speech Annotation Pipeline", css=css) as demo:
|
| 135 |
-
gr.Markdown("""# 🎙️ Speech Annotation Pipeline
|
| 136 |
-
*Upload audio · Detect speakers · Export transcript*""")
|
| 137 |
|
| 138 |
with gr.Row():
|
| 139 |
with gr.Column(scale=1):
|
| 140 |
-
audio_input
|
| 141 |
-
num_speakers = gr.Slider(minimum=0, maximum=10, step=1, value=0,
|
| 142 |
-
|
| 143 |
-
run_btn = gr.Button("▶ Run Annotation", variant="primary", size="lg")
|
| 144 |
status_box = gr.Textbox(label="Status", value="Ready.", interactive=False)
|
| 145 |
gr.Markdown("### 📥 Export")
|
|
|
|
| 146 |
with gr.Row():
|
| 147 |
json_btn = gr.Button("⬇ JSON", size="sm")
|
| 148 |
-
csv_btn
|
|
|
|
| 149 |
json_file = gr.File(label="JSON Download", visible=True)
|
| 150 |
-
csv_file
|
| 151 |
|
| 152 |
with gr.Column(scale=2):
|
| 153 |
gr.Markdown("### 💬 Conversation View")
|
| 154 |
-
conversation_html = gr.HTML(
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
"Transcript will appear here after processing…</div>")
|
| 158 |
gr.Markdown("### 📋 Segment Table")
|
| 159 |
table_html = gr.HTML(value="")
|
| 160 |
|
| 161 |
-
run_btn.click(
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
| 164 |
json_btn.click(fn=export_json, inputs=[], outputs=[json_file])
|
| 165 |
-
csv_btn.click(fn=export_csv,
|
| 166 |
|
| 167 |
if __name__ == "__main__":
|
| 168 |
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|
|
|
|
| 9 |
_last_segments = []
|
| 10 |
|
| 11 |
SPEAKER_COLORS = [
|
| 12 |
+
("#4F46E5", "#EEF2FF"),
|
| 13 |
+
("#059669", "#ECFDF5"),
|
| 14 |
+
("#DC2626", "#FEF2F2"),
|
| 15 |
+
("#D97706", "#FFFBEB"),
|
| 16 |
+
("#7C3AED", "#F5F3FF"),
|
| 17 |
+
("#0891B2", "#ECFEFF"),
|
| 18 |
+
("#DB2777", "#FDF2F8"),
|
| 19 |
+
("#65A30D", "#F7FEE7"),
|
| 20 |
+
("#EA580C", "#FFF7ED"),
|
| 21 |
+
("#0284C7", "#F0F9FF"),
|
| 22 |
]
|
| 23 |
|
| 24 |
def make_conversation_html(segments):
|
|
|
|
| 27 |
speaker_list = list(dict.fromkeys(s.speaker for s in segments))
|
| 28 |
color_map = {spk: SPEAKER_COLORS[i % len(SPEAKER_COLORS)] for i, spk in enumerate(speaker_list)}
|
| 29 |
|
|
|
|
| 30 |
legend_items = "".join(
|
| 31 |
f"<span style='display:inline-flex;align-items:center;gap:6px;margin-right:16px'>"
|
| 32 |
f"<span style='width:12px;height:12px;border-radius:50%;background:{color_map[spk][0]}'></span>"
|
|
|
|
| 35 |
)
|
| 36 |
legend = f"<div style='padding:12px 16px;border-bottom:1px solid #e5e7eb;display:flex;flex-wrap:wrap;gap:4px'>{legend_items}</div>"
|
| 37 |
|
|
|
|
| 38 |
bubbles = ""
|
| 39 |
for seg in segments:
|
| 40 |
fg, bg = color_map[seg.speaker]
|
| 41 |
align = "flex-end" if speaker_list.index(seg.speaker) % 2 == 1 else "flex-start"
|
| 42 |
+
text_align = "text-align:right;" if align == "flex-end" else ""
|
| 43 |
+
radius = "4px 16px 16px 16px" if align == "flex-start" else "16px 4px 16px 16px"
|
| 44 |
+
|
| 45 |
bubbles += f"""
|
| 46 |
+
<div style='display:flex;justify-content:{align};margin:6px 12px'>
|
| 47 |
+
<div style='max-width:75%'>
|
| 48 |
+
<div style='font-size:11px;color:#6b7280;margin-bottom:3px;{text_align}'>
|
| 49 |
+
<span style='font-weight:600;color:{fg}'>Speaker {seg.speaker}</span>
|
| 50 |
+
· {seg.start_fmt} → {seg.end_fmt}
|
| 51 |
+
</div>
|
| 52 |
+
<div style='background:{bg};border:1px solid {fg}30;color:#111827;padding:10px 14px;border-radius:{radius};font-size:14px;line-height:1.5'>
|
| 53 |
+
{seg.text}
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>"""
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
return f"""
|
| 59 |
+
<div style='border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;font-family:system-ui,sans-serif;background:white'>
|
| 60 |
+
{legend}
|
| 61 |
+
<div style='height:480px;overflow-y:auto;padding:8px 0;background:#f9fafb'>
|
| 62 |
+
{bubbles}
|
| 63 |
+
</div>
|
| 64 |
+
</div>"""
|
|
|
|
| 65 |
|
| 66 |
def make_table_html(segments):
|
| 67 |
if not segments:
|
| 68 |
return ""
|
| 69 |
+
|
| 70 |
speaker_list = list(dict.fromkeys(s.speaker for s in segments))
|
| 71 |
color_map = {spk: SPEAKER_COLORS[i % len(SPEAKER_COLORS)] for i, spk in enumerate(speaker_list)}
|
| 72 |
+
|
| 73 |
rows = "".join(
|
| 74 |
f"<tr style='border-bottom:1px solid #f3f4f6'>"
|
| 75 |
+
f"<td style='padding:8px 12px'><span style='background:{color_map[s.speaker][1]};color:{color_map[s.speaker][0]};padding:3px 10px;border-radius:99px;font-weight:700;font-size:13px'>{s.speaker}</span></td>"
|
|
|
|
|
|
|
| 76 |
f"<td style='padding:8px 12px;color:#6b7280;font-size:13px;white-space:nowrap'>{s.start_fmt}</td>"
|
| 77 |
f"<td style='padding:8px 12px;color:#6b7280;font-size:13px;white-space:nowrap'>{s.end_fmt}</td>"
|
| 78 |
f"<td style='padding:8px 12px;font-size:14px;color:#111827'>{s.text}</td>"
|
| 79 |
f"</tr>"
|
| 80 |
for s in segments
|
| 81 |
)
|
| 82 |
+
|
| 83 |
return f"""
|
| 84 |
+
<div style='border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;font-family:system-ui,sans-serif'>
|
| 85 |
+
<table style='width:100%;border-collapse:collapse'>
|
| 86 |
+
<thead>
|
| 87 |
+
<tr style='background:#1e3a5f;color:white'>
|
| 88 |
+
<th style='padding:10px 12px;text-align:left;font-size:13px'>Speaker</th>
|
| 89 |
+
<th style='padding:10px 12px;text-align:left;font-size:13px'>Start</th>
|
| 90 |
+
<th style='padding:10px 12px;text-align:left;font-size:13px'>End</th>
|
| 91 |
+
<th style='padding:10px 12px;text-align:left;font-size:13px'>Transcript</th>
|
| 92 |
+
</tr>
|
| 93 |
+
</thead>
|
| 94 |
+
<tbody>{rows}</tbody>
|
| 95 |
+
</table>
|
| 96 |
+
</div>"""
|
| 97 |
|
| 98 |
def process_audio(audio_path, num_speakers):
|
| 99 |
global _last_segments
|
| 100 |
+
|
| 101 |
if audio_path is None:
|
| 102 |
return "⚠️ Please upload an audio file first.", "", ""
|
| 103 |
+
|
| 104 |
try:
|
| 105 |
pipeline = get_pipeline()
|
| 106 |
n = int(num_speakers) if num_speakers and int(num_speakers) > 0 else 0
|
| 107 |
segments = pipeline.process(audio_path, num_speakers=n)
|
| 108 |
except Exception as e:
|
| 109 |
return f"❌ Error: {e}\n{traceback.format_exc()}", "", ""
|
| 110 |
+
|
| 111 |
if not segments:
|
| 112 |
return "⚠️ No speech detected.", "", ""
|
| 113 |
+
|
| 114 |
_last_segments = segments
|
| 115 |
unique = len(set(s.speaker for s in segments))
|
| 116 |
status = f"✅ Done — {len(segments)} segments · {unique} speaker(s) detected"
|
| 117 |
return status, make_conversation_html(segments), make_table_html(segments)
|
| 118 |
|
| 119 |
def export_json():
|
| 120 |
+
if not _last_segments:
|
| 121 |
+
return None
|
| 122 |
out = str(UPLOAD_DIR / "annotation.json")
|
| 123 |
to_json(_last_segments, out)
|
| 124 |
return out
|
| 125 |
|
| 126 |
def export_csv():
|
| 127 |
+
if not _last_segments:
|
| 128 |
+
return None
|
| 129 |
out = str(UPLOAD_DIR / "annotation.csv")
|
| 130 |
to_csv(_last_segments, out)
|
| 131 |
return out
|
|
|
|
| 136 |
"""
|
| 137 |
|
| 138 |
with gr.Blocks(title="Speech Annotation Pipeline", css=css) as demo:
|
| 139 |
+
gr.Markdown(\"\"\"# 🎙️ Speech Annotation Pipeline
|
| 140 |
+
*Upload audio · Detect speakers · Export transcript*\"\"\")
|
| 141 |
|
| 142 |
with gr.Row():
|
| 143 |
with gr.Column(scale=1):
|
| 144 |
+
audio_input = gr.Audio(label="Upload Audio (.wav / .mp3 / .flac)", type="filepath")
|
| 145 |
+
num_speakers = gr.Slider(minimum=0, maximum=10, step=1, value=0, label="Number of speakers (0 = auto-detect)")
|
| 146 |
+
run_btn = gr.Button("▶ Run Annotation", variant="primary", size="lg")
|
|
|
|
| 147 |
status_box = gr.Textbox(label="Status", value="Ready.", interactive=False)
|
| 148 |
gr.Markdown("### 📥 Export")
|
| 149 |
+
|
| 150 |
with gr.Row():
|
| 151 |
json_btn = gr.Button("⬇ JSON", size="sm")
|
| 152 |
+
csv_btn = gr.Button("⬇ CSV", size="sm")
|
| 153 |
+
|
| 154 |
json_file = gr.File(label="JSON Download", visible=True)
|
| 155 |
+
csv_file = gr.File(label="CSV Download", visible=True)
|
| 156 |
|
| 157 |
with gr.Column(scale=2):
|
| 158 |
gr.Markdown("### 💬 Conversation View")
|
| 159 |
+
conversation_html = gr.HTML(
|
| 160 |
+
value="<div style='height:480px;border:1px solid #e5e7eb;border-radius:12px;display:flex;align-items:center;justify-content:center;color:#9ca3af;font-family:system-ui'>Transcript will appear here after processing…</div>"
|
| 161 |
+
)
|
|
|
|
| 162 |
gr.Markdown("### 📋 Segment Table")
|
| 163 |
table_html = gr.HTML(value="")
|
| 164 |
|
| 165 |
+
run_btn.click(
|
| 166 |
+
fn=process_audio,
|
| 167 |
+
inputs=[audio_input, num_speakers],
|
| 168 |
+
outputs=[status_box, conversation_html, table_html]
|
| 169 |
+
)
|
| 170 |
json_btn.click(fn=export_json, inputs=[], outputs=[json_file])
|
| 171 |
+
csv_btn.click(fn=export_csv, inputs=[], outputs=[csv_file])
|
| 172 |
|
| 173 |
if __name__ == "__main__":
|
| 174 |
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|