formatters.pyā¢11.8 kB
"""
Formatters for displaying exchanges in various formats.
"""
import json
from datetime import datetime
from typing import Optional, List
from voice_mode.exchanges.models import Exchange, Conversation
class ExchangeFormatter:
"""Format exchanges for display."""
# Color codes for terminal output
COLORS = {
'reset': '\033[0m',
'bold': '\033[1m',
'dim': '\033[2m',
'red': '\033[31m',
'green': '\033[32m',
'yellow': '\033[33m',
'blue': '\033[34m',
'magenta': '\033[35m',
'cyan': '\033[36m',
'white': '\033[37m',
}
@classmethod
def simple(cls, exchange: Exchange, color: bool = True, show_timing: bool = True) -> str:
"""One-line format with optional color.
Format: [HH:MM:SS] š¤/š TYPE [transport] Text [timing]
Args:
exchange: Exchange to format
color: Whether to include color codes
show_timing: Whether to show timing info
Returns:
Formatted string
"""
# Format timestamp
time_str = exchange.timestamp.strftime("%H:%M:%S")
# Choose emoji and color based on type
if exchange.is_stt:
emoji = "š¤"
type_str = "STT"
type_color = cls.COLORS['green'] if color else ''
else:
emoji = "š"
type_str = "TTS"
type_color = cls.COLORS['blue'] if color else ''
# Get transport
transport = exchange.metadata.transport if exchange.metadata else "unknown"
# Format timing if available
timing_str = ""
if show_timing and exchange.metadata and exchange.metadata.timing:
timing_str = f" [{exchange.metadata.timing}]"
# Build the output
parts = []
# Time
if color:
parts.append(f"{cls.COLORS['dim']}[{time_str}]{cls.COLORS['reset']}")
else:
parts.append(f"[{time_str}]")
# Emoji and type
parts.append(emoji)
if color:
parts.append(f"{type_color}{type_str}{cls.COLORS['reset']}")
else:
parts.append(type_str)
# Transport
if color:
parts.append(f"{cls.COLORS['dim']}[{transport}]{cls.COLORS['reset']}")
else:
parts.append(f"[{transport}]")
# Text (truncated if too long)
text = exchange.text
if len(text) > 80:
text = text[:77] + "..."
parts.append(text)
# Timing
if timing_str:
if color:
parts.append(f"{cls.COLORS['dim']}{timing_str}{cls.COLORS['reset']}")
else:
parts.append(timing_str)
return " ".join(parts)
@classmethod
def pretty(cls, exchange: Exchange, truncate: int = 80, show_metadata: bool = True) -> str:
"""Pretty format with box drawing and metadata.
Args:
exchange: Exchange to format
truncate: Maximum text length (0 for no truncation)
show_metadata: Whether to show metadata
Returns:
Multi-line formatted string
"""
lines = []
# Header line
time_str = exchange.timestamp.strftime("%H:%M:%S")
type_str = "STT" if exchange.is_stt else "TTS"
emoji = "šļø" if exchange.is_stt else "š"
transport = exchange.metadata.transport if exchange.metadata else "unknown"
header = f"āā {time_str} ā {type_str} ā {emoji} {transport} "
header += "ā" * (80 - len(header) - 1) + "ā"
lines.append(header)
# Text content (word-wrapped if needed)
text = exchange.text
if truncate > 0 and len(text) > truncate:
text = text[:truncate-3] + "..."
# Simple word wrapping
text_lines = []
words = text.split()
current_line = ""
for word in words:
if len(current_line) + len(word) + 1 <= 76: # Leave room for borders
if current_line:
current_line += " "
current_line += word
else:
if current_line:
text_lines.append(current_line)
current_line = word
if current_line:
text_lines.append(current_line)
for line in text_lines:
lines.append(f"ā {line:<76} ā")
# Metadata
if show_metadata and exchange.metadata:
lines.append("ā" + "ā" * 78 + "ā¤")
# Provider info
if exchange.metadata.provider or exchange.metadata.model:
provider_str = f"Provider: {exchange.metadata.provider or 'unknown'}"
if exchange.metadata.model:
provider_str += f" | Model: {exchange.metadata.model}"
if exchange.metadata.voice and exchange.is_tts:
provider_str += f" | Voice: {exchange.metadata.voice}"
lines.append(f"ā {provider_str:<76} ā")
# Timing
if exchange.metadata.timing:
lines.append(f"ā Timing: {exchange.metadata.timing:<68} ā")
# Audio format
if exchange.metadata.audio_format:
lines.append(f"ā Audio Format: {exchange.metadata.audio_format:<62} ā")
# Footer
lines.append("ā" + "ā" * 78 + "ā")
return "\n".join(lines)
@classmethod
def json(cls, exchange: Exchange, indent: int = 2) -> str:
"""JSON format.
Args:
exchange: Exchange to format
indent: JSON indentation level
Returns:
Pretty-printed JSON string
"""
return json.dumps(exchange.to_dict(), indent=indent, default=str)
@classmethod
def markdown(cls, conversation: Conversation, include_metadata: bool = False) -> str:
"""Markdown transcript format.
Args:
conversation: Conversation to format
include_metadata: Whether to include metadata table
Returns:
Markdown-formatted string
"""
lines = []
# Header
lines.append(f"# Conversation {conversation.id}")
lines.append("")
# Metadata table
if include_metadata:
lines.append("| Property | Value |")
lines.append("|----------|-------|")
lines.append(f"| Start Time | {conversation.start_time.strftime('%Y-%m-%d %H:%M:%S')} |")
lines.append(f"| End Time | {conversation.end_time.strftime('%Y-%m-%d %H:%M:%S')} |")
lines.append(f"| Duration | {conversation.duration} |")
lines.append(f"| Exchanges | {conversation.exchange_count} |")
if conversation.project_path:
lines.append(f"| Project | `{conversation.project_path}` |")
lines.append("")
# Transcript
lines.append("## Transcript")
lines.append("")
for exchange in conversation.exchanges:
time_str = exchange.timestamp.strftime("%H:%M:%S")
speaker = "**User**" if exchange.is_stt else "**Assistant**"
lines.append(f"*[{time_str}]* {speaker}: {exchange.text}")
lines.append("")
return "\n".join(lines)
@classmethod
def csv_header(cls) -> str:
"""Get CSV header row."""
return "timestamp,conversation_id,type,text,transport,provider,model,voice,timing"
@classmethod
def csv(cls, exchange: Exchange) -> str:
"""CSV format for single exchange.
Args:
exchange: Exchange to format
Returns:
CSV row string
"""
# Escape text for CSV
text = exchange.text.replace('"', '""')
if ',' in text or '\n' in text or '"' in text:
text = f'"{text}"'
# Get metadata fields
transport = exchange.metadata.transport if exchange.metadata else ""
provider = exchange.metadata.provider if exchange.metadata else ""
model = exchange.metadata.model if exchange.metadata else ""
voice = exchange.metadata.voice if exchange.metadata else ""
timing = exchange.metadata.timing if exchange.metadata else ""
return f"{exchange.timestamp.isoformat()},{exchange.conversation_id},{exchange.type},{text},{transport},{provider},{model},{voice},{timing}"
@classmethod
def html(cls, conversation: Conversation) -> str:
"""HTML format with styling.
Args:
conversation: Conversation to format
Returns:
HTML string with embedded styles
"""
html_parts = ["""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Conversation {}</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}}
.conversation {{
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.metadata {{
background: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin-bottom: 20px;
font-size: 14px;
}}
.exchange {{
margin: 15px 0;
display: flex;
gap: 10px;
}}
.timestamp {{
color: #666;
font-size: 12px;
min-width: 60px;
}}
.speaker {{
font-weight: bold;
min-width: 80px;
}}
.speaker.user {{
color: #2e7d32;
}}
.speaker.assistant {{
color: #1976d2;
}}
.text {{
flex: 1;
}}
</style>
</head>
<body>
<div class="conversation">
<h1>Conversation {}</h1>
""".format(conversation.id, conversation.id)]
# Metadata
html_parts.append('<div class="metadata">')
html_parts.append(f'<strong>Start:</strong> {conversation.start_time.strftime("%Y-%m-%d %H:%M:%S")}<br>')
html_parts.append(f'<strong>Duration:</strong> {conversation.duration}<br>')
html_parts.append(f'<strong>Exchanges:</strong> {conversation.exchange_count}')
if conversation.project_path:
html_parts.append(f'<br><strong>Project:</strong> <code>{conversation.project_path}</code>')
html_parts.append('</div>')
# Exchanges
for exchange in conversation.exchanges:
time_str = exchange.timestamp.strftime("%H:%M:%S")
speaker_class = "user" if exchange.is_stt else "assistant"
speaker_name = "User" if exchange.is_stt else "Assistant"
html_parts.append('<div class="exchange">')
html_parts.append(f'<div class="timestamp">{time_str}</div>')
html_parts.append(f'<div class="speaker {speaker_class}">{speaker_name}:</div>')
html_parts.append(f'<div class="text">{exchange.text}</div>')
html_parts.append('</div>')
html_parts.append("""
</div>
</body>
</html>
""")
return "".join(html_parts)