import 'dart:io'; import 'package:dio/dio.dart'; import '../cekirdek/log.dart'; /// ACE-Step REST API'sine konuşan istemci (start_api_server.bat ile başlatılır). /// Port: 8001 (varsayılan). /// /// Endpoints: /// - POST /release_task → task oluştur (task_id döner) /// - POST /query_result → task durumlarını sorgula (1=tamam, 0=çalışıyor, 2=hata) /// - GET /v1/audio?path=... → ses dosyası indir /// - GET /health → ping class AceStepApiIstemci { final String baseUrl; final String? apiKey; late final Dio _dio; AceStepApiIstemci({ this.baseUrl = 'http://127.0.0.1:8001', this.apiKey, }) { _dio = Dio(BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(minutes: 5), headers: { 'Content-Type': 'application/json', if (apiKey != null) 'Authorization': 'Bearer $apiKey', }, )); } Future sagliklimi() async { try { final r = await _dio.get('/health', options: Options(receiveTimeout: const Duration(seconds: 3))); return r.statusCode == 200; } catch (_) { return false; } } /// Bağlantı testi: TCP soket static Future portAcikMi({String host = '127.0.0.1', int port = 8001}) async { try { final s = await Socket.connect(host, port, timeout: const Duration(milliseconds: 500)); s.destroy(); return true; } catch (_) { return false; } } /// Yeni bir müzik üretim görevi oluşturur. /// Returns: task_id listesi (batchSize > 1 ise birden fazla) Future> uretimBaslat({ // Temel required String prompt, String globalCaption = '', String lyrics = '', double? audioDuration, int inferenceSteps = 8, double guidanceScale = 7.0, int? bpm, String keyScale = '', String timeSignature = '', String vocalLanguage = 'en', /// Otomatik dil algılama — LLM her vokal segmenti için dili tespit eder. /// Çok dilli şarkılar için (TR + EN + ES karışık) açık tutulmalı. bool useCotLanguage = true, int seed = -1, bool useRandomSeed = true, String taskType = 'text2music', bool thinking = false, bool sampleMode = false, String sampleQuery = '', bool useFormat = false, String? model, // Gelişmiş — DiT String inferMethod = 'ode', // ode | sde double shift = 3.0, // 1.0-5.0 bool useAdg = false, double cfgIntervalStart = 0.0, double cfgIntervalEnd = 1.0, String? customTimesteps, // Repaint / audio2audio double repaintingStart = 0.0, double? repaintingEnd, String repaintMode = 'balanced', // conservative | balanced | aggressive double repaintStrength = 0.5, int repaintLatentCrossfadeFrames = 10, double repaintWavCrossfadeSec = 0.0, String? referenceAudioPath, String? srcAudioPath, double audioCoverStrength = 1.0, double coverNoiseStrength = 0.0, String chunkMaskMode = 'auto', // auto | explicit String audioCodeString = '', // Analiz modları bool analysisOnly = false, bool fullAnalysisOnly = false, bool extractCodesOnly = false, // Batch int? batchSize, // Instruction String? instruction, // Çıkış formatı String? audioFormat, // wav/mp3/flac int? mp3Bitrate, // 128/192/256/320 int? mp3SampleRate, // 22050/44100/48000 bool? enableNormalization, double? normalizationDb, // örn -14 double? fadeInDuration, // saniye double? fadeOutDuration, // saniye double? scoreScale, double? latentShift, double? latentRescale, int? lmBatchChunkSize, String? loraPath, String? samplerMode, double? velocityNormThreshold, double? velocityEmaFactor, int? mlxVaeChunkSize, // ===== LM (Language Model) parametreleri — lirik/caption üretimi için ===== /// LM için olumsuz prompt — neyi istemediğin LLM'e söyle String? lmNegativePrompt, /// LM sampling sıcaklığı (0-2). Düşük = tutarlı, yüksek = yaratıcı double? lmTemperature, /// LM CFG ölçeği — talimatlara sadakat (1-10) double? lmCfgScale, /// LM top-k sampling int? lmTopK, /// LM nucleus sampling (0-1) double? lmTopP, /// LM tekrar cezası (1.0 = nötr, >1 = tekrara karşı, <1 = tekrara eğilim) double? lmRepetitionPenalty, /// Özel LM model yolu String? lmModelPath, /// LM backend: vllm | pt | mlx String? lmBackend, // ===== Decoder / VAE parametreleri ===== /// Mozaik VAE dekodlama (VRAM tasarrufu) bool? useTiledDecode, /// Sınırlandırılmış codec dekodlama (kalite/hız trade-off) bool? constrainedDecoding, bool? constrainedDecodingDebug, // ===== Caption / metadata ===== /// Chain-of-Thought caption (otomatik etiketleme) bool? useCotCaption, /// Caption formatlama bool? isFormatCaption, /// LM batch işlemler aktif bool? allowLmBatch, /// Parça/track adı String? trackName, // Ekstra ayarlar (server-side de olabilir, doğrudan eklenir) Map ekstraParametreler = const {}, }) async { final body = { 'prompt': prompt, 'global_caption': globalCaption, 'lyrics': lyrics, 'thinking': thinking, 'sample_mode': sampleMode, 'sample_query': sampleQuery, 'use_format': useFormat, 'inference_steps': inferenceSteps, 'guidance_scale': guidanceScale, 'vocal_language': vocalLanguage, 'use_cot_language': useCotLanguage, 'use_random_seed': useRandomSeed, 'seed': seed, 'task_type': taskType, 'infer_method': inferMethod, 'shift': shift, 'use_adg': useAdg, 'cfg_interval_start': cfgIntervalStart, 'cfg_interval_end': cfgIntervalEnd, 'repainting_start': repaintingStart, 'repaint_mode': repaintMode, 'repaint_strength': repaintStrength, 'repaint_latent_crossfade_frames': repaintLatentCrossfadeFrames, 'repaint_wav_crossfade_sec': repaintWavCrossfadeSec, 'audio_cover_strength': audioCoverStrength, 'cover_noise_strength': coverNoiseStrength, 'chunk_mask_mode': chunkMaskMode, 'audio_code_string': audioCodeString, 'analysis_only': analysisOnly, 'full_analysis_only': fullAnalysisOnly, 'extract_codes_only': extractCodesOnly, if (audioDuration != null) 'audio_duration': audioDuration, if (bpm != null) 'bpm': bpm, if (keyScale.isNotEmpty) 'key_scale': keyScale, if (timeSignature.isNotEmpty) 'time_signature': timeSignature, if (model != null && model.isNotEmpty) 'model': model, if (customTimesteps != null && customTimesteps.isNotEmpty) 'timesteps': customTimesteps, if (repaintingEnd != null) 'repainting_end': repaintingEnd, if (referenceAudioPath != null && referenceAudioPath.isNotEmpty) 'reference_audio_path': referenceAudioPath, if (srcAudioPath != null && srcAudioPath.isNotEmpty) 'src_audio_path': srcAudioPath, if (batchSize != null) 'batch_size': batchSize, if (instruction != null && instruction.isNotEmpty) 'instruction': instruction, if (audioFormat != null && audioFormat.isNotEmpty) 'audio_format': audioFormat, if (mp3Bitrate != null) 'mp3_bitrate': mp3Bitrate, if (mp3SampleRate != null) 'mp3_sample_rate': mp3SampleRate, if (enableNormalization != null) 'enable_normalization': enableNormalization, if (normalizationDb != null) 'normalization_db': normalizationDb, if (fadeInDuration != null) 'fade_in_duration': fadeInDuration, if (fadeOutDuration != null) 'fade_out_duration': fadeOutDuration, if (scoreScale != null) 'score_scale': scoreScale, if (latentShift != null) 'latent_shift': latentShift, if (latentRescale != null) 'latent_rescale': latentRescale, if (lmBatchChunkSize != null) 'lm_batch_chunk_size': lmBatchChunkSize, if (loraPath != null && loraPath.isNotEmpty) 'lora_path': loraPath, if (samplerMode != null && samplerMode.isNotEmpty) 'sampler_mode': samplerMode, if (velocityNormThreshold != null) 'velocity_norm_threshold': velocityNormThreshold, if (velocityEmaFactor != null) 'velocity_ema_factor': velocityEmaFactor, if (mlxVaeChunkSize != null) 'mlx_vae_chunk_size': mlxVaeChunkSize, // LM parametreleri if (lmNegativePrompt != null && lmNegativePrompt.isNotEmpty) 'lm_negative_prompt': lmNegativePrompt, if (lmTemperature != null) 'lm_temperature': lmTemperature, if (lmCfgScale != null) 'lm_cfg_scale': lmCfgScale, if (lmTopK != null) 'lm_top_k': lmTopK, if (lmTopP != null) 'lm_top_p': lmTopP, if (lmRepetitionPenalty != null) 'lm_repetition_penalty': lmRepetitionPenalty, if (lmModelPath != null && lmModelPath.isNotEmpty) 'lm_model_path': lmModelPath, if (lmBackend != null && lmBackend.isNotEmpty) 'lm_backend': lmBackend, // Decoder if (useTiledDecode != null) 'use_tiled_decode': useTiledDecode, if (constrainedDecoding != null) 'constrained_decoding': constrainedDecoding, if (constrainedDecodingDebug != null) 'constrained_decoding_debug': constrainedDecodingDebug, // Caption if (useCotCaption != null) 'use_cot_caption': useCotCaption, if (isFormatCaption != null) 'is_format_caption': isFormatCaption, if (allowLmBatch != null) 'allow_lm_batch': allowLmBatch, if (trackName != null && trackName.isNotEmpty) 'track_name': trackName, ...ekstraParametreler, }; Log.bilgi('AceStepApi', 'Üretim başlatıldı: ${prompt.substring(0, prompt.length.clamp(0, 60))}...'); final r = await _dio.post('/release_task', data: body); final data = r.data is Map ? r.data as Map : {}; final inner = data['data'] is Map ? data['data'] as Map : data; // Batch desteği — server batchSize > 1 için 'task_ids' (liste) döner. // Tek üretim için 'task_id' (string) döner. İki formatı da yakala. final ids = []; final liste = inner['task_ids'] ?? inner['task_id_list']; if (liste is List) { for (final el in liste) { if (el != null) ids.add(el.toString()); } } else { final tek = inner['task_id']; if (tek != null) ids.add(tek.toString()); } if (ids.isEmpty) { throw Exception('task_id/task_ids boş döndü: ${r.data}'); } return ids; } /// Bir veya birden fazla task için ilerleme sorgular. /// Returns: List<{task_id, status (0/1/2), result, progress_text}> Future>> sonucSorgula(List taskIds) async { final r = await _dio.post('/query_result', data: { 'task_id_list': taskIds, }); final data = r.data is Map ? r.data as Map : {}; final inner = data['data']; if (inner is List) { return inner.map((e) => Map.from(e as Map)).toList(); } if (data is Map) { // Bazı sürümler doğrudan listeyi data anahtarı altında değil verebilir final liste = data['result'] ?? data['results'] ?? data['data']; if (liste is List) { return liste.map((e) => Map.from(e as Map)).toList(); } } return []; } /// /v1/audio?path=... ile ses dosyasını indirip belirtilen yola kaydeder. Future sesIndir(String sunucuPath, String hedefDosya) async { final r = await _dio.get( '/v1/audio', queryParameters: {'path': sunucuPath}, options: Options(responseType: ResponseType.bytes), ); final f = File(hedefDosya); await f.parent.create(recursive: true); await f.writeAsBytes(r.data as List); return f; } Future>> modelListesi() async { try { final r = await _dio.get('/v1/models'); final data = r.data; Log.bilgi('AceStepApi', 'Model listesi yanıtı: ${r.data}'); List? ham; if (data is Map) { // Olası anahtarları sırayla dene for (final anahtar in const [ 'data', 'models', 'available_models', 'list', 'available_lm_models', ]) { if (data[anahtar] is List) { ham = data[anahtar] as List; break; } } } else if (data is List) { ham = data; } if (ham == null) return []; // Liste içindeki öğeler Map veya String olabilir. return ham.map>((e) { if (e is Map) return Map.from(e); // String ise model id olarak sar return {'id': e.toString()}; }).toList(); } catch (e) { Log.uyari('AceStepApi', 'Model listesi alınamadı: $e'); } return []; } /// Sample mode için LLM ile rastgele caption/lyrics üretir. Future> rastgeleSampleOlustur({ String description = '', String vocalLanguage = 'en', }) async { try { final r = await _dio.post('/create_random_sample', data: { 'description': description, 'vocal_language': vocalLanguage, }); final data = r.data; if (data is Map) { return Map.from(data['data'] is Map ? data['data'] as Map : data); } } catch (e) { Log.uyari('AceStepApi', 'Random sample hatası: $e'); } return {}; } /// LLM ile lirik/caption format/iyileştirme. Future> formatIyilestir({ required String prompt, String lyrics = '', String vocalLanguage = 'en', }) async { try { final r = await _dio.post('/format_input', data: { 'prompt': prompt, 'lyrics': lyrics, 'vocal_language': vocalLanguage, }); final data = r.data; if (data is Map) { return Map.from(data['data'] is Map ? data['data'] as Map : data); } } catch (e) { Log.uyari('AceStepApi', 'Format hatası: $e'); } return {}; } }