Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ jobs:
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: go build -o goscribe-${{ matrix.suffix }}
run: go build -o goscribe-${{ matrix.suffix }} ./cmd/goscribe
- name: Verify binary
run: test -f goscribe-${{ matrix.suffix }}
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Binaries
goscribe
goscribe-*
/goscribe
/goscribe-*
transcribe
transcribe-*

Expand Down
16 changes: 8 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ help:
@echo " help - Show this help"

build:
$(GO) build -o $(BINARY_NAME)
$(GO) build -o $(BINARY_NAME) ./cmd/goscribe

build-optimized:
$(GO) build -ldflags="-s -w" -o $(BINARY_NAME)
$(GO) build -ldflags="-s -w" -o $(BINARY_NAME) ./cmd/goscribe

build-all:
GOOS=linux GOARCH=amd64 $(GO) build -o $(BINARY_NAME)-linux-amd64
GOOS=linux GOARCH=arm64 $(GO) build -o $(BINARY_NAME)-linux-arm64
GOOS=darwin GOARCH=amd64 $(GO) build -o $(BINARY_NAME)-darwin-amd64
GOOS=darwin GOARCH=arm64 $(GO) build -o $(BINARY_NAME)-darwin-arm64
GOOS=windows GOARCH=amd64 $(GO) build -o $(BINARY_NAME)-windows-amd64.exe
GOOS=linux GOARCH=amd64 $(GO) build -o $(BINARY_NAME)-linux-amd64 ./cmd/goscribe
GOOS=linux GOARCH=arm64 $(GO) build -o $(BINARY_NAME)-linux-arm64 ./cmd/goscribe
GOOS=darwin GOARCH=amd64 $(GO) build -o $(BINARY_NAME)-darwin-amd64 ./cmd/goscribe
GOOS=darwin GOARCH=arm64 $(GO) build -o $(BINARY_NAME)-darwin-arm64 ./cmd/goscribe
GOOS=windows GOARCH=amd64 $(GO) build -o $(BINARY_NAME)-windows-amd64.exe ./cmd/goscribe

install: build
sudo mv $(BINARY_NAME) $(INSTALL_PATH)/
Expand All @@ -54,4 +54,4 @@ lint:
$(GO) run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run ./...

run:
$(GO) run .
$(GO) run ./cmd/goscribe
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ make build && sudo make install
### Build manually

```bash
go build -o goscribe
go build -o goscribe ./cmd/goscribe
sudo mv goscribe /usr/local/bin/
```

Expand Down Expand Up @@ -148,6 +148,17 @@ Reset to defaults: `goscribe -init`.
- [Architecture](docs/ARCHITECTURE.md)
- Supported formats: mp3, mp4, mpeg, mpga, m4a, wav, webm, ogg, flac, aac, aiff

## Project Structure

```
cmd/goscribe/ CLI entry point and orchestration
pkg/config/ Config types, loading, validation (importable)
internal/provider/ Provider routing and fallback logic
internal/openai/ OpenAI API client
internal/gemini/ Gemini API client
internal/util/ Shared helpers (model limits, audio splitting, etc.)
```

## Development

```bash
Expand All @@ -156,6 +167,7 @@ make build-optimized # smaller binary (-ldflags="-s -w")
make build-all # cross-compile all platforms
make test # run tests (verbose)
make test-coverage # generate coverage.html
make run # go run ./cmd/goscribe
```

## Acknowledgments
Expand Down
64 changes: 27 additions & 37 deletions main.go → cmd/goscribe/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"os"
"path/filepath"
"strings"

"goscribe/internal/provider"
"goscribe/pkg/config"
)

type multiStringFlag []string
Expand Down Expand Up @@ -70,7 +73,6 @@ func normalizeArgs(rawArgs []string) ([]string, error) {
return normalized, nil
}

