Ver código fonte

Speaker Names Improvement

Benjamin Harris 1 mês atrás
pai
commit
0766ca96c4
3 arquivos alterados com 108 adições e 55 exclusões
  1. 68 40
      bridge/admin.py
  2. 20 11
      bridge/bridge.py
  3. 20 4
      bridge/speakers.json

+ 68 - 40
bridge/admin.py

@@ -131,20 +131,30 @@ def _recording_path(sid: str) -> Path | None:
 # ── Speaker API ───────────────────────────────────────────────────────────────
 
 class NameBody(BaseModel):
-    name: str
+    name: str | None = None
+    role: str | None = None
+    location: str | None = None
 
 class AddBody(BaseModel):
     id: str
-    name: str
+    name: str = ""
+    role: str = ""
+    location: str = ""
 
 
 @app.get("/api/speakers")
 def api_list():
     speakers = _load()
-    return {"speakers": [
-        {"id": k, "name": v, "has_recording": _recording_path(k) is not None}
-        for k, v in sorted(speakers.items())
-    ]}
+    result = []
+    for k, v in sorted(speakers.items()):
+        if isinstance(v, dict):
+            entry = {"id": k, "name": v.get("name", ""), "role": v.get("role", ""),
+                     "location": v.get("location", ""), "has_recording": _recording_path(k) is not None}
+        else:
+            entry = {"id": k, "name": str(v), "role": "", "location": "",
+                     "has_recording": _recording_path(k) is not None}
+        result.append(entry)
+    return {"speakers": result}
 
 
 @app.post("/api/speakers")
@@ -155,18 +165,24 @@ def api_add(body: AddBody):
         raise HTTPException(400, "Speaker ID cannot be empty")
     if sid in speakers:
         raise HTTPException(400, f"'{sid}' already exists")
-    speakers[sid] = body.name.strip()
+    speakers[sid] = {"name": body.name.strip(), "role": body.role.strip(), "location": body.location.strip()}
     _save(speakers)
-    return {"ok": True, "id": sid, "name": speakers[sid]}
+    return {"ok": True, "id": sid, "name": body.name}
 
 
 @app.put("/api/speakers/{sid}")
 def api_update(sid: str, body: NameBody):
-    name = body.name.strip()
-    if not name:
-        raise HTTPException(400, "Name cannot be empty")
     speakers = _load()
-    speakers[sid] = name
+    entry = speakers.get(sid, {})
+    if not isinstance(entry, dict):
+        entry = {"name": str(entry), "role": "", "location": ""}
+    if body.name is not None:
+        entry["name"] = body.name.strip()
+    if body.role is not None:
+        entry["role"] = body.role.strip()
+    if body.location is not None:
+        entry["location"] = body.location.strip()
+    speakers[sid] = entry
     _save(speakers)
     return {"ok": True}
 
