What You’ll Build
A complete deposition processing pipeline that:
- Transcribes audio with speaker identification
- Stores transcripts in a searchable vault
- Generates timelines of key testimony
- Cross-references witnesses to find contradictions
- Produces formatted impeachment reports
Time to complete: 30 minutes
Architecture
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Audio Files │ ──▶ │ Voice │ ──▶ │ Vault │
│ (per witness)│ │ Transcription│ │ Storage │
└──────────────┘ │ + Speakers │ │ + Indexing │
└──────────────┘ └──────┬───────┘
│
┌──────────────┐ ┌──────────────┐ ┌──────▼───────┐
│ PDF Report │ ◀── │ LLM │ ◀── │ Vault │
│ (Format) │ │ Analysis │ │ Search │
└──────────────┘ └──────────────┘ └──────────────┘
Prerequisites
- Case.dev API key (get one here)
- A vault for storing transcripts
- Deposition audio files (MP3, M4A, WAV, etc.)
Step 1: Set Up Your Vault
Create a vault to hold all deposition transcripts for a case:
import Casedev from 'casedev';
const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });
const vault = await client.vault.create({
name: 'Smith v. Jones — Depositions'
});
console.log(`Vault ID: ${vault.id}`);
Step 2: Upload and Transcribe a Deposition
Upload the audio file to your vault, then transcribe with speaker labels:
async function transcribeDeposition(
vaultId: string,
audioFile: Buffer,
filename: string,
witnessName: string,
speakersExpected: number = 3 // Witness, attorney, opposing counsel
) {
// 1. Upload audio to the vault
const upload = await client.vault.upload(vaultId, {
filename,
contentType: 'audio/mpeg',
metadata: {
witness: witnessName,
type: 'deposition-audio'
}
});
await fetch(upload.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'audio/mpeg' },
body: audioFile
});
// 2. Transcribe with speaker labels
const job = await client.voice.transcription.create({
vault_id: vaultId,
object_id: upload.objectId,
speaker_labels: true,
speakers_expected: speakersExpected,
word_boost: ['plaintiff', 'defendant', 'counsel', 'objection', 'stipulate']
});
// 3. Poll until complete
let result = await client.voice.transcription.retrieve(job.id);
while (result.status !== 'completed' && result.status !== 'failed') {
await new Promise(r => setTimeout(r, 10000));
result = await client.voice.transcription.retrieve(job.id);
}
if (result.status === 'failed') {
throw new Error(`Transcription failed: ${result.error}`);
}
console.log(`Transcribed ${witnessName}: ${result.word_count} words, ${Math.round(result.audio_duration / 60)} minutes`);
return result;
}
Speaker labels are automatic. The transcription service identifies speakers by voice characteristics. Set speakers_expected for better accuracy when you know the number of participants.
Step 3: Store Transcript in the Vault
The vault-based transcription automatically stores the transcript. Now ingest it for semantic search:
async function indexTranscript(
vaultId: string,
resultObjectId: string,
witnessName: string
) {
// Ingest the transcript for embedding generation + search
await client.vault.ingest(vaultId, resultObjectId);
// Poll until indexed
let obj = await client.vault.objects.retrieve(vaultId, resultObjectId);
while (obj.ingestionStatus === 'processing') {
await new Promise(r => setTimeout(r, 5000));
obj = await client.vault.objects.retrieve(vaultId, resultObjectId);
}
console.log(`Indexed transcript for ${witnessName}: ${obj.ingestionStatus}`);
return obj;
}
For production, use webhooks instead of polling for both transcription and ingestion status changes.
Step 4: Batch Process Multiple Depositions
Process all depositions for a case in sequence:
interface Deposition {
audioFile: Buffer;
filename: string;
witnessName: string;
date: string;
}
async function processCase(vaultId: string, depositions: Deposition[]) {
const results = [];
for (const depo of depositions) {
console.log(`\nProcessing: ${depo.witnessName} (${depo.date})`);
// Transcribe
const transcription = await transcribeDeposition(
vaultId,
depo.audioFile,
depo.filename,
depo.witnessName
);
// Index for search
await indexTranscript(vaultId, transcription.result_object_id, depo.witnessName);
results.push({
witness: depo.witnessName,
date: depo.date,
objectId: transcription.result_object_id,
wordCount: transcription.word_count,
duration: transcription.audio_duration
});
}
console.log(`\nProcessed ${results.length} depositions`);
return results;
}
Step 5: Generate a Testimony Timeline
Search across all depositions and build a chronological timeline of events:
async function generateTimeline(vaultId: string, topic: string) {
// Search across all indexed transcripts
const results = await client.vault.search(vaultId, {
query: `timeline of events regarding ${topic}`,
method: 'hybrid',
limit: 20
});
const chunks = results.chunks.map(c => c.text).join('\n\n');
const sources = results.sources.map(s => s.filename).join(', ');
// Generate structured timeline
const timeline = await client.llm.v1.chat.createCompletion({
model: 'anthropic/claude-sonnet-4.5',
messages: [
{
role: 'system',
content: `You are a litigation paralegal. Create a chronological timeline from deposition testimony.
For each event:
- Date (exact or approximate)
- What happened
- Who testified about it (witness name)
- Direct quote from testimony
- Page/line reference if available
Format as a markdown table sorted by date.`
},
{
role: 'user',
content: `Topic: ${topic}\n\nTestimony excerpts:\n${chunks}\n\nSources: ${sources}`
}
],
temperature: 0.2
});
return timeline.choices[0].message.content;
}
Step 6: Find Contradictions Across Witnesses
Search for a topic and compare what different witnesses said:
async function findContradictions(vaultId: string, topic: string) {
// Search across all testimony for the topic
const results = await client.vault.search(vaultId, {
query: topic,
method: 'hybrid',
limit: 20
});
// Group by source file (each transcript = one witness)
const byWitness: Record<string, string[]> = {};
for (const chunk of results.chunks) {
const witness = chunk.metadata?.witness || chunk.filename;
if (!byWitness[witness]) byWitness[witness] = [];
byWitness[witness].push(chunk.text);
}
// Use AI to find contradictions
const analysis = await client.llm.v1.chat.createCompletion({
model: 'anthropic/claude-sonnet-4.5',
messages: [
{
role: 'system',
content: `You are a litigation attorney preparing for cross-examination.
Analyze testimony from multiple witnesses and identify:
1. Direct contradictions (conflicting facts)
2. Inconsistencies (different emphasis or omissions)
3. Corroborations (matching testimony across witnesses)
For each finding, cite the specific quote and witness name.
Rate each contradiction as HIGH, MEDIUM, or LOW significance.`
},
{
role: 'user',
content: `Topic: ${topic}\n\nTestimony by witness:\n${
Object.entries(byWitness)
.map(([w, texts]) => `\n## ${w}\n${texts.join('\n')}`)
.join('\n')
}`
}
],
temperature: 0.3
});
return analysis.choices[0].message.content;
}
Step 7: Generate a PDF Impeachment Report
Combine the timeline and contradiction analysis into a formatted report:
async function generateImpeachmentReport(
vaultId: string,
caseName: string,
topics: string[]
) {
let reportContent = `# Impeachment Report\n\n**Case:** ${caseName}\n**Generated:** ${new Date().toLocaleDateString()}\n\n---\n\n`;
// Generate timeline
reportContent += `## Chronological Timeline\n\n`;
const timeline = await generateTimeline(vaultId, caseName);
reportContent += timeline + '\n\n---\n\n';
// Analyze each topic
reportContent += `## Contradiction Analysis\n\n`;
for (const topic of topics) {
reportContent += `### ${topic}\n\n`;
const contradictions = await findContradictions(vaultId, topic);
reportContent += contradictions + '\n\n';
}
// Convert to PDF
const report = await client.format.v1.document({
content: reportContent,
input_format: 'md',
output_format: 'pdf',
options: {
template: 'standard',
header: `CONFIDENTIAL — ${caseName} — Impeachment Report`,
footer: 'Page {{page}} of {{pages}}',
styles: {
h1: { font: 'Times New Roman', size: 16, bold: true, alignment: 'center' },
h2: { font: 'Times New Roman', size: 14, bold: true },
h3: { font: 'Times New Roman', size: 12, bold: true },
p: { font: 'Times New Roman', size: 11, spacingAfter: 8 }
}
}
});
return report;
}
Complete Example
Putting it all together:
import Casedev from 'casedev';
import fs from 'fs';
const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });
async function main() {
// Create a vault for the case
const vault = await client.vault.create({
name: 'Smith v. Jones — Depositions'
});
// Process depositions
await processCase(vault.id, [
{
audioFile: fs.readFileSync('depositions/ceo-johnson.m4a'),
filename: 'ceo-johnson.m4a',
witnessName: 'CEO Johnson',
date: '2024-03-15'
},
{
audioFile: fs.readFileSync('depositions/cto-smith.m4a'),
filename: 'cto-smith.m4a',
witnessName: 'CTO Smith',
date: '2024-03-18'
},
{
audioFile: fs.readFileSync('depositions/it-director-williams.m4a'),
filename: 'it-director-williams.m4a',
witnessName: 'IT Director Williams',
date: '2024-03-20'
}
]);
// Generate impeachment report
const report = await generateImpeachmentReport(
vault.id,
'Smith v. Jones',
[
'When the CEO learned of the data breach',
'Who authorized the system shutdown',
'Timeline of security notifications'
]
);
console.log('Report generated successfully');
}
main();
Example Contradiction Report
## CONTRADICTION IDENTIFIED — HIGH SIGNIFICANCE
### Topic: When the CEO learned of the data breach
**CEO Johnson (Deposition, March 15, 2024):**
> "I was not informed of any security incident until July 15th,
> when our IT director called me at home."
**CTO Smith (Deposition, March 18, 2024):**
> "I sent an urgent email to the entire executive team, including
> the CEO, on July 12th explicitly detailing the intrusion and
> recommending immediate action."
**IT Director Williams (Deposition, March 20, 2024):**
> "The CTO instructed me to brief the CEO on July 12th. I called
> his office but was told he was unavailable. I left a detailed
> voicemail."
### Analysis
The CEO claims no knowledge until July 15th, but both the CTO and
IT Director indicate notification attempts on July 12th — three days
earlier. The CTO claims direct email notification, and the IT
Director confirms a voicemail was left.
### Impeachment Strategy
1. Introduce July 12th email as Exhibit A
2. Subpoena CEO's voicemail records for July 12th
3. Confront CEO with CTO and IT Director testimony
Production Tips
Error Handling
try {
const result = await transcribeDeposition(vaultId, audio, filename, witness);
} catch (error) {
if (error.status === 413) {
console.error('Audio file too large — split into segments');
} else if (error.status === 429) {
console.error('Rate limited — retry after a delay');
} else {
console.error('Transcription failed:', error.message);
}
}
Use Webhooks in Production
Instead of polling, subscribe to events for transcription and ingestion completion:
// Subscribe to vault events
await client.vault.events.subscriptions.create(vaultId, {
url: 'https://your-app.com/webhooks/vault',
events: ['object.ingested', 'object.failed']
});
// Subscribe to transcription events
// Your webhook handler receives status updates automatically
For long depositions (2+ hours), transcription may take several minutes. Webhooks prevent wasted polling requests.
Next Steps