+
{previewUrl ? (
isVideo ? (
)
) : (
- <>
- {icon}
- >
+
+
+
+ {fileName?.split('.').pop() || "AUD"}
+
+
)}
)}
@@ -291,6 +319,8 @@ export default function LipSyncStudio({
apiKey,
onGenerationComplete,
historyItems,
+ droppedFiles,
+ onFilesHandled,
}) {
const PERSIST_KEY = "hg_lipsync_studio_persistent";
@@ -513,6 +543,26 @@ export default function LipSyncStudio({
[apiKey],
);
+ // ── Handle Dropped Files ────────────────────────────────────────────────
+ useEffect(() => {
+ if (droppedFiles && droppedFiles.length > 0) {
+ const imageFiles = droppedFiles.filter(f => f.type.startsWith('image/'));
+ const videoFiles = droppedFiles.filter(f => f.type.startsWith('video/'));
+ const audioFiles = droppedFiles.filter(f => f.type.startsWith('audio/'));
+
+ if (audioFiles.length > 0) {
+ handleAudioPick(audioFiles[0]);
+ } else if (videoFiles.length > 0) {
+ switchToVideo();
+ handleVideoPick(videoFiles[0]);
+ } else if (imageFiles.length > 0) {
+ switchToImage();
+ handleImageUpload(imageFiles[0]);
+ }
+ onFilesHandled?.();
+ }
+ }, [droppedFiles, onFilesHandled, handleAudioPick, handleVideoPick, handleImageUpload]);
+
// ── Mode toggle ─────────────────────────────────────────────────────────
const switchToImage = () => {
if (inputMode === "image") return;
diff --git a/packages/studio/src/components/VideoStudio.jsx b/packages/studio/src/components/VideoStudio.jsx
index 6cd99a7..55a4d06 100644
--- a/packages/studio/src/components/VideoStudio.jsx
+++ b/packages/studio/src/components/VideoStudio.jsx
@@ -235,6 +235,8 @@ export default function VideoStudio({
apiKey,
onGenerationComplete,
historyItems,
+ droppedFiles,
+ onFilesHandled,
}) {
const PERSIST_KEY = "hg_video_studio_persistent";
@@ -444,6 +446,19 @@ export default function VideoStudio({
}
}, [applyControlsForModel, defaultModel.id]);
+ // ── Adjust height on load ────────────────────────────────────────────────
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (textareaRef.current) {
+ const el = textareaRef.current;
+ el.style.height = "auto";
+ const maxH = window.innerWidth < 768 ? 150 : 250;
+ el.style.height = Math.min(el.scrollHeight, maxH) + "px";
+ }
+ }, 150);
+ return () => clearTimeout(timer);
+ }, []);
+
// ── Persistence: Save ────────────────────────────────────────────────────
useEffect(() => {
const timer = setTimeout(() => {
@@ -487,6 +502,86 @@ export default function VideoStudio({
localHistory,
]);
+ // ── Derived UI values ────────────────────────────────────────────────────
+
+ const processDroppedImage = async (file) => {
+ if (file.size > 10 * 1024 * 1024) {
+ alert("Image exceeds 10MB limit.");
+ return;
+ }
+ setImageUploading(true);
+ setImageProgress(0);
+ try {
+ const url = await uploadFile(apiKey, file, (pct) => {
+ setImageProgress(pct);
+ });
+ setUploadedImageUrl(url);
+ setUploadedVideoUrl(null);
+ setUploadedVideoName(null);
+ setV2vMode(false);
+ if (!imageMode) {
+ const firstI2V = i2vModels[0];
+ setImageMode(true);
+ setSelectedModel(firstI2V.id);
+ setSelectedModelName(firstI2V.name);
+ applyControlsForModel(firstI2V.id, true, false);
+ }
+ setPromptDisabled(false);
+ } catch (err) {
+ alert(`Image upload failed: ${err.message}`);
+ } finally {
+ setImageUploading(false);
+ setImageProgress(0);
+ }
+ };
+
+ const processDroppedVideo = async (file) => {
+ if (file.size > 50 * 1024 * 1024) {
+ alert("Video exceeds 50MB limit.");
+ return;
+ }
+ setVideoUploading(true);
+ setVideoProgress(0);
+ try {
+ const url = await uploadFile(apiKey, file, (pct) => {
+ setVideoProgress(pct);
+ });
+ setUploadedVideoUrl(url);
+ setUploadedVideoName(file.name);
+ if (imageMode) {
+ setUploadedImageUrl(null);
+ setImageMode(false);
+ }
+ setV2vMode(true);
+ const firstV2V = v2vModels[0];
+ setSelectedModel(firstV2V.id);
+ setSelectedModelName(firstV2V.name);
+ applyControlsForModel(firstV2V.id, false, true);
+ setPrompt("");
+ setPromptDisabled(true);
+ } catch (err) {
+ alert(`Video upload failed: ${err.message}`);
+ } finally {
+ setVideoUploading(false);
+ setVideoProgress(0);
+ }
+ };
+
+ // ── Handle Dropped Files ────────────────────────────────────────────────
+ useEffect(() => {
+ if (droppedFiles && droppedFiles.length > 0) {
+ const imageFiles = droppedFiles.filter(f => f.type.startsWith('image/'));
+ const videoFiles = droppedFiles.filter(f => f.type.startsWith('video/'));
+
+ if (videoFiles.length > 0) {
+ processDroppedVideo(videoFiles[0]);
+ } else if (imageFiles.length > 0) {
+ processDroppedImage(imageFiles[0]);
+ }
+ onFilesHandled?.();
+ }
+ }, [droppedFiles, onFilesHandled, processDroppedImage, processDroppedVideo]);
+
// Initialise controls for default model on mount
useEffect(() => {
if (hasRestored.current) return;
@@ -910,7 +1005,7 @@ export default function VideoStudio({
return (