@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/README.md +3 -1
- package/dist/index.d.mts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.js +287 -3
- package/dist/index.mjs +286 -4
- package/dist/server/index.js +678 -637
- package/dist/server/index.mjs +679 -638
- package/dist/server/ledvery.js +5 -1
- package/dist/server/ledvery.mjs +5 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ This creates all auth endpoints automatically:
|
|
|
34
34
|
|
|
35
35
|
API keys never reach the browser. Session tokens are HTTP-only cookies.
|
|
36
36
|
|
|
37
|
+
Need the cookie/header lifecycle or the proxy-vs-direct decision tree? See [`docs/AUTH_PROXY_PATTERN.md`](./docs/AUTH_PROXY_PATTERN.md).
|
|
38
|
+
|
|
37
39
|
## Environment Variables
|
|
38
40
|
|
|
39
41
|
```bash
|
|
@@ -295,7 +297,7 @@ export const { GET, POST, DELETE, PATCH } = createAuthRoutes({
|
|
|
295
297
|
|
|
296
298
|
```ts
|
|
297
299
|
// Read CSRF token from cookie and include in requests
|
|
298
|
-
const csrfToken = document.cookie.match(/
|
|
300
|
+
const csrfToken = document.cookie.match(/sm_csrf=([^;]+)/)?.[1]
|
|
299
301
|
|
|
300
302
|
fetch('/api/auth/login', {
|
|
301
303
|
method: 'POST',
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, FileStatus } from '@scalemule/sdk';
|
|
2
|
+
import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, TtsService, TtsJobStatus, FileStatus, TtsAudioInfo } from '@scalemule/sdk';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { ReactNode, ReactElement } from 'react';
|
|
5
5
|
import { MoneyClient } from '@scalemule/money';
|
|
@@ -166,6 +166,8 @@ interface ScaleMuleContextValue {
|
|
|
166
166
|
video: VideoService;
|
|
167
167
|
/** Base SDK audio service — exposed for `useMedia()` and `audio.uploadViaStorage()` */
|
|
168
168
|
audio: AudioService;
|
|
169
|
+
/** Base SDK TTS service — exposed for `useTtsJob()` and direct narration requests */
|
|
170
|
+
tts: TtsService;
|
|
169
171
|
/**
|
|
170
172
|
* Default media policy for `useMedia()` calls. Set via
|
|
171
173
|
* `<ScaleMuleProvider mediaPolicy="…">`; per-call overrides win.
|
|
@@ -360,6 +362,18 @@ interface UseAudioReturn {
|
|
|
360
362
|
}
|
|
361
363
|
declare function useAudio(): UseAudioReturn;
|
|
362
364
|
|
|
365
|
+
interface UseTtsJobOptions {
|
|
366
|
+
enabled?: boolean;
|
|
367
|
+
pollIntervalMs?: number;
|
|
368
|
+
}
|
|
369
|
+
interface UseTtsJobReturn {
|
|
370
|
+
job: TtsJobStatus | null;
|
|
371
|
+
loading: boolean;
|
|
372
|
+
error: ApiError | null;
|
|
373
|
+
refresh: () => Promise<TtsJobStatus | null>;
|
|
374
|
+
}
|
|
375
|
+
declare function useTtsJob(jobId: string | null | undefined, options?: UseTtsJobOptions): UseTtsJobReturn;
|
|
376
|
+
|
|
363
377
|
/**
|
|
364
378
|
* Conversation kinds recognized by the chat realtime channel naming scheme.
|
|
365
379
|
* Matches `broadcast_to_conversation` in `ms/scalemule-chat/src/realtime.rs`.
|
|
@@ -874,6 +888,21 @@ interface FeedbackWidgetProps {
|
|
|
874
888
|
*/
|
|
875
889
|
declare function FeedbackWidget(props: FeedbackWidgetProps): ReactElement | null;
|
|
876
890
|
|
|
891
|
+
type NarrationPlayerAudio = TtsAudioInfo & {
|
|
892
|
+
waveform_peaks?: number[] | null;
|
|
893
|
+
ai_generated?: boolean;
|
|
894
|
+
};
|
|
895
|
+
interface NarrationPlayerProps {
|
|
896
|
+
audio: NarrationPlayerAudio;
|
|
897
|
+
className?: string;
|
|
898
|
+
providerLabel?: string;
|
|
899
|
+
narrationLabel?: string;
|
|
900
|
+
refreshing?: boolean;
|
|
901
|
+
onRefresh?: () => void;
|
|
902
|
+
onPlaybackError?: () => void;
|
|
903
|
+
}
|
|
904
|
+
declare function NarrationPlayer({ audio, className, providerLabel, narrationLabel, refreshing, onRefresh, onPlaybackError, }: NarrationPlayerProps): react_jsx_runtime.JSX.Element | null;
|
|
905
|
+
|
|
877
906
|
/**
|
|
878
907
|
* Client-side validation helpers
|
|
879
908
|
*
|
|
@@ -1032,4 +1061,4 @@ declare function createSafeLogger(prefix: string): {
|
|
|
1032
1061
|
error: (message: string, data?: unknown) => void;
|
|
1033
1062
|
};
|
|
1034
1063
|
|
|
1035
|
-
export { ApiError$1 as ApiError, type AudioFile, type AudioUploadResult, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, type UseAudioReturn, type UseAudioUploadOptions, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
|
1064
|
+
export { ApiError$1 as ApiError, type AudioFile, type AudioUploadResult, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, NarrationPlayer, type NarrationPlayerAudio, type NarrationPlayerProps, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, type UseAudioReturn, type UseAudioUploadOptions, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, type UseTtsJobOptions, type UseTtsJobReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, validateForm, validators };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, FileStatus } from '@scalemule/sdk';
|
|
2
|
+
import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, TtsService, TtsJobStatus, FileStatus, TtsAudioInfo } from '@scalemule/sdk';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { ReactNode, ReactElement } from 'react';
|
|
5
5
|
import { MoneyClient } from '@scalemule/money';
|
|
@@ -166,6 +166,8 @@ interface ScaleMuleContextValue {
|
|
|
166
166
|
video: VideoService;
|
|
167
167
|
/** Base SDK audio service — exposed for `useMedia()` and `audio.uploadViaStorage()` */
|
|
168
168
|
audio: AudioService;
|
|
169
|
+
/** Base SDK TTS service — exposed for `useTtsJob()` and direct narration requests */
|
|
170
|
+
tts: TtsService;
|
|
169
171
|
/**
|
|
170
172
|
* Default media policy for `useMedia()` calls. Set via
|
|
171
173
|
* `<ScaleMuleProvider mediaPolicy="…">`; per-call overrides win.
|
|
@@ -360,6 +362,18 @@ interface UseAudioReturn {
|
|
|
360
362
|
}
|
|
361
363
|
declare function useAudio(): UseAudioReturn;
|
|
362
364
|
|
|
365
|
+
interface UseTtsJobOptions {
|
|
366
|
+
enabled?: boolean;
|
|
367
|
+
pollIntervalMs?: number;
|
|
368
|
+
}
|
|
369
|
+
interface UseTtsJobReturn {
|
|
370
|
+
job: TtsJobStatus | null;
|
|
371
|
+
loading: boolean;
|
|
372
|
+
error: ApiError | null;
|
|
373
|
+
refresh: () => Promise<TtsJobStatus | null>;
|
|
374
|
+
}
|
|
375
|
+
declare function useTtsJob(jobId: string | null | undefined, options?: UseTtsJobOptions): UseTtsJobReturn;
|
|
376
|
+
|
|
363
377
|
/**
|
|
364
378
|
* Conversation kinds recognized by the chat realtime channel naming scheme.
|
|
365
379
|
* Matches `broadcast_to_conversation` in `ms/scalemule-chat/src/realtime.rs`.
|
|
@@ -874,6 +888,21 @@ interface FeedbackWidgetProps {
|
|
|
874
888
|
*/
|
|
875
889
|
declare function FeedbackWidget(props: FeedbackWidgetProps): ReactElement | null;
|
|
876
890
|
|
|
891
|
+
type NarrationPlayerAudio = TtsAudioInfo & {
|
|
892
|
+
waveform_peaks?: number[] | null;
|
|
893
|
+
ai_generated?: boolean;
|
|
894
|
+
};
|
|
895
|
+
interface NarrationPlayerProps {
|
|
896
|
+
audio: NarrationPlayerAudio;
|
|
897
|
+
className?: string;
|
|
898
|
+
providerLabel?: string;
|
|
899
|
+
narrationLabel?: string;
|
|
900
|
+
refreshing?: boolean;
|
|
901
|
+
onRefresh?: () => void;
|
|
902
|
+
onPlaybackError?: () => void;
|
|
903
|
+
}
|
|
904
|
+
declare function NarrationPlayer({ audio, className, providerLabel, narrationLabel, refreshing, onRefresh, onPlaybackError, }: NarrationPlayerProps): react_jsx_runtime.JSX.Element | null;
|
|
905
|
+
|
|
877
906
|
/**
|
|
878
907
|
* Client-side validation helpers
|
|
879
908
|
*
|
|
@@ -1032,4 +1061,4 @@ declare function createSafeLogger(prefix: string): {
|
|
|
1032
1061
|
error: (message: string, data?: unknown) => void;
|
|
1033
1062
|
};
|
|
1034
1063
|
|
|
1035
|
-
export { ApiError$1 as ApiError, type AudioFile, type AudioUploadResult, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, type UseAudioReturn, type UseAudioUploadOptions, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
|
1064
|
+
export { ApiError$1 as ApiError, type AudioFile, type AudioUploadResult, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, NarrationPlayer, type NarrationPlayerAudio, type NarrationPlayerProps, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, type UseAudioReturn, type UseAudioUploadOptions, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, type UseTtsJobOptions, type UseTtsJobReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, validateForm, validators };
|
package/dist/index.js
CHANGED
|
@@ -7804,6 +7804,7 @@ function ScaleMuleProvider({
|
|
|
7804
7804
|
photo: baseClient.photo,
|
|
7805
7805
|
video: baseClient.video,
|
|
7806
7806
|
audio: baseClient.audio,
|
|
7807
|
+
tts: baseClient.tts,
|
|
7807
7808
|
mediaPolicy: effectiveMediaPolicy,
|
|
7808
7809
|
user,
|
|
7809
7810
|
setUser: handleSetUser,
|
|
@@ -9192,7 +9193,7 @@ function useAudio() {
|
|
|
9192
9193
|
return null;
|
|
9193
9194
|
}
|
|
9194
9195
|
try {
|
|
9195
|
-
const details = await client.get(`/v1/
|
|
9196
|
+
const details = await client.get(`/v1/audio/${audioId}`);
|
|
9196
9197
|
if (details.status === "ready" && details.url) {
|
|
9197
9198
|
return details.url;
|
|
9198
9199
|
}
|
|
@@ -9254,7 +9255,7 @@ function useAudio() {
|
|
|
9254
9255
|
const enriched = await Promise.all(
|
|
9255
9256
|
audioFiles.map(async (file) => {
|
|
9256
9257
|
try {
|
|
9257
|
-
const details = await client.get(`/v1/
|
|
9258
|
+
const details = await client.get(`/v1/audio/${file.id}`);
|
|
9258
9259
|
return {
|
|
9259
9260
|
audio_id: details.id,
|
|
9260
9261
|
file_id: file.id,
|
|
@@ -9290,7 +9291,7 @@ function useAudio() {
|
|
|
9290
9291
|
setError(null);
|
|
9291
9292
|
try {
|
|
9292
9293
|
const [audioDeleteResult, storageDeleteResult] = await Promise.allSettled([
|
|
9293
|
-
client.delete(`/v1/
|
|
9294
|
+
client.delete(`/v1/audio/${fileId}`),
|
|
9294
9295
|
client.delete(`/v1/storage/files/${fileId}`)
|
|
9295
9296
|
]);
|
|
9296
9297
|
if (storageDeleteResult.status === "rejected" && !isNotFoundError(storageDeleteResult.reason)) {
|
|
@@ -9317,6 +9318,63 @@ function useAudio() {
|
|
|
9317
9318
|
loading
|
|
9318
9319
|
};
|
|
9319
9320
|
}
|
|
9321
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
9322
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["ready", "failed"]);
|
|
9323
|
+
function useTtsJob(jobId, options) {
|
|
9324
|
+
const { tts } = useScaleMule();
|
|
9325
|
+
const [job, setJob] = React.useState(null);
|
|
9326
|
+
const [loading, setLoading] = React.useState(false);
|
|
9327
|
+
const [error, setError] = React.useState(null);
|
|
9328
|
+
const refresh = React.useCallback(async () => {
|
|
9329
|
+
if (!jobId) {
|
|
9330
|
+
setJob(null);
|
|
9331
|
+
return null;
|
|
9332
|
+
}
|
|
9333
|
+
setLoading(true);
|
|
9334
|
+
setError(null);
|
|
9335
|
+
try {
|
|
9336
|
+
const result = await tts.getJob(jobId);
|
|
9337
|
+
if (!result) {
|
|
9338
|
+
const apiError = { code: "tts_job_error", message: "Failed to load TTS job", status: 500 };
|
|
9339
|
+
setError(apiError);
|
|
9340
|
+
return null;
|
|
9341
|
+
}
|
|
9342
|
+
if (result.error || !result.data) {
|
|
9343
|
+
const apiError = result.error ?? { code: "tts_job_error", message: "Failed to load TTS job", status: 500 };
|
|
9344
|
+
setError(apiError);
|
|
9345
|
+
return null;
|
|
9346
|
+
}
|
|
9347
|
+
setJob(result.data);
|
|
9348
|
+
return result.data;
|
|
9349
|
+
} finally {
|
|
9350
|
+
setLoading(false);
|
|
9351
|
+
}
|
|
9352
|
+
}, [jobId, tts]);
|
|
9353
|
+
React.useEffect(() => {
|
|
9354
|
+
if (!jobId || options?.enabled === false) {
|
|
9355
|
+
setJob(null);
|
|
9356
|
+
setLoading(false);
|
|
9357
|
+
return;
|
|
9358
|
+
}
|
|
9359
|
+
let cancelled = false;
|
|
9360
|
+
let timeoutId = null;
|
|
9361
|
+
const poll = async () => {
|
|
9362
|
+
const next = await refresh();
|
|
9363
|
+
if (cancelled || !next || TERMINAL_STATUSES.has(next.status)) {
|
|
9364
|
+
return;
|
|
9365
|
+
}
|
|
9366
|
+
timeoutId = window.setTimeout(poll, options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
9367
|
+
};
|
|
9368
|
+
poll();
|
|
9369
|
+
return () => {
|
|
9370
|
+
cancelled = true;
|
|
9371
|
+
if (timeoutId != null) {
|
|
9372
|
+
window.clearTimeout(timeoutId);
|
|
9373
|
+
}
|
|
9374
|
+
};
|
|
9375
|
+
}, [jobId, options?.enabled, options?.pollIntervalMs, refresh]);
|
|
9376
|
+
return { job, loading, error, refresh };
|
|
9377
|
+
}
|
|
9320
9378
|
function useMedia() {
|
|
9321
9379
|
const { storage, photo, video, audio, mediaPolicy: providerDefaultPolicy } = useScaleMule();
|
|
9322
9380
|
const [uploading, setUploading] = React.useState(false);
|
|
@@ -11230,6 +11288,230 @@ function FeedbackWidget(props) {
|
|
|
11230
11288
|
) })
|
|
11231
11289
|
] });
|
|
11232
11290
|
}
|
|
11291
|
+
var SPEEDS = [1, 1.25, 1.5, 2];
|
|
11292
|
+
function formatTime(seconds) {
|
|
11293
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
11294
|
+
return "0:00";
|
|
11295
|
+
}
|
|
11296
|
+
const wholeSeconds = Math.floor(seconds);
|
|
11297
|
+
const minutes = Math.floor(wholeSeconds / 60);
|
|
11298
|
+
const remainder = wholeSeconds % 60;
|
|
11299
|
+
return `${minutes}:${remainder.toString().padStart(2, "0")}`;
|
|
11300
|
+
}
|
|
11301
|
+
function getNextSpeed(currentSpeed) {
|
|
11302
|
+
const currentIndex = SPEEDS.findIndex((speed) => speed === currentSpeed);
|
|
11303
|
+
if (currentIndex === -1) {
|
|
11304
|
+
return SPEEDS[0];
|
|
11305
|
+
}
|
|
11306
|
+
return SPEEDS[(currentIndex + 1) % SPEEDS.length];
|
|
11307
|
+
}
|
|
11308
|
+
function NarrationPlayer({
|
|
11309
|
+
audio,
|
|
11310
|
+
className,
|
|
11311
|
+
providerLabel = "ScaleMule",
|
|
11312
|
+
narrationLabel = "AI-Narrated",
|
|
11313
|
+
refreshing = false,
|
|
11314
|
+
onRefresh,
|
|
11315
|
+
onPlaybackError
|
|
11316
|
+
}) {
|
|
11317
|
+
const audioRef = React.useRef(null);
|
|
11318
|
+
const scrubRef = React.useRef(null);
|
|
11319
|
+
const [playing, setPlaying] = React.useState(false);
|
|
11320
|
+
const [currentTime, setCurrentTime] = React.useState(0);
|
|
11321
|
+
const [duration, setDuration] = React.useState(audio.duration_ms ? audio.duration_ms / 1e3 : 0);
|
|
11322
|
+
const [speed, setSpeed] = React.useState(1.5);
|
|
11323
|
+
const peaks = React.useMemo(() => {
|
|
11324
|
+
if (!Array.isArray(audio.waveform_peaks)) {
|
|
11325
|
+
return [];
|
|
11326
|
+
}
|
|
11327
|
+
const sampled = audio.waveform_peaks.filter((_, index) => index % 3 === 0).slice(0, 220);
|
|
11328
|
+
return sampled.length > 0 ? sampled : audio.waveform_peaks.slice(0, 220);
|
|
11329
|
+
}, [audio.waveform_peaks]);
|
|
11330
|
+
React.useEffect(() => {
|
|
11331
|
+
const element = audioRef.current;
|
|
11332
|
+
if (!element) return;
|
|
11333
|
+
setCurrentTime(0);
|
|
11334
|
+
setPlaying(false);
|
|
11335
|
+
element.pause();
|
|
11336
|
+
element.currentTime = 0;
|
|
11337
|
+
}, [audio.url]);
|
|
11338
|
+
React.useEffect(() => {
|
|
11339
|
+
const element = audioRef.current;
|
|
11340
|
+
if (!element) return;
|
|
11341
|
+
element.playbackRate = speed;
|
|
11342
|
+
element.preservesPitch = true;
|
|
11343
|
+
}, [speed]);
|
|
11344
|
+
function togglePlayback() {
|
|
11345
|
+
const element = audioRef.current;
|
|
11346
|
+
if (!element) return;
|
|
11347
|
+
if (element.paused) {
|
|
11348
|
+
element.play().catch(() => {
|
|
11349
|
+
setPlaying(false);
|
|
11350
|
+
onPlaybackError?.();
|
|
11351
|
+
});
|
|
11352
|
+
return;
|
|
11353
|
+
}
|
|
11354
|
+
element.pause();
|
|
11355
|
+
}
|
|
11356
|
+
function seekTo(clientX) {
|
|
11357
|
+
const element = audioRef.current;
|
|
11358
|
+
const scrubber = scrubRef.current;
|
|
11359
|
+
if (!element || !scrubber || !duration) return;
|
|
11360
|
+
const bounds = scrubber.getBoundingClientRect();
|
|
11361
|
+
const ratio = Math.max(0, Math.min(1, (clientX - bounds.left) / bounds.width));
|
|
11362
|
+
const nextTime = ratio * duration;
|
|
11363
|
+
element.currentTime = nextTime;
|
|
11364
|
+
setCurrentTime(nextTime);
|
|
11365
|
+
}
|
|
11366
|
+
const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
|
|
11367
|
+
const expiresLabel = audio.expires_at ? new Date(audio.expires_at).toLocaleTimeString([], {
|
|
11368
|
+
hour: "numeric",
|
|
11369
|
+
minute: "2-digit"
|
|
11370
|
+
}) : null;
|
|
11371
|
+
if (!audio.url) {
|
|
11372
|
+
return null;
|
|
11373
|
+
}
|
|
11374
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
11375
|
+
"div",
|
|
11376
|
+
{
|
|
11377
|
+
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 ?? ""),
|
|
11378
|
+
children: [
|
|
11379
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11380
|
+
"audio",
|
|
11381
|
+
{
|
|
11382
|
+
ref: audioRef,
|
|
11383
|
+
src: audio.url ?? void 0,
|
|
11384
|
+
preload: "metadata",
|
|
11385
|
+
onLoadedMetadata: (event) => {
|
|
11386
|
+
setDuration(event.currentTarget.duration || 0);
|
|
11387
|
+
},
|
|
11388
|
+
onTimeUpdate: (event) => {
|
|
11389
|
+
setCurrentTime(event.currentTarget.currentTime);
|
|
11390
|
+
},
|
|
11391
|
+
onPlay: () => setPlaying(true),
|
|
11392
|
+
onPause: () => setPlaying(false),
|
|
11393
|
+
onEnded: () => setPlaying(false),
|
|
11394
|
+
onError: () => onPlaybackError?.()
|
|
11395
|
+
}
|
|
11396
|
+
),
|
|
11397
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4 md:flex-row md:items-center", children: [
|
|
11398
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11399
|
+
"button",
|
|
11400
|
+
{
|
|
11401
|
+
type: "button",
|
|
11402
|
+
onClick: togglePlayback,
|
|
11403
|
+
"aria-label": playing ? "Pause narration" : "Play narration",
|
|
11404
|
+
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",
|
|
11405
|
+
children: playing ? /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", className: "h-7 w-7 fill-current", "aria-hidden": "true", children: [
|
|
11406
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "5", width: "4", height: "14", rx: "1.2" }),
|
|
11407
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "5", width: "4", height: "14", rx: "1.2" })
|
|
11408
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", className: "ml-1 h-7 w-7 fill-current", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.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" }) })
|
|
11409
|
+
}
|
|
11410
|
+
),
|
|
11411
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
11412
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 text-[0.8rem] font-medium text-slate-500", children: [
|
|
11413
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime(currentTime / speed) }),
|
|
11414
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
11415
|
+
/* @__PURE__ */ jsxRuntime.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 }),
|
|
11416
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime(duration / speed) })
|
|
11417
|
+
] })
|
|
11418
|
+
] }),
|
|
11419
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11420
|
+
"div",
|
|
11421
|
+
{
|
|
11422
|
+
ref: scrubRef,
|
|
11423
|
+
role: "slider",
|
|
11424
|
+
"aria-label": "Narration progress",
|
|
11425
|
+
"aria-valuemin": 0,
|
|
11426
|
+
"aria-valuemax": Math.max(1, Math.floor(duration)),
|
|
11427
|
+
"aria-valuenow": Math.floor(currentTime),
|
|
11428
|
+
tabIndex: 0,
|
|
11429
|
+
onClick: (event) => {
|
|
11430
|
+
seekTo(event.clientX);
|
|
11431
|
+
},
|
|
11432
|
+
className: "relative mt-4 h-12 cursor-pointer overflow-hidden rounded-[1rem] bg-slate-200/90 px-1",
|
|
11433
|
+
children: peaks.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
11434
|
+
"svg",
|
|
11435
|
+
{
|
|
11436
|
+
viewBox: `0 0 ${peaks.length} 40`,
|
|
11437
|
+
preserveAspectRatio: "none",
|
|
11438
|
+
className: "pointer-events-none absolute inset-0 h-full w-full",
|
|
11439
|
+
children: peaks.map((peak, index) => {
|
|
11440
|
+
const height = Math.max(2, Math.round(Math.abs(peak) * 34));
|
|
11441
|
+
const y = (40 - height) / 2;
|
|
11442
|
+
const filled = index / peaks.length < progress;
|
|
11443
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
11444
|
+
"rect",
|
|
11445
|
+
{
|
|
11446
|
+
x: index,
|
|
11447
|
+
y,
|
|
11448
|
+
width: 0.82,
|
|
11449
|
+
height,
|
|
11450
|
+
rx: 0.4,
|
|
11451
|
+
className: filled ? "fill-blue-600" : "fill-blue-300"
|
|
11452
|
+
},
|
|
11453
|
+
`${audio.id}-${index}`
|
|
11454
|
+
);
|
|
11455
|
+
})
|
|
11456
|
+
}
|
|
11457
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
11458
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-slate-200/90" }),
|
|
11459
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11460
|
+
"div",
|
|
11461
|
+
{
|
|
11462
|
+
className: "absolute inset-y-0 left-0 bg-blue-600",
|
|
11463
|
+
style: { width: `${progress * 100}%` }
|
|
11464
|
+
}
|
|
11465
|
+
)
|
|
11466
|
+
] })
|
|
11467
|
+
}
|
|
11468
|
+
),
|
|
11469
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex flex-wrap items-center gap-2 text-[0.72rem] font-medium text-slate-500", children: [
|
|
11470
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rounded-full border border-slate-200 bg-white/85 px-3 py-1 text-slate-700", children: [
|
|
11471
|
+
"Provider: ",
|
|
11472
|
+
providerLabel
|
|
11473
|
+
] }),
|
|
11474
|
+
duration > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rounded-full border border-slate-200 bg-white/65 px-3 py-1", children: [
|
|
11475
|
+
"Length ",
|
|
11476
|
+
formatTime(duration / speed)
|
|
11477
|
+
] }),
|
|
11478
|
+
expiresLabel && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rounded-full border border-slate-200 bg-white/65 px-3 py-1", children: [
|
|
11479
|
+
"Expires ",
|
|
11480
|
+
expiresLabel
|
|
11481
|
+
] })
|
|
11482
|
+
] })
|
|
11483
|
+
] }),
|
|
11484
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2 self-start md:self-center", children: [
|
|
11485
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
11486
|
+
"button",
|
|
11487
|
+
{
|
|
11488
|
+
type: "button",
|
|
11489
|
+
onClick: () => {
|
|
11490
|
+
setSpeed((currentSpeed) => getNextSpeed(currentSpeed));
|
|
11491
|
+
},
|
|
11492
|
+
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",
|
|
11493
|
+
children: [
|
|
11494
|
+
speed,
|
|
11495
|
+
"\xD7"
|
|
11496
|
+
]
|
|
11497
|
+
}
|
|
11498
|
+
),
|
|
11499
|
+
onRefresh && /* @__PURE__ */ jsxRuntime.jsx(
|
|
11500
|
+
"button",
|
|
11501
|
+
{
|
|
11502
|
+
type: "button",
|
|
11503
|
+
onClick: onRefresh,
|
|
11504
|
+
disabled: refreshing,
|
|
11505
|
+
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",
|
|
11506
|
+
children: refreshing ? "Refreshing\u2026" : "Refresh"
|
|
11507
|
+
}
|
|
11508
|
+
)
|
|
11509
|
+
] })
|
|
11510
|
+
] })
|
|
11511
|
+
]
|
|
11512
|
+
}
|
|
11513
|
+
);
|
|
11514
|
+
}
|
|
11233
11515
|
|
|
11234
11516
|
// src/validation.ts
|
|
11235
11517
|
var phoneCountries = [
|
|
@@ -11531,6 +11813,7 @@ Object.defineProperty(exports, "createMoneyClient", {
|
|
|
11531
11813
|
get: function () { return money.createMoneyClient; }
|
|
11532
11814
|
});
|
|
11533
11815
|
exports.FeedbackWidget = FeedbackWidget;
|
|
11816
|
+
exports.NarrationPlayer = NarrationPlayer;
|
|
11534
11817
|
exports.ScaleMuleApiError = ScaleMuleApiError;
|
|
11535
11818
|
exports.ScaleMuleClient = ScaleMuleClient2;
|
|
11536
11819
|
exports.ScaleMuleMedia = ScaleMuleMedia;
|
|
@@ -11557,6 +11840,7 @@ exports.useRealtime = useRealtime;
|
|
|
11557
11840
|
exports.useScaleMule = useScaleMule;
|
|
11558
11841
|
exports.useScaleMuleClient = useScaleMuleClient;
|
|
11559
11842
|
exports.useShare = useShare;
|
|
11843
|
+
exports.useTtsJob = useTtsJob;
|
|
11560
11844
|
exports.useUser = useUser;
|
|
11561
11845
|
exports.validateForm = validateForm;
|
|
11562
11846
|
exports.validators = validators;
|