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