// lib/formats/openclap.dart import 'dart:convert'; import 'dart:typed_data'; import 'dart:math'; import 'dart:io'; import 'package:uuid/uuid.dart'; import 'package:yaml/yaml.dart'; import 'package:http/http.dart' as http; enum ClapFormat { clap0('clap-0'), clap0b('clap-0b'); final String value; const ClapFormat(this.value); static ClapFormat fromString(String value) { return ClapFormat.values.firstWhere( (e) => e.value == value, orElse: () => ClapFormat.clap0, ); } } enum ClapSegmentCategory { splat('SPLAT'), mesh('MESH'), depth('DEPTH'), effect('EFFECT'), event('EVENT'), interface('INTERFACE'), phenomenon('PHENOMENON'), video('VIDEO'), image('IMAGE'), transition('TRANSITION'), character('CHARACTER'), location('LOCATION'), time('TIME'), era('ERA'), lighting('LIGHTING'), weather('WEATHER'), action('ACTION'), music('MUSIC'), sound('SOUND'), dialogue('DIALOGUE'), style('STYLE'), camera('CAMERA'), group('GROUP'), generic('GENERIC'); final String value; const ClapSegmentCategory(this.value); // Updated to handle nullable String input static ClapSegmentCategory fromString(String? value) { if (value == null) return ClapSegmentCategory.generic; return ClapSegmentCategory.values.firstWhere( (e) => e.value == value.toUpperCase(), orElse: () => ClapSegmentCategory.generic, ); } } enum ClapImageRatio { landscape('LANDSCAPE'), portrait('PORTRAIT'), square('SQUARE'); final String value; const ClapImageRatio(this.value); static ClapImageRatio fromString(String? value) { if (value == null) return ClapImageRatio.landscape; return ClapImageRatio.values.firstWhere( (e) => e.value == value.toUpperCase(), orElse: () => ClapImageRatio.landscape, ); } } enum ClapOutputType { text('TEXT'), animation('ANIMATION'), interface('INTERFACE'), event('EVENT'), phenomenon('PHENOMENON'), transition('TRANSITION'), image('IMAGE'), imageSegmentation('IMAGE_SEGMENTATION'), imageDepth('IMAGE_DEPTH'), video('VIDEO'), videoSegmentation('VIDEO_SEGMENTATION'), videoDepth('VIDEO_DEPTH'), audio('AUDIO'); final String value; const ClapOutputType(this.value); static ClapOutputType fromString(String? value) { if (value == null) return ClapOutputType.text; return ClapOutputType.values.firstWhere( (e) => e.value == value.toUpperCase(), orElse: () => ClapOutputType.text, ); } } enum ClapAssetSource { remote('REMOTE'), path('PATH'), data('DATA'), prompt('PROMPT'), empty('EMPTY'); final String value; const ClapAssetSource(this.value); static ClapAssetSource fromString(String? value) { if (value == null) return ClapAssetSource.empty; return ClapAssetSource.values.firstWhere( (e) => e.value == value.toUpperCase(), orElse: () => ClapAssetSource.empty, ); } } /// Data classes for CLAP structure class ClapMeta { final String id; final String title; final String description; final String caption; final String licence; final int bpm; final double frameRate; final List tags; final String thumbnailUrl; final ClapImageRatio imageRatio; final int durationInMs; final int width; final int height; final String imagePrompt; final String systemPrompt; final String storyPrompt; final bool isLoop; final bool isInteractive; ClapMeta({ String? id, this.title = '', this.description = '', this.caption = '', this.licence = '', this.bpm = 120, this.frameRate = 24, this.tags = const [], this.thumbnailUrl = '', ClapImageRatio? imageRatio, this.durationInMs = 4000, this.width = 1024, this.height = 576, this.imagePrompt = '', this.systemPrompt = '', this.storyPrompt = '', this.isLoop = false, this.isInteractive = false, }) : id = id ?? const Uuid().v4(), imageRatio = imageRatio ?? ClapImageRatio.landscape; factory ClapMeta.fromMap(Map map) { return ClapMeta( id: map['id'] as String?, title: map['title'] as String? ?? '', description: map['description'] as String? ?? '', caption: map['caption'] as String? ?? '', licence: map['licence'] as String? ?? '', bpm: (map['bpm'] as num?)?.toInt() ?? 120, frameRate: (map['frameRate'] as num?)?.toDouble() ?? 24, tags: List.from(map['tags'] ?? []), thumbnailUrl: map['thumbnailUrl'] as String? ?? '', imageRatio: ClapImageRatio.fromString(map['imageRatio'] as String?), durationInMs: (map['durationInMs'] as num?)?.toInt() ?? 4000, width: (map['width'] as num?)?.toInt() ?? 1024, height: (map['height'] as num?)?.toInt() ?? 576, imagePrompt: map['imagePrompt'] as String? ?? '', systemPrompt: map['systemPrompt'] as String? ?? '', storyPrompt: map['storyPrompt'] as String? ?? '', isLoop: map['isLoop'] as bool? ?? false, isInteractive: map['isInteractive'] as bool? ?? false, ); } Map toMap() { return { 'id': id, 'title': title, 'description': description, 'caption': caption, 'licence': licence, 'bpm': bpm, 'frameRate': frameRate, 'tags': tags, 'thumbnailUrl': thumbnailUrl, 'imageRatio': imageRatio.value, 'durationInMs': durationInMs, 'width': width, 'height': height, 'imagePrompt': imagePrompt, 'systemPrompt': systemPrompt, 'storyPrompt': storyPrompt, 'isLoop': isLoop, 'isInteractive': isInteractive, }; } } class ClapSegment { final String id; final String parentId; final List childrenIds; final int track; final int startTimeInMs; final int endTimeInMs; final ClapSegmentCategory category; final String entityId; final String workflowId; final String sceneId; final int startTimeInLines; final int endTimeInLines; final String prompt; final String label; final ClapOutputType outputType; final String renderId; final String status; final String assetUrl; final int assetDurationInMs; final ClapAssetSource assetSourceType; final String assetFileFormat; final String createdAt; final String createdBy; final int revision; final String editedBy; final double outputGain; final int seed; ClapSegment({ String? id, this.parentId = '', this.childrenIds = const [], this.track = 0, this.startTimeInMs = 0, this.endTimeInMs = 0, ClapSegmentCategory? category, this.entityId = '', this.workflowId = '', this.sceneId = '', this.startTimeInLines = 0, this.endTimeInLines = 0, this.prompt = '', this.label = '', ClapOutputType? outputType, this.renderId = '', this.status = 'TO_GENERATE', this.assetUrl = '', this.assetDurationInMs = 0, ClapAssetSource? assetSourceType, this.assetFileFormat = '', String? createdAt, this.createdBy = 'ai', this.revision = 0, this.editedBy = 'ai', this.outputGain = 0, int? seed, }) : id = id ?? const Uuid().v4(), category = category ?? ClapSegmentCategory.generic, outputType = outputType ?? ClapOutputType.text, assetSourceType = assetSourceType ?? ClapAssetSource.empty, createdAt = createdAt ?? DateTime.now().toIso8601String(), seed = seed ?? Random().nextInt(1 << 31); factory ClapSegment.fromMap(Map map) { return ClapSegment( id: map['id'] as String?, parentId: map['parentId'] as String? ?? '', childrenIds: List.from(map['childrenIds'] ?? []), track: (map['track'] as num?)?.toInt() ?? 0, startTimeInMs: (map['startTimeInMs'] as num?)?.toInt() ?? 0, endTimeInMs: (map['endTimeInMs'] as num?)?.toInt() ?? 0, category: ClapSegmentCategory.fromString(map['category'] as String?), entityId: map['entityId'] as String? ?? '', workflowId: map['workflowId'] as String? ?? '', sceneId: map['sceneId'] as String? ?? '', startTimeInLines: (map['startTimeInLines'] as num?)?.toInt() ?? 0, endTimeInLines: (map['endTimeInLines'] as num?)?.toInt() ?? 0, prompt: map['prompt'] as String? ?? '', label: map['label'] as String? ?? '', outputType: ClapOutputType.fromString(map['outputType'] as String?), renderId: map['renderId'] as String? ?? '', status: map['status'] as String? ?? 'TO_GENERATE', assetUrl: map['assetUrl'] as String? ?? '', assetDurationInMs: (map['assetDurationInMs'] as num?)?.toInt() ?? 0, assetSourceType: ClapAssetSource.fromString(map['assetSourceType'] as String?), assetFileFormat: map['assetFileFormat'] as String? ?? '', createdAt: map['createdAt'] as String?, createdBy: map['createdBy'] as String? ?? 'ai', revision: (map['revision'] as num?)?.toInt() ?? 0, editedBy: map['editedBy'] as String? ?? 'ai', outputGain: (map['outputGain'] as num?)?.toDouble() ?? 0, seed: (map['seed'] as num?)?.toInt(), ); } Map toMap() { return { 'id': id, 'parentId': parentId, 'childrenIds': childrenIds, 'track': track, 'startTimeInMs': startTimeInMs, 'endTimeInMs': endTimeInMs, 'category': category.value, 'entityId': entityId, 'workflowId': workflowId, 'sceneId': sceneId, 'startTimeInLines': startTimeInLines, 'endTimeInLines': endTimeInLines, 'prompt': prompt, 'label': label, 'outputType': outputType.value, 'renderId': renderId, 'status': status, 'assetUrl': assetUrl, 'assetDurationInMs': assetDurationInMs, 'assetSourceType': assetSourceType.value, 'assetFileFormat': assetFileFormat, 'createdAt': createdAt, 'createdBy': createdBy, 'revision': revision, 'editedBy': editedBy, 'outputGain': outputGain, 'seed': seed, }; } } /// Main CLAP parser class class ClapParser { static Future> parseClap(dynamic source, { bool debug = false, void Function(double progress, String message)? onProgress, }) async { onProgress?.call(0, 'Opening .clap file...'); // Handle different input types String yamlString; if (source is String) { if (source.startsWith('data:application/x-gzip;base64,') || source.startsWith('data:application/octet-stream;base64,')) { // Handle base64 data URI yamlString = await _decompressBase64(source); } else if (source.startsWith('http://') || source.startsWith('https://')) { // Handle remote URL onProgress?.call(0.2, 'Downloading .clap file...'); final response = await http.get(Uri.parse(source)); if (response.statusCode != 200) { throw Exception('Failed to download the .clap file'); } yamlString = await _decompressBytes(response.bodyBytes); } else { // Assume direct YAML string yamlString = source; } } else if (source is Uint8List) { // Handle compressed bytes yamlString = await _decompressBytes(source); } else { throw Exception('Unsupported source type'); } onProgress?.call(0.4, 'Parsing .clap file...'); // Parse YAML final yaml = loadYaml(yamlString) as YamlList; if (yaml.length < 2) { throw Exception('Invalid CLAP file: missing header or metadata'); } // Validate format final header = yaml[0] as YamlMap; if (header['format'] != ClapFormat.clap0.value) { throw Exception('Invalid CLAP format'); } onProgress?.call(0.6, 'Processing metadata...'); // Parse metadata final meta = ClapMeta.fromMap(_yamlToMap(yaml[1] as YamlMap)); // Parse segments and other components final segments = []; final expectedSegments = (header['numberOfSegments'] as int?) ?? 0; onProgress?.call(0.8, 'Processing segments...'); for (int i = 2; i < yaml.length && i < (2 + expectedSegments); i++) { segments.add(ClapSegment.fromMap(_yamlToMap(yaml[i] as YamlMap))); } onProgress?.call(1.0, 'Completed parsing'); return { 'meta': meta, 'segments': segments, // Add other components as needed }; } /// Helper method to decompress base64 data URI static Future _decompressBase64(String dataUri) async { final base64Data = dataUri.split(',')[1]; final bytes = base64Decode(base64Data); return _decompressBytes(bytes); } /// Helper method to decompress gzipped bytes static Future _decompressBytes(Uint8List bytes) async { try { final decompressed = GZipCodec().decode(bytes); return utf8.decode(decompressed); } catch (e) { throw Exception('Failed to decompress CLAP file: $e'); } } /// Helper method to convert YamlMap to regular Map static Map _yamlToMap(YamlMap yaml) { return Map.from(yaml); } } /// CLAP Serializer class for creating CLAP files class ClapSerializer { static Future serializeClap(Map clap) async { final meta = clap['meta'] as ClapMeta; final segments = (clap['segments'] as List?)?.toList() ?? []; // Create header final header = { 'format': ClapFormat.clap0.value, 'numberOfSegments': segments.length, // Add other counts as needed }; // Create YAML entries final entries = [ header, meta.toMap(), ...segments.map((s) => s.toMap()), ]; // Convert to YAML string final yaml = toYamlString(entries); // Compress final compressed = GZipCodec().encode(utf8.encode(yaml)); return Uint8List.fromList(compressed); } /// Helper method to convert data to YAML string static String toYamlString(List> entries) { final buffer = StringBuffer(); for (final entry in entries) { buffer.writeln('---'); _writeYamlMap(entry, buffer); } return buffer.toString(); } /// Helper method to write map as YAML static void _writeYamlMap(Map map, StringBuffer buffer, [String indent = '']) { for (final entry in map.entries) { if (entry.value == null) continue; if (entry.value is Map) { buffer.writeln('$indent${entry.key}:'); _writeYamlMap(entry.value as Map, buffer, '$indent '); } else if (entry.value is List) { if ((entry.value as List).isEmpty) { buffer.writeln('$indent${entry.key}: []'); } else { buffer.writeln('$indent${entry.key}:'); for (final item in entry.value as List) { if (item is Map) { buffer.writeln('$indent -'); _writeYamlMap(item as Map, buffer, '$indent '); } else { buffer.writeln('$indent - $item'); } } } } else { buffer.writeln('$indent${entry.key}: ${_formatYamlValue(entry.value)}'); } } } /// Helper method to format YAML values static String _formatYamlValue(dynamic value) { if (value is String) { if (value.contains('\n') || value.contains(':') || value.contains('#')) { return '|\n ${value.replaceAll('\n', '\n ')}'; } return value.contains(' ') ? '"$value"' : value; } return value.toString(); } } /// Example usage class class ClapFile { static Future fromSource(dynamic source) async { final parsed = await ClapParser.parseClap(source); return ClapFile._(parsed); } final ClapMeta meta; final List segments; // Add other components as needed ClapFile._(Map parsed) : meta = parsed['meta'] as ClapMeta, segments = (parsed['segments'] as List?)?.toList() ?? []; Future serialize() async { return ClapSerializer.serializeClap({ 'meta': meta, 'segments': segments, }); } /// Helper method to save to a file Future saveToFile(String path) async { final bytes = await serialize(); await File(path).writeAsBytes(bytes); } /// Helper method to create a data URI Future toDataUri() async { final bytes = await serialize(); final base64 = base64Encode(bytes); return 'data:application/x-gzip;base64,$base64'; } }