From 84be774b34c656d76e8f0e5ff40f32def132ddd5 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 23 May 2026 17:56:07 +0000 Subject: [PATCH] Share module proposal execution and transcript-section prompt payload helpers --- .../promptcontext/transcript_section.go | 45 +++++++ .../promptcontext/transcript_section_test.go | 95 +++++++++++++++ .../proposal_generation/module_proposal.go | 81 ++++++++++++ .../module_proposal_test.go | 115 ++++++++++++++++++ internal/modules/glossary/module.go | 64 +--------- internal/modules/glossary/prompt.go | 36 +----- internal/modules/grammar/module.go | 64 +--------- internal/modules/grammar/prompt.go | 37 +----- internal/modules/homophones/module.go | 64 +--------- internal/modules/homophones/prompt.go | 36 +----- internal/modules/spoken_word/module.go | 64 +--------- internal/modules/spoken_word/prompt.go | 36 +----- 12 files changed, 360 insertions(+), 377 deletions(-) create mode 100644 internal/framework/promptcontext/transcript_section.go create mode 100644 internal/framework/promptcontext/transcript_section_test.go create mode 100644 internal/framework/proposal_generation/module_proposal.go create mode 100644 internal/framework/proposal_generation/module_proposal_test.go diff --git a/internal/framework/promptcontext/transcript_section.go b/internal/framework/promptcontext/transcript_section.go new file mode 100644 index 0000000..81d3186 --- /dev/null +++ b/internal/framework/promptcontext/transcript_section.go @@ -0,0 +1,45 @@ +package promptcontext + +import ( + "encoding/json" + + "gitea.maximumdirect.net/eric/audita/internal/core/schema" +) + +type transcriptSectionSegment struct { + ID int `json:"id"` + Speaker string `json:"speaker"` + Start float64 `json:"start"` + End float64 `json:"end"` + Text string `json:"text"` + Categories []string `json:"categories,omitempty"` +} + +type transcriptSectionPayload struct { + SectionIndex int `json:"section_index"` + Segments []transcriptSectionSegment `json:"segments"` +} + +// MarshalTranscriptSectionJSON builds the standardized transcript-section JSON +// payload consumed by proposal prompt templates. +func MarshalTranscriptSectionJSON(transcript *schema.Transcript, sectionIndex int) ([]byte, error) { + payload := transcriptSectionPayload{ + SectionIndex: sectionIndex, + Segments: make([]transcriptSectionSegment, 0), + } + + if transcript != nil { + for _, s := range transcript.Segments { + payload.Segments = append(payload.Segments, transcriptSectionSegment{ + ID: s.ID, + Speaker: s.Speaker, + Start: s.Start, + End: s.End, + Text: s.Text, + Categories: append([]string(nil), s.Categories...), + }) + } + } + + return json.MarshalIndent(payload, "", " ") +} diff --git a/internal/framework/promptcontext/transcript_section_test.go b/internal/framework/promptcontext/transcript_section_test.go new file mode 100644 index 0000000..062e8db --- /dev/null +++ b/internal/framework/promptcontext/transcript_section_test.go @@ -0,0 +1,95 @@ +package promptcontext + +import ( + "encoding/json" + "reflect" + "testing" + + "gitea.maximumdirect.net/eric/audita/internal/core/schema" +) + +func TestMarshalTranscriptSectionJSONShape(t *testing.T) { + transcript := &schema.Transcript{Segments: []schema.Segment{ + {ID: 1, Speaker: "A", Start: 0.1, End: 1.2, Text: "alpha", Categories: []string{"session", "intro"}}, + }} + + raw, err := MarshalTranscriptSectionJSON(transcript, 3) + if err != nil { + t.Fatalf("MarshalTranscriptSectionJSON error: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(raw, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + if got := decoded["section_index"]; got != float64(3) { + t.Fatalf("section_index: got=%v want=%v", got, 3) + } + segments, ok := decoded["segments"].([]any) + if !ok || len(segments) != 1 { + t.Fatalf("segments shape mismatch: %T %+v", decoded["segments"], decoded["segments"]) + } + first, ok := segments[0].(map[string]any) + if !ok { + t.Fatalf("segment shape mismatch: %T", segments[0]) + } + if first["id"] != float64(1) || first["speaker"] != "A" || first["start"] != 0.1 || first["end"] != 1.2 || first["text"] != "alpha" { + t.Fatalf("unexpected segment fields: %+v", first) + } + cats, ok := first["categories"].([]any) + if !ok || len(cats) != 2 || cats[0] != "session" || cats[1] != "intro" { + t.Fatalf("unexpected categories: %+v", first["categories"]) + } +} + +func TestMarshalTranscriptSectionJSONEmptyTranscript(t *testing.T) { + raw, err := MarshalTranscriptSectionJSON(nil, 0) + if err != nil { + t.Fatalf("MarshalTranscriptSectionJSON error: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(raw, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + if got := decoded["section_index"]; got != float64(0) { + t.Fatalf("section_index: got=%v want=%v", got, 0) + } + segments, ok := decoded["segments"].([]any) + if !ok { + t.Fatalf("segments shape mismatch: %T", decoded["segments"]) + } + if len(segments) != 0 { + t.Fatalf("expected empty segments, got %d", len(segments)) + } +} + +func TestMarshalTranscriptSectionJSONCopiesCategories(t *testing.T) { + transcript := &schema.Transcript{Segments: []schema.Segment{ + {ID: 1, Speaker: "A", Start: 0, End: 1, Text: "alpha", Categories: []string{"kept"}}, + }} + + raw, err := MarshalTranscriptSectionJSON(transcript, 1) + if err != nil { + t.Fatalf("MarshalTranscriptSectionJSON error: %v", err) + } + + transcript.Segments[0].Categories[0] = "changed" + + var decoded struct { + Segments []struct { + Categories []string `json:"categories"` + } `json:"segments"` + } + if err := json.Unmarshal(raw, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(decoded.Segments) != 1 { + t.Fatalf("expected one segment, got %d", len(decoded.Segments)) + } + if !reflect.DeepEqual(decoded.Segments[0].Categories, []string{"kept"}) { + t.Fatalf("expected copied categories, got %v", decoded.Segments[0].Categories) + } +} diff --git a/internal/framework/proposal_generation/module_proposal.go b/internal/framework/proposal_generation/module_proposal.go new file mode 100644 index 0000000..4669379 --- /dev/null +++ b/internal/framework/proposal_generation/module_proposal.go @@ -0,0 +1,81 @@ +package proposal_generation + +import ( + "context" + "fmt" + "strings" + + "gitea.maximumdirect.net/eric/audita/internal/core/schema" + "gitea.maximumdirect.net/eric/audita/internal/framework/contracts" + "gitea.maximumdirect.net/eric/audita/internal/framework/stagename" + "gitea.maximumdirect.net/eric/audita/internal/prompts" +) + +type ProposalMessageBuilder func(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) + +type ModuleProposalRequest struct { + ProposalRequest contracts.ProposalRequest + PromptID string + BuildMessages ProposalMessageBuilder +} + +// ExecuteModuleProposal runs shared proposal generation plumbing for one +// module, leaving only module-specific prompt message building at call sites. +func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (contracts.ProposalResult, error) { + if req.BuildMessages == nil { + return contracts.ProposalResult{}, fmt.Errorf("proposal message builder is required") + } + if strings.TrimSpace(req.PromptID) == "" { + return contracts.ProposalResult{}, fmt.Errorf("prompt ID must not be empty") + } + + sectionIndex := 0 + if req.ProposalRequest.Section != nil { + sectionIndex = req.ProposalRequest.Section.Index + } + + transcriptDescription := "" + if req.ProposalRequest.Config != nil { + transcriptDescription = req.ProposalRequest.Config.TranscriptDescription + } + + messages, err := req.BuildMessages( + req.ProposalRequest.WorkingTranscript, + req.ProposalRequest.Glossary, + sectionIndex, + transcriptDescription, + ) + if err != nil { + return contracts.ProposalResult{}, err + } + + promptMetadata, ok := prompts.LookupMetadata(req.PromptID) + if !ok { + return contracts.ProposalResult{}, fmt.Errorf("unknown prompt ID %q", req.PromptID) + } + + generated, err := GenerateCandidates(ctx, Request{ + ModuleKey: req.ProposalRequest.RunSpec.ModuleKey, + ModuleInstance: req.ProposalRequest.RunSpec.InstanceName, + ReplacementPolicy: req.ProposalRequest.RunSpec.ReplacementPolicy, + WorkingTranscript: req.ProposalRequest.WorkingTranscript, + Section: req.ProposalRequest.Section, + Glossary: req.ProposalRequest.Glossary, + Config: req.ProposalRequest.Config, + Messages: messages, + PromptMetadata: promptMetadata.DiagnosticsMap(), + StageName: stagename.ModuleProposal(req.ProposalRequest.RunSpec.InstanceName, sectionIndexPtr(req.ProposalRequest.Section)), + StartIndex: 0, + LLMClient: req.ProposalRequest.LLMClient, + Scheduler: req.ProposalRequest.LLMScheduler, + DiagnosticsDir: req.ProposalRequest.DiagnosticsDir, + }) + if err != nil { + return contracts.ProposalResult{}, err + } + + return contracts.ProposalResult{ + Proposals: generated.Corrections, + Warnings: generated.Warnings, + }, nil +} diff --git a/internal/framework/proposal_generation/module_proposal_test.go b/internal/framework/proposal_generation/module_proposal_test.go new file mode 100644 index 0000000..fa23fee --- /dev/null +++ b/internal/framework/proposal_generation/module_proposal_test.go @@ -0,0 +1,115 @@ +package proposal_generation + +import ( + "context" + "errors" + "testing" + + "gitea.maximumdirect.net/eric/audita/internal/core/config" + "gitea.maximumdirect.net/eric/audita/internal/core/schema" + "gitea.maximumdirect.net/eric/audita/internal/framework/contracts" + "gitea.maximumdirect.net/eric/audita/internal/framework/proposals" + "gitea.maximumdirect.net/eric/audita/internal/prompts" +) + +func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing.T) { + client := &fakeStructuredClient{ + responses: []StructuredCorrectionSet{ + {Corrections: []StructuredCorrectionProposal{{TargetSegmentID: 1, OriginalText: "teh", CorrectedText: "the", Confidence: 0.9}}}, + }, + } + section := contracts.SectionMetadata{Index: 7} + cfg := config.Default() + cfg.TranscriptDescription = "Hearing transcript with role titles." + transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "teh"}}} + glossary := &schema.Glossary{Entries: []schema.GlossaryEntry{{Name: "X"}}} + + var gotSectionIndex int + var gotDescription string + var gotTranscript *schema.Transcript + var gotGlossary *schema.Glossary + + out, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ + ProposalRequest: contracts.ProposalRequest{ + ExecutionContext: contracts.ExecutionContext{ + Config: &cfg, + WorkingTranscript: transcript, + Glossary: glossary, + Section: §ion, + }, + RunSpec: contracts.ModuleRunSpec{ + ModuleKey: "grammar", + InstanceName: "grammar", + ReplacementPolicy: proposals.ReplacementPolicyRequireUnique, + }, + LLMClient: client, + }, + PromptID: prompts.PromptIDModuleGrammarProposal, + BuildMessages: func(inTranscript *schema.Transcript, inGlossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) { + gotSectionIndex = sectionIndex + gotDescription = transcriptDescription + gotTranscript = inTranscript + gotGlossary = inGlossary + return []contracts.LLMMessage{{Role: "system", Content: "sys"}, {Role: "user", Content: "usr"}}, nil + }, + }) + if err != nil { + t.Fatalf("ExecuteModuleProposal error: %v", err) + } + if gotSectionIndex != 7 { + t.Fatalf("section index: got=%d want=%d", gotSectionIndex, 7) + } + if gotDescription != cfg.TranscriptDescription { + t.Fatalf("transcript description: got=%q want=%q", gotDescription, cfg.TranscriptDescription) + } + if gotTranscript != transcript { + t.Fatalf("expected shared transcript pointer") + } + if gotGlossary != glossary { + t.Fatalf("expected shared glossary pointer") + } + if len(client.calls) != 1 || client.calls[0].StageName != "grammar:proposal:section-0007" { + t.Fatalf("unexpected stage name calls: %+v", client.calls) + } + if len(out.Proposals) != 1 || out.Proposals[0].CorrectedText != "the" { + t.Fatalf("unexpected proposals: %+v", out) + } +} + +func TestExecuteModuleProposalValidatesInputs(t *testing.T) { + if _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{}); err == nil { + t.Fatalf("expected missing message builder error") + } + + _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ + BuildMessages: func(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) { + return nil, nil + }, + }) + if err == nil { + t.Fatalf("expected empty prompt ID error") + } + + _, err = ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ + PromptID: "missing.prompt.id", + BuildMessages: func(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) { + return []contracts.LLMMessage{{Role: "system", Content: "sys"}, {Role: "user", Content: "usr"}}, nil + }, + }) + if err == nil { + t.Fatalf("expected unknown prompt ID error") + } +} + +func TestExecuteModuleProposalPropagatesBuilderError(t *testing.T) { + wantErr := errors.New("builder failed") + _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ + PromptID: prompts.PromptIDModuleGlossaryProposal, + BuildMessages: func(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) { + return nil, wantErr + }, + }) + if !errors.Is(err, wantErr) { + t.Fatalf("expected builder error, got %v", err) + } +} diff --git a/internal/modules/glossary/module.go b/internal/modules/glossary/module.go index d497c4f..4f88839 100644 --- a/internal/modules/glossary/module.go +++ b/internal/modules/glossary/module.go @@ -3,11 +3,10 @@ package glossary import ( "context" - "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/contracts" "gitea.maximumdirect.net/eric/audita/internal/framework/proposal_generation" "gitea.maximumdirect.net/eric/audita/internal/framework/proposals" - "gitea.maximumdirect.net/eric/audita/internal/framework/stagename" + "gitea.maximumdirect.net/eric/audita/internal/prompts" builtinvalidators "gitea.maximumdirect.net/eric/audita/internal/validators" ) @@ -37,62 +36,9 @@ func (m *Module) Validators() []contracts.Validator { } func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (contracts.ProposalResult, error) { - sectionTranscript := transcriptForSection(req.WorkingTranscript, req.Section) - sectionIndex := 0 - if req.Section != nil { - sectionIndex = req.Section.Index - } - transcriptDescription := "" - if req.Config != nil { - transcriptDescription = req.Config.TranscriptDescription - } - messages, err := BuildProposalMessages(sectionTranscript, req.Glossary, sectionIndex, transcriptDescription) - if err != nil { - return contracts.ProposalResult{}, err - } - - generated, err := proposal_generation.GenerateCandidates(ctx, proposal_generation.Request{ - ModuleKey: req.RunSpec.ModuleKey, - ModuleInstance: req.RunSpec.InstanceName, - ReplacementPolicy: req.RunSpec.ReplacementPolicy, - WorkingTranscript: req.WorkingTranscript, - Section: req.Section, - Glossary: req.Glossary, - Config: req.Config, - Messages: messages, - PromptMetadata: proposalPromptMetadata().DiagnosticsMap(), - StageName: stagename.ModuleProposal(req.RunSpec.InstanceName, sectionIndexPtr(req.Section)), - StartIndex: 0, - LLMClient: req.LLMClient, - Scheduler: req.LLMScheduler, - DiagnosticsDir: req.DiagnosticsDir, + return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{ + ProposalRequest: req, + PromptID: prompts.PromptIDModuleGlossaryProposal, + BuildMessages: BuildProposalMessages, }) - if err != nil { - return contracts.ProposalResult{}, err - } - return contracts.ProposalResult{ - Proposals: generated.Corrections, - Warnings: generated.Warnings, - }, nil -} - -func transcriptForSection(transcript *schema.Transcript, section *contracts.SectionMetadata) *schema.Transcript { - if section == nil || transcript == nil { - return transcript - } - segments := make([]schema.Segment, 0, len(transcript.Segments)) - for _, seg := range transcript.Segments { - if seg.ID >= section.StartSegmentID && seg.ID <= section.EndSegmentID { - segments = append(segments, seg) - } - } - return &schema.Transcript{Segments: segments} -} - -func sectionIndexPtr(section *contracts.SectionMetadata) *int { - if section == nil { - return nil - } - index := section.Index - return &index } diff --git a/internal/modules/glossary/prompt.go b/internal/modules/glossary/prompt.go index 8978dca..44a7add 100644 --- a/internal/modules/glossary/prompt.go +++ b/internal/modules/glossary/prompt.go @@ -10,43 +10,13 @@ import ( "gitea.maximumdirect.net/eric/audita/internal/prompts" ) -type promptSegment struct { - ID int `json:"id"` - Speaker string `json:"speaker"` - Start float64 `json:"start"` - End float64 `json:"end"` - Text string `json:"text"` - Categories []string `json:"categories,omitempty"` -} - -type promptTranscriptSection struct { - SectionIndex int `json:"section_index"` - Segments []promptSegment `json:"segments"` -} - func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) { glossaryJSON, err := json.MarshalIndent(glossary, "", " ") if err != nil { return nil, fmt.Errorf("marshal glossary prompt context: %w", err) } - sectionPayload := promptTranscriptSection{ - SectionIndex: sectionIndex, - Segments: make([]promptSegment, 0), - } - if transcript != nil { - for _, s := range transcript.Segments { - sectionPayload.Segments = append(sectionPayload.Segments, promptSegment{ - ID: s.ID, - Speaker: s.Speaker, - Start: s.Start, - End: s.End, - Text: s.Text, - Categories: append([]string(nil), s.Categories...), - }) - } - } - sectionJSON, err := json.MarshalIndent(sectionPayload, "", " ") + sectionJSON, err := promptcontext.MarshalTranscriptSectionJSON(transcript, sectionIndex) if err != nil { return nil, fmt.Errorf("marshal transcript prompt context: %w", err) } @@ -65,7 +35,3 @@ func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Gloss {Role: "user", Content: user}, }, nil } - -func proposalPromptMetadata() prompts.Metadata { - return prompts.MustLookupMetadata(prompts.PromptIDModuleGlossaryProposal) -} diff --git a/internal/modules/grammar/module.go b/internal/modules/grammar/module.go index c6b030b..69c1b3d 100644 --- a/internal/modules/grammar/module.go +++ b/internal/modules/grammar/module.go @@ -3,11 +3,10 @@ package grammar import ( "context" - "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/contracts" "gitea.maximumdirect.net/eric/audita/internal/framework/proposal_generation" "gitea.maximumdirect.net/eric/audita/internal/framework/proposals" - "gitea.maximumdirect.net/eric/audita/internal/framework/stagename" + "gitea.maximumdirect.net/eric/audita/internal/prompts" builtinvalidators "gitea.maximumdirect.net/eric/audita/internal/validators" ) @@ -37,62 +36,9 @@ func (m *Module) Validators() []contracts.Validator { } func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (contracts.ProposalResult, error) { - sectionTranscript := transcriptForSection(req.WorkingTranscript, req.Section) - sectionIndex := 0 - if req.Section != nil { - sectionIndex = req.Section.Index - } - transcriptDescription := "" - if req.Config != nil { - transcriptDescription = req.Config.TranscriptDescription - } - messages, err := BuildProposalMessages(sectionTranscript, req.Glossary, sectionIndex, transcriptDescription) - if err != nil { - return contracts.ProposalResult{}, err - } - - generated, err := proposal_generation.GenerateCandidates(ctx, proposal_generation.Request{ - ModuleKey: req.RunSpec.ModuleKey, - ModuleInstance: req.RunSpec.InstanceName, - ReplacementPolicy: req.RunSpec.ReplacementPolicy, - WorkingTranscript: req.WorkingTranscript, - Section: req.Section, - Glossary: req.Glossary, - Config: req.Config, - Messages: messages, - PromptMetadata: proposalPromptMetadata().DiagnosticsMap(), - StageName: stagename.ModuleProposal(req.RunSpec.InstanceName, sectionIndexPtr(req.Section)), - StartIndex: 0, - LLMClient: req.LLMClient, - Scheduler: req.LLMScheduler, - DiagnosticsDir: req.DiagnosticsDir, + return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{ + ProposalRequest: req, + PromptID: prompts.PromptIDModuleGrammarProposal, + BuildMessages: BuildProposalMessages, }) - if err != nil { - return contracts.ProposalResult{}, err - } - return contracts.ProposalResult{ - Proposals: generated.Corrections, - Warnings: generated.Warnings, - }, nil -} - -func transcriptForSection(transcript *schema.Transcript, section *contracts.SectionMetadata) *schema.Transcript { - if section == nil || transcript == nil { - return transcript - } - segments := make([]schema.Segment, 0, len(transcript.Segments)) - for _, seg := range transcript.Segments { - if seg.ID >= section.StartSegmentID && seg.ID <= section.EndSegmentID { - segments = append(segments, seg) - } - } - return &schema.Transcript{Segments: segments} -} - -func sectionIndexPtr(section *contracts.SectionMetadata) *int { - if section == nil { - return nil - } - index := section.Index - return &index } diff --git a/internal/modules/grammar/prompt.go b/internal/modules/grammar/prompt.go index 1f4b354..152b15a 100644 --- a/internal/modules/grammar/prompt.go +++ b/internal/modules/grammar/prompt.go @@ -10,20 +10,6 @@ import ( "gitea.maximumdirect.net/eric/audita/internal/prompts" ) -type promptSegment struct { - ID int `json:"id"` - Speaker string `json:"speaker"` - Start float64 `json:"start"` - End float64 `json:"end"` - Text string `json:"text"` - Categories []string `json:"categories,omitempty"` -} - -type promptTranscriptSection struct { - SectionIndex int `json:"section_index"` - Segments []promptSegment `json:"segments"` -} - // BuildProposalMessages constrains corrections to punctuation/capitalization/ // spacing cleanup with strict meaning guards. func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) { @@ -32,24 +18,7 @@ func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Gloss return nil, fmt.Errorf("marshal glossary prompt context: %w", err) } - sectionPayload := promptTranscriptSection{ - SectionIndex: sectionIndex, - Segments: make([]promptSegment, 0), - } - if transcript != nil { - for _, s := range transcript.Segments { - sectionPayload.Segments = append(sectionPayload.Segments, promptSegment{ - ID: s.ID, - Speaker: s.Speaker, - Start: s.Start, - End: s.End, - Text: s.Text, - Categories: append([]string(nil), s.Categories...), - }) - } - } - - sectionJSON, err := json.MarshalIndent(sectionPayload, "", " ") + sectionJSON, err := promptcontext.MarshalTranscriptSectionJSON(transcript, sectionIndex) if err != nil { return nil, fmt.Errorf("marshal transcript prompt context: %w", err) } @@ -68,7 +37,3 @@ func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Gloss {Role: "user", Content: user}, }, nil } - -func proposalPromptMetadata() prompts.Metadata { - return prompts.MustLookupMetadata(prompts.PromptIDModuleGrammarProposal) -} diff --git a/internal/modules/homophones/module.go b/internal/modules/homophones/module.go index d878af9..1fbef43 100644 --- a/internal/modules/homophones/module.go +++ b/internal/modules/homophones/module.go @@ -3,11 +3,10 @@ package homophones import ( "context" - "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/contracts" "gitea.maximumdirect.net/eric/audita/internal/framework/proposal_generation" "gitea.maximumdirect.net/eric/audita/internal/framework/proposals" - "gitea.maximumdirect.net/eric/audita/internal/framework/stagename" + "gitea.maximumdirect.net/eric/audita/internal/prompts" builtinvalidators "gitea.maximumdirect.net/eric/audita/internal/validators" ) @@ -37,62 +36,9 @@ func (m *Module) Validators() []contracts.Validator { } func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (contracts.ProposalResult, error) { - sectionTranscript := transcriptForSection(req.WorkingTranscript, req.Section) - sectionIndex := 0 - if req.Section != nil { - sectionIndex = req.Section.Index - } - transcriptDescription := "" - if req.Config != nil { - transcriptDescription = req.Config.TranscriptDescription - } - messages, err := BuildProposalMessages(sectionTranscript, req.Glossary, sectionIndex, transcriptDescription) - if err != nil { - return contracts.ProposalResult{}, err - } - - generated, err := proposal_generation.GenerateCandidates(ctx, proposal_generation.Request{ - ModuleKey: req.RunSpec.ModuleKey, - ModuleInstance: req.RunSpec.InstanceName, - ReplacementPolicy: req.RunSpec.ReplacementPolicy, - WorkingTranscript: req.WorkingTranscript, - Section: req.Section, - Glossary: req.Glossary, - Config: req.Config, - Messages: messages, - PromptMetadata: proposalPromptMetadata().DiagnosticsMap(), - StageName: stagename.ModuleProposal(req.RunSpec.InstanceName, sectionIndexPtr(req.Section)), - StartIndex: 0, - LLMClient: req.LLMClient, - Scheduler: req.LLMScheduler, - DiagnosticsDir: req.DiagnosticsDir, + return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{ + ProposalRequest: req, + PromptID: prompts.PromptIDModuleHomophonesProposal, + BuildMessages: BuildProposalMessages, }) - if err != nil { - return contracts.ProposalResult{}, err - } - return contracts.ProposalResult{ - Proposals: generated.Corrections, - Warnings: generated.Warnings, - }, nil -} - -func transcriptForSection(transcript *schema.Transcript, section *contracts.SectionMetadata) *schema.Transcript { - if section == nil || transcript == nil { - return transcript - } - segments := make([]schema.Segment, 0, len(transcript.Segments)) - for _, seg := range transcript.Segments { - if seg.ID >= section.StartSegmentID && seg.ID <= section.EndSegmentID { - segments = append(segments, seg) - } - } - return &schema.Transcript{Segments: segments} -} - -func sectionIndexPtr(section *contracts.SectionMetadata) *int { - if section == nil { - return nil - } - index := section.Index - return &index } diff --git a/internal/modules/homophones/prompt.go b/internal/modules/homophones/prompt.go index 4905b80..3618eb3 100644 --- a/internal/modules/homophones/prompt.go +++ b/internal/modules/homophones/prompt.go @@ -10,20 +10,6 @@ import ( "gitea.maximumdirect.net/eric/audita/internal/prompts" ) -type promptSegment struct { - ID int `json:"id"` - Speaker string `json:"speaker"` - Start float64 `json:"start"` - End float64 `json:"end"` - Text string `json:"text"` - Categories []string `json:"categories,omitempty"` -} - -type promptTranscriptSection struct { - SectionIndex int `json:"section_index"` - Segments []promptSegment `json:"segments"` -} - // BuildProposalMessages constrains corrections to conservative homophone and // mistranscription updates. func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) { @@ -32,23 +18,7 @@ func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Gloss return nil, fmt.Errorf("marshal glossary prompt context: %w", err) } - sectionPayload := promptTranscriptSection{ - SectionIndex: sectionIndex, - Segments: make([]promptSegment, 0), - } - if transcript != nil { - for _, s := range transcript.Segments { - sectionPayload.Segments = append(sectionPayload.Segments, promptSegment{ - ID: s.ID, - Speaker: s.Speaker, - Start: s.Start, - End: s.End, - Text: s.Text, - Categories: append([]string(nil), s.Categories...), - }) - } - } - sectionJSON, err := json.MarshalIndent(sectionPayload, "", " ") + sectionJSON, err := promptcontext.MarshalTranscriptSectionJSON(transcript, sectionIndex) if err != nil { return nil, fmt.Errorf("marshal transcript prompt context: %w", err) } @@ -67,7 +37,3 @@ func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Gloss {Role: "user", Content: user}, }, nil } - -func proposalPromptMetadata() prompts.Metadata { - return prompts.MustLookupMetadata(prompts.PromptIDModuleHomophonesProposal) -} diff --git a/internal/modules/spoken_word/module.go b/internal/modules/spoken_word/module.go index e23e413..36641c2 100644 --- a/internal/modules/spoken_word/module.go +++ b/internal/modules/spoken_word/module.go @@ -3,11 +3,10 @@ package spoken_word import ( "context" - "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/contracts" "gitea.maximumdirect.net/eric/audita/internal/framework/proposal_generation" "gitea.maximumdirect.net/eric/audita/internal/framework/proposals" - "gitea.maximumdirect.net/eric/audita/internal/framework/stagename" + "gitea.maximumdirect.net/eric/audita/internal/prompts" builtinvalidators "gitea.maximumdirect.net/eric/audita/internal/validators" ) @@ -37,62 +36,9 @@ func (m *Module) Validators() []contracts.Validator { } func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (contracts.ProposalResult, error) { - sectionTranscript := transcriptForSection(req.WorkingTranscript, req.Section) - sectionIndex := 0 - if req.Section != nil { - sectionIndex = req.Section.Index - } - transcriptDescription := "" - if req.Config != nil { - transcriptDescription = req.Config.TranscriptDescription - } - messages, err := BuildProposalMessages(sectionTranscript, req.Glossary, sectionIndex, transcriptDescription) - if err != nil { - return contracts.ProposalResult{}, err - } - - generated, err := proposal_generation.GenerateCandidates(ctx, proposal_generation.Request{ - ModuleKey: req.RunSpec.ModuleKey, - ModuleInstance: req.RunSpec.InstanceName, - ReplacementPolicy: req.RunSpec.ReplacementPolicy, - WorkingTranscript: req.WorkingTranscript, - Section: req.Section, - Glossary: req.Glossary, - Config: req.Config, - Messages: messages, - PromptMetadata: proposalPromptMetadata().DiagnosticsMap(), - StageName: stagename.ModuleProposal(req.RunSpec.InstanceName, sectionIndexPtr(req.Section)), - StartIndex: 0, - LLMClient: req.LLMClient, - Scheduler: req.LLMScheduler, - DiagnosticsDir: req.DiagnosticsDir, + return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{ + ProposalRequest: req, + PromptID: prompts.PromptIDModuleSpokenWordProposal, + BuildMessages: BuildProposalMessages, }) - if err != nil { - return contracts.ProposalResult{}, err - } - return contracts.ProposalResult{ - Proposals: generated.Corrections, - Warnings: generated.Warnings, - }, nil -} - -func transcriptForSection(transcript *schema.Transcript, section *contracts.SectionMetadata) *schema.Transcript { - if section == nil || transcript == nil { - return transcript - } - segments := make([]schema.Segment, 0, len(transcript.Segments)) - for _, seg := range transcript.Segments { - if seg.ID >= section.StartSegmentID && seg.ID <= section.EndSegmentID { - segments = append(segments, seg) - } - } - return &schema.Transcript{Segments: segments} -} - -func sectionIndexPtr(section *contracts.SectionMetadata) *int { - if section == nil { - return nil - } - index := section.Index - return &index } diff --git a/internal/modules/spoken_word/prompt.go b/internal/modules/spoken_word/prompt.go index 4bbad79..4745cd5 100644 --- a/internal/modules/spoken_word/prompt.go +++ b/internal/modules/spoken_word/prompt.go @@ -10,20 +10,6 @@ import ( "gitea.maximumdirect.net/eric/audita/internal/prompts" ) -type promptSegment struct { - ID int `json:"id"` - Speaker string `json:"speaker"` - Start float64 `json:"start"` - End float64 `json:"end"` - Text string `json:"text"` - Categories []string `json:"categories,omitempty"` -} - -type promptTranscriptSection struct { - SectionIndex int `json:"section_index"` - Segments []promptSegment `json:"segments"` -} - // BuildProposalMessages constrains corrections to conservative dysfluency // cleanup with strict semantic preservation. func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) ([]contracts.LLMMessage, error) { @@ -32,23 +18,7 @@ func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Gloss return nil, fmt.Errorf("marshal glossary prompt context: %w", err) } - sectionPayload := promptTranscriptSection{ - SectionIndex: sectionIndex, - Segments: make([]promptSegment, 0), - } - if transcript != nil { - for _, s := range transcript.Segments { - sectionPayload.Segments = append(sectionPayload.Segments, promptSegment{ - ID: s.ID, - Speaker: s.Speaker, - Start: s.Start, - End: s.End, - Text: s.Text, - Categories: append([]string(nil), s.Categories...), - }) - } - } - sectionJSON, err := json.MarshalIndent(sectionPayload, "", " ") + sectionJSON, err := promptcontext.MarshalTranscriptSectionJSON(transcript, sectionIndex) if err != nil { return nil, fmt.Errorf("marshal transcript prompt context: %w", err) } @@ -67,7 +37,3 @@ func BuildProposalMessages(transcript *schema.Transcript, glossary *schema.Gloss {Role: "user", Content: user}, }, nil } - -func proposalPromptMetadata() prompts.Metadata { - return prompts.MustLookupMetadata(prompts.PromptIDModuleSpokenWordProposal) -}