Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
77ec09a
Add Vercel build workflow for CI/CD
athakur959-cloud Jul 25, 2026
87e743a
Update build and setup scripts in package.json
athakur959-cloud Jul 25, 2026
965e822
Add POST endpoint for image generation
athakur959-cloud Jul 25, 2026
82ae555
Add reference image support for image generation
athakur959-cloud Jul 25, 2026
d30a86e
Enhance POST route with new parameters and logic
athakur959-cloud Jul 25, 2026
9544bb8
Convert Home component to Studio with generation features
athakur959-cloud Jul 25, 2026
7a68ad0
Update dependencies in package.json
athakur959-cloud Jul 25, 2026
019e711
Update package.json with dependencies and scripts
athakur959-cloud Jul 25, 2026
08284f4
Remove build configuration from package.json
athakur959-cloud Jul 25, 2026
ebcca4a
Update package.json
athakur959-cloud Jul 25, 2026
6ca984f
Add vercel.json configuration file
athakur959-cloud Jul 25, 2026
14ac5fb
Clean up scripts in package.json
athakur959-cloud Jul 25, 2026
1259bf5
chore: update package.json and package-lock.json
athakur959-cloud Jul 25, 2026
643ede1
on branch main
athakur959-cloud Jul 25, 2026
8555250
Update buildCommand in vercel.json
athakur959-cloud Jul 25, 2026
c2fe8f9
Rename project and update package details
athakur959-cloud Jul 25, 2026
1bffd27
Update package name and dependencies
athakur959-cloud Jul 25, 2026
626d63d
Modify vercel.json to add build environment variables
athakur959-cloud Jul 25, 2026
ae47636
commit
athakur959-cloud Jul 25, 2026
9310215
Update Vercel configuration for build commands
athakur959-cloud Jul 26, 2026
81dfbdb
Fix JSON formatting in vercel.json
athakur959-cloud Jul 26, 2026
dcb5363
Fix missing newline at end of package.json
athakur959-cloud Jul 26, 2026
446e3c4
Reorder and update build scripts in package.json
athakur959-cloud Jul 26, 2026
bd879ed
Update Vercel configuration for Next.js project
athakur959-cloud Jul 26, 2026
d11967b
Update muapi.js
athakur959-cloud Jul 26, 2026
a5ccecb
Refactor HuggingFaceClient methods for clarity
athakur959-cloud Jul 26, 2026
14d122f
supabase
athakur959-cloud Jul 26, 2026
721e46a
supabase
athakur959-cloud Jul 26, 2026
1bdfa4a
commit
athakur959-cloud Jul 26, 2026
f0aa38f
commit
athakur959-cloud Jul 26, 2026
4ae3b3a
Commit
athakur959-cloud Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/vercel-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Vercel Build

on:
push:
branches:
- main
- master
- dev
pull_request:

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: true

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'

- name: Install dependencies
run: npm install

- name: Run setup script
run: npm run setup

- name: Build project
run: npm run build
working-directory: packages/studio

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: vercel-build
path: packages/studio/.next
81 changes: 81 additions & 0 deletions app/api/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { InferenceClient } from "@huggingface/inference";
import { toBufferFromPayload } from "../../../lib/binaryPayload";

const client = new InferenceClient(process.env.HF_TOKEN);

