cjm-speaker-diarization-adapter-interface 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cjm_speaker_diarization_adapter_interface-0.0.1/LICENSE +201 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/PKG-INFO +49 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/README.md +33 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface/__init__.py +1 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface/adapter.py +70 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface/generic.py +79 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface/storage.py +203 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface.egg-info/PKG-INFO +49 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface.egg-info/SOURCES.txt +14 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface.egg-info/dependency_links.txt +1 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface.egg-info/requires.txt +2 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface.egg-info/top_level.txt +1 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/pyproject.toml +29 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/setup.cfg +4 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/tests/test_adapter.py +87 -0
- cjm_speaker_diarization_adapter_interface-0.0.1/tests/test_storage.py +71 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2022, fastai
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cjm-speaker-diarization-adapter-interface
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Typed speaker-diarization task-adapter interface — SpeakerDiarizationAdapter ABC + GenericSpeakerDiarizationAdapter (thin pass-through around a diarization tool) and the SpeakerDiarizationToolProtocol. The SpeakerDiarizationResult data noun lives in cjm-capability-primitives; turns are ANONYMOUS clusters — identity is the correction TUI's assignment lane, never this interface's job.
|
|
5
|
+
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/cj-mills/cjm-speaker-diarization-adapter-interface
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: cjm-substrate>=0.0.58
|
|
14
|
+
Requires-Dist: cjm-capability-primitives>=0.0.12
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# cjm-speaker-diarization-adapter-interface
|
|
18
|
+
|
|
19
|
+
<!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
|
|
20
|
+
|
|
21
|
+
Adapter interface for speaker-diarization capabilities — the speaker_diarization task-channel contract (audio → typed SpeakerTurn/SpeakerDiarizationResult) that capability libs implement and workflow cores consume; the generic adapter owns the SQLite cache bookends (content-addressed, speaker-count hints fold into the config hash).
|
|
22
|
+
|
|
23
|
+
## Modules
|
|
24
|
+
|
|
25
|
+
- **`cjm_speaker_diarization_adapter_interface.__init__`**
|
|
26
|
+
- **`cjm_speaker_diarization_adapter_interface.adapter`** — The typed speaker-diarization task contract — the `SpeakerDiarizationAdapter` ABC + the `SpeakerDiarizationToolProtocol` structural contract (capability-unit Option C; session-D DECs 18d7de80 + d6df3a8e).
|
|
27
|
+
- **`cjm_speaker_diarization_adapter_interface.generic`** — The generic (tool-agnostic) speaker-diarization adapter — cache-check, invoke the bound tool's pure-compute `diarize`, persist. Reused across every tool capability satisfying `SpeakerDiarizationToolProtocol`, exactly as `GenericVADAdapter` is reused across VAD tools.
|
|
28
|
+
- **`cjm_speaker_diarization_adapter_interface.storage`** — Standardized SQLite storage for speaker-diarization results with content hashing.
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### `cjm_speaker_diarization_adapter_interface.adapter`
|
|
33
|
+
|
|
34
|
+
- `SpeakerDiarizationAdapter` _class_ — Typed speaker-diarization task adapter: source audio path in,
|
|
35
|
+
- `SpeakerDiarizationToolProtocol` _class_ — Structural contract for speaker-diarization tool capabilities.
|
|
36
|
+
|
|
37
|
+
### `cjm_speaker_diarization_adapter_interface.generic`
|
|
38
|
+
|
|
39
|
+
- `GenericSpeakerDiarizationAdapter` _class_ — Generic speaker-diarization adapter: cache-check -> pure-compute tool -> persist.
|
|
40
|
+
|
|
41
|
+
### `cjm_speaker_diarization_adapter_interface.storage`
|
|
42
|
+
|
|
43
|
+
- `SpeakerDiarizationRow` _class_ — A single row from the speaker_diarization_results table.
|
|
44
|
+
- `SpeakerDiarizationStorage` _class_ — Standardized SQLite storage for speaker-diarization results.
|
|
45
|
+
|
|
46
|
+
## Dependencies
|
|
47
|
+
|
|
48
|
+
**Depends on:** `cjm-capability-primitives`, `cjm-substrate`
|
|
49
|
+
**Used by:** `cjm-capability-pyannote`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# cjm-speaker-diarization-adapter-interface
|
|
2
|
+
|
|
3
|
+
<!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
|
|
4
|
+
|
|
5
|
+
Adapter interface for speaker-diarization capabilities — the speaker_diarization task-channel contract (audio → typed SpeakerTurn/SpeakerDiarizationResult) that capability libs implement and workflow cores consume; the generic adapter owns the SQLite cache bookends (content-addressed, speaker-count hints fold into the config hash).
|
|
6
|
+
|
|
7
|
+
## Modules
|
|
8
|
+
|
|
9
|
+
- **`cjm_speaker_diarization_adapter_interface.__init__`**
|
|
10
|
+
- **`cjm_speaker_diarization_adapter_interface.adapter`** — The typed speaker-diarization task contract — the `SpeakerDiarizationAdapter` ABC + the `SpeakerDiarizationToolProtocol` structural contract (capability-unit Option C; session-D DECs 18d7de80 + d6df3a8e).
|
|
11
|
+
- **`cjm_speaker_diarization_adapter_interface.generic`** — The generic (tool-agnostic) speaker-diarization adapter — cache-check, invoke the bound tool's pure-compute `diarize`, persist. Reused across every tool capability satisfying `SpeakerDiarizationToolProtocol`, exactly as `GenericVADAdapter` is reused across VAD tools.
|
|
12
|
+
- **`cjm_speaker_diarization_adapter_interface.storage`** — Standardized SQLite storage for speaker-diarization results with content hashing.
|
|
13
|
+
|
|
14
|
+
## API
|
|
15
|
+
|
|
16
|
+
### `cjm_speaker_diarization_adapter_interface.adapter`
|
|
17
|
+
|
|
18
|
+
- `SpeakerDiarizationAdapter` _class_ — Typed speaker-diarization task adapter: source audio path in,
|
|
19
|
+
- `SpeakerDiarizationToolProtocol` _class_ — Structural contract for speaker-diarization tool capabilities.
|
|
20
|
+
|
|
21
|
+
### `cjm_speaker_diarization_adapter_interface.generic`
|
|
22
|
+
|
|
23
|
+
- `GenericSpeakerDiarizationAdapter` _class_ — Generic speaker-diarization adapter: cache-check -> pure-compute tool -> persist.
|
|
24
|
+
|
|
25
|
+
### `cjm_speaker_diarization_adapter_interface.storage`
|
|
26
|
+
|
|
27
|
+
- `SpeakerDiarizationRow` _class_ — A single row from the speaker_diarization_results table.
|
|
28
|
+
- `SpeakerDiarizationStorage` _class_ — Standardized SQLite storage for speaker-diarization results.
|
|
29
|
+
|
|
30
|
+
## Dependencies
|
|
31
|
+
|
|
32
|
+
**Depends on:** `cjm-capability-primitives`, `cjm-substrate`
|
|
33
|
+
**Used by:** `cjm-capability-pyannote`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface/adapter.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""The typed speaker-diarization task contract — the `SpeakerDiarizationAdapter` ABC + the `SpeakerDiarizationToolProtocol` structural contract (capability-unit Option C; session-D DECs 18d7de80 + d6df3a8e)."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import Any, ClassVar, Dict, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from cjm_capability_primitives.speaker_diarization import SpeakerDiarizationResult
|
|
7
|
+
from cjm_substrate.core.adapter import TaskAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Canonical home (PILLAR 1c): the data noun lives in cjm-capability-primitives,
|
|
11
|
+
# so the tool capability depends on it, never on this adapter machinery.
|
|
12
|
+
# SCOPE (physical-speaker principle, DEC 44afb2df): this interface carries
|
|
13
|
+
# DIARIZATION only — audio in, ANONYMOUS time-ranged turns out. Mapping
|
|
14
|
+
# clusters to named identities is the correction TUI's assignment lane
|
|
15
|
+
# (human judgment); assignment semantics never enter this contract.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class SpeakerDiarizationToolProtocol(Protocol):
|
|
20
|
+
"""Structural contract for speaker-diarization tool capabilities.
|
|
21
|
+
|
|
22
|
+
`diarize` reads the original source audio (path) and returns time-ordered
|
|
23
|
+
anonymous speaker turns in that audio's coordinates. `get_current_config`
|
|
24
|
+
supplies the effective config whose canonical hash joins downstream
|
|
25
|
+
identity/provenance composites."""
|
|
26
|
+
def diarize(self, audio: str, **kwargs) -> SpeakerDiarizationResult: ...
|
|
27
|
+
def get_current_config(self) -> Dict[str, Any]: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SpeakerDiarizationAdapter(TaskAdapter):
|
|
31
|
+
"""Typed speaker-diarization task adapter: source audio path in,
|
|
32
|
+
`SpeakerDiarizationResult` (anonymous time-ranged turns) out.
|
|
33
|
+
|
|
34
|
+
Input contract: the caller passes a path to the audio whose COORDINATES it
|
|
35
|
+
needs preserved — turn times always index into exactly that audio (a tool
|
|
36
|
+
that resamples internally must report source-coordinate times). Cluster
|
|
37
|
+
labels are result-scoped only.
|
|
38
|
+
|
|
39
|
+
Speaker-count hints ride kwargs (`num_speakers`, `min_speakers`,
|
|
40
|
+
`max_speakers`) — optional, tool-interpreted; a tool without count control
|
|
41
|
+
ignores them.
|
|
42
|
+
|
|
43
|
+
No cache/persist bookends in v0: the transcription workflow persists
|
|
44
|
+
results on its own run manifests (turns ride BESIDE transcripts,
|
|
45
|
+
a9cadfec), and diarization runs once per source per config — a cache DB
|
|
46
|
+
can land in a generic-adapter revision if re-run pressure appears.
|
|
47
|
+
|
|
48
|
+
Implementations run in-worker beside their tool capability and are
|
|
49
|
+
constructed with the bound tool instance: `AdapterClass(tool)`. The result
|
|
50
|
+
DTO is wire-registered ("speaker_diarization.result"): returned values
|
|
51
|
+
cross the worker boundary typed.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
task_name: ClassVar[str] = "speaker_diarization"
|
|
55
|
+
required_tool_protocol: ClassVar[type] = SpeakerDiarizationToolProtocol
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
tool: SpeakerDiarizationToolProtocol, # The bound tool capability instance (worker-side binding)
|
|
60
|
+
):
|
|
61
|
+
self.tool = tool
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def diarize(
|
|
65
|
+
self,
|
|
66
|
+
audio: str, # Path to the source audio to diarize (turn times index into THIS audio)
|
|
67
|
+
**kwargs, # Speaker-count hints + provenance + tool options
|
|
68
|
+
) -> SpeakerDiarizationResult: # Time-ordered anonymous speaker turns
|
|
69
|
+
"""Diarize audio into time-ordered anonymous speaker turns."""
|
|
70
|
+
...
|
cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface/generic.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""The generic (tool-agnostic) speaker-diarization adapter — cache-check, invoke the bound tool's pure-compute `diarize`, persist. Reused across every tool capability satisfying `SpeakerDiarizationToolProtocol`, exactly as `GenericVADAdapter` is reused across VAD tools."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from cjm_capability_primitives.speaker_diarization import SpeakerDiarizationResult, SpeakerTurn
|
|
6
|
+
from cjm_substrate.core.wire import get_call_envelope
|
|
7
|
+
from cjm_substrate.utils.hashing import hash_dict_canonical, hash_file
|
|
8
|
+
|
|
9
|
+
from .adapter import SpeakerDiarizationAdapter
|
|
10
|
+
from .storage import SpeakerDiarizationStorage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GenericSpeakerDiarizationAdapter(SpeakerDiarizationAdapter):
|
|
14
|
+
"""Generic speaker-diarization adapter: cache-check -> pure-compute tool -> persist.
|
|
15
|
+
|
|
16
|
+
Works against ANY tool satisfying `SpeakerDiarizationToolProtocol`. The
|
|
17
|
+
bookends landed 2026-07-26 (the VAD-adapter precedent this class was born
|
|
18
|
+
citing): full-source diarization costs ~46s/hour on cuda and sources run
|
|
19
|
+
multi-hour, so re-runs are worth caching.
|
|
20
|
+
|
|
21
|
+
1. cache check (file_path + file_hash + config_hash) BEFORE invoking the
|
|
22
|
+
tool, so a hit never loads the pipeline;
|
|
23
|
+
2. the tool's pure-compute `diarize` on a miss / forced call;
|
|
24
|
+
3. `save_with_logging` (upsert by file_path + config_hash).
|
|
25
|
+
|
|
26
|
+
Unlike VAD, `diarize` kwargs are NOT provenance pass-through: the
|
|
27
|
+
speaker-count hints (num_speakers / min_speakers / max_speakers) steer the
|
|
28
|
+
clustering, so they FOLD INTO `config_hash` — the same audio diarized
|
|
29
|
+
under a different hint is a different cache row. `force` rides
|
|
30
|
+
`CallEnvelope.control` (not a task kwarg, keeping `diarize(audio)` pure).
|
|
31
|
+
Storage lives at `<CAPABILITY_DATA_DIR>/speaker_diarization.db`; the
|
|
32
|
+
substrate injects the per-capability `CAPABILITY_DATA_DIR` at spawn.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Per-call kwargs that change the clustering output — cache identity,
|
|
36
|
+
# mirrored from the pyannote tool's diarize signature.
|
|
37
|
+
HINT_KEYS = ("num_speakers", "min_speakers", "max_speakers")
|
|
38
|
+
|
|
39
|
+
def _get_storage(self) -> SpeakerDiarizationStorage:
|
|
40
|
+
"""Lazily open the per-capability cache DB under the substrate-injected
|
|
41
|
+
CAPABILITY_DATA_DIR (created at worker spawn)."""
|
|
42
|
+
if getattr(self, "_storage", None) is None:
|
|
43
|
+
db_path = os.path.join(os.environ["CAPABILITY_DATA_DIR"], "speaker_diarization.db")
|
|
44
|
+
self._storage = SpeakerDiarizationStorage(db_path)
|
|
45
|
+
return self._storage
|
|
46
|
+
|
|
47
|
+
def diarize(
|
|
48
|
+
self,
|
|
49
|
+
audio: str, # Path to the source audio to diarize (decoded PCM rendition)
|
|
50
|
+
**kwargs, # Speaker-count hints (cache identity) + provenance + tool options
|
|
51
|
+
) -> SpeakerDiarizationResult: # Time-ordered anonymous speaker turns
|
|
52
|
+
"""Cache-check, invoke the bound tool's pure-compute `diarize`, persist."""
|
|
53
|
+
file_path = str(audio)
|
|
54
|
+
file_hash = hash_file(file_path)
|
|
55
|
+
hints = {k: kwargs[k] for k in self.HINT_KEYS if kwargs.get(k) is not None}
|
|
56
|
+
config_hash = hash_dict_canonical({**self.tool.get_current_config(), **hints})
|
|
57
|
+
|
|
58
|
+
env = get_call_envelope()
|
|
59
|
+
force = bool(env.control.get("force")) if env is not None else False
|
|
60
|
+
|
|
61
|
+
storage = self._get_storage()
|
|
62
|
+
if not force:
|
|
63
|
+
cached = storage.get_cached(file_path, file_hash, config_hash)
|
|
64
|
+
if cached is not None:
|
|
65
|
+
return SpeakerDiarizationResult(
|
|
66
|
+
turns=[SpeakerTurn(**t) for t in (cached.turns or [])],
|
|
67
|
+
metadata=cached.metadata or {},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Pure compute on the tool (cache miss / forced).
|
|
71
|
+
result = self.tool.diarize(audio, **kwargs)
|
|
72
|
+
|
|
73
|
+
# Persist. Content-addressed row; cache key is (file_path, config_hash)
|
|
74
|
+
# with file_hash for content-correctness.
|
|
75
|
+
storage.save_with_logging(
|
|
76
|
+
file_path=file_path, file_hash=file_hash, config_hash=config_hash,
|
|
77
|
+
turns=[t.to_dict() for t in result.turns], metadata=result.metadata,
|
|
78
|
+
)
|
|
79
|
+
return result
|
cjm_speaker_diarization_adapter_interface-0.0.1/cjm_speaker_diarization_adapter_interface/storage.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Standardized SQLite storage for speaker-diarization results with content hashing.
|
|
2
|
+
|
|
3
|
+
Cache shape follows the VAD-adapter precedent: content-addressed rows upserted
|
|
4
|
+
by (file_path, config_hash) with file_hash for content-correctness. Landed
|
|
5
|
+
2026-07-26 — full-source diarization (~46s/hour on cuda, multi-hour sources)
|
|
6
|
+
made re-runs worth caching, unlike the pysbd-precedent no-cache assessment
|
|
7
|
+
this pair was born under."""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import sqlite3
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from cjm_substrate.core.journal_store import SubstrateEventType
|
|
17
|
+
from cjm_substrate.core.wire import record_account
|
|
18
|
+
from cjm_substrate.utils.hashing import hash_file
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# CR-14 follow-up: storage events are substrate-family accounts. The helpers
|
|
22
|
+
# below RECORD them (no-op outside a worker call span); the host journals
|
|
23
|
+
# them off the response header with worker_reported=True.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SpeakerDiarizationRow:
|
|
28
|
+
"""A single row from the speaker_diarization_results table."""
|
|
29
|
+
file_path: str # Path to the analyzed (decoded PCM rendition) audio file
|
|
30
|
+
file_hash: str # Hash of the analyzed file in "algo:hexdigest" format
|
|
31
|
+
config_hash: str # Hash of the diarization config (tool config + speaker-count hints)
|
|
32
|
+
turns: Optional[List[Dict[str, Any]]] = None # Diarized speech turns (serialized SpeakerTurns)
|
|
33
|
+
metadata: Optional[Dict[str, Any]] = None # Diarization metadata
|
|
34
|
+
created_at: Optional[float] = None # Unix timestamp
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SpeakerDiarizationStorage:
|
|
38
|
+
"""Standardized SQLite storage for speaker-diarization results."""
|
|
39
|
+
|
|
40
|
+
SCHEMA = """
|
|
41
|
+
CREATE TABLE IF NOT EXISTS speaker_diarization_results (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
file_path TEXT NOT NULL,
|
|
44
|
+
file_hash TEXT NOT NULL,
|
|
45
|
+
config_hash TEXT NOT NULL,
|
|
46
|
+
turns JSON,
|
|
47
|
+
metadata JSON,
|
|
48
|
+
created_at REAL NOT NULL,
|
|
49
|
+
UNIQUE(file_path, config_hash)
|
|
50
|
+
)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
INDEX = "CREATE INDEX IF NOT EXISTS idx_speaker_diarization_file_path ON speaker_diarization_results(file_path);"
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
db_path: str # Absolute path to the SQLite database file
|
|
58
|
+
):
|
|
59
|
+
"""Initialize storage and create table if needed."""
|
|
60
|
+
self.db_path = db_path
|
|
61
|
+
with sqlite3.connect(self.db_path) as con:
|
|
62
|
+
con.execute(self.SCHEMA)
|
|
63
|
+
con.execute(self.INDEX)
|
|
64
|
+
|
|
65
|
+
def save(
|
|
66
|
+
self,
|
|
67
|
+
file_path: str, # Path to the analyzed audio file
|
|
68
|
+
file_hash: str, # Hash of the analyzed file in "algo:hexdigest" format
|
|
69
|
+
config_hash: str, # Hash of the diarization config (tool config + hints)
|
|
70
|
+
turns: Optional[List[Dict[str, Any]]] = None, # Diarized speech turns
|
|
71
|
+
metadata: Optional[Dict[str, Any]] = None # Diarization metadata
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Save or replace a diarization result (upsert by file_path + config_hash)."""
|
|
74
|
+
with sqlite3.connect(self.db_path) as con:
|
|
75
|
+
con.execute(
|
|
76
|
+
"""INSERT OR REPLACE INTO speaker_diarization_results
|
|
77
|
+
(file_path, file_hash, config_hash, turns, metadata, created_at)
|
|
78
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
79
|
+
(
|
|
80
|
+
file_path,
|
|
81
|
+
file_hash,
|
|
82
|
+
config_hash,
|
|
83
|
+
json.dumps(turns) if turns else None,
|
|
84
|
+
json.dumps(metadata) if metadata else None,
|
|
85
|
+
time.time()
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def save_with_logging(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
file_path: str, # Path to the analyzed audio file
|
|
93
|
+
file_hash: str, # Hash of the analyzed file in "algo:hexdigest" format
|
|
94
|
+
config_hash: str, # Hash of the diarization config (tool config + hints)
|
|
95
|
+
turns: Optional[List[Dict[str, Any]]] = None, # Diarized speech turns
|
|
96
|
+
metadata: Optional[Dict[str, Any]] = None, # Diarization metadata
|
|
97
|
+
logger: Optional[logging.Logger] = None # Optional logger for success/failure messages
|
|
98
|
+
) -> bool: # True if saved; False if the save failed (error logged, not raised)
|
|
99
|
+
"""Save a result, logging success/failure. Failures are logged and swallowed (returns False).
|
|
100
|
+
|
|
101
|
+
CR-14 follow-up: records a RESULT_SAVED account either way (ok flag +
|
|
102
|
+
file/config references — the journal never carries content) so saves AND
|
|
103
|
+
swallowed save-failures become auditable journal rows."""
|
|
104
|
+
try:
|
|
105
|
+
self.save(
|
|
106
|
+
file_path=file_path,
|
|
107
|
+
file_hash=file_hash,
|
|
108
|
+
config_hash=config_hash,
|
|
109
|
+
turns=turns,
|
|
110
|
+
metadata=metadata,
|
|
111
|
+
)
|
|
112
|
+
record_account(SubstrateEventType.RESULT_SAVED.value, {
|
|
113
|
+
"ok": True, "file_path": file_path, "file_hash": file_hash,
|
|
114
|
+
"config_hash": config_hash,
|
|
115
|
+
})
|
|
116
|
+
if logger:
|
|
117
|
+
logger.info(f"Saved diarization result to DB ({file_path})")
|
|
118
|
+
return True
|
|
119
|
+
except Exception as e:
|
|
120
|
+
record_account(SubstrateEventType.RESULT_SAVED.value, {
|
|
121
|
+
"ok": False, "file_path": file_path, "file_hash": file_hash,
|
|
122
|
+
"config_hash": config_hash, "error": str(e),
|
|
123
|
+
})
|
|
124
|
+
if logger:
|
|
125
|
+
logger.error(f"Failed to save to DB: {e}")
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def get_cached(
|
|
129
|
+
self,
|
|
130
|
+
file_path: str, # Path to the audio file
|
|
131
|
+
file_hash: str, # Content hash of the file (cache miss if the file changed)
|
|
132
|
+
config_hash: str # Config hash to match
|
|
133
|
+
) -> Optional[SpeakerDiarizationRow]: # Cached row or None
|
|
134
|
+
"""Retrieve a content-correct cached diarization result.
|
|
135
|
+
|
|
136
|
+
Matches on file_path + file_hash + config_hash, so a changed file (new
|
|
137
|
+
file_hash) misses the cache even though a stale row may still exist at the
|
|
138
|
+
same (file_path, config_hash) — the next save() replaces it.
|
|
139
|
+
|
|
140
|
+
CR-14 follow-up: a hit records a CACHE_HIT account (the cache-serving
|
|
141
|
+
decision is an account-of-action)."""
|
|
142
|
+
with sqlite3.connect(self.db_path) as con:
|
|
143
|
+
cur = con.execute(
|
|
144
|
+
"""SELECT file_path, file_hash, config_hash, turns, metadata, created_at
|
|
145
|
+
FROM speaker_diarization_results
|
|
146
|
+
WHERE file_path = ? AND file_hash = ? AND config_hash = ?""",
|
|
147
|
+
(file_path, file_hash, config_hash)
|
|
148
|
+
)
|
|
149
|
+
row = cur.fetchone()
|
|
150
|
+
if not row:
|
|
151
|
+
return None
|
|
152
|
+
record_account(SubstrateEventType.CACHE_HIT.value, {
|
|
153
|
+
"file_path": file_path, "file_hash": file_hash,
|
|
154
|
+
"config_hash": config_hash,
|
|
155
|
+
})
|
|
156
|
+
return SpeakerDiarizationRow(
|
|
157
|
+
file_path=row[0],
|
|
158
|
+
file_hash=row[1],
|
|
159
|
+
config_hash=row[2],
|
|
160
|
+
turns=json.loads(row[3]) if row[3] else None,
|
|
161
|
+
metadata=json.loads(row[4]) if row[4] else None,
|
|
162
|
+
created_at=row[5]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def list_jobs(
|
|
166
|
+
self,
|
|
167
|
+
limit: int = 100 # Maximum number of rows to return
|
|
168
|
+
) -> List[SpeakerDiarizationRow]: # List of diarization rows
|
|
169
|
+
"""List diarization results ordered by creation time (newest first)."""
|
|
170
|
+
results = []
|
|
171
|
+
with sqlite3.connect(self.db_path) as con:
|
|
172
|
+
cur = con.execute(
|
|
173
|
+
"""SELECT file_path, file_hash, config_hash, turns, metadata, created_at
|
|
174
|
+
FROM speaker_diarization_results ORDER BY created_at DESC LIMIT ?""",
|
|
175
|
+
(limit,)
|
|
176
|
+
)
|
|
177
|
+
for row in cur:
|
|
178
|
+
results.append(SpeakerDiarizationRow(
|
|
179
|
+
file_path=row[0],
|
|
180
|
+
file_hash=row[1],
|
|
181
|
+
config_hash=row[2],
|
|
182
|
+
turns=json.loads(row[3]) if row[3] else None,
|
|
183
|
+
metadata=json.loads(row[4]) if row[4] else None,
|
|
184
|
+
created_at=row[5]
|
|
185
|
+
))
|
|
186
|
+
return results
|
|
187
|
+
|
|
188
|
+
def verify_file(
|
|
189
|
+
self,
|
|
190
|
+
file_path: str, # Path to the audio file
|
|
191
|
+
config_hash: str # Config hash to look up
|
|
192
|
+
) -> Optional[bool]: # True if file matches, False if changed, None if not found
|
|
193
|
+
"""Verify the analyzed file still matches the hash stored for (file_path, config_hash)."""
|
|
194
|
+
with sqlite3.connect(self.db_path) as con:
|
|
195
|
+
cur = con.execute(
|
|
196
|
+
"""SELECT file_hash FROM speaker_diarization_results
|
|
197
|
+
WHERE file_path = ? AND config_hash = ?""",
|
|
198
|
+
(file_path, config_hash)
|
|
199
|
+
)
|
|
200
|
+
row = cur.fetchone()
|
|
201
|
+
if not row:
|
|
202
|
+
return None
|
|
203
|
+
return hash_file(file_path) == row[0]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cjm-speaker-diarization-adapter-interface
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Typed speaker-diarization task-adapter interface — SpeakerDiarizationAdapter ABC + GenericSpeakerDiarizationAdapter (thin pass-through around a diarization tool) and the SpeakerDiarizationToolProtocol. The SpeakerDiarizationResult data noun lives in cjm-capability-primitives; turns are ANONYMOUS clusters — identity is the correction TUI's assignment lane, never this interface's job.
|
|
5
|
+
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/cj-mills/cjm-speaker-diarization-adapter-interface
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: cjm-substrate>=0.0.58
|
|
14
|
+
Requires-Dist: cjm-capability-primitives>=0.0.12
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# cjm-speaker-diarization-adapter-interface
|
|
18
|
+
|
|
19
|
+
<!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
|
|
20
|
+
|
|
21
|
+
Adapter interface for speaker-diarization capabilities — the speaker_diarization task-channel contract (audio → typed SpeakerTurn/SpeakerDiarizationResult) that capability libs implement and workflow cores consume; the generic adapter owns the SQLite cache bookends (content-addressed, speaker-count hints fold into the config hash).
|
|
22
|
+
|
|
23
|
+
## Modules
|
|
24
|
+
|
|
25
|
+
- **`cjm_speaker_diarization_adapter_interface.__init__`**
|
|
26
|
+
- **`cjm_speaker_diarization_adapter_interface.adapter`** — The typed speaker-diarization task contract — the `SpeakerDiarizationAdapter` ABC + the `SpeakerDiarizationToolProtocol` structural contract (capability-unit Option C; session-D DECs 18d7de80 + d6df3a8e).
|
|
27
|
+
- **`cjm_speaker_diarization_adapter_interface.generic`** — The generic (tool-agnostic) speaker-diarization adapter — cache-check, invoke the bound tool's pure-compute `diarize`, persist. Reused across every tool capability satisfying `SpeakerDiarizationToolProtocol`, exactly as `GenericVADAdapter` is reused across VAD tools.
|
|
28
|
+
- **`cjm_speaker_diarization_adapter_interface.storage`** — Standardized SQLite storage for speaker-diarization results with content hashing.
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### `cjm_speaker_diarization_adapter_interface.adapter`
|
|
33
|
+
|
|
34
|
+
- `SpeakerDiarizationAdapter` _class_ — Typed speaker-diarization task adapter: source audio path in,
|
|
35
|
+
- `SpeakerDiarizationToolProtocol` _class_ — Structural contract for speaker-diarization tool capabilities.
|
|
36
|
+
|
|
37
|
+
### `cjm_speaker_diarization_adapter_interface.generic`
|
|
38
|
+
|
|
39
|
+
- `GenericSpeakerDiarizationAdapter` _class_ — Generic speaker-diarization adapter: cache-check -> pure-compute tool -> persist.
|
|
40
|
+
|
|
41
|
+
### `cjm_speaker_diarization_adapter_interface.storage`
|
|
42
|
+
|
|
43
|
+
- `SpeakerDiarizationRow` _class_ — A single row from the speaker_diarization_results table.
|
|
44
|
+
- `SpeakerDiarizationStorage` _class_ — Standardized SQLite storage for speaker-diarization results.
|
|
45
|
+
|
|
46
|
+
## Dependencies
|
|
47
|
+
|
|
48
|
+
**Depends on:** `cjm-capability-primitives`, `cjm-substrate`
|
|
49
|
+
**Used by:** `cjm-capability-pyannote`
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
cjm_speaker_diarization_adapter_interface/__init__.py
|
|
5
|
+
cjm_speaker_diarization_adapter_interface/adapter.py
|
|
6
|
+
cjm_speaker_diarization_adapter_interface/generic.py
|
|
7
|
+
cjm_speaker_diarization_adapter_interface/storage.py
|
|
8
|
+
cjm_speaker_diarization_adapter_interface.egg-info/PKG-INFO
|
|
9
|
+
cjm_speaker_diarization_adapter_interface.egg-info/SOURCES.txt
|
|
10
|
+
cjm_speaker_diarization_adapter_interface.egg-info/dependency_links.txt
|
|
11
|
+
cjm_speaker_diarization_adapter_interface.egg-info/requires.txt
|
|
12
|
+
cjm_speaker_diarization_adapter_interface.egg-info/top_level.txt
|
|
13
|
+
tests/test_adapter.py
|
|
14
|
+
tests/test_storage.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cjm_speaker_diarization_adapter_interface
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cjm-speaker-diarization-adapter-interface"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Typed speaker-diarization task-adapter interface — SpeakerDiarizationAdapter ABC + GenericSpeakerDiarizationAdapter (thin pass-through around a diarization tool) and the SpeakerDiarizationToolProtocol. The SpeakerDiarizationResult data noun lives in cjm-capability-primitives; turns are ANONYMOUS clusters — identity is the correction TUI's assignment lane, never this interface's job."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
authors = [{name = "Christian J. Mills", email = "9126128+cj-mills@users.noreply.github.com"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"cjm-substrate>=0.0.58",
|
|
19
|
+
"cjm-capability-primitives>=0.0.12",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Repository = "https://github.com/cj-mills/cjm-speaker-diarization-adapter-interface"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.dynamic]
|
|
26
|
+
version = {attr = "cjm_speaker_diarization_adapter_interface.__version__"}
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
include = ["cjm_speaker_diarization_adapter_interface*"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Tests for the speaker-diarization adapter interface — protocol conformance + the generic pass-through."""
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from cjm_capability_primitives.speaker_diarization import (SpeakerDiarizationResult,
|
|
5
|
+
SpeakerTurn)
|
|
6
|
+
from cjm_speaker_diarization_adapter_interface.adapter import (
|
|
7
|
+
SpeakerDiarizationAdapter, SpeakerDiarizationToolProtocol)
|
|
8
|
+
from cjm_speaker_diarization_adapter_interface.generic import (
|
|
9
|
+
GenericSpeakerDiarizationAdapter)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FakeDiarizerTool:
|
|
13
|
+
"""Minimal tool satisfying the structural protocol: two fixed turns,
|
|
14
|
+
honoring a num_speakers=1 hint by merging them."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.calls = 0
|
|
18
|
+
self.last_kwargs: Dict[str, Any] = {}
|
|
19
|
+
|
|
20
|
+
def diarize(self, audio: str, **kwargs) -> SpeakerDiarizationResult:
|
|
21
|
+
self.calls += 1
|
|
22
|
+
self.last_kwargs = dict(kwargs)
|
|
23
|
+
if kwargs.get("num_speakers") == 1:
|
|
24
|
+
turns = [SpeakerTurn(start=0.2, end=3.9, speaker="SPEAKER_00")]
|
|
25
|
+
else:
|
|
26
|
+
turns = [SpeakerTurn(start=0.2, end=1.5, speaker="SPEAKER_00"),
|
|
27
|
+
SpeakerTurn(start=1.8, end=3.9, speaker="SPEAKER_01")]
|
|
28
|
+
return SpeakerDiarizationResult(
|
|
29
|
+
turns=turns, metadata={"speaker_count": len({t.speaker for t in turns}),
|
|
30
|
+
"audio": audio})
|
|
31
|
+
|
|
32
|
+
def get_current_config(self) -> Dict[str, Any]:
|
|
33
|
+
return {"model_id": "fake-diarizer"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_fake_tool_satisfies_protocol():
|
|
37
|
+
assert isinstance(FakeDiarizerTool(), SpeakerDiarizationToolProtocol)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_adapter_declares_task_contract():
|
|
41
|
+
assert SpeakerDiarizationAdapter.task_name == "speaker_diarization"
|
|
42
|
+
assert (SpeakerDiarizationAdapter.required_tool_protocol
|
|
43
|
+
is SpeakerDiarizationToolProtocol)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_generic_adapter_passes_through_with_hints(tmp_path, monkeypatch):
|
|
47
|
+
monkeypatch.setenv("CAPABILITY_DATA_DIR", str(tmp_path))
|
|
48
|
+
audio = tmp_path / "source.wav"
|
|
49
|
+
audio.write_bytes(b"fake-pcm-bytes")
|
|
50
|
+
tool = FakeDiarizerTool()
|
|
51
|
+
adapter = GenericSpeakerDiarizationAdapter(tool)
|
|
52
|
+
result = adapter.diarize(str(audio))
|
|
53
|
+
assert tool.calls == 1
|
|
54
|
+
assert [t.speaker for t in result.turns] == ["SPEAKER_00", "SPEAKER_01"]
|
|
55
|
+
assert result.metadata["audio"] == str(audio)
|
|
56
|
+
# speaker-count hints ride kwargs untouched — and fold into the cache
|
|
57
|
+
# key, so the hinted call is a MISS, not a replay of the unhinted row
|
|
58
|
+
hinted = adapter.diarize(str(audio), num_speakers=1)
|
|
59
|
+
assert tool.calls == 2
|
|
60
|
+
assert tool.last_kwargs == {"num_speakers": 1}
|
|
61
|
+
assert len(hinted.turns) == 1 and hinted.metadata["speaker_count"] == 1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_generic_adapter_cache_bookends(tmp_path, monkeypatch):
|
|
65
|
+
monkeypatch.setenv("CAPABILITY_DATA_DIR", str(tmp_path))
|
|
66
|
+
audio = tmp_path / "source.wav"
|
|
67
|
+
audio.write_bytes(b"fake-pcm-bytes")
|
|
68
|
+
tool = FakeDiarizerTool()
|
|
69
|
+
adapter = GenericSpeakerDiarizationAdapter(tool)
|
|
70
|
+
first = adapter.diarize(str(audio))
|
|
71
|
+
assert tool.calls == 1
|
|
72
|
+
# same audio + same config -> served from cache, tool NOT re-invoked;
|
|
73
|
+
# turns come back typed
|
|
74
|
+
replay = adapter.diarize(str(audio))
|
|
75
|
+
assert tool.calls == 1
|
|
76
|
+
assert isinstance(replay.turns[0], SpeakerTurn)
|
|
77
|
+
assert ([(t.start, t.end, t.speaker) for t in replay.turns]
|
|
78
|
+
== [(t.start, t.end, t.speaker) for t in first.turns])
|
|
79
|
+
# a hinted call is a DIFFERENT cache row; replaying the same hint hits it
|
|
80
|
+
adapter.diarize(str(audio), num_speakers=1)
|
|
81
|
+
assert tool.calls == 2
|
|
82
|
+
adapter.diarize(str(audio), num_speakers=1)
|
|
83
|
+
assert tool.calls == 2
|
|
84
|
+
# content change at the same path -> new file_hash -> miss
|
|
85
|
+
audio.write_bytes(b"different-pcm-bytes")
|
|
86
|
+
adapter.diarize(str(audio))
|
|
87
|
+
assert tool.calls == 3
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tests for the speaker-diarization storage layer — content-correct cache semantics on the VAD-adapter precedent."""
|
|
2
|
+
|
|
3
|
+
from cjm_substrate.utils.hashing import hash_file
|
|
4
|
+
from cjm_speaker_diarization_adapter_interface.storage import (
|
|
5
|
+
SpeakerDiarizationRow, SpeakerDiarizationStorage)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _seeded_storage(tmp_path):
|
|
9
|
+
"""Storage with one saved result for a real (tiny) audio file."""
|
|
10
|
+
storage = SpeakerDiarizationStorage(str(tmp_path / "speaker_diarization.db"))
|
|
11
|
+
audio = tmp_path / "a.wav"
|
|
12
|
+
audio.write_bytes(b"pcm-one")
|
|
13
|
+
turns = [{"start": 0.2, "end": 1.5, "speaker": "SPEAKER_00", "confidence": None},
|
|
14
|
+
{"start": 1.8, "end": 3.9, "speaker": "SPEAKER_01", "confidence": 0.9}]
|
|
15
|
+
storage.save(file_path=str(audio), file_hash="sha256:aaa", config_hash="cfg1",
|
|
16
|
+
turns=turns, metadata={"speaker_count": 2})
|
|
17
|
+
return storage, str(audio), turns
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_save_get_cached_and_content_correctness(tmp_path):
|
|
21
|
+
storage, audio, turns = _seeded_storage(tmp_path)
|
|
22
|
+
row = storage.get_cached(audio, "sha256:aaa", "cfg1")
|
|
23
|
+
assert isinstance(row, SpeakerDiarizationRow)
|
|
24
|
+
assert row.turns == turns
|
|
25
|
+
assert row.metadata == {"speaker_count": 2}
|
|
26
|
+
assert row.created_at is not None
|
|
27
|
+
# changed content (new file_hash) misses even though a row exists at
|
|
28
|
+
# the same (file_path, config_hash)
|
|
29
|
+
assert storage.get_cached(audio, "sha256:bbb", "cfg1") is None
|
|
30
|
+
# unknown config misses
|
|
31
|
+
assert storage.get_cached(audio, "sha256:aaa", "cfg2") is None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_insert_or_replace_on_cache_key(tmp_path):
|
|
35
|
+
storage, audio, _ = _seeded_storage(tmp_path)
|
|
36
|
+
storage.save(file_path=audio, file_hash="sha256:bbb", config_hash="cfg1",
|
|
37
|
+
turns=[{"start": 0.0, "end": 9.9, "speaker": "SPEAKER_00",
|
|
38
|
+
"confidence": None}])
|
|
39
|
+
# upsert replaced the (file_path, cfg1) row — no duplicate
|
|
40
|
+
assert len(storage.list_jobs()) == 1
|
|
41
|
+
assert storage.get_cached(audio, "sha256:aaa", "cfg1") is None
|
|
42
|
+
row = storage.get_cached(audio, "sha256:bbb", "cfg1")
|
|
43
|
+
assert row.turns[0]["end"] == 9.9
|
|
44
|
+
assert row.metadata is None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_save_with_logging_success_and_swallowed_failure(tmp_path):
|
|
48
|
+
storage, audio, turns = _seeded_storage(tmp_path)
|
|
49
|
+
assert storage.save_with_logging(file_path=audio, file_hash="sha256:aaa",
|
|
50
|
+
config_hash="cfg2", turns=turns) is True
|
|
51
|
+
|
|
52
|
+
def _boom(*args, **kwargs):
|
|
53
|
+
raise RuntimeError("disk full")
|
|
54
|
+
|
|
55
|
+
storage.save = _boom # instance attribute shadows the method
|
|
56
|
+
assert storage.save_with_logging(file_path=audio, file_hash="x",
|
|
57
|
+
config_hash="cfg3") is False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_list_jobs_and_verify_file(tmp_path):
|
|
61
|
+
storage, audio, _ = _seeded_storage(tmp_path)
|
|
62
|
+
storage.save(file_path=audio, file_hash="h", config_hash="cfg2")
|
|
63
|
+
rows = storage.list_jobs()
|
|
64
|
+
assert len(rows) == 2
|
|
65
|
+
assert rows[0].created_at >= rows[1].created_at # newest first
|
|
66
|
+
# verify_file compares the stored hash against the file's REAL hash
|
|
67
|
+
storage.save(file_path=audio, file_hash=hash_file(audio),
|
|
68
|
+
config_hash="cfg-real")
|
|
69
|
+
assert storage.verify_file(audio, "cfg-real") is True
|
|
70
|
+
assert storage.verify_file(audio, "cfg1") is False # seeded fake hash
|
|
71
|
+
assert storage.verify_file(audio, "nope") is None
|