@aivue/360-spin 1.0.2 → 2.0.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.
- package/CHANGELOG.md +29 -0
- package/README.md +126 -4
- package/dist/360-spin.css +1 -1
- package/dist/components/Ai360Generator.vue.d.ts +26 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +674 -168
- package/dist/index.mjs.map +1 -1
- package/dist/types/index.d.ts +109 -0
- package/dist/utils/ai-generator.d.ts +45 -0
- package/package.json +2 -3
package/dist/index.mjs
CHANGED
|
@@ -1,100 +1,103 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
var W = Object.defineProperty;
|
|
2
|
+
var Q = (u, a, e) => a in u ? W(u, a, { enumerable: !0, configurable: !0, writable: !0, value: e }) : u[a] = e;
|
|
3
|
+
var O = (u, a, e) => Q(u, typeof a != "symbol" ? a + "" : a, e);
|
|
4
|
+
import { ref as f, computed as F, onUnmounted as Y, defineComponent as j, onMounted as Z, createElementBlock as _, openBlock as y, normalizeStyle as X, normalizeClass as G, unref as k, createCommentVNode as C, withDirectives as U, createElementVNode as r, toDisplayString as D, vShow as q, vModelText as ee, withModifiers as R, vModelSelect as N, createVNode as ae, Fragment as te, renderList as re } from "vue";
|
|
5
|
+
function oe(u, a) {
|
|
6
|
+
const e = f(!1), l = f(!0), n = f(0), i = f(null), s = f([]), v = f(!1), c = f(0), m = f(0), p = F(() => u.mode && u.mode !== "auto" ? u.mode : Array.isArray(u.animatedImage) ? "frames" : "gif"), S = F(() => p.value === "gif" && typeof u.animatedImage == "string" ? u.animatedImage : ""), B = F(() => {
|
|
7
|
+
if (p.value === "frames" && Array.isArray(u.animatedImage)) {
|
|
8
|
+
const d = u.animatedImage, w = n.value % d.length;
|
|
9
|
+
return d[w];
|
|
7
10
|
}
|
|
8
11
|
return "";
|
|
9
|
-
}), T =
|
|
10
|
-
async function
|
|
11
|
-
|
|
12
|
+
}), T = F(() => Array.isArray(u.animatedImage) ? u.animatedImage : []), h = F(() => T.value.length);
|
|
13
|
+
async function L() {
|
|
14
|
+
l.value = !0;
|
|
12
15
|
try {
|
|
13
|
-
if (
|
|
14
|
-
await S
|
|
15
|
-
else if (
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
if (p.value === "gif")
|
|
17
|
+
await $(S.value);
|
|
18
|
+
else if (p.value === "frames") {
|
|
19
|
+
const d = T.value.map((b) => $(b)), w = await Promise.all(d);
|
|
20
|
+
s.value = w;
|
|
18
21
|
}
|
|
19
|
-
await
|
|
20
|
-
} catch (
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function
|
|
25
|
-
return new Promise((
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
clearTimeout(
|
|
31
|
-
},
|
|
32
|
-
clearTimeout(
|
|
33
|
-
},
|
|
22
|
+
await $(u.staticImage), l.value = !1, a("loaded");
|
|
23
|
+
} catch (d) {
|
|
24
|
+
l.value = !1, a("error", d);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function $(d, w = 5e3) {
|
|
28
|
+
return new Promise((b, I) => {
|
|
29
|
+
const A = new Image(), o = setTimeout(() => {
|
|
30
|
+
I(new Error(`Image load timeout: ${d}`));
|
|
31
|
+
}, w);
|
|
32
|
+
A.onload = () => {
|
|
33
|
+
clearTimeout(o), b(A);
|
|
34
|
+
}, A.onerror = () => {
|
|
35
|
+
clearTimeout(o), I(new Error(`Failed to load image: ${d}`));
|
|
36
|
+
}, A.src = d;
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
39
|
+
function x() {
|
|
40
|
+
e.value || l.value || (e.value = !0, a("animation-start"), p.value === "frames" && z());
|
|
38
41
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
42
|
+
function E() {
|
|
43
|
+
e.value && (e.value = !1, a("animation-end"), i.value !== null && (cancelAnimationFrame(i.value), i.value = null), p.value === "frames" && (n.value = 0));
|
|
41
44
|
}
|
|
42
|
-
function
|
|
43
|
-
if (
|
|
44
|
-
const
|
|
45
|
-
let
|
|
46
|
-
function
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
45
|
+
function z() {
|
|
46
|
+
if (h.value === 0) return;
|
|
47
|
+
const d = 1e3 / (u.frameRate || 30);
|
|
48
|
+
let w = Date.now();
|
|
49
|
+
function b() {
|
|
50
|
+
const I = Date.now();
|
|
51
|
+
if (I - w >= d && (u.direction === "clockwise" ? n.value = (n.value + 1) % h.value : n.value = n.value === 0 ? h.value - 1 : n.value - 1, a("frame-change", n.value), w = I, !u.loop && n.value === 0)) {
|
|
52
|
+
E();
|
|
50
53
|
return;
|
|
51
54
|
}
|
|
52
|
-
|
|
55
|
+
e.value && (i.value = requestAnimationFrame(b));
|
|
53
56
|
}
|
|
54
|
-
|
|
57
|
+
i.value = requestAnimationFrame(b);
|
|
55
58
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
59
|
+
function V(d) {
|
|
60
|
+
p.value !== "frames" || h.value === 0 || (v.value = !0, m.value = n.value, d instanceof TouchEvent ? c.value = d.touches[0].clientX : c.value = d.clientX, e.value && E());
|
|
58
61
|
}
|
|
59
|
-
function
|
|
60
|
-
if (!v.value ||
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
let
|
|
66
|
-
for (;
|
|
67
|
-
for (;
|
|
68
|
-
|
|
62
|
+
function M(d) {
|
|
63
|
+
if (!v.value || p.value !== "frames") return;
|
|
64
|
+
d.preventDefault();
|
|
65
|
+
let w;
|
|
66
|
+
d instanceof TouchEvent ? w = d.touches[0].clientX : w = d.clientX;
|
|
67
|
+
const b = w - c.value, I = u.dragSensitivity || 10, A = Math.floor(b / I);
|
|
68
|
+
let o = m.value + A;
|
|
69
|
+
for (; o < 0; ) o += h.value;
|
|
70
|
+
for (; o >= h.value; ) o -= h.value;
|
|
71
|
+
o !== n.value && (n.value = o, a("frame-change", n.value));
|
|
69
72
|
}
|
|
70
|
-
function
|
|
73
|
+
function K(d) {
|
|
71
74
|
v.value = !1;
|
|
72
75
|
}
|
|
73
|
-
return
|
|
74
|
-
|
|
76
|
+
return Y(() => {
|
|
77
|
+
i.value !== null && cancelAnimationFrame(i.value);
|
|
75
78
|
}), {
|
|
76
|
-
isAnimating:
|
|
77
|
-
isLoading:
|
|
78
|
-
currentFrameIndex:
|
|
79
|
-
currentMode:
|
|
80
|
-
animatedImageUrl:
|
|
81
|
-
currentFrameUrl:
|
|
82
|
-
totalFrames:
|
|
83
|
-
startAnimation:
|
|
84
|
-
stopAnimation:
|
|
85
|
-
preloadImages:
|
|
86
|
-
handleDragStart:
|
|
87
|
-
handleDragMove:
|
|
88
|
-
handleDragEnd:
|
|
79
|
+
isAnimating: e,
|
|
80
|
+
isLoading: l,
|
|
81
|
+
currentFrameIndex: n,
|
|
82
|
+
currentMode: p,
|
|
83
|
+
animatedImageUrl: S,
|
|
84
|
+
currentFrameUrl: B,
|
|
85
|
+
totalFrames: h,
|
|
86
|
+
startAnimation: x,
|
|
87
|
+
stopAnimation: E,
|
|
88
|
+
preloadImages: L,
|
|
89
|
+
handleDragStart: V,
|
|
90
|
+
handleDragMove: M,
|
|
91
|
+
handleDragEnd: K
|
|
89
92
|
};
|
|
90
93
|
}
|
|
91
|
-
const
|
|
94
|
+
const ne = {
|
|
92
95
|
key: 0,
|
|
93
96
|
class: "ai-360-spin__loading"
|
|
94
|
-
},
|
|
97
|
+
}, ie = { class: "ai-360-spin__loading-text" }, se = ["src", "alt"], le = ["src", "alt"], ue = ["src", "alt"], ce = {
|
|
95
98
|
key: 3,
|
|
96
99
|
class: "ai-360-spin__hint"
|
|
97
|
-
},
|
|
100
|
+
}, de = /* @__PURE__ */ j({
|
|
98
101
|
__name: "Ai360Spin",
|
|
99
102
|
props: {
|
|
100
103
|
staticImage: {},
|
|
@@ -118,132 +121,635 @@ const G = {
|
|
|
118
121
|
dragSensitivity: { default: 10 }
|
|
119
122
|
},
|
|
120
123
|
emits: ["animation-start", "animation-end", "loaded", "error", "frame-change"],
|
|
121
|
-
setup(
|
|
122
|
-
const
|
|
123
|
-
isAnimating:
|
|
124
|
-
isLoading:
|
|
124
|
+
setup(u, { emit: a }) {
|
|
125
|
+
const e = u, l = a, n = f(null), {
|
|
126
|
+
isAnimating: i,
|
|
127
|
+
isLoading: s,
|
|
125
128
|
currentMode: v,
|
|
126
|
-
animatedImageUrl:
|
|
127
|
-
currentFrameUrl:
|
|
128
|
-
startAnimation:
|
|
129
|
-
stopAnimation:
|
|
130
|
-
preloadImages:
|
|
129
|
+
animatedImageUrl: c,
|
|
130
|
+
currentFrameUrl: m,
|
|
131
|
+
startAnimation: p,
|
|
132
|
+
stopAnimation: S,
|
|
133
|
+
preloadImages: B,
|
|
131
134
|
handleDragStart: T,
|
|
132
|
-
handleDragMove:
|
|
133
|
-
handleDragEnd:
|
|
134
|
-
} =
|
|
135
|
-
width: typeof
|
|
136
|
-
height: typeof
|
|
137
|
-
})),
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
function U() {
|
|
142
|
-
a.trigger === "hover" && I();
|
|
135
|
+
handleDragMove: h,
|
|
136
|
+
handleDragEnd: L
|
|
137
|
+
} = oe(e, l), $ = F(() => ({
|
|
138
|
+
width: typeof e.width == "number" ? `${e.width}px` : e.width,
|
|
139
|
+
height: typeof e.height == "number" ? `${e.height}px` : e.height
|
|
140
|
+
})), x = F(() => e.trigger === "hover" && !i.value), E = F(() => "Hover to spin");
|
|
141
|
+
function z() {
|
|
142
|
+
e.trigger === "hover" && p();
|
|
143
143
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
144
|
+
function V() {
|
|
145
|
+
e.trigger === "hover" && S();
|
|
146
146
|
}
|
|
147
|
-
function
|
|
148
|
-
|
|
147
|
+
function M() {
|
|
148
|
+
e.trigger === "click" && (i.value ? S() : p());
|
|
149
149
|
}
|
|
150
|
-
function
|
|
151
|
-
|
|
150
|
+
function K(o) {
|
|
151
|
+
e.enableDragSpin && v.value === "frames" ? T(o) : e.trigger === "click" && M();
|
|
152
152
|
}
|
|
153
|
-
function
|
|
154
|
-
|
|
153
|
+
function d(o) {
|
|
154
|
+
e.enableDragSpin && v.value === "frames" && h(o);
|
|
155
155
|
}
|
|
156
|
-
function
|
|
157
|
-
|
|
156
|
+
function w(o) {
|
|
157
|
+
e.enableDragSpin && v.value === "frames" && L(o);
|
|
158
158
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
159
|
+
function b() {
|
|
160
|
+
console.log("[Ai360Spin] Static image loaded, clearing loading state"), s.value && (s.value = !1), console.log("[Ai360Spin] isLoading after static load:", s.value);
|
|
161
161
|
}
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
console.error("Image failed to load:", (w = e.target) == null ? void 0 : w.src), l.value && (l.value = !1), c("error", new Error("Failed to load image"));
|
|
162
|
+
function I() {
|
|
163
|
+
l("loaded");
|
|
165
164
|
}
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
function A(o) {
|
|
166
|
+
var t;
|
|
167
|
+
console.error("Image failed to load:", (t = o.target) == null ? void 0 : t.src), s.value && (s.value = !1), l("error", new Error("Failed to load image"));
|
|
168
|
+
}
|
|
169
|
+
return Z(async () => {
|
|
170
|
+
if (console.log("[Ai360Spin] onMounted - preload:", e.preload, "isLoading:", s.value), e.preload)
|
|
168
171
|
try {
|
|
169
|
-
await
|
|
170
|
-
} catch (
|
|
171
|
-
console.error("[Ai360Spin] Preload failed:",
|
|
172
|
+
await B();
|
|
173
|
+
} catch (o) {
|
|
174
|
+
console.error("[Ai360Spin] Preload failed:", o), s.value = !1;
|
|
172
175
|
}
|
|
173
176
|
else
|
|
174
|
-
console.log("[Ai360Spin] Preload disabled, clearing loading state"),
|
|
175
|
-
|
|
176
|
-
}), (
|
|
177
|
+
console.log("[Ai360Spin] Preload disabled, clearing loading state"), s.value = !1, console.log("[Ai360Spin] isLoading after clear:", s.value);
|
|
178
|
+
e.trigger === "auto" && p();
|
|
179
|
+
}), (o, t) => (y(), _("div", {
|
|
177
180
|
ref_key: "containerRef",
|
|
178
|
-
ref:
|
|
179
|
-
class:
|
|
180
|
-
style:
|
|
181
|
-
onMouseenter:
|
|
182
|
-
onMouseleave:
|
|
183
|
-
onClick:
|
|
184
|
-
onTouchstart:
|
|
185
|
-
onTouchmove:
|
|
186
|
-
onTouchend:
|
|
181
|
+
ref: n,
|
|
182
|
+
class: G(["ai-360-spin", o.containerClass, { "ai-360-spin--animating": k(i), "ai-360-spin--loading": k(s) }]),
|
|
183
|
+
style: X($.value),
|
|
184
|
+
onMouseenter: z,
|
|
185
|
+
onMouseleave: V,
|
|
186
|
+
onClick: M,
|
|
187
|
+
onTouchstart: K,
|
|
188
|
+
onTouchmove: d,
|
|
189
|
+
onTouchend: w
|
|
187
190
|
}, [
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
k(s) && o.showLoading ? (y(), _("div", ne, [
|
|
192
|
+
t[0] || (t[0] = r("div", { class: "ai-360-spin__spinner" }, null, -1)),
|
|
193
|
+
r("p", ie, D(o.loadingText), 1)
|
|
191
194
|
])) : C("", !0),
|
|
192
|
-
|
|
193
|
-
src:
|
|
194
|
-
alt:
|
|
195
|
-
class:
|
|
196
|
-
onLoad:
|
|
197
|
-
onError:
|
|
198
|
-
}, null, 42,
|
|
199
|
-
[
|
|
195
|
+
U(r("img", {
|
|
196
|
+
src: o.staticImage,
|
|
197
|
+
alt: o.alt,
|
|
198
|
+
class: G(["ai-360-spin__image", "ai-360-spin__image--static", o.imageClass]),
|
|
199
|
+
onLoad: b,
|
|
200
|
+
onError: A
|
|
201
|
+
}, null, 42, se), [
|
|
202
|
+
[q, !k(i) && !k(s)]
|
|
200
203
|
]),
|
|
201
|
-
|
|
204
|
+
k(v) === "gif" && !k(s) ? U((y(), _("img", {
|
|
202
205
|
key: 1,
|
|
203
|
-
src:
|
|
204
|
-
alt:
|
|
205
|
-
class:
|
|
206
|
-
onLoad:
|
|
207
|
-
onError:
|
|
208
|
-
}, null, 42,
|
|
209
|
-
[
|
|
206
|
+
src: k(c),
|
|
207
|
+
alt: o.alt,
|
|
208
|
+
class: G(["ai-360-spin__image", "ai-360-spin__image--animated", o.imageClass]),
|
|
209
|
+
onLoad: I,
|
|
210
|
+
onError: A
|
|
211
|
+
}, null, 42, le)), [
|
|
212
|
+
[q, k(i)]
|
|
210
213
|
]) : C("", !0),
|
|
211
|
-
|
|
214
|
+
k(v) === "frames" && !k(s) ? U((y(), _("img", {
|
|
212
215
|
key: 2,
|
|
213
|
-
src:
|
|
214
|
-
alt:
|
|
215
|
-
class:
|
|
216
|
-
}, null, 10,
|
|
217
|
-
[
|
|
216
|
+
src: k(m),
|
|
217
|
+
alt: o.alt,
|
|
218
|
+
class: G(["ai-360-spin__image", "ai-360-spin__image--frame", o.imageClass])
|
|
219
|
+
}, null, 10, ue)), [
|
|
220
|
+
[q, k(i)]
|
|
218
221
|
]) : C("", !0),
|
|
219
|
-
!
|
|
220
|
-
|
|
222
|
+
!k(i) && !k(s) && x.value ? (y(), _("div", ce, [
|
|
223
|
+
t[1] || (t[1] = r("svg", {
|
|
221
224
|
class: "ai-360-spin__hint-icon",
|
|
222
225
|
viewBox: "0 0 24 24",
|
|
223
226
|
fill: "none",
|
|
224
227
|
stroke: "currentColor"
|
|
225
228
|
}, [
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
r("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
|
|
230
|
+
r("path", { d: "M2 17l10 5 10-5" }),
|
|
231
|
+
r("path", { d: "M2 12l10 5 10-5" })
|
|
229
232
|
], -1)),
|
|
230
|
-
|
|
233
|
+
r("span", null, D(E.value), 1)
|
|
231
234
|
])) : C("", !0)
|
|
232
235
|
], 38));
|
|
233
236
|
}
|
|
234
|
-
}),
|
|
235
|
-
const
|
|
236
|
-
for (const [
|
|
237
|
-
|
|
238
|
-
return
|
|
239
|
-
},
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
}), H = (u, a) => {
|
|
238
|
+
const e = u.__vccOpts || u;
|
|
239
|
+
for (const [l, n] of a)
|
|
240
|
+
e[l] = n;
|
|
241
|
+
return e;
|
|
242
|
+
}, J = /* @__PURE__ */ H(de, [["__scopeId", "data-v-915b2a52"]]);
|
|
243
|
+
class ge {
|
|
244
|
+
constructor(a, e) {
|
|
245
|
+
O(this, "config");
|
|
246
|
+
O(this, "onProgress");
|
|
247
|
+
this.config = {
|
|
248
|
+
provider: a.provider || "openai",
|
|
249
|
+
apiKey: a.apiKey,
|
|
250
|
+
frameCount: a.frameCount || 36,
|
|
251
|
+
backgroundColor: a.backgroundColor || "white",
|
|
252
|
+
customBackgroundColor: a.customBackgroundColor || "#ffffff",
|
|
253
|
+
quality: a.quality || 80,
|
|
254
|
+
imageSize: a.imageSize || "1024x1024",
|
|
255
|
+
model: a.model || (a.provider === "openai" ? "dall-e-3" : "stable-diffusion-xl-1024-v1-0"),
|
|
256
|
+
useVisionAnalysis: a.useVisionAnalysis !== !1,
|
|
257
|
+
promptTemplate: a.promptTemplate || ""
|
|
258
|
+
}, this.onProgress = e;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Generate 360-degree frames from a single product image
|
|
262
|
+
*/
|
|
263
|
+
async generate(a) {
|
|
264
|
+
const e = Date.now(), l = [];
|
|
265
|
+
try {
|
|
266
|
+
const n = typeof a == "string" ? a : await this.fileToBase64(a);
|
|
267
|
+
this.updateProgress(0, "Analyzing product image...");
|
|
268
|
+
let i = "";
|
|
269
|
+
this.config.useVisionAnalysis && this.config.provider === "openai" ? i = await this.analyzeProduct(n) : i = "Product image", this.updateProgress(10, "Product analyzed. Generating frames...");
|
|
270
|
+
const s = 360 / this.config.frameCount;
|
|
271
|
+
for (let c = 0; c < this.config.frameCount; c++) {
|
|
272
|
+
const m = Math.round(c * s), p = 10 + Math.round(c / this.config.frameCount * 85);
|
|
273
|
+
this.updateProgress(p, `Generating frame ${c + 1}/${this.config.frameCount} (${m}°)...`, l);
|
|
274
|
+
const S = await this.generateFrame(i, m, c);
|
|
275
|
+
l.push(S);
|
|
276
|
+
}
|
|
277
|
+
this.updateProgress(100, "Generation complete!", l);
|
|
278
|
+
const v = Date.now() - e;
|
|
279
|
+
return {
|
|
280
|
+
frames: l,
|
|
281
|
+
productDescription: i,
|
|
282
|
+
metadata: {
|
|
283
|
+
provider: this.config.provider,
|
|
284
|
+
frameCount: this.config.frameCount,
|
|
285
|
+
generationTime: v,
|
|
286
|
+
model: this.config.model
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
} catch (n) {
|
|
290
|
+
throw console.error("AI 360 Generation error:", n), n;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Analyze product image using GPT-4 Vision
|
|
295
|
+
*/
|
|
296
|
+
async analyzeProduct(a) {
|
|
297
|
+
var n, i;
|
|
298
|
+
const e = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: {
|
|
301
|
+
"Content-Type": "application/json",
|
|
302
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
303
|
+
},
|
|
304
|
+
body: JSON.stringify({
|
|
305
|
+
model: "gpt-4o",
|
|
306
|
+
messages: [
|
|
307
|
+
{
|
|
308
|
+
role: "user",
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: "Analyze this product image and provide a detailed description including: product type, color, material, key features, and style. Be concise but specific. This will be used to generate 360-degree views."
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
type: "image_url",
|
|
316
|
+
image_url: { url: a }
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
],
|
|
321
|
+
max_tokens: 300
|
|
322
|
+
})
|
|
323
|
+
});
|
|
324
|
+
if (!e.ok)
|
|
325
|
+
throw new Error(`Vision API error: ${e.statusText}`);
|
|
326
|
+
return ((i = (n = (await e.json()).choices[0]) == null ? void 0 : n.message) == null ? void 0 : i.content) || "Product";
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Generate a single frame at a specific angle
|
|
330
|
+
*/
|
|
331
|
+
async generateFrame(a, e, l) {
|
|
332
|
+
return this.config.provider === "openai" ? this.generateFrameOpenAI(a, e, l) : this.generateFrameStability(a, e, l);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Generate frame using OpenAI DALL-E
|
|
336
|
+
*/
|
|
337
|
+
async generateFrameOpenAI(a, e, l) {
|
|
338
|
+
var m;
|
|
339
|
+
const n = this.getBackgroundColorValue(), i = this.config.promptTemplate || `Create a high-quality product photograph of: ${a}
|
|
340
|
+
View angle: ${e} degrees rotation (0° is front view, rotating clockwise around vertical axis)
|
|
341
|
+
Background: ${n}
|
|
342
|
+
Style: Professional product photography, studio lighting, high detail, sharp focus, centered composition
|
|
343
|
+
The product should be clearly visible and well-lit from this ${e}° angle.`, s = await fetch("https://api.openai.com/v1/images/generations", {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
"Content-Type": "application/json",
|
|
347
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
348
|
+
},
|
|
349
|
+
body: JSON.stringify({
|
|
350
|
+
model: this.config.model,
|
|
351
|
+
prompt: i.substring(0, 4e3),
|
|
352
|
+
// DALL-E has prompt limits
|
|
353
|
+
n: 1,
|
|
354
|
+
size: this.config.imageSize,
|
|
355
|
+
quality: "hd",
|
|
356
|
+
style: "natural"
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
if (!s.ok)
|
|
360
|
+
throw new Error(`OpenAI API error: ${s.statusText}`);
|
|
361
|
+
const c = (m = (await s.json()).data[0]) == null ? void 0 : m.url;
|
|
362
|
+
if (!c)
|
|
363
|
+
throw new Error("No image URL returned from OpenAI");
|
|
364
|
+
return this.urlToBase64(c);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Generate frame using Stability AI
|
|
368
|
+
*/
|
|
369
|
+
async generateFrameStability(a, e, l) {
|
|
370
|
+
var m;
|
|
371
|
+
const n = this.getBackgroundColorValue(), i = this.config.promptTemplate || `Professional product photograph of ${a}, viewed from ${e} degrees angle, ${n} background, studio lighting, high quality, detailed, centered`, s = await fetch("https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image", {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: {
|
|
374
|
+
"Content-Type": "application/json",
|
|
375
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
376
|
+
Accept: "application/json"
|
|
377
|
+
},
|
|
378
|
+
body: JSON.stringify({
|
|
379
|
+
text_prompts: [
|
|
380
|
+
{
|
|
381
|
+
text: i,
|
|
382
|
+
weight: 1
|
|
383
|
+
}
|
|
384
|
+
],
|
|
385
|
+
cfg_scale: 7,
|
|
386
|
+
height: 1024,
|
|
387
|
+
width: 1024,
|
|
388
|
+
samples: 1,
|
|
389
|
+
steps: 30
|
|
390
|
+
})
|
|
391
|
+
});
|
|
392
|
+
if (!s.ok)
|
|
393
|
+
throw new Error(`Stability AI error: ${s.statusText}`);
|
|
394
|
+
const c = (m = (await s.json()).artifacts[0]) == null ? void 0 : m.base64;
|
|
395
|
+
if (!c)
|
|
396
|
+
throw new Error("No image returned from Stability AI");
|
|
397
|
+
return `data:image/png;base64,${c}`;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get background color value based on config
|
|
401
|
+
*/
|
|
402
|
+
getBackgroundColorValue() {
|
|
403
|
+
switch (this.config.backgroundColor) {
|
|
404
|
+
case "white":
|
|
405
|
+
return "pure white";
|
|
406
|
+
case "transparent":
|
|
407
|
+
return "transparent/alpha channel";
|
|
408
|
+
case "black":
|
|
409
|
+
return "pure black";
|
|
410
|
+
case "custom":
|
|
411
|
+
return this.config.customBackgroundColor;
|
|
412
|
+
default:
|
|
413
|
+
return "white";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Convert File to base64
|
|
418
|
+
*/
|
|
419
|
+
fileToBase64(a) {
|
|
420
|
+
return new Promise((e, l) => {
|
|
421
|
+
const n = new FileReader();
|
|
422
|
+
n.onload = () => e(n.result), n.onerror = l, n.readAsDataURL(a);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Convert URL to base64
|
|
427
|
+
*/
|
|
428
|
+
async urlToBase64(a) {
|
|
429
|
+
try {
|
|
430
|
+
const l = await (await fetch(a)).blob();
|
|
431
|
+
return new Promise((n, i) => {
|
|
432
|
+
const s = new FileReader();
|
|
433
|
+
s.onload = () => n(s.result), s.onerror = i, s.readAsDataURL(l);
|
|
434
|
+
});
|
|
435
|
+
} catch (e) {
|
|
436
|
+
return console.error("Error converting URL to base64:", e), a;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Update progress callback
|
|
441
|
+
*/
|
|
442
|
+
updateProgress(a, e, l = []) {
|
|
443
|
+
this.onProgress && this.onProgress({
|
|
444
|
+
currentFrame: l.length,
|
|
445
|
+
totalFrames: this.config.frameCount,
|
|
446
|
+
percentage: a,
|
|
447
|
+
status: e,
|
|
448
|
+
generatedFrames: l
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const me = { class: "ai-360-generator" }, pe = {
|
|
453
|
+
key: 0,
|
|
454
|
+
class: "ai-360-generator__api-key"
|
|
455
|
+
}, fe = { for: "api-key-input" }, ve = ["placeholder"], he = {
|
|
456
|
+
key: 1,
|
|
457
|
+
class: "ai-360-generator__upload"
|
|
458
|
+
}, _e = {
|
|
459
|
+
key: 0,
|
|
460
|
+
class: "ai-360-generator__dropzone-content"
|
|
461
|
+
}, ye = {
|
|
462
|
+
key: 1,
|
|
463
|
+
class: "ai-360-generator__preview"
|
|
464
|
+
}, we = ["src"], ke = {
|
|
465
|
+
key: 0,
|
|
466
|
+
class: "ai-360-generator__options"
|
|
467
|
+
}, be = { class: "ai-360-generator__option" }, Ae = { class: "ai-360-generator__option" }, Se = { class: "ai-360-generator__option" }, Ie = {
|
|
468
|
+
key: 2,
|
|
469
|
+
class: "ai-360-generator__progress"
|
|
470
|
+
}, Ce = { class: "ai-360-generator__progress-bar" }, Fe = { class: "ai-360-generator__progress-text" }, Te = { class: "ai-360-generator__progress-detail" }, $e = {
|
|
471
|
+
key: 3,
|
|
472
|
+
class: "ai-360-generator__result"
|
|
473
|
+
}, Pe = { class: "ai-360-generator__viewer" }, De = {
|
|
474
|
+
key: 0,
|
|
475
|
+
class: "ai-360-generator__frame-grid"
|
|
476
|
+
}, Be = ["src", "alt"], Ee = {
|
|
477
|
+
key: 4,
|
|
478
|
+
class: "ai-360-generator__error"
|
|
479
|
+
}, Me = /* @__PURE__ */ j({
|
|
480
|
+
__name: "Ai360Generator",
|
|
481
|
+
props: {
|
|
482
|
+
provider: { default: "openai" },
|
|
483
|
+
apiKey: { default: "" },
|
|
484
|
+
autoSaveApiKey: { type: Boolean, default: !0 },
|
|
485
|
+
showFramePreview: { type: Boolean, default: !0 }
|
|
486
|
+
},
|
|
487
|
+
emits: ["frames-generated", "generation-start", "generation-complete", "generation-error"],
|
|
488
|
+
setup(u, { emit: a }) {
|
|
489
|
+
const e = u, l = a, n = f(e.apiKey || localStorage.getItem(`ai_360_api_key_${e.provider}`) || ""), i = f(null), s = f(!1), v = f(!1), c = f([]), m = f(null), p = f(null), S = f(36), B = f("white"), T = f(80), h = f({
|
|
490
|
+
currentFrame: 0,
|
|
491
|
+
totalFrames: 0,
|
|
492
|
+
percentage: 0,
|
|
493
|
+
status: "",
|
|
494
|
+
generatedFrames: []
|
|
495
|
+
}), L = F(() => n.value.length > 0), $ = F(() => e.provider === "openai" ? "OpenAI" : "Stability AI");
|
|
496
|
+
function x() {
|
|
497
|
+
e.autoSaveApiKey && localStorage.setItem(`ai_360_api_key_${e.provider}`, n.value);
|
|
498
|
+
}
|
|
499
|
+
function E() {
|
|
500
|
+
var o;
|
|
501
|
+
(o = p.value) == null || o.click();
|
|
502
|
+
}
|
|
503
|
+
function z(o) {
|
|
504
|
+
var P;
|
|
505
|
+
const g = (P = o.target.files) == null ? void 0 : P[0];
|
|
506
|
+
g && M(g);
|
|
507
|
+
}
|
|
508
|
+
function V(o) {
|
|
509
|
+
var g;
|
|
510
|
+
s.value = !1;
|
|
511
|
+
const t = (g = o.dataTransfer) == null ? void 0 : g.files[0];
|
|
512
|
+
t && t.type.startsWith("image/") && M(t);
|
|
513
|
+
}
|
|
514
|
+
function M(o) {
|
|
515
|
+
const t = new FileReader();
|
|
516
|
+
t.onload = (g) => {
|
|
517
|
+
var P;
|
|
518
|
+
i.value = (P = g.target) == null ? void 0 : P.result;
|
|
519
|
+
}, t.readAsDataURL(o);
|
|
520
|
+
}
|
|
521
|
+
function K() {
|
|
522
|
+
i.value = null, p.value && (p.value.value = "");
|
|
523
|
+
}
|
|
524
|
+
async function d() {
|
|
525
|
+
if (!(!i.value || !n.value)) {
|
|
526
|
+
v.value = !0, m.value = null, c.value = [], l("generation-start");
|
|
527
|
+
try {
|
|
528
|
+
const t = await new ge(
|
|
529
|
+
{
|
|
530
|
+
provider: e.provider,
|
|
531
|
+
apiKey: n.value,
|
|
532
|
+
frameCount: S.value,
|
|
533
|
+
backgroundColor: B.value,
|
|
534
|
+
quality: T.value,
|
|
535
|
+
useVisionAnalysis: !0
|
|
536
|
+
},
|
|
537
|
+
(g) => {
|
|
538
|
+
h.value = g;
|
|
539
|
+
}
|
|
540
|
+
).generate(i.value);
|
|
541
|
+
c.value = t.frames, l("generation-complete", t.frames);
|
|
542
|
+
} catch (o) {
|
|
543
|
+
const t = o instanceof Error ? o.message : "Generation failed";
|
|
544
|
+
m.value = t, l("generation-error", o instanceof Error ? o : new Error(t));
|
|
545
|
+
} finally {
|
|
546
|
+
v.value = !1;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function w() {
|
|
551
|
+
c.value.forEach((o, t) => {
|
|
552
|
+
const g = document.createElement("a");
|
|
553
|
+
g.href = o, g.download = `360-frame-${String(t + 1).padStart(3, "0")}.png`, g.click();
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function b() {
|
|
557
|
+
c.value = [], i.value = null, m.value = null, h.value = {
|
|
558
|
+
currentFrame: 0,
|
|
559
|
+
totalFrames: 0,
|
|
560
|
+
percentage: 0,
|
|
561
|
+
status: "",
|
|
562
|
+
generatedFrames: []
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function I() {
|
|
566
|
+
l("frames-generated", c.value);
|
|
567
|
+
}
|
|
568
|
+
function A() {
|
|
569
|
+
m.value = null;
|
|
570
|
+
}
|
|
571
|
+
return (o, t) => (y(), _("div", me, [
|
|
572
|
+
L.value ? C("", !0) : (y(), _("div", pe, [
|
|
573
|
+
r("label", fe, D($.value) + " API Key:", 1),
|
|
574
|
+
U(r("input", {
|
|
575
|
+
id: "api-key-input",
|
|
576
|
+
"onUpdate:modelValue": t[0] || (t[0] = (g) => n.value = g),
|
|
577
|
+
type: "password",
|
|
578
|
+
placeholder: `Enter your ${$.value} API key`,
|
|
579
|
+
class: "ai-360-generator__input"
|
|
580
|
+
}, null, 8, ve), [
|
|
581
|
+
[ee, n.value]
|
|
582
|
+
]),
|
|
583
|
+
r("button", {
|
|
584
|
+
onClick: x,
|
|
585
|
+
class: "ai-360-generator__button"
|
|
586
|
+
}, " Save API Key ")
|
|
587
|
+
])),
|
|
588
|
+
L.value && !v.value && c.value.length === 0 ? (y(), _("div", he, [
|
|
589
|
+
r("div", {
|
|
590
|
+
class: G(["ai-360-generator__dropzone", { "ai-360-generator__dropzone--dragging": s.value }]),
|
|
591
|
+
onDrop: R(V, ["prevent"]),
|
|
592
|
+
onDragover: t[1] || (t[1] = R((g) => s.value = !0, ["prevent"])),
|
|
593
|
+
onDragleave: t[2] || (t[2] = R((g) => s.value = !1, ["prevent"])),
|
|
594
|
+
onClick: E
|
|
595
|
+
}, [
|
|
596
|
+
r("input", {
|
|
597
|
+
ref_key: "fileInputRef",
|
|
598
|
+
ref: p,
|
|
599
|
+
type: "file",
|
|
600
|
+
accept: "image/*",
|
|
601
|
+
style: { display: "none" },
|
|
602
|
+
onChange: z
|
|
603
|
+
}, null, 544),
|
|
604
|
+
i.value ? (y(), _("div", ye, [
|
|
605
|
+
r("img", {
|
|
606
|
+
src: i.value,
|
|
607
|
+
alt: "Uploaded product",
|
|
608
|
+
class: "ai-360-generator__preview-image"
|
|
609
|
+
}, null, 8, we),
|
|
610
|
+
r("button", {
|
|
611
|
+
onClick: R(K, ["stop"]),
|
|
612
|
+
class: "ai-360-generator__clear-button"
|
|
613
|
+
}, " ✕ ")
|
|
614
|
+
])) : (y(), _("div", _e, t[6] || (t[6] = [
|
|
615
|
+
r("svg", {
|
|
616
|
+
class: "ai-360-generator__upload-icon",
|
|
617
|
+
viewBox: "0 0 24 24",
|
|
618
|
+
fill: "none",
|
|
619
|
+
stroke: "currentColor"
|
|
620
|
+
}, [
|
|
621
|
+
r("path", {
|
|
622
|
+
d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12",
|
|
623
|
+
"stroke-width": "2",
|
|
624
|
+
"stroke-linecap": "round",
|
|
625
|
+
"stroke-linejoin": "round"
|
|
626
|
+
})
|
|
627
|
+
], -1),
|
|
628
|
+
r("p", { class: "ai-360-generator__dropzone-text" }, " Drop an image here or click to upload ", -1),
|
|
629
|
+
r("p", { class: "ai-360-generator__dropzone-hint" }, " Upload a product image to generate 360° views ", -1)
|
|
630
|
+
])))
|
|
631
|
+
], 34),
|
|
632
|
+
i.value ? (y(), _("div", ke, [
|
|
633
|
+
r("div", be, [
|
|
634
|
+
t[8] || (t[8] = r("label", null, "Frame Count:", -1)),
|
|
635
|
+
U(r("select", {
|
|
636
|
+
"onUpdate:modelValue": t[3] || (t[3] = (g) => S.value = g),
|
|
637
|
+
class: "ai-360-generator__select"
|
|
638
|
+
}, t[7] || (t[7] = [
|
|
639
|
+
r("option", { value: 12 }, "12 frames (Fast)", -1),
|
|
640
|
+
r("option", { value: 24 }, "24 frames (Balanced)", -1),
|
|
641
|
+
r("option", { value: 36 }, "36 frames (Smooth)", -1),
|
|
642
|
+
r("option", { value: 72 }, "72 frames (Ultra Smooth)", -1)
|
|
643
|
+
]), 512), [
|
|
644
|
+
[
|
|
645
|
+
N,
|
|
646
|
+
S.value,
|
|
647
|
+
void 0,
|
|
648
|
+
{ number: !0 }
|
|
649
|
+
]
|
|
650
|
+
])
|
|
651
|
+
]),
|
|
652
|
+
r("div", Ae, [
|
|
653
|
+
t[10] || (t[10] = r("label", null, "Background:", -1)),
|
|
654
|
+
U(r("select", {
|
|
655
|
+
"onUpdate:modelValue": t[4] || (t[4] = (g) => B.value = g),
|
|
656
|
+
class: "ai-360-generator__select"
|
|
657
|
+
}, t[9] || (t[9] = [
|
|
658
|
+
r("option", { value: "white" }, "White", -1),
|
|
659
|
+
r("option", { value: "transparent" }, "Transparent", -1),
|
|
660
|
+
r("option", { value: "black" }, "Black", -1)
|
|
661
|
+
]), 512), [
|
|
662
|
+
[N, B.value]
|
|
663
|
+
])
|
|
664
|
+
]),
|
|
665
|
+
r("div", Se, [
|
|
666
|
+
t[12] || (t[12] = r("label", null, "Quality:", -1)),
|
|
667
|
+
U(r("select", {
|
|
668
|
+
"onUpdate:modelValue": t[5] || (t[5] = (g) => T.value = g),
|
|
669
|
+
class: "ai-360-generator__select"
|
|
670
|
+
}, t[11] || (t[11] = [
|
|
671
|
+
r("option", { value: 60 }, "Standard", -1),
|
|
672
|
+
r("option", { value: 80 }, "High", -1),
|
|
673
|
+
r("option", { value: 100 }, "Ultra", -1)
|
|
674
|
+
]), 512), [
|
|
675
|
+
[
|
|
676
|
+
N,
|
|
677
|
+
T.value,
|
|
678
|
+
void 0,
|
|
679
|
+
{ number: !0 }
|
|
680
|
+
]
|
|
681
|
+
])
|
|
682
|
+
]),
|
|
683
|
+
r("button", {
|
|
684
|
+
onClick: d,
|
|
685
|
+
class: "ai-360-generator__generate-button"
|
|
686
|
+
}, " 🤖 Generate 360° View ")
|
|
687
|
+
])) : C("", !0)
|
|
688
|
+
])) : C("", !0),
|
|
689
|
+
v.value ? (y(), _("div", Ie, [
|
|
690
|
+
r("div", Ce, [
|
|
691
|
+
r("div", {
|
|
692
|
+
class: "ai-360-generator__progress-fill",
|
|
693
|
+
style: X({ width: h.value.percentage + "%" })
|
|
694
|
+
}, null, 4)
|
|
695
|
+
]),
|
|
696
|
+
r("p", Fe, D(h.value.status), 1),
|
|
697
|
+
r("p", Te, " Frame " + D(h.value.currentFrame) + " / " + D(h.value.totalFrames) + " (" + D(Math.round(h.value.percentage)) + "%) ", 1)
|
|
698
|
+
])) : C("", !0),
|
|
699
|
+
c.value.length > 0 && !v.value ? (y(), _("div", $e, [
|
|
700
|
+
t[13] || (t[13] = r("h3", { class: "ai-360-generator__result-title" }, "✅ 360° View Generated!", -1)),
|
|
701
|
+
r("div", Pe, [
|
|
702
|
+
ae(J, {
|
|
703
|
+
"static-image": c.value[0],
|
|
704
|
+
"animated-image": c.value,
|
|
705
|
+
mode: "frames",
|
|
706
|
+
trigger: "hover",
|
|
707
|
+
"enable-drag-spin": !0,
|
|
708
|
+
"frame-rate": 30
|
|
709
|
+
}, null, 8, ["static-image", "animated-image"])
|
|
710
|
+
]),
|
|
711
|
+
r("div", { class: "ai-360-generator__actions" }, [
|
|
712
|
+
r("button", {
|
|
713
|
+
onClick: w,
|
|
714
|
+
class: "ai-360-generator__button"
|
|
715
|
+
}, " 📥 Download Frames "),
|
|
716
|
+
r("button", {
|
|
717
|
+
onClick: b,
|
|
718
|
+
class: "ai-360-generator__button ai-360-generator__button--secondary"
|
|
719
|
+
}, " 🔄 Generate Another "),
|
|
720
|
+
r("button", {
|
|
721
|
+
onClick: I,
|
|
722
|
+
class: "ai-360-generator__button ai-360-generator__button--primary"
|
|
723
|
+
}, " ✓ Use These Frames ")
|
|
724
|
+
]),
|
|
725
|
+
o.showFramePreview ? (y(), _("div", De, [
|
|
726
|
+
(y(!0), _(te, null, re(c.value.slice(0, 12), (g, P) => (y(), _("img", {
|
|
727
|
+
key: P,
|
|
728
|
+
src: g,
|
|
729
|
+
alt: `Frame ${P + 1}`,
|
|
730
|
+
class: "ai-360-generator__frame-thumbnail"
|
|
731
|
+
}, null, 8, Be))), 128))
|
|
732
|
+
])) : C("", !0)
|
|
733
|
+
])) : C("", !0),
|
|
734
|
+
m.value ? (y(), _("div", Ee, [
|
|
735
|
+
r("p", null, "❌ " + D(m.value), 1),
|
|
736
|
+
r("button", {
|
|
737
|
+
onClick: A,
|
|
738
|
+
class: "ai-360-generator__button"
|
|
739
|
+
}, " Dismiss ")
|
|
740
|
+
])) : C("", !0)
|
|
741
|
+
]));
|
|
742
|
+
}
|
|
743
|
+
}), Ue = /* @__PURE__ */ H(Me, [["__scopeId", "data-v-fc0644b2"]]), ze = {
|
|
744
|
+
install(u) {
|
|
745
|
+
u.component("Ai360Spin", J), u.component("Ai360Generator", Ue);
|
|
242
746
|
}
|
|
243
747
|
};
|
|
244
748
|
export {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
749
|
+
ge as AI360Generator,
|
|
750
|
+
Ue as Ai360Generator,
|
|
751
|
+
J as Ai360Spin,
|
|
752
|
+
ze as default,
|
|
753
|
+
oe as use360Spin
|
|
248
754
|
};
|
|
249
755
|
//# sourceMappingURL=index.mjs.map
|