export async function POST(req: Request) {
try {
const { prompt, type, orientation, length, referenceAsset } = await req.json();

if (!prompt) {
return Response.json({ error: "Prompt is required" }, { status: 400 });
}

// Automatic Model & Dimension Selection based on your chosen parameters
let selectedModel = "";
let width = 1024;
let height = 1024;

// Handle Image Generation & Orientation mapping
if (type === 'image') {
selectedModel = "black-forest-labs/FLUX.1-dev";

if (orientation === 'portrait') {
width = 768;
height = 1344;
} else if (orientation === 'landscape') {
width = 1344;
height = 768;
}

const payload: any = {
model: selectedModel,
inputs: referenceAsset
? { prompt: `${prompt}, maintaining exact identical facial features, body structure, skintone, and curves`, image: referenceAsset }
: prompt,
parameters: {
width,
height,
num_inference_steps: 30,
nologo: true,
...(referenceAsset ? { strength: 0.65 } : {})
}
};

const imageBlob = referenceAsset
? await client.imageToImage(payload)
: await client.textToImage(payload);

const buffer = await toBufferFromPayload(imageBlob);
return Response.json({ success: true, data: `data:image/jpeg;base64,${buffer.toString("base64")}` });
}

// Handle Video Generation & Length mapping
if (type === 'video') {
// Automatically map video length to the ideal model pipeline
if (length === 'long') {
selectedModel = "tencent/HunyuanVideo";
} else {
selectedModel = "ali-vilab/text-to-video-ms-1.7b";
}

const videoBlob = await client.request({
model: selectedModel,
inputs: referenceAsset
? { prompt: `${prompt}, preserving identical face, features, figure, curves, and skintone`, video: referenceAsset }
: prompt,
parameters: {
nologo: true,
watermark: false,
}
});

const buffer = await toBufferFromPayload(videoBlob);
return Response.json({ success: true, data: `data:video/mp4;base64,${buffer.toString("base64")}` });
}

return Response.json({ error: "Invalid generation type selected" }, { status: 400 });
} catch (error: any) {
return Response.json({ error: error.message || "Generation failed" }, { status: 500 });
}
}
145 changes: 142 additions & 3 deletions app/page.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,144 @@
import { redirect } from 'next/navigation';
'use client';
import { useState } from 'react';

export default function Home() {
redirect('/studio');
export default function Studio() {
const [prompt, setPrompt] = useState('');
const [type, setType] = useState('image');
const [orientation, setOrientation] = useState('square');
const [length, setLength] = useState('short');
const [referenceAsset, setReferenceAsset] = useState(null);
const [output, setOutput] = useState(null);
const [loading, setLoading] = useState(false);

// Handle local reference file selection and convert to base64
function handleFileChange(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onloadend = () => {
setReferenceAsset(reader.result);
};
reader.readAsDataURL(file);
}

async function handleGeneration() {
if (!prompt.trim()) return;
setLoading(true);
setOutput(null);

try {
const res = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, type, orientation, length, referenceAsset }),
});

const data = await res.json();
if (data.success) {
setOutput(data.data);
} else {
alert(data.error || "Generation failed");
}
} catch (err) {
alert("An error occurred while connecting to the server.");
}
setLoading(false);
}

return (
<div style={{ maxWidth: '680px', margin: '40px auto', padding: '24px', background: '#16161e', color: '#fff', borderRadius: '12px', fontFamily: 'sans-serif', border: '1px solid #2d2d3d' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2 style={{ margin: 0, fontSize: '20px' }}>Open-Generative-AI Studio</h2>
<span style={{ background: '#2563eb', color: '#fff', padding: '4px 8px', borderRadius: '4px', fontSize: '11px', fontWeight: '600' }}>Identity Consistency Mode</span>
</div>

{/* Type Selector */}
<div style={{ marginBottom: '16px', display: 'flex', gap: '10px' }}>
<button
onClick={() => setType('image')}
style={{ flex: 1, padding: '10px', background: type === 'image' ? '#7c3aed' : '#0b0b0f', color: '#fff', border: '1px solid #2d2d3d', borderRadius: '6px', cursor: 'pointer', fontWeight: '600' }}
>
Image Mode
</button>
<button
onClick={() => setType('video')}
style={{ flex: 1, padding: '10px', background: type === 'video' ? '#7c3aed' : '#0b0b0f', color: '#fff', border: '1px solid #2d2d3d', borderRadius: '6px', cursor: 'pointer', fontWeight: '600' }}
>
Video Mode
</button>
</div>

{/* Conditional Options: Orientation for Images, Length for Videos */}
{type === 'image' ? (
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontWeight: '600', marginBottom: '6px', fontSize: '13px', color: '#a1a1aa' }}>Image Orientation</label>
<div style={{ display: 'flex', gap: '10px' }}>
{['square', 'portrait', 'landscape'].map((ori) => (
<button
key={ori}
onClick={() => setOrientation(ori)}
style={{ flex: 1, padding: '8px', background: orientation === ori ? '#2563eb' : '#0b0b0f', color: '#fff', border: '1px solid #2d2d3d', borderRadius: '6px', cursor: 'pointer', textTransform: 'capitalize', fontSize: '12px' }}
>
{ori}
</button>
))}
</div>
</div>
) : (
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontWeight: '600', marginBottom: '6px', fontSize: '13px', color: '#a1a1aa' }}>Video Duration / Length</label>
<div style={{ display: 'flex', gap: '10px' }}>
{['short', 'long'].map((len) => (
<button
key={len}
onClick={() => setLength(len)}
style={{ flex: 1, padding: '8px', background: length === len ? '#2563eb' : '#0b0b0f', color: '#fff', border: '1px solid #2d2d3d', borderRadius: '6px', cursor: 'pointer', textTransform: 'capitalize', fontSize: '12px' }}
>
{len}
</button>
))}
</div>
</div>
)}

