701 lines
22 KiB
Go
701 lines
22 KiB
Go
package seriatim
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
|
)
|
|
|
|
// MaxOutputFileBytes bounds each Seriatim JSON or rendered-text result.
|
|
const MaxOutputFileBytes int64 = 64 * 1024 * 1024
|
|
|
|
// EnvConfig defines optional Seriatim environment tuning values.
|
|
type EnvConfig struct {
|
|
OverlapWordRunGap *float64
|
|
OverlapWordRunReorderWindow *float64
|
|
BackchannelMaxDuration *float64
|
|
FillerMaxDuration *float64
|
|
}
|
|
|
|
// SubprocessRunnerConfig defines deterministic settings for Seriatim CLI execution.
|
|
type SubprocessRunnerConfig struct {
|
|
Binary string
|
|
Timeout time.Duration
|
|
OutputSchema string
|
|
CoalesceGap *float64
|
|
Report bool
|
|
Env EnvConfig
|
|
}
|
|
|
|
// SubprocessRunner invokes Seriatim via subprocess.
|
|
type SubprocessRunner struct {
|
|
binary string
|
|
timeout time.Duration
|
|
outputSchema string
|
|
coalesceGap *float64
|
|
report bool
|
|
env EnvConfig
|
|
}
|
|
|
|
// NewSubprocessRunnerFromConfigValues parses config-derived values once.
|
|
func NewSubprocessRunnerFromConfigValues(
|
|
binary string,
|
|
timeout string,
|
|
outputSchema string,
|
|
coalesceGap *float64,
|
|
report bool,
|
|
env EnvConfig,
|
|
) (*SubprocessRunner, error) {
|
|
if strings.TrimSpace(timeout) == "" {
|
|
return nil, fmt.Errorf("seriatim timeout is required")
|
|
}
|
|
parsedTimeout, err := time.ParseDuration(timeout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse seriatim timeout %q: %w", timeout, err)
|
|
}
|
|
return NewSubprocessRunner(SubprocessRunnerConfig{
|
|
Binary: binary,
|
|
Timeout: parsedTimeout,
|
|
OutputSchema: outputSchema,
|
|
CoalesceGap: coalesceGap,
|
|
Report: report,
|
|
Env: env,
|
|
})
|
|
}
|
|
|
|
// NewSubprocessRunner constructs a validated Seriatim subprocess runner.
|
|
func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error) {
|
|
if strings.TrimSpace(cfg.Binary) == "" {
|
|
return nil, fmt.Errorf("seriatim binary is required")
|
|
}
|
|
if cfg.Timeout <= 0 {
|
|
return nil, fmt.Errorf("seriatim timeout must be > 0")
|
|
}
|
|
if strings.TrimSpace(cfg.OutputSchema) == "" {
|
|
return nil, fmt.Errorf("seriatim output schema is required")
|
|
}
|
|
if err := validateOutputSchema(cfg.OutputSchema); err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.CoalesceGap != nil && *cfg.CoalesceGap < 0 {
|
|
return nil, fmt.Errorf("seriatim coalesce gap must be >= 0")
|
|
}
|
|
|
|
return &SubprocessRunner{
|
|
binary: cfg.Binary,
|
|
timeout: cfg.Timeout,
|
|
outputSchema: cfg.OutputSchema,
|
|
coalesceGap: cfg.CoalesceGap,
|
|
report: cfg.Report,
|
|
env: cfg.Env,
|
|
}, nil
|
|
}
|
|
|
|
func validateOutputSchema(schema string) error {
|
|
switch strings.TrimSpace(schema) {
|
|
case "seriatim-minimal", "seriatim-intermediate", "seriatim-full":
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("seriatim output schema %q is unsupported", schema)
|
|
}
|
|
}
|
|
|
|
// Run executes Seriatim merge with deterministic flags and validates output artifacts.
|
|
func (r *SubprocessRunner) Run(ctx context.Context, req MergeRequest) (MergeResult, error) {
|
|
if r == nil {
|
|
return MergeResult{}, fmt.Errorf("seriatim subprocess runner is nil")
|
|
}
|
|
if strings.TrimSpace(req.OutputMergedTranscriptPath) == "" {
|
|
return MergeResult{}, fmt.Errorf("seriatim merge output path is required")
|
|
}
|
|
if len(req.InputTranscriptPaths) == 0 {
|
|
return MergeResult{}, fmt.Errorf("seriatim merge requires at least one input transcript")
|
|
}
|
|
if r.report && strings.TrimSpace(req.ReportPath) == "" {
|
|
return MergeResult{}, fmt.Errorf("seriatim report is enabled but report path is missing")
|
|
}
|
|
|
|
args := r.buildMergeArgs(req)
|
|
env := r.buildEnvOverrides()
|
|
|
|
if req.GeneratedConfigPath != "" {
|
|
if err := r.writeMergeInvocationConfig(req, args); err != nil {
|
|
return MergeResult{}, fmt.Errorf("write seriatim invocation config %q: %w", req.GeneratedConfigPath, err)
|
|
}
|
|
}
|
|
|
|
runRes, err := subprocess.Run(ctx, subprocess.RunRequest{
|
|
Executable: r.binary,
|
|
Args: args,
|
|
Timeout: r.timeout,
|
|
EnvOverrides: env,
|
|
DiagnosticOwner: "seriatim",
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
})
|
|
if err != nil {
|
|
return MergeResult{
|
|
MergedTranscriptPath: req.OutputMergedTranscriptPath,
|
|
ReportPath: req.ReportPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: r.binary,
|
|
OutputSchema: r.outputSchema,
|
|
}, fmt.Errorf("run seriatim merge (binary=%q): %w", r.binary, err)
|
|
}
|
|
|
|
if err := validateJSONFile(req.OutputMergedTranscriptPath); err != nil {
|
|
return MergeResult{
|
|
MergedTranscriptPath: req.OutputMergedTranscriptPath,
|
|
ReportPath: req.ReportPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: r.binary,
|
|
OutputSchema: r.outputSchema,
|
|
}, fmt.Errorf("validate seriatim merged output %q: %w", req.OutputMergedTranscriptPath, err)
|
|
}
|
|
|
|
if r.report {
|
|
if err := validateJSONFile(req.ReportPath); err != nil {
|
|
return MergeResult{
|
|
MergedTranscriptPath: req.OutputMergedTranscriptPath,
|
|
ReportPath: req.ReportPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: r.binary,
|
|
OutputSchema: r.outputSchema,
|
|
}, fmt.Errorf("validate seriatim report output %q: %w", req.ReportPath, err)
|
|
}
|
|
}
|
|
|
|
return MergeResult{
|
|
MergedTranscriptPath: req.OutputMergedTranscriptPath,
|
|
ReportPath: req.ReportPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: r.binary,
|
|
OutputSchema: r.outputSchema,
|
|
Metadata: map[string]any{
|
|
"adapter": "seriatim_subprocess",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Trim executes Seriatim trim with deterministic flags and validates output artifacts.
|
|
func (r *SubprocessRunner) Trim(ctx context.Context, req TrimRequest) (TrimResult, error) {
|
|
if r == nil {
|
|
return TrimResult{}, fmt.Errorf("seriatim subprocess runner is nil")
|
|
}
|
|
if strings.TrimSpace(req.InputTranscriptPath) == "" {
|
|
return TrimResult{}, fmt.Errorf("seriatim trim input path is required")
|
|
}
|
|
if strings.TrimSpace(req.OutputTrimmedPath) == "" {
|
|
return TrimResult{}, fmt.Errorf("seriatim trim output path is required")
|
|
}
|
|
if strings.TrimSpace(req.KeepSelector) == "" {
|
|
return TrimResult{}, fmt.Errorf("seriatim trim keep selector is required")
|
|
}
|
|
|
|
binary := r.binary
|
|
if strings.TrimSpace(req.Binary) != "" {
|
|
binary = strings.TrimSpace(req.Binary)
|
|
}
|
|
|
|
timeout := r.timeout
|
|
if req.Timeout < 0 {
|
|
return TrimResult{}, fmt.Errorf("seriatim trim timeout must be >= 0")
|
|
}
|
|
if req.Timeout > 0 {
|
|
timeout = req.Timeout
|
|
}
|
|
|
|
args := buildTrimArgs(req)
|
|
if req.GeneratedConfigPath != "" {
|
|
if err := writeTrimInvocationConfig(req, args, binary, timeout); err != nil {
|
|
return TrimResult{}, fmt.Errorf("write seriatim trim invocation config %q: %w", req.GeneratedConfigPath, err)
|
|
}
|
|
}
|
|
|
|
runRes, err := subprocess.Run(ctx, subprocess.RunRequest{
|
|
Executable: binary,
|
|
Args: args,
|
|
Timeout: timeout,
|
|
DiagnosticOwner: "seriatim",
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
})
|
|
if err != nil {
|
|
return TrimResult{
|
|
OutputTrimmedPath: req.OutputTrimmedPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
KeepSelector: req.KeepSelector,
|
|
}, fmt.Errorf("run seriatim trim (binary=%q): %w", binary, err)
|
|
}
|
|
|
|
if err := validateJSONFileWithSegments(req.OutputTrimmedPath); err != nil {
|
|
return TrimResult{
|
|
OutputTrimmedPath: req.OutputTrimmedPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
KeepSelector: req.KeepSelector,
|
|
}, fmt.Errorf("validate seriatim trimmed output %q: %w", req.OutputTrimmedPath, err)
|
|
}
|
|
|
|
return TrimResult{
|
|
OutputTrimmedPath: req.OutputTrimmedPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
KeepSelector: req.KeepSelector,
|
|
Metadata: map[string]any{
|
|
"adapter": "seriatim_subprocess",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Normalize executes Seriatim normalize with deterministic flags and validates output artifacts.
|
|
func (r *SubprocessRunner) Normalize(ctx context.Context, req NormalizeRequest) (NormalizeResult, error) {
|
|
if r == nil {
|
|
return NormalizeResult{}, fmt.Errorf("seriatim subprocess runner is nil")
|
|
}
|
|
if strings.TrimSpace(req.InputTranscriptPath) == "" {
|
|
return NormalizeResult{}, fmt.Errorf("seriatim normalize input path is required")
|
|
}
|
|
if strings.TrimSpace(req.OutputNormalizedPath) == "" {
|
|
return NormalizeResult{}, fmt.Errorf("seriatim normalize output path is required")
|
|
}
|
|
|
|
binary := r.binary
|
|
if strings.TrimSpace(req.Binary) != "" {
|
|
binary = strings.TrimSpace(req.Binary)
|
|
}
|
|
|
|
timeout := r.timeout
|
|
if req.Timeout < 0 {
|
|
return NormalizeResult{}, fmt.Errorf("seriatim normalize timeout must be >= 0")
|
|
}
|
|
if req.Timeout > 0 {
|
|
timeout = req.Timeout
|
|
}
|
|
|
|
outputSchema := strings.TrimSpace(req.OutputSchema)
|
|
if outputSchema == "" {
|
|
outputSchema = r.outputSchema
|
|
}
|
|
if err := validateOutputSchema(outputSchema); err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
|
|
args := buildNormalizeArgs(req, outputSchema)
|
|
if req.GeneratedConfigPath != "" {
|
|
if err := writeNormalizeInvocationConfig(req, args, binary, timeout, outputSchema); err != nil {
|
|
return NormalizeResult{}, fmt.Errorf("write seriatim normalize invocation config %q: %w", req.GeneratedConfigPath, err)
|
|
}
|
|
}
|
|
|
|
runRes, err := subprocess.Run(ctx, subprocess.RunRequest{
|
|
Executable: binary,
|
|
Args: args,
|
|
Timeout: timeout,
|
|
DiagnosticOwner: "seriatim",
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
})
|
|
if err != nil {
|
|
return NormalizeResult{
|
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
|
ReportPath: req.ReportPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
OutputSchema: outputSchema,
|
|
}, fmt.Errorf("run seriatim normalize (binary=%q): %w", binary, err)
|
|
}
|
|
|
|
if err := validateJSONFileWithSegments(req.OutputNormalizedPath); err != nil {
|
|
return NormalizeResult{
|
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
|
ReportPath: req.ReportPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
OutputSchema: outputSchema,
|
|
}, fmt.Errorf("validate seriatim normalized output %q: %w", req.OutputNormalizedPath, err)
|
|
}
|
|
if strings.TrimSpace(req.ReportPath) != "" {
|
|
if err := validateJSONFile(req.ReportPath); err != nil {
|
|
return NormalizeResult{
|
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
|
ReportPath: req.ReportPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
OutputSchema: outputSchema,
|
|
}, fmt.Errorf("validate seriatim normalize report output %q: %w", req.ReportPath, err)
|
|
}
|
|
}
|
|
|
|
return NormalizeResult{
|
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
|
ReportPath: req.ReportPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
OutputSchema: outputSchema,
|
|
Metadata: map[string]any{
|
|
"adapter": "seriatim_subprocess",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Render executes Seriatim render with deterministic flags and validates non-empty text output.
|
|
func (r *SubprocessRunner) Render(ctx context.Context, req RenderRequest) (RenderResult, error) {
|
|
if r == nil {
|
|
return RenderResult{}, fmt.Errorf("seriatim subprocess runner is nil")
|
|
}
|
|
if strings.TrimSpace(req.InputTranscriptPath) == "" {
|
|
return RenderResult{}, fmt.Errorf("seriatim render input path is required")
|
|
}
|
|
if strings.TrimSpace(req.OutputRenderedPath) == "" {
|
|
return RenderResult{}, fmt.Errorf("seriatim render output path is required")
|
|
}
|
|
format := strings.TrimSpace(req.Format)
|
|
if format == "" {
|
|
format = "markdown"
|
|
}
|
|
if format != "markdown" {
|
|
return RenderResult{}, fmt.Errorf("seriatim render format %q is unsupported", req.Format)
|
|
}
|
|
|
|
binary := r.binary
|
|
if strings.TrimSpace(req.Binary) != "" {
|
|
binary = strings.TrimSpace(req.Binary)
|
|
}
|
|
|
|
timeout := r.timeout
|
|
if req.Timeout < 0 {
|
|
return RenderResult{}, fmt.Errorf("seriatim render timeout must be >= 0")
|
|
}
|
|
if req.Timeout > 0 {
|
|
timeout = req.Timeout
|
|
}
|
|
|
|
args := buildRenderArgs(req, format)
|
|
if req.GeneratedConfigPath != "" {
|
|
if err := writeRenderInvocationConfig(req, args, binary, timeout, format); err != nil {
|
|
return RenderResult{}, fmt.Errorf("write seriatim render invocation config %q: %w", req.GeneratedConfigPath, err)
|
|
}
|
|
}
|
|
|
|
runRes, err := subprocess.Run(ctx, subprocess.RunRequest{
|
|
Executable: binary,
|
|
Args: args,
|
|
Timeout: timeout,
|
|
DiagnosticOwner: "seriatim",
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
})
|
|
if err != nil {
|
|
return RenderResult{
|
|
OutputRenderedPath: req.OutputRenderedPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
Format: format,
|
|
Title: req.Title,
|
|
}, fmt.Errorf("run seriatim render (binary=%q): %w", binary, err)
|
|
}
|
|
|
|
if err := validateNonEmptyTextFile(req.OutputRenderedPath); err != nil {
|
|
return RenderResult{
|
|
OutputRenderedPath: req.OutputRenderedPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
Format: format,
|
|
Title: req.Title,
|
|
}, fmt.Errorf("validate seriatim rendered output %q: %w", req.OutputRenderedPath, err)
|
|
}
|
|
|
|
return RenderResult{
|
|
OutputRenderedPath: req.OutputRenderedPath,
|
|
StdoutLogPath: req.StdoutLogPath,
|
|
StderrLogPath: req.StderrLogPath,
|
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
ExitCode: runRes.ExitCode,
|
|
Duration: runRes.Duration,
|
|
InvokedBinary: binary,
|
|
Format: format,
|
|
Title: req.Title,
|
|
Metadata: map[string]any{
|
|
"adapter": "seriatim_subprocess",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (r *SubprocessRunner) buildMergeArgs(req MergeRequest) []string {
|
|
args := []string{"merge"}
|
|
|
|
for _, path := range req.InputTranscriptPaths {
|
|
args = append(args, "--input-file", path)
|
|
}
|
|
args = append(args, "--output-file", req.OutputMergedTranscriptPath)
|
|
|
|
if r.report {
|
|
args = append(args, "--report-file", req.ReportPath)
|
|
}
|
|
if strings.TrimSpace(req.SpeakersPath) != "" {
|
|
args = append(args, "--speakers", req.SpeakersPath)
|
|
}
|
|
if strings.TrimSpace(req.AutocorrectPath) != "" {
|
|
args = append(args, "--autocorrect", req.AutocorrectPath)
|
|
}
|
|
|
|
args = append(args, "--output-schema", r.outputSchema)
|
|
if r.coalesceGap != nil {
|
|
args = append(args, "--coalesce-gap", strconv.FormatFloat(*r.coalesceGap, 'f', -1, 64))
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
func (r *SubprocessRunner) buildEnvOverrides() map[string]string {
|
|
out := map[string]string{}
|
|
if r.env.OverlapWordRunGap != nil {
|
|
out["SERIATIM_OVERLAP_WORD_RUN_GAP"] = strconv.FormatFloat(*r.env.OverlapWordRunGap, 'f', -1, 64)
|
|
}
|
|
if r.env.OverlapWordRunReorderWindow != nil {
|
|
out["SERIATIM_OVERLAP_WORD_RUN_REORDER_WINDOW"] = strconv.FormatFloat(*r.env.OverlapWordRunReorderWindow, 'f', -1, 64)
|
|
}
|
|
if r.env.BackchannelMaxDuration != nil {
|
|
out["SERIATIM_BACKCHANNEL_MAX_DURATION"] = strconv.FormatFloat(*r.env.BackchannelMaxDuration, 'f', -1, 64)
|
|
}
|
|
if r.env.FillerMaxDuration != nil {
|
|
out["SERIATIM_FILLER_MAX_DURATION"] = strconv.FormatFloat(*r.env.FillerMaxDuration, 'f', -1, 64)
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *SubprocessRunner) writeMergeInvocationConfig(req MergeRequest, args []string) error {
|
|
payload := map[string]any{
|
|
"schema": "seriatim.generated.v1",
|
|
"binary": r.binary,
|
|
"command": "merge",
|
|
"args": args,
|
|
"timeout": r.timeout.String(),
|
|
"output_schema": r.outputSchema,
|
|
"report_enabled": r.report,
|
|
"input_transcript_paths": req.InputTranscriptPaths,
|
|
"output_path": req.OutputMergedTranscriptPath,
|
|
}
|
|
if req.ReportPath != "" {
|
|
payload["report_path"] = req.ReportPath
|
|
}
|
|
if req.SpeakersPath != "" {
|
|
payload["speakers_path"] = req.SpeakersPath
|
|
}
|
|
if req.AutocorrectPath != "" {
|
|
payload["autocorrect_path"] = req.AutocorrectPath
|
|
}
|
|
if r.coalesceGap != nil {
|
|
payload["coalesce_gap"] = *r.coalesceGap
|
|
}
|
|
|
|
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, fileops.WorkspaceFileMode)
|
|
}
|
|
|
|
func buildTrimArgs(req TrimRequest) []string {
|
|
return []string{
|
|
"trim",
|
|
"--input-file", req.InputTranscriptPath,
|
|
"--output-file", req.OutputTrimmedPath,
|
|
"--keep", req.KeepSelector,
|
|
}
|
|
}
|
|
|
|
func buildNormalizeArgs(req NormalizeRequest, outputSchema string) []string {
|
|
args := []string{
|
|
"normalize",
|
|
"--input-file", req.InputTranscriptPath,
|
|
"--output-file", req.OutputNormalizedPath,
|
|
"--output-schema", outputSchema,
|
|
}
|
|
if strings.TrimSpace(req.ReportPath) != "" {
|
|
args = append(args, "--report-file", req.ReportPath)
|
|
}
|
|
return args
|
|
}
|
|
|
|
func buildRenderArgs(req RenderRequest, format string) []string {
|
|
args := []string{
|
|
"render",
|
|
"--input-file", req.InputTranscriptPath,
|
|
"--output-file", req.OutputRenderedPath,
|
|
"--format", format,
|
|
"--include-timestamps=" + strconv.FormatBool(req.IncludeTimestamps),
|
|
"--include-segment-ids=" + strconv.FormatBool(req.IncludeSegmentIDs),
|
|
"--include-metadata=" + strconv.FormatBool(req.IncludeMetadata),
|
|
}
|
|
if strings.TrimSpace(req.Title) != "" {
|
|
args = append(args, "--title", req.Title)
|
|
}
|
|
return args
|
|
}
|
|
|
|
func writeTrimInvocationConfig(req TrimRequest, args []string, binary string, timeout time.Duration) error {
|
|
payload := map[string]any{
|
|
"schema": "seriatim.generated.v1",
|
|
"command": "trim",
|
|
"binary": binary,
|
|
"args": args,
|
|
"timeout": timeout.String(),
|
|
"input_path": req.InputTranscriptPath,
|
|
"output_path": req.OutputTrimmedPath,
|
|
"keep_selector": req.KeepSelector,
|
|
}
|
|
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, fileops.WorkspaceFileMode)
|
|
}
|
|
|
|
func writeNormalizeInvocationConfig(req NormalizeRequest, args []string, binary string, timeout time.Duration, outputSchema string) error {
|
|
payload := map[string]any{
|
|
"schema": "seriatim.generated.v1",
|
|
"command": "normalize",
|
|
"binary": binary,
|
|
"args": args,
|
|
"timeout": timeout.String(),
|
|
"input_path": req.InputTranscriptPath,
|
|
"output_path": req.OutputNormalizedPath,
|
|
"output_schema": outputSchema,
|
|
"report_path": req.ReportPath,
|
|
}
|
|
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, fileops.WorkspaceFileMode)
|
|
}
|
|
|
|
func writeRenderInvocationConfig(req RenderRequest, args []string, binary string, timeout time.Duration, format string) error {
|
|
payload := map[string]any{
|
|
"schema": "seriatim.generated.v1",
|
|
"command": "render",
|
|
"binary": binary,
|
|
"args": args,
|
|
"timeout": timeout.String(),
|
|
"input_path": req.InputTranscriptPath,
|
|
"output_path": req.OutputRenderedPath,
|
|
"format": format,
|
|
"title": req.Title,
|
|
"include_timestamps": req.IncludeTimestamps,
|
|
"include_segment_ids": req.IncludeSegmentIDs,
|
|
"include_metadata": req.IncludeMetadata,
|
|
}
|
|
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, fileops.WorkspaceFileMode)
|
|
}
|
|
|
|
func validateJSONFile(path string) error {
|
|
data, err := readSeriatimResult(path, "JSON output")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return fmt.Errorf("parse json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateJSONFileWithSegments(path string) error {
|
|
data, err := readSeriatimResult(path, "transcript JSON output")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(data, &payload); err != nil {
|
|
return fmt.Errorf("parse json: %w", err)
|
|
}
|
|
|
|
segments, ok := payload["segments"]
|
|
if !ok {
|
|
return fmt.Errorf("top-level segments is required")
|
|
}
|
|
if _, ok := segments.([]any); !ok {
|
|
return fmt.Errorf("top-level segments must be an array")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateNonEmptyTextFile(path string) error {
|
|
data, err := readSeriatimResult(path, "rendered text output")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(data) == 0 {
|
|
return fmt.Errorf("file is empty")
|
|
}
|
|
if !utf8.Valid(data) {
|
|
return fmt.Errorf("file is not valid utf-8 text")
|
|
}
|
|
if strings.TrimSpace(string(data)) == "" {
|
|
return fmt.Errorf("file has no non-whitespace content")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readSeriatimResult(path, category string) ([]byte, error) {
|
|
data, err := fileops.ReadRegularFile(path, MaxOutputFileBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("seriatim %s exceeds or cannot be read within %d-byte limit: %w", category, MaxOutputFileBytes, err)
|
|
}
|
|
return data, nil
|
|
}
|