Transcripts
Transcripts provide a text record of conversations, enabling analytics, compliance, quality assurance, and AI training.
Transcript Types
| Type | When Available | Use Case |
|---|---|---|
| Real-time | During call | Live monitoring, agent assist |
| Post-call | After call ends | QA, analytics, compliance |
| Summary | After call ends | Quick review, CRM notes |
Configuration
Enable Transcripts
{
"agent": {
"transcripts": {
"enabled": true,
"includeTimestamps": true,
"includeConfidence": true,
"generateSummary": true,
"storage": "s3",
"retentionDays": 90
}
}
}
Real-time Transcripts
WebSocket Events
{
"event": "transcript.updated",
"data": {
"call_id": "call_abc123",
"turn_id": "turn_5",
"role": "user",
"text": "What's the status of my order?",
"is_final": true,
"confidence": 0.95,
"timestamp_ms": 12500,
"words": [
{"word": "What's", "start": 12500, "end": 12700, "confidence": 0.98},
{"word": "the", "start": 12700, "end": 12850, "confidence": 0.99},
{"word": "status", "start": 12850, "end": 13200, "confidence": 0.96}
]
}
}
Implementation
type TranscriptHandler struct {
transcripts map[string]*Transcript
storage Storage
}
type Transcript struct {
CallID string
Turns []TranscriptTurn
StartTime time.Time
}
type TranscriptTurn struct {
ID string
Role string // "user" or "assistant"
Text string
Confidence float32
Timestamp time.Duration
Words []WordTiming
}
func (h *TranscriptHandler) OnTranscript(event TranscriptEvent) {
transcript := h.getOrCreate(event.CallID)
turn := TranscriptTurn{
ID: event.TurnID,
Role: event.Role,
Text: event.Text,
Confidence: event.Confidence,
Timestamp: time.Duration(event.TimestampMs) * time.Millisecond,
Words: event.Words,
}
transcript.AddTurn(turn)
// Emit webhook
h.webhooks.Send("transcript.updated", event)
}
Post-Call Transcripts
Transcript Structure
{
"id": "trans_xyz789",
"call_id": "call_abc123",
"agent_id": "agent_456",
"duration_ms": 180000,
"language": "en-US",
"turns": [
{
"id": "turn_1",
"role": "assistant",
"text": "Hello! Thank you for calling Acme Support. How can I help you today?",
"timestamp_ms": 0,
"duration_ms": 3500
},
{
"id": "turn_2",
"role": "user",
"text": "Hi, I want to check the status of my order.",
"timestamp_ms": 4000,
"duration_ms": 2500,
"confidence": 0.94
},
{
"id": "turn_3",
"role": "assistant",
"text": "I'd be happy to help with that. What's your order number?",
"timestamp_ms": 6800,
"duration_ms": 2800
}
],
"metadata": {
"topics": ["order_status"],
"sentiment": "neutral",
"intent": "check_order",
"entities": [
{"type": "order_id", "value": "ORD-12345", "turn_id": "turn_4"}
]
},
"summary": "Customer called to check order status. Agent looked up order ORD-12345 which has shipped. Customer was satisfied with the update.",
"created_at": "2024-12-28T16:35:00Z"
}
Generating Summaries
func (h *TranscriptHandler) generateSummary(transcript *Transcript) string {
// Build conversation text
var conversation strings.Builder
for _, turn := range transcript.Turns {
conversation.WriteString(fmt.Sprintf("%s: %s\n", turn.Role, turn.Text))
}
// Use LLM to generate summary
prompt := fmt.Sprintf(`Summarize this customer service call in 2-3 sentences:
%s
Summary:`, conversation.String())
summary, _ := h.llm.Generate(context.Background(), []Message{
{Role: "user", Content: prompt},
})
return summary
}
Accessing Transcripts
Via API
# Get transcript for a call
curl https://api.edesy.in/v1/transcripts/trans_xyz789 \
-H "Authorization: Bearer $API_KEY"
# List transcripts
curl "https://api.edesy.in/v1/transcripts?call_id=call_abc123" \
-H "Authorization: Bearer $API_KEY"
# Get transcript as text
curl https://api.edesy.in/v1/transcripts/trans_xyz789/text \
-H "Authorization: Bearer $API_KEY"
Text Format
[00:00] Assistant: Hello! Thank you for calling Acme Support. How can I help you today?
[00:04] User: Hi, I want to check the status of my order.
[00:07] Assistant: I'd be happy to help with that. What's your order number?
[00:10] User: It's ORD-12345.
[00:13] Assistant: Let me look that up for you.
[00:18] Assistant: Your order has been shipped and is expected to arrive by December 30th.
[00:23] User: Great, thank you!
[00:25] Assistant: You're welcome! Is there anything else I can help with?
[00:28] User: No, that's all. Bye!
[00:30] Assistant: Thank you for calling. Have a great day!
Via Webhook
{
"event": "call.ended",
"data": {
"call_id": "call_abc123",
"transcript": {
"id": "trans_xyz789",
"url": "https://api.edesy.in/v1/transcripts/trans_xyz789",
"summary": "Customer checked order status. Order has shipped.",
"turn_count": 10
}
}
}
Analytics
Sentiment Analysis
type SentimentAnalyzer struct {
llm LLMProvider
}
func (a *SentimentAnalyzer) Analyze(transcript *Transcript) SentimentResult {
// Per-turn sentiment
for _, turn := range transcript.Turns {
if turn.Role == "user" {
turn.Sentiment = a.analyzeTurn(turn.Text)
}
}
// Overall sentiment
return SentimentResult{
Overall: a.calculateOverall(transcript.Turns),
Trend: a.calculateTrend(transcript.Turns),
KeyMoments: a.findKeyMoments(transcript.Turns),
}
}
func (a *SentimentAnalyzer) analyzeTurn(text string) string {
prompt := fmt.Sprintf(`Classify the sentiment of this customer statement as positive, negative, or neutral:
"%s"
Sentiment:`, text)
result, _ := a.llm.Generate(context.Background(), []Message{
{Role: "user", Content: prompt},
})
return strings.TrimSpace(strings.ToLower(result))
}
Topic Extraction
func extractTopics(transcript *Transcript) []string {
// Combine all text
var text strings.Builder
for _, turn := range transcript.Turns {
text.WriteString(turn.Text + " ")
}
prompt := fmt.Sprintf(`Extract the main topics discussed in this conversation as a JSON array:
%s
Topics:`, text.String())
result, _ := llm.Generate(context.Background(), []Message{
{Role: "user", Content: prompt},
})
var topics []string
json.Unmarshal([]byte(result), &topics)
return topics
}
Entity Extraction
type Entity struct {
Type string // order_id, phone, email, name, etc.
Value string
TurnID string
}
func extractEntities(transcript *Transcript) []Entity {
var entities []Entity
for _, turn := range transcript.Turns {
// Order IDs
orderPattern := regexp.MustCompile(`(?i)(order|ord)[#\-\s]?(\d+)`)
matches := orderPattern.FindAllStringSubmatch(turn.Text, -1)
for _, match := range matches {
entities = append(entities, Entity{
Type: "order_id",
Value: match[2],
TurnID: turn.ID,
})
}
// Phone numbers
phonePattern := regexp.MustCompile(`\b\d{3}[-.]?\d{3}[-.]?\d{4}\b`)
phones := phonePattern.FindAllString(turn.Text, -1)
for _, phone := range phones {
entities = append(entities, Entity{
Type: "phone",
Value: phone,
TurnID: turn.ID,
})
}
// Emails
emailPattern := regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`)
emails := emailPattern.FindAllString(turn.Text, -1)
for _, email := range emails {
entities = append(entities, Entity{
Type: "email",
Value: email,
TurnID: turn.ID,
})
}
}
return entities
}
Search and Query
Full-Text Search
type TranscriptSearch struct {
elastic *elasticsearch.Client
}
func (s *TranscriptSearch) Index(transcript *Transcript) error {
doc := map[string]any{
"call_id": transcript.CallID,
"agent_id": transcript.AgentID,
"full_text": transcript.FullText(),
"summary": transcript.Summary,
"topics": transcript.Topics,
"sentiment": transcript.Sentiment,
"created_at": transcript.CreatedAt,
}
_, err := s.elastic.Index(
"transcripts",
esutil.NewJSONReader(doc),
s.elastic.Index.WithDocumentID(transcript.ID),
)
return err
}
func (s *TranscriptSearch) Search(query string) ([]Transcript, error) {
res, _ := s.elastic.Search(
s.elastic.Search.WithIndex("transcripts"),
s.elastic.Search.WithBody(strings.NewReader(fmt.Sprintf(`{
"query": {
"multi_match": {
"query": "%s",
"fields": ["full_text", "summary", "topics"]
}
}
}`, query))),
)
// Parse and return results
return parseSearchResults(res)
}
Privacy and Compliance
PII Redaction
func redactPII(transcript *Transcript) *Transcript {
redacted := *transcript
for i, turn := range redacted.Turns {
// Redact credit card numbers
turn.Text = redactPattern(turn.Text, `\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b`, "[CARD]")
// Redact SSN
turn.Text = redactPattern(turn.Text, `\b\d{3}-\d{2}-\d{4}\b`, "[SSN]")
// Redact phone numbers
turn.Text = redactPattern(turn.Text, `\b\d{3}[-.]?\d{3}[-.]?\d{4}\b`, "[PHONE]")
redacted.Turns[i] = turn
}
return &redacted
}
func redactPattern(text, pattern, replacement string) string {
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(text, replacement)
}
Data Retention
func (h *TranscriptHandler) ApplyRetention() {
cutoff := time.Now().AddDate(0, 0, -h.retentionDays)
expired, _ := h.db.Find(&Transcript{
CreatedAt: lt(cutoff),
})
for _, transcript := range expired {
h.storage.Delete(transcript.StorageURL)
h.db.Delete(&transcript)
}
}
Best Practices
1. Include Word-Level Timestamps
Enables precise audio-text alignment:
{
"words": [
{"word": "order", "start": 12850, "end": 13200, "confidence": 0.96}
]
}
2. Store Raw and Processed
type TranscriptStorage struct {
Raw string // Exactly as transcribed
Processed string // Cleaned, formatted
Redacted string // PII removed
}
3. Link to Recordings
{
"transcript_id": "trans_xyz",
"recording_id": "rec_abc",
"alignment": {
"turn_1": {"start_ms": 0, "end_ms": 3500}
}
}