{/* Reference Asset Uploader for Facial/Figure Consistency */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontWeight: '600', marginBottom: '6px', fontSize: '13px', color: '#a1a1aa' }}>Reference Asset (Identity & Feature Lock)</label>
<input
type="file"
accept="image/*,video/*"
onChange={handleFileChange}
style={{ width: '100%', padding: '8px', background: '#0b0b0f', border: '1px solid #2d2d3d', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>

{/* Prompt Input */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontWeight: '600', marginBottom: '6px', fontSize: '13px', color: '#a1a1aa' }}>Creative Prompt</label>
<textarea
rows={3}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe your scene, pose, or motion..."
style={{ width: '100%', padding: '10px', boxSizing: 'border-box', background: '#0b0b0f', border: '1px solid #2d2d3d', color: '#fff', borderRadius: '6px', fontSize: '14px' }}
/>
</div>

<button
onClick={handleGeneration}
disabled={loading}
style={{ width: '100%', background: '#7c3aed', color: '#fff', border: 'none', padding: '12px', borderRadius: '6px', fontSize: '14px', fontWeight: '600', cursor: 'pointer', marginBottom: '20px' }}
>
{loading ? `Synthesizing ${type}...` : `Generate ${type === 'image' ? 'Image' : 'Video'}`}
</button>

<label style={{ display: 'block', fontWeight: '600', marginBottom: '6px', fontSize: '13px', color: '#a1a1aa' }}>Output Panel</label>
<div style={{ padding: '16px', background: '#0b0b0f', border: '1px solid #2d2d3d', borderRadius: '6px', minHeight: '200px', display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }}>
{loading && <p style={{ color: '#a1a1aa' }}>Processing model pipeline with identity preservation...</p>}
{!loading && !output && <p style={{ color: '#52525b' }}>Ready for your selections...</p>}
{!loading && output && type === 'image' && <img src={output} alt="Generated output" style={{ maxWidth: '100%', maxHeight: '400px', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }} />}
{!loading && output && type === 'video' && <video src={output} controls autoPlay loop style={{ maxWidth: '100%', maxHeight: '400px', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }} />}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
Expand Down
21 changes: 21 additions & 0 deletions lib/binaryPayload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
async function toBufferFromPayload(payload) {
if (typeof Blob !== 'undefined' && payload instanceof Blob) {
return Buffer.from(await payload.arrayBuffer());
}

if (typeof payload === 'string') {
return Buffer.from(payload, 'utf8');
}

if (payload && typeof payload === 'object' && typeof payload.arrayBuffer === 'function') {
return Buffer.from(await payload.arrayBuffer());
}

if (payload && typeof payload === 'object' && typeof payload.buffer === 'object') {
return Buffer.from(payload.buffer);
}

return Buffer.from(payload);
}

module.exports = { toBufferFromPayload };
6 changes: 6 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Loading