Spaces:
Sleeping
Sleeping
colab-user commited on
Commit ·
f2126e0
1
Parent(s): d8fc038
fix FE & schema
Browse files- app/schemas/models.py +18 -8
- app/static/js/app.js +37 -21
app/schemas/models.py
CHANGED
|
@@ -15,15 +15,23 @@ class ProcessingStatus(str, Enum):
|
|
| 15 |
|
| 16 |
|
| 17 |
class TranscriptSegment(BaseModel):
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
speaker: str = Field(
|
| 22 |
-
role: Optional[str] = Field(
|
| 23 |
default=None,
|
| 24 |
-
description="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
)
|
| 26 |
-
text: str = Field(..., description="Transcribed text")
|
| 27 |
|
| 28 |
|
| 29 |
@property
|
|
@@ -63,11 +71,13 @@ class TranscriptionResponse(BaseModel):
|
|
| 63 |
duration: float = Field(default=0.0, description="Audio duration in seconds")
|
| 64 |
speaker_count: int = Field(default=0, description="Number of detected speakers")
|
| 65 |
processing_time: float = Field(default=0.0, description="Processing time in seconds")
|
|
|
|
| 66 |
|
| 67 |
roles: Optional[dict[str, str]] = Field(
|
| 68 |
default=None,
|
| 69 |
-
description="
|
| 70 |
)
|
|
|
|
| 71 |
download_txt: Optional[str] = Field(default=None, description="Download URL for TXT file")
|
| 72 |
download_csv: Optional[str] = Field(default=None, description="Download URL for CSV file")
|
| 73 |
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
class TranscriptSegment(BaseModel):
|
| 18 |
+
start: float
|
| 19 |
+
end: float
|
| 20 |
+
|
| 21 |
+
speaker: Optional[str] = Field(
|
|
|
|
| 22 |
default=None,
|
| 23 |
+
description="Internal speaker id (debug only)"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
role: str = Field(
|
| 27 |
+
...,
|
| 28 |
+
description="Conversation role (NV = agent, KH = customer)"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
text: str = Field(
|
| 32 |
+
...,
|
| 33 |
+
description="Transcribed text"
|
| 34 |
)
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
@property
|
|
|
|
| 71 |
duration: float = Field(default=0.0, description="Audio duration in seconds")
|
| 72 |
speaker_count: int = Field(default=0, description="Number of detected speakers")
|
| 73 |
processing_time: float = Field(default=0.0, description="Processing time in seconds")
|
| 74 |
+
speakers: Optional[list[str]] = None
|
| 75 |
|
| 76 |
roles: Optional[dict[str, str]] = Field(
|
| 77 |
default=None,
|
| 78 |
+
description="Internal mapping speaker_id → role (debug / audit only)"
|
| 79 |
)
|
| 80 |
+
|
| 81 |
download_txt: Optional[str] = Field(default=None, description="Download URL for TXT file")
|
| 82 |
download_csv: Optional[str] = Field(default=None, description="Download URL for CSV file")
|
| 83 |
|
app/static/js/app.js
CHANGED
|
@@ -206,53 +206,68 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 206 |
// =====================
|
| 207 |
|
| 208 |
function displayResults(result) {
|
| 209 |
-
//
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
| 218 |
|
| 219 |
-
// Render transcript
|
| 220 |
-
renderTranscript(result.segments);
|
| 221 |
|
| 222 |
-
// Show results
|
| 223 |
showSection('results');
|
| 224 |
}
|
| 225 |
|
| 226 |
function renderTranscript(segments) {
|
| 227 |
elements.transcriptContainer.innerHTML = '';
|
| 228 |
|
| 229 |
-
const
|
| 230 |
let colorIndex = 0;
|
| 231 |
|
| 232 |
segments.forEach((segment) => {
|
|
|
|
| 233 |
|
| 234 |
-
|
| 235 |
-
? segment.role
|
| 236 |
-
: segment.speaker;
|
| 237 |
-
|
| 238 |
-
if (!(displayName in speakerColors)) {
|
| 239 |
colorIndex++;
|
| 240 |
-
|
| 241 |
}
|
| 242 |
|
| 243 |
const segmentEl = document.createElement('div');
|
| 244 |
-
segmentEl.className = `segment ${
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
segmentEl.innerHTML = `
|
| 247 |
<div class="segment-header">
|
| 248 |
<span class="segment-speaker">
|
| 249 |
-
${escapeHtml(
|
| 250 |
</span>
|
| 251 |
<span class="segment-time">
|
| 252 |
-
${formatTime(
|
| 253 |
</span>
|
| 254 |
</div>
|
| 255 |
-
<p class="segment-text">${
|
| 256 |
`;
|
| 257 |
|
| 258 |
elements.transcriptContainer.appendChild(segmentEl);
|
|
@@ -260,6 +275,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 260 |
}
|
| 261 |
|
| 262 |
|
|
|
|
| 263 |
function formatTime(seconds) {
|
| 264 |
const h = Math.floor(seconds / 3600);
|
| 265 |
const m = Math.floor((seconds % 3600) / 60);
|
|
|
|
| 206 |
// =====================
|
| 207 |
|
| 208 |
function displayResults(result) {
|
| 209 |
+
// ===== Metadata =====
|
| 210 |
+
|
| 211 |
+
// Nếu chỉ dùng ROLE thì không nên hiển thị speaker_count
|
| 212 |
+
if (result.roles) {
|
| 213 |
+
const roleCount = Object.keys(result.roles).length;
|
| 214 |
+
elements.speakerCount.textContent = `${roleCount} role${roleCount !== 1 ? 's' : ''}`;
|
| 215 |
+
} else {
|
| 216 |
+
elements.speakerCount.textContent = '';
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
elements.durationInfo.textContent = formatDuration(result.duration || 0);
|
| 220 |
+
elements.processingTime.textContent = `${result.processing_time || 0}s`;
|
| 221 |
|
| 222 |
+
// ===== Download links =====
|
| 223 |
+
if (result.download_txt) {
|
| 224 |
+
elements.downloadTxt.href = result.download_txt;
|
| 225 |
+
elements.downloadTxt.style.display = 'inline-block';
|
| 226 |
+
}
|
| 227 |
|
| 228 |
+
if (result.download_csv) {
|
| 229 |
+
elements.downloadCsv.href = result.download_csv;
|
| 230 |
+
elements.downloadCsv.style.display = 'inline-block';
|
| 231 |
+
}
|
| 232 |
|
| 233 |
+
// ===== Render transcript =====
|
| 234 |
+
renderTranscript(result.segments || []);
|
| 235 |
|
| 236 |
+
// ===== Show results =====
|
| 237 |
showSection('results');
|
| 238 |
}
|
| 239 |
|
| 240 |
function renderTranscript(segments) {
|
| 241 |
elements.transcriptContainer.innerHTML = '';
|
| 242 |
|
| 243 |
+
const roleColors = {};
|
| 244 |
let colorIndex = 0;
|
| 245 |
|
| 246 |
segments.forEach((segment) => {
|
| 247 |
+
const role = segment.role || 'UNKNOWN';
|
| 248 |
|
| 249 |
+
if (!(role in roleColors)) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
colorIndex++;
|
| 251 |
+
roleColors[role] = `speaker-${Math.min(colorIndex, 5)}`;
|
| 252 |
}
|
| 253 |
|
| 254 |
const segmentEl = document.createElement('div');
|
| 255 |
+
segmentEl.className = `segment ${roleColors[role]}`;
|
| 256 |
+
|
| 257 |
+
const start = typeof segment.start === 'number' ? segment.start : 0;
|
| 258 |
+
const end = typeof segment.end === 'number' ? segment.end : 0;
|
| 259 |
+
const text = segment.text ? escapeHtml(segment.text) : '';
|
| 260 |
|
| 261 |
segmentEl.innerHTML = `
|
| 262 |
<div class="segment-header">
|
| 263 |
<span class="segment-speaker">
|
| 264 |
+
${escapeHtml(role)}
|
| 265 |
</span>
|
| 266 |
<span class="segment-time">
|
| 267 |
+
${formatTime(start)} - ${formatTime(end)}
|
| 268 |
</span>
|
| 269 |
</div>
|
| 270 |
+
<p class="segment-text">${text}</p>
|
| 271 |
`;
|
| 272 |
|
| 273 |
elements.transcriptContainer.appendChild(segmentEl);
|
|
|
|
| 275 |
}
|
| 276 |
|
| 277 |
|
| 278 |
+
|
| 279 |
function formatTime(seconds) {
|
| 280 |
const h = Math.floor(seconds / 3600);
|
| 281 |
const m = Math.floor((seconds % 3600) / 60);
|