Implement artifact-level render command with Markdown output and update docs
This commit is contained in:
@@ -16,6 +16,8 @@ const (
|
||||
DefaultInputReader = "json-files"
|
||||
DefaultOutputModules = "json"
|
||||
DefaultOutputSchema = OutputSchemaIntermediate
|
||||
DefaultRenderTitle = "Transcript"
|
||||
RenderFormatMarkdown = "markdown"
|
||||
DefaultPreprocessingModules = "validate-raw,normalize-speakers,trim-text"
|
||||
DefaultPostprocessingModules = "detect-overlaps,resolve-overlaps,backchannel,filler,resolve-danglers,coalesce,detect-overlaps,autocorrect,assign-ids,validate-output"
|
||||
DefaultOverlapWordRunGap = 1.0
|
||||
@@ -69,6 +71,17 @@ type NormalizeOptions struct {
|
||||
OutputModules string
|
||||
}
|
||||
|
||||
// RenderOptions captures raw CLI option values before validation.
|
||||
type RenderOptions struct {
|
||||
InputFile string
|
||||
OutputFile string
|
||||
Format string
|
||||
Title string
|
||||
IncludeTimestamps bool
|
||||
IncludeSegmentIDs bool
|
||||
IncludeMetadata bool
|
||||
}
|
||||
|
||||
// Config is the validated runtime configuration for a merge invocation.
|
||||
type Config struct {
|
||||
InputFiles []string
|
||||
@@ -108,6 +121,17 @@ type NormalizeConfig struct {
|
||||
OutputModules []string
|
||||
}
|
||||
|
||||
// RenderConfig is the validated runtime configuration for a render invocation.
|
||||
type RenderConfig struct {
|
||||
InputFile string
|
||||
OutputFile string
|
||||
Format string
|
||||
Title string
|
||||
IncludeTimestamps bool
|
||||
IncludeSegmentIDs bool
|
||||
IncludeMetadata bool
|
||||
}
|
||||
|
||||
// NewMergeConfig validates raw merge options and returns normalized config.
|
||||
func NewMergeConfig(opts MergeOptions) (Config, error) {
|
||||
cfg := Config{
|
||||
@@ -303,6 +327,42 @@ func NewNormalizeConfig(opts NormalizeOptions) (NormalizeConfig, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewRenderConfig validates raw render options and returns normalized config.
|
||||
func NewRenderConfig(opts RenderOptions) (RenderConfig, error) {
|
||||
inputFile, err := normalizeSingleInputFile(opts.InputFile, "--input-file")
|
||||
if err != nil {
|
||||
return RenderConfig{}, err
|
||||
}
|
||||
|
||||
outputFile, err := normalizeOutputPath(opts.OutputFile, "--output-file")
|
||||
if err != nil {
|
||||
return RenderConfig{}, err
|
||||
}
|
||||
|
||||
format := strings.TrimSpace(opts.Format)
|
||||
if format == "" {
|
||||
return RenderConfig{}, errors.New("--format is required")
|
||||
}
|
||||
if err := validateRenderFormat(format); err != nil {
|
||||
return RenderConfig{}, err
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(opts.Title)
|
||||
if title == "" {
|
||||
title = DefaultRenderTitle
|
||||
}
|
||||
|
||||
return RenderConfig{
|
||||
InputFile: inputFile,
|
||||
OutputFile: outputFile,
|
||||
Format: format,
|
||||
Title: title,
|
||||
IncludeTimestamps: opts.IncludeTimestamps,
|
||||
IncludeSegmentIDs: opts.IncludeSegmentIDs,
|
||||
IncludeMetadata: opts.IncludeMetadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseModuleList(value string) ([]string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
@@ -485,3 +545,12 @@ func validateNormalizeOutputModules(modules []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRenderFormat(format string) error {
|
||||
switch format {
|
||||
case RenderFormatMarkdown:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("--format must be %q", RenderFormatMarkdown)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,6 +804,97 @@ func TestNewNormalizeConfigTreatsWhitespaceReportFileAsOmitted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenderConfigRequiresInputOutputAndFormat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
input := writeTempFile(t, dir, "input.json")
|
||||
output := filepath.Join(dir, "rendered.md")
|
||||
|
||||
_, err := NewRenderConfig(RenderOptions{
|
||||
OutputFile: output,
|
||||
Format: RenderFormatMarkdown,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "--input-file is required") {
|
||||
t.Fatalf("expected input-file required error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = NewRenderConfig(RenderOptions{
|
||||
InputFile: input,
|
||||
Format: RenderFormatMarkdown,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "--output-file is required") {
|
||||
t.Fatalf("expected output-file required error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = NewRenderConfig(RenderOptions{
|
||||
InputFile: input,
|
||||
OutputFile: output,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "--format is required") {
|
||||
t.Fatalf("expected format required error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenderConfigRejectsUnknownFormat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
input := writeTempFile(t, dir, "input.json")
|
||||
output := filepath.Join(dir, "rendered.md")
|
||||
|
||||
opts := validRenderOptions(input, output)
|
||||
opts.Format = "txt"
|
||||
_, err := NewRenderConfig(opts)
|
||||
if err == nil {
|
||||
t.Fatal("expected format validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "--format must be") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenderConfigAppliesDefaultsAndFlags(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
input := writeTempFile(t, dir, "input.json")
|
||||
output := filepath.Join(dir, "rendered.md")
|
||||
|
||||
cfg, err := NewRenderConfig(validRenderOptions(input, output))
|
||||
if err != nil {
|
||||
t.Fatalf("config failed: %v", err)
|
||||
}
|
||||
if cfg.Title != DefaultRenderTitle {
|
||||
t.Fatalf("title = %q, want %q", cfg.Title, DefaultRenderTitle)
|
||||
}
|
||||
if !cfg.IncludeTimestamps {
|
||||
t.Fatal("include timestamps should default true")
|
||||
}
|
||||
if cfg.IncludeSegmentIDs {
|
||||
t.Fatal("include segment IDs should default false")
|
||||
}
|
||||
if cfg.IncludeMetadata {
|
||||
t.Fatal("include metadata should default false")
|
||||
}
|
||||
|
||||
opts := validRenderOptions(input, output)
|
||||
opts.Title = "Meeting Notes"
|
||||
opts.IncludeTimestamps = false
|
||||
opts.IncludeSegmentIDs = true
|
||||
opts.IncludeMetadata = true
|
||||
cfg, err = NewRenderConfig(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("config failed: %v", err)
|
||||
}
|
||||
if cfg.Title != "Meeting Notes" {
|
||||
t.Fatalf("title = %q, want Meeting Notes", cfg.Title)
|
||||
}
|
||||
if cfg.IncludeTimestamps {
|
||||
t.Fatal("include timestamps should be false")
|
||||
}
|
||||
if !cfg.IncludeSegmentIDs {
|
||||
t.Fatal("include segment IDs should be true")
|
||||
}
|
||||
if !cfg.IncludeMetadata {
|
||||
t.Fatal("include metadata should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func assertPositiveFloatEnvValidation(t *testing.T, envName string) {
|
||||
t.Helper()
|
||||
|
||||
@@ -862,6 +953,18 @@ func validNormalizeOptions(inputFile string, outputFile string) NormalizeOptions
|
||||
}
|
||||
}
|
||||
|
||||
func validRenderOptions(inputFile string, outputFile string) RenderOptions {
|
||||
return RenderOptions{
|
||||
InputFile: inputFile,
|
||||
OutputFile: outputFile,
|
||||
Format: RenderFormatMarkdown,
|
||||
Title: DefaultRenderTitle,
|
||||
IncludeTimestamps: true,
|
||||
IncludeSegmentIDs: false,
|
||||
IncludeMetadata: false,
|
||||
}
|
||||
}
|
||||
|
||||
func writeTempFile(t *testing.T, dir string, name string) string {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user