fix(media): disclose the transcription server before dictation uses it
Dictation falls back to the owner's transcription server when the voice provider cannot transcribe, so that server is a voice recipient too: it is listed for voice use (as "Transcription (<origin>)") whenever it would take the recording, and not when the voice provider transcribes itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
parent
97c7fd3a34
commit
ebac3ef9d1
@ -157,6 +157,27 @@ describe("consent grants", () => {
|
|||||||
expect(JSON.stringify(status)).not.toContain("comfy-key");
|
expect(JSON.stringify(status)).not.toContain("comfy-key");
|
||||||
expect((await aiConsentStatus(deps, actor, { uses: ["voice"] })).recipients).toHaveLength(1);
|
expect((await aiConsentStatus(deps, actor, { uses: ["voice"] })).recipients).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
it("discloses the transcription server for dictation when the voice provider cannot transcribe", async () => {
|
||||||
|
const { deps, actor } = setup();
|
||||||
|
vi.mocked(deps.prisma.integrationProviderConfig.findUnique).mockResolvedValue({
|
||||||
|
id: "media",
|
||||||
|
ciphertext: "fake-media-ciphertext-voice",
|
||||||
|
} as never);
|
||||||
|
deps.secrets = {
|
||||||
|
load: vi.fn(() =>
|
||||||
|
JSON.stringify({ comfyui: null, transcription: { baseUrl: "http://127.0.0.1:8080/v1" } }),
|
||||||
|
),
|
||||||
|
} as unknown as RouterDeps["secrets"];
|
||||||
|
vi.mocked(deps.prisma.spaceVoicePreference.findFirst).mockResolvedValue({
|
||||||
|
credential: { provider: "kokoro" },
|
||||||
|
voiceId: "af_heart",
|
||||||
|
} as never);
|
||||||
|
const status = await aiConsentStatus(deps, actor, { uses: ["voice"] });
|
||||||
|
expect(status.recipients.map((recipient) => [recipient.name, recipient.use])).toEqual([
|
||||||
|
["Local voice", "voice"],
|
||||||
|
["Transcription (http://127.0.0.1:8080)", "voice"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
it("returns an operator policy without requiring a hosted provider", async () => {
|
it("returns an operator policy without requiring a hosted provider", async () => {
|
||||||
const { deps, actor } = setup();
|
const { deps, actor } = setup();
|
||||||
deps.env.privacyPolicyUrl = "https://example.com/privacy";
|
deps.env.privacyPolicyUrl = "https://example.com/privacy";
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
parseModelSecret,
|
parseModelSecret,
|
||||||
selectConfiguredModel,
|
selectConfiguredModel,
|
||||||
toStringRecord,
|
toStringRecord,
|
||||||
|
voiceCatalogEntry,
|
||||||
} from "@rakazo/adapters";
|
} from "@rakazo/adapters";
|
||||||
import type { Actor, AiConsentQuery, AiConsentStatus, AiRecipient } from "@rakazo/contracts";
|
import type { Actor, AiConsentQuery, AiConsentStatus, AiRecipient } from "@rakazo/contracts";
|
||||||
import { AI_DATA_DISCLOSURES, AI_DISCLOSURE_VERSION, AI_PRIVACY_URL } from "@rakazo/contracts";
|
import { AI_DATA_DISCLOSURES, AI_DISCLOSURE_VERSION, AI_PRIVACY_URL } from "@rakazo/contracts";
|
||||||
@ -186,6 +187,21 @@ export async function aiConsentStatus(
|
|||||||
}
|
}
|
||||||
for (const voice of voices)
|
for (const voice of voices)
|
||||||
add(aiRecipient({ provider: voice.credential.provider, use: "voice" }));
|
add(aiRecipient({ provider: voice.credential.provider, use: "voice" }));
|
||||||
|
if (uses.includes("voice")) {
|
||||||
|
// Dictation goes to the owner's transcription server when the voice provider cannot transcribe.
|
||||||
|
const transcription = (await loadMediaSettings(deps)).transcription;
|
||||||
|
const credential = transcription ? await findDefaultVoiceCredential(deps.prisma, actor) : null;
|
||||||
|
if (transcription && !(credential && voiceCatalogEntry(credential.provider)?.transcribe)) {
|
||||||
|
add(
|
||||||
|
aiRecipient({
|
||||||
|
provider: MEDIA_TRANSCRIPTION_PROVIDER_ID,
|
||||||
|
use: "voice",
|
||||||
|
baseUrl: transcription.baseUrl,
|
||||||
|
modelId: transcription.model,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (includesMemory) {
|
if (includesMemory) {
|
||||||
if (memoryOwner) {
|
if (memoryOwner) {
|
||||||
const memory = await deps.prisma.spaceMemoryConfig.findUnique({
|
const memory = await deps.prisma.spaceMemoryConfig.findUnique({
|
||||||
|
|||||||
@ -52,6 +52,8 @@ export function aiRecipient(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recipientName(provider: string, use: AiDataUse) {
|
function recipientName(provider: string, use: AiDataUse) {
|
||||||
|
// The transcription server also takes dictation, so it keeps its name for voice use.
|
||||||
|
if (provider === MEDIA_TRANSCRIPTION_PROVIDER_ID) return "Transcription";
|
||||||
if (use === "voice") return voiceCatalogEntry(provider)?.name ?? provider;
|
if (use === "voice") return voiceCatalogEntry(provider)?.name ?? provider;
|
||||||
if (use === "memory") {
|
if (use === "memory") {
|
||||||
if (provider === "supermemory") return "Supermemory";
|
if (provider === "supermemory") return "Supermemory";
|
||||||
@ -61,6 +63,5 @@ function recipientName(provider: string, use: AiDataUse) {
|
|||||||
}
|
}
|
||||||
if (provider === "cursor") return "Cursor";
|
if (provider === "cursor") return "Cursor";
|
||||||
if (provider === MEDIA_COMFYUI_PROVIDER_ID) return "ComfyUI";
|
if (provider === MEDIA_COMFYUI_PROVIDER_ID) return "ComfyUI";
|
||||||
if (provider === MEDIA_TRANSCRIPTION_PROVIDER_ID) return "Transcription";
|
|
||||||
return listPiCatalog().find((entry) => entry.provider === provider)?.providerName ?? provider;
|
return listPiCatalog().find((entry) => entry.provider === provider)?.providerName ?? provider;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user