filter_var(env('followup_audio.ENABLED', false), FILTER_VALIDATE_BOOLEAN), 'audio_verified' => filter_var(env('followup_audio.AUDIO_VERIFIED', false), FILTER_VALIDATE_BOOLEAN), // Each selected application must separately pass all three synthetic fixtures. 'verified_profiles' => array_values(array_intersect(['qwen', 'openai'], array_filter(array_map('trim', explode(',', (string) env('followup_audio.VERIFIED_PROFILES', '')))))), // LLM-suggested media offsets are not reliable ASR alignment; leave off until separately verified. 'timestamp_verified' => false, 'profile' => (string) env('followup_audio.PROFILE', 'qwen'), 'max_bytes' => 524288000, 'max_seconds' => 3600, 'chunk_bytes' => 2097152, 'retention_days' => 90, 'lease_seconds' => 600, 'request_timeout' => (int) env('followup_audio.REQUEST_TIMEOUT', 240), 'concurrency' => max(1, min(8, (int) env('followup_audio.CONCURRENCY', 1))), // Keep one stable key across web/worker nodes; empty reuses the existing prescription AI key. 'encryption_key' => (string) env('followup_audio.ENCRYPTION_KEY', ''), 'private_dir' => runtime_path() . 'private/followup_audio', // Original upload stays <=500 MiB. The same Dify app receives at most this private processing copy. 'upstream_max_bytes' => (int) env('followup_audio.UPSTREAM_MAX_BYTES', 20971520), 'normalize_timeout' => (int) env('followup_audio.NORMALIZE_TIMEOUT', 120), 'ffmpeg' => (string) env('followup_audio.FFMPEG', 'ffmpeg'), 'ffprobe' => (string) env('followup_audio.FFPROBE', 'ffprobe'), 'max_response_bytes' => 8388608, 'asr_stereo_chunk_seconds' => (int) env('followup_audio.ASR_STEREO_CHUNK_SECONDS', 15), 'asr_chunk_seconds' => (int) env('followup_audio.ASR_CHUNK_SECONDS', 120), 'asr_max_chunk_bytes' => 4194304, 'asr_request_timeout' => (int) env('followup_audio.ASR_REQUEST_TIMEOUT', 240), 'extraction_request_timeout' => (int) env('followup_audio.EXTRACTION_REQUEST_TIMEOUT', 240), // HTTP is permitted only for explicitly acknowledged synthetic probes, never patient requests. 'allow_insecure_synthetic' => filter_var(env('followup_audio.ALLOW_INSECURE_SYNTHETIC', false), FILTER_VALIDATE_BOOLEAN), // Stable logical slots preserve task schema. No model identifier or supplier is hardcoded. 'providers' => array_combine(['qwen', 'openai'], array_map(static function (string $profile): array { $prefix = 'followup_audio.' . strtoupper($profile) . '_'; return [ 'driver' => (string) env($prefix . 'DRIVER', 'dify'), // Only the dify driver may reuse empty base/key fields from prescription_ai. 'base_url' => (string) env($prefix . 'BASE_URL', ''), 'api_key' => (string) env($prefix . 'API_KEY', ''), 'model' => (string) env($prefix . 'MODEL', ''), 'label' => (string) env($prefix . 'LABEL', $profile), // Explicit server-only two-stage identities; no fallback to prescription_ai or another slot. 'allow_loopback_tunnel' => filter_var(env($prefix . 'ALLOW_LOOPBACK_TUNNEL', false), FILTER_VALIDATE_BOOLEAN), 'asr' => [ 'base_url' => (string) env($prefix . 'ASR_BASE_URL', ''), 'api_key' => (string) env($prefix . 'ASR_API_KEY', ''), 'model' => (string) env($prefix . 'ASR_MODEL', ''), ], 'extraction' => [ 'base_url' => (string) env($prefix . 'EXTRACTION_BASE_URL', ''), 'api_key' => (string) env($prefix . 'EXTRACTION_API_KEY', ''), 'model' => (string) env($prefix . 'EXTRACTION_MODEL', ''), 'response_format' => (string) env($prefix . 'EXTRACTION_RESPONSE_FORMAT', 'json_schema'), 'max_tokens' => (int) env($prefix . 'EXTRACTION_MAX_TOKENS', 8192), // Unset is portable; explicit true/false opts into the provider's Qwen chat-template extension. 'enable_thinking' => env($prefix . 'EXTRACTION_ENABLE_THINKING', null) === null ? null : (filter_var(env($prefix . 'EXTRACTION_ENABLE_THINKING'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? 'invalid'), ], 'verified_fingerprint' => (string) env($prefix . 'VERIFIED_FINGERPRINT', ''), ]; }, ['qwen', 'openai'])), ];