Implement artifact-level render command with Markdown output and update docs
This commit is contained in:
39
internal/cli/render.go
Normal file
39
internal/cli/render.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.maximumdirect.net/eric/seriatim/internal/config"
|
||||
"gitea.maximumdirect.net/eric/seriatim/internal/render"
|
||||
)
|
||||
|
||||
func newRenderCommand() *cobra.Command {
|
||||
opts := config.RenderOptions{
|
||||
Title: config.DefaultRenderTitle,
|
||||
IncludeTimestamps: true,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "render",
|
||||
Short: "Render a seriatim transcript artifact into human-readable output",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.NewRenderConfig(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return render.Run(cmd.Context(), cfg)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.InputFile, "input-file", "", "input seriatim transcript artifact JSON file")
|
||||
flags.StringVar(&opts.OutputFile, "output-file", "", "rendered output file path")
|
||||
flags.StringVar(&opts.Format, "format", "", "output format (markdown)")
|
||||
flags.StringVar(&opts.Title, "title", config.DefaultRenderTitle, "document title")
|
||||
flags.BoolVar(&opts.IncludeTimestamps, "include-timestamps", true, "include segment timestamps")
|
||||
flags.BoolVar(&opts.IncludeSegmentIDs, "include-segment-ids", false, "include segment IDs")
|
||||
flags.BoolVar(&opts.IncludeMetadata, "include-metadata", false, "include artifact metadata")
|
||||
|
||||
return cmd
|
||||
}
|
||||
208
internal/cli/render_test.go
Normal file
208
internal/cli/render_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/seriatim/internal/config"
|
||||
)
|
||||
|
||||
func TestRenderCommandIsRecognized(t *testing.T) {
|
||||
cmd := NewRootCommand()
|
||||
cmd.SetArgs([]string{"render", "--help"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("render command should be recognized: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHelpIncludesRender(t *testing.T) {
|
||||
cmd := NewRootCommand()
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&out)
|
||||
cmd.SetArgs([]string{"--help"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("help failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "render") {
|
||||
t.Fatalf("root help missing render command:\n%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderEndToEndMarkdownOutput(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
input := writeJSONFile(t, dir, "input.json", `{
|
||||
"metadata": {
|
||||
"application": "seriatim",
|
||||
"version": "v-test",
|
||||
"output_schema": "seriatim-intermediate"
|
||||
},
|
||||
"segments": [
|
||||
{"id": 1, "start": 1, "end": 4, "speaker": "Eric", "text": "Hello there."},
|
||||
{"id": 2, "start": 5, "end": 8, "speaker": "Mike", "text": "Yeah.", "categories": ["backchannel"]}
|
||||
]
|
||||
}`)
|
||||
output := writeJSONFile(t, dir, "output.md", "")
|
||||
|
||||
err := executeRender(
|
||||
"--input-file", input,
|
||||
"--output-file", output,
|
||||
"--format", config.RenderFormatMarkdown,
|
||||
"--title", "Transcript",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("render failed: %v", err)
|
||||
}
|
||||
|
||||
data := readFile(t, output)
|
||||
if !strings.Contains(data, "# Transcript") {
|
||||
t.Fatalf("missing title:\n%s", data)
|
||||
}
|
||||
if !strings.Contains(data, "[00:00:01–00:00:04] **Eric:** Hello there.") {
|
||||
t.Fatalf("missing first segment:\n%s", data)
|
||||
}
|
||||
if !strings.Contains(data, "[00:00:05–00:00:08] **Mike:** *Yeah.*") {
|
||||
t.Fatalf("missing italicized backchannel segment:\n%s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderRejectsUnsupportedFormat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
input := writeJSONFile(t, dir, "input.json", `{"metadata":{"application":"seriatim","version":"v-test","output_schema":"seriatim-minimal"},"segments":[]}`)
|
||||
output := writeJSONFile(t, dir, "output.md", "")
|
||||
|
||||
err := executeRender(
|
||||
"--input-file", input,
|
||||
"--output-file", output,
|
||||
"--format", "txt",
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected format error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "--format must be") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderRejectsMalformedAndRawInput(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
output := writeJSONFile(t, dir, "output.md", "")
|
||||
|
||||
malformed := writeJSONFile(t, dir, "malformed.json", `{"metadata":`)
|
||||
err := executeRender(
|
||||
"--input-file", malformed,
|
||||
"--output-file", output,
|
||||
"--format", config.RenderFormatMarkdown,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected malformed input error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "input JSON is malformed") {
|
||||
t.Fatalf("unexpected malformed input error: %v", err)
|
||||
}
|
||||
|
||||
raw := writeJSONFile(t, dir, "raw.json", `{"segments":[{"id":0,"start":0.1,"end":1.1,"text":"hello","words":[{"word":"hello"}]}]}`)
|
||||
err = executeRender(
|
||||
"--input-file", raw,
|
||||
"--output-file", output,
|
||||
"--format", config.RenderFormatMarkdown,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected artifact validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not a valid seriatim output artifact") {
|
||||
t.Fatalf("unexpected raw input error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderSupportsMinimalIntermediateAndFullInputs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
}{
|
||||
{
|
||||
name: "minimal",
|
||||
content: `{
|
||||
"metadata": {
|
||||
"application": "seriatim",
|
||||
"version": "v-test",
|
||||
"output_schema": "seriatim-minimal"
|
||||
},
|
||||
"segments": [{"id":1,"start":1,"end":2,"speaker":"A","text":"one"}]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "intermediate",
|
||||
content: `{
|
||||
"metadata": {
|
||||
"application": "seriatim",
|
||||
"version": "v-test",
|
||||
"output_schema": "seriatim-intermediate"
|
||||
},
|
||||
"segments": [{"id":1,"start":1,"end":2,"speaker":"A","text":"one","categories":["filler"]}]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "full",
|
||||
content: `{
|
||||
"metadata": {
|
||||
"application": "seriatim",
|
||||
"version": "v-test",
|
||||
"input_reader": "json-files",
|
||||
"input_files": ["input.json"],
|
||||
"preprocessing_modules": [],
|
||||
"postprocessing_modules": [],
|
||||
"output_modules": ["json"]
|
||||
},
|
||||
"segments": [{
|
||||
"id":1,
|
||||
"source":"input.json",
|
||||
"source_segment_index":0,
|
||||
"speaker":"A",
|
||||
"start":1,
|
||||
"end":2,
|
||||
"text":"one"
|
||||
}],
|
||||
"overlap_groups": []
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
input := writeJSONFile(t, dir, "input.json", test.content)
|
||||
output := writeJSONFile(t, dir, "output.md", "")
|
||||
|
||||
err := executeRender(
|
||||
"--input-file", input,
|
||||
"--output-file", output,
|
||||
"--format", config.RenderFormatMarkdown,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("render failed: %v", err)
|
||||
}
|
||||
data := readFile(t, output)
|
||||
if !strings.Contains(data, "**A:**") {
|
||||
t.Fatalf("missing rendered segment for %s input:\n%s", test.name, data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func executeRender(args ...string) error {
|
||||
cmd := NewRootCommand()
|
||||
cmd.SetArgs(append([]string{"render"}, args...))
|
||||
return cmd.Execute()
|
||||
}
|
||||
|
||||
func readFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func NewRootCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "seriatim",
|
||||
Short: "Merge, trim, and normalize transcript artifacts",
|
||||
Short: "Merge, trim, normalize, and render transcript artifacts",
|
||||
Version: buildinfo.Version,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
@@ -18,6 +18,7 @@ func NewRootCommand() *cobra.Command {
|
||||
|
||||
cmd.AddCommand(newMergeCommand())
|
||||
cmd.AddCommand(newNormalizeCommand())
|
||||
cmd.AddCommand(newRenderCommand())
|
||||
cmd.AddCommand(newTrimCommand())
|
||||
return cmd
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user