speech.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /**
  2. * Which connection dictation is written down by.
  3. *
  4. * The speech-to-text connection when the workshop has one; otherwise the chat
  5. * provider, when that vendor has a speech model (OpenAI and the servers that
  6. * speak its API do, Anthropic does not); otherwise nothing, and the browser's
  7. * own recognition is all there is.
  8. */
  9. import { db } from '@/lib/db'
  10. import { isCloudInstance } from '@/lib/cloud-instance'
  11. import { isDemoMode } from '@/lib/demo'
  12. import { canTranscribe, transcriptionModel } from '@/features/ai/Lib/transcription'
  13. import { aiSetup, configuredAiProvider } from './ai'
  14. import { openCredentials } from './vault'
  15. export const SPEECH_CONNECTOR_ID = 'speech-to-text'
  16. /**
  17. * Who decides how a dictation is written down. 'ai' is always the speech
  18. * model; 'choice' lets each person pick it or the browser from the mic's menu.
  19. */
  20. export type DictationMode = 'ai' | 'choice'
  21. export interface SpeechSetup {
  22. /** The client to build: OpenAI's own address, or a server the workshop named. */
  23. provider: 'openai' | 'openai-compatible'
  24. apiKey: string
  25. baseUrl?: string
  26. model: string
  27. }
  28. function settingOf(settings: unknown, key: string): string {
  29. const value = (settings as Record<string, unknown> | null)?.[key]
  30. return typeof value === 'string' ? value.trim() : ''
  31. }
  32. export function dictationModeOf(settings: unknown): DictationMode {
  33. return settingOf(settings, 'mode') === 'choice' ? 'choice' : 'ai'
  34. }
  35. async function speechConnection(organizationId: string) {
  36. return db.integrationConnection.findFirst({
  37. where: { organizationId, connectorId: SPEECH_CONNECTOR_ID, status: 'active' },
  38. select: { credentials: true, settings: true },
  39. })
  40. }
  41. /**
  42. * What a page needs to know to draw the mic, without opening any credentials:
  43. * whether a speech model is there at all, and who chooses the engine. With
  44. * no speech connection the chat provider stands in, and people may choose,
  45. * since nobody has said otherwise.
  46. */
  47. export async function configuredDictation(
  48. organizationId: string
  49. ): Promise<{ available: boolean; mode: DictationMode }> {
  50. // The mic falls back to the browser's own recognition, which never leaves it.
  51. if (isDemoMode) return { available: false, mode: 'choice' }
  52. const connection = await speechConnection(organizationId)
  53. if (connection) return { available: true, mode: dictationModeOf(connection.settings) }
  54. const provider = await configuredAiProvider(organizationId)
  55. return { available: canTranscribe(provider), mode: 'choice' }
  56. }
  57. /** The keys and the model, for the one request that sends a recording. */
  58. export async function speechSetup(organizationId: string): Promise<SpeechSetup | null> {
  59. // A recording is somebody's voice, sent to a vendor on somebody's key; the
  60. // demo has neither to offer. See assertAiAllowed.
  61. if (isDemoMode) return null
  62. const connection = await speechConnection(organizationId)
  63. if (connection) {
  64. const credentials = openCredentials(connection.credentials)
  65. const apiKey = typeof credentials.apiKey === 'string' ? credentials.apiKey : ''
  66. // A self-hosted field: never read on the cloud instance, whatever is stored.
  67. const baseUrl =
  68. !isCloudInstance() && typeof credentials.baseUrl === 'string'
  69. ? credentials.baseUrl.trim()
  70. : ''
  71. const provider = baseUrl ? 'openai-compatible' : 'openai'
  72. if (provider === 'openai' && !apiKey) return null
  73. return {
  74. provider,
  75. apiKey,
  76. ...(baseUrl ? { baseUrl } : {}),
  77. model: settingOf(connection.settings, 'model') || transcriptionModel(provider),
  78. }
  79. }
  80. const chat = await aiSetup(organizationId)
  81. if (!chat || !canTranscribe(chat.provider)) return null
  82. const provider = chat.provider === 'openai' ? 'openai' : 'openai-compatible'
  83. return {
  84. provider,
  85. apiKey: chat.apiKey,
  86. ...(chat.baseUrl ? { baseUrl: chat.baseUrl } : {}),
  87. model: transcriptionModel(provider),
  88. }
  89. }