teams-transcripts 0.5.0__py3-none-any.whl

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.
Files changed (42) hide show
  1. legacy/__init__.py +0 -0
  2. legacy/transcript_download.py +3969 -0
  3. teams_transcripts/__init__.py +1 -0
  4. teams_transcripts/__main__.py +288 -0
  5. teams_transcripts/adapters/__init__.py +1 -0
  6. teams_transcripts/adapters/cdp/__init__.py +1 -0
  7. teams_transcripts/adapters/cdp/browser.py +196 -0
  8. teams_transcripts/adapters/cdp/connection.py +168 -0
  9. teams_transcripts/adapters/cdp/helpers.py +449 -0
  10. teams_transcripts/adapters/cdp/teams.py +463 -0
  11. teams_transcripts/adapters/vertex.py +274 -0
  12. teams_transcripts/domain/__init__.py +1 -0
  13. teams_transcripts/domain/chat.py +115 -0
  14. teams_transcripts/domain/html.py +126 -0
  15. teams_transcripts/domain/mcps.py +343 -0
  16. teams_transcripts/domain/models.py +531 -0
  17. teams_transcripts/domain/participants.py +292 -0
  18. teams_transcripts/domain/scoring.py +106 -0
  19. teams_transcripts/domain/speakers.py +130 -0
  20. teams_transcripts/domain/summary.py +266 -0
  21. teams_transcripts/domain/timestamps.py +350 -0
  22. teams_transcripts/domain/vtt.py +444 -0
  23. teams_transcripts/infrastructure/__init__.py +1 -0
  24. teams_transcripts/infrastructure/config.py +987 -0
  25. teams_transcripts/infrastructure/errors.py +120 -0
  26. teams_transcripts/infrastructure/logging.py +69 -0
  27. teams_transcripts/infrastructure/signals.py +143 -0
  28. teams_transcripts/mcp_server.py +818 -0
  29. teams_transcripts/ports/__init__.py +1 -0
  30. teams_transcripts/ports/agent.py +58 -0
  31. teams_transcripts/ports/browser.py +126 -0
  32. teams_transcripts/services/__init__.py +1 -0
  33. teams_transcripts/services/downloader.py +1283 -0
  34. teams_transcripts/services/extraction.py +1603 -0
  35. teams_transcripts/services/listing.py +826 -0
  36. teams_transcripts/services/navigation.py +1721 -0
  37. teams_transcripts/services/output.py +84 -0
  38. teams_transcripts/services/tenant.py +684 -0
  39. teams_transcripts-0.5.0.dist-info/METADATA +1438 -0
  40. teams_transcripts-0.5.0.dist-info/RECORD +42 -0
  41. teams_transcripts-0.5.0.dist-info/WHEEL +4 -0
  42. teams_transcripts-0.5.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,531 @@