@@ -683,9 +699,13 @@ HTML = """<!DOCTYPE html>
       <input id="new-initials" placeholder="J.B.B">
     </div>
     <div class="field">
-      <label>Name</label>
+      <label>Name / Role</label>
       <input id="new-name" placeholder="John Brown">
     </div>
+    <div class="field">
+      <label>Locality</label>
+      <input id="new-location" placeholder="Sydney">
+    </div>
     <div class="modal-actions">
       <button class="btn btn-ghost" onclick="closeAddModal()">Cancel</button>
       <button class="btn btn-primary" onclick="addSpeaker()">Add</button>
@@ -738,10 +758,20 @@ function render() {
   filterTable();
 }
 
+function mkEdit(id, field, value) {
+  return `<div class="name-cell">
+      <span class="name-display" onclick="startEdit(this,'${id}','${field}')"
+            title="Click to edit">${value}</span>
+      <input class="name-input" style="display:none"
+             onblur="saveEdit(this,'${id}','${field}')"
+             onkeydown="editKeydown(event,this,'${id}','${field}')">
+    </div>`;
+}
+
 function makeRow(s) {
   const tr = document.createElement('tr');
   tr.dataset.id   = s.id;
-  tr.dataset.name = s.name.toLowerCase();
+  tr.dataset.name = (s.name + ' ' + s.role + ' ' + s.location).toLowerCase();
 
   const recHtml = s.has_recording
     ? `<span class="rec-badge rec-yes">&#9654; Recorded</span>
@@ -751,15 +781,9 @@ function makeRow(s) {
 
   tr.innerHTML = `
     <td class="sid">${esc(s.id)}</td>
-    <td>
-      <div class="name-cell">
-        <span class="name-display" onclick="startEdit(this, '${esc(s.id)}')"
-              title="Click to edit">${esc(s.name)}</span>
-        <input class="name-input" style="display:none"
-               onblur="saveEdit(this,'${esc(s.id)}')"
-               onkeydown="nameKeydown(event,this,'${esc(s.id)}')">
-      </div>
-    </td>
+    <td>${mkEdit(esc(s.id), 'name', esc(s.name))}</td>
+    <td>${mkEdit(esc(s.id), 'role', esc(s.role))}</td>
+    <td>${mkEdit(esc(s.id), 'location', esc(s.location))}</td>
     <td>${recHtml}</td>
     <td>
       <div class="actions">
@@ -792,7 +816,7 @@ function filterTable() {
     q ? `${visible} of ${speakers.length} speakers` : `${speakers.length} speakers`;
 }
 
-function startEdit(span, id) {
+function startEdit(span, id, field) {
   const input = span.nextElementSibling;
   input.value = span.textContent;
   span.style.display = 'none';
@@ -801,7 +825,7 @@ function startEdit(span, id) {
   input.select();
 }
 
-function nameKeydown(e, input, id) {
+function editKeydown(e, input, id, field) {
   if (e.key === 'Enter')  { input.blur(); }
   if (e.key === 'Escape') { cancelEdit(input); }
 }
@@ -812,19 +836,19 @@ function cancelEdit(input) {
   span.style.display = '';
 }
 
-async function saveEdit(input, id) {
-  const name = input.value.trim();
+async function saveEdit(input, id, field) {
+  const value = input.value.trim();
   const span  = input.previousElementSibling;
-  if (!name || name === span.textContent) { cancelEdit(input); return; }
+  if (value === span.textContent) { cancelEdit(input); return; }
+  const body = {};
+  body[field] = value;
   const res = await fetch(`/api/speakers/${encodeURIComponent(id)}`, {
     method: 'PUT',
     headers: {'Content-Type': 'application/json'},
-    body: JSON.stringify({name})
+    body: JSON.stringify(body)
   });
   if (res.ok) {
-    span.textContent = name;
-    const tr = input.closest('tr');
-    tr.dataset.name = name.toLowerCase();
+    span.textContent = value;
     toast('Saved');
   } else {
     toast('Save failed', true);
@@ -837,10 +861,12 @@ function openAddModal() {
     .filter(s => /^SPEAKER_\\d+$/.test(s.id))
     .map(s => parseInt(s.id.split('_')[1]));
   const next = nums.length ? Math.max(...nums) + 1 : 0;
-  document.getElementById('new-id').value   = `SPEAKER_${String(next).padStart(2,'0')}`;
-  document.getElementById('new-name').value = '';
+  document.getElementById('new-id').value       = `SPEAKER_${String(next).padStart(2,'0')}`;
+  document.getElementById('new-initials').value = '';
+  document.getElementById('new-name').value     = '';
+  document.getElementById('new-location').value = '';
   document.getElementById('add-modal').classList.add('open');
-  setTimeout(() => document.getElementById('new-name').focus(), 50);
+  setTimeout(() => document.getElementById('new-initials').focus(), 50);
 }
 
 function closeAddModal(e) {
@@ -849,17 +875,19 @@ function closeAddModal(e) {
 }
 
 async function addSpeaker() {
-  const id   = document.getElementById('new-id').value.trim();
-  const name = document.getElementById('new-name').value.trim();
-  if (!id || !name) { toast('ID and name are required', true); return; }
+  const id       = document.getElementById('new-id').value.trim();
+  const name     = document.getElementById('new-initials').value.trim();
+  const role     = document.getElementById('new-name').value.trim();
+  const location = document.getElementById('new-location').value.trim();
+  if (!id) { toast('Speaker ID is required', true); return; }
   const res = await fetch('/api/speakers', {
     method: 'POST',
     headers: {'Content-Type': 'application/json'},
-    body: JSON.stringify({id, name})
+    body: JSON.stringify({id, name, role, location})
   });
   if (res.ok) {
     closeAddModal();
-    toast(`Added ${name}`);
+    toast(`Added ${name || id}`);
     await load();
   } else {
     const err = await res.json().catch(() => ({detail:'Error'}));

+ 20 - 11
bridge/bridge.py

@@ -48,11 +48,11 @@ AUDIO_DEVICE: int | None = 12
 
 SPEAKERS_FILE = Path(__file__).parent / "speakers.json"
 
-DEFAULT_SPEAKERS: dict[str, str] = {
-    "SPEAKER_00": "A.A.A": "Serving Brother": "Sydney",
-    "SPEAKER_01": "A.A.A": "Contributor": "London",
-    "SPEAKER_02": "A.A.A": "Contributor": "Hobart",
-    "SPEAKER_03": "A.A.A": "Contributor": "Perth",
+DEFAULT_SPEAKERS: dict[str, dict] = {
+    "SPEAKER_00": {"name": "A.A.A", "role": "Serving Brother", "location": "Sydney"},
+    "SPEAKER_01": {"name": "A.A.A", "role": "Contributor",     "location": "London"},
+    "SPEAKER_02": {"name": "A.A.A", "role": "Contributor",     "location": "Hobart"},
+    "SPEAKER_03": {"name": "A.A.A", "role": "Contributor",     "location": "Perth"},
 }
 
 # ── Audio injection queue ─────────────────────────────────────────────────────
@@ -87,7 +87,7 @@ async def inject_clear():
 
 # ── Speaker persistence ───────────────────────────────────────────────────────
 
-def _load_speakers() -> dict[str, str]:
+def _load_speakers() -> dict:
     if SPEAKERS_FILE.exists():
         try:
             data = json.loads(SPEAKERS_FILE.read_text(encoding="utf-8"))
@@ -99,7 +99,7 @@ def _load_speakers() -> dict[str, str]:
     return dict(DEFAULT_SPEAKERS)
 
 
-def _write_speakers(names: dict[str, str]) -> None:
+def _write_speakers(names: dict) -> None:
     try:
         SPEAKERS_FILE.write_text(
             json.dumps(names, indent=2, ensure_ascii=False),
@@ -116,7 +116,7 @@ class BridgeState:
 
     def __init__(self):
         self._lock                         = threading.Lock()
-        self.speaker_names: dict[str, str] = _load_speakers()
+        self.speaker_names: dict         = _load_speakers()
         self._seen: set[str]               = set(self.speaker_names)
         self._current_speaker: str | None  = None
         self._speaker_changed              = False
@@ -126,7 +126,11 @@ class BridgeState:
 
     def set_speaker_name(self, speaker_id: str, name: str) -> None:
         with self._lock:
-            self.speaker_names[speaker_id] = name.strip()
+            entry = self.speaker_names.get(speaker_id, {})
+            if isinstance(entry, dict):
+                self.speaker_names[speaker_id] = {**entry, "name": name.strip()}
+            else:
+                self.speaker_names[speaker_id] = {"name": name.strip()}
             self._seen.add(speaker_id)
             _write_speakers(self.speaker_names)
 
@@ -143,7 +147,12 @@ class BridgeState:
     def _resolve(self, speaker_id: str | None) -> str | None:
         if not speaker_id:
             return None
-        return self.speaker_names.get(speaker_id, speaker_id)
+        entry = self.speaker_names.get(speaker_id)
+        if entry is None:
+            return speaker_id
+        if isinstance(entry, dict):
+            return entry.get("name") or speaker_id
+        return str(entry)
 
     def push_final(self, text: str, speaker_id: str | None, mqtt_client: mqtt.Client) -> None:
         with self._lock:
@@ -379,7 +388,7 @@ def main() -> None:
         pcm_input=True,
         backend_policy="localagreement",
         confidence_validation=True,
-        min_chunk_size=1.5,
+        min_chunk_size=3,
         vac=False,
     )
 

+ 20 - 4
bridge/speakers.json

@@ -1,6 +1,22 @@
 {
-  "SPEAKER_00": "Serving Brother",
-  "SPEAKER_01": "Contributor",
-  "SPEAKER_02": "Contributor",
-  "SPEAKER_03": "Contributor"
+  "SPEAKER_00": {
+    "name": "A.A.A",
+    "role": "Serving Brother",
+    "location": "Sydney"
+  },
+  "SPEAKER_01": {
+    "name": "A.A.A",
+    "role": "Contributor",
+    "location": "London"
+  },
+  "SPEAKER_02": {
+    "name": "A.A.A",
+    "role": "Contributor",
+    "location": "Hobart"
+  },
+  "SPEAKER_03": {
+    "name": "A.A.A",
+    "role": "Contributor",
+    "location": "Perth"
+  }
 }