diff --git a/frontend/python/compare.py b/frontend/python/compare.py new file mode 100644 index 0000000000..c2ecaf25f7 --- /dev/null +++ b/frontend/python/compare.py @@ -0,0 +1,314 @@ +import os +import hashlib +import filecmp +from pathlib import Path +from datetime import datetime +import difflib + + +def get_file_hash(filepath): + """Calculate MD5 hash of a file.""" + md5_hash = hashlib.md5() + try: + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + except Exception: + return "Error calculating hash" + + +def format_size(size_bytes): + """Format file size nicely.""" + if size_bytes < 1000: + return f"{size_bytes}B" + elif size_bytes < 1000000: + return f"{size_bytes / 1000:.1f}KB" + else: + return f"{size_bytes / 1000000:.1f}MB" + + +def count_lines(filepath): + """Count lines in a file safely.""" + try: + with open(filepath, "rb") as f: + return sum(1 for _ in f) + except Exception: + return 0 + + +def calculate_diff_ratio(file1, file2): + """Calculate how different two files are using difflib.""" + try: + with ( + open(file1, "r", errors="replace") as f1, + open(file2, "r", errors="replace") as f2, + ): + text1 = f1.readlines() + text2 = f2.readlines() + matcher = difflib.SequenceMatcher(None, text1, text2) + return 1.0 - matcher.ratio() # Higher value means more different + except Exception: + return 1.0 # If we can't compare, assume completely different + + +def get_relative_path(file_path, base_path): + """Get relative path from base directory.""" + return str(Path(file_path).relative_to(base_path)) + + +def collect_files_recursive(folder_path): + """Recursively collect all files in a folder.""" + folder_path = Path(folder_path) + file_dict = {} + + for path in folder_path.rglob("*"): + if path.is_file(): + rel_path = get_relative_path(path, folder_path) + file_dict[rel_path] = path + + return file_dict + + +def compare_folders_recursive(folder1, folder2, output_file=None): + """Compare two folders recursively and generate a detailed report.""" + # Automatically generate output filename if not specified + if output_file is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_file = f"folder_comparison_{timestamp}.md" + + # Get folder paths + folder1_path = Path(folder1) + folder2_path = Path(folder2) + + # Get shorthand names for display + folder1_name = folder1_path.name + folder2_name = folder2_path.name + + # Get all files recursively + print(f"Collecting files from {folder1_name}...") + files1 = collect_files_recursive(folder1_path) + print(f"Collecting files from {folder2_name}...") + files2 = collect_files_recursive(folder2_path) + + # All unique file paths across both folders + all_files = sorted(set(files1.keys()) | set(files2.keys())) + + # Calculate column widths for alignment + max_filepath_len = max(len(file) for file in all_files) + 2 + hash_col_width = 10 # For hash comparison + size_col_width = 20 # For size and line count + diff_col_width = 30 # For difference description + + # Initialize report + report = [ + f"# Recursive Folder Comparison Report: {folder1_name} vs {folder2_name}\n", + f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n", + "## File Comparison Table\n", + f"| {'File Path'.ljust(max_filepath_len)} | {'Status'.ljust(hash_col_width)} | {folder1_name.ljust(size_col_width)} | {folder2_name.ljust(size_col_width)} | {'Difference'.ljust(diff_col_width)} |", + f"|{'-' * max_filepath_len}|{'-' * hash_col_width}|{'-' * size_col_width}|{'-' * size_col_width}|{'-' * diff_col_width}|", + ] + + # Track file categories + identical_files = [] + different_files = [] + only_in_folder1 = [] + only_in_folder2 = [] + + # Store detailed file comparisons for sorting later + file_comparisons = [] + + # Compare each file + print(f"Comparing {len(all_files)} unique files...") + for rel_path in all_files: + in_folder1 = rel_path in files1 + in_folder2 = rel_path in files2 + + # Default values + status = "—" + size1_str = "—" + size2_str = "—" + diff_str = "—" + diff_ratio = 0 + + # Get details for folder1 + if in_folder1: + file1 = files1[rel_path] + size1 = file1.stat().st_size + lines1 = count_lines(file1) + hash1 = get_file_hash(file1) + size1_str = f"{format_size(size1)} ({lines1} lines)" + else: + hash1 = "—" + size1 = 0 + lines1 = 0 + + # Get details for folder2 + if in_folder2: + file2 = files2[rel_path] + size2 = file2.stat().st_size + lines2 = count_lines(file2) + hash2 = get_file_hash(file2) + size2_str = f"{format_size(size2)} ({lines2} lines)" + else: + hash2 = "—" + size2 = 0 + lines2 = 0 + + # Calculate difference and determine status + if not in_folder1: + status = "MISSING" + diff_str = f"Only in {folder2_name}" + only_in_folder2.append(rel_path) + diff_ratio = 1.0 # Completely different (missing) + elif not in_folder2: + status = "MISSING" + diff_str = f"Only in {folder1_name}" + only_in_folder1.append(rel_path) + diff_ratio = 1.0 # Completely different (missing) + else: + # Both files exist, compare them + if hash1 == hash2 and hash1 != "Error calculating hash": + status = "IDENTICAL" + diff_str = "Identical content" + identical_files.append(rel_path) + diff_ratio = 0.0 + else: + status = "DIFFERENT" + size_diff = size2 - size1 + lines_diff = lines2 - lines1 + sign = "+" if size_diff > 0 else "" + sign_lines = "+" if lines_diff > 0 else "" + + # Calculate how different the files are + diff_ratio = calculate_diff_ratio(file1, file2) + different_files.append((rel_path, diff_ratio)) + + # Format difference description + diff_pct = int(diff_ratio * 100) + diff_str = f"{sign}{format_size(size_diff)} ({sign_lines}{lines_diff} lines) {diff_pct}% different" + + # Store comparison for sorting later + file_comparisons.append( + { + "rel_path": rel_path, + "status": status, + "size1_str": size1_str, + "size2_str": size2_str, + "diff_str": diff_str, + "diff_ratio": diff_ratio, + "in_folder1": in_folder1, + "in_folder2": in_folder2, + "hash1": hash1[:8] if len(hash1) > 8 else hash1, + "hash2": hash2[:8] if len(hash2) > 8 else hash2, + } + ) + + # Sort files by difference ratio (most different first) + file_comparisons.sort(key=lambda x: x["diff_ratio"], reverse=True) + + # Generate the sorted table + for comp in file_comparisons: + hash_display = ( + f"{comp['hash1']}:{comp['hash2']}" + if comp["status"] == "DIFFERENT" + else comp["status"] + ) + report.append( + f"| {comp['rel_path'].ljust(max_filepath_len)} | " + f"{hash_display.ljust(hash_col_width)} | " + f"{comp['size1_str'].ljust(size_col_width)} | " + f"{comp['size2_str'].ljust(size_col_width)} | " + f"{comp['diff_str'].ljust(diff_col_width)} |" + ) + + # Sort different files by difference ratio for the detailed section + different_files.sort(key=lambda x: x[1], reverse=True) + + # Add Files Missing From Each Folder sections + report.append("\n## Files Present in One Folder But Missing in the Other\n") + + if only_in_folder1: + report.append( + f"\n### Files Only in {folder1_name} ({len(only_in_folder1)} files)" + ) + for rel_path in sorted(only_in_folder1): + file1 = files1[rel_path] + size1 = file1.stat().st_size + lines1 = count_lines(file1) + hash1 = get_file_hash(file1)[:8] + report.append( + f"- `{rel_path}` ({format_size(size1)}, {lines1} lines, hash: {hash1})" + ) + + if only_in_folder2: + report.append( + f"\n### Files Only in {folder2_name} ({len(only_in_folder2)} files)" + ) + for rel_path in sorted(only_in_folder2): + file2 = files2[rel_path] + size2 = file2.stat().st_size + lines2 = count_lines(file2) + hash2 = get_file_hash(file2)[:8] + report.append( + f"- `{rel_path}` ({format_size(size2)}, {lines2} lines, hash: {hash2})" + ) + + # Add detailed section for different files + if different_files: + report.append(f"\n## Files with Differences (Sorted by Most Different First)\n") + for rel_path, diff_ratio in different_files: + file1 = files1[rel_path] + file2 = files2[rel_path] + size1 = file1.stat().st_size + size2 = file2.stat().st_size + lines1 = count_lines(file1) + lines2 = count_lines(file2) + hash1 = get_file_hash(file1)[:8] + hash2 = get_file_hash(file2)[:8] + size_diff = size2 - size1 + lines_diff = lines2 - lines1 + diff_pct = int(diff_ratio * 100) + + report.append(f"### `{rel_path}` ({diff_pct}% different)") + report.append( + f"- {folder1_name}: {format_size(size1)}, {lines1} lines, hash: {hash1}" + ) + report.append( + f"- {folder2_name}: {format_size(size2)}, {lines2} lines, hash: {hash2}" + ) + report.append( + f"- Difference: {size_diff:+d} bytes, {lines_diff:+d} lines\n" + ) + + # Add summary section + report.extend( + [ + "\n## Summary", + f"- **Total Unique Files**: {len(all_files)}", + f"- **Files in {folder1_name}**: {len(files1)}", + f"- **Files in {folder2_name}**: {len(files2)}", + f"- **Identical Files**: {len(identical_files)}", + f"- **Different Files**: {len(different_files)}", + f"- **Files Only in {folder1_name}**: {len(only_in_folder1)}", + f"- **Files Only in {folder2_name}**: {len(only_in_folder2)}", + f"\n## Path Information", + f"- {folder1_name}: `{folder1}`", + f"- {folder2_name}: `{folder2}`", + ] + ) + + # Write report to file + with open(output_file, "w") as f: + f.write("\n".join(report)) + + print(f"Recursive comparison report written to {output_file}") + return "\n".join(report) + + +# Execute the comparison +if __name__ == "__main__": + folder1 = "/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/" + folder2 = "/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/" + + report = compare_folders_recursive(folder1, folder2) diff --git a/frontend/python/folder_comparison_20250416_133319.md b/frontend/python/folder_comparison_20250416_133319.md new file mode 100644 index 0000000000..f9fcbe754b --- /dev/null +++ b/frontend/python/folder_comparison_20250416_133319.md @@ -0,0 +1,165 @@ +# Recursive Folder Comparison Report: Classes vs Classes + +Generated on: 2025-04-16 13:33:19 + +## File Comparison Table + +| File Path | Status | Classes | Classes | Difference | +|------------------------------------|----------|--------------------|--------------------|------------------------------| +| ApiHelpers.ts | MISSING | — | 637B (17 lines) | Only in Classes | +| ApiManager/SegmentationApi.ts | MISSING | 1.9KB (84 lines) | — | Only in Classes | +| ApiManager/StudioGen2Api.ts | MISSING | — | 1.6KB (61 lines) | Only in Classes | +| EnvironmentVariables.ts | MISSING | — | 263B (10 lines) | Only in Classes | +| GetFrontendEnvironment.ts | MISSING | — | 1.9KB (65 lines) | Only in Classes | +| ApiManager/MediaUploadApi.ts | 707d2bf2:06a93d8c | 10.3KB (349 lines) | 10.0KB (339 lines) | -255B (-10 lines) 72% different | +| ApiManager/EngineApi.ts | c4850e80:85e20a81 | 1.0KB (39 lines) | 5.5KB (213 lines) | +4.5KB (+174 lines) 69% different | +| ApiManager/JobsApi.ts | f7eb8e22:2aa03349 | 2.0KB (77 lines) | 1.9KB (74 lines) | -56B (-3 lines) 28% different | +| ApiManager/ApiManager.ts | 44db0b07:67a3a046 | 5.5KB (235 lines) | 5.3KB (211 lines) | -200B (-24 lines) 14% different | +| ApiManager/BillingApi.ts | 20051893:f5b7af84 | 1.0KB (33 lines) | 1.0KB (33 lines) | -8B (0 lines) 6% different | +| ApiManager/VoiceDesignerApi.ts | e4d874c3:3635ba42 | 2.0KB (77 lines) | 2.1KB (78 lines) | +94B (+1 lines) 5% different | +| ApiManager/MediaFilesApi.ts | be67aab2:44342e0f | 10.6KB (364 lines) | 10.6KB (369 lines) | +56B (+5 lines) 5% different | +| ApiManager/PromptsApi.ts | d73d329a:c4824d2e | 648B (25 lines) | 657B (25 lines) | +9B (0 lines) 4% different | +| ApiManager/MiscApi.ts | 298f5d93:187ad676 | 799B (26 lines) | 799B (26 lines) | 0B (0 lines) 3% different | +| ApiManager/TtsApi.ts | 5feb5cad:6228b278 | 1.7KB (64 lines) | 1.7KB (64 lines) | +2B (0 lines) 3% different | +| ApiManager/index.ts | 55a9094b:1fe59701 | 564B (18 lines) | 597B (19 lines) | +33B (+1 lines) 2% different | +| ApiManager/UserBookmarksApi.ts | e3e4cb19:fd3afc78 | 5.4KB (201 lines) | 5.5KB (202 lines) | +56B (+1 lines) 2% different | +| ApiManager/WeightsApi.ts | c12a98df:8a7dbb68 | 7.8KB (295 lines) | 7.9KB (296 lines) | +39B (+1 lines) 2% different | +| ApiManager/VideoApi.ts | a9947328:cb947c07 | 1.9KB (70 lines) | 1.9KB (70 lines) | -11B (0 lines) 1% different | +| ApiManager/BetaKeysApi.ts | 8d4ec6f1:33f1e4b3 | 4.3KB (167 lines) | 4.3KB (167 lines) | -3B (0 lines) 1% different | +| ApiManager/UsersApi.ts | 65121217:72db0eae | 3.9KB (161 lines) | 3.9KB (161 lines) | -6B (0 lines) 0% different | +| ApiManager/AnalyticsApi.ts | IDENTICAL | 944B (38 lines) | 944B (38 lines) | Identical content | +| ApiManager/CommentsApi.ts | IDENTICAL | 2.3KB (91 lines) | 2.3KB (91 lines) | Identical content | +| ApiManager/FeaturedItemsApi.ts | IDENTICAL | 2.3KB (96 lines) | 2.3KB (96 lines) | Identical content | +| ApiManager/ModerationApi.ts | IDENTICAL | 960B (42 lines) | 960B (42 lines) | Identical content | +| ApiManager/UserRatingsApi.ts | IDENTICAL | 2.4KB (92 lines) | 2.4KB (92 lines) | Identical content | +| ApiManager/VoiceConversionApi.ts | IDENTICAL | 1.4KB (53 lines) | 1.4KB (53 lines) | Identical content | + +## Files Present in One Folder But Missing in the Other + + +### Files Only in Classes (23 files) +- `ApiManager/SegmentationApi.ts` (1.9KB, 84 lines, hash: bb14fbf6) +- `ApiManager/enums/Api.ts` (254B, 7 lines, hash: ea867d98) +- `ApiManager/enums/Billing.ts` (242B, 11 lines, hash: e47c3eaa) +- `ApiManager/enums/Job.ts` (433B, 16 lines, hash: d148fd64) +- `ApiManager/enums/MediaFileClass.ts` (524B, 26 lines, hash: c90ce542) +- `ApiManager/enums/MediaFileType.ts` (1.7KB, 58 lines, hash: a2374fec) +- `ApiManager/enums/QueryFilters.ts` (579B, 29 lines, hash: 6d10a292) +- `ApiManager/enums/Tts.ts` (205B, 7 lines, hash: f6393d2e) +- `ApiManager/enums/UserFeatures.ts` (167B, 6 lines, hash: 05d49e51) +- `ApiManager/enums/Visibility.ts` (92B, 5 lines, hash: 297d031b) +- `ApiManager/enums/WeightCategory.ts` (180B, 8 lines, hash: f9be3d32) +- `ApiManager/enums/WeightType.ts` (208B, 11 lines, hash: 80436726) +- `ApiManager/models/BetaKeys.ts` (502B, 26 lines, hash: 7c718ac6) +- `ApiManager/models/Billing.tsx` (416B, 17 lines, hash: 8db6eb3b) +- `ApiManager/models/Dataset.ts` (588B, 23 lines, hash: 6af860ff) +- `ApiManager/models/Job.ts` (1.8KB, 72 lines, hash: 9d7b385a) +- `ApiManager/models/MediaFile.ts` (1.8KB, 59 lines, hash: 41778349) +- `ApiManager/models/Pagination.ts` (200B, 9 lines, hash: 837b24d3) +- `ApiManager/models/Prompts.ts` (424B, 16 lines, hash: 8ef1f366) +- `ApiManager/models/StatusAlert.ts` (175B, 9 lines, hash: fcee4684) +- `ApiManager/models/UserBookmark.ts` (1.6KB, 75 lines, hash: 8f70d1b1) +- `ApiManager/models/Users.ts` (1.3KB, 50 lines, hash: 5ed82bbd) +- `ApiManager/models/Weight.ts` (767B, 40 lines, hash: 3f9cccdb) + +### Files Only in Classes (4 files) +- `ApiHelpers.ts` (637B, 17 lines, hash: 9b638c6c) +- `ApiManager/StudioGen2Api.ts` (1.6KB, 61 lines, hash: b2707195) +- `EnvironmentVariables.ts` (263B, 10 lines, hash: 29c93666) +- `GetFrontendEnvironment.ts` (1.9KB, 65 lines, hash: 7ee8b738) + +## Files with Differences (Sorted by Most Different First) + +### `ApiManager/MediaUploadApi.ts` (72% different) +- Classes: 10.3KB, 349 lines, hash: 707d2bf2 +- Classes: 10.0KB, 339 lines, hash: 06a93d8c +- Difference: -255 bytes, -10 lines + +### `ApiManager/EngineApi.ts` (69% different) +- Classes: 1.0KB, 39 lines, hash: c4850e80 +- Classes: 5.5KB, 213 lines, hash: 85e20a81 +- Difference: +4469 bytes, +174 lines + +### `ApiManager/JobsApi.ts` (28% different) +- Classes: 2.0KB, 77 lines, hash: f7eb8e22 +- Classes: 1.9KB, 74 lines, hash: 2aa03349 +- Difference: -56 bytes, -3 lines + +### `ApiManager/ApiManager.ts` (14% different) +- Classes: 5.5KB, 235 lines, hash: 44db0b07 +- Classes: 5.3KB, 211 lines, hash: 67a3a046 +- Difference: -200 bytes, -24 lines + +### `ApiManager/BillingApi.ts` (6% different) +- Classes: 1.0KB, 33 lines, hash: 20051893 +- Classes: 1.0KB, 33 lines, hash: f5b7af84 +- Difference: -8 bytes, +0 lines + +### `ApiManager/VoiceDesignerApi.ts` (5% different) +- Classes: 2.0KB, 77 lines, hash: e4d874c3 +- Classes: 2.1KB, 78 lines, hash: 3635ba42 +- Difference: +94 bytes, +1 lines + +### `ApiManager/MediaFilesApi.ts` (5% different) +- Classes: 10.6KB, 364 lines, hash: be67aab2 +- Classes: 10.6KB, 369 lines, hash: 44342e0f +- Difference: +56 bytes, +5 lines + +### `ApiManager/PromptsApi.ts` (4% different) +- Classes: 648B, 25 lines, hash: d73d329a +- Classes: 657B, 25 lines, hash: c4824d2e +- Difference: +9 bytes, +0 lines + +### `ApiManager/MiscApi.ts` (3% different) +- Classes: 799B, 26 lines, hash: 298f5d93 +- Classes: 799B, 26 lines, hash: 187ad676 +- Difference: +0 bytes, +0 lines + +### `ApiManager/TtsApi.ts` (3% different) +- Classes: 1.7KB, 64 lines, hash: 5feb5cad +- Classes: 1.7KB, 64 lines, hash: 6228b278 +- Difference: +2 bytes, +0 lines + +### `ApiManager/index.ts` (2% different) +- Classes: 564B, 18 lines, hash: 55a9094b +- Classes: 597B, 19 lines, hash: 1fe59701 +- Difference: +33 bytes, +1 lines + +### `ApiManager/UserBookmarksApi.ts` (2% different) +- Classes: 5.4KB, 201 lines, hash: e3e4cb19 +- Classes: 5.5KB, 202 lines, hash: fd3afc78 +- Difference: +56 bytes, +1 lines + +### `ApiManager/WeightsApi.ts` (2% different) +- Classes: 7.8KB, 295 lines, hash: c12a98df +- Classes: 7.9KB, 296 lines, hash: 8a7dbb68 +- Difference: +39 bytes, +1 lines + +### `ApiManager/VideoApi.ts` (1% different) +- Classes: 1.9KB, 70 lines, hash: a9947328 +- Classes: 1.9KB, 70 lines, hash: cb947c07 +- Difference: -11 bytes, +0 lines + +### `ApiManager/BetaKeysApi.ts` (1% different) +- Classes: 4.3KB, 167 lines, hash: 8d4ec6f1 +- Classes: 4.3KB, 167 lines, hash: 33f1e4b3 +- Difference: -3 bytes, +0 lines + +### `ApiManager/UsersApi.ts` (0% different) +- Classes: 3.9KB, 161 lines, hash: 65121217 +- Classes: 3.9KB, 161 lines, hash: 72db0eae +- Difference: -6 bytes, +0 lines + + +## Summary +- **Total Unique Files**: 49 +- **Files in Classes**: 45 +- **Files in Classes**: 26 +- **Identical Files**: 6 +- **Different Files**: 16 +- **Files Only in Classes**: 23 +- **Files Only in Classes**: 4 + +## Path Information +- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/` +- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/` \ No newline at end of file diff --git a/frontend/storyteller_backend_api/apis/AnalyticsApi.ts b/frontend/storyteller_backend_api/apis/AnalyticsApi.ts new file mode 100644 index 0000000000..b7868b88f3 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/AnalyticsApi.ts @@ -0,0 +1,38 @@ +import { ApiManager, ApiResponse } from "./ApiManager"; + +export class AnalyticsApi extends ApiManager { + public PostAnalytics({ + maybeLastAction, + maybeLogToken, + }: { + maybeLastAction: string; + maybeLogToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/analytics/log_session`; + + const body = { + maybe_last_action: maybeLastAction, + maybe_log_token: maybeLogToken, + }; + + return this.post< + { + maybe_last_action: string; + maybe_log_token: string; + }, + { + success?: boolean; + log_token?: string; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + data: response.log_token, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/ApiHelpers.ts b/frontend/storyteller_backend_api/apis/ApiHelpers.ts new file mode 100644 index 0000000000..b09045ffe0 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/ApiHelpers.ts @@ -0,0 +1,17 @@ +// import { GetCdnOrigin } from "~/api/GetCdnOrigin"; +// import environmentVariables from "./EnvironmentVariables"; + +// export async function get_media_url(mediaId: string) { +// //This is for prod when we have the proper info on the url. +// const api_base_url = environmentVariables.values.BASE_API; +// const url = `${api_base_url}/v1/media_files/file/${mediaId}`; + +// const response = await fetch(url); +// const json = await JSON.parse(await response.text()); +// const bucketPath = json["media_file"]["public_bucket_path"]; + +// const media_api_base_url = GetCdnOrigin(); + +// const media_url = `${media_api_base_url}${bucketPath}`; +// return media_url; +// } diff --git a/frontend/storyteller_backend_api/apis/ApiManager.ts b/frontend/storyteller_backend_api/apis/ApiManager.ts new file mode 100644 index 0000000000..3b2f566fa4 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/ApiManager.ts @@ -0,0 +1,235 @@ +// import { bool } from "@techstark/opencv-js"; +import { API_TARGETS } from "../enums/Api"; + +type NonNullableObject = NonNullable; + +export interface ApiResponse { + success: boolean; + errorMessage?: string; + data?: T; + pagination?: P; +} + +export class ApiManager { + ApiTargets: Record = {}; + + constructor() { + this.ApiTargets = { + BaseApi: API_TARGETS.BASE_API, + GoggleApi: API_TARGETS.GOOGLE_API, + FunnelApi: API_TARGETS.FUNNEL_API, + CdnApi: API_TARGETS.CDN_API, + GravatarApi: API_TARGETS.GRAVATAR_API, + }; + } + + public async fetch( + endpoint: string, + { + method, + query, + body, + }: { + method: string; + query?: Record; + body?: B; + }, + ): Promise { + const queryInString = + query && + Object.entries(query).reduce( + (allOptions, [key, value]) => { + if (!value) { + return allOptions; + } + allOptions[key] = value.toString(); + return allOptions; + }, + {} as Record, + ); + + const endpointWithQueries = queryInString + ? endpoint + "?" + new URLSearchParams(queryInString) + : endpoint; + + const bodyInString = JSON.stringify(body); + + const response = await fetch(endpointWithQueries, { + method, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + credentials: "include", + body: bodyInString, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + } + + public async fetchMultipartFormData( + endpoint: string, + { + method, + body, + }: { + method: string; + body: FormData; + }, + ): Promise { + const response = await fetch(endpoint, { + method, + headers: { + Accept: "application/json", + }, + credentials: "include", + body: body, + }); + return response.json(); + } + + protected get({ + endpoint, + query, + }: { + endpoint: string; + query?: Record; + }): Promise { + return this.fetch(endpoint, { method: "GET", query }); + } + + protected post({ + endpoint, + query, + body, + }: { + endpoint: string; + query?: Record; + body?: B; + }): Promise { + return this.fetch(endpoint, { + method: "POST", + query, + body, + }); + } + + protected delete({ + endpoint, + query, + body, + }: { + endpoint: string; + query?: Record; + body?: B; + }): Promise { + return this.fetch(endpoint, { + method: "DELETE", + query, + body, + }); + } + + protected async postFormVideo({ + endpoint, + formRecord, + uuid, + blob, + blobFileName, + }: { + endpoint: string; + formRecord: Record; + uuid: string; + blob?: Blob | File; + blobFileName?: string; + }): Promise { + const formData = new FormData(); + formData.append("uuid_idempotency_token", uuid); + Object.entries(formRecord).forEach(([key, value]) => { + formData.append(key, value); + }); + + if (blob && blobFileName) { + formData.append("video", blob, blobFileName); + } else if (blob) { + formData.append("video", blob); + } + + return this.fetchMultipartFormData(endpoint, { + method: "POST", + body: formData, + }); + } + + protected async postForm({ + endpoint, + formRecord, + uuid, + blob, + blobFileName, + }: { + endpoint: string; + formRecord: Record; + uuid: string; + blob?: Blob | File; + blobFileName?: string; + }): Promise { + const formData = new FormData(); + formData.append("uuid_idempotency_token", uuid); + Object.entries(formRecord).forEach(([key, value]) => { + formData.append(key, value); + }); + + if (blob && blobFileName) { + formData.append("file", blob, blobFileName); + } else if (blob) { + formData.append("file", blob); + } + + return this.fetchMultipartFormData(endpoint, { + method: "POST", + body: formData, + }); + } + + protected camelToSnakeCase(str: string) { + return str.replace(/([a-z0])([A-Z])/g, "$1_$2").toLowerCase(); + } + + protected parseQueryValues( + params: Record, + ): Record { + return Object.entries(params).reduce( + (allParams, [key, value]) => { + if (!value) { + return allParams; + } + const snakeKey = this.camelToSnakeCase(key); + if (Array.isArray(value)) { + return { ...allParams, [snakeKey]: value.join(",") }; + } + return { ...allParams, [snakeKey]: value.toString() }; + }, + {} as Record, + ); + } + + protected parseBodyValues( + params: NonNullableObject, + ): B { + return Object.entries(params).reduce((allParams, [key, value]) => { + if (!value) { + return allParams; + } + const snakeKey = this.camelToSnakeCase(key); + if (Array.isArray(value)) { + return { ...allParams, [snakeKey]: value }; + } + return { ...allParams, [snakeKey]: value }; + }, {} as B); + } +} diff --git a/frontend/storyteller_backend_api/apis/BetaKeysApi.ts b/frontend/storyteller_backend_api/apis/BetaKeysApi.ts new file mode 100644 index 0000000000..e91fe83f1a --- /dev/null +++ b/frontend/storyteller_backend_api/apis/BetaKeysApi.ts @@ -0,0 +1,167 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { BetaKey } from "../models/BetaKeys"; +import { Pagination } from "../models/Pagination"; + +export class BetaKeysApi extends ApiManager { + public CreateBetaKey({ + maybeNote, + maybeReferrerUsername, + numberOfKeys, + uuidIdempotencyToken, + }: { + maybeNote?: string; + maybeReferrerUsername?: string; + numberOfKeys: number; + uuidIdempotencyToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/create`; + + const body = { + maybe_note: maybeNote, + maybe_referrer_username: maybeReferrerUsername, + number_of_keys: numberOfKeys, + uuid_idempotency_token: uuidIdempotencyToken, + }; + + return this.post< + { + maybe_note?: string; + maybe_referrer_username?: string; + number_of_keys: number; + uuid_idempotency_token: string; + }, + { + success?: boolean; + beta_keys?: string[]; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + data: response.beta_keys, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListBetaKeys({ + sortAscending, + pageSize, + pageIndex, + maybeReferrerUsername, + onlyListRemaining, + }: { + sortAscending?: boolean; + pageSize?: number; + pageIndex?: number; + maybeReferrerUsername?: string; + onlyListRemaining?: boolean; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/list`; + + const query = { + sort_ascending: sortAscending, + page_size: pageSize, + page_index: pageIndex, + maybe_referrer_username: maybeReferrerUsername, + only_list_remaining: onlyListRemaining, + }; + + return this.get<{ + success?: boolean; + beta_keys?: BetaKey[]; + pagination?: Pagination; + BadInput?: string; + }>({ endpoint, query }) + .then((response) => ({ + success: response.success ?? false, + data: response.beta_keys, + pagination: response.pagination, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public RedeemBetaKey({ + betaKey, + }: { + betaKey: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/redeem`; + + return this.post< + { + beta_key: string; + }, + { + success?: boolean; + BadInput?: string; + } + >({ endpoint, body: { beta_key: betaKey } }) + .then((response) => ({ + success: response.success ?? false, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public UpdateBetaKeyIsDistributed({ + betaKey, + isDistributed, + }: { + betaKey: string; + isDistributed: boolean; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${betaKey}/distributed`; + + return this.post< + { + is_distributed: boolean; + }, + { + success?: boolean; + BadInput?: string; + } + >({ endpoint, body: { is_distributed: isDistributed } }) + .then((response) => ({ + success: response.success ?? false, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public UpdateBetaKeyNote({ + token, + note, + }: { + token: string; + note?: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${token}/note`; + + return this.post< + { + note?: string; + }, + { + success?: boolean; + BadInput?: string; + } + >({ endpoint, body: { note } }) + .then((response) => ({ + success: response.success ?? false, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/BillingApi.ts b/frontend/storyteller_backend_api/apis/BillingApi.ts new file mode 100644 index 0000000000..304cedf5ba --- /dev/null +++ b/frontend/storyteller_backend_api/apis/BillingApi.ts @@ -0,0 +1,33 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { Subscription } from "../models/Billing"; +import { LoyaltyProgram } from "../enums/Billing"; + +export class BillingApi extends ApiManager { + public async ListActiveSubscriptions(): Promise< + ApiResponse<{ + active_subscriptions: Subscription[]; + maybe_loyalty_program?: LoyaltyProgram; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/billing/active_subscriptions`; + return await this.get<{ + success: boolean; + active_subscriptions?: Subscription[]; + maybe_loyalty_program?: LoyaltyProgram; + error_message?: string; + }>({ endpoint: endpoint }) + .then((response) => ({ + success: response.success, + data: { + active_subscriptions: response.active_subscriptions || [], + maybe_loyalty_program: response.maybe_loyalty_program, + }, + })) + .catch((err) => { + return { + success: false, + errorMessage: err.message, + }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/CommentsApi.ts b/frontend/storyteller_backend_api/apis/CommentsApi.ts new file mode 100644 index 0000000000..dd010312eb --- /dev/null +++ b/frontend/storyteller_backend_api/apis/CommentsApi.ts @@ -0,0 +1,91 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; + +export class CommentsApi extends ApiManager { + public CreateComment({ + commentMarkdown, + entityToken, + entityType, + uuidIdempotencyToken, + }: { + commentMarkdown: string; + entityToken: string; + entityType: string; + uuidIdempotencyToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/new`; + + const body = { + comment_markdown: commentMarkdown, + entity_token: entityToken, + entity_type: entityType, + uuid_idempotency_token: uuidIdempotencyToken, + }; + + return this.post< + { + comment_markdown: string; + entity_token: string; + entity_type: string; + uuid_idempotency_token: string; + }, + { + success?: boolean; + comment_token?: string; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + data: response.comment_token, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public DeleteComment({ + commentToken, + }: { + commentToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/delete/${commentToken}`; + + return this.post< + { as_mod: boolean }, + { + success?: boolean; + BadInput?: string; + } + >({ endpoint, body: { as_mod: true } }) + .then((response) => ({ + success: response.success ?? false, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListCommentsByEntity({ + entityType, + entityToken, + }: { + entityType: string; + entityToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/list/${entityType}/${entityToken}`; + + return this.get<{ + success: boolean; + comments: string[]; + }>({ endpoint }) + .then((response) => ({ + success: true, + data: response.comments, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/EngineApi.ts b/frontend/storyteller_backend_api/apis/EngineApi.ts new file mode 100644 index 0000000000..92c4bc8343 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/EngineApi.ts @@ -0,0 +1,213 @@ +import { ApiManager, ApiResponse } from "./ApiManager"; + +export class EngineApi extends ApiManager { + public async ConvertTbxToGltf({ + mediaFileToken, + uuidIdempotencyToken, + }: { + mediaFileToken: string; + uuidIdempotencyToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/conversion/enqueue_fbx_to_gltf`; + + const body = { + media_file_token: mediaFileToken, + uuid_idempotency_token: uuidIdempotencyToken, + }; + + return this.post< + { media_file_token: string; uuid_idempotency_token: string }, + { success?: boolean; inference_job_token?: string; BadInput?: string } + >({ + endpoint, + body: body, + }) + .then((response) => { + return { + success: response.success ?? false, + data: response.inference_job_token, + errorMessage: response.BadInput, + }; + }) + .catch((err) => { + return { + success: false, + errorMessage: err.message, + }; + }); + } + + public async uploadSceneSnapshot({ + screenshot, + sceneMediaToken, + }: { + screenshot: File; // base64 encoded PNG + sceneMediaToken?: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/scene_snapshot`; + const formData = new FormData(); + formData.append("snapshot", screenshot); // Changed from "screenshot" to "snapshot" to match API spec + if (sceneMediaToken) { + formData.append("scene_media_token", sceneMediaToken); + } + + // for now ... + const uuidIdempotencyToken = crypto.randomUUID(); // Generate a new UUID + formData.append("uuid_idempotency_token", uuidIdempotencyToken); // Added uuid_idempotency_token + + const response = await fetch(endpoint, { + method: "POST", + headers: { + Accept: "application/json", + }, + credentials: "include", + body: formData, + }); + + const postResponse = await response.json(); + + console.log(postResponse); + + let result: { success: boolean; data?: string; errorMessage?: string }; + + if (postResponse.success) { + result = { + success: true, + data: postResponse.snapshot_media_token, + errorMessage: undefined, + }; + } else { + result = { + success: false, + errorMessage: postResponse.BadInput, + }; + } + + return result; + } + + public async enqueueImageGeneration({ + disableSystemPrompt, + prompt, + snapshotMediaToken, + additionalImages, + }: { + disableSystemPrompt: boolean; + prompt: string; + snapshotMediaToken: string; + additionalImages?: string[]; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/prompt`; + + const uuidIdempotencyToken = crypto.randomUUID(); // Generate a new UUID + const body = { + uuid_idempotency_token: uuidIdempotencyToken, + disable_system_prompt: disableSystemPrompt, + prompt, + snapshot_media_token: snapshotMediaToken, // Changed from scene_media_token to snapshot_media_token + additional_images: additionalImages, + }; + + const postResponse = await this.post< + { + uuid_idempotency_token: string; + disable_system_prompt: boolean; + prompt: string; + snapshot_media_token: string; + additional_images?: string[]; + }, + { success?: boolean; job_token?: string; BadInput?: string } + >({ + endpoint, + body, + }); + + // Check if the response is successful + const isSuccess = postResponse.success ?? false; + + // Prepare the result object + const result = { + success: isSuccess, + data: isSuccess ? postResponse.job_token : undefined, + errorMessage: isSuccess ? undefined : postResponse.BadInput, + }; + + return result; + } + + public async pollJobSession(jobToken: string): Promise< + ApiResponse<{ + status: string; + result: { + generated_images?: string[]; + error?: string; + }; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session/${jobToken}`; + + const response = await this.get<{ + success?: boolean; + status?: string; + result?: { + generated_images?: string[]; + error?: string; + }; + BadInput?: string; + }>({ + endpoint, + }); + + const success = response.success ?? false; + const status = response.status ?? ""; + const result = response.result ?? { + generated_images: [], + error: undefined, + }; + const errorMessage = response.BadInput; + + return { + success, + data: { status, result }, + errorMessage, + }; + } + + public async pollStudioSessionJobs(jobToken: string): Promise< + ApiResponse<{ + status: string; + result: { + generated_images?: string[]; + error?: string; + }; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/session_jobs/${jobToken}`; + + const response = await this.get<{ + success?: boolean; + status?: string; + result?: { + generated_images?: string[]; + error?: string; + }; + BadInput?: string; + }>({ + endpoint, + }); + + const success = response.success ?? false; + const status = response.status ?? ""; + const result = response.result ?? { + generated_images: [], + error: undefined, + }; + const errorMessage = response.BadInput; + + return { + success, + data: { status, result }, + errorMessage, + }; + } +} diff --git a/frontend/storyteller_backend_api/apis/FeaturedItemsApi.ts b/frontend/storyteller_backend_api/apis/FeaturedItemsApi.ts new file mode 100644 index 0000000000..1af1f28310 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/FeaturedItemsApi.ts @@ -0,0 +1,96 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; + +export class FeaturedItemsApi extends ApiManager { + public CreateFeaturedItem({ + entityToken, + entityType, + }: { + entityToken: string; + entityType: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/create`; + + const body = { + entity_token: entityToken, + entity_type: entityType, + }; + + return this.post< + { + entity_token: string; + entity_type: string; + }, + { + success?: boolean; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public DeleteFeaturedItem({ + entityToken, + entityType, + }: { + entityToken: string; + entityType: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/delete`; + + const body = { + entity_token: entityToken, + entity_type: entityType, + }; + + return this.delete< + { + entity_token: string; + entity_type: string; + }, + { + success?: boolean; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public CheckFeaturedItem({ + entityType, + entityToken, + }: { + entityType: string; + entityToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/is_featured/${entityType}/${entityToken}`; + + return this.post< + undefined, + { + success?: boolean; + is_featured?: boolean; + BadInput?: string; + } + >({ endpoint }) + .then((response) => ({ + success: response.success ?? false, + data: response.is_featured, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/JobsApi.ts b/frontend/storyteller_backend_api/apis/JobsApi.ts new file mode 100644 index 0000000000..885140a1dc --- /dev/null +++ b/frontend/storyteller_backend_api/apis/JobsApi.ts @@ -0,0 +1,93 @@ +// libs/api-client/src/JobsApi.ts +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { Job, JobPreview } from "../models/Job"; +import { JobState } from "../models/JobState"; + +export class JobsApi extends ApiManager { + public GetPreviewStatusByJobToken({ + token, + }: { + token: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/workflows/preview_status/${token}`; + + return this.get>({ endpoint }) + .then((response) => ({ + success: response.success, + data: response.data, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public GetJobByToken({ + token, + }: { + token: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${token}`; + + return this.get<{ + success: boolean; + state: JobState; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: response.state, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListJobs(): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/batch`; + + return this.get<{ + success: boolean; + job_states: JobState[]; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: response.job_states, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListRecentJobs(): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session`; + + return this.get<{ + success: boolean; + jobs: Job[]; + error_reason?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: response.jobs, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public DeleteJobByToken(jobToken: string): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${jobToken}`; + + return this.delete< + undefined, + { + success: boolean; + } + >({ endpoint }) + .then((response) => ({ + success: response.success, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} \ No newline at end of file diff --git a/frontend/storyteller_backend_api/apis/MediaUploadApi.ts b/frontend/storyteller_backend_api/apis/MediaUploadApi.ts new file mode 100644 index 0000000000..6dfaf5f2df --- /dev/null +++ b/frontend/storyteller_backend_api/apis/MediaUploadApi.ts @@ -0,0 +1,346 @@ + +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { FilterEngineCategories } from "../enums/QueryFilters"; +import { Visibility } from "../enums/Visibility"; +export class MediaUploadApi extends ApiManager { + private sessionToken: string = ""; + + private async getSessionTokenForUploadStudioShot() { + const endpoint = `${this.ApiTargets.BaseApi}/v1/session_token`; + return await this.get<{ + maybe_signed_session: string; + }>({ endpoint }) + .then((response) => { + return { + success: true, + data: response["maybe_signed_session"], + }; + }) + .catch((err) => { + return { + success: false, + errorMessage: err.message, + }; + }); + } + + public async UploadStudioShot({ + maybe_title, + uuid_idempotency_token, + blob, + fileName, + maybe_visibility, + }: { + maybe_title?: string; + uuid_idempotency_token: string; + blob: Blob | null; + fileName: string; + maybe_visibility?: Visibility; + }): Promise> { + // This function is a bit special it requires that + const endpoint = `${this.ApiTargets.uploadApi}/v1/media_files/upload/studio_shot`; + + if (!this.sessionToken) { + console.log("Session Token"); + const response = await this.getSessionTokenForUploadStudioShot(); + if (response.success && response.data) { + this.sessionToken = response.data; + console.log(`Token ${this.sessionToken}`); + } else { + return { + success: false, + errorMessage: "Could not obtain session tokens.", + }; + } + } else { + console.log(`Session Token: ${this.sessionToken}`); + } + + const options: Record = { + maybe_title, + maybe_frame_rate_fps: 60, + maybe_visibility: maybe_visibility?.toString(), + }; + + const formRecord = Object.entries(options).reduce( + (allOptions, [key, value]) => { + if (value === undefined) { + return allOptions; + } + return { ...allOptions, [key]: value.toString() }; + }, + {} as Record, + ); + + const formData = new FormData(); + formData.append("uuid_idempotency_token", uuid_idempotency_token); + Object.entries(formRecord).forEach(([key, value]) => { + formData.append(key, value); + }); + if (blob && fileName) { + formData.append("file", blob, fileName); + } else if (blob) { + formData.append("file", blob); + } + + const response = await fetch(endpoint, { + method: "POST", + headers: { + Accept: "application/json", + Session: this.sessionToken, + }, + credentials: "include", + body: formData, + }); + + const result = await response.json(); + + if (result.success && result.media_file_token) { + return { success: true, data: result.media_file_token }; + } else { + return { success: false, errorMessage: "Failed to Get Media Token" }; + } + } + + private async Upload({ + endpoint, + uuid, + blob, + fileName, + options, + }: { + endpoint: string; + blob: Blob | File; + fileName: string; + uuid: string; + options: Record; + }): Promise> { + const formRecord = Object.entries(options).reduce( + (allOptions, [key, value]) => { + if (value === undefined) { + return allOptions; + } + return { ...allOptions, [key]: value.toString() }; + }, + {} as Record, + ); + + return await this.postForm<{ + success: boolean; + media_file_token?: string; + BadInput?: string; + }>({ endpoint, formRecord, blob, blobFileName: fileName, uuid }) + .then((response) => ({ + success: Boolean(response.success ?? false), + data: response.media_file_token, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { + success: false, + errorMessage: err.message, + }; + }); + } + + public async UploadAudio({ + blob, + fileName, + uuid, + maybe_title, + maybe_visibility = Visibility.Public, + }: { + blob: Blob; + fileName: string; + uuid: string; + maybe_title?: string | undefined; + maybe_visibility?: Visibility | undefined; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/audio`; + const options: Record = { + maybe_title, + maybe_visibility: maybe_visibility?.toString(), + }; + return this.Upload({ endpoint, blob, fileName, uuid, options }); + } + + public async UploadImage({ + blob, + fileName, + uuid, + maybe_title, + maybe_visibility = Visibility.Public, + is_intermediate_system_file = EIntermediateFile.true + }: { + blob: Blob; + fileName: string; + uuid: string; + maybe_title?: string | undefined; + maybe_visibility?: Visibility | undefined; + is_intermediate_system_file?: EIntermediateFile; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/image`; + const options: Record = { + is_intermediate_system_file: EIntermediateFile[is_intermediate_system_file], + maybe_title, + maybe_visibility: maybe_visibility?.toString(), + }; + return this.Upload({ endpoint, blob, fileName, uuid, options }); + } + + public async UploadNewEngineAsset({ + file, + fileName, + uuid, + engine_category, + maybe_animation_type, + maybe_duration_millis, + maybe_title, + maybe_visibility = Visibility.Public, + }: { + file: File; + fileName: string; + uuid: string; + engine_category: FilterEngineCategories; + maybe_animation_type?: string; + maybe_duration_millis?: number; + maybe_title?: string; + maybe_visibility?: Visibility; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_engine_asset`; + const options: Record = { + engine_category, + maybe_title, + maybe_visibility: maybe_visibility?.toString(), + maybe_animation_type, + maybe_duration_millis, + }; + return this.Upload({ endpoint, blob: file, fileName, uuid, options }); + } + + public async UploadNewScene({ + blob, + fileName, + uuid, + maybe_title, + maybe_visibility = Visibility.Public, + }: { + blob: Blob; + fileName: string; + uuid: string; + maybe_title?: string; + maybe_visibility?: Visibility; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_scene`; + const options: Record = { + maybe_title, + maybe_visibility: maybe_visibility?.toString(), + }; + return this.Upload({ endpoint, blob, fileName, uuid, options }); + } + + public async UploadNewVideo({ + blob, + fileName, + uuid, + maybe_title, + maybe_visibility = Visibility.Public, + maybe_style_name, + maybe_scene_source_media_file_token, + }: { + blob: Blob; + fileName: string; + uuid: string; + maybe_title?: string; + maybe_visibility?: Visibility; + maybe_style_name?: string; + maybe_scene_source_media_file_token?: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_video`; + const options: Record = { + is_intermediate_system_file: "true", + maybe_title, + maybe_visibility: maybe_visibility?.toString(), + maybe_style_name, + maybe_scene_source_media_file_token, + }; + return this.Upload({ endpoint, blob, fileName, uuid, options }); + } + + public async UploadPmx({ + file, + fileName, + uuid, + engine_category, + maybe_animation_type, + maybe_duration_millis, + maybe_title, + maybe_visibility = Visibility.Public, + }: { + file: File; + fileName: string; + uuid: string; + engine_category?: string; + maybe_animation_type?: string; + maybe_duration_millis?: number; + maybe_title?: string; + maybe_visibility?: Visibility; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/pmx`; + const options: Record = { + is_intermediate_system_file: "true", + engine_category, + maybe_animation_type, + maybe_duration_millis, + maybe_title, + maybe_visibility, + }; + return this.Upload({ endpoint, blob: file, fileName, uuid, options }); + } + + public async UploadSavedScene({ + blob, + fileName, + uuid, + mediaToken, + }: { + blob: Blob; + fileName: string; + uuid: string; + mediaToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/saved_scene/${mediaToken}`; + const options: Record = {}; + return this.Upload({ endpoint, blob, fileName, uuid, options }); + } + + // this is used to send the scene snapshot to the vst upload endpoint this creates a snapshot of the scene where in we can remix from the og. + public async UploadSceneSnapshotMediaFileForm({ + blob, + uuid, + maybe_title, // title of the scene at the time + maybe_scene_source_media_file_token, // original token that started it all + }: { + blob: Blob; + uuid: string; + maybe_title: string; + maybe_scene_source_media_file_token: string | undefined; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/scene_snapshot`; + + const options: Record = { + maybe_title: maybe_title, + maybe_scene_source_media_file_token: maybe_scene_source_media_file_token, + }; + + const fileName = `${maybe_title}-${uuid}`; + return this.Upload({ endpoint, blob, fileName, uuid, options }); // token comes back and send it VST + } + + + + + +} + + diff --git a/frontend/storyteller_backend_api/apis/MiscApi.ts b/frontend/storyteller_backend_api/apis/MiscApi.ts new file mode 100644 index 0000000000..e72a46362e --- /dev/null +++ b/frontend/storyteller_backend_api/apis/MiscApi.ts @@ -0,0 +1,26 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { StatusAlert } from "../models/StatusAlert"; + +export class MiscApi extends ApiManager { + public GetStatusAlertCheck(): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/session`; + return this.get<{ + success: boolean; + maybe_alert?: Omit; + refresh_interval_millis: number; + }>({ endpoint: endpoint }) + .then((response) => ({ + success: response.success, + data: { + ...response.maybe_alert, + refresh_interval_millis: response.refresh_interval_millis, + }, + })) + .catch((err) => { + return { + success: false, + errorMessage: err.mesasge, + }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/ModerationApi.ts b/frontend/storyteller_backend_api/apis/ModerationApi.ts new file mode 100644 index 0000000000..8074ce11dd --- /dev/null +++ b/frontend/storyteller_backend_api/apis/ModerationApi.ts @@ -0,0 +1,42 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; + +export class ModerationApi extends ApiManager { + public async UpdateFeatureFlagsByToken({ + usernameOrToken, + flags, + }: { + usernameOrToken: string; + flags: string[]; + }): Promise< + ApiResponse<{ + inference_job_token?: string; + inference_job_token_type?: string; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/moderation/user_feature_flags/${usernameOrToken}`; + + const body = { + action: { + AddFlags: { + flags, + }, + }, + }; + + return this.post< + { action: { AddFlags: { flags: string[] } } }, + { success?: boolean; BadInput?: string } + >({ + endpoint, + body: body, + }) + .then((response) => ({ + success: response.success ?? false, + errorMessage: response.BadInput, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } +} diff --git a/frontend/storyteller_backend_api/apis/PromptsApi.ts b/frontend/storyteller_backend_api/apis/PromptsApi.ts new file mode 100644 index 0000000000..7719156083 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/PromptsApi.ts @@ -0,0 +1,25 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { Prompts } from "../models/Prompts"; + +export class PromptsApi extends ApiManager { + public GetPromptsByToken({ + token, + }: { + token: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/prompts/${token}`; + + return this.get<{ + success: boolean; + prompt: Prompts; + error_reason?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: response.prompt, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/SegmentationApi.ts b/frontend/storyteller_backend_api/apis/SegmentationApi.ts new file mode 100644 index 0000000000..dfafc18771 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/SegmentationApi.ts @@ -0,0 +1,84 @@ +import { ApiManager } from "../apis/ApiManager"; +import { v4 as uuidv4 } from "uuid"; + +export interface Coordinates { + coordinates: [number, number]; + include: boolean; +} + +export interface ObjectData { + style: { + color: SegmentationColor; + }; + object_id: number; + points: Coordinates[]; +} + +export type SegmentationColor = [number, number, number]; + +export interface RequestFrame { + timestamp: number; + objects: ObjectData[]; +} +export interface ResponseFrame { + b64_image_data: number; + idx: number; + timestamp: number; + objects: ObjectData[]; + preview_image_url: string; +} + +export interface SegmentationRequest { + session_id: string; + fps: number; + frames: RequestFrame[]; + propagate: boolean; +} + +export interface SegmentationResponse { + session_id: string; + fps: number; + masked_video_cdn_url: string; + frames: ResponseFrame[]; + propagate: boolean; +} + +export class SegmentationApi extends ApiManager { + // returns uuid + public async createSession( + blobVideo: File | Blob, + ): Promise<{ session_id: string }> { + const endpoint = `https://hax.storyteller.ai/segmentation/new_session`; + const id = uuidv4().toString(); + return this.postFormVideo<{ session_id: "fix-mimetype" }>({ + endpoint: endpoint, + formRecord: { + session_id: "fix-mimetype", + }, + uuid: id, + blobFileName: id, + blob: blobVideo, + }); + } + + public addPointsToSession( + session_id: string, + fps: number, + frames: RequestFrame[], + propagate: boolean, + ): Promise { + const endpoint = `https://hax.storyteller.ai/segmentation/generate_masks`; + + const segmentationRequest: SegmentationRequest = { + session_id: session_id, + fps: fps, + frames: frames, + propagate: propagate, + }; + + return this.post({ + endpoint: endpoint, + body: segmentationRequest, + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/TtsApi.ts b/frontend/storyteller_backend_api/apis/TtsApi.ts new file mode 100644 index 0000000000..704bba79b1 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/TtsApi.ts @@ -0,0 +1,64 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { GenerateTtsAudioErrorType } from "../enums/Tts"; +import { Visibility } from "../enums/Visibility"; + +export interface GenerateTtsAudioRequest { + creator_set_visibility?: Visibility; + uuid_idempotency_token: string; + tts_model_token: string; + inference_text: string; + is_storyteller_demo?: boolean; +} + +export interface GenerateTtsAudioResponse { + success?: boolean; + inference_job_token?: string; + inference_job_token_type?: string; + BadInput?: string; +} + +export class TtsApi extends ApiManager { + public async GenerateTtsAudio(request: GenerateTtsAudioRequest): Promise< + ApiResponse<{ + inference_job_token?: string; + inference_job_token_type?: string; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/tts/inference`; + + const body = { + ...request, + ...(request.creator_set_visibility + ? {} + : { creator_set_visibility: Visibility.Public }), + }; + + return this.post({ + endpoint, + body: body, + }) + .then((response) => { + if (!response.success) { + return { + success: false, + errorMessage: response.BadInput, + data: {}, + }; + } + return { + success: true, + data: { + inference_job_token: response.inference_job_token, + inference_job_token_type: response.inference_job_token_type, + }, + }; + }) + .catch(() => { + return { + success: false, + errorMessage: GenerateTtsAudioErrorType.UnknownError, + data: {}, + }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/UserBookmarksApi.ts b/frontend/storyteller_backend_api/apis/UserBookmarksApi.ts new file mode 100644 index 0000000000..7d2faded35 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/UserBookmarksApi.ts @@ -0,0 +1,201 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { + UserBookmarkBatch, + UserBookmarkByEntity, + UserBookmarkByUser, +} from "../models/UserBookmark"; +import { Pagination } from "../models/Pagination"; + +export enum ScopedEntityTypes { + USER = "user", + TTS_MODEL = "tts-model", + TTS_RESULT = "tts-result", + W2L_TEMPLATE = "w2l-template", + W2L_RESULT = "w2l-result", + MEDIA_FILE = "media_file", + MODEL_WEIGHT = "model_weight", + VOICE_CONVERSION_MODEL = "voice_conversion_model", + ZS_VOICE = "z_voice", +} + +export enum ScopedWeightTypes { + HIFIGAN_TTl2 = "hifigan_tt2", + RVC_V2 = "rvc_v2", + SD_1_5 = "sd_1.5", + SDXL = "sdxl", + SO_VITS_SVC = "so_vits_svc", + TT2 = "tt2", + LORA = "loRA", + VALL_E = "vall_e", + COMFY_UI = "comfy_ui", +} + +export enum ScopedMediaFileType { + IMAGE_GENERATION = "image_generation", + TEXT_TO_SPEECH = "text_to_speech", + VOCODER = "vocoder", + VOICE_CONVERSION = "voice_conversion", + WORKFLOW_CONFIG = "workflow_config", +} + +interface ListUserBookmarksByUserRequest { + username: string; + sort_ascending?: boolean; + page_size?: number; + page_index?: number; + maybe_scoped_entity_type?: ScopedEntityTypes[]; + maybe_scoped_weight_type?: ScopedWeightTypes[]; + maybe_scoped_media_file_type?: ScopedMediaFileType[]; +} + +export class UserBookmarksApi extends ApiManager { + public CreateUserBookmark({ + entityToken, + entityType, + }: { + entityToken: string; + entityType: string; + }): Promise< + ApiResponse<{ + new_bookmark_count_for_entity?: number; + user_bookmark_token?: string; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/create`; + const body = { + entity_token: entityToken, + entity_type: entityType, + }; + + return this.post< + { + entity_token: string; + entity_type: string; + }, + { + success?: boolean; + new_bookmark_count_for_entity?: number; + user_bookmark_token: string; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + data: { + new_bookmark_count_for_entity: response.new_bookmark_count_for_entity, + user_bookmark_token: response.user_bookmark_token, + }, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public DeleteUserBookmark({ + entityToken, + }: { + entityToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/delete/${entityToken}`; + + return this.delete< + { as_mod: boolean }, + { + success?: boolean; + BadInput?: string; + } + >({ endpoint, body: { as_mod: true } }) + .then((response) => ({ + success: response.success ?? false, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListUserBookmarks(): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/batch`; + + return this.get<{ + success?: boolean; + bookmarks?: UserBookmarkBatch[]; + BadInput?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success ?? false, + data: response.bookmarks, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListUserBookmarksByUser({ + username, + sort_ascending, + page_size, + page_index, + maybe_scoped_entity_type, + maybe_scoped_weight_type, + maybe_scoped_media_file_type, + }: ListUserBookmarksByUserRequest): Promise< + ApiResponse + > { + const user = username; + const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${user}`; + + const query = { + sort_ascending, + page_size, + page_index, + maybe_scoped_entity_type: maybe_scoped_entity_type + ? maybe_scoped_entity_type.join(",") + : undefined, + maybe_scoped_weight_type: maybe_scoped_weight_type + ? maybe_scoped_weight_type.join(",") + : undefined, + maybe_scoped_media_file_type: maybe_scoped_media_file_type + ? maybe_scoped_media_file_type.join(",") + : undefined, + }; + + return this.get<{ + success?: boolean; + pagination?: Pagination; + results: UserBookmarkByUser[]; + }>({ endpoint, query }) + .then((response) => ({ + success: response.success ?? false, + data: response.results, + pagination: response.pagination, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListUserBookmarksByEntity({ + entityType, + entityToken, + }: { + entityType: string; + entityToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${entityType}/${entityToken}`; + + return this.get<{ + success: boolean; + user_bookmarks: UserBookmarkByEntity[]; + }>({ endpoint }) + .then((response) => ({ + success: true, + data: response.user_bookmarks, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/UserRatingsApi.ts b/frontend/storyteller_backend_api/apis/UserRatingsApi.ts new file mode 100644 index 0000000000..66621cbb59 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/UserRatingsApi.ts @@ -0,0 +1,92 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; + +export interface UserRating { + entity_token: string; + entity_type: string; + rating_value: string; +} + +export class UserRatingApi extends ApiManager { + public ListUserRatings(): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/batch`; + + return this.get<{ + success?: boolean; + ratings?: UserRating[]; + BadInput?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success ?? false, + data: response.ratings, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public PostUserRating({ + entityToken, + entityType, + ratingValue, + }: { + entityToken: string; + entityType: string; + ratingValue: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/rate`; + + const body = { + entity_token: entityToken, + entity_type: entityType, + rating_value: ratingValue, + }; + + return this.post< + { + entity_token: string; + entity_type: string; + rating_value: string; + }, + { + success?: boolean; + new_positive_rating_count_for_entity?: number; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + data: response.new_positive_rating_count_for_entity, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListUserRatingByEntity({ + entityType, + entityToken, + }: { + entityType: string; + entityToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/view/${entityType}/${entityToken}`; + + return this.get<{ + success?: boolean; + maybe_rating_value?: string; + BadInput?: string; + }>({ endpoint }) + .then((response) => { + return { + success: response.success ?? false, + data: response.maybe_rating_value, + errorMessage: response.BadInput, + }; + }) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/UsersApi.ts b/frontend/storyteller_backend_api/apis/UsersApi.ts new file mode 100644 index 0000000000..169fc7cdfb --- /dev/null +++ b/frontend/storyteller_backend_api/apis/UsersApi.ts @@ -0,0 +1,161 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { UserInfo } from "../models/Users"; + +export class UsersApi extends ApiManager { + public GetSession(): Promise< + ApiResponse<{ + loggedIn: boolean; + user?: UserInfo; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/session`; + return this.get<{ + success: boolean; + logged_in: boolean; + user?: UserInfo; + error_message?: string; + }>({ endpoint: endpoint }) + .then((response) => ({ + success: response.success, + data: { + loggedIn: response.logged_in, + user: response.user, + }, + })) + .catch((err) => { + return { + success: false, + errorMessage: err.mesasge, + }; + }); + } + + public GetUserProfile(username: string): Promise< + ApiResponse<{ + user?: UserInfo; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/user/${username}/profile`; + return this.get<{ + success: boolean; + user?: UserInfo; + error_message?: string; + }>({ endpoint: endpoint }) + .then((response) => ({ + success: response.success, + data: { + user: response.user, + }, + })) + .catch((err) => { + return { + success: false, + error_message: err.message, + }; + }); + } + + public async Login({ + usernameOrEmail, + password, + }: { + usernameOrEmail: string; + password: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/login`; + const body = { + username_or_email: usernameOrEmail, + password: password, + }; + return await this.post< + { username_or_email: string; password: string }, + { + success: boolean; + signed_session?: string; + error_message?: string; + error_type?: string; + } + >({ + endpoint: endpoint, + body: body, + }) + .then((response) => ({ + success: response.success, + data: { signedSession: response.signed_session }, + errorMessage: response.error_message, + })) + .catch((err) => { + return { + success: false, + error_message: err.message, + }; + }); + } + + public Logout(): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/logout`; + return this.post({ + endpoint: endpoint, + }) + .then((response) => ({ + success: response.success, + })) + .catch((err) => { + return { + success: false, + errorMessage: err.message, + }; + }); + } + + public async Signup({ + username, + email, + password, + passwordConfirmation, + }: { + username: string; + email: string; + password: string; + passwordConfirmation: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/create_account`; + const body = { + email_address: email, + password, + password_confirmation: passwordConfirmation, + username, + }; + return await this.post< + { + username: string; + email_address: string; + password: string; + password_confirmation: string; + }, + { + success: boolean; + signed_session?: string; + error_fields?: Record; + error_message?: string; + error_type?: string; + } + >({ + endpoint: endpoint, + body: body, + }) + .then((response) => { + return { + success: response.success, + data: { signedSession: response.signed_session }, + errorMessage: Object.values(response.error_fields ?? {}).join(","), + }; + }) + .catch((err) => { + return { + success: false, + error_message: err.message, + }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/VideoApi.ts b/frontend/storyteller_backend_api/apis/VideoApi.ts new file mode 100644 index 0000000000..343546ce79 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/VideoApi.ts @@ -0,0 +1,70 @@ +import { ApiManager, ApiResponse } from "../apis/ApiManager"; +import { Visibility } from "../enums/Visibility"; + +interface GenerateVideoStyleTransferRequest { + creator_set_visibility: string; + disable_lcm: boolean; + enable_lipsync: boolean; + frame_skip: number; + global_ipa_media_token: string; + input_depth_file: string; + input_file: string; + input_normal_file: string; + input_outline_file: string; + negative_prompt: string; + prompt: string; + remove_watermark: boolean; + style: string; + travel_prompt: string; + trim_end_millis: number; + trim_start_millis: number; + use_cinematic: boolean; + use_face_detailer: boolean; + use_strength: number; + use_upscaler: boolean; + uuid_idempotency_token: string; +} + +export class VideoApi extends ApiManager { + public async EnqueueStudio({ + enqueueVideo, + }: { + enqueueVideo: GenerateVideoStyleTransferRequest; + }): Promise< + ApiResponse<{ + inference_job_token?: string; + inference_job_token_type?: string; + }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/workflows/enqueue_studio`; + + const body = { + ...enqueueVideo, + ...(enqueueVideo.creator_set_visibility + ? {} + : { creator_set_visibility: Visibility.Public }), + }; + + return await this.post< + GenerateVideoStyleTransferRequest, + { + success?: boolean; + inference_job_token?: string; + inference_job_token_type?: string; + BadInput?: string; + } + >({ endpoint, body: body }) + .then((response) => ({ + success: Boolean(response.success ?? false), + data: { + inference_job_token: response.inference_job_token, + inference_job_token_type: response.inference_job_token_type, + }, + errorMessage: response.BadInput, + })) + .catch((err) => { + console.log(err.message); + return { success: false, error_reason: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/VoiceConversionApi.ts b/frontend/storyteller_backend_api/apis/VoiceConversionApi.ts new file mode 100644 index 0000000000..e9a27ad2bf --- /dev/null +++ b/frontend/storyteller_backend_api/apis/VoiceConversionApi.ts @@ -0,0 +1,53 @@ +import { ApiManager, ApiResponse } from "./ApiManager"; + +interface VoiceConversionParams { + autoPredictF0?: boolean; + creatorSetVisibility?: string; + isStorytellerDemo?: boolean; + overrideF0Method?: string; + sourceMediaUploadToken: string; + transpose?: number; + uuidIdempotencyToken: string; + voiceConversionModelToken: string; +} + +interface VoiceConversionRequest { + auto_predict_f0: boolean; + creator_set_visibility: string; + is_storyteller_demo: boolean; + override_f0_method: string; + source_media_upload_token: string; + transpose: number; + uuid_idempotency_token: string; + voice_conversion_model_token: string; +} + +export class VoiceConversionApi extends ApiManager { + public ConvertVoice( + params: VoiceConversionParams, + ): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_conversion/inference`; + + const body = this.parseBodyValues< + VoiceConversionParams, + VoiceConversionRequest + >(params); + + return this.post< + VoiceConversionRequest, + { + success?: boolean; + inference_job_token?: string; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + data: response.inference_job_token, + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/VoiceDesignerApi.ts b/frontend/storyteller_backend_api/apis/VoiceDesignerApi.ts new file mode 100644 index 0000000000..6054d72ce0 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/VoiceDesignerApi.ts @@ -0,0 +1,77 @@ +import { ApiManager, ApiResponse } from "./ApiManager"; +import { Pagination } from "../models/Pagination"; +import { ZsDataset } from "../models/Dataset"; + +interface VoiceDesignerParams { + text: string; + uuidIdempotencyToken: string; + voiceToken: string; +} + +interface VoiceDesignerRequest { + text: string; + uuid_idempotency_token: string; + voice_token: string; +} + +export class VoiceDesignerApi extends ApiManager { + public EnqueueTts( + params: VoiceDesignerParams, + ): Promise< + ApiResponse<{ inference_job_token?: string; voice_token?: string }> + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_designer/enqueue_tts`; + + const body = this.parseBodyValues< + VoiceDesignerParams, + VoiceDesignerRequest + >(params); + + return this.post< + VoiceDesignerRequest, + { + success?: boolean; + inference_job_token?: string; + voice_token: string; + BadInput?: string; + } + >({ endpoint, body }) + .then((response) => ({ + success: response.success ?? false, + ...(response.success + ? { + data: { + inference_job_token: response.inference_job_token, + voice_token: response.voice_token, + }, + } + : {}), + errorMessage: response.BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListDatasetsByUser({ + username, + }: { + username: string; + }): Promise> { + const user = username; + const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_designer/user/${user}/list`; + + return this.get<{ + success: boolean; + datasets: ZsDataset[]; + pagination: Pagination; + }>({ endpoint }) + .then((response) => ({ + success: true, + data: response.datasets, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/WeightsApi.ts b/frontend/storyteller_backend_api/apis/WeightsApi.ts new file mode 100644 index 0000000000..099bfd33a6 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/WeightsApi.ts @@ -0,0 +1,295 @@ +import { ApiManager, ApiResponse } from "./ApiManager"; +import { Weight } from "../models/Weight"; +import { Pagination, PaginationInfinite } from "../models/Pagination"; +import { + FilterEngineCategories, + FilterMediaClasses, + FilterMediaType, +} from "../enums/QueryFilters"; +import { Visibility } from "../enums/Visibility"; + +export enum ScopedWeightType { + HIFIGAN_TT2 = "hifigan_tt2", + RVC_V2 = "rvc_v2", + SD_1_5 = "sd_1.5", + SDXL = "sdxl", + SO_VITS_SVC = "so_vits_svc", + TT2 = "tt2", + LORA = "loRA", + VALL_E = "vall_e", + COMFY_UI = "comfy_ui", +} +export enum ScopedWeightCategory { + IMAGE_GENERATION = "image_generation", + TEXT_TO_SPEECH = "text_to_speech", + VOCODER = "vocoder", + VOICE_CONVERSION = "voice_conversion", + WORKFLOW_CONFIG = "workflow_config", +} + +interface WeightsRequest { + pageSize?: number; + weightType?: ScopedWeightType[]; + weightCategory?: ScopedWeightCategory[]; + //TODO: what is scoped?? + scopedWeightType?: ScopedWeightType[]; + scopedWeightCategory?: ScopedWeightCategory[]; +} + +export interface ListWeightsByUserRequest extends WeightsRequest { + username: string; + pageIndex?: number; + sortAscending?: boolean; +} + +export interface ListWeightsRequest extends WeightsRequest { + cursor?: string; + cursorIsReversed?: boolean; +} + +export interface ListFeaturedWeightsRequest { + sortAscending?: boolean; + pageSize?: number; + cursor?: string; + cursorIsReversed?: boolean; + filterMediaClasses?: FilterMediaClasses[]; + filterMediaType?: FilterMediaType[]; + filterEngineCategories?: FilterEngineCategories[]; +} + +export interface SearchWeightParams { + ietfLanguageSubtag: string; + searchTerm: string; + weightCategory: ScopedWeightCategory; + weightType: ScopedWeightType; +} + +export interface SearchWeightRequest { + ietf_language_subtag: string; + search_term: string; + weight_category: string; + weight_type: string; +} + +interface GetWeightByTokenResponse extends Weight { + success: boolean; +} + +export interface UpdateWeightByTokenParams { + weightToken: string; + coverImageMediaFileToken: string; + descriptionMarkdown: string; + title: string; + visibility: Visibility; +} +export interface UpdateWeightByTokenRequest { + cover_image_media_file_token: string; + description_markdown: string; + title: string; + visibility: Visibility; +} + +export class WeightsApi extends ApiManager { + public ListWeightsByUser({ + username, + ...params + }: ListWeightsByUserRequest): Promise> { + const user = username; + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/by_user/${user}`; + + const query = this.parseQueryValues(params); + + return this.get<{ + success: boolean; + results: Weight[]; + pagination: Pagination; + }>({ endpoint, query }) + .then((response) => ({ + success: true, + data: response.results, + pagination: response.pagination, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListWeights({ + ...params + }: ListWeightsRequest): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list`; + + const query = this.parseQueryValues(params); + + return this.get<{ + success: boolean; + results: Weight[]; + pagination: PaginationInfinite; + }>({ endpoint, query }) + .then((response) => ({ + success: true, + data: response.results, + pagination: response.pagination, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListWeightsFeatured({ + ...params + }: ListFeaturedWeightsRequest): Promise< + ApiResponse + > { + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list_featured`; + + const query = this.parseQueryValues(params); + + return this.get<{ + success: boolean; + results: Weight[]; + pagination: PaginationInfinite; + }>({ endpoint, query }) + .then((response) => ({ + success: true, + data: response.results, + pagination: response.pagination, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public ListWeightsPinned(): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list_pinned`; + + return this.get<{ + success: boolean; + results: Weight[]; + }>({ endpoint }) + .then((response) => ({ + success: true, + data: response.results, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public SearchWeights( + params: SearchWeightParams, + ): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/search`; + + const body = this.parseBodyValues( + params, + ); + + return this.post< + SearchWeightRequest, + { + success: boolean; + weights: Weight[]; + } + >({ endpoint, body }) + .then((response) => ({ + success: true, + data: response.weights, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public GetWeightByToken({ + weightToken, + }: { + weightToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`; + + return this.get({ endpoint }) + .then(({ success, ...mediaFile }) => ({ + success: success, + data: mediaFile, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public UpdateWeightByToken({ + weightToken, + ...params + }: UpdateWeightByTokenParams): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`; + + const body = this.parseBodyValues< + Omit, + UpdateWeightByTokenRequest + >(params); + + return this.post< + UpdateWeightByTokenRequest, + { success?: boolean; BadInput?: string } + >({ endpoint, body }) + .then(({ success, BadInput }) => ({ + success: success ?? false, + errorMessage: BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public DeleteWeightByToken({ + weightToken, + }: { + weightToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`; + + const body = { + as_mod: true, + set_delete: true, + }; + + return this.delete< + { as_mod: boolean; set_delete: boolean }, + { success?: boolean; BadInput?: string } + >({ endpoint, body }) + .then(({ success, BadInput }) => ({ + success: success ?? false, + errorMessage: BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } + + public UpdateWeightCoverImageByToken({ + weightToken, + coverImageMediaFileToken, + }: { + weightToken: string; + coverImageMediaFileToken: string; + }): Promise> { + const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/${weightToken}/cover_image`; + + const body = { + cover_image_media_file_token: coverImageMediaFileToken, + }; + + return this.post< + { cover_image_media_file_token: string }, + { success?: boolean; BadInput?: string } + >({ endpoint, body }) + .then(({ success, BadInput }) => ({ + success: success ?? false, + errorMessage: BadInput, + })) + .catch((err) => { + return { success: false, errorMessage: err.message }; + }); + } +} diff --git a/frontend/storyteller_backend_api/apis/index.ts b/frontend/storyteller_backend_api/apis/index.ts new file mode 100644 index 0000000000..ef8b261a96 --- /dev/null +++ b/frontend/storyteller_backend_api/apis/index.ts @@ -0,0 +1,18 @@ +// https://storyteller-docs.netlify.app/#/ +export * from "./AnalyticsApi"; +export * from "./BetaKeysApi"; +export * from "./BillingApi"; +export * from "./CommentsApi"; +export * from "./EngineApi"; +export * from "./FeaturedItemsApi"; +export * from "./JobsApi"; +export * from "./MediaFilesApi"; +export * from "./MediaUploadApi"; +export * from "./MiscApi"; +export * from "./ModerationApi"; +export * from "./PromptsApi"; +export * from "./TtsApi"; +export * from "./UserBookmarksApi"; +export * from "./UsersApi"; +export * from "./VideoApi"; +export * from "./WeightsApi"; diff --git a/frontend/storyteller_backend_api/enums/Api.ts b/frontend/storyteller_backend_api/enums/Api.ts new file mode 100644 index 0000000000..302021939d --- /dev/null +++ b/frontend/storyteller_backend_api/enums/Api.ts @@ -0,0 +1,7 @@ +export enum API_TARGETS { + BASE_API = "https://api.storyteller.ai", + GOOGLE_API = "https://storage.googleapis.com", + FUNNEL_API = "https://style.storyteller.ai", + CDN_API = "https://cdn.storyteller.ai", + GRAVATAR_API = "https://www.gravatar.com", +} diff --git a/frontend/storyteller_backend_api/enums/Billing.ts b/frontend/storyteller_backend_api/enums/Billing.ts new file mode 100644 index 0000000000..c602de1b8a --- /dev/null +++ b/frontend/storyteller_backend_api/enums/Billing.ts @@ -0,0 +1,11 @@ +export enum SubscriptionNamespace { + FAKEYOU = "fakeyou", +} +export enum SubscriptionProduct { + PLUS = "fakeyou_plus", + PRO = "fakeyou_pro", + ELITE = "fakeyou_elite", +} +export enum LoyaltyProgram { + CONTRIBUTOR = "fakeyou_contributor", +} diff --git a/frontend/storyteller_backend_api/enums/Job.ts b/frontend/storyteller_backend_api/enums/Job.ts new file mode 100644 index 0000000000..5b21d780e4 --- /dev/null +++ b/frontend/storyteller_backend_api/enums/Job.ts @@ -0,0 +1,16 @@ +export enum JobStatus { + PENDING = "pending", + STARTED = "started", + ATTEMPT_FAILED = "attempt_failed", + COMPLETE_SUCCESS = "complete_success", + COMPLETE_FAILURE = "complete_failure", + DEAD = "dead", + CANCELLED_BY_USER = "cancelled_by_user", + CANCELLED_BY_SYSTEM = "cancelled_by_system", +} + +export enum JobType { + TextToSpeech = "text_to_speech", + VoiceConversion = "voice_conversion", + VideoStyleTransfer = "workflow", +} diff --git a/frontend/storyteller_backend_api/enums/MediaFileClass.ts b/frontend/storyteller_backend_api/enums/MediaFileClass.ts new file mode 100644 index 0000000000..1535b7472e --- /dev/null +++ b/frontend/storyteller_backend_api/enums/MediaFileClass.ts @@ -0,0 +1,26 @@ +export enum MediaFileClass { + /// Unknown (default value) + /// This will be present until we migrate all old files. + Unknown = "unknown", + + /// Audio files: wav, mp3, etc. + Audio = "audio", + + /// Image files: png, jpeg, etc. + Image = "image", + + /// Video files: mp4, etc. + Video = "video", + + /// Engine "animations" + Animation = "animation", + + /// Engine "characters" + Character = "character", + + /// Engine "prop" items + Prop = "prop", + + /// Engine scenes (internal and external scenes) + Scene = "scene", +} diff --git a/frontend/storyteller_backend_api/enums/MediaFileType.ts b/frontend/storyteller_backend_api/enums/MediaFileType.ts new file mode 100644 index 0000000000..4d26c3e62c --- /dev/null +++ b/frontend/storyteller_backend_api/enums/MediaFileType.ts @@ -0,0 +1,58 @@ +export enum MediaFileType { + Audio = "audio", + Video = "video", + Image = "image", + + // BVH is a very popular file format for motion capture. + // It is compatible with our engine. + BVH = "bvh", + + // GLTF is the text-based format of GLB. Unforunately it requires a + // second file to include the visual data, so we strongly discourage + // the use of this format in favor of GLB. + GLTF = "gltf", + + // GLB is the format we convert FBX files into. It's a very popular + // open source 3D file format. + GLB = "glb", + + // MMD is the format for anime characters. + // open source 3D file format. + MMD = "mmd", + + // FBX isn't supported by Storyteller Engine, but we can get + // uploads in this format. It's a very popular 3D graphics format. + FBX = "fbx", + + // Full Storyteller Engine scenes (RON = Rusty Object Notation) + // This is a temporary format that will eventually go away. + Scene = "scene", + + None = "none", +} + +export enum MediaFileSubtype { + /// Animation file from Mixamo + /// Primarily used for FBX and GLB. + Mixamo = "mixamo", + + /// Animation file from MocapNet + /// Primarily used for BVH. + MocapNet = "mocap_net", + + /// Generic animation case + /// Used for BVH files, but can also pertain to animation-only files of other types. + AnimationOnly = "animation_only", + + // TODO(bt,2024-03-08): Migrate records and code, then remove + /// DEPRECATED: Use `SceneImport` instead. + Scene = "scene", + + /// Generic 3D scene file. + /// Can pertain to glTF, glB, FBX, etc. + SceneImport = "scene_import", + + /// Native Storyteller scene format. + /// Typically stored in a `.scn.ron` file. + StorytellerScene = "storyteller_scene", +} diff --git a/frontend/storyteller_backend_api/enums/QueryFilters.ts b/frontend/storyteller_backend_api/enums/QueryFilters.ts new file mode 100644 index 0000000000..4f9392a1f7 --- /dev/null +++ b/frontend/storyteller_backend_api/enums/QueryFilters.ts @@ -0,0 +1,29 @@ +export enum FilterMediaClasses { + AUDIO = "audio", + IMAGE = "image", + VIDEO = "video", +} + +export enum FilterMediaType { + SCENE_JSON = "scene_json", + GLB = "glb", + GLTF = "gltf", + PMX = "pmx", + PMD = "pmd", + VMD = "vmd", +} + +export enum FilterEngineCategories { + ANIMATION = "animation", + AUDIO = "audio", + CHARACTER = "character", + CREATURE = "creature", + EXPRESSION = "expression", + IMAGE_PLANE = "image_plane", + LOCATION = "location", + OBJECT = "object", + SCENE = "scene", + SET_DRESSING = "set_dressing", + SKYBOX = "skybox", + VIDEO_PLANE = "video_plane", +} diff --git a/frontend/storyteller_backend_api/enums/Tts.ts b/frontend/storyteller_backend_api/enums/Tts.ts new file mode 100644 index 0000000000..3823fcb569 --- /dev/null +++ b/frontend/storyteller_backend_api/enums/Tts.ts @@ -0,0 +1,7 @@ +export enum GenerateTtsAudioErrorType { + BadRequest = "bad_request", + NotFound = "not_found", + TooManyRequests = "too_many_requests", + ServerError = "server_error", + UnknownError = "unknown_error", +} diff --git a/frontend/storyteller_backend_api/enums/UserFeatures.ts b/frontend/storyteller_backend_api/enums/UserFeatures.ts new file mode 100644 index 0000000000..76c4b77ae8 --- /dev/null +++ b/frontend/storyteller_backend_api/enums/UserFeatures.ts @@ -0,0 +1,6 @@ +export enum USER_FEATURE_FLAGS { + EXPLORE_MEDIA = "explore_media", + STUDIO = "studio", + UPLOAD_3D = "upload_3d", + VIDEO_STYLE_TRANSFER = "video_style_transfer", +} diff --git a/frontend/storyteller_backend_api/enums/Visibility.ts b/frontend/storyteller_backend_api/enums/Visibility.ts new file mode 100644 index 0000000000..a0f80fa4e9 --- /dev/null +++ b/frontend/storyteller_backend_api/enums/Visibility.ts @@ -0,0 +1,5 @@ +export enum Visibility { + Public = "public", + Hidden = "hidden", + Private = "private", +} diff --git a/frontend/storyteller_backend_api/enums/WeightCategory.ts b/frontend/storyteller_backend_api/enums/WeightCategory.ts new file mode 100644 index 0000000000..834ed7b43f --- /dev/null +++ b/frontend/storyteller_backend_api/enums/WeightCategory.ts @@ -0,0 +1,8 @@ +export enum WeightCategory { + TTS = "text_to_speech", + VOCODER = "vocoder", + VC = "voice_conversion", + ZS = "zero_shot", + SD = "image_generation", + WF = "workflow_config", +} diff --git a/frontend/storyteller_backend_api/enums/WeightType.ts b/frontend/storyteller_backend_api/enums/WeightType.ts new file mode 100644 index 0000000000..246e7ca6b1 --- /dev/null +++ b/frontend/storyteller_backend_api/enums/WeightType.ts @@ -0,0 +1,11 @@ +export enum WeightType { + TT2 = "tt2", + HIFIGAN_TT2 = "hifigan_tt2", + RVCv2 = "rvc_v2", + SD_15 = "sd_1.5", + SDXL = "sdxl", + SVC = "so_vits_svc", + LORA = "loRA", + VALL_E = "vall_e", + NONE = "none", +} diff --git a/frontend/storyteller_backend_api/folder_comparison_20250416_133319.md b/frontend/storyteller_backend_api/folder_comparison_20250416_133319.md new file mode 100644 index 0000000000..b1b22594f9 --- /dev/null +++ b/frontend/storyteller_backend_api/folder_comparison_20250416_133319.md @@ -0,0 +1,143 @@ +# Recursive Folder Comparison Report: Classes vs Classes + +Generated on: 2025-04-16 13:33:19 + +## File Comparison Table + +| File Path | Status | Classes | Classes | Difference | +|------------------------------------|----------|--------------------|--------------------|------------------------------| + +| ApiManager/MediaFilesApi.ts | be67aab2:44342e0f | 10.6KB (364 lines) | 10.6KB (369 lines) | +56B (+5 lines) 5% different | + +| ApiManager/VoiceDesignerApi.ts | e4d874c3:3635ba42 | 2.0KB (77 lines) | 2.1KB (78 lines) | +94B (+1 lines) 5% different | +| ApiManager/PromptsApi.ts | d73d329a:c4824d2e | 648B (25 lines) | 657B (25 lines) | +9B (0 lines) 4% different | + +## Files Present in One Folder But Missing in the Other + + +### Files Only in Classes (23 files) +- `ApiManager/SegmentationApi.ts` (1.9KB, 84 lines, hash: bb14fbf6) +- `ApiManager/enums/Api.ts` (254B, 7 lines, hash: ea867d98) +- `ApiManager/enums/Billing.ts` (242B, 11 lines, hash: e47c3eaa) +- `ApiManager/enums/Job.ts` (433B, 16 lines, hash: d148fd64) +- `ApiManager/enums/MediaFileClass.ts` (524B, 26 lines, hash: c90ce542) +- `ApiManager/enums/MediaFileType.ts` (1.7KB, 58 lines, hash: a2374fec) +- `ApiManager/enums/QueryFilters.ts` (579B, 29 lines, hash: 6d10a292) +- `ApiManager/enums/Tts.ts` (205B, 7 lines, hash: f6393d2e) +- `ApiManager/enums/UserFeatures.ts` (167B, 6 lines, hash: 05d49e51) +- `ApiManager/enums/Visibility.ts` (92B, 5 lines, hash: 297d031b) +- `ApiManager/enums/WeightCategory.ts` (180B, 8 lines, hash: f9be3d32) +- `ApiManager/enums/WeightType.ts` (208B, 11 lines, hash: 80436726) +- `ApiManager/models/BetaKeys.ts` (502B, 26 lines, hash: 7c718ac6) +- `ApiManager/models/Billing.tsx` (416B, 17 lines, hash: 8db6eb3b) +- `ApiManager/models/Dataset.ts` (588B, 23 lines, hash: 6af860ff) +- `ApiManager/models/Job.ts` (1.8KB, 72 lines, hash: 9d7b385a) +- `ApiManager/models/MediaFile.ts` (1.8KB, 59 lines, hash: 41778349) +- `ApiManager/models/Pagination.ts` (200B, 9 lines, hash: 837b24d3) +- `ApiManager/models/Prompts.ts` (424B, 16 lines, hash: 8ef1f366) +- `ApiManager/models/StatusAlert.ts` (175B, 9 lines, hash: fcee4684) +- `ApiManager/models/UserBookmark.ts` (1.6KB, 75 lines, hash: 8f70d1b1) +- `ApiManager/models/Users.ts` (1.3KB, 50 lines, hash: 5ed82bbd) +- `ApiManager/models/Weight.ts` (767B, 40 lines, hash: 3f9cccdb) + +### Files Only in Classes (4 files) +- `ApiHelpers.ts` (637B, 17 lines, hash: 9b638c6c) +- `ApiManager/StudioGen2Api.ts` (1.6KB, 61 lines, hash: b2707195) +- `EnvironmentVariables.ts` (263B, 10 lines, hash: 29c93666) +- `GetFrontendEnvironment.ts` (1.9KB, 65 lines, hash: 7ee8b738) + +## Files with Differences (Sorted by Most Different First) + +### `ApiManager/MediaUploadApi.ts` (72% different) +- Classes: 10.3KB, 349 lines, hash: 707d2bf2 +- Classes: 10.0KB, 339 lines, hash: 06a93d8c +- Difference: -255 bytes, -10 lines + +### `ApiManager/EngineApi.ts` (69% different) +- Classes: 1.0KB, 39 lines, hash: c4850e80 +- Classes: 5.5KB, 213 lines, hash: 85e20a81 +- Difference: +4469 bytes, +174 lines + +### `ApiManager/JobsApi.ts` (28% different) +- Classes: 2.0KB, 77 lines, hash: f7eb8e22 +- Classes: 1.9KB, 74 lines, hash: 2aa03349 +- Difference: -56 bytes, -3 lines + +### `ApiManager/ApiManager.ts` (14% different) +- Classes: 5.5KB, 235 lines, hash: 44db0b07 +- Classes: 5.3KB, 211 lines, hash: 67a3a046 +- Difference: -200 bytes, -24 lines + +### `ApiManager/BillingApi.ts` (6% different) +- Classes: 1.0KB, 33 lines, hash: 20051893 +- Classes: 1.0KB, 33 lines, hash: f5b7af84 +- Difference: -8 bytes, +0 lines + +### `ApiManager/VoiceDesignerApi.ts` (5% different) +- Classes: 2.0KB, 77 lines, hash: e4d874c3 +- Classes: 2.1KB, 78 lines, hash: 3635ba42 +- Difference: +94 bytes, +1 lines + +### `ApiManager/MediaFilesApi.ts` (5% different) +- Classes: 10.6KB, 364 lines, hash: be67aab2 +- Classes: 10.6KB, 369 lines, hash: 44342e0f +- Difference: +56 bytes, +5 lines + +### `ApiManager/PromptsApi.ts` (4% different) +- Classes: 648B, 25 lines, hash: d73d329a +- Classes: 657B, 25 lines, hash: c4824d2e +- Difference: +9 bytes, +0 lines + +### `ApiManager/MiscApi.ts` (3% different) +- Classes: 799B, 26 lines, hash: 298f5d93 +- Classes: 799B, 26 lines, hash: 187ad676 +- Difference: +0 bytes, +0 lines + +### `ApiManager/TtsApi.ts` (3% different) +- Classes: 1.7KB, 64 lines, hash: 5feb5cad +- Classes: 1.7KB, 64 lines, hash: 6228b278 +- Difference: +2 bytes, +0 lines + +### `ApiManager/index.ts` (2% different) +- Classes: 564B, 18 lines, hash: 55a9094b +- Classes: 597B, 19 lines, hash: 1fe59701 +- Difference: +33 bytes, +1 lines + +### `ApiManager/UserBookmarksApi.ts` (2% different) +- Classes: 5.4KB, 201 lines, hash: e3e4cb19 +- Classes: 5.5KB, 202 lines, hash: fd3afc78 +- Difference: +56 bytes, +1 lines + +### `ApiManager/WeightsApi.ts` (2% different) +- Classes: 7.8KB, 295 lines, hash: c12a98df +- Classes: 7.9KB, 296 lines, hash: 8a7dbb68 +- Difference: +39 bytes, +1 lines + +### `ApiManager/VideoApi.ts` (1% different) +- Classes: 1.9KB, 70 lines, hash: a9947328 +- Classes: 1.9KB, 70 lines, hash: cb947c07 +- Difference: -11 bytes, +0 lines + +### `ApiManager/BetaKeysApi.ts` (1% different) +- Classes: 4.3KB, 167 lines, hash: 8d4ec6f1 +- Classes: 4.3KB, 167 lines, hash: 33f1e4b3 +- Difference: -3 bytes, +0 lines + +### `ApiManager/UsersApi.ts` (0% different) +- Classes: 3.9KB, 161 lines, hash: 65121217 +- Classes: 3.9KB, 161 lines, hash: 72db0eae +- Difference: -6 bytes, +0 lines + + +## Summary +- **Total Unique Files**: 49 +- **Files in Classes**: 45 +- **Files in Classes**: 26 +- **Identical Files**: 6 +- **Different Files**: 16 +- **Files Only in Classes**: 23 +- **Files Only in Classes**: 4 + +## Path Information +- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/` +- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/` \ No newline at end of file diff --git a/frontend/storyteller_backend_api/models/BetaKeys.ts b/frontend/storyteller_backend_api/models/BetaKeys.ts new file mode 100644 index 0000000000..9bde5a9de6 --- /dev/null +++ b/frontend/storyteller_backend_api/models/BetaKeys.ts @@ -0,0 +1,26 @@ +interface DefaultAvatar { + color_index: number; + image_index: number; +} + +interface User { + default_avatar: DefaultAvatar; + display_name: string; + gravatar_hash: string; + user_token: string; + username: string; +} + +export interface BetaKey { + created_at: string; + creator: User; + is_distributed: boolean; + key_value: string; + maybe_note?: string; + maybe_note_html?: string; + maybe_redeemed_at?: string; + maybe_redeemer?: User; + maybe_referrer?: User; + product: string; + token: string; +} diff --git a/frontend/storyteller_backend_api/models/Billing.tsx b/frontend/storyteller_backend_api/models/Billing.tsx new file mode 100644 index 0000000000..c0cb1c4c4d --- /dev/null +++ b/frontend/storyteller_backend_api/models/Billing.tsx @@ -0,0 +1,17 @@ +import { + LoyaltyProgram, + SubscriptionNamespace, + SubscriptionProduct, +} from "../enums/Billing"; + +export interface ActiveSubscriptions { + // user's list of subscriptions + active_subscriptions: Subscription[]; + // special programs that the user could be in + maybe_loyalty_program?: LoyaltyProgram; +} + +export interface Subscription { + namespace: SubscriptionNamespace; + product_slug: SubscriptionProduct; +} diff --git a/frontend/storyteller_backend_api/models/Dataset.ts b/frontend/storyteller_backend_api/models/Dataset.ts new file mode 100644 index 0000000000..783c8c259b --- /dev/null +++ b/frontend/storyteller_backend_api/models/Dataset.ts @@ -0,0 +1,23 @@ +export interface ZsDataset { + created_at: string; // ISO 8601 date-time string + creator: UserDetailsLight; + creator_set_visibility: string; // Enum or specific string values + dataset_token: string; + ietf_language_tag: string; + ietf_primary_language_subtag: string; + title: string; + updated_at: string; // ISO 8601 date-time string +} + +interface UserDetailsLight { + default_avatar: UserDefaultAvatarInfo; + display_name: string; + gravatar_hash: string; + user_token: string; + username: string; +} + +interface UserDefaultAvatarInfo { + color_index: number; + image_index: number; +} diff --git a/frontend/storyteller_backend_api/models/Job.ts b/frontend/storyteller_backend_api/models/Job.ts new file mode 100644 index 0000000000..ed8fb27c8c --- /dev/null +++ b/frontend/storyteller_backend_api/models/Job.ts @@ -0,0 +1,72 @@ +import { JobStatus } from "../enums/Job"; + +export interface Job { + created_at: string; + job_token: string; + maybe_result: JobMaybeResult; + request: JobRequest; + status: JobStatusResponse; + updated_at: string; +} + +export interface JobMaybeResult { + entity_token: string; + entity_type: string; + // maybe_public_bucket_media_path: string; + maybe_successfully_completed_at: string; + media_links: { + cdn_url: string; + }; +} +export interface JobRequest { + inference_category: string; + maybe_model_title: string; + maybe_model_token: string; + maybe_model_type: string; + maybe_raw_inference_text: string; + maybe_style_name: string; +} + +export interface JobStatusResponse { + attempt_count: number; + maybe_assigned_cluster: string; + maybe_assigned_worker: string; + maybe_extra_status_description: string; + maybe_failure_category: string; + maybe_first_started_at: string; + requires_keepalive: boolean; + progress_percentage: number; + status: JobStatus; +} + +export interface JobPreview { + success: boolean; + state: { + job_token: string; + request: Record; + status: { + status: string; + maybe_extra_status_description: string | null; + maybe_assigned_worker: string; + maybe_assigned_cluster: string; + maybe_first_started_at: string; + attempt_count: number; + requires_keepalive: boolean; + maybe_failure_category: string; + progress_percentage: number; + }; + maybe_result: { + expected_stages: number; + currently_active_stage: number; + per_stage_frame_count: number; + stages: Array<{ + stage_progress: number; + expected_frame_count: number; + stage_complete: boolean; + frames: string[]; + }>; + }; + created_at: string; + updated_at: string; + }; +} diff --git a/frontend/storyteller_backend_api/models/JobState.ts b/frontend/storyteller_backend_api/models/JobState.ts new file mode 100644 index 0000000000..29c103ef74 --- /dev/null +++ b/frontend/storyteller_backend_api/models/JobState.ts @@ -0,0 +1,35 @@ +interface MaybeResult { + entity_token: string; + entity_type: string; + maybe_public_bucket_media_path: string; + maybe_successfully_completed_at: string; +} + +interface Request { + inference_category: string; + maybe_model_title?: string; + maybe_model_token: string; + maybe_model_type: string; + maybe_raw_inference_text: string; + maybe_style_name: string; +} + +interface Status { + attempt_count: number; + maybe_assigned_cluster?: string; + maybe_assigned_worker?: string; + maybe_extra_status_description?: string; + maybe_failure_category?: string; + maybe_first_started_at: string; + requires_keepalive: boolean; + status: string; +} + +export interface JobState { + created_at: string; + job_token: string; + maybe_result?: MaybeResult; + request: Request; + status: Status; + updated_at: string; +} diff --git a/frontend/storyteller_backend_api/models/MediaFile.ts b/frontend/storyteller_backend_api/models/MediaFile.ts new file mode 100644 index 0000000000..2c57e7ba3e --- /dev/null +++ b/frontend/storyteller_backend_api/models/MediaFile.ts @@ -0,0 +1,59 @@ +import { MediaFileClass } from "../enums/MediaFileClass"; +import { MediaFileType, MediaFileSubtype } from "../enums/MediaFileType"; +import { WeightCategory } from "../enums/WeightCategory"; +import { WeightType } from "../enums/WeightType"; +import { UserInfo } from "./Users"; + +export interface MediaFile { + token: string; + media_type: MediaFileType; + media_class: MediaFileClass | null; + maybe_animation_type: string | null; + maybe_media_subtype: MediaFileSubtype | null; + maybe_engine_extension: string | null; + maybe_batch_token: string; + maybe_original_filename: string | null; + maybe_creator_user: UserInfo | null; + maybe_prompt_token: string | null; + origin: { + origin_category: string; + product_category: string; + maybe_model: { title: string } | null; + }; + origin_category: string; + origin_product_category: string; + maybe_origin_model_type: null | string; + maybe_origin_model_token: null | string; + maybe_duration_millis: number | null; + maybe_style_name: null | string; + public_bucket_path: string; + public_bucket_url: string; + media_links: { + cdn_url: string; + thumbnail_template: string; + }; + cover_image: { + maybe_cover_image_public_bucket_path: null | string; + default_cover: { + image_index: number; + color_index: number; + }; + }; + creator_set_visibility: string; + maybe_title: null | string; + maybe_text_transcript: null | string; + stats: { + positive_rating_count: number; + bookmark_count: number; + }; + maybe_model_weight_info: { + title: string; + weight_token: string; + weight_category: WeightCategory; + weight_type: WeightType; + maybe_weight_creator: UserInfo; + maybe_cover_image_public_bucket_path: string; + }; + created_at: string; + updated_at: string; +} diff --git a/frontend/storyteller_backend_api/models/Pagination.ts b/frontend/storyteller_backend_api/models/Pagination.ts new file mode 100644 index 0000000000..8481f8c54e --- /dev/null +++ b/frontend/storyteller_backend_api/models/Pagination.ts @@ -0,0 +1,9 @@ +export interface Pagination { + current: number; + total_page_count: number; +} +export interface PaginationInfinite { + cursor_is_reversed: boolean; + maybe_next?: string; + maybe_previous?: string; +} diff --git a/frontend/storyteller_backend_api/models/Prompts.ts b/frontend/storyteller_backend_api/models/Prompts.ts new file mode 100644 index 0000000000..fafe2fd394 --- /dev/null +++ b/frontend/storyteller_backend_api/models/Prompts.ts @@ -0,0 +1,16 @@ +export interface Prompts { + created_at: string; + lcm_disabled: boolean; + lipsync_enabled: boolean; + maybe_inference_duration_millis: number; + maybe_moderator_fields: string; + maybe_negative_prompt: string; + maybe_positive_prompt: string; + maybe_strength: number; + maybe_style_name: string; + prompt_type: string; + token: string; + use_cinematic: boolean; + used_face_detailer: boolean; + used_upscaler: boolean; +} diff --git a/frontend/storyteller_backend_api/models/StatusAlert.ts b/frontend/storyteller_backend_api/models/StatusAlert.ts new file mode 100644 index 0000000000..e4b011d86f --- /dev/null +++ b/frontend/storyteller_backend_api/models/StatusAlert.ts @@ -0,0 +1,9 @@ +export interface StatusAlert { + maybe_alert?: MaybeAlert; + refresh_interval_millis: number; +} + +interface MaybeAlert { + maybe_category?: string; + maybe_message?: string; +} diff --git a/frontend/storyteller_backend_api/models/UserBookmark.ts b/frontend/storyteller_backend_api/models/UserBookmark.ts new file mode 100644 index 0000000000..2477ffae53 --- /dev/null +++ b/frontend/storyteller_backend_api/models/UserBookmark.ts @@ -0,0 +1,75 @@ +export interface UserBookmarkBatch { + entity_token: string; + entity_type: string; + is_bookmarked: boolean; + maybe_bookmark_token: string; +} + +interface DefaultAvatar { + color_index: number; + image_index: number; +} + +interface User { + default_avatar: DefaultAvatar; + display_name: string; + gravatar_hash: string; + user_token: string; + username: string; +} + +export interface UserBookmarkByEntity { + created_at: string; + token: string; + updated_at: string; + user: User; +} + +interface DefaultAvatar { + color_index: number; + image_index: number; +} + +interface Creator { + default_avatar: DefaultAvatar; + display_name: string; + gravatar_hash: string; + user_token: string; + username: string; +} + +interface MediaFileData { + maybe_creator: Creator; + media_type: string; // Adjust type as per your specific schema + public_bucket_path: string; +} + +interface WeightData { + maybe_cover_image_public_bucket_path: string; + maybe_creator: Creator; + title: string; + weight_category: string; // Adjust type as per your specific schema + weight_type: string; // Adjust type as per your specific schema +} + +interface Stats { + bookmark_count: number; + positive_rating_count: number; +} + +interface Details { + entity_token: string; + entity_type: string; // Adjust type as per your specific schema + maybe_media_file_data?: MediaFileData; + maybe_summary_text?: string; + maybe_thumbnail_url?: string; + maybe_weight_data?: WeightData; + stats: Stats; +} + +export interface UserBookmarkByUser { + created_at: string; + details: Details; + token: string; + updated_at: string; +} diff --git a/frontend/storyteller_backend_api/models/Users.ts b/frontend/storyteller_backend_api/models/Users.ts new file mode 100644 index 0000000000..8fa2cf9560 --- /dev/null +++ b/frontend/storyteller_backend_api/models/Users.ts @@ -0,0 +1,50 @@ +import { USER_FEATURE_FLAGS } from "../enums/UserFeatures"; + +export interface CoreInfo { + default_avatar: { + color_index: number; + image_index: number; + }; + display_name: string; + gravatar_hash: string; + user_token: string; + username: string; +} + +export interface UserInfo { + user_token: string; + username: string; + display_name: string; + email_gravatar_hash: string; + + core_info: CoreInfo; + + // Rollout feature flags + can_access_studio: boolean; + maybe_feature_flags: USER_FEATURE_FLAGS[]; + + // Usage + can_use_tts: boolean; + can_use_w2l: boolean; + can_delete_own_tts_results: boolean; + can_delete_own_w2l_results: boolean; + can_delete_own_account: boolean; + + // Contribution + can_upload_tts_models: boolean; + can_upload_w2l_templates: boolean; + can_delete_own_tts_models: boolean; + can_delete_own_w2l_templates: boolean; + + // Moderation + can_approve_w2l_templates: boolean; + can_edit_other_users_profiles: boolean; + can_edit_other_users_tts_models: boolean; + can_edit_other_users_w2l_templates: boolean; + can_delete_other_users_tts_models: boolean; + can_delete_other_users_tts_results: boolean; + can_delete_other_users_w2l_templates: boolean; + can_delete_other_users_w2l_results: boolean; + can_ban_users: boolean; + can_delete_users: boolean; +} diff --git a/frontend/storyteller_backend_api/models/Weight.ts b/frontend/storyteller_backend_api/models/Weight.ts new file mode 100644 index 0000000000..aec5f5c14f --- /dev/null +++ b/frontend/storyteller_backend_api/models/Weight.ts @@ -0,0 +1,40 @@ +interface CoverImage { + default_cover: { + color_index: number; + image_index: number; + }; + maybe_cover_image_public_bucket_path: string; +} + +interface DefaultAvatar { + color_index: number; + image_index: number; +} + +interface Creator { + default_avatar: DefaultAvatar; + display_name: string; + gravatar_hash: string; + user_token: string; + username: string; +} + +interface Stats { + bookmark_count: number; + positive_rating_count: number; +} + +export interface Weight { + cover_image: CoverImage; + created_at: string; + creator: Creator; + creator_set_visibility: string; + file_checksum_sha2: string; + file_size_bytes: number; + stats: Stats; + title: string; + updated_at: string; + weight_category: string; + weight_token: string; + weight_type: string; +}