149 lines
3.7 KiB
Go
149 lines
3.7 KiB
Go
package docx
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// VoiceExtractor handles extracting and transcribing audio from DOCX
|
|
type VoiceExtractor struct {
|
|
filePath string
|
|
}
|
|
|
|
// NewVoiceExtractor creates a new voice extractor
|
|
func NewVoiceExtractor(filePath string) *VoiceExtractor {
|
|
return &VoiceExtractor{filePath: filePath}
|
|
}
|
|
|
|
// ExtractAudioFiles finds and extracts audio files from DOCX
|
|
func (v *VoiceExtractor) ExtractAudioFiles() (map[string][]byte, error) {
|
|
audioFormats := map[string]bool{
|
|
".m4a": true,
|
|
".mp3": true,
|
|
".wav": true,
|
|
".ogg": true,
|
|
".aac": true,
|
|
".flac": true,
|
|
}
|
|
|
|
f, err := os.Open(v.filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
fileInfo, _ := f.Stat()
|
|
reader, err := zip.NewReader(f, fileInfo.Size())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open ZIP: %w", err)
|
|
}
|
|
|
|
audioFiles := make(map[string][]byte)
|
|
|
|
for _, file := range reader.File {
|
|
// Check for audio files in media/ or anywhere in docProps
|
|
if strings.Contains(strings.ToLower(file.Name), "media") || strings.Contains(strings.ToLower(file.Name), "docprops") {
|
|
ext := strings.ToLower(filepath.Ext(file.Name))
|
|
if audioFormats[ext] {
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
defer rc.Close()
|
|
|
|
data, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
fileName := filepath.Base(file.Name)
|
|
audioFiles[fileName] = data
|
|
}
|
|
}
|
|
}
|
|
|
|
return audioFiles, nil
|
|
}
|
|
|
|
// TranscribeAudio uses Claude CLI to transcribe audio
|
|
func (v *VoiceExtractor) TranscribeAudio(audioBytes []byte, filename string) (string, error) {
|
|
// Write audio to temp file
|
|
tmpFile, err := os.CreateTemp("", "audio-*."+strings.TrimPrefix(filepath.Ext(filename), "."))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temp file: %w", err)
|
|
}
|
|
defer os.Remove(tmpFile.Name())
|
|
|
|
if _, err := tmpFile.Write(audioBytes); err != nil {
|
|
tmpFile.Close()
|
|
return "", fmt.Errorf("failed to write audio: %w", err)
|
|
}
|
|
tmpFile.Close()
|
|
|
|
// Call claude CLI to transcribe
|
|
// Note: This assumes claude binary is in PATH
|
|
cmd := exec.Command("claude", "-p", fmt.Sprintf("Transcribe this audio file: %s", tmpFile.Name()))
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to transcribe: %v (stderr: %s)", err, stderr.String())
|
|
}
|
|
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
// VoiceSample represents extracted voice + transcription
|
|
type VoiceSample struct {
|
|
Filename string `json:"filename"`
|
|
Content []byte `json:"content,omitempty"` // only for storage
|
|
Transcript string `json:"transcript"`
|
|
}
|
|
|
|
// ExtractAndTranscribe extracts audio from DOCX and transcribes it
|
|
func (v *VoiceExtractor) ExtractAndTranscribe() ([]VoiceSample, error) {
|
|
audioFiles, err := v.ExtractAudioFiles()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var samples []VoiceSample
|
|
for filename, content := range audioFiles {
|
|
transcript, err := v.TranscribeAudio(content, filename)
|
|
if err != nil {
|
|
// Log but continue — transcription failure shouldn't block intake
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to transcribe %s: %v\n", filename, err)
|
|
transcript = "[transcription failed]"
|
|
}
|
|
|
|
samples = append(samples, VoiceSample{
|
|
Filename: filename,
|
|
Content: content,
|
|
Transcript: transcript,
|
|
})
|
|
}
|
|
|
|
return samples, nil
|
|
}
|
|
|
|
// StorageFormat returns the sample as JSON for SQLite storage
|
|
func (vs *VoiceSample) JSON() (string, error) {
|
|
// Don't store the raw audio bytes in DB — only filename + transcript
|
|
storage := map[string]interface{}{
|
|
"filename": vs.Filename,
|
|
"transcript": vs.Transcript,
|
|
}
|
|
b, err := json.Marshal(storage)
|
|
return string(b), err
|
|
}
|