
Implementation Guide: Capture deposition proceedings and generate structured summaries with key admissions
Step-by-step implementation guide for deploying AI to capture deposition proceedings and generate structured summaries with key admissions for Legal Services clients.
Hardware Procurement
Shure MXA920 Ceiling Array Microphone
Shure MXA920 Ceiling Array Microphone
$3,800 per unit MSP cost / $4,900 suggested resale per unit
Primary ambient audio capture device for permanent deposition suites. Automatic Coverage technology provides superior multi-speaker directional pickup without manual beam steering. Ceiling-mount installation ensures unobstructed table space and consistent capture regardless of speaker position. One unit per deposition room.
EPOS EXPAND Capture 5 Tabletop Speakerphone
$250 per unit MSP cost / $400 suggested resale per unit
Backup and portable tabletop microphone with 7-beamforming microphone array for speaker attribution. Used as a secondary capture device in case of ceiling mic issues, or for portable deployment when depositions occur outside the firm's main office (e.g., at opposing counsel's office, hospital bedside depositions). Also provides Teams Room audio integration.
Jabra PanaCast 50 Video Bar
$1,100 per unit MSP cost / $1,500 suggested resale per unit
Combined video and supplemental audio capture for video-recorded depositions. Three 13MP cameras with 180° field of view capture all participants. 8-microphone array provides secondary audio stream for redundancy. Particularly important for remote/hybrid depositions conducted via Teams or Zoom where video capture of demeanor is required under FRCP Rule 30(b)(3).
Lenovo ThinkCentre M90q Gen 4 Tiny Desktop
$700 per unit MSP cost / $950 suggested resale per unit
Dedicated recording workstation for each deposition room. Runs the recording capture application (OBS Studio or custom capture agent), manages local buffering of audio/video files, and initiates upload to the transcription pipeline. Tiny form factor mounts behind monitor or under desk. Windows 11 Pro required for BitLocker encryption and domain join.
Synology DiskStation DS1621+
$900 MSP cost (diskless) / $1,300 suggested resale (diskless)
On-premises NAS for encrypted archival of all deposition recordings, raw transcripts, and AI-generated summaries. 6-bay design supports RAID 5/6 for redundancy. Serves as the primary local repository with 7+ year retention capability per legal hold requirements. Synology Hyper Backup handles automated cloud replication to Azure Blob Storage.
WD Red Plus NAS Hard Drive 4TB
$100 per unit MSP cost / $140 suggested resale per unit
NAS-rated 3.5" SATA drives for the Synology DS1621+ in RAID 5 configuration. 6 × 4TB in RAID 5 provides approximately 20TB usable storage. At ~500MB/hour for high-quality stereo audio, this supports approximately 40,000 hours of deposition recordings—sufficient for 5+ years for most mid-size litigation firms.
APC Smart-UPS 1500VA LCD
$450 per unit MSP cost / $600 suggested resale per unit
Uninterruptible power supply for each deposition room's recording workstation and network equipment. A power interruption during a deposition recording can have severe legal consequences. The SMT1500C provides approximately 20 minutes of runtime for graceful shutdown and has USB connectivity for automatic shutdown scripting via PowerChute.
Dell UltraSharp 24 Monitor
$200 per unit MSP cost / $280 suggested resale per unit
Display for the recording workstation in each deposition room. Used by the paralegal or tech staff to monitor recording status, verify audio levels, and manage the capture session. IPS panel with USB-C connectivity for clean single-cable connection to ThinkCentre.
Software Procurement
CaseMark Professional (White-Label)
$25 per deposition summary (MSP cost); resell at $45–$60 per summary. Professional tier ~$500/month for volume pricing
Primary AI deposition summarization engine. Converts raw transcripts into structured summaries with key admissions, credibility analysis, event chronologies, and page-line citations. White-label capability allows MSP to brand the portal, PDFs, and client-facing interface under the MSP's own identity. SOC 2 Type II certified. Integrates with Smokeball directly; API available for Clio and other platforms.
Deepgram Nova-3 API
$0.0043/minute for pre-recorded audio; $0.0077/minute for streaming. Typical firm: $50–$200/month based on deposition volume
Primary speech-to-text engine for converting deposition audio recordings into text transcripts with speaker diarization (speaker identification and labeling). Nova-3 delivers a 54.3% reduction in word error rate versus competitors and includes native punctuation, custom vocabulary (legal terminology), and speaker diarization at the base price. $200 free credit provided on signup.
Anthropic Claude API (Sonnet)
$3/MTok input, $15/MTok output. Typical deposition (50-page transcript ~25K tokens): ~$0.15–$0.50 per summarization run
Secondary/custom LLM summarization layer for firms that need bespoke summary formats beyond CaseMark's templates, or for MSPs building custom prompt-based workflows for specific practice areas (e.g., medical malpractice with ICD-10 code extraction). Used when CaseMark templates are insufficient or when the MSP wants to offer differentiated summarization products.
Microsoft 365 Business Premium
$22/user/month (CSP cost); resell at $28–$35/user/month
Foundation platform providing Azure AD (Entra ID) for identity management, SharePoint Online for document management and transcript archival, Microsoft Teams for hybrid/remote deposition capture, OneDrive for per-attorney file sync, and Intune for workstation management. BitLocker enforcement via Intune protects recording workstations.
OBS Studio
Free
Audio and video recording application on the deposition room workstation. Records multi-channel audio from the Shure MXA920 (via Dante/USB) and video from the Jabra PanaCast 50 simultaneously. Configured with automated scene switching and recording profiles for consistent capture. Scriptable via obs-websocket for automation.
Clio Manage (or client's existing PMS)
$39–$99/user/month (paid by client); integration labor included in MSP implementation fee
The firm's practice management system where deposition summaries, transcripts, and recordings are filed to the appropriate matter/case. The MSP builds an automated integration that pushes completed summaries from the AI pipeline directly into the correct matter's document folder via Clio's REST API.
Synology Hyper Backup + Azure Blob Storage
Azure Blob Storage (Cool tier): ~$0.01/GB/month. Estimated $20–$80/month for typical firm's archive
Automated encrypted backup of all on-premises deposition recordings and transcripts to Azure Blob Storage. Provides geographic redundancy and disaster recovery capability. Cool tier pricing optimized for archival data that is rarely accessed but must be retained for 7+ years per legal hold requirements.
Rev Human Transcription (Backup)
$1.99/audio minute for human transcription; Rev Max subscription at $29.99/month includes 20 hours of AI transcription
Human transcription backstop for high-stakes depositions where 99.5%+ accuracy is required or where audio quality is poor (e.g., hostile witness, heavy accents, multiple crosstalk). The AI pipeline processes all depositions by default, but attorneys can flag specific depositions for human review via Rev. Rev Max subscription at $29.99/month provides 20 hours of AI transcription as a secondary processing path.
Prerequisites
- Minimum 25 Mbps upload bandwidth on the firm's internet connection (50 Mbps+ recommended for real-time streaming transcription); verify with speed test from each deposition room location
- Failover/redundant internet connection (cellular failover at minimum) — a dropped recording during deposition has severe legal consequences
- Dedicated VLAN capability on the firm's network switch infrastructure (managed switches required, not consumer-grade)
- Microsoft 365 Business Premium licenses for all attorneys and paralegals who will access the system (or equivalent Azure AD/Entra ID for SSO)
- Practice management software with API access enabled (Clio Manage Boutique tier or higher, Smokeball Boost tier, or equivalent)
- Electrical outlets and ceiling mounting points in each deposition room (verify ceiling can support Shure MXA920 weight of 7.9 lbs; standard T-bar grid ceiling or hard ceiling with appropriate mounting hardware)
- Room acoustic assessment completed — deposition rooms should have some acoustic treatment (carpet, acoustic panels) to minimize reverberation that degrades transcription accuracy
- Written consent/disclosure policy reviewed and approved by the firm's managing partner and ethics counsel — MSP provides templates but the firm must approve the legal language
- Client has designated a 'Deposition Technology Coordinator' (typically a senior paralegal) who will be the primary point of contact for training and ongoing operations
- Existing antivirus/endpoint protection solution compatible with OBS Studio and Deepgram desktop agents (whitelist rules may be needed)
- Physical security controls for deposition rooms — rooms must be lockable and recording workstations should not be accessible to unauthorized persons
- Azure subscription (can be created during implementation) for Azure Blob Storage backup destination
Installation Steps
Step 1: Network Infrastructure Preparation
Create a dedicated VLAN for deposition room equipment to isolate recording traffic from general office network traffic. This ensures consistent bandwidth for audio/video upload and adds a security boundary around sensitive deposition data. Configure QoS rules to prioritize traffic from deposition room workstations to cloud transcription API endpoints.
# adjust for client's switch vendor
configure terminal
vlan 50
name DEPOSITION_CAPTURE
exit
interface range GigabitEthernet0/1-4
switchport mode access
switchport access vlan 50
no shutdown
exit- Configure DHCP scope for VLAN 50 on firewall/DHCP server
- Subnet: 10.10.50.0/24, Gateway: 10.10.50.1
- DNS: firm's DNS servers
- QoS: Mark VLAN 50 traffic as AF31 (Assured Forwarding) for priority
Adjust VLAN ID and interface range to match client's actual switch hardware. For Meraki, configure via Dashboard > Switch > Routing & DHCP. For Ubiquiti, configure via UniFi Network controller. Ensure the firewall allows outbound HTTPS (443) from VLAN 50 to Deepgram API endpoints (api.deepgram.com), CaseMark endpoints, and Azure Blob Storage endpoints.
Step 2: Deposition Room Physical Setup — Ceiling Microphone Installation
Install the Shure MXA920 ceiling array microphone in each deposition room. The MXA920 should be centered over the deposition table at the recommended height of 6–12 feet above the talker plane (seated head height, approximately 4 feet). The microphone connects via a single Ethernet cable carrying both Dante audio and PoE power.
The MXA920 uses Automatic Coverage technology and generally requires minimal configuration out of the box. However, use Shure Designer to verify coverage zones include all seating positions at the deposition table. If the room has significant reverberation, enable IntelliMix noise reduction on the MXA920. A Shure ANIUSB-MATRIX may be needed to bridge Dante audio to USB for the recording workstation if the workstation doesn't have a Dante Virtual Soundcard license.
Step 3: Deposition Room Physical Setup — Video Bar and Workstation
Mount the Jabra PanaCast 50 video bar at the end of the deposition table (or on a wall mount at table height) facing the deposition participants. Connect via USB-C to the recording workstation. Position the workstation, monitor, UPS, and EPOS Capture 5 backup microphone. The workstation monitor should be positioned so the paralegal can see recording status without being obtrusive to the deposition.
Cable management is important — deposition rooms must look professional. Use cable raceways or in-wall conduit. The EPOS Capture 5 serves as both a backup microphone and a speaker for playing back recordings during the deposition if needed. Verify the UPS is providing battery backup (not just surge protection) by disconnecting AC power briefly and confirming the workstation stays running.
Step 4: Windows 11 Pro Workstation Configuration and Hardening
Configure the recording workstation with appropriate security hardening, domain join (or Entra ID join), BitLocker encryption, and local user accounts. The workstation must be encrypted at rest because it temporarily stores deposition recordings containing privileged attorney-client communications.
# Enable BitLocker via PowerShell (run as Administrator)
Enable-BitLocker -MountPoint "C:" -EncryptionMethod XtsAes256 -UsedSpaceOnly -RecoveryPasswordProtector
# Store BitLocker recovery key to Azure AD
BackupToAAD-BitLockerKeyProtector -MountPoint "C:" -KeyProtectorId (Get-BitLockerVolume -MountPoint "C:").KeyProtector[0].KeyProtectorId# Disable unnecessary services
Set-Service -Name "WSearch" -StartupType Disabled # Windows Search indexer not needed
Set-Service -Name "MapsBroker" -StartupType Disabled# Configure Windows Firewall - allow only required outbound
New-NetFirewallRule -DisplayName "Allow Deepgram API" -Direction Outbound -RemoteAddress Any -RemotePort 443 -Protocol TCP -Action Allow
New-NetFirewallRule -DisplayName "Allow SMB to NAS" -Direction Outbound -RemoteAddress 10.10.50.0/24 -RemotePort 445 -Protocol TCP -Action Allow# Set power plan to High Performance (prevent sleep during recording)
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c# Create local recording directory structure
New-Item -Path "C:\DepositionRecordings" -ItemType Directory
New-Item -Path "C:\DepositionRecordings\Pending" -ItemType Directory
New-Item -Path "C:\DepositionRecordings\Processed" -ItemType Directory
New-Item -Path "C:\DepositionRecordings\Archive" -ItemType DirectoryBitLocker is MANDATORY — unencrypted deposition recordings on a stolen workstation would be a catastrophic data breach involving privileged material. Verify BitLocker recovery keys are backed up to Azure AD before proceeding. Configure Intune compliance policy to block access if BitLocker is disabled. Set active hours broadly to prevent Windows Update reboots during depositions.
Step 5: Install and Configure Shure ANIUSB-MATRIX Audio Bridge
Install the Shure ANIUSB-MATRIX to bridge Dante audio from the MXA920 ceiling microphone to USB audio input on the recording workstation. This device converts the networked Dante audio stream into a standard USB audio device that OBS Studio and other recording applications can use directly.
Get-PnpDevice -Class AudioEndpoint | Where-Object {$_.FriendlyName -like '*Shure*'}If the firm's budget does not accommodate the ANIUSB-MATRIX (~$800), an alternative is to install Dante Virtual Soundcard on the workstation ($49.99 license from Audinate). However, the ANIUSB-MATRIX is more reliable and does not require driver management. The ANIUSB-MATRIX also provides a hardware automixer, which is beneficial for multi-speaker deposition scenarios.
Step 6: Install and Configure OBS Studio for Deposition Recording
Install OBS Studio as the primary recording application. Configure it with two audio sources (Shure ceiling mic as primary, EPOS Capture 5 as backup) and one video source (Jabra PanaCast 50). Create recording profiles optimized for deposition capture with appropriate file naming conventions and automatic file splitting.
winget install OBSProject.OBSStudioInstall obs-websocket plugin (for automation) — included in OBS 28+. Verify by navigating to: Tools > WebSocket Server Settings > Enable WebSocket Server. Set port: 4455 and set a strong authentication password.
MKV container is used during recording because it is resilient to corruption if the recording is interrupted (e.g., power failure). OBS automatically remuxes to MP4 after recording stops. FLAC audio encoding is lossless, which is critical for legal recordings where audio quality may be scrutinized. The dual audio track setup ensures that if the ceiling mic has an issue, the tabletop mic recording is preserved independently.
Step 7: Configure Synology NAS for Deposition Archive Storage
Set up the Synology DS1621+ NAS with RAID 5, create shared folders for deposition recordings with appropriate access controls, enable encryption, and configure automated backup to Azure Blob Storage via Hyper Backup.
net use Z: \\<NAS-IP>\DepositionRecordings /user:depo-workstation-01 /persistent:yesEncryption keys for the shared folders must be documented and stored securely — if lost, data is unrecoverable. Store keys in the MSP's password manager (e.g., IT Glue, Hudu) AND provide the client's managing partner with a sealed envelope containing the keys for safe deposit. RAID 5 provides single-drive fault tolerance; monitor drive health via Synology's Storage Manager alerts and DSM email notifications. Configure S.M.A.R.T. extended tests monthly.
Step 8: Set Up Deepgram API Account and Test Transcription
Create a Deepgram account, obtain API keys, configure the Nova-3 model with legal-specific settings, and validate transcription accuracy with a test recording. Deepgram will be the primary speech-to-text engine that converts deposition audio into text with speaker diarization.
[System.Environment]::SetEnvironmentVariable('DEEPGRAM_API_KEY', 'your-api-key-here', 'Machine')curl -X POST "https://api.deepgram.com/v1/listen?model=nova-3&smart_format=true&diarize=true&punctuate=true¶graphs=true&utterances=true&language=en-US" -H "Authorization: Token $env:DEEPGRAM_API_KEY" -H "Content-Type: audio/wav" --data-binary @C:\DepositionRecordings\test_sample.wav -o C:\DepositionRecordings\test_transcript.json- model=nova-3 — highest accuracy model
- diarize=true — speaker identification
- smart_format=true — formats numbers, dates, currency
- punctuate=true — adds punctuation
- paragraphs=true — groups into paragraphs
- utterances=true — returns speaker-attributed utterances
- keywords=deposition:2,objection:2,stipulate:2,exhibit:2 — boost legal terms
- numerals=true — converts spoken numbers to digits
Deepgram provides $200 in free credits on signup — sufficient for approximately 775 hours of pre-recorded transcription at $0.0043/min. Use this credit for all testing and initial deployments. The 'keywords' parameter boosts recognition accuracy for specified legal terms — add firm-specific terms (case names, party names, expert names) for each deposition. Store the API key in Azure Key Vault in production rather than environment variables for better security.
Step 9: Set Up CaseMark Account and Configure White-Label Portal
Create a CaseMark Professional account, configure the white-label branding (MSP's or firm's branding), set up the deposition summary templates, and test the summary generation pipeline with a sample transcript.
CaseMark's SOC 2 Type II certification and contractual commitment not to train on client data are critical selling points for law firm clients. During the white-label setup, ensure the custom domain SSL certificate is properly configured. CaseMark's first summary is free for evaluation — use this to demonstrate value to the firm's attorneys before going live. The per-summary pricing ($25 base) provides clear margin when resold at $45–$60.
Step 10: Build the Automated Deposition Processing Pipeline
Create the Python-based automation pipeline that watches for new recordings, extracts audio, sends to Deepgram for transcription, formats the transcript, sends to CaseMark (or Claude API) for summarization, and files results to the NAS and practice management system. This is the core integration logic that ties all components together.
winget install Python.Python.3.11mkdir C:\DepositionPipeline
cd C:\DepositionPipeline
python -m venv venv
.\venv\Scripts\Activate.ps1pip install deepgram-sdk anthropic requests watchdog python-docx schedule
pip install azure-storage-blob python-dotenvDEEPGRAM_API_KEY=your_key
CASEMARK_API_KEY=your_key
ANTHROPIC_API_KEY=your_key
CLIO_API_TOKEN=your_token
AZURE_STORAGE_CONNECTION_STRING=your_connection
NAS_SHARE_PATH=\\nas-ip\DepositionRecordings- Main entry point: deposition_pipeline.py
- transcriber.py — Deepgram integration
- summarizer.py — CaseMark/Claude integration
- pms_integration.py — Clio API integration
- file_watcher.py — monitors recording directory
pip install pywin32
python deposition_service.py install
python deposition_service.py startGet-Service DepositionPipelineThe pipeline runs as a Windows Service so it starts automatically on boot and runs in the background. It monitors C:\DepositionRecordings\Pending for new files. When a recording is completed (file is no longer being written to), it initiates the transcription→summarization→filing workflow automatically. All processing is logged to C:\DepositionPipeline\logs for troubleshooting. See the custom_ai_components section for complete source code.
Step 11: Configure Practice Management System Integration (Clio)
Set up the Clio API integration to automatically file completed deposition transcripts and AI summaries to the correct matter in Clio Manage. This requires creating a Clio API application, configuring OAuth2 authentication, and mapping the pipeline output to Clio's document management structure.
# open in browser to begin authorization flow
https://app.clio.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:8080/callback&scope=documents:write%20matters:read%20contacts:readcurl -X POST https://app.clio.com/oauth/token -d "grant_type=authorization_code&code=AUTH_CODE&client_id=YOUR_CLIENT_ID&client_secret=YOUR_SECRET&redirect_uri=http://localhost:8080/callback"# .env entries
CLIO_CLIENT_ID=your_id
CLIO_CLIENT_SECRET=your_secret
CLIO_REFRESH_TOKEN=your_refresh_tokencurl -H "Authorization: Bearer ACCESS_TOKEN" https://app.clio.com/api/v4/matters.json?limit=5If the firm uses Smokeball instead of Clio, CaseMark has a direct embedded integration — no custom API work needed. For PracticePanther or MyCase, use Zapier as a middleware layer (Zapier webhook trigger → PMS document upload action). Clio's OAuth tokens expire after 24 hours, so the pipeline must handle token refresh automatically using the refresh token. See the pms_integration.py component in custom_ai_components.
Step 12: Create Recording Consent and Disclosure Workflow
Implement the compliance workflow for deposition recording consent. This includes on-screen consent capture at the start of each recording session, generation of a consent log, and integration with the firm's existing deposition notice procedures. This step is critical for two-party consent state compliance.
THIS IS THE MOST LEGALLY SENSITIVE STEP. The MSP provides the technology and templates, but the firm's attorneys must approve all consent language and take responsibility for compliance with applicable recording consent laws. In two-party consent states (CA, FL, IL, MA, PA, WA, and others), ALL parties must consent to the recording. The consent should be captured on the record (stated by the noticing attorney at the start of the deposition) AND documented in writing. The MSP should not provide legal advice — frame this as 'here is what other firms typically do' and defer to the firm's ethics counsel.
Step 13: Configure Monitoring, Alerting, and Logging
Set up comprehensive monitoring for the entire deposition capture pipeline including recording workstation health, NAS storage capacity, API availability, and pipeline processing status. Configure alerts to the MSP's RMM/PSA system and to the firm's designated coordinator.
$obs = Get-Process obs64 -ErrorAction SilentlyContinue
if (-not $obs) { Write-Output 'ALERT: OBS Studio not running on deposition workstation'; exit 1 }$response = Invoke-WebRequest -Uri 'https://api.deepgram.com/v1/listen' -Method OPTIONS -TimeoutSec 10 -ErrorAction SilentlyContinue
if ($response.StatusCode -ne 200 -and $response.StatusCode -ne 405) { Write-Output 'ALERT: Deepgram API unreachable'; exit 1 }$usage = (Get-PSDrive Z).Used / (Get-PSDrive Z).Free * 100
if ($usage -gt 80) { Write-Output "ALERT: NAS storage at $usage% capacity"; exit 1 }$svc = Get-Service DepositionPipeline -ErrorAction SilentlyContinue
if ($svc.Status -ne 'Running') { Write-Output 'ALERT: Deposition Pipeline service stopped'; exit 1 }$stale = Get-ChildItem C:\DepositionRecordings\Pending -File | Where-Object { $_.LastWriteTime -lt (Get-Date).AddHours(-4) }
if ($stale) { Write-Output "ALERT: $($stale.Count) files stuck in Pending queue"; exit 1 }Monitoring cadence: workstation health checks every 5 minutes, API reachability every 15 minutes, storage capacity daily, pipeline queue hourly. Create a dashboard in your RMM/PSA showing all deposition infrastructure at a glance. Set up escalation: Level 1 (pipeline service restart) → Level 2 (API/connectivity issues) → Level 3 (hardware failure, data loss risk). Configure PagerDuty or Opsgenie for after-hours alerts if the firm conducts depositions outside business hours.
Step 14: End-to-End System Testing with Mock Deposition
Conduct a full end-to-end test of the entire system using a mock deposition with firm staff. This validates every component from audio capture through final summary delivery to the practice management system. Record issues and tune the system before going live.
type C:\DepositionPipeline\logs\pipeline.logRun at least 3 mock depositions before going live: one in ideal conditions, one with deliberate challenges (low voice, background noise, crosstalk), and one simulating a remote/hybrid deposition via Teams. Acceptable thresholds: WER <8%, speaker attribution >90%, summary captures 100% of flagged admissions. If thresholds aren't met, adjust Deepgram keywords, MXA920 coverage zones, or room acoustics before proceeding.
Custom AI Components
Deposition Processing Pipeline
Type: workflow
The core automation workflow that orchestrates the entire deposition capture-to-summary pipeline. It watches for completed recordings, extracts audio, sends to Deepgram for transcription with speaker diarization, formats the transcript into a structured document, sends to CaseMark or Claude for summarization, and files results to the NAS and practice management system. Runs as a Windows Service.
Implementation:
# Core pipeline orchestrator for deposition processing
# deposition_pipeline.py
# Core pipeline orchestrator for deposition processing
import os
import sys
import json
import time
import logging
import shutil
from pathlib import Path
from datetime import datetime
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from dotenv import load_dotenv
load_dotenv('C:\\DepositionPipeline\\.env')
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('C:\\DepositionPipeline\\logs\\pipeline.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger('DepositionPipeline')
# Import sub-modules
from transcriber import DeepgramTranscriber
from summarizer import DepositionSummarizer
from pms_integration import ClioIntegration
from consent_manager import ConsentManager
# Configuration
WATCH_DIR = Path('C:/DepositionRecordings/Pending')
PROCESSED_DIR = Path('C:/DepositionRecordings/Processed')
ARCHIVE_DIR = Path('C:/DepositionRecordings/Archive')
NAS_PATH = Path(os.getenv('NAS_SHARE_PATH', 'Z:\\'))
STABILITY_WAIT = 30 # seconds to wait after file stops growing
class RecordingHandler(FileSystemEventHandler):
"""Watches for new recording files and triggers processing."""
def __init__(self):
self.transcriber = DeepgramTranscriber(api_key=os.getenv('DEEPGRAM_API_KEY'))
self.summarizer = DepositionSummarizer(
casemark_key=os.getenv('CASEMARK_API_KEY'),
anthropic_key=os.getenv('ANTHROPIC_API_KEY')
)
self.clio = ClioIntegration(
client_id=os.getenv('CLIO_CLIENT_ID'),
client_secret=os.getenv('CLIO_CLIENT_SECRET'),
refresh_token=os.getenv('CLIO_REFRESH_TOKEN')
)
self.processing = set()
def on_created(self, event):
if event.is_directory:
return
filepath = Path(event.src_path)
if filepath.suffix.lower() in ['.mp4', '.mkv', '.wav', '.mp3', '.flac']:
logger.info(f'New recording detected: {filepath.name}')
self._wait_for_stable(filepath)
if filepath.name not in self.processing:
self.processing.add(filepath.name)
try:
self._process_recording(filepath)
except Exception as e:
logger.error(f'Pipeline error for {filepath.name}: {e}', exc_info=True)
finally:
self.processing.discard(filepath.name)
def _wait_for_stable(self, filepath, timeout=600):
"""Wait until file size stops changing (recording finished)."""
logger.info(f'Waiting for {filepath.name} to stabilize...')
prev_size = -1
stable_count = 0
elapsed = 0
while elapsed < timeout:
try:
current_size = filepath.stat().st_size
except FileNotFoundError:
return
if current_size == prev_size:
stable_count += 1
if stable_count >= 3: # stable for 3 checks (30 sec)
logger.info(f'{filepath.name} is stable at {current_size / 1e6:.1f} MB')
return
else:
stable_count = 0
prev_size = current_size
time.sleep(10)
elapsed += 10
logger.warning(f'{filepath.name} did not stabilize within {timeout}s, processing anyway')
def _process_recording(self, filepath):
"""Full pipeline: transcribe → summarize → file."""
session_id = datetime.now().strftime('%Y%m%d_%H%M%S')
logger.info(f'=== Processing {filepath.name} | Session: {session_id} ===')
# Step 1: Transcribe with Deepgram
logger.info('Step 1: Sending to Deepgram for transcription...')
transcript_result = self.transcriber.transcribe(
audio_path=str(filepath),
options={
'model': 'nova-3',
'smart_format': True,
'diarize': True,
'punctuate': True,
'paragraphs': True,
'utterances': True,
'language': 'en-US',
'keywords': ['objection:2', 'stipulate:2', 'exhibit:2',
'deposition:2', 'counsel:2', 'testimony:2']
}
)
# Step 2: Format transcript with speaker labels
logger.info('Step 2: Formatting transcript with speaker labels...')
formatted_transcript = self.transcriber.format_transcript(transcript_result)
# Save raw transcript
transcript_path = PROCESSED_DIR / f'{session_id}_transcript.json'
with open(transcript_path, 'w') as f:
json.dump(transcript_result, f, indent=2)
formatted_path = PROCESSED_DIR / f'{session_id}_transcript.txt'
with open(formatted_path, 'w') as f:
f.write(formatted_transcript)
logger.info(f'Transcript saved: {formatted_path}')
# Step 3: Generate AI Summary
logger.info('Step 3: Generating AI deposition summary...')
summary = self.summarizer.generate_summary(
transcript_text=formatted_transcript,
session_id=session_id
)
summary_path = PROCESSED_DIR / f'{session_id}_summary.docx'
self.summarizer.save_as_docx(summary, summary_path)
logger.info(f'Summary saved: {summary_path}')
# Step 4: Archive to NAS
logger.info('Step 4: Archiving to NAS...')
nas_session_dir = NAS_PATH / 'DepositionRecordings' / session_id
nas_session_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(filepath, nas_session_dir / filepath.name)
shutil.copy2(formatted_path, NAS_PATH / 'Transcripts' / formatted_path.name)
shutil.copy2(summary_path, NAS_PATH / 'AISummaries' / summary_path.name)
logger.info('Files archived to NAS')
# Step 5: Upload to Practice Management System
logger.info('Step 5: Filing to Clio...')
# Matter ID should be set in the session metadata file
metadata_path = WATCH_DIR / f'{filepath.stem}_metadata.json'
matter_id = None
if metadata_path.exists():
with open(metadata_path) as f:
metadata = json.load(f)
matter_id = metadata.get('clio_matter_id')
if matter_id:
self.clio.upload_document(
matter_id=matter_id,
file_path=str(summary_path),
name=f'AI Deposition Summary - {session_id}',
category='AI Deposition Summary'
)
self.clio.upload_document(
matter_id=matter_id,
file_path=str(formatted_path),
name=f'AI Deposition Transcript - {session_id}',
category='AI Deposition Transcript'
)
logger.info(f'Documents filed to Clio matter {matter_id}')
else:
logger.warning('No matter_id found in metadata — skipping Clio upload')
# Step 6: Move original to archive
archive_path = ARCHIVE_DIR / filepath.name
shutil.move(str(filepath), str(archive_path))
if metadata_path.exists():
shutil.move(str(metadata_path), str(ARCHIVE_DIR / metadata_path.name))
logger.info(f'=== Pipeline complete for {filepath.name} | Session: {session_id} ===')
def main():
# Ensure directories exist
for d in [WATCH_DIR, PROCESSED_DIR, ARCHIVE_DIR]:
d.mkdir(parents=True, exist_ok=True)
Path('C:/DepositionPipeline/logs').mkdir(parents=True, exist_ok=True)
logger.info('Deposition Processing Pipeline starting...')
logger.info(f'Watching directory: {WATCH_DIR}')
handler = RecordingHandler()
observer = Observer()
observer.schedule(handler, str(WATCH_DIR), recursive=False)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
logger.info('Pipeline stopped.')
if __name__ == '__main__':
main()Deepgram Transcription Module
Type: integration Handles communication with the Deepgram Nova-3 API for speech-to-text transcription with speaker diarization. Includes retry logic, error handling, and transcript formatting with speaker labels and page-line numbering suitable for legal citation.
Implementation:
# Deepgram Nova-3 integration for deposition transcription
# transcriber.py
# Deepgram Nova-3 integration for deposition transcription
import json
import logging
import time
import math
from pathlib import Path
from deepgram import DeepgramClient, PrerecordedOptions, FileSource
logger = logging.getLogger('DepositionPipeline.Transcriber')
LINES_PER_PAGE = 25 # Standard legal transcript page format
CHARS_PER_LINE = 70 # Approximate characters per line
class DeepgramTranscriber:
def __init__(self, api_key: str):
self.client = DeepgramClient(api_key)
def transcribe(self, audio_path: str, options: dict, max_retries: int = 3) -> dict:
"""Transcribe audio file using Deepgram Nova-3 with retry logic."""
logger.info(f'Transcribing: {audio_path}')
file_size = Path(audio_path).stat().st_size / 1e6
logger.info(f'File size: {file_size:.1f} MB')
with open(audio_path, 'rb') as audio_file:
buffer_data = audio_file.read()
payload: FileSource = {'buffer': buffer_data}
dg_options = PrerecordedOptions(
model=options.get('model', 'nova-3'),
smart_format=options.get('smart_format', True),
diarize=options.get('diarize', True),
punctuate=options.get('punctuate', True),
paragraphs=options.get('paragraphs', True),
utterances=options.get('utterances', True),
language=options.get('language', 'en-US'),
keywords=options.get('keywords', [])
)
for attempt in range(max_retries):
try:
response = self.client.listen.rest.v('1').transcribe_file(
payload, dg_options
)
result = response.to_dict() if hasattr(response, 'to_dict') else json.loads(str(response))
logger.info(f'Transcription complete. Words: {self._count_words(result)}')
return result
except Exception as e:
logger.warning(f'Transcription attempt {attempt+1} failed: {e}')
if attempt < max_retries - 1:
wait_time = 2 ** attempt * 5
logger.info(f'Retrying in {wait_time}s...')
time.sleep(wait_time)
else:
logger.error('All transcription attempts failed')
raise
def _count_words(self, result: dict) -> int:
try:
return len(result['results']['channels'][0]['alternatives'][0]['words'])
except (KeyError, IndexError):
return 0
def format_transcript(self, result: dict) -> str:
"""Format Deepgram output into legal transcript format with page:line numbering."""
utterances = result.get('results', {}).get('utterances', [])
if not utterances:
# Fallback to paragraphs if utterances not available
return self._format_from_paragraphs(result)
# Build speaker map
speaker_map = {}
speaker_counter = 1
for utt in utterances:
speaker_id = utt.get('speaker', 0)
if speaker_id not in speaker_map:
speaker_map[speaker_id] = f'SPEAKER {speaker_counter}'
speaker_counter += 1
# Format with page:line numbers
lines = []
current_line = 1
current_page = 1
# Header
lines.append(f'PAGE {current_page}')
lines.append('=' * 70)
lines.append('')
current_line = 1
for utt in utterances:
speaker = speaker_map.get(utt.get('speaker', 0), 'UNKNOWN')
text = utt.get('transcript', '').strip()
timestamp = utt.get('start', 0)
# Format timestamp
mins = int(timestamp // 60)
secs = int(timestamp % 60)
ts_str = f'[{mins:02d}:{secs:02d}]'
# Speaker line
speaker_line = f'{current_page}:{current_line:02d} {ts_str} {speaker}:'
lines.append(speaker_line)
current_line += 1
# Wrap text to ~70 chars per line
words = text.split()
current_text_line = ''
for word in words:
if len(current_text_line) + len(word) + 1 > CHARS_PER_LINE:
lines.append(f'{current_page}:{current_line:02d} {current_text_line}')
current_line += 1
current_text_line = word
else:
current_text_line = f'{current_text_line} {word}'.strip()
# Page break
if current_line > LINES_PER_PAGE:
current_page += 1
current_line = 1
lines.append('')
lines.append(f'PAGE {current_page}')
lines.append('=' * 70)
lines.append('')
if current_text_line:
lines.append(f'{current_page}:{current_line:02d} {current_text_line}')
current_line += 1
lines.append('') # Blank line between utterances
current_line += 1
if current_line > LINES_PER_PAGE:
current_page += 1
current_line = 1
lines.append(f'PAGE {current_page}')
lines.append('=' * 70)
lines.append('')
# Footer
lines.append('')
lines.append(f'--- END OF TRANSCRIPT ---')
lines.append(f'Total Pages: {current_page}')
lines.append(f'Speaker Map: {json.dumps(speaker_map)}')
lines.append(f'Note: Speaker identities should be verified by reviewing attorney.')
lines.append(f' This is an AI-generated transcript, not a certified court reporter transcript.')
return '\n'.join(lines)
def _format_from_paragraphs(self, result: dict) -> str:
"""Fallback formatting from paragraph-level results."""
try:
paragraphs = result['results']['channels'][0]['alternatives'][0]['paragraphs']['paragraphs']
lines = []
page = 1
line = 1
for para in paragraphs:
speaker = para.get('speaker', 0)
for sent in para.get('sentences', []):
text = sent.get('text', '')
lines.append(f'{page}:{line:02d} SPEAKER {speaker + 1}: {text}')
line += 1
if line > LINES_PER_PAGE:
page += 1
line = 1
lines.append('')
line += 1
return '\n'.join(lines)
except (KeyError, IndexError) as e:
logger.error(f'Failed to format transcript: {e}')
# Last resort: return raw text
return result.get('results', {}).get('channels', [{}])[0].get('alternatives', [{}])[0].get('transcript', 'TRANSCRIPTION ERROR')Deposition Summarization Module
Type: skill Generates structured deposition summaries using CaseMark API as the primary engine and Anthropic Claude as a fallback/custom engine. Produces summaries with key admissions, contradictions, chronological timelines, exhibit references, and page-line citations in DOCX format.
Implementation
# Deposition summary generation using CaseMark and Claude
# summarizer.py
# Deposition summary generation using CaseMark and Claude
import json
import logging
import requests
from pathlib import Path
from datetime import datetime
from docx import Document
from docx.shared import Pt, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
logger = logging.getLogger('DepositionPipeline.Summarizer')
# Claude prompt template for deposition summarization
DEPOSITION_SUMMARY_PROMPT = """You are a senior litigation paralegal with 20 years of experience summarizing depositions. You are analyzing a deposition transcript that was generated by AI speech-to-text (with possible minor transcription errors). Your task is to produce a comprehensive, structured deposition summary.
IMPORTANT RULES:
1. Every factual assertion must include a page:line citation in the format (Page:Line)
2. Use exact quotes for key admissions — surround with quotation marks
3. Flag any statements that contradict other testimony, prior pleadings, or documentary evidence
4. Note any objections made and whether they were sustained
5. Identify all exhibits referenced and what they were used for
6. Flag credibility issues: evasive answers, memory failures, contradictions
7. Do NOT fabricate citations — if you cannot identify the exact page:line, use [approximate location]
Produce the summary in the following sections:
## 1. DEPOSITION OVERVIEW
- Deponent name (as identified in transcript)
- Date of deposition
- Duration (approximate from timestamps)
- Attorneys present (as identified)
- Key topics covered
## 2. KEY ADMISSIONS
List each significant admission by the deponent, with:
- The exact quote
- Page:line citation
- Why this admission is significant for the case
- Suggested follow-up or impeachment use
## 3. CONTRADICTIONS & IMPEACHMENT MATERIAL
List any statements that:
- Contradict other statements within this deposition
- Appear to contradict common knowledge or documentary evidence
- Show evasiveness or memory issues
Include page:line citations for each.
## 4. CHRONOLOGICAL TIMELINE
Extract all dates, times, and events mentioned, in chronological order:
| Date/Time | Event | Speaker | Citation |
## 5. EXHIBIT REFERENCES
List all exhibits mentioned:
| Exhibit # | Description | How Used | Citation |
## 6. OBJECTIONS LOG
List all objections:
| Objection | Basis | Ruling | Citation |
## 7. CREDIBILITY ASSESSMENT NOTES
- Overall cooperation level
- Areas of evasiveness
- Memory/recall issues
- Demeanor notes (if discernible from transcript)
## 8. RECOMMENDED FOLLOW-UP
- Areas requiring further discovery
- Suggested interrogatories or document requests
- Potential motion topics (e.g., motion to compel if witness refused to answer)
---
Here is the deposition transcript:
{transcript}
---
Produce the structured summary now. Be thorough and cite every assertion."""
class DepositionSummarizer:
def __init__(self, casemark_key: str = None, anthropic_key: str = None):
self.casemark_key = casemark_key
self.anthropic_key = anthropic_key
def generate_summary(self, transcript_text: str, session_id: str) -> str:
"""Generate summary using CaseMark (primary) or Claude (fallback)."""
# Try CaseMark first
if self.casemark_key:
try:
logger.info('Attempting summary via CaseMark API...')
return self._summarize_casemark(transcript_text, session_id)
except Exception as e:
logger.warning(f'CaseMark failed, falling back to Claude: {e}')
# Fallback to Claude
if self.anthropic_key:
logger.info('Generating summary via Claude API...')
return self._summarize_claude(transcript_text)
raise RuntimeError('No summarization API available (both CaseMark and Claude failed/unconfigured)')
def _summarize_casemark(self, transcript_text: str, session_id: str) -> str:
"""Submit transcript to CaseMark API for summarization."""
# CaseMark API endpoint (verify current endpoint with CaseMark docs)
url = 'https://api.casemark.ai/v1/summaries'
headers = {
'Authorization': f'Bearer {self.casemark_key}',
'Content-Type': 'application/json'
}
payload = {
'document_type': 'deposition_transcript',
'content': transcript_text,
'template': 'deposition_summary',
'options': {
'include_key_admissions': True,
'include_contradictions': True,
'include_chronology': True,
'include_exhibit_references': True,
'include_credibility_notes': True,
'citation_format': 'page_line'
},
'reference_id': session_id
}
response = requests.post(url, json=payload, headers=headers, timeout=300)
response.raise_for_status()
result = response.json()
summary_text = result.get('summary', '')
if not summary_text:
# CaseMark may use async processing — poll for result
job_id = result.get('job_id')
if job_id:
summary_text = self._poll_casemark(job_id)
logger.info(f'CaseMark summary generated: {len(summary_text)} chars')
return summary_text
def _poll_casemark(self, job_id: str, timeout: int = 600, interval: int = 10) -> str:
"""Poll CaseMark for async job completion."""
import time
url = f'https://api.casemark.ai/v1/summaries/{job_id}'
headers = {'Authorization': f'Bearer {self.casemark_key}'}
elapsed = 0
while elapsed < timeout:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
result = response.json()
if result.get('status') == 'completed':
return result.get('summary', '')
elif result.get('status') == 'failed':
raise RuntimeError(f'CaseMark job failed: {result.get("error")}')
time.sleep(interval)
elapsed += interval
raise TimeoutError(f'CaseMark job {job_id} timed out after {timeout}s')
def _summarize_claude(self, transcript_text: str) -> str:
"""Generate summary using Anthropic Claude API."""
import anthropic
client = anthropic.Anthropic(api_key=self.anthropic_key)
# Truncate very long transcripts to fit context window
# Claude claude-sonnet-4-20250514 supports 200K tokens (~150K words)
max_chars = 500000 # ~125K words, safe for Sonnet
if len(transcript_text) > max_chars:
logger.warning(f'Transcript truncated from {len(transcript_text)} to {max_chars} chars')
transcript_text = transcript_text[:max_chars] + '\n\n[TRANSCRIPT TRUNCATED DUE TO LENGTH]'
prompt = DEPOSITION_SUMMARY_PROMPT.format(transcript=transcript_text)
message = client.messages.create(
model='claude-sonnet-4-20250514',
max_tokens=8192,
messages=[
{'role': 'user', 'content': prompt}
]
)
summary = message.content[0].text
logger.info(f'Claude summary generated: {len(summary)} chars, {message.usage.input_tokens} input tokens, {message.usage.output_tokens} output tokens')
return summary
def save_as_docx(self, summary_text: str, output_path: Path):
"""Save summary as a formatted Word document."""
doc = Document()
# Title
title = doc.add_heading('AI DEPOSITION SUMMARY', level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Metadata
doc.add_paragraph(f'Generated: {datetime.now().strftime("%B %d, %Y at %I:%M %p")}')
doc.add_paragraph('CONFIDENTIAL — ATTORNEY WORK PRODUCT')
doc.add_paragraph('')
# Disclaimer
disclaimer = doc.add_paragraph()
disclaimer.style = doc.styles['Intense Quote']
disclaimer.add_run(
'NOTICE: This summary was generated by artificial intelligence and has not been '
'verified by a human reviewer. Speaker identifications are based on AI voice '
'diarization and may contain errors. Page:line citations reference the AI-generated '
'transcript, not the certified court reporter transcript. This document is attorney '
'work product and should be reviewed for accuracy before reliance.'
)
# Parse and format summary sections
lines = summary_text.split('\n')
for line in lines:
line = line.rstrip()
if line.startswith('## '):
doc.add_heading(line[3:], level=2)
elif line.startswith('### '):
doc.add_heading(line[4:], level=3)
elif line.startswith('| ') and '|' in line[1:]:
# Simple table row — add as formatted paragraph
p = doc.add_paragraph(line, style='List Bullet')
p.paragraph_format.space_after = Pt(2)
elif line.startswith('- '):
doc.add_paragraph(line[2:], style='List Bullet')
elif line.startswith(' - '):
doc.add_paragraph(line[4:], style='List Bullet 2')
elif line.strip():
doc.add_paragraph(line)
# Footer
doc.add_paragraph('')
footer = doc.add_paragraph('--- END OF AI SUMMARY ---')
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.save(str(output_path))
logger.info(f'DOCX saved: {output_path}')Clio Practice Management Integration
Type: integration
Handles OAuth2 authentication with Clio's REST API and uploads deposition transcripts and summaries as documents to the correct matter. Includes automatic token refresh and matter lookup capabilities.
Implementation:
# Clio Manage API integration for document filing
# pms_integration.py
# Clio Manage API integration for document filing
import json
import logging
import requests
from pathlib import Path
from datetime import datetime, timedelta
logger = logging.getLogger('DepositionPipeline.ClioIntegration')
CLIO_BASE_URL = 'https://app.clio.com/api/v4'
CLIO_TOKEN_URL = 'https://app.clio.com/oauth/token'
class ClioIntegration:
def __init__(self, client_id: str, client_secret: str, refresh_token: str):
self.client_id = client_id
self.client_secret = client_secret
self.refresh_token = refresh_token
self.access_token = None
self.token_expiry = datetime.min
def _ensure_token(self):
"""Refresh access token if expired or missing."""
if self.access_token and datetime.now() < self.token_expiry:
return
logger.info('Refreshing Clio access token...')
response = requests.post(CLIO_TOKEN_URL, data={
'grant_type': 'refresh_token',
'refresh_token': self.refresh_token,
'client_id': self.client_id,
'client_secret': self.client_secret
})
response.raise_for_status()
token_data = response.json()
self.access_token = token_data['access_token']
self.refresh_token = token_data.get('refresh_token', self.refresh_token)
# Clio tokens expire in ~24 hours; refresh 1 hour early
self.token_expiry = datetime.now() + timedelta(hours=23)
logger.info('Clio token refreshed successfully')
# Persist updated refresh token
self._save_refresh_token()
def _save_refresh_token(self):
"""Persist refresh token to .env file for service restarts."""
env_path = Path('C:/DepositionPipeline/.env')
if env_path.exists():
lines = env_path.read_text().splitlines()
new_lines = []
found = False
for line in lines:
if line.startswith('CLIO_REFRESH_TOKEN='):
new_lines.append(f'CLIO_REFRESH_TOKEN={self.refresh_token}')
found = True
else:
new_lines.append(line)
if not found:
new_lines.append(f'CLIO_REFRESH_TOKEN={self.refresh_token}')
env_path.write_text('\n'.join(new_lines))
def _headers(self):
self._ensure_token()
return {
'Authorization': f'Bearer {self.access_token}',
'Content-Type': 'application/json'
}
def get_matter(self, matter_id: int) -> dict:
"""Retrieve matter details from Clio."""
response = requests.get(
f'{CLIO_BASE_URL}/matters/{matter_id}.json',
headers=self._headers(),
params={'fields': 'id,display_number,description,client'}
)
response.raise_for_status()
return response.json()['data']
def search_matters(self, query: str) -> list:
"""Search for matters by name or number."""
response = requests.get(
f'{CLIO_BASE_URL}/matters.json',
headers=self._headers(),
params={'query': query, 'fields': 'id,display_number,description', 'limit': 10}
)
response.raise_for_status()
return response.json()['data']
def upload_document(self, matter_id: int, file_path: str, name: str, category: str = None):
"""Upload a document to a Clio matter."""
self._ensure_token()
file_path = Path(file_path)
if not file_path.exists():
raise FileNotFoundError(f'Document not found: {file_path}')
# Step 1: Create the document record
doc_data = {
'data': {
'name': name,
'parent': {'id': matter_id, 'type': 'Matter'},
}
}
response = requests.post(
f'{CLIO_BASE_URL}/documents.json',
headers=self._headers(),
json=doc_data
)
response.raise_for_status()
doc_id = response.json()['data']['id']
logger.info(f'Created Clio document record: {doc_id}')
# Step 2: Upload the file content
# Get the upload URL
upload_headers = {
'Authorization': f'Bearer {self.access_token}'
}
# Determine content type
content_types = {
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.txt': 'text/plain',
'.pdf': 'application/pdf',
'.json': 'application/json',
'.mp4': 'video/mp4',
'.wav': 'audio/wav'
}
content_type = content_types.get(file_path.suffix.lower(), 'application/octet-stream')
with open(file_path, 'rb') as f:
files = {'file': (file_path.name, f, content_type)}
put_data = {'data': json.dumps({'uuid': None})}
response = requests.put(
f'{CLIO_BASE_URL}/documents/{doc_id}.json',
headers=upload_headers,
files=files,
data=put_data
)
response.raise_for_status()
logger.info(f'Uploaded {file_path.name} to Clio document {doc_id} in matter {matter_id}')
return doc_idDeposition Session Manager
Type: agent A GUI application that runs on the deposition room workstation, providing the paralegal with a simple interface to start/stop recordings, enter case metadata (matter ID, deponent name, case caption), complete the consent checklist, and monitor recording status. Controls OBS Studio via WebSocket.
Implementation
# Tkinter-based GUI for deposition session management. Runs on the recording
# workstation; controls OBS via WebSocket.
# session_manager.py
# Tkinter-based GUI for deposition session management
# Runs on the recording workstation; controls OBS via WebSocket
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import logging
import threading
from pathlib import Path
from datetime import datetime
import websocket
import hashlib
import base64
logger = logging.getLogger('DepositionSessionManager')
OBS_WS_HOST = 'localhost'
OBS_WS_PORT = 4455
OBS_WS_PASSWORD = os.getenv('OBS_WS_PASSWORD', 'change-me-in-production')
METADATA_DIR = Path('C:/DepositionRecordings/Pending')
class OBSController:
"""Controls OBS Studio via obs-websocket protocol."""
def __init__(self, host, port, password):
self.url = f'ws://{host}:{port}'
self.password = password
self.ws = None
self.msg_id = 0
def connect(self):
self.ws = websocket.create_connection(self.url)
# Handle authentication (obs-websocket v5 protocol)
hello = json.loads(self.ws.recv())
if hello.get('d', {}).get('authentication'):
auth = hello['d']['authentication']
challenge = auth['challenge']
salt = auth['salt']
# Generate auth string
secret = base64.b64encode(
hashlib.sha256((self.password + salt).encode()).digest()
).decode()
auth_str = base64.b64encode(
hashlib.sha256((secret + challenge).encode()).digest()
).decode()
identify = {
'op': 1,
'd': {
'rpcVersion': 1,
'authentication': auth_str
}
}
self.ws.send(json.dumps(identify))
resp = json.loads(self.ws.recv())
def send_request(self, request_type, request_data=None):
self.msg_id += 1
msg = {
'op': 6,
'd': {
'requestType': request_type,
'requestId': str(self.msg_id),
'requestData': request_data or {}
}
}
self.ws.send(json.dumps(msg))
return json.loads(self.ws.recv())
def start_recording(self):
return self.send_request('StartRecord')
def stop_recording(self):
return self.send_request('StopRecord')
def get_record_status(self):
return self.send_request('GetRecordStatus')
class DepositionSessionGUI:
def __init__(self):
self.root = tk.Tk()
self.root.title('Deposition Capture Manager')
self.root.geometry('600x800')
self.root.configure(bg='#1a1a2e')
self.obs = OBSController(OBS_WS_HOST, OBS_WS_PORT, OBS_WS_PASSWORD)
self.is_recording = False
self.session_start = None
self._build_ui()
self._connect_obs()
def _build_ui(self):
style = ttk.Style()
style.configure('Title.TLabel', font=('Segoe UI', 16, 'bold'))
style.configure('Status.TLabel', font=('Segoe UI', 12))
# Header
header = ttk.Frame(self.root, padding=10)
header.pack(fill='x')
ttk.Label(header, text='DEPOSITION CAPTURE SYSTEM', style='Title.TLabel').pack()
# Case Info Frame
info_frame = ttk.LabelFrame(self.root, text='Case Information', padding=10)
info_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(info_frame, text='Clio Matter ID:').grid(row=0, column=0, sticky='e', padx=5)
self.matter_id_var = tk.StringVar()
ttk.Entry(info_frame, textvariable=self.matter_id_var, width=30).grid(row=0, column=1)
ttk.Label(info_frame, text='Case Caption:').grid(row=1, column=0, sticky='e', padx=5)
self.case_caption_var = tk.StringVar()
ttk.Entry(info_frame, textvariable=self.case_caption_var, width=30).grid(row=1, column=1)
ttk.Label(info_frame, text='Deponent Name:').grid(row=2, column=0, sticky='e', padx=5)
self.deponent_var = tk.StringVar()
ttk.Entry(info_frame, textvariable=self.deponent_var, width=30).grid(row=2, column=1)
ttk.Label(info_frame, text='Noticing Attorney:').grid(row=3, column=0, sticky='e', padx=5)
self.attorney_var = tk.StringVar()
ttk.Entry(info_frame, textvariable=self.attorney_var, width=30).grid(row=3, column=1)
# Consent Checklist
consent_frame = ttk.LabelFrame(self.root, text='Pre-Recording Consent Checklist', padding=10)
consent_frame.pack(fill='x', padx=10, pady=5)
self.consent_vars = []
checklist_items = [
'Deposition notice specifies recording method per FRCP 30(b)(3)',
'All parties informed of AI recording and transcription on the record',
'Written consent form signed (or objections noted on record)',
'Court reporter present (if required by jurisdiction)',
'Audio levels verified — all microphones showing signal',
'Backup microphone (EPOS Capture 5) verified',
'UPS battery status confirmed (>50%)',
'Sufficient storage space available on workstation and NAS'
]
for i, item in enumerate(checklist_items):
var = tk.BooleanVar()
self.consent_vars.append(var)
cb = ttk.Checkbutton(consent_frame, text=item, variable=var,
command=self._check_ready)
cb.grid(row=i, column=0, sticky='w', pady=2)
# Recording Controls
control_frame = ttk.Frame(self.root, padding=10)
control_frame.pack(fill='x', padx=10)
self.start_btn = ttk.Button(control_frame, text='▶ START RECORDING',
command=self._start_recording, state='disabled')
self.start_btn.pack(side='left', padx=5)
self.stop_btn = ttk.Button(control_frame, text='⏹ STOP RECORDING',
command=self._stop_recording, state='disabled')
self.stop_btn.pack(side='left', padx=5)
# Status Display
status_frame = ttk.LabelFrame(self.root, text='Recording Status', padding=10)
status_frame.pack(fill='x', padx=10, pady=5)
self.status_label = ttk.Label(status_frame, text='NOT RECORDING',
style='Status.TLabel', foreground='gray')
self.status_label.pack()
self.timer_label = ttk.Label(status_frame, text='00:00:00',
font=('Consolas', 24))
self.timer_label.pack()
self.obs_status = ttk.Label(status_frame, text='OBS: Connecting...',
foreground='orange')
self.obs_status.pack()
def _connect_obs(self):
try:
self.obs.connect()
self.obs_status.configure(text='OBS: Connected ✓', foreground='green')
except Exception as e:
self.obs_status.configure(text=f'OBS: Connection Failed ✗', foreground='red')
logger.error(f'OBS connection failed: {e}')
def _check_ready(self):
all_checked = all(var.get() for var in self.consent_vars)
has_info = bool(self.matter_id_var.get() and self.deponent_var.get())
if all_checked and has_info and not self.is_recording:
self.start_btn.configure(state='normal')
else:
self.start_btn.configure(state='disabled')
def _start_recording(self):
try:
# Save session metadata
session_id = datetime.now().strftime('%Y-%m-%d_Deposition_%H-%M-%S')
metadata = {
'session_id': session_id,
'clio_matter_id': self.matter_id_var.get(),
'case_caption': self.case_caption_var.get(),
'deponent': self.deponent_var.get(),
'noticing_attorney': self.attorney_var.get(),
'start_time': datetime.now().isoformat(),
'consent_checklist_completed': True
}
metadata_path = METADATA_DIR / f'{session_id}_metadata.json'
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
self.obs.start_recording()
self.is_recording = True
self.session_start = datetime.now()
self.status_label.configure(text='● RECORDING', foreground='red')
self.start_btn.configure(state='disabled')
self.stop_btn.configure(state='normal')
self._update_timer()
logger.info(f'Recording started: {session_id}')
except Exception as e:
messagebox.showerror('Recording Error', f'Failed to start recording: {e}')
def _stop_recording(self):
if messagebox.askyesno('Stop Recording', 'Are you sure you want to stop recording?'):
try:
self.obs.stop_recording()
self.is_recording = False
self.status_label.configure(text='RECORDING STOPPED — Processing...', foreground='blue')
self.stop_btn.configure(state='disabled')
logger.info('Recording stopped')
except Exception as e:
messagebox.showerror('Error', f'Failed to stop recording: {e}')
def _update_timer(self):
if self.is_recording and self.session_start:
elapsed = datetime.now() - self.session_start
hours, remainder = divmod(int(elapsed.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
self.timer_label.configure(text=f'{hours:02d}:{minutes:02d}:{seconds:02d}')
self.root.after(1000, self._update_timer)
def run(self):
self.root.mainloop()
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
app = DepositionSessionGUI()
app.run()Speaker Identification Post-Processor
Type: prompt A prompt template and post-processing workflow that takes the Deepgram diarization output (generic SPEAKER 1, SPEAKER 2 labels) and maps them to actual participant names based on the session metadata and deposition conventions (e.g., the first extended speaker is typically the questioning attorney, the most responding speaker is the deponent).
Implementation
Purpose
Deepgram's diarization outputs generic speaker labels (Speaker 0, Speaker 1, etc.). This component uses an LLM to map those labels to actual participant names/roles based on deposition context.
Claude Prompt Template
You are analyzing a deposition transcript with AI-generated speaker labels. Your task is to identify which speaker label corresponds to which participant based on contextual clues.
- Case Caption: {case_caption}
- Deponent: {deponent_name}
- Noticing Attorney: {noticing_attorney}
- Expected participants: Questioning attorney, defending attorney, deponent, court reporter
Deposition Convention Clues:
Here are the first 50 utterances of the transcript with speaker labels: {first_50_utterances}
Based on these clues, provide your best mapping. Respond in this exact JSON format:
{
"speaker_map": {
"0": {"name": "identified name or role", "role": "questioning_attorney|defending_attorney|deponent|court_reporter|unknown", "confidence": 0.0-1.0},
"1": {"name": "...", "role": "...", "confidence": 0.0-1.0}
},
"reasoning": "Brief explanation of how you determined each mapping"
}Python Implementation
# maps Deepgram speaker IDs to named participants
# speaker_identifier.py
import json
import anthropic
def identify_speakers(transcript_result: dict, metadata: dict, api_key: str) -> dict:
"""Map generic speaker IDs to actual names/roles."""
utterances = transcript_result.get('results', {}).get('utterances', [])
first_50 = utterances[:50]
utterance_text = '\n'.join([
f"Speaker {u['speaker']}: {u['transcript']}"
for u in first_50
])
prompt = f"""You are analyzing a deposition transcript with AI-generated speaker labels.
Deposition Metadata:
- Case Caption: {metadata.get('case_caption', 'Unknown')}
- Deponent: {metadata.get('deponent', 'Unknown')}
- Noticing Attorney: {metadata.get('noticing_attorney', 'Unknown')}
Deposition conventions: The questioning attorney typically speaks first after the court reporter,
asking the deponent to state their name. The deponent gives short answers early. The defending
attorney interjects with objections.
First 50 utterances:
{utterance_text}
Map each speaker number to a name and role. Respond in JSON:
{{
"speaker_map": {{
"0": {{"name": "...", "role": "questioning_attorney|defending_attorney|deponent|court_reporter|unknown", "confidence": 0.0-1.0}}
}},
"reasoning": "..."
}}"""
client = anthropic.Anthropic(api_key=api_key)
response = client.messages.create(
model='claude-sonnet-4-20250514',
max_tokens=1024,
messages=[{'role': 'user', 'content': prompt}]
)
result = json.loads(response.content[0].text)
return result
def apply_speaker_names(transcript_text: str, speaker_map: dict) -> str:
"""Replace generic SPEAKER N labels with identified names."""
for speaker_id, info in speaker_map.get('speaker_map', {}).items():
old_label = f'SPEAKER {int(speaker_id) + 1}'
name = info.get('name', old_label)
role = info.get('role', 'unknown')
confidence = info.get('confidence', 0)
if confidence >= 0.7:
new_label = f'{name} ({role.replace("_", " ").title()})'
else:
new_label = f'{old_label} [possibly {name}]'
transcript_text = transcript_text.replace(old_label, new_label)
return transcript_textIntegration Point
Call identify_speakers() after Deepgram transcription but before summarization. Insert into the pipeline between Step 2 (format transcript) and Step 3 (generate summary) in deposition_pipeline.py.
Testing & Validation
- AUDIO CAPTURE TEST: In each deposition room, have 4 people sit in different positions around the table. Each person reads a 2-minute scripted passage with legal terminology (objections, exhibit references, stipulations). Record via OBS. Verify: all 4 voices are clearly captured, no dropouts, audio levels between -12dB and -6dB peak on OBS meters for both the Shure ceiling mic and EPOS tabletop mic tracks.
- SPEAKER DIARIZATION ACCURACY TEST: Using the 4-person test recording, run through Deepgram and verify that speaker diarization correctly identifies at least 4 distinct speakers. Acceptable threshold: >90% of utterances attributed to the correct speaker. Have a paralegal manually score 50 random utterances.
- TRANSCRIPTION ACCURACY TEST (WORD ERROR RATE): Using the scripted test passage (where exact text is known), compare Deepgram's output word-by-word. Calculate WER = (substitutions + insertions + deletions) / total words. Acceptable threshold: WER < 8% for clear audio. Test with both normal speaking volume and soft speaking to verify.
- LEGAL TERMINOLOGY RECOGNITION TEST: Include these terms in the test script: 'plaintiff', 'defendant', 'stipulate', 'objection', 'hearsay', 'deposition', 'exhibit', 'sustained', 'overruled', 'interrogatories', 'subpoena duces tecum'. Verify Deepgram transcribes all correctly. Use Deepgram keyword boosting if any fail.
- CROSSTALK HANDLING TEST: Have two speakers deliberately talk over each other for 30 seconds. Verify that Deepgram captures content from both speakers (even if attribution is imperfect) and does not drop significant content during the overlap.
- END-TO-END PIPELINE TIMING TEST: Record a 30-minute mock deposition. Stop recording and start a timer. Measure: (a) time for file to appear in Pending directory, (b) time for Deepgram transcription to complete, (c) time for CaseMark/Claude summary generation, (d) time for documents to appear in Clio. Total acceptable: <15 minutes for a 30-minute recording. Document each phase timing.
- SUMMARY QUALITY VALIDATION: Plant 5 specific 'key admission' statements in the mock deposition script. After AI summary generation, verify the summary correctly identifies and cites all 5 admissions with accurate page:line references. Have a senior paralegal or attorney review the full summary for legal accuracy and usefulness.
- CONSENT WORKFLOW TEST: Launch the Deposition Session Manager GUI. Attempt to start recording without completing all checklist items — verify the Start button remains disabled. Complete all items and verify recording starts successfully. Verify the metadata JSON file is created with correct case information.
- NAS ARCHIVAL TEST: After a mock deposition completes processing, verify: (a) recording file exists in the NAS DepositionRecordings share, (b) transcript exists in the Transcripts share, (c) summary DOCX exists in the AISummaries share. Verify files are encrypted at rest by checking Synology shared folder encryption status.
- AZURE BACKUP VERIFICATION TEST: After the Synology Hyper Backup schedule runs, verify files are replicated to Azure Blob Storage. Log into Azure Portal > Storage Account > Container and confirm deposition files are present. Test a restore by downloading a file from Azure and verifying it matches the NAS original.
- CLIO INTEGRATION TEST: Verify the summary DOCX and transcript TXT are automatically filed to the correct matter in Clio. Open the matter in Clio, navigate to Documents, confirm both files are present with correct names and document categories. Verify file contents are intact by downloading from Clio and opening.
- POWER FAILURE RESILIENCE TEST: During a mock recording, unplug the workstation's AC power (UPS should take over). Verify: (a) UPS alarm sounds, (b) recording continues uninterrupted, (c) PowerChute initiates graceful shutdown after 5 minutes on battery. Reconnect power and verify OBS recording file is intact (MKV container should be uncorrupted).
- FAILOVER INTERNET TEST: During a pipeline processing run (Deepgram transcription in progress), simulate primary internet failure by disconnecting the WAN cable. Verify: (a) pipeline retries after connection is restored, (b) no data loss, (c) the retry logic in transcriber.py handles the failure gracefully.
- MULTI-DEPOSITION CONCURRENT TEST: Start recordings in both deposition rooms simultaneously. Run both through the pipeline and verify they process correctly without conflicts — separate session IDs, separate metadata files, separate Clio matter filings.
- ACCESS CONTROL TEST: Attempt to access the NAS DepositionRecordings share with the attorney-readonly account — verify read-only access (can view/download but not delete or modify). Attempt access with an unauthorized account — verify access is denied. Verify BitLocker is active on both recording workstations.
Client Handoff
The client handoff should be conducted over two sessions.
SESSION 1 (2 hours, attorneys + paralegals)
SESSION 2 (1 hour, designated Deposition Technology Coordinator + managing partner)
Documentation Package
Success Criteria
Maintenance
Ongoing Maintenance Schedule
Weekly
- Review pipeline processing logs for errors or stuck jobs (C:\DepositionPipeline\logs\pipeline.log).
- Check NAS storage utilization and project when additional drives or archival pruning will be needed.
- Verify Synology Hyper Backup completed successfully in the past 7 days.
Monthly
- Apply Windows updates to recording workstations during a scheduled maintenance window (never during business hours when depositions may be scheduled).
- Check for OBS Studio updates and test in a non-production environment before deploying.
- Review Deepgram API usage and costs in the Deepgram console — alert the client if usage is trending above budget.
- Run S.M.A.R.T. extended tests on NAS drives.
- Verify UPS battery health via PowerChute reports.
Quarterly
- Conduct a test restore from Azure Blob Storage backup to verify recoverability.
- Review and update the Deepgram keyword boost list with any new case-specific terminology the firm has encountered.
- Review CaseMark summary quality with the firm's senior paralegal — adjust templates if needed.
- Audit access controls on the NAS and Clio API integration.
- Review consent/disclosure templates with firm ethics counsel for any regulatory changes.
Semi-Annually
- Full system health check including microphone calibration (verify Shure MXA920 coverage zones haven't drifted), network performance testing, and end-to-end pipeline timing test.
- Review API vendor compliance certifications (SOC 2 reports from Deepgram, CaseMark).
SLA Considerations
- Severity 1 (1-hour response, 4-hour resolution): Recording workstation issues affecting an active or imminent deposition.
- Severity 2 (4-hour response, next business day resolution): Pipeline processing failures (stuck transcriptions, failed summaries).
- Severity 2 (4-hour response, next business day resolution): NAS or backup failures.
- Severity 3 (next business day): Routine configuration changes.
Escalation Path
- Tier 1 (MSP helpdesk): Handles workstation restarts, pipeline service restarts, basic OBS troubleshooting.
- Tier 2 (MSP engineer): Handles API integration issues, Deepgram/CaseMark configuration, NAS administration.
- Tier 3 (MSP architect + vendor support): Handles Shure microphone hardware issues, Deepgram API outages, CaseMark platform issues.
Model/API Update Triggers
- If Deepgram releases a new model version (e.g., Nova-4), test with 5 sample depositions in a staging environment before switching production.
- If CaseMark updates their summary templates, review output with the firm before accepting changes.
- If Anthropic releases new Claude models, evaluate for cost/quality improvements on the custom summarization path.
Alternatives
Turnkey CaseMark-Only Approach (No Custom Pipeline)
Instead of building the custom Python pipeline with Deepgram + Claude + Clio integration, use CaseMark as the sole platform. The paralegal manually uploads completed recording files to the CaseMark white-label portal, CaseMark handles both transcription and summarization, and the paralegal manually downloads the summary and files it in the PMS. No custom code or API integration required.
Full Custom Pipeline with Self-Hosted Whisper
Replace the Deepgram cloud API with a self-hosted OpenAI Whisper model running on an on-premises GPU workstation. Transcription happens entirely on-premises with no audio data ever leaving the firm's network. Use Claude API only for summarization (much smaller data payload than full audio).
Otter.ai Business + Manual Summarization
Use Otter.ai Business for real-time transcription during depositions, with attorneys and paralegals manually reviewing and summarizing transcripts using traditional methods or basic AI assistants.
Steno Full-Service Approach
Engage Steno as a full-service provider that combines human court reporting, AI-assisted transcription via Transcript Genius, and deposition logistics. Steno handles everything end-to-end — the MSP only provides hardware and network infrastructure.
Want early access to the full toolkit?