diff --git a/internal/ingestion/component/media_dispatch_test.go b/internal/ingestion/component/media_dispatch_test.go index 2c465616cc..ee98c2bf73 100644 --- a/internal/ingestion/component/media_dispatch_test.go +++ b/internal/ingestion/component/media_dispatch_test.go @@ -25,6 +25,7 @@ import ( "ragflow/internal/common" "ragflow/internal/entity" modelModule "ragflow/internal/entity/models" + "ragflow/internal/ingestion/component/schema" "ragflow/internal/utility" "gorm.io/gorm" @@ -362,9 +363,13 @@ func TestMaybeDispatchAudio_TextCarriesTranscription(t *testing.T) { } } -// TestMaybeDispatchAudio_DefaultOutputFormatJson covers Parser 2.11: -// the default audio output_format must be "json" (matching Python -// parser.py:232 and AllowedOutputFormat["audio"]={"json"}). +// TestMaybeDispatchAudio_DefaultOutputFormatJson covers the +// maybeDispatchAudio fallback: when an audio setup omits +// output_format entirely, the dispatch defaults to "json" and wraps +// the transcription as a JSON item. (The defaultSetups value is +// "text" to mirror the Python audio setup in parser.py; this test +// deliberately supplies an empty setup to exercise the fallback +// inside the dispatch itself.) func TestMaybeDispatchAudio_DefaultOutputFormatJson(t *testing.T) { const want = "hello world" drv := &audioTranscribeDriver{transcription: want} @@ -373,8 +378,9 @@ func TestMaybeDispatchAudio_DefaultOutputFormatJson(t *testing.T) { resolveTenantModelByType = func(ctx context.Context, db *gorm.DB, tenantID string, modelType entity.ModelType) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) { return drv, "asr-model", &modelModule.APIConfig{}, 0, nil } - setups := defaultSetups() - // Do NOT set output_format — exercise the default path. + // No output_format key — exercise the default path inside + // maybeDispatchAudio. + setups := map[string]schema.ParserSetup{"audio": {}} res, dispatched, err := maybeDispatchAudio( context.Background(), nil, diff --git a/internal/ingestion/component/parser.go b/internal/ingestion/component/parser.go index 5031a5b4be..19c74443f0 100644 --- a/internal/ingestion/component/parser.go +++ b/internal/ingestion/component/parser.go @@ -314,7 +314,7 @@ func defaultSetups() map[string]schema.ParserSetup { "aiff", "au", "midi", "wma", "realaudio", "vqf", "oggvorbis", "ape", }, - "output_format": "json", + "output_format": "text", }, "video": { "suffix": []string{"mp4", "avi", "mkv"}, diff --git a/internal/ingestion/component/parser_dispatch_test.go b/internal/ingestion/component/parser_dispatch_test.go index 35036258e4..f1f9badb43 100644 --- a/internal/ingestion/component/parser_dispatch_test.go +++ b/internal/ingestion/component/parser_dispatch_test.go @@ -305,6 +305,84 @@ func TestDefaultSetups_DOCX_OutputFormatMarkdown(t *testing.T) { } } +// TestResolveOutputFormat_AudioOutputFormats pins the audio-family +// whitelist against the builtin audio template. The template +// (ingestion_pipeline_audio.json) and the Python default audio setup +// (rag/flow/parser/parser.py) both use output_format="text" — an audio +// transcription is inherently plain text. "text" must therefore pass +// the whitelist, "json" must stay accepted, and a format outside the +// whitelist must still be rejected. +func TestResolveOutputFormat_AudioOutputFormats(t *testing.T) { + allowed := schema.ParserParam{}.Defaults().AllowedOutputFormat + audioAllowed, ok := allowed["audio"] + if !ok { + t.Fatal("allowed_output_format: audio key missing") + } + has := func(want string) bool { + for _, v := range audioAllowed { + if strings.EqualFold(v, want) { + return true + } + } + return false + } + if !has("text") { + t.Errorf("allowed_output_format[audio] = %v, want it to include %q (builtin audio template and Python default use it)", audioAllowed, "text") + } + if !has("json") { + t.Errorf("allowed_output_format[audio] = %v, want it to include %q", audioAllowed, "json") + } + + cases := []struct { + name string + format string + wantErr bool + }{ + {name: "text accepted", format: "text"}, + {name: "json accepted", format: "json"}, + {name: "html rejected", format: "html", wantErr: true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + setups := map[string]schema.ParserSetup{"audio": {"output_format": tc.format}} + got, err := resolveOutputFormat("audio", setups, allowed) + if tc.wantErr { + if err == nil { + t.Fatalf("want error for audio output_format=%q, got %q", tc.format, got) + } + return + } + if err != nil { + t.Fatalf("audio output_format=%q: unexpected error: %v", tc.format, err) + } + if got != tc.format { + t.Errorf("got %q, want %q", got, tc.format) + } + }) + } +} + +// TestDefaultSetups_Audio_OutputFormatText pins the audio default to +// "text", matching the Python default setup +// (rag/flow/parser/parser.py audio block) and the builtin audio +// template. The default feeds both the whitelist gate and the ASR +// dispatch, so it must not drift to a format the audio pipeline does +// not produce. +func TestDefaultSetups_Audio_OutputFormatText(t *testing.T) { + setups := defaultSetups() + audio, ok := setups["audio"] + if !ok { + t.Fatal("defaultSetups: audio key missing") + } + got, ok := audio["output_format"].(string) + if !ok { + t.Fatal("defaultSetups: audio.output_format missing or not a string") + } + if got != "text" { + t.Errorf("audio.output_format = %q, want %q", got, "text") + } +} + func TestConfigureParserFromSetups_UsesPythonFamilySetup(t *testing.T) { setups := defaultSetups() got := &captureSetupConfigurer{} diff --git a/internal/ingestion/component/schema/parser.go b/internal/ingestion/component/schema/parser.go index d47ed57df9..5f6e91d1fc 100644 --- a/internal/ingestion/component/schema/parser.go +++ b/internal/ingestion/component/schema/parser.go @@ -85,7 +85,7 @@ func (ParserParam) Defaults() ParserParam { "markdown": {"text", "json"}, "text&code": {"text", "json"}, "html": {"text", "json"}, - "audio": {"json"}, + "audio": {"text", "json"}, "video": {}, "epub": {"text", "json"}, "json": {"json"},