Skip to content

Commit ae5edae

Browse files
committed
refactor: reorganize code into proper package structure
- Move main.go to cmd/ai-podcast directory following Go conventions - Consolidate packages to avoid fragmentation: - internal/content: combines article fetching and text processing - internal/audio: audio processing functionality - internal/ai: OpenAI integration - Move all test files to their respective packages - Export necessary constants and functions for cross-package usage - Generate package-specific mocks in each package's mocks directory - Update all imports to reflect new package structure - Fix import shadowing and other linting issues All tests pass with race detection enabled and no linting issues remain.
1 parent b2eb506 commit ae5edae

20 files changed

Lines changed: 234 additions & 161 deletions

main.go renamed to cmd/ai-podcast/main.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"sync"
99
"time"
1010

11+
"github.com/radio-t/ai-podcast/internal/ai"
12+
"github.com/radio-t/ai-podcast/internal/audio"
13+
"github.com/radio-t/ai-podcast/internal/content"
1114
"github.com/radio-t/ai-podcast/podcast"
1215
)
1316

@@ -102,9 +105,9 @@ func main() {
102105

103106
func run(config podcast.Config) error {
104107
// create services
105-
articleFetcher := NewHTTPArticleFetcher(nil)
106-
openAI := NewOpenAIService(config.OpenAIAPIKey, nil)
107-
audioProcessor := NewFFmpegAudioProcessor()
108+
articleFetcher := content.NewHTTPArticleFetcher(nil)
109+
openAI := ai.NewOpenAIService(config.OpenAIAPIKey, nil)
110+
audioProcessor := audio.NewFFmpegAudioProcessor()
108111

109112
return runWithDependencies(config, articleFetcher, openAI, audioProcessor)
110113
}
@@ -156,7 +159,7 @@ func runWithDependencies(config podcast.Config, articleFetcher ArticleFetcher, o
156159
// generateAndStreamToIcecast generates speech for each message and streams to Icecast
157160
func generateAndStreamToIcecast(params podcast.GenerateAndStreamParams, openAI OpenAIClient, audioProcessor AudioProcessor) error {
158161
// create text processor
159-
textProcessor := NewTextProcessor()
162+
textProcessor := content.NewTextProcessor()
160163

161164
// map host names to their gender and voice
162165
hostMap := podcast.CreateHostMap(params.Config.Hosts)
@@ -190,9 +193,9 @@ func generateAndStreamToIcecast(params podcast.GenerateAndStreamParams, openAI O
190193
}
191194

192195
// create concat file for ffmpeg
193-
concatFile, err := createConcatFile(tempDir, audioFiles)
196+
concatFile, err := audio.CreateConcatFile(tempDir, audioFiles)
194197
if err != nil {
195-
return err
198+
return fmt.Errorf("failed to create concat file: %w", err)
196199
}
197200

198201
// stream to Icecast
@@ -260,7 +263,7 @@ func generateAndPlayLocally(params podcast.GenerateAndStreamParams, openAI OpenA
260263
stopChan := make(chan struct{})
261264

262265
// create a buffer for pre-generated segments
263-
segmentBuffer := make([]podcast.SpeechSegment, 0, preGeneratedSegmentsBuffer)
266+
segmentBuffer := make([]podcast.SpeechSegment, 0, content.PreGeneratedSegmentsBuffer)
264267
bufferMutex := sync.Mutex{}
265268

266269
// start background worker for speech generation
@@ -275,7 +278,7 @@ func generateAndPlayLocally(params podcast.GenerateAndStreamParams, openAI OpenA
275278
// start pre-generating segments
276279
fmt.Println("Starting pre-generation of segments...")
277280
currentIndex := 0
278-
for i := 0; i < preGeneratedSegmentsBuffer && currentIndex < len(params.Discussion.Messages); i++ {
281+
for i := 0; i < content.PreGeneratedSegmentsBuffer && currentIndex < len(params.Discussion.Messages); i++ {
279282
msg := params.Discussion.Messages[currentIndex]
280283
reqParams := podcast.CreateSpeechRequestParams{
281284
Msg: msg,
@@ -389,7 +392,7 @@ func processSegments(params podcast.ProcessSegmentsParams, audioProcessor AudioP
389392
select {
390393
case segment = <-params.ResultChan:
391394
fmt.Printf("Received segment %d from %s\n", segment.Index, segment.Host)
392-
case <-time.After(speechGenerationTimeout):
395+
case <-time.After(content.SpeechGenerationTimeout):
393396
fmt.Println("Timeout waiting for speech generation!")
394397
return nil, fmt.Errorf("timeout waiting for speech generation")
395398
}
@@ -474,9 +477,10 @@ func processOrderedSegment(params podcast.ProcessOrderedSegmentParams, audioProc
474477

475478
// playSegment plays a single audio segment
476479
func playSegment(params podcast.PlaySegmentParams, audioProcessor AudioProcessor) error {
480+
textProcessor := content.NewTextProcessor()
477481
playStartTime := time.Now()
478482
fmt.Printf("\nPlaying audio from %s (message %d)...\n", params.Segment.Host, params.Index+1)
479-
fmt.Printf("Text: %s\n", truncateString(params.Segment.Msg.Content, displayTruncateLength))
483+
fmt.Printf("Text: %s\n", textProcessor.TruncateString(params.Segment.Msg.Content, content.DisplayTruncateLength))
480484

481485
err := audioProcessor.Play(params.Filename)
482486
if err != nil {

main_test.go renamed to cmd/ai-podcast/main_test.go

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"github.com/stretchr/testify/assert"
88
"github.com/stretchr/testify/require"
99

10-
"github.com/radio-t/ai-podcast/mocks"
10+
"github.com/radio-t/ai-podcast/cmd/ai-podcast/mocks"
1111
"github.com/radio-t/ai-podcast/podcast"
1212
)
1313

@@ -61,14 +61,6 @@ func TestCreateSpeechRequest(t *testing.T) {
6161
}
6262
}
6363

64-
func TestNewOpenAIService(t *testing.T) {
65-
t.Run("with nil http client", func(t *testing.T) {
66-
service := NewOpenAIService("test-key", nil)
67-
assert.Equal(t, "test-key", service.apiKey)
68-
assert.NotNil(t, service.httpClient)
69-
})
70-
}
71-
7264
// Test using generated mocks
7365
func TestGenerateAndStreamWithMocks(t *testing.T) {
7466
mockOpenAI := &mocks.OpenAIClientMock{}
@@ -146,16 +138,6 @@ func TestGenerateAndStreamWithMocks(t *testing.T) {
146138
assert.Len(t, mockArticle.FetchCalls(), 1)
147139
}
148140

149-
func TestNewFFmpegAudioProcessor(t *testing.T) {
150-
processor := NewFFmpegAudioProcessor()
151-
assert.NotNil(t, processor)
152-
}
153-
154-
func TestNewHTTPArticleFetcher(t *testing.T) {
155-
fetcher := NewHTTPArticleFetcher(nil)
156-
assert.NotNil(t, fetcher)
157-
}
158-
159141
func TestRunWithDependencies(t *testing.T) {
160142
tests := []struct {
161143
name string

internal/ai/mocks/http_client.go

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openai.go renamed to internal/ai/openai.go

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package main
1+
package ai
22

33
import (
44
"bytes"
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"strings"
1111

12+
"github.com/radio-t/ai-podcast/internal/content"
1213
"github.com/radio-t/ai-podcast/podcast"
1314
)
1415

@@ -28,7 +29,7 @@ type OpenAIService struct {
2829
// NewOpenAIService creates a new OpenAI service
2930
func NewOpenAIService(apiKey string, httpClient HTTPClient) *OpenAIService {
3031
if httpClient == nil {
31-
httpClient = &http.Client{Timeout: openAIHTTPTimeout}
32+
httpClient = &http.Client{Timeout: content.OpenAIHTTPTimeout}
3233
}
3334
return &OpenAIService{
3435
apiKey: apiKey,
@@ -65,7 +66,7 @@ type OpenAITTSRequest struct {
6566
// GenerateDiscussion uses OpenAI API to create a discussion between hosts
6667
func (s *OpenAIService) GenerateDiscussion(params podcast.GenerateDiscussionParams) (podcast.Discussion, error) {
6768
// calculate target number of messages based on duration
68-
targetMessages := params.TargetDuration * messagesPerMinute
69+
targetMessages := params.TargetDuration * content.MessagesPerMinute
6970

7071
// create the system prompt
7172
systemPrompt := s.createDiscussionPrompt(params.Hosts, targetMessages, params.TargetDuration)
@@ -81,18 +82,18 @@ func (s *OpenAIService) GenerateDiscussion(params podcast.GenerateDiscussionPara
8182
params.Title, params.ArticleText),
8283
},
8384
},
84-
Temperature: openAITemperature,
85-
MaxTokens: openAIMaxTokens,
85+
Temperature: content.OpenAITemperature,
86+
MaxTokens: content.OpenAIMaxTokens,
8687
}
8788

8889
// call the OpenAI API
89-
content, err := s.callChatAPI(request)
90+
responseContent, err := s.callChatAPI(request)
9091
if err != nil {
9192
return podcast.Discussion{}, fmt.Errorf("failed to generate discussion: %w", err)
9293
}
9394

9495
// extract and parse the JSON response
95-
messages, err := s.extractMessages(content)
96+
messages, err := s.extractMessages(responseContent)
9697
if err != nil {
9798
return podcast.Discussion{}, fmt.Errorf("failed to parse discussion: %w", err)
9899
}
@@ -259,7 +260,7 @@ Start with a brief introduction of the article topic before jumping into the hea
259260
260261
Make it feel like a real tech podcast discussion with passionate experts who aren't afraid to get heated and use strong language when they disagree.`
261262

262-
return fmt.Sprintf(basePrompt, hostDescriptions, targetMessages, messagesPerMinute, targetDuration)
263+
return fmt.Sprintf(basePrompt, hostDescriptions, targetMessages, content.MessagesPerMinute, targetDuration)
263264
}
264265

265266
// prepareHostDescriptions formats host information for the prompt
@@ -273,32 +274,32 @@ func (s *OpenAIService) prepareHostDescriptions(hosts []podcast.Host) string {
273274
}
274275

275276
// extractMessages extracts and parses messages from the OpenAI response
276-
func (s *OpenAIService) extractMessages(content string) ([]podcast.Message, error) {
277+
func (s *OpenAIService) extractMessages(responseContent string) ([]podcast.Message, error) {
277278
// the LLM may wrap the JSON in backticks or code block, so remove those
278-
content = strings.TrimSpace(content)
279-
if strings.HasPrefix(content, "```json") {
280-
content = strings.TrimPrefix(content, "```json")
281-
content = strings.TrimSuffix(content, "```")
282-
} else if strings.HasPrefix(content, "```") {
283-
content = strings.TrimPrefix(content, "```")
284-
content = strings.TrimSuffix(content, "```")
279+
responseContent = strings.TrimSpace(responseContent)
280+
if strings.HasPrefix(responseContent, "```json") {
281+
responseContent = strings.TrimPrefix(responseContent, "```json")
282+
responseContent = strings.TrimSuffix(responseContent, "```")
283+
} else if strings.HasPrefix(responseContent, "```") {
284+
responseContent = strings.TrimPrefix(responseContent, "```")
285+
responseContent = strings.TrimSuffix(responseContent, "```")
285286
}
286-
content = strings.TrimSpace(content)
287+
responseContent = strings.TrimSpace(responseContent)
287288

288289
// parse the JSON into our structure
289290
var rawMessages []struct {
290291
Host string `json:"host"`
291292
Content string `json:"content"`
292293
}
293294

294-
err := json.Unmarshal([]byte(content), &rawMessages)
295+
err := json.Unmarshal([]byte(responseContent), &rawMessages)
295296
if err != nil {
296297
// if unmarshaling fails, try to extract JSON from the text
297-
startIdx := strings.Index(content, "[")
298-
endIdx := strings.LastIndex(content, "]")
298+
startIdx := strings.Index(responseContent, "[")
299+
endIdx := strings.LastIndex(responseContent, "]")
299300
if startIdx >= 0 && endIdx > startIdx {
300-
content = content[startIdx : endIdx+1]
301-
err = json.Unmarshal([]byte(content), &rawMessages)
301+
responseContent = responseContent[startIdx : endIdx+1]
302+
err = json.Unmarshal([]byte(responseContent), &rawMessages)
302303
}
303304

304305
if err != nil {

openai_test.go renamed to internal/ai/openai_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package main
1+
package ai
22

33
import (
44
"bytes"
@@ -11,7 +11,7 @@ import (
1111
"strings"
1212
"testing"
1313

14-
"github.com/radio-t/ai-podcast/mocks"
14+
"github.com/radio-t/ai-podcast/internal/ai/mocks"
1515
"github.com/radio-t/ai-podcast/podcast"
1616
"github.com/stretchr/testify/assert"
1717
"github.com/stretchr/testify/require"

0 commit comments

Comments
 (0)