aitube2 / lib /formats /openclap.dart
jbilcke-hf's picture
jbilcke-hf HF Staff
initial commit log 🪵🦫
5acd9c3
// 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<String> 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<String, dynamic> 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<String>.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<String, dynamic> 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<String> 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<String, dynamic> map) {
return ClapSegment(
id: map['id'] as String?,
parentId: map['parentId'] as String? ?? '',
childrenIds: List<String>.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<String, dynamic> 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<Map<String, dynamic>> 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 = <ClapSegment>[];
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<String> _decompressBase64(String dataUri) async {
final base64Data = dataUri.split(',')[1];
final bytes = base64Decode(base64Data);
return _decompressBytes(bytes);
}
/// Helper method to decompress gzipped bytes
static Future<String> _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<String, dynamic> _yamlToMap(YamlMap yaml) {
return Map<String, dynamic>.from(yaml);
}
}
/// CLAP Serializer class for creating CLAP files
class ClapSerializer {
static Future<Uint8List> serializeClap(Map<String, dynamic> clap) async {
final meta = clap['meta'] as ClapMeta;
final segments = (clap['segments'] as List<ClapSegment>?)?.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<Map<String, dynamic>> 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<String, dynamic> 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<String, dynamic>, 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<String, dynamic>, 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<ClapFile> fromSource(dynamic source) async {
final parsed = await ClapParser.parseClap(source);
return ClapFile._(parsed);
}
final ClapMeta meta;
final List<ClapSegment> segments;
// Add other components as needed
ClapFile._(Map<String, dynamic> parsed)
: meta = parsed['meta'] as ClapMeta,
segments = (parsed['segments'] as List<ClapSegment>?)?.toList() ?? [];
Future<Uint8List> serialize() async {
return ClapSerializer.serializeClap({
'meta': meta,
'segments': segments,
});
}
/// Helper method to save to a file
Future<void> saveToFile(String path) async {
final bytes = await serialize();
await File(path).writeAsBytes(bytes);
}
/// Helper method to create a data URI
Future<String> toDataUri() async {
final bytes = await serialize();
final base64 = base64Encode(bytes);
return 'data:application/x-gzip;base64,$base64';
}
}