// runOptions holds all resolved CLI options for the run function
type runOptions struct {
apiKey string
geminiKey string
Expand All @@ -82,12 +84,10 @@ type runOptions struct {
autoSelect bool
configFile string
transcriptFiles []string
args []string // remaining positional args
args []string
}

// run contains the core application logic, separated from main for testability
func run(opts runOptions) error {
// Determine which config file to use
configPath := opts.configFile
if configPath == "" {
homeDir, err := os.UserHomeDir()
Expand All @@ -98,45 +98,44 @@ func run(opts runOptions) error {

if _, err := os.Stat(configPath); os.IsNotExist(err) {
fmt.Println("Config file not found. Creating default config...")
if err := createDefaultConfig(); err != nil {
if err := config.CreateDefault(); err != nil {
return fmt.Errorf("creating default config: %w", err)
}
}
}

config, err := loadConfigActions(configPath)
cfg, err := config.LoadConfig(configPath)
if err != nil {
return fmt.Errorf("loading config file: %w", err)
}

// Resolve API keys from config
postActions := cfg.PostActions

apiKey := opts.apiKey
if apiKey == "XXXX" && config.OpenAIAPIKey != "" {
apiKey = config.OpenAIAPIKey
if apiKey == "XXXX" && cfg.OpenAIAPIKey != "" {
apiKey = cfg.OpenAIAPIKey
fmt.Println("Using API key from config file")
}

geminiKey := opts.geminiKey
if geminiKey == "" && config.GeminiAPIKey != "" {
geminiKey = config.GeminiAPIKey
if geminiKey == "" && cfg.GeminiAPIKey != "" {
geminiKey = cfg.GeminiAPIKey
}

// Determine active provider: flag > config > default
activeProvider := "openai"
if opts.provider != "" {
activeProvider = opts.provider
} else if config.Provider != "" {
activeProvider = config.Provider
} else if cfg.Provider != "" {
activeProvider = cfg.Provider
}

geminiModel := config.GeminiModel
geminiModel := cfg.GeminiModel
if geminiModel == "" {
geminiModel = "gemini-2.0-flash"
}

enableFallback := opts.enableFallback

// List actions and exit if requested
if opts.listActions {
fmt.Println("Available post-processing actions:")
fmt.Println()
Expand All @@ -154,7 +153,6 @@ func run(opts runOptions) error {
var audioPath string
var transcriptFilename string

// Handle transcript file mode
if len(opts.transcriptFiles) > 0 {
if opts.postAction == "" && !opts.autoSelect {
return fmt.Errorf("-action or --auto is required when using -transcript")
Expand Down Expand Up @@ -206,7 +204,7 @@ func run(opts runOptions) error {
}

fmt.Printf("Transcribing audio with %s...\n", activeProvider)
transcription, err = transcribeAudioWithProvider(audioPath, activeProvider, apiKey, geminiKey, geminiModel, enableFallback)
transcription, err = provider.TranscribeAudio(audioPath, activeProvider, apiKey, geminiKey, geminiModel, enableFallback)
if err != nil {
return fmt.Errorf("transcription failed: %w", err)
}
Expand All @@ -218,13 +216,12 @@ func run(opts runOptions) error {
fmt.Printf("Raw transcript saved to %s\n", transcriptFilename)
}

// Resolve action IDs
var processedFiles []string
var actionIDs []string

if opts.autoSelect {
fmt.Printf("\n🤖 Analyzing transcript with %s to select best actions...\n", activeProvider)
selectedActions, err := selectBestActionsWithProvider(transcription, activeProvider, apiKey, geminiKey, geminiModel)
selectedActions, err := provider.SelectBestActions(transcription, postActions, activeProvider, apiKey, geminiKey, geminiModel)
if err != nil {
fmt.Printf("⚠ Warning: Auto-selection failed: %v\n", err)
fmt.Println("Continuing without post-processing.")
Expand All @@ -240,7 +237,6 @@ func run(opts runOptions) error {
actionIDs[i] = strings.TrimSpace(id)
}

// Process selected actions
if len(actionIDs) > 0 {
fmt.Printf("\nProcessing %d action(s)...\n", len(actionIDs))

Expand All @@ -249,13 +245,13 @@ func run(opts runOptions) error {
continue
}

action := findAction(actionID)
action := config.FindAction(postActions, actionID)
if action == nil {
return fmt.Errorf("unknown action '%s'. Use -list-actions to see available options", actionID)
}

fmt.Printf("\n[%d/%d] Applying post-processing with %s: %s...\n", idx+1, len(actionIDs), activeProvider, action.Name)
processed, err := processWithProviderChunked(transcription, action, activeProvider, apiKey, geminiKey, geminiModel, enableFallback)
processed, err := provider.ProcessChunked(transcription, action, activeProvider, apiKey, geminiKey, geminiModel, enableFallback)
if err != nil {
fmt.Printf("⚠ Warning: Post-processing failed: %v\n", err)
if len(opts.transcriptFiles) == 0 && len(actionIDs) == 1 {
Expand Down Expand Up @@ -293,7 +289,6 @@ func run(opts runOptions) error {
}
}

// Print confirmation summary
fmt.Println(strings.Repeat("=", 70))
fmt.Printf("Summary:\n")
if len(opts.transcriptFiles) > 0 {
Expand All @@ -315,16 +310,15 @@ func run(opts runOptions) error {
fmt.Printf(" - %s\n", pf)
}
}
if apiKey != "XXXX" {
fmt.Printf(" API key: %s\n", apiKey)
if apiKey != "XXXX" && len(apiKey) > 0 {
fmt.Printf(" API key: [configured]\n")
}
fmt.Println(strings.Repeat("=", 70))

return nil
}

func main() {
// Preprocess arguments
if len(os.Args) > 1 {
normalized, err := normalizeArgs(os.Args[1:])
if err != nil {
Expand All @@ -334,10 +328,9 @@ func main() {
os.Args = append([]string{os.Args[0]}, normalized...)
}

// Define command-line flags
apiKey := flag.String("k", "XXXX", "OpenAI API key")
geminiKey := flag.String("gemini-key", "", "Gemini API key")
provider := flag.String("provider", "", "AI provider: openai or gemini (default: from config or openai)")
providerFlag := flag.String("provider", "", "AI provider: openai or gemini (default: from config or openai)")
noFallback := flag.Bool("no-fallback", false, "Disable automatic fallback to alternate provider on failure")
output := flag.String("o", "", "Output file name (default: same as audio file with .txt extension)")
listActions := flag.Bool("list-actions", false, "List available post-processing actions")
Expand All @@ -351,7 +344,6 @@ func main() {
var transcriptFiles multiStringFlag
flag.Var(&transcriptFiles, "transcript", "Process existing transcript file(s) (skips transcription)")

// Custom usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "goscribe - AI-powered audio transcription with OpenAI or Gemini\n\n")
fmt.Fprintf(os.Stderr, "USAGE:\n")
Expand Down Expand Up @@ -401,41 +393,39 @@ func main() {

flag.Parse()

// Handle one-shot config commands
if *setKey != "" {
if err := storeAPIKey(*setKey); err != nil {
if err := config.StoreAPIKey(*setKey); err != nil {
fmt.Printf("Error storing API key: %v\n", err)
os.Exit(1)
}
return
}
if *setGeminiKey != "" {
if err := storeGeminiAPIKey(*setGeminiKey); err != nil {
if err := config.StoreGeminiAPIKey(*setGeminiKey); err != nil {
fmt.Printf("Error storing Gemini API key: %v\n", err)
os.Exit(1)
}
return
}
if *setProviderFlag != "" {
if err := setDefaultProvider(*setProviderFlag); err != nil {
if err := config.SetDefaultProvider(*setProviderFlag); err != nil {
fmt.Printf("Error setting provider: %v\n", err)
os.Exit(1)
}
return
}
if *initConfig {
if err := resetConfig(); err != nil {
if err := config.Reset(); err != nil {
fmt.Printf("Error resetting config: %v\n", err)
os.Exit(1)
}
return
}

// Run core logic
opts := runOptions{
apiKey: *apiKey,
geminiKey: *geminiKey,
provider: *provider,
provider: *providerFlag,
enableFallback: !*noFallback,
output: *output,
listActions: *listActions,
Expand Down
Loading