1use crate::services::ai::openai::OpenAIClient;
8use crate::services::ai::openrouter::OpenRouterClient;
9use crate::services::vad::{LocalVadDetector, VadAudioProcessor, VadSyncDetector};
10use crate::{
11 Result,
12 config::{Config, ConfigService},
13 core::{file_manager::FileManager, matcher::engine::MatchEngine},
14 error::SubXError,
15 services::ai::AIProvider,
16};
17
18pub struct ComponentFactory {
43 config: Config,
44}
45
46impl ComponentFactory {
47 pub fn new(config_service: &dyn ConfigService) -> Result<Self> {
57 let config = config_service.get_config()?;
58 Ok(Self { config })
59 }
60
61 pub fn create_match_engine(&self) -> Result<MatchEngine> {
70 let ai_provider = self.create_ai_provider()?;
71 let match_config = crate::core::matcher::MatchConfig {
72 confidence_threshold: 0.8, max_sample_length: self.config.ai.max_sample_length,
74 enable_content_analysis: true,
75 backup_enabled: self.config.general.backup_enabled,
76 relocation_mode: crate::core::matcher::engine::FileRelocationMode::None,
77 conflict_resolution: crate::core::matcher::engine::ConflictResolution::AutoRename,
78 ai_model: self.config.ai.model.clone(),
79 };
80 Ok(MatchEngine::new(ai_provider, match_config))
81 }
82
83 pub fn create_file_manager(&self) -> FileManager {
88 FileManager::new()
91 }
92
93 pub fn create_ai_provider(&self) -> Result<Box<dyn AIProvider>> {
103 create_ai_provider(&self.config.ai)
104 }
105
106 pub fn config(&self) -> &Config {
110 &self.config
111 }
112
113 pub fn create_vad_sync_detector(&self) -> Result<VadSyncDetector> {
121 VadSyncDetector::new(self.config.sync.vad.clone())
122 }
123
124 pub fn create_vad_detector(&self) -> Result<LocalVadDetector> {
132 LocalVadDetector::new(self.config.sync.vad.clone())
133 }
134
135 pub fn create_audio_processor(&self) -> Result<VadAudioProcessor> {
143 VadAudioProcessor::new()
144 }
145}
146
147fn validate_ai_config(ai_config: &crate::config::AIConfig) -> Result<()> {
161 if ai_config.api_key.as_deref().unwrap_or("").trim().is_empty() {
162 return Err(SubXError::config(
163 "AI API key is required. Set ai.api_key in configuration or use environment variable."
164 .to_string(),
165 ));
166 }
167 if ai_config.model.trim().is_empty() {
168 return Err(SubXError::config(
169 "AI model is required. Set ai.model in configuration.".to_string(),
170 ));
171 }
172 if ai_config.temperature < 0.0 || ai_config.temperature > 2.0 {
173 return Err(SubXError::config(
174 "AI temperature must be between 0.0 and 2.0.".to_string(),
175 ));
176 }
177 if ai_config.max_tokens == 0 {
178 return Err(SubXError::config(
179 "AI max_tokens must be greater than 0.".to_string(),
180 ));
181 }
182 Ok(())
183}
184
185pub fn create_ai_provider(ai_config: &crate::config::AIConfig) -> Result<Box<dyn AIProvider>> {
190 match ai_config.provider.as_str() {
191 "openai" => {
192 validate_ai_config(ai_config)?;
193 let client = OpenAIClient::from_config(ai_config)?;
194 Ok(Box::new(client))
195 }
196 "openrouter" => {
197 validate_ai_config(ai_config)?;
198 let client = OpenRouterClient::from_config(ai_config)?;
199 Ok(Box::new(client))
200 }
201 "azure-openai" => {
202 validate_ai_config(ai_config)?;
203 let client =
204 crate::services::ai::azure_openai::AzureOpenAIClient::from_config(ai_config)?;
205 Ok(Box::new(client))
206 }
207 other => Err(SubXError::config(format!(
208 "Unsupported AI provider: {}. Supported providers: openai, openrouter, anthropic, azure-openai",
209 other
210 ))),
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::config::test_service::TestConfigService;
218
219 #[test]
220 fn test_component_factory_creation() {
221 let config_service = TestConfigService::default();
222 let factory = ComponentFactory::new(&config_service);
223 assert!(factory.is_ok());
224 }
225
226 #[test]
227 fn test_factory_creation() {
228 let config_service = TestConfigService::default();
229 let factory = ComponentFactory::new(&config_service);
230 assert!(factory.is_ok());
231 }
232
233 #[test]
234 fn test_create_file_manager() {
235 let config_service = TestConfigService::default();
236 let factory = ComponentFactory::new(&config_service).unwrap();
237
238 let _file_manager = factory.create_file_manager();
239 }
242
243 #[test]
244 fn test_unsupported_ai_provider() {
245 let mut config = crate::config::Config::default();
246 config.ai.provider = "unsupported".to_string();
247
248 let result: Result<Box<dyn AIProvider>> = create_ai_provider(&config.ai);
249 assert!(result.is_err());
250
251 match result {
252 Err(e) => {
253 let error_msg = e.to_string();
254 assert!(error_msg.contains("Unsupported AI provider"));
255 }
256 Ok(_) => panic!("Expected error for unsupported provider"),
257 }
258 }
259
260 #[test]
261 fn test_create_vad_sync_detector() {
262 let config_service = TestConfigService::default();
263 let factory = ComponentFactory::new(&config_service).unwrap();
264 let result = factory.create_vad_sync_detector();
265 assert!(result.is_ok());
266 }
267
268 #[test]
269 fn test_create_vad_detector() {
270 let config_service = TestConfigService::default();
271 let factory = ComponentFactory::new(&config_service).unwrap();
272 let result = factory.create_vad_detector();
273 assert!(result.is_ok());
274 }
275
276 #[test]
277 fn test_create_audio_processor() {
278 let config_service = TestConfigService::default();
279 let factory = ComponentFactory::new(&config_service).unwrap();
280 let result = factory.create_audio_processor();
281 assert!(result.is_ok());
282 }
283
284 #[test]
285 fn test_create_ai_provider_openai_success() {
286 let config_service = TestConfigService::default();
287 config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "test-api-key");
288 let factory = ComponentFactory::new(&config_service).unwrap();
289 let result = factory.create_ai_provider();
290 assert!(result.is_ok());
291 }
292
293 #[test]
294 fn test_create_ai_provider_missing_api_key() {
295 let config_service = TestConfigService::default();
296 config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "");
297 let factory = ComponentFactory::new(&config_service).unwrap();
298 let result = factory.create_ai_provider();
299 assert!(result.is_err());
300 let error_msg = result.err().unwrap().to_string();
301 assert!(error_msg.contains("API key is required"));
302 }
303
304 #[test]
305 fn test_create_ai_provider_unsupported_provider() {
306 let config_service = TestConfigService::default();
307 config_service.set_ai_settings_and_key("unsupported-provider", "model", "key");
308 let factory = ComponentFactory::new(&config_service).unwrap();
309 let result = factory.create_ai_provider();
310 assert!(result.is_err());
311 let error_msg = result.err().unwrap().to_string();
312 assert!(error_msg.contains("Unsupported AI provider"));
313 }
314
315 #[test]
316 fn test_create_ai_provider_with_custom_base_url() {
317 let config_service = TestConfigService::default();
318 config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "test-api-key");
319 config_service.config_mut().ai.base_url = "https://custom-api.com/v1".to_string();
320 let factory = ComponentFactory::new(&config_service).unwrap();
321 let result = factory.create_ai_provider();
322 assert!(result.is_ok());
323 }
324
325 #[test]
326 fn test_create_ai_provider_openrouter_success() {
327 let config_service = TestConfigService::default();
328 config_service.set_ai_settings_and_key(
329 "openrouter",
330 "deepseek/deepseek-r1-0528:free",
331 "test-openrouter-key",
332 );
333 let factory = ComponentFactory::new(&config_service).unwrap();
334 let result = factory.create_ai_provider();
335 assert!(result.is_ok());
336 }
337
338 #[test]
339 fn test_create_ai_provider_azure_openai_success() {
340 let mut config = crate::config::Config::default();
341 config.ai.provider = "azure-openai".to_string();
342 config.ai.api_key = Some("azure-key-123".to_string());
343 config.ai.model = "dep123".to_string();
344 config.ai.api_version = Some("2025-04-01-preview".to_string());
345 config.ai.base_url = "https://example.openai.azure.com".to_string();
346 let result = create_ai_provider(&config.ai);
347 assert!(result.is_ok());
348 }
349}