@scalemule/nextjs 0.1.18 → 0.1.20

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/dist/index.mjs CHANGED
@@ -7784,6 +7784,7 @@ function ScaleMuleProvider({
7784
7784
  photo: baseClient.photo,
7785
7785
  video: baseClient.video,
7786
7786
  audio: baseClient.audio,
7787
+ tts: baseClient.tts,
7787
7788
  mediaPolicy: effectiveMediaPolicy,
7788
7789
  user,
7789
7790
  setUser: handleSetUser,
@@ -9172,7 +9173,7 @@ function useAudio() {
9172
9173
  return null;
9173
9174
  }
9174
9175
  try {
9175
- const details = await client.get(`/v1/audios/${audioId}`);
9176
+ const details = await client.get(`/v1/audio/${audioId}`);
9176
9177
  if (details.status === "ready" && details.url) {
9177
9178
  return details.url;
9178
9179
  }
@@ -9234,7 +9235,7 @@ function useAudio() {
9234
9235
  const enriched = await Promise.all(
9235
9236
  audioFiles.map(async (file) => {
9236
9237
  try {
9237
- const details = await client.get(`/v1/audios/${file.id}`);
9238
+ const details = await client.get(`/v1/audio/${file.id}`);
9238
9239
  return {
9239
9240
  audio_id: details.id,
9240
9241
  file_id: file.id,
@@ -9270,7 +9271,7 @@ function useAudio() {
9270
9271
  setError(null);
9271
9272
  try {
9272
9273
  const [audioDeleteResult, storageDeleteResult] = await Promise.allSettled([
9273
- client.delete(`/v1/audios/${fileId}`),
9274
+ client.delete(`/v1/audio/${fileId}`),
9274
9275
  client.delete(`/v1/storage/files/${fileId}`)
9275
9276
  ]);
9276
9277
  if (storageDeleteResult.status === "rejected" && !isNotFoundError(storageDeleteResult.reason)) {
@@ -9297,6 +9298,63 @@ function useAudio() {
9297
9298
  loading
9298
9299
  };
9299
9300
  }
9301
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
9302
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["ready", "failed"]);
9303
+ function useTtsJob(jobId, options) {
9304
+ const { tts } = useScaleMule();
9305
+ const [job, setJob] = useState(null);
9306
+ const [loading, setLoading] = useState(false);
9307
+ const [error, setError] = useState(null);
9308
+ const refresh = useCallback(async () => {
9309
+ if (!jobId) {
9310
+ setJob(null);
9311
+ return null;
9312
+ }
9313
+ setLoading(true);
9314
+ setError(null);
9315
+ try {
9316
+ const result = await tts.getJob(jobId);
9317
+ if (!result) {
9318
+ const apiError = { code: "tts_job_error", message: "Failed to load TTS job", status: 500 };
9319
+ setError(apiError);
9320
+ return null;
9321
+ }
9322
+ if (result.error || !result.data) {
9323
+ const apiError = result.error ?? { code: "tts_job_error", message: "Failed to load TTS job", status: 500 };
9324
+ setError(apiError);
9325
+ return null;
9326
+ }
9327
+ setJob(result.data);
9328
+ return result.data;
9329
+ } finally {
9330
+ setLoading(false);
9331
+ }
9332
+ }, [jobId, tts]);
9333
+ useEffect(() => {
9334
+ if (!jobId || options?.enabled === false) {
9335
+ setJob(null);
9336
+ setLoading(false);
9337
+ return;
9338
+ }
9339
+ let cancelled = false;
9340
+ let timeoutId = null;
9341
+ const poll = async () => {
9342
+ const next = await refresh();
9343
+ if (cancelled || !next || TERMINAL_STATUSES.has(next.status)) {
9344
+ return;
9345
+ }
9346
+ timeoutId = window.setTimeout(poll, options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
9347
+ };
9348
+ poll();
9349
+ return () => {
9350
+ cancelled = true;
9351
+ if (timeoutId != null) {
9352
+ window.clearTimeout(timeoutId);
9353
+ }
9354
+ };
9355
+ }, [jobId, options?.enabled, options?.pollIntervalMs, refresh]);
9356
+ return { job, loading, error, refresh };
9357
+ }
9300
9358
  function useMedia() {
9301
9359
  const { storage, photo, video, audio, mediaPolicy: providerDefaultPolicy } = useScaleMule();
9302
9360
  const [uploading, setUploading] = useState(false);
@@ -11210,6 +11268,230 @@ function FeedbackWidget(props) {
11210
11268
  ) })
11211
11269
  ] });
11212
11270
  }
11271
+ var SPEEDS = [1, 1.25, 1.5, 2];
11272
+ function formatTime(seconds) {
11273
+ if (!Number.isFinite(seconds) || seconds <= 0) {
11274
+ return "0:00";
11275
+ }
11276
+ const wholeSeconds = Math.floor(seconds);
11277
+ const minutes = Math.floor(wholeSeconds / 60);
11278
+ const remainder = wholeSeconds % 60;
11279
+ return `${minutes}:${remainder.toString().padStart(2, "0")}`;
11280
+ }
11281
+ function getNextSpeed(currentSpeed) {
11282
+ const currentIndex = SPEEDS.findIndex((speed) => speed === currentSpeed);
11283
+ if (currentIndex === -1) {
11284
+ return SPEEDS[0];
11285
+ }
11286
+ return SPEEDS[(currentIndex + 1) % SPEEDS.length];
11287
+ }
11288
+ function NarrationPlayer({
11289
+ audio,
11290
+ className,
11291
+ providerLabel = "ScaleMule",
11292
+ narrationLabel = "AI-Narrated",
11293
+ refreshing = false,
11294
+ onRefresh,
11295
+ onPlaybackError
11296
+ }) {
11297
+ const audioRef = useRef(null);
11298
+ const scrubRef = useRef(null);
11299
+ const [playing, setPlaying] = useState(false);
11300
+ const [currentTime, setCurrentTime] = useState(0);
11301
+ const [duration, setDuration] = useState(audio.duration_ms ? audio.duration_ms / 1e3 : 0);
11302
+ const [speed, setSpeed] = useState(1.5);
11303
+ const peaks = useMemo(() => {
11304
+ if (!Array.isArray(audio.waveform_peaks)) {
11305
+ return [];
11306
+ }
11307
+ const sampled = audio.waveform_peaks.filter((_, index) => index % 3 === 0).slice(0, 220);
11308
+ return sampled.length > 0 ? sampled : audio.waveform_peaks.slice(0, 220);
11309
+ }, [audio.waveform_peaks]);
11310
+ useEffect(() => {
11311
+ const element = audioRef.current;
11312
+ if (!element) return;
11313
+ setCurrentTime(0);
11314
+ setPlaying(false);
11315
+ element.pause();
11316
+ element.currentTime = 0;
11317
+ }, [audio.url]);
11318
+ useEffect(() => {
11319
+ const element = audioRef.current;
11320
+ if (!element) return;
11321
+ element.playbackRate = speed;
11322
+ element.preservesPitch = true;
11323
+ }, [speed]);
11324
+ function togglePlayback() {
11325
+ const element = audioRef.current;
11326
+ if (!element) return;
11327
+ if (element.paused) {
11328
+ element.play().catch(() => {
11329
+ setPlaying(false);
11330
+ onPlaybackError?.();
11331
+ });
11332
+ return;
11333
+ }
11334
+ element.pause();
11335
+ }
11336
+ function seekTo(clientX) {
11337
+ const element = audioRef.current;
11338
+ const scrubber = scrubRef.current;
11339
+ if (!element || !scrubber || !duration) return;
11340
+ const bounds = scrubber.getBoundingClientRect();
11341
+ const ratio = Math.max(0, Math.min(1, (clientX - bounds.left) / bounds.width));
11342
+ const nextTime = ratio * duration;
11343
+ element.currentTime = nextTime;
11344
+ setCurrentTime(nextTime);
11345
+ }
11346
+ const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
11347
+ const expiresLabel = audio.expires_at ? new Date(audio.expires_at).toLocaleTimeString([], {
11348
+ hour: "numeric",
11349
+ minute: "2-digit"
11350
+ }) : null;
11351
+ if (!audio.url) {
11352
+ return null;
11353
+ }
11354
+ return /* @__PURE__ */ jsxs(
11355
+ "div",
11356
+ {
11357
+ className: "rounded-[1.75rem] border border-slate-200/80 bg-[linear-gradient(180deg,#f8fbff_0%,#edf4ff_100%)] p-4 text-slate-900 shadow-[0_22px_70px_rgba(4,12,24,0.28)] " + (className ?? ""),
11358
+ children: [
11359
+ /* @__PURE__ */ jsx(
11360
+ "audio",
11361
+ {
11362
+ ref: audioRef,
11363
+ src: audio.url ?? void 0,
11364
+ preload: "metadata",
11365
+ onLoadedMetadata: (event) => {
11366
+ setDuration(event.currentTarget.duration || 0);
11367
+ },
11368
+ onTimeUpdate: (event) => {
11369
+ setCurrentTime(event.currentTarget.currentTime);
11370
+ },
11371
+ onPlay: () => setPlaying(true),
11372
+ onPause: () => setPlaying(false),
11373
+ onEnded: () => setPlaying(false),
11374
+ onError: () => onPlaybackError?.()
11375
+ }
11376
+ ),
11377
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 md:flex-row md:items-center", children: [
11378
+ /* @__PURE__ */ jsx(
11379
+ "button",
11380
+ {
11381
+ type: "button",
11382
+ onClick: togglePlayback,
11383
+ "aria-label": playing ? "Pause narration" : "Play narration",
11384
+ className: "flex h-[4.5rem] w-[4.5rem] shrink-0 items-center justify-center rounded-full bg-slate-950 text-white shadow-[0_10px_28px_rgba(15,23,42,0.25)] transition hover:bg-slate-800",
11385
+ children: playing ? /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: "h-7 w-7 fill-current", "aria-hidden": "true", children: [
11386
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "5", width: "4", height: "14", rx: "1.2" }),
11387
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "5", width: "4", height: "14", rx: "1.2" })
11388
+ ] }) : /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "ml-1 h-7 w-7 fill-current", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M7 5.2c0-1.02 1.12-1.65 2-.98l10.2 7.78a1.24 1.24 0 0 1 0 1.98L9 21.76c-.88.67-2-.04-2-1.08V5.2Z" }) })
11389
+ }
11390
+ ),
11391
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
11392
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 text-[0.8rem] font-medium text-slate-500", children: [
11393
+ /* @__PURE__ */ jsx("span", { children: formatTime(currentTime / speed) }),
11394
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
11395
+ /* @__PURE__ */ jsx("span", { className: "rounded-full bg-blue-100 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.24em] text-blue-700", children: narrationLabel }),
11396
+ /* @__PURE__ */ jsx("span", { children: formatTime(duration / speed) })
11397
+ ] })
11398
+ ] }),
11399
+ /* @__PURE__ */ jsx(
11400
+ "div",
11401
+ {
11402
+ ref: scrubRef,
11403
+ role: "slider",
11404
+ "aria-label": "Narration progress",
11405
+ "aria-valuemin": 0,
11406
+ "aria-valuemax": Math.max(1, Math.floor(duration)),
11407
+ "aria-valuenow": Math.floor(currentTime),
11408
+ tabIndex: 0,
11409
+ onClick: (event) => {
11410
+ seekTo(event.clientX);
11411
+ },
11412
+ className: "relative mt-4 h-12 cursor-pointer overflow-hidden rounded-[1rem] bg-slate-200/90 px-1",
11413
+ children: peaks.length > 0 ? /* @__PURE__ */ jsx(
11414
+ "svg",
11415
+ {
11416
+ viewBox: `0 0 ${peaks.length} 40`,
11417
+ preserveAspectRatio: "none",
11418
+ className: "pointer-events-none absolute inset-0 h-full w-full",
11419
+ children: peaks.map((peak, index) => {
11420
+ const height = Math.max(2, Math.round(Math.abs(peak) * 34));
11421
+ const y = (40 - height) / 2;
11422
+ const filled = index / peaks.length < progress;
11423
+ return /* @__PURE__ */ jsx(
11424
+ "rect",
11425
+ {
11426
+ x: index,
11427
+ y,
11428
+ width: 0.82,
11429
+ height,
11430
+ rx: 0.4,
11431
+ className: filled ? "fill-blue-600" : "fill-blue-300"
11432
+ },
11433
+ `${audio.id}-${index}`
11434
+ );
11435
+ })
11436
+ }
11437
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
11438
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-slate-200/90" }),
11439
+ /* @__PURE__ */ jsx(
11440
+ "div",
11441
+ {
11442
+ className: "absolute inset-y-0 left-0 bg-blue-600",
11443
+ style: { width: `${progress * 100}%` }
11444
+ }
11445
+ )
11446
+ ] })
11447
+ }
11448
+ ),
11449
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap items-center gap-2 text-[0.72rem] font-medium text-slate-500", children: [
11450
+ /* @__PURE__ */ jsxs("span", { className: "rounded-full border border-slate-200 bg-white/85 px-3 py-1 text-slate-700", children: [
11451
+ "Provider: ",
11452
+ providerLabel
11453
+ ] }),
11454
+ duration > 0 && /* @__PURE__ */ jsxs("span", { className: "rounded-full border border-slate-200 bg-white/65 px-3 py-1", children: [
11455
+ "Length ",
11456
+ formatTime(duration / speed)
11457
+ ] }),
11458
+ expiresLabel && /* @__PURE__ */ jsxs("span", { className: "rounded-full border border-slate-200 bg-white/65 px-3 py-1", children: [
11459
+ "Expires ",
11460
+ expiresLabel
11461
+ ] })
11462
+ ] })
11463
+ ] }),
11464
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2 self-start md:self-center", children: [
11465
+ /* @__PURE__ */ jsxs(
11466
+ "button",
11467
+ {
11468
+ type: "button",
11469
+ onClick: () => {
11470
+ setSpeed((currentSpeed) => getNextSpeed(currentSpeed));
11471
+ },
11472
+ className: "rounded-full border border-slate-300 bg-white/90 px-4 py-2 text-base font-semibold text-slate-700 shadow-sm hover:bg-white",
11473
+ children: [
11474
+ speed,
11475
+ "\xD7"
11476
+ ]
11477
+ }
11478
+ ),
11479
+ onRefresh && /* @__PURE__ */ jsx(
11480
+ "button",
11481
+ {
11482
+ type: "button",
11483
+ onClick: onRefresh,
11484
+ disabled: refreshing,
11485
+ className: "rounded-full border border-slate-300 bg-white/75 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-white disabled:cursor-not-allowed disabled:opacity-60",
11486
+ children: refreshing ? "Refreshing\u2026" : "Refresh"
11487
+ }
11488
+ )
11489
+ ] })
11490
+ ] })
11491
+ ]
11492
+ }
11493
+ );
11494
+ }
11213
11495
 
11214
11496
  // src/validation.ts
11215
11497
  var phoneCountries = [
@@ -11502,4 +11784,4 @@ function createSafeLogger(prefix) {
11502
11784
  };
11503
11785
  }
11504
11786
 
11505
- export { FeedbackWidget, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, ScaleMuleProvider, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
11787
+ export { FeedbackWidget, NarrationPlayer, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, ScaleMuleProvider, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, validateForm, validators };