Compare commits

...

6 Commits

Author SHA1 Message Date
Martin Guitteny 5bd8064158 🔨(agents) change json metadata dump to bucket_s3
Change the saving of the json metadata to the minio bucket_s3
to prepare the changes to map the name in the diarization
2025-09-11 11:42:13 +02:00
Martin Guitteny 22a95af04d 🚧(agents) add metadata extractor agent
Add a visible agent that joins the room and
writes in a json all the active speaker informations.
2025-09-09 17:25:27 +02:00
Martin Guitteny bf7e17921d 📝(docs) fix path developping locally
Fix the path of the app in the doc to improve developer experience
2025-09-09 15:21:52 +02:00
lebaudantoine 3393858552 ⬆️(livekit) upgrade local LiveKit server to version 1.9.0
Update development environment LiveKit server from previous version
to 1.9.0 for latest features and bug fixes.

Ensures development environment stays current
with LiveKit production version.
2025-09-09 15:20:51 +02:00
lebaudantoine e7c12b4253 🐛(frontend) fix CSS rules that hide Crisp support button
Correct CSS specificity issues that were overriding and hiding the Crisp
chatbot button, ensuring support functionality remains accessible to
users.
2025-09-09 10:38:02 +02:00
lebaudantoine 45a69aaaf0 🚨(backend) fix Helmfile compatibility issue
Update Helmfile to resolve compatibility errors that occur
when running the tilt stack with latest helmfile version.
2025-09-09 10:04:58 +02:00
6 changed files with 286 additions and 69 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM livekit/livekit-server:v1.8.0
FROM livekit/livekit-server:v1.9.0
# We inject the nip.io certificate manually because the livekit chart doesn't support volume mounting
COPY rootCA.pem /etc/ssl/certs/
+1 -1
View File
@@ -142,4 +142,4 @@ Once the Kubernetes cluster is ready, start the application stack locally:
$ make start-tilt-keycloak
```
Monitor Tilts progress at [http://localhost:10350/](http://localhost:10350/). After Tilt actions finish, you can access the app at [https://visio.127.0.0.1.nip.io/](https://visio.127.0.0.1.nip.io/).
Monitor Tilts progress at [http://localhost:10350/](http://localhost:10350/). After Tilt actions finish, you can access the app at [https://meet.127.0.0.1.nip.io/](https://meet.127.0.0.1.nip.io/).
+211
View File
@@ -0,0 +1,211 @@
"""Visible room join agent (for connection/testing) + JSON speaker intervals per participant."""
import logging
import os
from datetime import datetime, timezone
from typing import Dict, List, Optional
import json
import pathlib
from io import BytesIO
from minio import Minio
from minio.error import S3Error
from dotenv import load_dotenv
from livekit import api, rtc
from livekit.agents import (
AutoSubscribe,
JobContext,
JobRequest,
WorkerOptions,
WorkerPermissions,
cli,
)
load_dotenv()
logger = logging.getLogger("visible-joiner")
logger.setLevel(logging.INFO)
logger.propagate = False
if not logger.handlers:
_h = logging.StreamHandler()
_h.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s %(name)s - %(message)s"))
logger.addHandler(_h)
VISIBLE_AGENT_NAME = os.getenv("VISIBLE_AGENT_NAME", "visible-joiner")
class SpeakerTracker:
def __init__(self, room_name: str, write_json: bool = True):
self.room_name = room_name
self.active_since: Dict[str, datetime] = {}
self.by_participant: Dict[str, List[dict]] = {}
self.write_json_flag = write_json
ts = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
outdir = pathlib.Path("./speaker_logs")
outdir.mkdir(parents=True, exist_ok=True)
self.json_path: Optional[pathlib.Path] = outdir / f"speakers_{room_name}_{ts}.json" if write_json else None
def _now(self) -> datetime:
return datetime.now(timezone.utc)
def _emit_interval(self, identity: str, start: datetime, end: datetime):
dur = max(0.0, (end - start).total_seconds())
seg = {
"start_iso": start.isoformat(),
"end_iso": end.isoformat(),
"duration_sec": round(dur, 3),
}
self.by_participant.setdefault(identity, []).append(seg)
def update_active_speakers(self, current_identities: List[str]):
now = self._now()
current = set(current_identities)
before = set(self.active_since.keys())
for ident in current - before:
self.active_since[ident] = now
for ident in before - current:
start = self.active_since.pop(ident, None)
if start:
self._emit_interval(ident, start, now)
def on_participant_disconnected(self, identity: str):
now = self._now()
start = self.active_since.pop(identity, None)
if start:
self._emit_interval(identity, start, now)
def flush_all(self):
now = self._now()
for ident, start in list(self.active_since.items()):
self._emit_interval(ident, start, now)
self.active_since.clear()
def build_json(self) -> dict:
return {
"room": self.room_name,
"generated_at": self._now().isoformat(),
"by_participant": self.by_participant,
}
def write_json(self):
def _as_bool(v: str, default=False):
if v is None:
return default
return v.strip().lower() in ("1", "true", "yes", "y")
if not self.write_json_flag:
return
payload = self.build_json()
minio_client = Minio(
endpoint=os.getenv("AWS_S3_ENDPOINT_URL"),
access_key=os.getenv("AWS_S3_ACCESS_KEY_ID"),
secret_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
secure= _as_bool(os.getenv("AWS_S3_SECURE_ACCESS", "false"))
)
bucket = "meet-media-storage"
ts = self._now().strftime("%Y%m%dT%H%M%SZ")
object_name = f"speaker_logs/{self.room_name}/speakers_{self.room_name}_{ts}.json"
data = json.dumps(payload, indent=2).encode("utf-8")
stream = BytesIO(data)
try:
minio_client.put_object(
bucket,
object_name,
stream,
length=len(data),
content_type="application/json",
)
logger.info("Uploaded speaker intervals JSON to s3://%s/%s", bucket, object_name)
except S3Error:
logger.exception("Failed to upload JSON to bucket=%s object=%s", bucket, object_name)
async def entrypoint(ctx: JobContext):
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
lp = ctx.room.local_participant
logger.info(
"Connected to SFU; room=%s identity=%s sid=%s",
ctx.room.name,
getattr(lp, "identity", "<unknown>"),
getattr(lp, "sid", "<unknown>"),
)
tracker = SpeakerTracker(ctx.room.name, write_json=True)
def _on_participant_connected(p: rtc.RemoteParticipant):
logger.info("remote participant connected: %s (%s)", p.identity, p.sid)
def _on_active_speakers_changed(speakers: list[rtc.Participant]):
idents = [p.identity for p in speakers]
logger.info("active speakers changed: %s", idents)
tracker.update_active_speakers(idents)
def _on_participant_disconnected(p: rtc.RemoteParticipant):
logger.info("remote participant disconnected: %s", p.identity)
tracker.on_participant_disconnected(p.identity)
if not ctx.proc.userdata.get("events_registered"):
ctx.room.on("participant_connected", _on_participant_connected)
ctx.room.on("active_speakers_changed", _on_active_speakers_changed)
ctx.room.on("participant_disconnected", _on_participant_disconnected)
ctx.proc.userdata["events_registered"] = True
async def cleanup():
if ctx.proc.userdata.get("events_registered"):
ctx.room.off("participant_connected", _on_participant_connected)
ctx.room.off("active_speakers_changed", _on_active_speakers_changed)
ctx.room.off("participant_disconnected", _on_participant_disconnected)
ctx.proc.userdata["events_registered"] = False
tracker.flush_all()
tracker.write_json()
ctx.add_shutdown_callback(cleanup)
async def handle_job_request(job_req: JobRequest) -> None:
room_name = job_req.room.name
agent_identity = f"{VISIBLE_AGENT_NAME}-{room_name}"
async with api.LiveKitAPI() as lk:
try:
resp = await lk.room.list_participants(
list=api.ListParticipantsRequest(room=room_name)
)
already_present = any(
p.kind == rtc.ParticipantKind.PARTICIPANT_KIND_AGENT
and p.identity == agent_identity
for p in resp.participants
)
if already_present:
logger.info("Agent already in the room '%s' — reject", room_name)
await job_req.reject()
else:
logger.info("Accept job for '%s' — identity=%s", room_name, agent_identity)
await job_req.accept(identity=agent_identity)
except Exception:
logger.exception("Error treating the job for '%s'", room_name)
await job_req.reject()
if __name__ == "__main__":
cli.run_app(
WorkerOptions(
entrypoint_fnc=entrypoint,
request_fnc=handle_job_request,
agent_name=VISIBLE_AGENT_NAME,
permissions=WorkerPermissions(),
)
)
+4
View File
@@ -29,6 +29,10 @@ body:has(.lk-video-conference) #crisp-chatbox-button {
display: none !important;
}
body:has(.lk-video-conference) #crisp-chatbox > * > div[role='button'] {
display: none !important;
}
@keyframes slide-full {
from {
transform: translateY(100%);
-67
View File
@@ -1,67 +0,0 @@
environments:
dev-keycloak:
values:
- version: 0.0.1
- env.d/{{ .Environment.Name }}/values.secrets.yaml
dev-dinum:
values:
- version: 0.0.1
- env.d/{{ .Environment.Name }}/values.secrets.yaml
dev:
values:
- version: 0.0.1
- env.d/{{ .Environment.Name }}/values.secrets.yaml
repositories:
- name: livekit
url: https://helm.livekit.io
releases:
- name: extra
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: ./extra
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
values:
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
- addRedirect: {{ .Values | get "addRedirect" "False" }}
enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
newDomain: {{ .Values | get "newDomain" "demo.com" }}
- realm: |
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 8 }}
- name: meet
version: {{ .Values.version }}
namespace: {{ .Namespace }}
missingFileHandler: Warn
chart: ./meet
values:
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
- name: livekit
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: livekit/livekit-server
values:
- env.d/{{ .Environment.Name }}/values.livekit.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
- name: livekit-egress
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: livekit/egress
values:
- env.d/{{ .Environment.Name }}/values.egress.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
+69
View File
@@ -0,0 +1,69 @@
environments:
dev-keycloak:
values:
- version: 0.0.1
- env.d/dev-keycloak/values.secrets.yaml
dev-dinum:
values:
- version: 0.0.1
- env.d/dev-dinum/values.secrets.yaml
dev:
values:
- version: 0.0.1
- env.d/dev/values.secrets.yaml
---
repositories:
- name: livekit
url: https://helm.livekit.io
releases:
- name: extra
installed: {{ regexMatch "^dev.*" .Environment.Name }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: ./extra
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
values:
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
- addRedirect: {{ .Values | get "addRedirect" "False" }}
- enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
- oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
- newDomain: {{ .Values | get "newDomain" "demo.com" }}
- realm: |
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 8 }}
- name: meet
version: {{ .Values.version }}
namespace: {{ .Namespace }}
missingFileHandler: Warn
chart: ./meet
values:
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
- name: livekit
installed: {{ regexMatch "^dev.*" .Environment.Name }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: livekit/livekit-server
values:
- env.d/{{ .Environment.Name }}/values.livekit.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
- name: livekit-egress
installed: {{ regexMatch "^dev.*" .Environment.Name }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: livekit/egress
values:
- env.d/{{ .Environment.Name }}/values.egress.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml