@gradio/core 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/src/Blocks.svelte +75 -4
  3. package/dist/src/Render.svelte +2 -2
  4. package/dist/src/RenderComponent.svelte +21 -1
  5. package/dist/src/api_docs/ApiDocs.svelte +6 -1
  6. package/dist/src/api_docs/Settings.svelte +74 -1
  7. package/dist/src/api_docs/Settings.svelte.d.ts +3 -0
  8. package/dist/src/api_docs/img/record-stop.svg +1 -0
  9. package/dist/src/api_docs/img/record.svg +1 -0
  10. package/dist/src/gradio_helper.d.ts +1 -11
  11. package/dist/src/gradio_helper.js +3 -19
  12. package/dist/src/i18n.d.ts +12 -5
  13. package/dist/src/i18n.js +93 -12
  14. package/dist/src/init.js +40 -20
  15. package/dist/src/lang/en.json +6 -1
  16. package/dist/src/lang/es.json +2 -1
  17. package/dist/src/lang/fr.json +2 -1
  18. package/dist/src/lang/ja.json +2 -1
  19. package/dist/src/lang/ko.json +2 -1
  20. package/dist/src/lang/lt.json +2 -1
  21. package/dist/src/lang/nb.json +2 -1
  22. package/dist/src/lang/nl.json +2 -1
  23. package/dist/src/lang/pl.json +2 -1
  24. package/dist/src/lang/pt-BR.json +1 -0
  25. package/dist/src/lang/pt.json +2 -1
  26. package/dist/src/lang/ro.json +2 -1
  27. package/dist/src/lang/ru.json +2 -1
  28. package/dist/src/lang/sv.json +2 -1
  29. package/dist/src/lang/ta.json +2 -1
  30. package/dist/src/lang/th.json +2 -1
  31. package/dist/src/lang/tr.json +2 -1
  32. package/dist/src/lang/uk.json +2 -1
  33. package/dist/src/lang/ur.json +2 -1
  34. package/dist/src/lang/uz.json +2 -1
  35. package/dist/src/lang/zh-CN.json +2 -1
  36. package/dist/src/lang/zh-TW.json +2 -1
  37. package/dist/src/screen_recorder.d.ts +16 -0
  38. package/dist/src/screen_recorder.js +255 -0
  39. package/package.json +53 -53
  40. package/src/Blocks.svelte +86 -6
  41. package/src/Render.svelte +2 -2
  42. package/src/RenderComponent.svelte +21 -1
  43. package/src/api_docs/ApiDocs.svelte +7 -1
  44. package/src/api_docs/Settings.svelte +77 -1
  45. package/src/api_docs/img/record-stop.svg +1 -0
  46. package/src/api_docs/img/record.svg +1 -0
  47. package/src/gradio_helper.ts +5 -21
  48. package/src/i18n.test.ts +120 -1
  49. package/src/i18n.ts +126 -24
  50. package/src/init.ts +48 -26
  51. package/src/lang/en.json +6 -1
  52. package/src/lang/es.json +2 -1
  53. package/src/lang/fr.json +2 -1
  54. package/src/lang/ja.json +2 -1
  55. package/src/lang/ko.json +2 -1
  56. package/src/lang/lt.json +2 -1
  57. package/src/lang/nb.json +2 -1
  58. package/src/lang/nl.json +2 -1
  59. package/src/lang/pl.json +2 -1
  60. package/src/lang/pt-BR.json +1 -0
  61. package/src/lang/pt.json +2 -1
  62. package/src/lang/ro.json +2 -1
  63. package/src/lang/ru.json +2 -1
  64. package/src/lang/sv.json +2 -1
  65. package/src/lang/ta.json +2 -1
  66. package/src/lang/th.json +2 -1
  67. package/src/lang/tr.json +2 -1
  68. package/src/lang/uk.json +2 -1
  69. package/src/lang/ur.json +2 -1
  70. package/src/lang/uz.json +2 -1
  71. package/src/lang/zh-CN.json +2 -1
  72. package/src/lang/zh-TW.json +2 -1
  73. package/src/screen_recorder.ts +361 -0
package/dist/src/init.js CHANGED
@@ -24,7 +24,7 @@ export function create_components(initial_layout) {
24
24
  const layout_store = writable(initial_layout);
25
25
  let _components = [];
26
26
  let app;
27
- let keyed_component_values = {};
27
+ let keys_per_render_id = {};
28
28
  let _rootNode;
29
29
  function set_event_specific_args(dependencies) {
30
30
  dependencies.forEach((dep) => {
@@ -42,7 +42,6 @@ export function create_components(initial_layout) {
42
42
  // make sure the state is settled before proceeding
43
43
  flush();
44
44
  app = _app;
45
- store_keyed_values(_components);
46
45
  _components = components;
47
46
  inputs = new Set();
48
47
  outputs = new Set();
@@ -81,7 +80,24 @@ export function create_components(initial_layout) {
81
80
  * Rerender the layout when the config has been modified to attach new components
82
81
  */
83
82
  function rerender_layout({ render_id, components, layout, root, dependencies }) {
84
- let _constructor_map = preload_all_components(components, root);
83
+ components.forEach((c) => {
84
+ for (const prop in c.props) {
85
+ if (c.props[prop] === null) {
86
+ c.props[prop] = undefined;
87
+ }
88
+ }
89
+ });
90
+ let replacement_components = [];
91
+ let new_components = [];
92
+ components.forEach((c) => {
93
+ if (c.key == null || !keys_per_render_id[render_id]?.includes(c.key)) {
94
+ new_components.push(c);
95
+ }
96
+ else {
97
+ replacement_components.push(c);
98
+ }
99
+ });
100
+ let _constructor_map = preload_all_components(new_components, root);
85
101
  _constructor_map.forEach((v, k) => {
86
102
  constructor_map.set(k, v);
87
103
  });
@@ -104,17 +120,27 @@ export function create_components(initial_layout) {
104
120
  }
105
121
  };
106
122
  add_to_current_children(current_element);
107
- store_keyed_values(all_current_children);
108
123
  Object.entries(instance_map).forEach(([id, component]) => {
109
124
  let _id = Number(id);
110
125
  if (component.rendered_in === render_id) {
111
- delete instance_map[_id];
112
- if (_component_map.has(_id)) {
113
- _component_map.delete(_id);
126
+ let replacement_component = replacement_components.find((c) => c.key === component.key);
127
+ if (component.key != null && replacement_component !== undefined) {
128
+ const instance = instance_map[component.id];
129
+ for (const prop in replacement_component.props) {
130
+ if (!replacement_component.props.preserved_by_key?.includes(prop)) {
131
+ instance.props[prop] = replacement_component.props[prop];
132
+ }
133
+ }
134
+ }
135
+ else {
136
+ delete instance_map[_id];
137
+ if (_component_map.has(_id)) {
138
+ _component_map.delete(_id);
139
+ }
114
140
  }
115
141
  }
116
142
  });
117
- components.forEach((c) => {
143
+ new_components.forEach((c) => {
118
144
  instance_map[c.id] = c;
119
145
  _component_map.set(c.id, c);
120
146
  });
@@ -123,12 +149,17 @@ export function create_components(initial_layout) {
123
149
  }
124
150
  walk_layout(layout, root, _components.concat(components), current_element.parent).then(() => {
125
151
  layout_store.set(_rootNode);
152
+ keys_per_render_id[render_id] = components
153
+ .map((c) => c.key)
154
+ .filter((c) => c != null);
126
155
  });
127
156
  set_event_specific_args(dependencies);
128
157
  }
129
158
  async function walk_layout(node, root, components, parent) {
130
159
  const instance = instance_map[node.id];
131
- instance.component = (await constructor_map.get(instance.component_class_id || instance.type))?.default;
160
+ if (!instance.component) {
161
+ instance.component = (await constructor_map.get(instance.component_class_id || instance.type))?.default;
162
+ }
132
163
  instance.parent = parent;
133
164
  if (instance.type === "dataset") {
134
165
  instance.props.component_map = get_component(instance.type, instance.component_class_id, root, components, instance.props.components).example_components;
@@ -138,10 +169,6 @@ export function create_components(initial_layout) {
138
169
  }
139
170
  instance.props.interactive = determine_interactivity(instance.id, instance.props.interactive, instance.props.value, inputs, outputs);
140
171
  instance.props.server = process_server_fn(instance.id, instance.props.server_fns, app);
141
- if (instance.key != null &&
142
- keyed_component_values[instance.key] !== undefined) {
143
- instance.props.value = keyed_component_values[instance.key];
144
- }
145
172
  _component_map.set(instance.id, instance);
146
173
  if (node.children) {
147
174
  instance.children = await Promise.all(node.children.map((v) => walk_layout(v, root, components, instance)));
@@ -178,13 +205,6 @@ export function create_components(initial_layout) {
178
205
  }
179
206
  let update_scheduled = false;
180
207
  let update_scheduled_store = writable(false);
181
- function store_keyed_values(components) {
182
- components.forEach((c) => {
183
- if (c.key != null) {
184
- keyed_component_values[c.key] = c.props.value;
185
- }
186
- });
187
- }
188
208
  function flush() {
189
209
  layout_store.update((layout) => {
190
210
  for (let i = 0; i < pending_updates.length; i++) {
@@ -69,6 +69,10 @@
69
69
  "language": "Language",
70
70
  "display_theme": "Display Theme",
71
71
  "pwa": "Progressive Web App",
72
+ "record": "Record",
73
+ "stop_recording": "Stop Recording",
74
+ "screen_studio": "Screen Studio",
75
+ "share_gradio_tab": "[Sharing] Gradio Tab",
72
76
  "run": "Run"
73
77
  },
74
78
  "dataframe": {
@@ -98,7 +102,8 @@
98
102
  "runtime_error": "there is a runtime error",
99
103
  "space_not_working": "\"Space isn't working because\" {0}",
100
104
  "space_paused": "the space is paused",
101
- "use_via_api": "Use via API"
105
+ "use_via_api": "Use via API",
106
+ "use_via_api_or_mcp": "Use via API or MCP"
102
107
  },
103
108
  "file": {
104
109
  "uploading": "Uploading..."
@@ -88,7 +88,8 @@
88
88
  "runtime_error": "hay un error de ejecución",
89
89
  "space_not_working": "\"El Space no funciona porque\" {0}",
90
90
  "space_paused": "el space está pausado",
91
- "use_via_api": "Usar vía API"
91
+ "use_via_api": "Usar vía API",
92
+ "use_via_api_or_mcp": "Usar vía API o MCP"
92
93
  },
93
94
  "file": {
94
95
  "uploading": "Subiendo..."
@@ -89,7 +89,8 @@
89
89
  "runtime_error": "il y a une erreur d'exécution",
90
90
  "space_not_working": "\"Le Space ne fonctionne pas car\" {0}",
91
91
  "space_paused": "le Space est en pause",
92
- "use_via_api": "Utiliser via l'API"
92
+ "use_via_api": "Utiliser via API",
93
+ "use_via_api_or_mcp": "Utiliser via API ou MCP"
93
94
  },
94
95
  "file": {
95
96
  "uploading": "Téléchargement..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "ランタイムエラーがあります",
88
88
  "space_not_working": "\"Spaceが動作していません。理由:\" {0}",
89
89
  "space_paused": "Spaceが一時停止されています",
90
- "use_via_api": "APIを介して使用"
90
+ "use_via_api": "APIを介して使用",
91
+ "use_via_api_or_mcp": "APIまたはMCP経由で使用"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "アップロード中..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "런타임 오류가 있습니다",
88
88
  "space_not_working": "\"Space가 작동하지 않는 이유:\" {0}",
89
89
  "space_paused": "space가 일시 중지되었습니다",
90
- "use_via_api": "API를 통해 사용"
90
+ "use_via_api": "API를 통해 사용",
91
+ "use_via_api_or_mcp": "API 또는 MCP를 통해 사용"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "업로드 중..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "yra vykdymo klaida",
98
98
  "space_not_working": "\"Erdvė neveikia, nes\" {0}",
99
99
  "space_paused": "erdvė yra pristabdyta",
100
- "use_via_api": "Naudoti per API"
100
+ "use_via_api": "Naudoti per API",
101
+ "use_via_api_or_mcp": "Naudoti per API arba MCP"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "Įkeliama..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "Det er en kjøretidsfeil",
88
88
  "space_not_working": "\"Space fungerer ikke fordi\" {0}",
89
89
  "space_paused": "Space er pauset",
90
- "use_via_api": "Bruk via API"
90
+ "use_via_api": "Bruk via API",
91
+ "use_via_api_or_mcp": "Bruk via API eller MCP"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "Laster opp..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "Er is een runtime-fout",
98
98
  "space_not_working": "\"Space werkt niet omdat\" {0}",
99
99
  "space_paused": "De Space is gepauzeerd",
100
- "use_via_api": "Gebruik via API"
100
+ "use_via_api": "Gebruik via API",
101
+ "use_via_api_or_mcp": "Gebruik via API of MCP"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "Uploaden..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "Wystąpił błąd wykonania",
88
88
  "space_not_working": "\"Space nie działa, ponieważ\" {0}",
89
89
  "space_paused": "Space jest wstrzymany",
90
- "use_via_api": "Użyj przez API"
90
+ "use_via_api": "Użyj przez API",
91
+ "use_via_api_or_mcp": "Użyj przez API lub MCP"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "Przesyłanie..."
@@ -98,6 +98,7 @@
98
98
  "space_not_working": "\"O Space não está funcionando porque\" {0}",
99
99
  "space_paused": "O Space está pausado",
100
100
  "use_via_api": "Usar via API",
101
+ "use_via_api_or_mcp": "Usar via API ou MCP",
101
102
  "runtime_error": "Houve um erro de execução"
102
103
  },
103
104
  "file": {
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "Há um erro de execução",
88
88
  "space_not_working": "\"O Space não está a funcionar porque\" {0}",
89
89
  "space_paused": "O Space está em pausa",
90
- "use_via_api": "Utilizar via API"
90
+ "use_via_api": "Usar via API",
91
+ "use_via_api_or_mcp": "Usar via API ou MCP"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "A carregar..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "Există o eroare de execuție",
88
88
  "space_not_working": "\"Space-ul nu funcționează deoarece\" {0}",
89
89
  "space_paused": "Space-ul este în pauză",
90
- "use_via_api": "Utilizați prin API"
90
+ "use_via_api": "Utilizați prin API",
91
+ "use_via_api_or_mcp": "Utilizați prin API sau MCP"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "Se încarcă..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "Произошла ошибка выполнения",
88
88
  "space_not_working": "\"Space не работает, потому что\" {0}",
89
89
  "space_paused": "Space приостановлен",
90
- "use_via_api": "Использовать через API"
90
+ "use_via_api": "Использовать через API",
91
+ "use_via_api_or_mcp": "Использовать через API или MCP"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "Загрузка..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "Det finns ett körtidsfel",
88
88
  "space_not_working": "\"Space fungerar inte eftersom\" {0}",
89
89
  "space_paused": "Space är pausat",
90
- "use_via_api": "Använd via API"
90
+ "use_via_api": "Använd via API",
91
+ "use_via_api_or_mcp": "Använd via API eller MCP"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "Laddar upp..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "இயக்க நேர பிழை உள்ளது",
98
98
  "space_not_working": "\"Space செயல்படவில்லை ஏனெனில்\" {0}",
99
99
  "space_paused": "Space இடைநிறுத்தப்பட்டுள்ளது",
100
- "use_via_api": "API மூலம் பயன்படுத்தவும்"
100
+ "use_via_api": "API மூலம் பயன்படுத்தவும்",
101
+ "use_via_api_or_mcp": "API அல்லது MCP மூலம் பயன்படுத்தவும்"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "பதிவேற்றுகிறது..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "เกิดข้อผิดพลาดขณะทำงาน",
88
88
  "space_not_working": "\"Space ใช้งานไม่ได้เนื่องจาก\" {0}",
89
89
  "space_paused": "Space ถูกหยุดชั่วคราว",
90
- "use_via_api": "ใช้งานผ่าน API"
90
+ "use_via_api": "ใช้งานผ่าน API",
91
+ "use_via_api_or_mcp": "ใช้งานผ่าน API หรือ MCP"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "กำลังอัปโหลด..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "Bir çalışma zamanı hatası var",
98
98
  "space_not_working": "\"Space çalışmıyor çünkü\" {0}",
99
99
  "space_paused": "Space duraklatıldı",
100
- "use_via_api": "API üzerinden kullan"
100
+ "use_via_api": "API üzerinden kullan",
101
+ "use_via_api_or_mcp": "API veya MCP üzerinden kullan"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "Yükleniyor..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "Є помилка виконання",
98
98
  "space_not_working": "\"Space не працює, оскільки\" {0}",
99
99
  "space_paused": "Space призупинено",
100
- "use_via_api": "Використовувати через API"
100
+ "use_via_api": "Використовувати через API",
101
+ "use_via_api_or_mcp": "Використовувати через API або MCP"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "Завантаження..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "رن ٹائم میں خطا ہے",
98
98
  "space_not_working": "\"Space کام نہیں کر رہا ہے کیونکہ\" {0}",
99
99
  "space_paused": "Space موقوف ہے",
100
- "use_via_api": "API کے ذریعے استعمال کریں"
100
+ "use_via_api": "API کے ذریعے استعمال کریں",
101
+ "use_via_api_or_mcp": "API یا MCP کے ذریعے استعمال کریں"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "اپلوڈ ہو رہا ہے..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "Bajarilish vaqti xatosi mavjud",
98
98
  "space_not_working": "\"Space ishlamayapti, chunki\" {0}",
99
99
  "space_paused": "Space to'xtatilgan",
100
- "use_via_api": "API orqali foydalaning"
100
+ "use_via_api": "API orqali foydalaning",
101
+ "use_via_api_or_mcp": "API yoki MCP orqali foydalaning"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "Yuklanmoqda..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "存在运行时错误",
88
88
  "space_not_working": "\"Space 无法工作,原因:\" {0}",
89
89
  "space_paused": "Space 已暂停",
90
- "use_via_api": "通过 API 使用"
90
+ "use_via_api": "通过 API 使用",
91
+ "use_via_api_or_mcp": "通过 API 或 MCP 使用"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "正在上传..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "有執行時錯誤",
98
98
  "space_not_working": "\"Space 無法運作,因為\" {0}",
99
99
  "space_paused": "Space 已暫停",
100
- "use_via_api": "透過 API 使用"
100
+ "use_via_api": "透過 API 使用",
101
+ "use_via_api_or_mcp": "透過 API 或 MCP 使用"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "上傳中..."
@@ -0,0 +1,16 @@
1
+ import type { ToastMessage } from "@gradio/statustracker";
2
+ export declare function initialize(rootPath: string, add_new_message: (title: string, message: string, type: ToastMessage["type"]) => void, recordingStateCallback?: (isRecording: boolean) => void): void;
3
+ export declare function startRecording(): Promise<void>;
4
+ export declare function stopRecording(): void;
5
+ export declare function isCurrentlyRecording(): boolean;
6
+ export declare function markRemoveSegmentStart(): void;
7
+ export declare function markRemoveSegmentEnd(): void;
8
+ export declare function clearRemoveSegment(): void;
9
+ export declare function addZoomEffect(is_input: boolean, params: {
10
+ boundingBox: {
11
+ topLeft: [number, number];
12
+ bottomRight: [number, number];
13
+ };
14
+ duration?: number;
15
+ }): void;
16
+ export declare function zoom(is_input: boolean, elements: number[], duration?: number): void;
@@ -0,0 +1,255 @@
1
+ let isRecording = false;
2
+ let mediaRecorder = null;
3
+ let recordedChunks = [];
4
+ let recordingStartTime = 0;
5
+ let animationFrameId = null;
6
+ let removeSegment = {};
7
+ let root;
8
+ let add_message_callback;
9
+ let onRecordingStateChange = null;
10
+ let zoomEffects = [];
11
+ export function initialize(rootPath, add_new_message, recordingStateCallback) {
12
+ root = rootPath;
13
+ add_message_callback = add_new_message;
14
+ if (recordingStateCallback) {
15
+ onRecordingStateChange = recordingStateCallback;
16
+ }
17
+ }
18
+ export async function startRecording() {
19
+ if (isRecording) {
20
+ return;
21
+ }
22
+ try {
23
+ const originalTitle = document.title;
24
+ document.title = "[Sharing] Gradio Tab";
25
+ const stream = await navigator.mediaDevices.getDisplayMedia({
26
+ video: {
27
+ width: { ideal: 1920 },
28
+ height: { ideal: 1080 },
29
+ frameRate: { ideal: 30 }
30
+ },
31
+ audio: true,
32
+ selfBrowserSurface: "include"
33
+ });
34
+ document.title = originalTitle;
35
+ const options = {
36
+ videoBitsPerSecond: 5000000
37
+ };
38
+ mediaRecorder = new MediaRecorder(stream, options);
39
+ recordedChunks = [];
40
+ removeSegment = {};
41
+ mediaRecorder.ondataavailable = handleDataAvailable;
42
+ mediaRecorder.onstop = handleStop;
43
+ mediaRecorder.start(1000);
44
+ isRecording = true;
45
+ if (onRecordingStateChange) {
46
+ onRecordingStateChange(true);
47
+ }
48
+ recordingStartTime = Date.now();
49
+ }
50
+ catch (error) {
51
+ add_message_callback("Recording Error", "Failed to start recording: " + error.message, "error");
52
+ }
53
+ }
54
+ export function stopRecording() {
55
+ if (!isRecording || !mediaRecorder) {
56
+ return;
57
+ }
58
+ mediaRecorder.stop();
59
+ isRecording = false;
60
+ if (onRecordingStateChange) {
61
+ onRecordingStateChange(false);
62
+ }
63
+ }
64
+ export function isCurrentlyRecording() {
65
+ return isRecording;
66
+ }
67
+ export function markRemoveSegmentStart() {
68
+ if (!isRecording) {
69
+ return;
70
+ }
71
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
72
+ removeSegment.start = currentTime;
73
+ }
74
+ export function markRemoveSegmentEnd() {
75
+ if (!isRecording || removeSegment.start === undefined) {
76
+ return;
77
+ }
78
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
79
+ removeSegment.end = currentTime;
80
+ }
81
+ export function clearRemoveSegment() {
82
+ removeSegment = {};
83
+ }
84
+ export function addZoomEffect(is_input, params) {
85
+ if (!isRecording) {
86
+ return;
87
+ }
88
+ const FPS = 30;
89
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
90
+ const currentFrame = is_input
91
+ ? Math.floor((currentTime - 2) * FPS)
92
+ : Math.floor(currentTime * FPS);
93
+ if (params.boundingBox &&
94
+ params.boundingBox.topLeft &&
95
+ params.boundingBox.bottomRight &&
96
+ params.boundingBox.topLeft.length === 2 &&
97
+ params.boundingBox.bottomRight.length === 2) {
98
+ const newEffectDuration = params.duration || 2.0;
99
+ const newEffectEndFrame = currentFrame + Math.floor(newEffectDuration * FPS);
100
+ const hasOverlap = zoomEffects.some((existingEffect) => {
101
+ const existingEffectEndFrame = existingEffect.start_frame +
102
+ Math.floor((existingEffect.duration || 2.0) * FPS);
103
+ return ((currentFrame >= existingEffect.start_frame &&
104
+ currentFrame <= existingEffectEndFrame) ||
105
+ (newEffectEndFrame >= existingEffect.start_frame &&
106
+ newEffectEndFrame <= existingEffectEndFrame) ||
107
+ (currentFrame <= existingEffect.start_frame &&
108
+ newEffectEndFrame >= existingEffectEndFrame));
109
+ });
110
+ if (!hasOverlap) {
111
+ zoomEffects.push({
112
+ boundingBox: params.boundingBox,
113
+ start_frame: currentFrame,
114
+ duration: newEffectDuration
115
+ });
116
+ }
117
+ }
118
+ }
119
+ export function zoom(is_input, elements, duration = 2.0) {
120
+ if (!isRecording) {
121
+ return;
122
+ }
123
+ try {
124
+ setTimeout(() => {
125
+ if (!elements || elements.length === 0) {
126
+ return;
127
+ }
128
+ let minLeft = Infinity;
129
+ let minTop = Infinity;
130
+ let maxRight = 0;
131
+ let maxBottom = 0;
132
+ let foundElements = false;
133
+ for (const elementId of elements) {
134
+ const selector = `#component-${elementId}`;
135
+ const element = document.querySelector(selector);
136
+ if (element) {
137
+ foundElements = true;
138
+ const rect = element.getBoundingClientRect();
139
+ minLeft = Math.min(minLeft, rect.left);
140
+ minTop = Math.min(minTop, rect.top);
141
+ maxRight = Math.max(maxRight, rect.right);
142
+ maxBottom = Math.max(maxBottom, rect.bottom);
143
+ }
144
+ }
145
+ if (!foundElements) {
146
+ return;
147
+ }
148
+ const viewportWidth = window.innerWidth;
149
+ const viewportHeight = window.innerHeight;
150
+ const boxWidth = Math.min(maxRight, viewportWidth) - Math.max(0, minLeft);
151
+ const boxHeight = Math.min(maxBottom, viewportHeight) - Math.max(0, minTop);
152
+ const widthPercentage = boxWidth / viewportWidth;
153
+ const heightPercentage = boxHeight / viewportHeight;
154
+ if (widthPercentage >= 0.8 || heightPercentage >= 0.8) {
155
+ return;
156
+ }
157
+ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
158
+ let topLeft = [
159
+ Math.max(0, minLeft) / viewportWidth,
160
+ Math.max(0, minTop) / viewportHeight
161
+ ];
162
+ let bottomRight = [
163
+ Math.min(maxRight, viewportWidth) / viewportWidth,
164
+ Math.min(maxBottom, viewportHeight) / viewportHeight
165
+ ];
166
+ if (isSafari) {
167
+ topLeft[0] = Math.max(0, topLeft[0] * 0.9);
168
+ bottomRight[0] = Math.min(1, bottomRight[0] * 0.9);
169
+ const width = bottomRight[0] - topLeft[0];
170
+ const center = (topLeft[0] + bottomRight[0]) / 2;
171
+ const newCenter = center * 0.9;
172
+ topLeft[0] = Math.max(0, newCenter - width / 2);
173
+ bottomRight[0] = Math.min(1, newCenter + width / 2);
174
+ }
175
+ topLeft[0] = Math.max(0, topLeft[0]);
176
+ topLeft[1] = Math.max(0, topLeft[1]);
177
+ bottomRight[0] = Math.min(1, bottomRight[0]);
178
+ bottomRight[1] = Math.min(1, bottomRight[1]);
179
+ addZoomEffect(is_input, {
180
+ boundingBox: {
181
+ topLeft,
182
+ bottomRight
183
+ },
184
+ duration: duration
185
+ });
186
+ }, 300);
187
+ }
188
+ catch (error) {
189
+ // pass
190
+ }
191
+ }
192
+ function handleDataAvailable(event) {
193
+ if (event.data.size > 0) {
194
+ recordedChunks.push(event.data);
195
+ }
196
+ }
197
+ function handleStop() {
198
+ isRecording = false;
199
+ if (onRecordingStateChange) {
200
+ onRecordingStateChange(false);
201
+ }
202
+ const blob = new Blob(recordedChunks, {
203
+ type: "video/mp4"
204
+ });
205
+ handleRecordingComplete(blob);
206
+ const screenStream = mediaRecorder?.stream?.getTracks() || [];
207
+ screenStream.forEach((track) => track.stop());
208
+ if (animationFrameId !== null) {
209
+ cancelAnimationFrame(animationFrameId);
210
+ animationFrameId = null;
211
+ }
212
+ }
213
+ async function handleRecordingComplete(recordedBlob) {
214
+ try {
215
+ add_message_callback("Processing video", "This may take a few seconds...", "info");
216
+ const formData = new FormData();
217
+ formData.append("video", recordedBlob, "recording.mp4");
218
+ if (removeSegment.start !== undefined && removeSegment.end !== undefined) {
219
+ formData.append("remove_segment_start", removeSegment.start.toString());
220
+ formData.append("remove_segment_end", removeSegment.end.toString());
221
+ }
222
+ if (zoomEffects.length > 0) {
223
+ formData.append("zoom_effects", JSON.stringify(zoomEffects));
224
+ }
225
+ const response = await fetch(root + "/gradio_api/process_recording", {
226
+ method: "POST",
227
+ body: formData
228
+ });
229
+ if (!response.ok) {
230
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
231
+ }
232
+ const processedBlob = await response.blob();
233
+ const defaultFilename = `gradio-screen-recording-${new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "")}.mp4`;
234
+ saveWithDownloadAttribute(processedBlob, defaultFilename);
235
+ zoomEffects = [];
236
+ }
237
+ catch (error) {
238
+ add_message_callback("Processing Error", "Failed to process recording. Saving original version.", "warning");
239
+ const defaultFilename = `gradio-screen-recording-${new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "")}.mp4`;
240
+ saveWithDownloadAttribute(recordedBlob, defaultFilename);
241
+ }
242
+ }
243
+ function saveWithDownloadAttribute(blob, suggestedName) {
244
+ const url = URL.createObjectURL(blob);
245
+ const a = document.createElement("a");
246
+ a.style.display = "none";
247
+ a.href = url;
248
+ a.download = suggestedName;
249
+ document.body.appendChild(a);
250
+ a.click();
251
+ setTimeout(() => {
252
+ document.body.removeChild(a);
253
+ URL.revokeObjectURL(url);
254
+ }, 100);
255
+ }