Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@
"os"
"path/filepath"
"strings"

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

type multiStringFlag []string
Expand Down Expand Up @@ -70,7 +73,6 @@
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 @@
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 @@

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 @@
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 @@
}

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 @@
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 @@
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 @@
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 @@
}
}

// Print confirmation summary
fmt.Println(strings.Repeat("=", 70))
fmt.Printf("Summary:\n")
if len(opts.transcriptFiles) > 0 {
Expand All @@ -315,16 +310,15 @@
fmt.Printf(" - %s\n", pf)
}
}
if apiKey != "XXXX" {
fmt.Printf(" API key: %s\n", apiKey)
if apiKey != "XXXX" && len(apiKey) > 8 {
fmt.Printf(" API key: %s...%s\n", apiKey[:4], apiKey[len(apiKey)-4:])
Comment thread Fixed
Comment thread Fixed
}
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 @@
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 @@
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 @@

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