@opengeni/core 0.12.7 → 0.14.3
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/access/index.d.ts +22 -0
- package/dist/application/new-session-drafts.d.ts +14 -0
- package/dist/application/session-commands.d.ts +107 -0
- package/dist/billing/limits.d.ts +29 -0
- package/dist/dependencies.d.ts +137 -0
- package/dist/domain/capabilities.d.ts +62 -0
- package/dist/domain/environments.d.ts +33 -0
- package/dist/domain/insights.d.ts +11 -0
- package/dist/domain/packs.d.ts +27 -0
- package/dist/domain/resources.d.ts +32 -0
- package/dist/domain/scheduled-tasks.d.ts +72 -0
- package/dist/domain/session-tool-policy.d.ts +31 -0
- package/dist/domain/sessions.d.ts +256 -0
- package/dist/domain/slack-bot.d.ts +19 -0
- package/dist/domain/workspace-members.d.ts +34 -0
- package/dist/index.d.ts +23 -1189
- package/dist/index.js +758 -115
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-type.d.ts +2 -0
- package/dist/rigs/index.d.ts +57 -0
- package/dist/sandbox/fleet.d.ts +197 -0
- package/dist/sandbox/routing.d.ts +55 -0
- package/dist/sandbox-types.d.ts +52 -0
- package/dist/session-authorization.d.ts +36 -0
- package/dist/transcription.d.ts +71 -0
- package/dist/workflow-wake-contract.d.ts +4 -0
- package/package.json +11 -11
- package/src/access/index.ts +73 -2
- package/src/application/new-session-drafts.ts +3 -0
- package/src/application/session-commands.ts +22 -9
- package/src/dependencies.ts +5 -0
- package/src/domain/insights.ts +480 -0
- package/src/domain/session-tool-policy.ts +22 -39
- package/src/domain/sessions.ts +140 -72
- package/src/domain/slack-bot.ts +2 -4
- package/src/index.ts +2 -0
- package/src/sandbox/fleet.ts +96 -33
- package/src/sandbox/routing.ts +29 -7
- package/src/transcription.ts +142 -0
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from "./workflow-wake-contract";
|
|
|
35
35
|
// structural TYPES live here.
|
|
36
36
|
export * from "./sandbox-types";
|
|
37
37
|
export * from "./managed-auth-type";
|
|
38
|
+
export * from "./transcription";
|
|
38
39
|
|
|
39
40
|
// Sandbox fleet/routing service — the closure of `domain/sessions.ts`
|
|
40
41
|
// (`swapActiveSandbox` + `FleetContext`). apps/api re-imports these for its MCP
|
|
@@ -60,6 +61,7 @@ export * from "./domain/resources";
|
|
|
60
61
|
export * from "./domain/session-tool-policy";
|
|
61
62
|
export * from "./domain/scheduled-tasks";
|
|
62
63
|
export * from "./domain/sessions";
|
|
64
|
+
export * from "./domain/insights";
|
|
63
65
|
export * from "./domain/slack-bot";
|
|
64
66
|
export * from "./domain/workspace-members";
|
|
65
67
|
export * from "./application/new-session-drafts";
|
package/src/sandbox/fleet.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type BackendUnresolvableCode,
|
|
34
34
|
type ControlRpc,
|
|
35
35
|
type NatsRequestConnection,
|
|
36
|
+
type SelfhostedRelayConfig,
|
|
36
37
|
} from "@opengeni/runtime/sandbox";
|
|
37
38
|
import { HTTPException } from "hono/http-exception";
|
|
38
39
|
import { relayConfigFromSettings } from "./routing";
|
|
@@ -512,11 +513,94 @@ export type RunOnResult = {
|
|
|
512
513
|
stdout?: string;
|
|
513
514
|
stderr?: string;
|
|
514
515
|
exitCode?: number | null;
|
|
516
|
+
/** Exec only: whether the machine killed the child at its process deadline. */
|
|
517
|
+
timedOut?: boolean;
|
|
518
|
+
/** Exec only: the effective clamped process deadline enforced by the machine. */
|
|
519
|
+
deadlineMs?: number;
|
|
515
520
|
content?: string;
|
|
516
521
|
bytesWritten?: number;
|
|
517
522
|
reason?: string;
|
|
518
523
|
};
|
|
519
524
|
|
|
525
|
+
export type RunOnSelfhostedMachine = {
|
|
526
|
+
workspaceId: string;
|
|
527
|
+
agentId: string;
|
|
528
|
+
controlRpc: ControlRpc;
|
|
529
|
+
relay: SelfhostedRelayConfig;
|
|
530
|
+
/** Short request/reply deadline for read/write and other control operations. */
|
|
531
|
+
controlTimeoutMs: number;
|
|
532
|
+
/** Longer agent-side process deadline for exec. */
|
|
533
|
+
execTimeoutMs: number;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Execute the one-off machine operation once the workspace/enrollment lookup has
|
|
538
|
+
* succeeded. Kept separate from {@link runOnSandbox} so the command/deadline
|
|
539
|
+
* contract is deterministic against an in-memory ControlRpc without weakening
|
|
540
|
+
* the production ownership lookup or requiring a real machine.
|
|
541
|
+
*/
|
|
542
|
+
export async function executeRunOnSelfhostedMachine(
|
|
543
|
+
machine: RunOnSelfhostedMachine,
|
|
544
|
+
target: string,
|
|
545
|
+
op: RunOnOp,
|
|
546
|
+
): Promise<RunOnResult> {
|
|
547
|
+
const session = new SelfhostedSession({
|
|
548
|
+
workspaceId: machine.workspaceId,
|
|
549
|
+
agentId: machine.agentId,
|
|
550
|
+
controlRpc: machine.controlRpc,
|
|
551
|
+
relay: machine.relay,
|
|
552
|
+
timeoutMs: machine.controlTimeoutMs,
|
|
553
|
+
execTimeoutMs: machine.execTimeoutMs,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
if (op.kind === "exec") {
|
|
558
|
+
const deadlineMs = session.effectiveExecDeadlineMs;
|
|
559
|
+
const res = await session.exec({
|
|
560
|
+
cmd: op.cmd,
|
|
561
|
+
...(op.workdir ? { workdir: op.workdir } : {}),
|
|
562
|
+
});
|
|
563
|
+
const timedOut = res.timedOut === true;
|
|
564
|
+
const hasTerminalExit = res.exitCode !== null;
|
|
565
|
+
return {
|
|
566
|
+
target,
|
|
567
|
+
kind: "exec",
|
|
568
|
+
// `ok` means the one-off operation reached a terminal response. Preserve
|
|
569
|
+
// the established non-zero-exit behavior, but never claim success when
|
|
570
|
+
// the machine killed the child or returned no terminal exit proof.
|
|
571
|
+
ok: !timedOut && hasTerminalExit,
|
|
572
|
+
stdout: res.stdout,
|
|
573
|
+
stderr: res.stderr,
|
|
574
|
+
exitCode: res.exitCode,
|
|
575
|
+
timedOut,
|
|
576
|
+
deadlineMs,
|
|
577
|
+
...(timedOut
|
|
578
|
+
? { reason: `command exceeded the ${deadlineMs} ms execution deadline` }
|
|
579
|
+
: !hasTerminalExit
|
|
580
|
+
? { reason: "machine returned no terminal exit code" }
|
|
581
|
+
: {}),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
if (op.kind === "read") {
|
|
585
|
+
const bytes = await session.readFile({ path: op.path });
|
|
586
|
+
return { target, kind: "read", ok: true, content: new TextDecoder().decode(bytes) };
|
|
587
|
+
}
|
|
588
|
+
const bytesWritten = await session.writeFile({ path: op.path, content: op.content });
|
|
589
|
+
return { target, kind: "write", ok: true, bytesWritten };
|
|
590
|
+
} catch (error) {
|
|
591
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
592
|
+
return {
|
|
593
|
+
target,
|
|
594
|
+
kind: op.kind,
|
|
595
|
+
ok: false,
|
|
596
|
+
reason,
|
|
597
|
+
// A transport failure is not evidence that the process itself timed out,
|
|
598
|
+
// so leave `timedOut` absent while still reporting the enforced deadline.
|
|
599
|
+
...(op.kind === "exec" ? { deadlineMs: session.effectiveExecDeadlineMs } : {}),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
520
604
|
/**
|
|
521
605
|
* Run a ONE-OFF op against a SPECIFIC target WITHOUT changing the active pointer
|
|
522
606
|
* (the design `run_on`). Only selfhosted targets are routable as a one-off here
|
|
@@ -554,39 +638,18 @@ export async function runOnSandbox(
|
|
|
554
638
|
return { target, kind: op.kind, ok: false, reason: `sandbox ${target} is not enrolled/active` };
|
|
555
639
|
}
|
|
556
640
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
});
|
|
570
|
-
return {
|
|
571
|
-
target,
|
|
572
|
-
kind: "exec",
|
|
573
|
-
ok: true,
|
|
574
|
-
stdout: res.stdout,
|
|
575
|
-
stderr: res.stderr,
|
|
576
|
-
exitCode: res.exitCode,
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
if (op.kind === "read") {
|
|
580
|
-
const bytes = await session.readFile({ path: op.path });
|
|
581
|
-
return { target, kind: "read", ok: true, content: new TextDecoder().decode(bytes) };
|
|
582
|
-
}
|
|
583
|
-
// write
|
|
584
|
-
const bytesWritten = await session.writeFile({ path: op.path, content: op.content });
|
|
585
|
-
return { target, kind: "write", ok: true, bytesWritten };
|
|
586
|
-
} catch (error) {
|
|
587
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
588
|
-
return { target, kind: op.kind, ok: false, reason };
|
|
589
|
-
}
|
|
641
|
+
return executeRunOnSelfhostedMachine(
|
|
642
|
+
{
|
|
643
|
+
workspaceId: ctx.workspaceId,
|
|
644
|
+
agentId: sandbox.enrollmentId,
|
|
645
|
+
controlRpc: controlRpc(services.bus),
|
|
646
|
+
relay: relayConfigFromSettings(services.settings),
|
|
647
|
+
controlTimeoutMs: services.settings.sandboxSelfhostedControlTimeoutMs,
|
|
648
|
+
execTimeoutMs: services.settings.sandboxSelfhostedExecTimeoutMs,
|
|
649
|
+
},
|
|
650
|
+
target,
|
|
651
|
+
op,
|
|
652
|
+
);
|
|
590
653
|
}
|
|
591
654
|
|
|
592
655
|
export type ProvisionResult =
|
package/src/sandbox/routing.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// The DB-coupled glue (readActiveSandbox / getSandbox / the selfhosted ControlRpc
|
|
12
12
|
// over the events bus) lives here, not in the leaf (which stays db-free).
|
|
13
13
|
|
|
14
|
-
import type
|
|
14
|
+
import { sandboxArchiveCaptureTimeoutMs, type Settings } from "@opengeni/config";
|
|
15
15
|
import {
|
|
16
16
|
advanceWorkspaceGenerationForDirectRequest,
|
|
17
17
|
advanceWorkspaceGenerationForRetainedProcess,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
makeActiveBackendResolver,
|
|
34
34
|
NatsControlRpc,
|
|
35
35
|
RoutingSandboxSession,
|
|
36
|
+
resolveModalCheckpointProviderBindingForSession,
|
|
36
37
|
type ControlRpc,
|
|
37
38
|
type EstablishedSandboxSession,
|
|
38
39
|
type NatsRequestConnection,
|
|
@@ -44,6 +45,13 @@ import {
|
|
|
44
45
|
type SelfhostedRelayConfig,
|
|
45
46
|
} from "@opengeni/runtime/sandbox";
|
|
46
47
|
|
|
48
|
+
type PersistableMutationAdmission = {
|
|
49
|
+
admission: SandboxWorkspaceMutationAdmission;
|
|
50
|
+
providerBinding: Awaited<
|
|
51
|
+
ReturnType<typeof resolveModalCheckpointProviderBindingForSession>
|
|
52
|
+
> | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
47
55
|
export type ChannelARoutingServices = {
|
|
48
56
|
db: Database;
|
|
49
57
|
settings: Settings;
|
|
@@ -151,7 +159,7 @@ export function wrapChannelABoxWithRouting(
|
|
|
151
159
|
}: {
|
|
152
160
|
op: string;
|
|
153
161
|
backend: ResolvedActiveBackend;
|
|
154
|
-
}): Promise<
|
|
162
|
+
}): Promise<PersistableMutationAdmission | null> => {
|
|
155
163
|
// Connected Machines and other non-persistable targets intentionally do
|
|
156
164
|
// not dirty or advance the cloud-home archive generation.
|
|
157
165
|
if (
|
|
@@ -164,7 +172,11 @@ export function wrapChannelABoxWithRouting(
|
|
|
164
172
|
if (backend.activeEpoch === undefined) {
|
|
165
173
|
throw new Error("API-direct workspace mutation resolved without an active route epoch");
|
|
166
174
|
}
|
|
167
|
-
|
|
175
|
+
const providerBinding =
|
|
176
|
+
homeLease.backend === "modal"
|
|
177
|
+
? await resolveModalCheckpointProviderBindingForSession(settings, backend.session)
|
|
178
|
+
: null;
|
|
179
|
+
const admission = await advanceWorkspaceGenerationForDirectRequest(db, {
|
|
168
180
|
accountId: ids.accountId,
|
|
169
181
|
workspaceId: ids.workspaceId,
|
|
170
182
|
sessionId: ids.sessionId,
|
|
@@ -176,7 +188,9 @@ export function wrapChannelABoxWithRouting(
|
|
|
176
188
|
routeTargetId: backend.sandboxId,
|
|
177
189
|
routeEpoch: backend.activeEpoch,
|
|
178
190
|
operation: op,
|
|
191
|
+
captureWaitMs: sandboxArchiveCaptureTimeoutMs(settings),
|
|
179
192
|
});
|
|
193
|
+
return { admission, providerBinding };
|
|
180
194
|
}
|
|
181
195
|
: undefined;
|
|
182
196
|
const afterMutation = homeLease
|
|
@@ -198,16 +212,22 @@ export function wrapChannelABoxWithRouting(
|
|
|
198
212
|
if (
|
|
199
213
|
!admission ||
|
|
200
214
|
typeof admission !== "object" ||
|
|
201
|
-
typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).id !== "string" ||
|
|
202
|
-
typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).workspaceGeneration !==
|
|
203
|
-
"number" ||
|
|
204
215
|
backend.leaseEpoch === undefined ||
|
|
205
216
|
backend.providerInstanceId === undefined ||
|
|
206
217
|
backend.activeEpoch === undefined
|
|
207
218
|
) {
|
|
208
219
|
throw new Error("API-direct workspace mutation settlement lacked its exact admission");
|
|
209
220
|
}
|
|
210
|
-
const
|
|
221
|
+
const boundAdmission = admission as Partial<PersistableMutationAdmission>;
|
|
222
|
+
const exactAdmission = boundAdmission.admission;
|
|
223
|
+
if (
|
|
224
|
+
!exactAdmission ||
|
|
225
|
+
typeof exactAdmission.id !== "string" ||
|
|
226
|
+
typeof exactAdmission.workspaceGeneration !== "number" ||
|
|
227
|
+
!("providerBinding" in boundAdmission)
|
|
228
|
+
) {
|
|
229
|
+
throw new Error("API-direct workspace mutation settlement lacked its bound admission");
|
|
230
|
+
}
|
|
211
231
|
if (outcome === "resolved" && retainedProcess) {
|
|
212
232
|
await retainWorkspaceMutationProcess(db, {
|
|
213
233
|
accountId: ids.accountId,
|
|
@@ -218,6 +238,7 @@ export function wrapChannelABoxWithRouting(
|
|
|
218
238
|
admissionId: exactAdmission.id,
|
|
219
239
|
admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
|
|
220
240
|
operation: op,
|
|
241
|
+
providerBinding: boundAdmission.providerBinding ?? null,
|
|
221
242
|
owner: {
|
|
222
243
|
kind: "direct",
|
|
223
244
|
requestId: ids.directRequest.requestId,
|
|
@@ -263,6 +284,7 @@ export function wrapChannelABoxWithRouting(
|
|
|
263
284
|
sessionId: ids.sessionId,
|
|
264
285
|
processId: process.id,
|
|
265
286
|
operation: op,
|
|
287
|
+
captureWaitMs: sandboxArchiveCaptureTimeoutMs(settings),
|
|
266
288
|
})
|
|
267
289
|
: undefined;
|
|
268
290
|
const afterProcessMutation = homeLease
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { TranscribeAudioResponse, VoiceInputErrorCode } from "@opengeni/contracts";
|
|
2
|
+
|
|
3
|
+
export type TranscriptionLimits = {
|
|
4
|
+
maxDurationSeconds: number;
|
|
5
|
+
maxSizeBytes: number;
|
|
6
|
+
acceptedMimeTypes: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type TranscriptionRequest = {
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
accountId: string;
|
|
12
|
+
audio: Uint8Array;
|
|
13
|
+
mimeType: string;
|
|
14
|
+
/** Optional client-reported duration; enforced as a soft ceiling before upstream. */
|
|
15
|
+
durationSeconds?: number | undefined;
|
|
16
|
+
signal?: AbortSignal | undefined;
|
|
17
|
+
requestId: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type TranscriptionResult = TranscribeAudioResponse & {
|
|
21
|
+
/** Server-private provider id for operational metrics only. Never returned to clients. */
|
|
22
|
+
providerId: string;
|
|
23
|
+
audioSeconds: number;
|
|
24
|
+
latencyMs: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class TranscriptionServiceError extends Error {
|
|
28
|
+
readonly code: VoiceInputErrorCode;
|
|
29
|
+
readonly status: number;
|
|
30
|
+
readonly retryable: boolean;
|
|
31
|
+
|
|
32
|
+
constructor(input: {
|
|
33
|
+
code: VoiceInputErrorCode;
|
|
34
|
+
message: string;
|
|
35
|
+
status?: number;
|
|
36
|
+
retryable?: boolean;
|
|
37
|
+
}) {
|
|
38
|
+
super(input.message);
|
|
39
|
+
this.name = "TranscriptionServiceError";
|
|
40
|
+
this.code = input.code;
|
|
41
|
+
this.status = input.status ?? statusForVoiceInputError(input.code);
|
|
42
|
+
this.retryable = input.retryable ?? false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function statusForVoiceInputError(code: VoiceInputErrorCode): number {
|
|
47
|
+
switch (code) {
|
|
48
|
+
case "permission_denied":
|
|
49
|
+
return 403;
|
|
50
|
+
case "policy_blocked":
|
|
51
|
+
return 403;
|
|
52
|
+
case "not_supported":
|
|
53
|
+
return 415;
|
|
54
|
+
case "unavailable":
|
|
55
|
+
return 503;
|
|
56
|
+
case "too_large":
|
|
57
|
+
return 413;
|
|
58
|
+
case "invalid_audio":
|
|
59
|
+
return 400;
|
|
60
|
+
case "timeout":
|
|
61
|
+
return 504;
|
|
62
|
+
case "cancelled":
|
|
63
|
+
return 499;
|
|
64
|
+
case "network":
|
|
65
|
+
case "provider":
|
|
66
|
+
return 502;
|
|
67
|
+
case "unknown":
|
|
68
|
+
default:
|
|
69
|
+
return 500;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Optional workspace scope for readiness checks during provider selection. */
|
|
74
|
+
export type TranscriptionAvailabilityContext = {
|
|
75
|
+
workspaceId?: string | undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extensible transcription provider port. Implementations own credentials and
|
|
80
|
+
* upstream request shape. Selection happens before audio is sent; providers must
|
|
81
|
+
* not fall back to another vendor after an upstream request may have started.
|
|
82
|
+
*/
|
|
83
|
+
export type TranscriptionProvider = {
|
|
84
|
+
readonly id: string;
|
|
85
|
+
readonly experimental?: boolean | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Deployment readiness when called without a workspace. When `workspaceId` is
|
|
88
|
+
* provided, providers may require a workspace-attached credential (e.g. Codex).
|
|
89
|
+
*/
|
|
90
|
+
available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
|
|
91
|
+
transcribe(input: {
|
|
92
|
+
audio: Uint8Array;
|
|
93
|
+
mimeType: string;
|
|
94
|
+
filename: string;
|
|
95
|
+
workspaceId: string;
|
|
96
|
+
signal?: AbortSignal | undefined;
|
|
97
|
+
}): Promise<{ text: string; languages: string[] }>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type TranscriptionService = {
|
|
101
|
+
limits(): TranscriptionLimits;
|
|
102
|
+
/** True when at least one ready provider can serve requests. */
|
|
103
|
+
available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
|
|
104
|
+
transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function normalizeMimeType(mimeType: string): string {
|
|
108
|
+
return mimeType.trim().toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isAcceptedMimeType(mimeType: string, accepted: readonly string[]): boolean {
|
|
112
|
+
const normalized = normalizeMimeType(mimeType);
|
|
113
|
+
if (accepted.some((candidate) => normalizeMimeType(candidate) === normalized)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// Allow bare type matches against codec-qualified allowlist entries.
|
|
117
|
+
const bare = normalized.split(";")[0]?.trim() ?? normalized;
|
|
118
|
+
return accepted.some((candidate) => {
|
|
119
|
+
const allowed = normalizeMimeType(candidate);
|
|
120
|
+
return allowed === bare || allowed.split(";")[0]?.trim() === bare;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function filenameForMimeType(mimeType: string): string {
|
|
125
|
+
const bare = normalizeMimeType(mimeType).split(";")[0] ?? "audio/webm";
|
|
126
|
+
switch (bare) {
|
|
127
|
+
case "audio/mp4":
|
|
128
|
+
case "audio/m4a":
|
|
129
|
+
return "audio.mp4";
|
|
130
|
+
case "audio/ogg":
|
|
131
|
+
return "audio.ogg";
|
|
132
|
+
case "audio/mpeg":
|
|
133
|
+
case "audio/mp3":
|
|
134
|
+
return "audio.mp3";
|
|
135
|
+
case "audio/wav":
|
|
136
|
+
case "audio/x-wav":
|
|
137
|
+
return "audio.wav";
|
|
138
|
+
case "audio/webm":
|
|
139
|
+
default:
|
|
140
|
+
return "audio.webm";
|
|
141
|
+
}
|
|
142
|
+
}
|