Phase 4: Export to SRT, WebVTT, ASS, plain text, and Markdown

- Implement ExportService using pysubs2 for caption formats (SRT, VTT, ASS)
  and custom formatters for plain text and Markdown
- SRT exports with [Speaker]: prefix, WebVTT with <v Speaker> voice tags,
  ASS with color-coded speaker styles
- Plain text groups by speaker with labels, Markdown adds timestamps
- Add export.start IPC handler and export_transcript Tauri command
- Add export dropdown menu in header (appears after transcription)
- Uses native save dialog for output file selection
- Add pysubs2 dependency
- Tests: 30 Python (6 export tests), 6 Rust, 0 Svelte errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 16:18:54 -08:00
parent 44480906a4
commit 415a648a2b
9 changed files with 557 additions and 9 deletions

133
python/tests/test_export.py Normal file
View File

@@ -0,0 +1,133 @@
"""Tests for the export service."""
import os
import tempfile
from voice_to_notes.services.export import (
ExportRequest,
ExportSegment,
ExportService,
make_export_request,
)
def _make_segments():
return [
ExportSegment(text="Hello there", start_ms=0, end_ms=2000, speaker="SPEAKER_00"),
ExportSegment(text="How are you?", start_ms=2500, end_ms=4500, speaker="SPEAKER_01"),
ExportSegment(text="I'm fine, thanks", start_ms=5000, end_ms=7500, speaker="SPEAKER_00"),
]
def _speaker_map():
return {"SPEAKER_00": "Alice", "SPEAKER_01": "Bob"}
def test_export_srt():
service = ExportService()
with tempfile.NamedTemporaryFile(suffix=".srt", delete=False) as f:
path = f.name
try:
req = ExportRequest(
segments=_make_segments(),
speakers=_speaker_map(),
format="srt",
output_path=path,
)
result = service.export(req)
assert result == path
content = open(path, encoding="utf-8").read()
assert "[Alice]:" in content
assert "[Bob]:" in content
assert "Hello there" in content
finally:
os.unlink(path)
def test_export_vtt():
service = ExportService()
with tempfile.NamedTemporaryFile(suffix=".vtt", delete=False) as f:
path = f.name
try:
req = ExportRequest(
segments=_make_segments(),
speakers=_speaker_map(),
format="vtt",
output_path=path,
)
result = service.export(req)
content = open(path, encoding="utf-8").read()
assert "<v Alice>" in content
assert "<v Bob>" in content
finally:
os.unlink(path)
def test_export_txt():
service = ExportService()
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
req = ExportRequest(
segments=_make_segments(),
speakers=_speaker_map(),
format="txt",
output_path=path,
title="Test Transcript",
)
result = service.export(req)
content = open(path, encoding="utf-8").read()
assert "Test Transcript" in content
assert "Alice:" in content
assert "Bob:" in content
assert "Hello there" in content
finally:
os.unlink(path)
def test_export_md():
service = ExportService()
with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as f:
path = f.name
try:
req = ExportRequest(
segments=_make_segments(),
speakers=_speaker_map(),
format="md",
output_path=path,
title="Test Transcript",
)
result = service.export(req)
content = open(path, encoding="utf-8").read()
assert "# Test Transcript" in content
assert "**Alice**" in content
assert "**Bob**" in content
finally:
os.unlink(path)
def test_make_export_request():
payload = {
"segments": [
{"text": "Hello", "start_ms": 0, "end_ms": 1000, "speaker": "SPK_0"},
],
"speakers": {"SPK_0": "Alice"},
"format": "srt",
"output_path": "/tmp/test.srt",
"title": "Test",
}
req = make_export_request(payload)
assert len(req.segments) == 1
assert req.segments[0].speaker == "SPK_0"
assert req.speakers["SPK_0"] == "Alice"
assert req.format == "srt"
def test_export_unsupported_format():
service = ExportService()
req = ExportRequest(format="xyz")
try:
service.export(req)
assert False, "Should have raised ValueError"
except ValueError as e:
assert "Unsupported" in str(e)