feat: git init

This commit is contained in:
2026-01-17 19:18:58 -06:00
commit b73d5b8078
18 changed files with 1274 additions and 0 deletions

56
pkg/audio/audio.go Normal file
View File

@@ -0,0 +1,56 @@
package audio
import (
"errors"
"os"
"path/filepath"
"strings"
)
// SupportedAudioFormats lists the audio formats that can be processed
type SupportedAudioFormats []string
var DefaultSupportedFormats = SupportedAudioFormats{
".mp3",
".wav",
".flac",
".m4a",
".ogg",
".opus",
}
// IsSupported checks if a file has a supported audio format
type AudioFile struct {
Path string
Format string
Size int64
}
func NewAudioFile(path string) (*AudioFile, error) {
fileInfo, err := os.Stat(path)
if err != nil {
return nil, err
}
ext := filepath.Ext(path)
if !IsSupported(ext) {
return nil, errors.New("unsupported audio format: " + ext)
}
return &AudioFile{
Path: path,
Format: ext,
Size: fileInfo.Size(),
}, nil
}
// IsSupported checks if the given extension is in supported formats
func IsSupported(ext string) bool {
ext = strings.ToLower(ext)
for _, format := range DefaultSupportedFormats {
if ext == format {
return true
}
}
return false
}

33
pkg/output/formatter.go Normal file
View File

@@ -0,0 +1,33 @@
package output
import (
"transcribe/internal/whisper"
)
// Formatter interface for converting transcription results to various output formats
type Formatter interface {
Format(result *whisper.TranscriptionResult) (string, error)
}
// FormatType represents the output format type
type FormatType string
const (
FormatText FormatType = "text"
FormatSRT FormatType = "srt"
FormatJSON FormatType = "json"
)
// NewFormatter creates a formatter for the given format type
func NewFormatter(format FormatType) Formatter {
switch format {
case FormatSRT:
return &SRTFormatter{}
case FormatJSON:
return &JSONFormatter{}
case FormatText:
fallthrough
default:
return &TextFormatter{}
}
}

19
pkg/output/json.go Normal file
View File

@@ -0,0 +1,19 @@
package output
import (
"encoding/json"
"transcribe/internal/whisper"
)
// JSONFormatter formats transcription results as JSON
type JSONFormatter struct{}
// Format converts transcription result to JSON format
func (f *JSONFormatter) Format(result *whisper.TranscriptionResult) (string, error) {
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
return "", err
}
return string(data), nil
}

49
pkg/output/srt.go Normal file
View File

@@ -0,0 +1,49 @@
package output
import (
"fmt"
"strings"
"transcribe/internal/whisper"
)
// SRTFormatter formats transcription results as SRT subtitles
type SRTFormatter struct{}
// Format converts transcription result to SRT format
func (f *SRTFormatter) Format(result *whisper.TranscriptionResult) (string, error) {
var builder strings.Builder
for i, seg := range result.Segments {
// Subtitle number (1-indexed)
builder.WriteString(fmt.Sprintf("%d\n", i+1))
// Timestamps in SRT format: HH:MM:SS,mmm --> HH:MM:SS,mmm
startTime := formatSRTTimestamp(seg.Start)
endTime := formatSRTTimestamp(seg.End)
builder.WriteString(fmt.Sprintf("%s --> %s\n", startTime, endTime))
// Text with optional speaker label
text := strings.TrimSpace(seg.Text)
if seg.Speaker != "" {
text = fmt.Sprintf("[%s] %s", seg.Speaker, text)
}
builder.WriteString(text)
builder.WriteString("\n\n")
}
return strings.TrimSuffix(builder.String(), "\n"), nil
}
// formatSRTTimestamp converts seconds to SRT timestamp format (HH:MM:SS,mmm)
func formatSRTTimestamp(seconds float64) string {
totalMs := int64(seconds * 1000)
ms := totalMs % 1000
totalSeconds := totalMs / 1000
s := totalSeconds % 60
totalMinutes := totalSeconds / 60
m := totalMinutes % 60
h := totalMinutes / 60
return fmt.Sprintf("%02d:%02d:%02d,%03d", h, m, s, ms)
}

41
pkg/output/text.go Normal file
View File

@@ -0,0 +1,41 @@
package output
import (
"fmt"
"strings"
"transcribe/internal/whisper"
)
// TextFormatter formats transcription results as plain text with timestamps
type TextFormatter struct{}
// Format converts transcription result to plain text with timestamps
func (f *TextFormatter) Format(result *whisper.TranscriptionResult) (string, error) {
var builder strings.Builder
for _, seg := range result.Segments {
// Format: [MM:SS - MM:SS] [Speaker] Text
startTime := formatTextTimestamp(seg.Start)
endTime := formatTextTimestamp(seg.End)
text := strings.TrimSpace(seg.Text)
if seg.Speaker != "" {
builder.WriteString(fmt.Sprintf("[%s - %s] [%s] %s\n", startTime, endTime, seg.Speaker, text))
} else {
builder.WriteString(fmt.Sprintf("[%s - %s] %s\n", startTime, endTime, text))
}
}
return strings.TrimSuffix(builder.String(), "\n"), nil
}
// formatTextTimestamp converts seconds to MM:SS.s format
func formatTextTimestamp(seconds float64) string {
totalSeconds := int(seconds)
m := totalSeconds / 60
s := totalSeconds % 60
tenths := int((seconds - float64(totalSeconds)) * 10)
return fmt.Sprintf("%02d:%02d.%d", m, s, tenths)
}

84
pkg/progress/spinner.go Normal file
View File

@@ -0,0 +1,84 @@
package progress
import (
"fmt"
"sync"
"time"
)
// Spinner displays an animated spinner with a message
type Spinner struct {
message string
frames []string
interval time.Duration
stop chan struct{}
done chan struct{}
mu sync.Mutex
running bool
}
// NewSpinner creates a new spinner with the given message
func NewSpinner(message string) *Spinner {
return &Spinner{
message: message,
frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
interval: 80 * time.Millisecond,
stop: make(chan struct{}),
done: make(chan struct{}),
}
}
// Start begins the spinner animation
func (s *Spinner) Start() {
s.mu.Lock()
if s.running {
s.mu.Unlock()
return
}
s.running = true
s.mu.Unlock()
go func() {
i := 0
for {
select {
case <-s.stop:
// Clear the line and signal done
fmt.Print("\r\033[K")
close(s.done)
return
default:
fmt.Printf("\r%s %s", s.frames[i%len(s.frames)], s.message)
i++
time.Sleep(s.interval)
}
}
}()
}
// Stop stops the spinner and clears the line
func (s *Spinner) Stop() {
s.mu.Lock()
if !s.running {
s.mu.Unlock()
return
}
s.running = false
s.mu.Unlock()
close(s.stop)
<-s.done
}
// StopWithMessage stops the spinner and prints a final message
func (s *Spinner) StopWithMessage(message string) {
s.Stop()
fmt.Println(message)
}
// UpdateMessage updates the spinner message while running
func (s *Spinner) UpdateMessage(message string) {
s.mu.Lock()
defer s.mu.Unlock()
s.message = message
}