204 lines
6.1 KiB
Go
204 lines
6.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
)
|
|
|
|
type finding struct {
|
|
Severity string
|
|
Category string
|
|
Message string
|
|
}
|
|
|
|
type findingError struct {
|
|
count int
|
|
}
|
|
|
|
func (e findingError) Error() string {
|
|
return fmt.Sprintf("%d validation error(s)", e.count)
|
|
}
|
|
|
|
func renderFindings(out io.Writer, campaign, sessionID string, findings []finding) error {
|
|
if campaign != "" || sessionID != "" {
|
|
fmt.Fprintf(out, "Campaign: %s\n", campaign)
|
|
fmt.Fprintf(out, "Session: %s\n\n", sessionID)
|
|
}
|
|
errorsCount := 0
|
|
for _, f := range findings {
|
|
if f.Severity == "ERROR" {
|
|
errorsCount++
|
|
}
|
|
fmt.Fprintf(out, "%-5s %-10s %s\n", f.Severity, f.Category, f.Message)
|
|
}
|
|
if errorsCount > 0 {
|
|
return findingError{count: errorsCount}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func okFinding(category, msg string) finding { return finding{"OK", category, msg} }
|
|
func infoFinding(category, msg string) finding { return finding{"INFO", category, msg} }
|
|
func warnFinding(category, msg string) finding { return finding{"WARN", category, msg} }
|
|
func errorFinding(category, msg string) finding { return finding{"ERROR", category, msg} }
|
|
|
|
func sessionSourceSummary(cfg *config.Config) string {
|
|
source := cfg.SessionSource.Source
|
|
if source == "" {
|
|
source = "session_config"
|
|
}
|
|
if cfg.SessionSource.S3Key != "" {
|
|
return source + " " + cfg.SessionSource.S3Key
|
|
}
|
|
return source + " " + cfg.SessionPath
|
|
}
|
|
|
|
func validateStableInputFindings(cfg *config.Config) []finding {
|
|
items := []struct {
|
|
name string
|
|
in config.ResolvedInputFile
|
|
}{
|
|
{"speakers", cfg.StableInputs.SpeakersFile},
|
|
{"autocorrect", cfg.StableInputs.AutocorrectFile},
|
|
{"glossary", cfg.StableInputs.GlossaryFile},
|
|
}
|
|
out := make([]finding, 0, len(items))
|
|
for _, item := range items {
|
|
path, err := resolveHelperConfigRelativePath(item.in)
|
|
if err != nil {
|
|
out = append(out, errorFinding("inputs", item.name+": "+err.Error()))
|
|
continue
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
out = append(out, errorFinding("inputs", fmt.Sprintf("%s missing: %v", item.name, err)))
|
|
} else {
|
|
out = append(out, okFinding("inputs", item.name+": "+path))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func resolveHelperConfigRelativePath(input config.ResolvedInputFile) (string, error) {
|
|
if strings.TrimSpace(input.ConfigPath) == "" {
|
|
return "", fmt.Errorf("source config path is required")
|
|
}
|
|
path := strings.TrimSpace(input.Path)
|
|
if path == "" {
|
|
return "", fmt.Errorf("path is required")
|
|
}
|
|
if filepath.IsAbs(path) {
|
|
return filepath.Clean(path), nil
|
|
}
|
|
return filepath.Clean(filepath.Join(filepath.Dir(input.ConfigPath), path)), nil
|
|
}
|
|
|
|
func validateLocalAudioFindings(cfg *config.Config) []finding {
|
|
if cfg.Session.Inputs.AudioS3 != nil {
|
|
return nil
|
|
}
|
|
audioDir := strings.TrimSpace(cfg.Session.Inputs.AudioDir)
|
|
if audioDir == "" && len(cfg.Session.Inputs.AudioFiles) == 0 {
|
|
return []finding{errorFinding("audio", "audio_dir, audio_files, or audio_s3 is required")}
|
|
}
|
|
base := filepath.Dir(cfg.SessionPath)
|
|
paths := []string{}
|
|
if audioDir != "" {
|
|
dir := audioDir
|
|
if !filepath.IsAbs(dir) {
|
|
dir = filepath.Join(base, dir)
|
|
}
|
|
matches, err := filepath.Glob(filepath.Join(dir, "*.flac"))
|
|
if err != nil || len(matches) == 0 {
|
|
return []finding{errorFinding("audio", "no .flac files found in "+dir)}
|
|
}
|
|
paths = append(paths, matches...)
|
|
}
|
|
for _, file := range cfg.Session.Inputs.AudioFiles {
|
|
p := file
|
|
if !filepath.IsAbs(p) {
|
|
p = filepath.Join(base, p)
|
|
}
|
|
paths = append(paths, p)
|
|
}
|
|
for _, p := range paths {
|
|
if _, err := os.Stat(p); err != nil {
|
|
return []finding{errorFinding("audio", fmt.Sprintf("audio file missing: %v", err))}
|
|
}
|
|
}
|
|
return []finding{okFinding("audio", fmt.Sprintf("%d local audio file(s)", len(paths)))}
|
|
}
|
|
|
|
func validateRemoteAudioFinding(ctx context.Context, cfg *config.Config, store storage.ObjectStore) finding {
|
|
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
audioPrefix := artifacts.S3AudioPrefix(sessionPrefix, cfg.Session.Inputs.AudioS3.Prefix)
|
|
objects, err := store.List(ctx, audioPrefix)
|
|
if err != nil {
|
|
return errorFinding("audio", err.Error())
|
|
}
|
|
count := 0
|
|
for _, obj := range objects {
|
|
if strings.HasSuffix(strings.ToLower(obj.Key), ".flac") {
|
|
count++
|
|
}
|
|
}
|
|
if count == 0 {
|
|
return errorFinding("audio", "no remote .flac objects found under "+audioPrefix)
|
|
}
|
|
return okFinding("audio", fmt.Sprintf("%d remote .flac object(s)", count))
|
|
}
|
|
|
|
func validatePreviousArtifactFindings(ctx context.Context, cfg *config.Config, store storage.ObjectStore, requirements []artifacts.PreviousArtifactRequirement) []finding {
|
|
out := []finding{}
|
|
prefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
|
_, err := artifacts.LoadCurrentState(ctx, store, prefix, artifacts.CurrentStateValidation{
|
|
ExpectedSessionID: strings.TrimSpace(cfg.Session.PreviousSessionID),
|
|
ExpectedCampaign: strings.TrimSpace(cfg.Session.Campaign),
|
|
ValidateRunID: true,
|
|
})
|
|
if err != nil {
|
|
out = append(out, errorFinding("previous", fmt.Sprintf("remote %v", err)))
|
|
return out
|
|
}
|
|
for _, req := range requirements {
|
|
out = append(out, okFinding("previous", fmt.Sprintf("%s required=%t", req.Name, req.Required)))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func loadLocalManifest(ctx context.Context, path string) (*manifest.Manifest, error) {
|
|
if _, err := os.Stat(path); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
store := &manifest.LocalStore{}
|
|
return store.Load(ctx, path)
|
|
}
|
|
|
|
func writeStageStatuses(out io.Writer, m *manifest.Manifest) {
|
|
if m == nil || len(m.Stages) == 0 {
|
|
fmt.Fprintln(out, "stages: no stages recorded")
|
|
return
|
|
}
|
|
fmt.Fprintln(out, "stages:")
|
|
names := make([]string, 0, len(m.Stages))
|
|
for name := range m.Stages {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
fmt.Fprintf(out, "- %s: %s\n", name, m.Stages[name].Status)
|
|
}
|
|
}
|