1
+ """Domain models -- immutable data classes for the transcript pipeline.
2
+
3
+ All models are plain data holders with no I/O, no network access, and no
4
+ dependency on infrastructure or adapters. They form the shared vocabulary
5
+ across the hexagonal architecture.
6
+
7
+ Schema version: 2.2.0
8
+ Created: 2026-04-25
9
+ Last updated: 2026-05-01 -- added DownloadInput Pydantic validation model
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import typing
15
+ from dataclasses import dataclass, field
16
+ from uuid import UUID
17
+
18
+ from pydantic import BaseModel, ConfigDict, model_validator
19
+
20
+ if typing.TYPE_CHECKING:
21
+ import datetime as dt
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Transcript domain objects
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class TranscriptCue:
30
+ """A structured transcript cue for JSON output.
31
+
32
+ Represents a single cue parsed from VTT content, with both formatted
33
+ timestamp strings and numeric seconds for programmatic consumption.
34
+
35
+ Fields:
36
+ id: Sequential cue identifier (1-based).
37
+ start: Start timestamp as VTT string (``HH:MM:SS.mmm``).
38
+ end: End timestamp as VTT string (``HH:MM:SS.mmm``).
39
+ start_sec: Start time in seconds (float).
40
+ end_sec: End time in seconds (float).
41
+ speaker: Speaker display name, or ``None`` when no ``<v>`` tag
42
+ is present on the cue.
43
+ text: Plain text content (voice tags stripped).
44
+ """
45
+
46
+ id: int
47
+ start: str
48
+ end: str
49
+ start_sec: float
50
+ end_sec: float
51
+ speaker: str | None
52
+ text: str
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class Segment:
57
+ """A single transcript segment (cue) extracted from the Teams DOM.
58
+
59
+ Represents one speaker turn with timing information. Used as the
60
+ intermediate representation between DOM extraction and VTT generation.
61
+
62
+ Fields:
63
+ speaker: Display name of the speaker (may contain HTML entities
64
+ in raw form; cleaned before VTT output).
65
+ text: Spoken text content (may contain HTML in raw form).
66
+ start_sec: Start time in seconds from meeting start. May be
67
+ ``None`` when the DOM element lacks timing data (forward-fill
68
+ is applied later).
69
+ end_sec: End time in seconds from meeting start. May be ``None``
70
+ (back-fill or default offset is applied later).
71
+ """
72
+
73
+ speaker: str
74
+ text: str
75
+ start_sec: float | None = None
76
+ end_sec: float | None = None
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class RosterMember:
81
+ """A member from the meeting roster popover.
82
+
83
+ Fields:
84
+ name: Display name (cleaned of suffixes like "(Guest)").
85
+ email: Email address. Empty string when unavailable.
86
+ object_id: Azure AD object ID (GUID). Empty string when unavailable.
87
+ role: Either ``"organiser"`` or ``"member"``.
88
+ rsvp_status: Calendar response status. One of ``"accepted"``,
89
+ ``"tentative"``, ``"declined"``, ``"none"`` (no response),
90
+ or ``""`` (unknown / not available).
91
+ """
92
+
93
+ name: str
94
+ email: str = ""
95
+ object_id: str = ""
96
+ role: str = "member"
97
+ rsvp_status: str = ""
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class MeetingParticipants:
102
+ """Categorised meeting participant lists.
103
+
104
+ Combines data from multiple extraction sources (participant popover,
105
+ Recap Speakers tab, Event/Call ``<partlist>`` XML) into four
106
+ disjoint-ish categories:
107
+
108
+ - **organiser**: the meeting creator (usually 1, rarely 0).
109
+ - **invited**: everyone on the meeting chat thread (full membership).
110
+ - **speakers**: those who spoke during the meeting (from Recap).
111
+ - **attendees**: those who joined the call (from the Event/Call
112
+ partlist -- may be incomplete due to Teams truncation).
113
+
114
+ Note: ``speakers`` and ``attendees`` are subsets of ``invited``.
115
+ A person may appear in both ``speakers`` and ``attendees``.
116
+
117
+ Fields:
118
+ organiser: The meeting organiser(s). Usually a single-element
119
+ list; empty when the organiser cannot be identified.
120
+ invited: All members of the meeting chat thread, sorted by name.
121
+ speakers: Members who spoke, sorted by name.
122
+ attendees: Members who joined the call, sorted by name.
123
+
124
+ Schema version: 1.1.0
125
+ """
126
+
127
+ organiser: list[RosterMember] = field(default_factory=list)
128
+ invited: list[RosterMember] = field(default_factory=list)
129
+ speakers: list[RosterMember] = field(default_factory=list)
130
+ attendees: list[RosterMember] = field(default_factory=list)
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class Reaction:
135
+ """A reaction on a chat message.
136
+
137
+ Fields:
138
+ type: Emotion key (e.g. ``"like"``, ``"heart"``).
139
+ users: List of resolved display names. May be empty when
140
+ only a summary count is available.
141
+ """
142
+
143
+ type: str
144
+ users: list[str] = field(default_factory=list)
145
+
146
+
147
+ @dataclass(frozen=True)
148
+ class ChatMessage:
149
+ """A chat message extracted from the meeting thread.
150
+
151
+ Fields:
152
+ timestamp: Local ISO 8601 datetime with timezone offset
153
+ (e.g. ``"2026-04-22T11:15:48+01:00"``).
154
+ author: Resolved display name of the message author.
155
+ text: Plain-text content (HTML stripped).
156
+ reactions: List of reactions, or ``None`` when no reactions exist.
157
+ """
158
+
159
+ timestamp: str
160
+ author: str
161
+ text: str
162
+ reactions: list[Reaction] | None = None
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # Verification result
167
+ # ---------------------------------------------------------------------------
168
+
169
+
170
+ @dataclass(frozen=True)
171
+ class VerificationResult:
172
+ """Result of verifying a VTT file's content.
173
+
174
+ Fields:
175
+ valid: Whether the content passed all checks.
176
+ cue_count: Number of cues found (0 when invalid).
177
+ speakers: Mapping of speaker name to cue count. Empty when invalid.
178
+ error: Human-readable error description. Empty string when valid.
179
+ """
180
+
181
+ valid: bool
182
+ cue_count: int = 0
183
+ speakers: dict[str, int] = field(default_factory=dict)
184
+ error: str = ""
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Result summary
189
+ # ---------------------------------------------------------------------------
190
+
191
+
192
+ @dataclass
193
+ class ResultSummary:
194
+ """Structured result summary for JSON output and dry-run display.
195
+
196
+ Mutable because fields are populated incrementally during the
197
+ pipeline (metadata first, then verification data, then chat count).
198
+
199
+ Fields:
200
+ status: Always ``"ok"`` for successful runs.
201
+ meeting: Meeting name as provided by the user.
202
+ tenant: Tenant domain name (e.g. ``"abccorp.com"``), or ``None``
203
+ when ``--tenant`` was not specified. Included in JSON output
204
+ only when not ``None``.
205
+ start: ISO datetime or date string for the meeting start.
206
+ end: ISO datetime string for the meeting end, or ``None``.
207
+ members: Sorted list of member display names, or ``None``.
208
+ organiser: Single organiser name, list of names, or ``None``.
209
+ parts: Number of transcript parts detected.
210
+ cues: Total cue count from verification, or ``None`` before download.
211
+ speakers: Mapping of speaker name to cue count, or ``None``.
212
+ file_size: Output file size in bytes, or ``None`` before download.
213
+ chat_messages: Count of extracted chat messages, or ``None``.
214
+ dry_run: ``True`` only for dry-run output.
215
+ file: Output file path (when writing to file), or ``None``.
216
+ transcript: Structured cue list for JSON output, or ``None``.
217
+ """
218
+
219
+ status: str = "ok"
220
+ meeting: str = ""
221
+ tenant: str | None = None
222
+ start: str = ""
223
+ end: str | None = None
224
+ members: list[str] | None = None
225
+ organiser: str | list[str] | None = None
226
+ parts: int = 1
227
+ cues: int | None = None
228
+ speakers: dict[str, int] | None = None
229
+ file_size: int | None = None
230
+ chat_messages: int | None = None
231
+ dry_run: bool = False
232
+ file: str | None = None
233
+ transcript: list[dict] | None = None
234
+
235
+ def to_dict(self) -> dict:
236
+ """Convert to a plain dict, omitting ``None`` values.
237
+
238
+ Matches the JSON output schema: keys with ``None`` values are
239
+ excluded, and ``dry_run`` is only included when ``True``.
240
+ """
241
+ d: dict[str, object] = {"status": self.status}
242
+ d["meeting"] = self.meeting
243
+ if self.tenant is not None:
244
+ d["tenant"] = self.tenant
245
+ d["start"] = self.start
246
+ if self.end is not None:
247
+ d["end"] = self.end
248
+ if self.members is not None:
249
+ d["members"] = self.members
250
+ if self.organiser is not None:
251
+ d["organiser"] = self.organiser
252
+ d["parts"] = self.parts
253
+ if self.cues is not None:
254
+ d["cues"] = self.cues
255
+ if self.speakers is not None:
256
+ d["speakers"] = self.speakers
257
+ if self.file_size is not None:
258
+ d["file_size"] = self.file_size
259
+ if self.chat_messages is not None:
260
+ d["chat_messages"] = self.chat_messages
261
+ if self.dry_run:
262
+ d["dry_run"] = True
263
+ if self.file is not None:
264
+ d["file"] = self.file
265
+ if self.transcript is not None:
266
+ d["transcript"] = self.transcript
267
+ return d
268
+
269
+
270
+ # ---------------------------------------------------------------------------
271
+ # Meeting configuration
272
+ # ---------------------------------------------------------------------------
273
+
274
+
275
+ class MeetingConfig(BaseModel):
276
+ """Resolved meeting configuration from CLI args and environment.
277
+
278
+ Immutable once constructed by ``infrastructure.config.resolve_config()``.
279
+ Passed through the pipeline as the single source of configuration truth.
280
+
281
+ This is a frozen Pydantic model. To create a modified copy (e.g. after
282
+ resolving ``--meeting-id`` via MCPS), use ``model_copy(update={...})``.
283
+
284
+ Fields:
285
+ meeting: Meeting name search string (``--meeting``). Empty string
286
+ when ``--meeting-id`` is used instead.
287
+ meeting_id: Meeting UUID from ``--meeting-id``, or ``None`` when
288
+ ``--meeting`` is used instead. When set, the pipeline resolves the
289
+ meeting subject via MCPS before searching, and verifies the
290
+ thread_id after navigation.
291
+ call_id: Call identifier from ``--call-id``, or ``None`` when not
292
+ specified. When set, the pipeline searches MCPS for the exact
293
+ call_id match and resolves subject, date, and thread_id. This
294
+ is the preferred identifier for recurring meetings as it uniquely
295
+ identifies a specific occurrence.
296
+ date: Target date as ``YYYY-MM-DD`` string.
297
+ time: Target time as ``HH:MM`` string, or ``None`` for date-only.
298
+ output: Output file path. ``None`` during ``--dry-run``.
299
+ output_to_stdout: ``True`` when no ``--output`` was given (content
300
+ goes to stdout at the end).
301
+ dry_run: ``True`` for ``--dry-run`` mode.
302
+ port: CDP debugging port number.
303
+ gcp_project: Google Cloud project ID for Vertex AI.
304
+ gcp_region: Google Cloud region for Vertex AI.
305
+ model: LLM model identifier.
306
+ adc: Path to application default credentials JSON.
307
+ output_format: ``"vtt"`` or ``"json"``.
308
+ tenant: Tenant domain name for verification (e.g. ``"abccorp.com"``),
309
+ or ``None`` to skip tenant checking.
310
+ quiet: Suppress diagnostic output.
311
+ verbose: Enable verbose diagnostic output.
312
+ include_transcript: Include transcript cues in output.
313
+ include_chat: Include chat messages in output.
314
+ include_metadata: Include participant metadata in output.
315
+ force_restart: Allow Teams restart even when call state is active or unknown.
316
+ """
317
+
318
+ model_config = ConfigDict(frozen=True)
319
+
320
+ meeting: str
321
+ date: str
322
+ time: str | None
323
+ output: str | None
324
+ output_to_stdout: bool
325
+ dry_run: bool
326
+ port: int
327
+ gcp_project: str
328
+ gcp_region: str
329
+ model: str
330
+ adc: str
331
+ output_format: str
332
+ meeting_id: str | None = None
333
+ call_id: str | None = None
334
+ tenant: str | None = None
335
+ quiet: bool = False
336
+ verbose: bool = False
337
+ include_transcript: bool = True
338
+ include_chat: bool = True
339
+ include_metadata: bool = True
340
+ force_restart: bool = False
341
+
342
+ @property
343
+ def format_json(self) -> bool:
344
+ """Whether output format is JSON."""
345
+ return self.output_format == "json"
346
+
347
+ @property
348
+ def all_components(self) -> bool:
349
+ """Whether all three output components are included."""
350
+ return self.include_transcript and self.include_chat and self.include_metadata
351
+
352
+
353
+ # ---------------------------------------------------------------------------
354
+ # Download input validation (Pydantic)
355
+ # ---------------------------------------------------------------------------
356
+
357
+
358
+ class DownloadInput(BaseModel):
359
+ """Validated download arguments shared by MCP tools and CLI config.
360
+
361
+ Captures the core argument-combination constraints that apply regardless
362
+ of entry point (MCP ``download()`` tool, CLI ``_resolve_config_inner()``).
363
+ All fields are optional with sensible defaults; the ``@model_validator``
364
+ enforces mutual-exclusivity and at-least-one rules.
365
+
366
+ Construct this early in both code paths. ``ValidationError`` is then
367
+ either converted to ``exit_error()`` (CLI) or propagated to the MCP
368
+ framework (which returns a structured error to the agent).
369
+
370
+ Fields:
371
+ meeting: Meeting name search string. Mutually exclusive with
372
+ ``meeting_id`` and ``call_id``.
373
+ meeting_id: Meeting UUID. Mutually exclusive with ``meeting``,
374
+ ``call_id``, ``date``, and ``time``.
375
+ call_id: Call identifier (from list_transcripts output). Mutually
376
+ exclusive with ``meeting``, ``meeting_id``, ``date``, and ``time``.
377
+ Uniquely identifies a specific occurrence of a recurring meeting.
378
+ date: Target date as ``YYYY-MM-DD``. Incompatible with
379
+ ``meeting_id`` and ``call_id`` (date is resolved automatically).
380
+ time: Target time as ``HH:MM``. Incompatible with ``meeting_id``
381
+ and ``call_id``.
382
+ include_transcript: Whether to download the transcript.
383
+ include_chat: Whether to download chat messages.
384
+ include_metadata: Whether to download participant metadata.
385
+
386
+ Schema version: 1.1.0
387
+ """
388
+
389
+ model_config = ConfigDict(frozen=True)
390
+
391
+ meeting: str | None = None
392
+ meeting_id: str | None = None
393
+ call_id: str | None = None
394
+ date: str | None = None
395
+ time: str | None = None
396
+ include_transcript: bool = True
397
+ include_chat: bool = True
398
+ include_metadata: bool = True
399
+ dry_run: bool = False
400
+
401
+ @model_validator(mode="after")
402
+ def _check_constraints(self) -> DownloadInput:
403
+ """Enforce argument-combination constraints.
404
+
405
+ Rules:
406
+ 1. ``meeting_id``, ``call_id``, and ``meeting`` are mutually exclusive.
407
+ 2. At least one of ``meeting_id``, ``call_id``, or ``meeting`` is required.
408
+ 3. ``date`` is incompatible with ``meeting_id`` and ``call_id``.
409
+ 4. ``time`` is incompatible with ``meeting_id`` and ``call_id``.
410
+ 5. At least one content flag must be ``True``.
411
+ 6. ``meeting_id`` must be a valid UUID.
412
+ 7. ``call_id`` must be a non-empty string (no format constraint).
413
+ """
414
+ id_sources = sum(1 for x in (self.meeting_id, self.call_id, self.meeting) if x)
415
+ if id_sources > 1:
416
+ raise ValueError(
417
+ "meeting_id, call_id, and meeting are mutually exclusive. "
418
+ "Pass exactly one identifier."
419
+ )
420
+ if id_sources == 0:
421
+ raise ValueError("One of meeting_id, call_id, or meeting is required.")
422
+ if self.meeting_id and self.date:
423
+ raise ValueError(
424
+ "date is incompatible with meeting_id. "
425
+ "The meeting date is resolved automatically from the UUID."
426
+ )
427
+ if self.meeting_id and self.time:
428
+ raise ValueError(
429
+ "time is incompatible with meeting_id. "
430
+ "The meeting time is resolved automatically from the UUID."
431
+ )
432
+ if self.call_id and self.date:
433
+ raise ValueError(
434
+ "date is incompatible with call_id. "
435
+ "The meeting date is resolved automatically from the call ID."
436
+ )
437
+ if self.call_id and self.time:
438
+ raise ValueError(
439
+ "time is incompatible with call_id. "
440
+ "The meeting time is resolved automatically from the call ID."
441
+ )
442
+ if self.meeting_id:
443
+ try:
444
+ UUID(self.meeting_id)
445
+ except ValueError as exc:
446
+ raise ValueError("meeting_id must be a UUID.") from exc
447
+ if self.call_id is not None and not self.call_id.strip():
448
+ raise ValueError("call_id must be a non-empty string.")
449
+ if (
450
+ not self.include_transcript
451
+ and not self.include_chat
452
+ and not self.include_metadata
453
+ and not self.dry_run
454
+ ):
455
+ raise ValueError(
456
+ "At least one of include_transcript, include_chat, "
457
+ "or include_metadata must be True."
458
+ )
459
+ return self
460
+
461
+
462
+ # ---------------------------------------------------------------------------
463
+ # CDP target (lightweight typed wrapper)
464
+ # ---------------------------------------------------------------------------
465
+
466
+
467
+ @dataclass(frozen=True)
468
+ class CdpTarget:
469
+ """A Chrome DevTools Protocol page target.
470
+
471
+ Fields:
472
+ id: Target identifier string.
473
+ url: Page URL.
474
+ title: Page title.
475
+ ws_url: WebSocket debugger URL for connecting.
476
+ """
477
+
478
+ id: str
479
+ url: str = ""
480
+ title: str = ""
481
+ ws_url: str = ""
482
+
483
+ @classmethod
484
+ def from_dict(cls, d: dict) -> CdpTarget:
485
+ """Construct from a raw CDP ``/json`` response entry."""
486
+ return cls(
487
+ id=d.get("id", ""),
488
+ url=d.get("url", ""),
489
+ title=d.get("title", ""),
490
+ ws_url=d.get("webSocketDebuggerUrl", ""),
491
+ )
492
+
493
+
494
+ # ---------------------------------------------------------------------------
495
+ # Meeting artifact (for --list-transcripts)
496
+ # ---------------------------------------------------------------------------
497
+
498
+
499
+ @dataclass(frozen=True)
500
+ class MeetingArtifact:
501
+ """A meeting with associated artifacts discovered via the MCPS API.
502
+
503
+ Represents a single meeting returned by
504
+ ``startListArtifactsByUserWorkflow``. Each meeting may have a
505
+ transcript, a recording, or both.
506
+
507
+ Fields:
508
+ subject: Meeting subject line (may be empty for unnamed meetings).
509
+ thread_id: Teams thread identifier (``19:meeting_...``).
510
+ call_id: Call identifier from MCPS.
511
+ ical_uid: iCalendar UID for calendar correlation.
512
+ date: Date the meeting occurred.
513
+ has_transcript: Whether a TranscriptV2 resource is present.
514
+ has_recording: Whether a Recording resource is present.
515
+ transcript_url: SharePoint URL for transcript content download,
516
+ or ``None`` when no transcript is available.
517
+ recording_url: SharePoint URL for recording download,
518
+ or ``None`` when no recording is available.
519
+
520
+ Schema version: 1.0.0
521
+ """
522
+
523
+ subject: str
524
+ thread_id: str
525
+ call_id: str
526
+ ical_uid: str
527
+ date: dt.date
528
+ has_transcript: bool
529
+ has_recording: bool
530
+ transcript_url: str | None = None
531
+ recording_url: str | None = None