mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 20:08:24 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fd8e3a242 | |||
| 17f5bb50cb | |||
| 96f97ed2d0 | |||
| 02d16cb55c | |||
| 7268ff6777 | |||
| cca5bc2186 | |||
| ec67a12fe4 | |||
| 05f32d008a |
@@ -150,13 +150,14 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
- name: Check code formatting with ruff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
run: uv run ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: ~/.local/bin/ruff check .
|
||||
run: uv run ruff check .
|
||||
|
||||
lint-summary:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -18,6 +18,12 @@ and this project adheres to
|
||||
### Changed
|
||||
|
||||
- ♻️(summary) change tasks endpoint signature
|
||||
- ⬆️(dependencies) update urllib3 to v2.7.0 [SECURITY]
|
||||
|
||||
### Changed
|
||||
|
||||
- 🧑💻(agents) use `uv` for package management
|
||||
- ✨(summary) improve speaker-to-participant assignment
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ build-frontend: ## build the frontend container
|
||||
.PHONY: build-frontend
|
||||
|
||||
build-agents: ## build the multi-user-transcriber agent container
|
||||
@$(COMPOSE) build multi-user-transcriber
|
||||
@$(COMPOSE) build multi-user-transcriber-dev
|
||||
.PHONY: build-agents
|
||||
|
||||
down: ## stop and remove containers, networks, images, and volumes
|
||||
@@ -138,7 +138,7 @@ run-agents: ## start the multi-user-transcriber agent
|
||||
.PHONY: run-agents
|
||||
|
||||
run-agent-multi-user-transcriber: ## start the LiveKit agents (multi users transcriber)
|
||||
@$(COMPOSE) up --force-recreate -d multi-user-transcriber
|
||||
@$(COMPOSE) up --force-recreate -d multi-user-transcriber-dev
|
||||
.PHONY: run-agent-multi-user-transcriber
|
||||
|
||||
run-agent-metadata-collector: ## start the LiveKit agents (metadata collector)
|
||||
|
||||
+12
-8
@@ -249,6 +249,7 @@ services:
|
||||
metadata-collector-dev:
|
||||
build:
|
||||
context: ./src/agents
|
||||
target: development
|
||||
command: ["python", "metadata_collector.py", "dev"]
|
||||
environment:
|
||||
- LIVEKIT_URL=ws://livekit:7880
|
||||
@@ -261,6 +262,7 @@ services:
|
||||
- AWS_S3_SECURE_ACCESS=False
|
||||
volumes:
|
||||
- ./src/agents:/app
|
||||
- /app/.venv
|
||||
depends_on:
|
||||
- livekit
|
||||
- minio
|
||||
@@ -269,6 +271,16 @@ services:
|
||||
- action: rebuild
|
||||
path: ./src/agents
|
||||
|
||||
multi-user-transcriber-dev:
|
||||
build:
|
||||
context: ./src/agents
|
||||
target: development
|
||||
env_file:
|
||||
- env.d/development/multi_user_transcriber
|
||||
volumes:
|
||||
- ./src/agents:/app
|
||||
- /app/.venv
|
||||
|
||||
redis-summary:
|
||||
image: redis
|
||||
ports:
|
||||
@@ -330,14 +342,6 @@ services:
|
||||
- action: rebuild
|
||||
path: ./src/summary
|
||||
|
||||
multi-user-transcriber:
|
||||
build:
|
||||
context: ./src/agents
|
||||
env_file:
|
||||
- env.d/development/multi_user_transcriber
|
||||
volumes:
|
||||
- ./src/agents:/app
|
||||
|
||||
networks:
|
||||
default:
|
||||
resource-server:
|
||||
|
||||
Generated
+12
-250
@@ -19,7 +19,7 @@
|
||||
"@types/office-runtime": "^1.0.35",
|
||||
"acorn": "^8.11.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"eslint-plugin-office-addins": "^4.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
@@ -4319,44 +4319,6 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
|
||||
@@ -4370,19 +4332,6 @@
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
|
||||
"integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@@ -6686,21 +6635,20 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/copy-webpack-plugin": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz",
|
||||
"integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==",
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz",
|
||||
"integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.3.2",
|
||||
"glob-parent": "^6.0.1",
|
||||
"globby": "^14.0.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"schema-utils": "^4.2.0",
|
||||
"serialize-javascript": "^6.0.2"
|
||||
"serialize-javascript": "^7.0.3",
|
||||
"tinyglobby": "^0.2.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.12.0"
|
||||
"node": ">= 20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -8062,36 +8010,6 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -8180,16 +8098,6 @@
|
||||
"node": ">= 4.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/faye-websocket": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
|
||||
@@ -8758,37 +8666,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/globby": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz",
|
||||
"integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^2.1.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"ignore": "^7.0.3",
|
||||
"path-type": "^6.0.0",
|
||||
"slash": "^5.1.0",
|
||||
"unicorn-magic": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globby/node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -11044,16 +10921,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
@@ -12638,19 +12505,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
|
||||
"integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
|
||||
@@ -12985,37 +12839,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -13502,17 +13325,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
@@ -13545,30 +13357,6 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
@@ -13842,13 +13630,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz",
|
||||
"integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-index": {
|
||||
@@ -14260,19 +14048,6 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sockjs": {
|
||||
"version": "0.3.24",
|
||||
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
|
||||
@@ -15311,19 +15086,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
||||
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@types/office-runtime": "^1.0.35",
|
||||
"acorn": "^8.11.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"eslint-plugin-office-addins": "^4.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Python
|
||||
__pycache__
|
||||
*.pyc
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
venv
|
||||
**/.venv
|
||||
|
||||
# System-specific files
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Docker
|
||||
compose.*
|
||||
env.d
|
||||
|
||||
# Docs
|
||||
docs
|
||||
*.md
|
||||
*.log
|
||||
|
||||
# Development/test cache & configurations
|
||||
data
|
||||
.cache
|
||||
.circleci
|
||||
.git
|
||||
.iml
|
||||
db.sqlite3
|
||||
.pylint.d
|
||||
|
||||
**/.idea
|
||||
**/.vscode
|
||||
**/.pytest_cache
|
||||
**/.mypy_cache
|
||||
**/.ruff_cache
|
||||
|
||||
# Env
|
||||
.env
|
||||
+42
-14
@@ -6,31 +6,61 @@ RUN apt-get update && apt-get install -y \
|
||||
libgobject-2.0-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# ---- Builder image ----
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
ENV UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
COPY pyproject.toml .
|
||||
|
||||
RUN mkdir /install && \
|
||||
pip install --prefix=/install .
|
||||
|
||||
FROM base AS development
|
||||
# Install uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml .
|
||||
RUN pip install --no-cache-dir ".[dev]"
|
||||
# Install production dependencies without the project itself (cacheable layer)
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked --no-install-project --no-dev
|
||||
|
||||
COPY . .
|
||||
# Install the project
|
||||
COPY . /app
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --no-dev
|
||||
|
||||
CMD ["python", "metadata_collector.py", "dev"]
|
||||
|
||||
# ---- Development image ----
|
||||
FROM base AS development
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --all-extras
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
CMD ["python", "multi_user_transcriber.py", "dev"]
|
||||
|
||||
|
||||
# ---- Production image ----
|
||||
FROM base AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /install /usr/local
|
||||
# Copy the pre-built virtualenv and application source
|
||||
COPY --from=builder /app /app
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Remove pip to reduce attack surface in production
|
||||
RUN pip uninstall -y pip
|
||||
@@ -39,6 +69,4 @@ RUN pip uninstall -y pip
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
COPY ./*.py /app/
|
||||
|
||||
CMD ["python", "multi_user_transcriber.py", "start"]
|
||||
|
||||
@@ -18,12 +18,8 @@ dev = [
|
||||
"ruff==0.15.6",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["multi_user_transcriber", "metadata_collector", "exceptions"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
|
||||
Generated
+1963
File diff suppressed because it is too large
Load Diff
@@ -61,6 +61,7 @@ dependencies = [
|
||||
"mozilla-django-oidc==5.0.2",
|
||||
"livekit-api==1.1.0",
|
||||
"aiohttp==3.13.4",
|
||||
"urllib3==2.7.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
Generated
+5
-3
@@ -1212,6 +1212,7 @@ dependencies = [
|
||||
{ name = "redis" },
|
||||
{ name = "requests" },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "whitenoise" },
|
||||
]
|
||||
|
||||
@@ -1273,6 +1274,7 @@ requires-dist = [
|
||||
{ name = "redis", specifier = "==5.2.1" },
|
||||
{ name = "requests", specifier = "==2.33.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.54.0" },
|
||||
{ name = "urllib3", specifier = "==2.7.0" },
|
||||
{ name = "whitenoise", specifier = "==6.12.0" },
|
||||
]
|
||||
|
||||
@@ -2260,11 +2262,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Generated
+419
-460
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@
|
||||
"wouter": "3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pandacss/dev": "1.8.2",
|
||||
"@pandacss/dev": "1.11.1",
|
||||
"@tanstack/eslint-plugin-query": "5.91.4",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
@@ -62,7 +62,7 @@
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"postcss": "8.5.10",
|
||||
"postcss": "8.5.14",
|
||||
"prettier": "3.8.1",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.3.2",
|
||||
|
||||
Generated
+1513
-524
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
"mjml": "4.18.0"
|
||||
"mjml": "5.0.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -105,6 +105,9 @@ class Settings(BaseSettings):
|
||||
|
||||
# Speaker to user assignment
|
||||
is_resolve_speaker_identities_enabled: bool = True
|
||||
resolve_speaker_identities_default_overlap_threshold: float = 0.5
|
||||
resolve_speaker_identities_enable_split_on_words: bool = True
|
||||
resolve_speaker_identities_max_word_duration: float = 1 # seconds
|
||||
|
||||
# Webhook-related settings
|
||||
webhook_max_retries: int = 2
|
||||
|
||||
@@ -14,11 +14,11 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from summary.core.config import get_settings
|
||||
|
||||
# Minimum fraction of a speaker's total duration that must overlap with a
|
||||
# participant's VAD to accept the assignment.
|
||||
DEFAULT_OVERLAP_THRESHOLD = 0.5
|
||||
settings = get_settings()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -74,7 +74,7 @@ class AssignmentResult:
|
||||
f" ({item['speaker']})" if name_to_speaker_count[name] > 1 else ""
|
||||
) # Add suffix only if there are multiple detected speakers per user
|
||||
return {**item, "speaker": f"{name}{suffix}"}
|
||||
return item
|
||||
return {**item}
|
||||
|
||||
def _process_segment(
|
||||
item: dict[str, Any], include_words: bool = False
|
||||
@@ -138,6 +138,78 @@ def _overlap_duration(
|
||||
return overlap
|
||||
|
||||
|
||||
def _format_timelines_debug(
|
||||
participant_timelines: dict[str, list[Interval]],
|
||||
participant_names: dict[str, str],
|
||||
speaker_timelines: dict[str, list[Interval]],
|
||||
) -> str:
|
||||
"""Render participant and speaker timelines side-by-side for debugging.
|
||||
|
||||
Each row is the slice between two consecutive interval boundaries
|
||||
(drawn from both sides). A filled cell marks an active participant
|
||||
(left block) or speaker (right block) during that slice, so vertical
|
||||
alignment makes overlap visually obvious.
|
||||
"""
|
||||
participant_ids = sorted(participant_timelines.keys())
|
||||
speaker_labels = sorted(speaker_timelines.keys())
|
||||
|
||||
if not participant_ids and not speaker_labels:
|
||||
return "(no timelines)"
|
||||
|
||||
boundaries: set[float] = set()
|
||||
for intervals in (*participant_timelines.values(), *speaker_timelines.values()):
|
||||
for iv in intervals:
|
||||
boundaries.add(iv.start)
|
||||
boundaries.add(iv.end)
|
||||
sorted_boundaries = sorted(boundaries)
|
||||
if len(sorted_boundaries) < 2:
|
||||
return "(no intervals)"
|
||||
|
||||
p_headers = [participant_names.get(pid, pid) for pid in participant_ids]
|
||||
s_headers = list(speaker_labels)
|
||||
p_widths = [max(len(h), 3) for h in p_headers]
|
||||
s_widths = [max(len(h), 3) for h in s_headers]
|
||||
|
||||
def _active(intervals: list[Interval], lo: float, hi: float) -> bool:
|
||||
mid = (lo + hi) / 2
|
||||
return any(iv.start <= mid < iv.end for iv in intervals)
|
||||
|
||||
def _cells(
|
||||
intervals_list: list[list[Interval]],
|
||||
widths: list[int],
|
||||
lo: float,
|
||||
hi: float,
|
||||
) -> str:
|
||||
return " ".join(
|
||||
("█" * w if _active(iv, lo, hi) else "·" * w)
|
||||
for iv, w in zip(intervals_list, widths, strict=True)
|
||||
)
|
||||
|
||||
p_iv_list = [participant_timelines[pid] for pid in participant_ids]
|
||||
s_iv_list = [speaker_timelines[sl] for sl in speaker_labels]
|
||||
|
||||
time_col = "[ start → end]"
|
||||
p_hdr = (
|
||||
" ".join(h.center(w) for h, w in zip(p_headers, p_widths, strict=True))
|
||||
or "(none)"
|
||||
)
|
||||
s_hdr = (
|
||||
" ".join(h.center(w) for h, w in zip(s_headers, s_widths, strict=True))
|
||||
or "(none)"
|
||||
)
|
||||
sep = " || "
|
||||
lines = [
|
||||
f"{time_col} {p_hdr}{sep}{s_hdr}",
|
||||
"-" * (len(time_col) + 2 + len(p_hdr) + len(sep) + len(s_hdr)),
|
||||
]
|
||||
for lo, hi in zip(sorted_boundaries, sorted_boundaries[1:], strict=False):
|
||||
time_str = f"[{lo:8.2f} → {hi:8.2f}]"
|
||||
p_row = _cells(p_iv_list, p_widths, lo, hi) or " " * len(p_hdr)
|
||||
s_row = _cells(s_iv_list, s_widths, lo, hi) or " " * len(s_hdr)
|
||||
lines.append(f"{time_str} {p_row}{sep}{s_row}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _build_participant_timelines(
|
||||
metadata: dict[str, Any],
|
||||
recording_start_datetime: datetime,
|
||||
@@ -199,24 +271,50 @@ def _build_participant_timelines(
|
||||
return intervals, participants_info
|
||||
|
||||
|
||||
def _build_speaker_timelines(
|
||||
transcription: Any,
|
||||
) -> dict[str, list[Interval]]:
|
||||
def _build_speaker_timelines(transcription: Any) -> dict[str, list[Interval]]:
|
||||
"""Build interval timelines from WhisperX transcription segments."""
|
||||
intervals: dict[str, list[Interval]] = {}
|
||||
|
||||
segments = transcription.segments if hasattr(transcription, "segments") else []
|
||||
max_word_duration = settings.resolve_speaker_identities_max_word_duration
|
||||
|
||||
for segment in segments:
|
||||
speaker = segment.get("speaker")
|
||||
if speaker is None:
|
||||
continue
|
||||
intervals.setdefault(speaker, []).append(
|
||||
Interval(segment["start"], segment["end"])
|
||||
)
|
||||
|
||||
words = [
|
||||
w
|
||||
for w in segment.get("words", [])
|
||||
if w.get("start") is not None and w.get("end") is not None
|
||||
]
|
||||
if not words:
|
||||
intervals.setdefault(speaker, []).append(
|
||||
Interval(segment["start"], segment["end"])
|
||||
)
|
||||
continue
|
||||
|
||||
start_time: float | None = segment["start"]
|
||||
for word in words:
|
||||
if start_time is None:
|
||||
start_time = word["start"]
|
||||
if not settings.resolve_speaker_identities_enable_split_on_words:
|
||||
continue
|
||||
if word["end"] - word["start"] > max_word_duration:
|
||||
end_time = word["start"] + max_word_duration
|
||||
if end_time > start_time:
|
||||
intervals.setdefault(speaker, []).append(
|
||||
Interval(start_time, end_time)
|
||||
)
|
||||
start_time = None
|
||||
|
||||
if start_time is not None:
|
||||
last = words[-1]
|
||||
end_time = min(last["end"], last["start"] + max_word_duration)
|
||||
if end_time > start_time:
|
||||
intervals.setdefault(speaker, []).append(Interval(start_time, end_time))
|
||||
|
||||
for speaker, speaker_intervals in intervals.items():
|
||||
intervals[speaker] = _merge_intervals(speaker_intervals)
|
||||
|
||||
return intervals
|
||||
|
||||
|
||||
@@ -225,7 +323,7 @@ def resolve_speaker_identities(
|
||||
transcription: Any,
|
||||
recording_start_datetime: datetime,
|
||||
recording_end_datetime: datetime,
|
||||
overlap_threshold: float = DEFAULT_OVERLAP_THRESHOLD,
|
||||
overlap_threshold: float = settings.resolve_speaker_identities_default_overlap_threshold, # noqa: E501
|
||||
) -> AssignmentResult:
|
||||
"""Match WhisperX speaker labels to participants.
|
||||
|
||||
@@ -247,9 +345,14 @@ def resolve_speaker_identities(
|
||||
speaker_timelines = _build_speaker_timelines(transcription)
|
||||
|
||||
logger.debug(
|
||||
"Assignment inputs: %d participants, %d speakers",
|
||||
"Assignment inputs: %d participants, %d speakers\n%s\n%s\n%s",
|
||||
len(participant_timelines),
|
||||
len(speaker_timelines),
|
||||
participant_timelines,
|
||||
speaker_timelines,
|
||||
_format_timelines_debug(
|
||||
participant_timelines, participant_names, speaker_timelines
|
||||
),
|
||||
)
|
||||
|
||||
result = AssignmentResult()
|
||||
|
||||
@@ -4,10 +4,12 @@ import math
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
from summary.core import user_assign
|
||||
from summary.core.user_assign import (
|
||||
AssignmentResult,
|
||||
Interval,
|
||||
SpeakerAssignment,
|
||||
_build_speaker_timelines,
|
||||
_merge_intervals,
|
||||
_overlap_duration,
|
||||
_total_duration,
|
||||
@@ -73,12 +75,22 @@ DIARIZATION_SINGLE_SPEAKER = FakeTranscription(
|
||||
"end": 3.545,
|
||||
"text": " The stale smell.",
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [
|
||||
{"word": "The", "start": 1.363, "end": 1.8},
|
||||
{"word": "stale", "start": 1.8, "end": 2.7},
|
||||
{"word": "smell.", "start": 2.7, "end": 3.545},
|
||||
],
|
||||
},
|
||||
{
|
||||
"start": 4.466,
|
||||
"end": 6.247,
|
||||
"text": "It takes heat.",
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [
|
||||
{"word": "It", "start": 4.466, "end": 4.7},
|
||||
{"word": "takes", "start": 4.7, "end": 5.5},
|
||||
{"word": "heat.", "start": 5.5, "end": 6.247},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
@@ -165,6 +177,174 @@ class TestTotalDuration:
|
||||
assert math.isclose(_total_duration([]), 0.0)
|
||||
|
||||
|
||||
class TestBuildSpeakerTimelines:
|
||||
"""Tests for _build_speaker_timelines."""
|
||||
|
||||
def test_segment_without_words_falls_back_to_segment_bounds(self):
|
||||
"""Segments missing a `words` key use the segment start/end as one interval."""
|
||||
transcription = FakeTranscription(
|
||||
segments=[{"start": 1.5, "end": 3.5, "speaker": "SPEAKER_00"}],
|
||||
)
|
||||
result = _build_speaker_timelines(transcription)
|
||||
assert result == {"SPEAKER_00": [Interval(1.5, 3.5)]}
|
||||
|
||||
def test_segment_with_only_none_word_timestamps_falls_back(self):
|
||||
"""If every word has None start/end, fall back to segment bounds."""
|
||||
transcription = FakeTranscription(
|
||||
segments=[
|
||||
{
|
||||
"start": 1.0,
|
||||
"end": 4.0,
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [
|
||||
{"word": "hi", "start": None, "end": None},
|
||||
{"word": "there", "start": None, "end": None},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
result = _build_speaker_timelines(transcription)
|
||||
assert result == {"SPEAKER_00": [Interval(1.0, 4.0)]}
|
||||
|
||||
def test_short_words_only_uses_segment_start_and_last_word_end(self):
|
||||
"""With no overly long words, the interval runs segment start to end."""
|
||||
transcription = FakeTranscription(
|
||||
segments=[
|
||||
{
|
||||
"start": 1.0,
|
||||
"end": 5.0,
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [
|
||||
{"word": "a", "start": 1.0, "end": 1.3},
|
||||
{"word": "b", "start": 1.4, "end": 1.7},
|
||||
{"word": "c", "start": 1.8, "end": 2.1},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
result = _build_speaker_timelines(transcription)
|
||||
# Tail: min(2.1, 1.8 + 1.0) = 2.1
|
||||
assert result == {"SPEAKER_00": [Interval(1.0, 2.1)]}
|
||||
|
||||
def test_long_word_caps_interval_at_max_duration(self):
|
||||
"""A word longer than the max-word-duration cap truncates the segment."""
|
||||
max_word_duration = (
|
||||
user_assign.settings.resolve_speaker_identities_max_word_duration
|
||||
)
|
||||
transcription = FakeTranscription(
|
||||
segments=[
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": max_word_duration + 7,
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [
|
||||
{
|
||||
"word": "pause",
|
||||
"start": 0.0,
|
||||
"end": max_word_duration + 7,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
result = _build_speaker_timelines(transcription)
|
||||
assert result == {"SPEAKER_00": [Interval(0.0, max_word_duration)]}
|
||||
|
||||
def test_long_word_in_middle_splits_segment(self):
|
||||
"""Short words around a long word produce two intervals (before-cap + after)."""
|
||||
transcription = FakeTranscription(
|
||||
segments=[
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 20.0,
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [
|
||||
{"word": "a", "start": 0.0, "end": 0.5},
|
||||
{"word": "long", "start": 1.0, "end": 15.0},
|
||||
{"word": "z", "start": 18.0, "end": 18.4},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
result = _build_speaker_timelines(transcription)
|
||||
# First emit: (0.0, 1.0 + 1.0). Then start_time resets, picks up at "z" (18.0).
|
||||
# Tail: min(18.4, 18.0 + 1.0) = 18.4. So second interval is (18.0, 18.4).
|
||||
assert result == {
|
||||
"SPEAKER_00": [Interval(0.0, 2.0), Interval(18.0, 18.4)],
|
||||
}
|
||||
|
||||
def test_tail_word_is_capped_at_max_duration(self):
|
||||
"""The trailing word's end is capped at word.start + max_word_duration."""
|
||||
transcription = FakeTranscription(
|
||||
segments=[
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 50.0,
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [
|
||||
{"word": "a", "start": 0.0, "end": 0.4},
|
||||
# Last word ends inside the cap, so the cap doesn't apply.
|
||||
{"word": "b", "start": 1.0, "end": 1.5},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
result = _build_speaker_timelines(transcription)
|
||||
# Tail: min(1.5, 1.0 + 1.0) = 1.5
|
||||
assert result == {"SPEAKER_00": [Interval(0.0, 1.5)]}
|
||||
|
||||
def test_split_on_words_disabled_keeps_segment_as_one_interval(self, monkeypatch):
|
||||
"""With splitting disabled, long words don't split the interval."""
|
||||
monkeypatch.setattr(
|
||||
user_assign,
|
||||
"settings",
|
||||
user_assign.settings.model_copy(
|
||||
update={"resolve_speaker_identities_enable_split_on_words": False},
|
||||
),
|
||||
)
|
||||
transcription = FakeTranscription(
|
||||
segments=[
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 20.0,
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [
|
||||
{"word": "a", "start": 0.0, "end": 0.5},
|
||||
{"word": "long", "start": 1.0, "end": 15.0},
|
||||
{"word": "z", "start": 18.0, "end": 18.4},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
result = _build_speaker_timelines(transcription)
|
||||
# No mid-segment split; tail caps at min(18.4, 18.0 + 1.0) = 18.4.
|
||||
assert result == {"SPEAKER_00": [Interval(0.0, 18.4)]}
|
||||
|
||||
def test_multiple_speakers_keep_separate_timelines(self):
|
||||
"""Segments from different speakers populate independent timeline entries."""
|
||||
transcription = FakeTranscription(
|
||||
segments=[
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 1.0,
|
||||
"speaker": "SPEAKER_00",
|
||||
"words": [{"word": "hi", "start": 0.0, "end": 0.5}],
|
||||
},
|
||||
{
|
||||
"start": 2.0,
|
||||
"end": 3.0,
|
||||
"speaker": "SPEAKER_01",
|
||||
"words": [{"word": "yo", "start": 2.0, "end": 2.5}],
|
||||
},
|
||||
],
|
||||
)
|
||||
result = _build_speaker_timelines(transcription)
|
||||
assert result == {
|
||||
"SPEAKER_00": [Interval(0.0, 0.5)],
|
||||
"SPEAKER_01": [Interval(2.0, 2.5)],
|
||||
}
|
||||
|
||||
|
||||
class TestResolveSpeakerIdentities:
|
||||
"""Tests for resolve_speaker_identities."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user