@agimon-ai/doompi-voice 0.0.1-alpha.0

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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.cjs +1 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.mjs +1 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.cjs +1 -0
  6. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.mjs +1 -0
  7. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.cjs +1 -0
  8. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.mjs +1 -0
  9. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  10. package/dist/autoCapture.cjs +8 -0
  11. package/dist/autoCapture.cjs.map +1 -0
  12. package/dist/autoCapture.d.cts +223 -0
  13. package/dist/autoCapture.d.cts.map +1 -0
  14. package/dist/autoCapture.d.mts +223 -0
  15. package/dist/autoCapture.d.mts.map +1 -0
  16. package/dist/autoCapture.mjs +8 -0
  17. package/dist/autoCapture.mjs.map +1 -0
  18. package/dist/catalog.cjs +2 -0
  19. package/dist/catalog.cjs.map +1 -0
  20. package/dist/catalog.mjs +2 -0
  21. package/dist/catalog.mjs.map +1 -0
  22. package/dist/download.cjs +2 -0
  23. package/dist/download.cjs.map +1 -0
  24. package/dist/download.mjs +2 -0
  25. package/dist/download.mjs.map +1 -0
  26. package/dist/extensions/doom.cjs +2 -0
  27. package/dist/extensions/doom.cjs.map +1 -0
  28. package/dist/extensions/doom.d.cts +6 -0
  29. package/dist/extensions/doom.d.cts.map +1 -0
  30. package/dist/extensions/doom.d.mts +6 -0
  31. package/dist/extensions/doom.d.mts.map +1 -0
  32. package/dist/extensions/doom.mjs +2 -0
  33. package/dist/extensions/doom.mjs.map +1 -0
  34. package/dist/extensions/pi.cjs +2 -0
  35. package/dist/extensions/pi.cjs.map +1 -0
  36. package/dist/extensions/pi.d.cts +2 -0
  37. package/dist/extensions/pi.d.mts +2 -0
  38. package/dist/extensions/pi.mjs +2 -0
  39. package/dist/extensions/pi.mjs.map +1 -0
  40. package/dist/index.cjs +1 -0
  41. package/dist/index.d.cts +9 -0
  42. package/dist/index.d.mts +9 -0
  43. package/dist/index.mjs +1 -0
  44. package/dist/infrastructure.cjs +3 -0
  45. package/dist/infrastructure.cjs.map +1 -0
  46. package/dist/infrastructure.d.cts +55 -0
  47. package/dist/infrastructure.d.cts.map +1 -0
  48. package/dist/infrastructure.d.mts +55 -0
  49. package/dist/infrastructure.d.mts.map +1 -0
  50. package/dist/infrastructure.mjs +3 -0
  51. package/dist/infrastructure.mjs.map +1 -0
  52. package/dist/install.cjs +2 -0
  53. package/dist/install.cjs.map +1 -0
  54. package/dist/install.mjs +2 -0
  55. package/dist/install.mjs.map +1 -0
  56. package/dist/narration.cjs +8 -0
  57. package/dist/narration.cjs.map +1 -0
  58. package/dist/narration.d.cts +110 -0
  59. package/dist/narration.d.cts.map +1 -0
  60. package/dist/narration.d.mts +110 -0
  61. package/dist/narration.d.mts.map +1 -0
  62. package/dist/narration.mjs +8 -0
  63. package/dist/narration.mjs.map +1 -0
  64. package/dist/pcm.cjs +2 -0
  65. package/dist/pcm.cjs.map +1 -0
  66. package/dist/pcm.d.cts +16 -0
  67. package/dist/pcm.d.cts.map +1 -0
  68. package/dist/pcm.d.mts +16 -0
  69. package/dist/pcm.d.mts.map +1 -0
  70. package/dist/pcm.mjs +2 -0
  71. package/dist/pcm.mjs.map +1 -0
  72. package/dist/semanticEcho.cjs +5 -0
  73. package/dist/semanticEcho.cjs.map +1 -0
  74. package/dist/semanticEcho.d.cts +100 -0
  75. package/dist/semanticEcho.d.cts.map +1 -0
  76. package/dist/semanticEcho.d.mts +100 -0
  77. package/dist/semanticEcho.d.mts.map +1 -0
  78. package/dist/semanticEcho.mjs +5 -0
  79. package/dist/semanticEcho.mjs.map +1 -0
  80. package/dist/types.cjs +2 -0
  81. package/dist/types.cjs.map +1 -0
  82. package/dist/types.d.cts +164 -0
  83. package/dist/types.d.cts.map +1 -0
  84. package/dist/types.d.mts +164 -0
  85. package/dist/types.d.mts.map +1 -0
  86. package/dist/types.mjs +2 -0
  87. package/dist/types.mjs.map +1 -0
  88. package/dist/vad.cjs +2 -0
  89. package/dist/vad.cjs.map +1 -0
  90. package/dist/vad.d.cts +72 -0
  91. package/dist/vad.d.cts.map +1 -0
  92. package/dist/vad.d.mts +72 -0
  93. package/dist/vad.d.mts.map +1 -0
  94. package/dist/vad.mjs +2 -0
  95. package/dist/vad.mjs.map +1 -0
  96. package/dist/voice.cjs +2 -0
  97. package/dist/voice.cjs.map +1 -0
  98. package/dist/voice.d.cts +128 -0
  99. package/dist/voice.d.cts.map +1 -0
  100. package/dist/voice.d.mts +128 -0
  101. package/dist/voice.d.mts.map +1 -0
  102. package/dist/voice.mjs +2 -0
  103. package/dist/voice.mjs.map +1 -0
  104. package/dist/voiceConfig.cjs +2 -0
  105. package/dist/voiceConfig.cjs.map +1 -0
  106. package/dist/voiceConfig.mjs +2 -0
  107. package/dist/voiceConfig.mjs.map +1 -0
  108. package/package.json +72 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice.mjs","names":[],"sources":["../src/voice.ts"],"sourcesContent":["import 'reflect-metadata';\nimport { createDoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport {\n DOOM_CONFIG_TYPES,\n globalDoomConfigPath,\n mergeDoomConfigs,\n parseDoomConfig,\n repositoryDoomConfigPath,\n resolveVoiceConfig,\n type DoomConfig,\n type IDoomConfigService,\n type ResolvedVoiceConfig,\n type VoiceAdapterConfig,\n type VoiceEngine,\n} from '@agimon-ai/doompi-config';\nimport type { LeaderBinding } from '@agimon-ai/doompi-extension-contracts/leader';\nimport {\n AutoCaptureController,\n VoiceAutoCaptureAdjudicator,\n type AutoCaptureModelContracts,\n type AutoCaptureModelRequest,\n type IAutoCaptureModelClient,\n} from './autoCapture.ts';\nimport { NarrationGenerator, type INarrationModelClient, type NarrationModelRequest } from './narration.ts';\nimport { SemanticEchoAdjudicator, type EchoModelRequest, type IEchoModelClient } from './semanticEcho.ts';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { Container, ContainerModule, inject, injectable } from 'inversify';\nimport {\n ExecutableResolver,\n FfmpegAudioRecorder,\n FfmpegPcmAudioRecorder,\n MacOsSayTtsAdapter,\n NodeBinaryProcessSpawner,\n NodeProcessSpawner,\n PcmWavAnalyzer,\n SystemClock,\n TemporaryWorkspace,\n} from './infrastructure.ts';\nimport {\n DOOM_VOICE_TYPES,\n type AutoCaptureIndicatorState,\n type AutoCaptureUi,\n type IAudioAnalyzer,\n type IAudioRecorder,\n type IBinaryProcessSpawner,\n type IClock,\n type IExecutableResolver,\n type IPcmAudioRecorder,\n type IProcessSpawner,\n type ITtsAdapter,\n type ITemporaryWorkspace,\n type ITranscriberAdapter,\n type ITranscriberRegistry,\n type IVoiceSessionController,\n type RecordingHandle,\n type SelectedTranscriber,\n type TimerHandle,\n type TranscriptionRequest,\n type VoiceActivityUpdate,\n type VoiceState,\n type VoiceUi,\n} from './types.ts';\n\nconst COMMAND_NAME = 'voice';\nconst AUTO_COMMAND_NAME = 'voice-auto';\nconst STATUS_KEY = 'doom-voice';\nconst MAX_RECORDING_MS = 300_000;\nconst ACTIVITY_INTERVAL_MS = 120;\nconst LEADER_GROUP_ORDER = 67;\nconst AUTO_LEADER_DETAIL = 'continuous autonomous conversation';\nconst MAX_ASK_USER_SPEECH_CHARACTERS = 240;\nconst MAX_ASK_USER_OPTIONS = 8;\n// Channel names from @juicesharp/rpiv-ask-user-question/events. Inlined rather than imported\n// because that package ships raw .ts and Node cannot strip types under node_modules. Its event\n// contract pins these names as immutable (breaking changes ship a new channel), so the literals\n// are safe to duplicate.\nconst ASK_USER_PROMPT_EVENT = 'rpiv:ask-user:prompt';\nconst ASK_USER_BLOCKED_EVENT = 'rpiv:ask-user:blocked';\n\nconst ASK_USER_PATH_PATTERN = /\\/(?:Users|home|private|tmp|var)\\/[^\\s,;:]+/gu;\nconst ASK_USER_SECRET_PATTERN = /\\b(?:sk|api|token|key)[-_][A-Za-z0-9_-]{12,}\\b/giu;\nconst ASK_USER_CODE_PATTERN = /`[^`]*`|<\\/?[A-Za-z][^>]*>|\\$\\([^)]*\\)|\\{[^}]*\\}/gu;\nconst RECORDING_FRAMES = ['·', '•', '●', '•'] as const;\nconst TRANSCRIBING_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const;\nconst AUTO_LANGUAGE = 'auto';\nconst WHISPER_CPP_ENGINE = 'whisper-cpp' as const;\nconst OPENAI_WHISPER_ENGINE = 'openai-whisper' as const;\nconst MLX_WHISPER_ENGINE = 'mlx-whisper' as const;\nconst WHISPER_CPP_BINARY = 'whisper-cli';\nconst OPENAI_WHISPER_BINARY = 'whisper';\nconst MLX_WHISPER_BINARY = 'mlx_whisper';\nconst TEXT_OUTPUT_FORMAT = 'txt';\nconst INFO_NOTIFICATION = 'info';\nconst ERROR_NOTIFICATION = 'error';\nconst RECORDING_REQUESTED_EVENT = 'doom_voice.recording_requested';\nconst RECORDING_STARTED_EVENT = 'doom_voice.recording_started';\nconst RECORDING_FAILED_EVENT = 'doom_voice.recording_failed';\nconst RECORDING_FINISHED_EVENT = 'doom_voice.recording_finished';\nconst TRANSCRIPTION_STARTED_EVENT = 'doom_voice.transcription_started';\nconst TRANSCRIPTION_FINISHED_EVENT = 'doom_voice.transcription_finished';\nconst TRANSCRIPTION_FAILED_EVENT = 'doom_voice.transcription_failed';\nconst IDLE_STATE = 'idle' as const;\nconst RECORDING_STATE = 'recording' as const;\nconst TRANSCRIBING_STATE = 'transcribing' as const;\n\ninterface FooterContributionValue {\n fullText: string;\n compactText: string;\n fullSegments: ReadonlyArray<{ text: string; color: 'warning' | 'accent' }>;\n compactSegments: ReadonlyArray<{ text: string; color: 'warning' | 'accent' }>;\n}\n\ninterface FooterContributionHandle {\n update(value: FooterContributionValue | undefined): void;\n dispose(): void;\n}\n\nexport const VOICE_LEADER_BINDINGS: readonly LeaderBinding[] = [\n {\n id: 'voice.toggle',\n path: [\n { key: 'v', label: 'voice', order: LEADER_GROUP_ORDER },\n { key: 'v', label: 'toggle', detail: 'record or transcribe' },\n ],\n command: { name: COMMAND_NAME },\n },\n {\n id: 'voice.auto-toggle',\n path: [\n { key: 'v', label: 'voice', order: LEADER_GROUP_ORDER },\n { key: 'a', label: 'auto', detail: AUTO_LEADER_DETAIL },\n ],\n command: { name: AUTO_COMMAND_NAME },\n },\n];\n\nfunction assertModelPath(config: VoiceAdapterConfig): void {\n if (config.model.path && !fs.existsSync(config.model.path))\n throw new Error(`Voice model not found: ${config.model.path}`);\n}\nfunction requireModel(config: VoiceAdapterConfig): string {\n const model = config.model.path ?? config.model.id;\n if (!model) throw new Error('Voice adapter requires a model path or id');\n return model;\n}\nfunction assertSuccess(engine: string, result: { code: number; stderr: string }): void {\n if (result.code !== 0)\n throw new Error(`${engine} transcription failed: ${result.stderr || `exit code ${result.code}`}`);\n}\n\nexport interface VoiceActivityPresentation {\n statusText: string;\n footer: FooterContributionValue;\n}\n\nexport interface AutoCaptureActivityPresentation {\n statusText: string;\n footer: FooterContributionValue;\n}\n\nexport function formatVoiceActivity(update: VoiceActivityUpdate): VoiceActivityPresentation {\n const frames = update.state === RECORDING_STATE ? RECORDING_FRAMES : TRANSCRIBING_FRAMES;\n const frameIndex = Math.max(0, Math.floor(update.frameIndex)) % frames.length;\n const frame = frames[frameIndex]!;\n const color = update.state === RECORDING_STATE ? ('warning' as const) : ('accent' as const);\n const elapsed = Math.max(0, Math.floor(update.elapsedSeconds ?? 0));\n const statusText =\n update.state === RECORDING_STATE\n ? `${frame} voice: recording ${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, '0')}`\n : `${frame} voice: transcribing`;\n return {\n statusText,\n footer: {\n fullText: frame,\n compactText: frame,\n fullSegments: [{ text: frame, color }],\n compactSegments: [{ text: frame, color }],\n },\n };\n}\n\nfunction createVoiceUi(context: ExtensionContext, footer: FooterContributionHandle): VoiceUi {\n return {\n notify: (message, level) => context.ui.notify(message, level),\n setStatus: (key, value) => context.ui.setStatus(key, value),\n setIndicator: (update) => footer.update(update ? formatVoiceActivity(update).footer : undefined),\n getEditorText: () => context.ui.getEditorText(),\n setEditorText: (text) => context.ui.setEditorText(text),\n };\n}\n\nexport function formatAutoCaptureActivity(state: AutoCaptureIndicatorState): AutoCaptureActivityPresentation {\n const presentation =\n state === 'listening'\n ? { frame: 'A', statusText: 'voice auto: listening', color: 'accent' as const }\n : state === 'speech'\n ? { frame: '!', statusText: 'voice auto: hearing speech', color: 'warning' as const }\n : state === 'processing'\n ? { frame: '…', statusText: 'voice auto: processing while listening', color: 'accent' as const }\n : state === 'narrating'\n ? { frame: 'A', statusText: 'voice auto: narrating and listening', color: 'accent' as const }\n : state === 'confirming'\n ? { frame: '?', statusText: 'voice auto: confirmation needed', color: 'warning' as const }\n : state === 'waiting'\n ? { frame: '…', statusText: 'voice auto: waiting for keyboard input', color: 'warning' as const }\n : { frame: 'A', statusText: 'voice auto: draining', color: 'warning' as const };\n return {\n statusText: presentation.statusText,\n footer: {\n fullText: presentation.frame,\n compactText: presentation.frame,\n fullSegments: [{ text: presentation.frame, color: presentation.color }],\n compactSegments: [{ text: presentation.frame, color: presentation.color }],\n },\n };\n}\n\nfunction createAutoCaptureUi(context: ExtensionContext, footer: FooterContributionHandle): AutoCaptureUi {\n return {\n notify: (message, level) => context.ui.notify(message, level),\n setStatus: (value) => context.ui.setStatus(STATUS_KEY, value),\n setIndicator: (state) => footer.update(state ? formatAutoCaptureActivity(state).footer : undefined),\n };\n}\n\ntype PiVoiceModelRequest = AutoCaptureModelRequest | EchoModelRequest | NarrationModelRequest;\ntype PiVoiceModelClient = IAutoCaptureModelClient & IEchoModelClient & INarrationModelClient;\nexport type AutoCaptureDeliveryIntent = 'immediate' | 'queuedFollowUp';\n\nexport function deliverAutoCaptureInput(\n pi: Pick<ExtensionAPI, 'sendUserMessage'>,\n context: Pick<ExtensionContext, 'isIdle'>,\n text: string,\n intent: AutoCaptureDeliveryIntent = 'immediate',\n): void {\n if (context.isIdle()) {\n pi.sendUserMessage(text);\n return;\n }\n pi.sendUserMessage(text, { deliverAs: intent === 'queuedFollowUp' ? 'followUp' : 'steer' });\n}\n\nexport interface AutoCapturePiEventController {\n observeInput(event: {\n text: string;\n source: 'interactive' | 'rpc' | 'extension';\n streamingBehavior?: 'steer' | 'followUp';\n }): void;\n beforeAgentStart(prompt: string): void;\n agentStarted(): void;\n finalizedAssistantIntent(sourceText: string): void;\n planWritten(): void;\n askUserPrompted(speech: string): void;\n askUserBlocked(blocked: boolean): void;\n turnFinished(turnIndex: number, reason: string, toolCount: number, latestAssistantText?: string): void;\n agentSettled(): Promise<void>;\n}\n\ninterface FinalizedIntentSource {\n text: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction sanitizeAskUserSpeech(value: string): string {\n const withoutControls = Array.from(value, (character) => {\n const codePoint = character.codePointAt(0) ?? 0;\n return codePoint < 32 || codePoint === 127 ? ' ' : character;\n }).join('');\n const sanitized = withoutControls\n .replace(ASK_USER_PATH_PATTERN, ' ')\n .replace(ASK_USER_SECRET_PATTERN, ' ')\n .replace(ASK_USER_CODE_PATTERN, ' ')\n .replace(/\\s+/gu, ' ')\n .trim();\n return Array.from(sanitized).slice(0, MAX_ASK_USER_SPEECH_CHARACTERS).join('').trim();\n}\n\nexport function extractAskUserQuestionSpeech(payload: unknown): string | undefined {\n if (!isRecord(payload) || !Array.isArray(payload.questions)) return undefined;\n const firstQuestion = payload.questions[0];\n if (!isRecord(firstQuestion) || typeof firstQuestion.question !== 'string') return undefined;\n const question = sanitizeAskUserSpeech(firstQuestion.question);\n if (!question) return undefined;\n const labels = Array.isArray(firstQuestion.options)\n ? firstQuestion.options.slice(0, MAX_ASK_USER_OPTIONS).flatMap((option) => {\n if (!isRecord(option) || typeof option.label !== 'string') return [];\n const label = sanitizeAskUserSpeech(option.label);\n return label ? [label] : [];\n })\n : [];\n return sanitizeAskUserSpeech(labels.length > 0 ? `${question} Options: ${labels.join(', ')}` : question) || undefined;\n}\n\nexport function isAskUserBlockedPayload(payload: unknown): payload is { active: boolean } {\n return isRecord(payload) && typeof payload.active === 'boolean';\n}\n\nexport function extractFinalizedIntentSource(message: unknown): FinalizedIntentSource | undefined {\n if (!isRecord(message) || message.role !== 'assistant' || !Array.isArray(message.content)) return undefined;\n const text: string[] = [];\n for (const block of message.content) {\n if (!isRecord(block)) continue;\n if (block.type === 'toolCall') return { text: text.join('').trim() };\n if (block.type === 'text' && typeof block.text === 'string') text.push(block.text);\n }\n return undefined;\n}\n\nexport function extractTerminalAssistantText(message: unknown): string | undefined {\n if (\n !isRecord(message) ||\n message.role !== 'assistant' ||\n message.stopReason === 'toolUse' ||\n !Array.isArray(message.content)\n )\n return undefined;\n const text: string[] = [];\n for (const block of message.content) {\n if (!isRecord(block)) continue;\n if (block.type === 'toolCall') return undefined;\n if (block.type === 'text' && typeof block.text === 'string') text.push(block.text);\n }\n return text.join('').trim() || undefined;\n}\n\nexport function isSuccessfulPlanWrite(toolName: string, isError: boolean, result: unknown): boolean {\n if (toolName !== 'write_plan' || isError || !isRecord(result)) return false;\n return isRecord(result.details) && result.details.written === true;\n}\n\nexport function registerAutoCapturePiEventHandlers(\n pi: Pick<ExtensionAPI, 'on'> & { events?: Pick<ExtensionAPI['events'], 'on'> },\n controller: AutoCapturePiEventController,\n): void {\n pi.events?.on(ASK_USER_PROMPT_EVENT, (payload) => {\n const speech = extractAskUserQuestionSpeech(payload);\n if (speech) controller.askUserPrompted(speech);\n });\n pi.events?.on(ASK_USER_BLOCKED_EVENT, (payload) => {\n if (isAskUserBlockedPayload(payload)) controller.askUserBlocked(payload.active);\n });\n pi.on('input', (event) => {\n controller.observeInput({\n text: event.text,\n source: event.source,\n ...(event.streamingBehavior ? { streamingBehavior: event.streamingBehavior } : {}),\n });\n });\n pi.on('before_agent_start', (event) => controller.beforeAgentStart(event.prompt));\n pi.on('agent_start', () => controller.agentStarted());\n pi.on('message_end', (event) => {\n const source = extractFinalizedIntentSource(event.message);\n if (source) controller.finalizedAssistantIntent(source.text);\n });\n pi.on('tool_execution_end', (event) => {\n if (isSuccessfulPlanWrite(event.toolName, event.isError, event.result)) controller.planWritten();\n });\n pi.on('turn_end', (event) => {\n const message = event.message;\n controller.turnFinished(\n event.turnIndex,\n message.role === 'assistant' ? message.stopReason : 'unknown',\n event.toolResults.length,\n extractTerminalAssistantText(message),\n );\n });\n pi.on('agent_settled', async () => controller.agentSettled());\n}\n\nexport function resolveAutoCaptureModelContracts(\n reference: string,\n context: ExtensionContext,\n): AutoCaptureModelContracts {\n const separator = reference.indexOf('/');\n if (separator <= 0 || separator === reference.length - 1)\n throw new Error('Autonomous voice model must use provider/model-id form');\n const provider = reference.slice(0, separator);\n const modelId = reference.slice(separator + 1);\n const model = context.modelRegistry.find(provider, modelId);\n if (!model) throw new Error(`Autonomous voice model is not registered: ${reference}`);\n if (!context.modelRegistry.hasConfiguredAuth(model))\n throw new Error(`Autonomous voice model has no configured authentication: ${reference}`);\n\n const modelClient: PiVoiceModelClient = {\n complete: async (request: PiVoiceModelRequest): Promise<string> => {\n const response = await context.modelRegistry.complete(\n model,\n {\n systemPrompt: request.systemPrompt,\n messages: [{ role: 'user', content: request.input, timestamp: Date.now() }],\n },\n {\n signal: request.signal,\n maxTokens: request.maxTokens,\n cacheRetention: request.cacheRetention,\n reasoningEffort: 'none',\n maxRetries: 0,\n },\n );\n if (response.stopReason === 'error' || response.stopReason === 'aborted')\n throw new Error(response.errorMessage ?? `Autonomous voice model stopped with ${response.stopReason}`);\n return response.content\n .filter((content) => content.type === 'text')\n .map((content) => content.text)\n .join('')\n .trim();\n },\n };\n return {\n adjudicator: new VoiceAutoCaptureAdjudicator(modelClient),\n echo: new SemanticEchoAdjudicator(modelClient),\n narrator: new NarrationGenerator(modelClient),\n };\n}\n\n@injectable()\nexport class WhisperCppAdapter implements ITranscriberAdapter {\n readonly engine = WHISPER_CPP_ENGINE;\n constructor(\n @inject(DOOM_VOICE_TYPES.ExecutableResolver) private readonly executables: IExecutableResolver,\n @inject(DOOM_VOICE_TYPES.ProcessSpawner) private readonly spawner: IProcessSpawner,\n ) {}\n preflight(config: VoiceAdapterConfig): void {\n this.executables.resolve(config.binary, WHISPER_CPP_BINARY);\n assertModelPath(config);\n if (!config.model.path) throw new Error('whisper-cpp requires a local model path');\n }\n async transcribe(request: TranscriptionRequest): Promise<string> {\n const outputPrefix = path.join(request.workspace, WHISPER_CPP_ENGINE);\n const args = [\n '--model',\n requireModel(request.config),\n '--file',\n request.audioPath,\n '--output-txt',\n '--output-file',\n outputPrefix,\n '--no-timestamps',\n ];\n if (request.language !== AUTO_LANGUAGE) args.push('--language', request.language);\n const result = await this.spawner.run(this.executables.resolve(request.config.binary, WHISPER_CPP_BINARY), args, {\n signal: request.signal,\n });\n assertSuccess(this.engine, result);\n return fs.readFileSync(`${outputPrefix}.txt`, 'utf8').trim();\n }\n}\n\n@injectable()\nexport class OpenAiWhisperAdapter implements ITranscriberAdapter {\n readonly engine = OPENAI_WHISPER_ENGINE;\n constructor(\n @inject(DOOM_VOICE_TYPES.ExecutableResolver) private readonly executables: IExecutableResolver,\n @inject(DOOM_VOICE_TYPES.ProcessSpawner) private readonly spawner: IProcessSpawner,\n ) {}\n preflight(config: VoiceAdapterConfig): void {\n this.executables.resolve(config.binary, OPENAI_WHISPER_BINARY);\n assertModelPath(config);\n requireModel(config);\n }\n async transcribe(request: TranscriptionRequest): Promise<string> {\n const args = [\n request.audioPath,\n '--model',\n requireModel(request.config),\n '--output_format',\n TEXT_OUTPUT_FORMAT,\n '--output_dir',\n request.workspace,\n ];\n if (request.language !== AUTO_LANGUAGE) args.push('--language', request.language);\n const result = await this.spawner.run(\n this.executables.resolve(request.config.binary, OPENAI_WHISPER_BINARY),\n args,\n {\n signal: request.signal,\n },\n );\n assertSuccess(this.engine, result);\n return fs.readFileSync(path.join(request.workspace, `${path.parse(request.audioPath).name}.txt`), 'utf8').trim();\n }\n}\n\n@injectable()\nexport class MlxWhisperAdapter implements ITranscriberAdapter {\n readonly engine = MLX_WHISPER_ENGINE;\n constructor(\n @inject(DOOM_VOICE_TYPES.ExecutableResolver) private readonly executables: IExecutableResolver,\n @inject(DOOM_VOICE_TYPES.ProcessSpawner) private readonly spawner: IProcessSpawner,\n ) {}\n preflight(config: VoiceAdapterConfig): void {\n this.executables.resolve(config.binary, MLX_WHISPER_BINARY);\n assertModelPath(config);\n requireModel(config);\n }\n async transcribe(request: TranscriptionRequest): Promise<string> {\n const outputName = MLX_WHISPER_ENGINE;\n const args = [\n request.audioPath,\n '--model',\n requireModel(request.config),\n '--output-dir',\n request.workspace,\n '--output-name',\n outputName,\n // `--output-format`, not `--format`: mlx_whisper rejects the short spelling\n // outright, so every transcription failed with a usage dump.\n '--output-format',\n TEXT_OUTPUT_FORMAT,\n ];\n if (request.language !== AUTO_LANGUAGE) args.push('--language', request.language);\n const result = await this.spawner.run(this.executables.resolve(request.config.binary, MLX_WHISPER_BINARY), args, {\n signal: request.signal,\n });\n assertSuccess(this.engine, result);\n return fs.readFileSync(path.join(request.workspace, `${outputName}.txt`), 'utf8').trim();\n }\n}\n\n@injectable()\nexport class TranscriberRegistry implements ITranscriberRegistry {\n private readonly adapters: Record<Exclude<VoiceEngine, 'auto'>, ITranscriberAdapter>;\n constructor(\n @inject(DOOM_VOICE_TYPES.WhisperCppAdapter) whisperCpp: ITranscriberAdapter,\n @inject(DOOM_VOICE_TYPES.OpenAiWhisperAdapter) openAi: ITranscriberAdapter,\n @inject(DOOM_VOICE_TYPES.MlxWhisperAdapter) mlx: ITranscriberAdapter,\n ) {\n this.adapters = {\n [WHISPER_CPP_ENGINE]: whisperCpp,\n [OPENAI_WHISPER_ENGINE]: openAi,\n [MLX_WHISPER_ENGINE]: mlx,\n };\n }\n select(config: ResolvedVoiceConfig): SelectedTranscriber {\n const engines: Exclude<VoiceEngine, 'auto'>[] =\n config.engine === AUTO_LANGUAGE\n ? [WHISPER_CPP_ENGINE, OPENAI_WHISPER_ENGINE, MLX_WHISPER_ENGINE]\n : [config.engine];\n const errors: string[] = [];\n for (const engine of engines) {\n const adapterConfig = config.adapters[engine];\n if (!adapterConfig) {\n errors.push(`${engine}: not configured`);\n continue;\n }\n try {\n const adapter = this.adapters[engine];\n adapter.preflight(adapterConfig);\n return { adapter, config: adapterConfig };\n } catch (error) {\n errors.push(`${engine}: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n throw new Error(`No usable voice transcription adapter. ${errors.join('; ')}`);\n }\n}\n\n@injectable()\nexport class VoiceSessionController implements IVoiceSessionController {\n private currentState: VoiceState = IDLE_STATE;\n private recording?: RecordingHandle;\n private workspace?: string;\n private config?: ResolvedVoiceConfig;\n private selected?: SelectedTranscriber;\n private startedAt = 0;\n private activityFrame = 0;\n private activityTimer?: TimerHandle;\n private limitTimer?: TimerHandle;\n private readonly telemetry = createDoomTelemetry({\n serviceName: 'doom-voice',\n packageName: '@agimon-ai/doompi-voice',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n constructor(\n @inject(DOOM_CONFIG_TYPES.ConfigService) private readonly configs: IDoomConfigService,\n @inject(DOOM_VOICE_TYPES.Clock) private readonly clock: IClock,\n @inject(DOOM_VOICE_TYPES.TemporaryWorkspace) private readonly workspaces: ITemporaryWorkspace,\n @inject(DOOM_VOICE_TYPES.AudioRecorder) private readonly recorder: IAudioRecorder,\n @inject(DOOM_VOICE_TYPES.AudioAnalyzer) private readonly analyzer: IAudioAnalyzer,\n @inject(DOOM_VOICE_TYPES.TranscriberRegistry) private readonly registry: ITranscriberRegistry,\n ) {}\n get state(): VoiceState {\n return this.currentState;\n }\n async toggle(ui: VoiceUi): Promise<void> {\n if (this.currentState === TRANSCRIBING_STATE) {\n ui.notify('Voice transcription is already running', INFO_NOTIFICATION);\n return;\n }\n if (this.currentState === RECORDING_STATE) {\n await this.stopAndTranscribe(ui);\n return;\n }\n await this.startRecording(ui);\n }\n private async startRecording(ui: VoiceUi): Promise<void> {\n try {\n const projectRoot = process.env.PI_PROJECT_ROOT ?? process.cwd();\n const loaded = this.configs.load(projectRoot).voice;\n if (!loaded) throw new Error('Voice is not configured in the Pi agent configuration.');\n const config = resolveVoiceConfig(loaded);\n this.recorder.preflight(config);\n const selected = this.registry.select(config);\n void this.telemetry.recordEvent(RECORDING_REQUESTED_EVENT, {\n engine: selected.adapter.engine,\n mode: config.language,\n });\n const workspace = this.workspaces.create();\n this.recording = this.recorder.start(config, workspace);\n this.workspace = workspace;\n this.config = config;\n this.selected = selected;\n this.currentState = RECORDING_STATE;\n this.startedAt = this.clock.now();\n this.startActivity(ui);\n this.limitTimer = this.clock.setTimeout(() => {\n void this.stopAndTranscribe(ui);\n }, MAX_RECORDING_MS);\n void this.telemetry.recordEvent(RECORDING_STARTED_EVENT, {\n engine: selected.adapter.engine,\n outcome: 'started',\n });\n } catch (error) {\n await this.telemetry.recordError(RECORDING_FAILED_EVENT, error, { outcome: 'failed' });\n this.reset(ui);\n ui.notify(error instanceof Error ? error.message : String(error), ERROR_NOTIFICATION);\n }\n }\n private startActivity(ui: VoiceUi): void {\n this.activityFrame = 0;\n this.publishActivity(ui);\n this.activityTimer = this.clock.setInterval(() => {\n this.activityFrame += 1;\n this.publishActivity(ui);\n }, ACTIVITY_INTERVAL_MS);\n }\n private publishActivity(ui: VoiceUi): void {\n if (this.currentState === IDLE_STATE) return;\n const update: VoiceActivityUpdate = {\n state: this.currentState,\n frameIndex: this.activityFrame,\n ...(this.currentState === RECORDING_STATE\n ? { elapsedSeconds: Math.max(0, Math.floor((this.clock.now() - this.startedAt) / 1_000)) }\n : {}),\n };\n ui.setIndicator(update);\n ui.setStatus(STATUS_KEY, formatVoiceActivity(update).statusText);\n }\n private async stopAndTranscribe(ui: VoiceUi): Promise<void> {\n const recording = this.recording;\n const workspace = this.workspace;\n const config = this.config;\n const selected = this.selected;\n if (!recording || !workspace || !config || !selected || this.currentState !== RECORDING_STATE) return;\n this.clearTimers();\n const recordingStartedAt = this.startedAt;\n this.currentState = TRANSCRIBING_STATE;\n this.startActivity(ui);\n void this.telemetry.recordEvent(TRANSCRIPTION_STARTED_EVENT, {\n engine: selected.adapter.engine,\n duration_ms: Math.max(0, this.clock.now() - recordingStartedAt),\n });\n try {\n await recording.stop();\n const recordingDurationMs = Math.max(0, this.clock.now() - recordingStartedAt);\n if (this.analyzer.analyze(recording.filePath).silent) {\n await this.telemetry.recordEvent(TRANSCRIPTION_FINISHED_EVENT, {\n engine: selected.adapter.engine,\n duration_ms: recordingDurationMs,\n silence: true,\n outcome: 'empty',\n });\n ui.notify('No speech detected', INFO_NOTIFICATION);\n return;\n }\n const transcript = (\n await selected.adapter.transcribe({\n audioPath: recording.filePath,\n workspace,\n config: selected.config,\n language: config.language,\n })\n ).trim();\n if (!transcript) {\n await this.telemetry.recordEvent(TRANSCRIPTION_FINISHED_EVENT, {\n engine: selected.adapter.engine,\n duration_ms: recordingDurationMs,\n empty: true,\n outcome: 'empty',\n });\n ui.notify('Voice transcription was empty', INFO_NOTIFICATION);\n return;\n }\n const draft = ui.getEditorText();\n ui.setEditorText(`${draft}${draft && !/\\s$/.test(draft) ? ' ' : ''}${transcript}`);\n await this.telemetry.recordEvent(TRANSCRIPTION_FINISHED_EVENT, {\n engine: selected.adapter.engine,\n duration_ms: recordingDurationMs,\n empty: false,\n outcome: 'completed',\n });\n } catch (error) {\n await this.telemetry.recordError(TRANSCRIPTION_FAILED_EVENT, error, {\n engine: selected.adapter.engine,\n duration_ms: Math.max(0, this.clock.now() - recordingStartedAt),\n });\n ui.notify(error instanceof Error ? error.message : String(error), ERROR_NOTIFICATION);\n } finally {\n this.reset(ui);\n }\n }\n async shutdown(ui?: VoiceUi): Promise<void> {\n this.clearTimers();\n if (this.recording && this.currentState === RECORDING_STATE) {\n await this.recording.abort();\n void this.telemetry.recordEvent(RECORDING_FINISHED_EVENT, { outcome: 'aborted' });\n }\n this.reset(ui);\n void this.telemetry.shutdown();\n }\n private clearTimers(): void {\n if (this.activityTimer) this.clock.clear(this.activityTimer);\n if (this.limitTimer) this.clock.clear(this.limitTimer);\n this.activityTimer = undefined;\n this.limitTimer = undefined;\n }\n private reset(ui?: VoiceUi): void {\n this.clearTimers();\n if (this.workspace) this.workspaces.remove(this.workspace);\n this.recording = undefined;\n this.workspace = undefined;\n this.config = undefined;\n this.selected = undefined;\n this.currentState = IDLE_STATE;\n this.activityFrame = 0;\n ui?.setIndicator(undefined);\n ui?.setStatus(STATUS_KEY, undefined);\n }\n}\n\nfunction readVoiceConfig(filePath: string): DoomConfig {\n if (!fs.existsSync(filePath)) return { projectTrust: 'ask' };\n return parseDoomConfig(fs.readFileSync(filePath, 'utf8'), filePath);\n}\n\nfunction voiceAgentConfigPath(homeDirectory: string): string {\n const agentDirectory = process.env.PI_CODING_AGENT_DIR || path.join(homeDirectory, '.pi', 'agent');\n return path.join(agentDirectory, 'doom-voice', 'config.yaml');\n}\n\n/**\n * Shared Doom config owns `voice:`; the package-scoped file is the fallback for\n * standalone installs that have no Doom config.\n */\nfunction readGlobalVoiceConfig(homeDirectory: string): DoomConfig {\n const doomConfig = readVoiceConfig(globalDoomConfigPath(homeDirectory));\n if (doomConfig.voice) return doomConfig;\n const packageConfig = readVoiceConfig(voiceAgentConfigPath(homeDirectory));\n return packageConfig.voice ? mergeDoomConfigs(doomConfig, packageConfig) : doomConfig;\n}\n\n@injectable()\nexport class PiVoiceConfigService implements IDoomConfigService {\n load(repoRoot: string, homeDirectory = os.homedir()): DoomConfig {\n const globalConfig = readGlobalVoiceConfig(homeDirectory);\n const trusted = process.env.PI_PROJECT_TRUST === 'trusted';\n if (!trusted) return globalConfig;\n\n const projectPath = repositoryDoomConfigPath(repoRoot);\n const projectConfig = readVoiceConfig(projectPath);\n if (projectConfig.projectTrust === 'never') return globalConfig;\n return mergeDoomConfigs(globalConfig, projectConfig);\n }\n}\n\nexport function createVoiceModule(): ContainerModule {\n return new ContainerModule(({ bind }) => {\n bind<IClock>(DOOM_VOICE_TYPES.Clock).to(SystemClock).inSingletonScope();\n bind<IExecutableResolver>(DOOM_VOICE_TYPES.ExecutableResolver).to(ExecutableResolver).inSingletonScope();\n bind<IProcessSpawner>(DOOM_VOICE_TYPES.ProcessSpawner).to(NodeProcessSpawner).inSingletonScope();\n bind<IBinaryProcessSpawner>(DOOM_VOICE_TYPES.BinaryProcessSpawner).to(NodeBinaryProcessSpawner).inSingletonScope();\n bind<ITemporaryWorkspace>(DOOM_VOICE_TYPES.TemporaryWorkspace).to(TemporaryWorkspace).inSingletonScope();\n bind<IAudioRecorder>(DOOM_VOICE_TYPES.AudioRecorder).to(FfmpegAudioRecorder).inSingletonScope();\n bind<IPcmAudioRecorder>(DOOM_VOICE_TYPES.PcmAudioRecorder).to(FfmpegPcmAudioRecorder).inSingletonScope();\n bind<ITtsAdapter>(DOOM_VOICE_TYPES.TtsAdapter).to(MacOsSayTtsAdapter).inSingletonScope();\n bind<IAudioAnalyzer>(DOOM_VOICE_TYPES.AudioAnalyzer).to(PcmWavAnalyzer).inSingletonScope();\n bind<ITranscriberAdapter>(DOOM_VOICE_TYPES.WhisperCppAdapter).to(WhisperCppAdapter).inSingletonScope();\n bind<ITranscriberAdapter>(DOOM_VOICE_TYPES.OpenAiWhisperAdapter).to(OpenAiWhisperAdapter).inSingletonScope();\n bind<ITranscriberAdapter>(DOOM_VOICE_TYPES.MlxWhisperAdapter).to(MlxWhisperAdapter).inSingletonScope();\n bind<ITranscriberRegistry>(DOOM_VOICE_TYPES.TranscriberRegistry).to(TranscriberRegistry).inSingletonScope();\n bind<IVoiceSessionController>(DOOM_VOICE_TYPES.SessionController).to(VoiceSessionController).inSingletonScope();\n });\n}\nexport function createVoiceContainer(): Container {\n const container = new Container();\n container.bind<IDoomConfigService>(DOOM_CONFIG_TYPES.ConfigService).to(PiVoiceConfigService).inSingletonScope();\n container.bind<IClock>(DOOM_VOICE_TYPES.Clock).to(SystemClock).inSingletonScope();\n container.bind<IExecutableResolver>(DOOM_VOICE_TYPES.ExecutableResolver).to(ExecutableResolver).inSingletonScope();\n container.bind<IProcessSpawner>(DOOM_VOICE_TYPES.ProcessSpawner).to(NodeProcessSpawner).inSingletonScope();\n container\n .bind<IBinaryProcessSpawner>(DOOM_VOICE_TYPES.BinaryProcessSpawner)\n .to(NodeBinaryProcessSpawner)\n .inSingletonScope();\n container.bind<ITemporaryWorkspace>(DOOM_VOICE_TYPES.TemporaryWorkspace).to(TemporaryWorkspace).inSingletonScope();\n container.bind<IAudioRecorder>(DOOM_VOICE_TYPES.AudioRecorder).to(FfmpegAudioRecorder).inSingletonScope();\n container.bind<IPcmAudioRecorder>(DOOM_VOICE_TYPES.PcmAudioRecorder).to(FfmpegPcmAudioRecorder).inSingletonScope();\n container.bind<ITtsAdapter>(DOOM_VOICE_TYPES.TtsAdapter).to(MacOsSayTtsAdapter).inSingletonScope();\n container.bind<IAudioAnalyzer>(DOOM_VOICE_TYPES.AudioAnalyzer).to(PcmWavAnalyzer).inSingletonScope();\n container.bind<ITranscriberAdapter>(DOOM_VOICE_TYPES.WhisperCppAdapter).to(WhisperCppAdapter).inSingletonScope();\n container\n .bind<ITranscriberAdapter>(DOOM_VOICE_TYPES.OpenAiWhisperAdapter)\n .to(OpenAiWhisperAdapter)\n .inSingletonScope();\n container.bind<ITranscriberAdapter>(DOOM_VOICE_TYPES.MlxWhisperAdapter).to(MlxWhisperAdapter).inSingletonScope();\n container.bind<ITranscriberRegistry>(DOOM_VOICE_TYPES.TranscriberRegistry).to(TranscriberRegistry).inSingletonScope();\n container\n .bind<IVoiceSessionController>(DOOM_VOICE_TYPES.SessionController)\n .to(VoiceSessionController)\n .inSingletonScope();\n return container;\n}\n\nexport function voiceExtension(pi: ExtensionAPI): void {\n const container = createVoiceContainer();\n const controller = container.get<IVoiceSessionController>(DOOM_VOICE_TYPES.SessionController);\n const configs = container.get<IDoomConfigService>(DOOM_CONFIG_TYPES.ConfigService);\n const footer: FooterContributionHandle = { update: () => undefined, dispose: () => undefined };\n let lastUi: VoiceUi | undefined;\n let lastAutoUi: AutoCaptureUi | undefined;\n let activeContext: ExtensionContext | undefined;\n const autoController = new AutoCaptureController({\n loadConfig: () => {\n const root = process.env.PI_PROJECT_ROOT ?? process.cwd();\n const loaded = configs.load(root).voice;\n if (!loaded) throw new Error('Voice is not configured in the Pi agent configuration.');\n return resolveVoiceConfig(loaded);\n },\n resolveModel: async (reference) => {\n if (!activeContext) throw new Error('No autonomous voice session is active');\n return resolveAutoCaptureModelContracts(reference, activeContext);\n },\n recorder: container.get<IPcmAudioRecorder>(DOOM_VOICE_TYPES.PcmAudioRecorder),\n transcribers: container.get<ITranscriberRegistry>(DOOM_VOICE_TYPES.TranscriberRegistry),\n workspaces: container.get<ITemporaryWorkspace>(DOOM_VOICE_TYPES.TemporaryWorkspace),\n tts: container.get<ITtsAdapter>(DOOM_VOICE_TYPES.TtsAdapter),\n clock: container.get<IClock>(DOOM_VOICE_TYPES.Clock),\n deliver: (text) => {\n if (!activeContext) throw new Error('No autonomous voice session is active');\n deliverAutoCaptureInput(pi, activeContext, text);\n },\n manualState: () => controller.state,\n });\n\n pi.registerCommand(COMMAND_NAME, {\n description: 'Toggle local voice recording and transcription',\n handler: async (_args, ctx) => {\n if (!ctx.hasUI) return;\n lastUi = createVoiceUi(ctx, footer);\n if (autoController.state !== 'disabled') {\n lastUi.notify('Disable autonomous voice before using manual voice', INFO_NOTIFICATION);\n return;\n }\n await controller.toggle(lastUi);\n },\n });\n pi.registerCommand(AUTO_COMMAND_NAME, {\n description: 'Toggle continuous autonomous voice conversation',\n handler: async (_args, ctx) => {\n if (!ctx.hasUI) return;\n activeContext = ctx;\n lastAutoUi = createAutoCaptureUi(ctx, footer);\n await autoController.toggle(lastAutoUi);\n },\n });\n pi.on('session_start', async (_event, ctx) => {\n activeContext = ctx;\n if (!ctx.hasUI) return;\n lastUi = createVoiceUi(ctx, footer);\n lastAutoUi = createAutoCaptureUi(ctx, footer);\n lastUi.setIndicator(undefined);\n lastUi.setStatus(STATUS_KEY, undefined);\n });\n registerAutoCapturePiEventHandlers(pi, autoController);\n pi.on('session_shutdown', async () => {\n const ui = lastUi;\n const autoUi = lastAutoUi;\n lastUi = undefined;\n lastAutoUi = undefined;\n activeContext = undefined;\n await autoController.shutdown(autoUi);\n await controller.shutdown(ui);\n footer.dispose();\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./catalog.cjs"),n=require("./download.cjs"),r=require("./install.cjs");let i=require("node:fs");i=e.__toESM(i,1);let a=require("node:path");a=e.__toESM(a,1);let o=require("node:os");o=e.__toESM(o,1);let s=require("@agimon-ai/doompi-config"),c=require("@agimon-ai/doompi-ui/config"),l=require("@agimon-ai/doompi-config/configWriter");const u=`voice`,d=`language`,f=`recorder.device`,p=`install`,m=`select`,h=`confirm`,g=`cancel`,_=[`voice`];function v(e,t){try{return e.resolve(void 0,t)}catch{return}}function y(e){let t=a.default.join(e,`Library`,`Python`),n=[];try{n=i.default.readdirSync(t,{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>a.default.join(t,e.name,`bin`))}catch{n=[]}return[...n,a.default.join(e,`.local`,`bin`),`/opt/homebrew/bin`,`/usr/local/bin`]}function b(e,t,n){let r=v(e,t);if(r)return r;for(let e of y(n)){let n=a.default.join(e,t);try{return i.default.accessSync(n,i.default.constants.X_OK),n}catch{continue}}}function x(e,t){return e.download?a.default.join(n.modelsDirectory(t),e.download.fileName):void 0}var S=class{deps;republish;install;notice;installedIds=new Set;configDirectory;configPath;homeDirectory;constructor(e,t){this.deps=e,this.republish=t,this.configDirectory=(0,s.globalDoomConfigDirectory)(e.homeDirectory),this.configPath=(0,s.globalDoomConfigPath)(e.homeDirectory),this.homeDirectory=e.homeDirectory??o.default.homedir()}async refresh(){let e=new Set;for(let r of t.VOICE_CATALOG){let i=x(r,this.configDirectory);if(i){await n.isDownloaded(i,r.sizeBytes)&&e.add(r.id);continue}b(this.deps.resolver,t.ENGINE_TOOLING[r.engine].binary,this.homeDirectory)&&e.add(r.id)}this.installedIds=e}sections(){let e=this.safeLoad(),n=e?.engine,r=this.activeModelId(e),i=this.install,a=i?.phase===`running`?i:void 0,o=t.VOICE_CATALOG.map(e=>{let n=this.installedIds.has(e.id),i=t.formatBytes(e.sizeBytes);return{id:e.id,label:e.label,group:t.engineGroupLabel(e.engine),...i?{detail:i}:{},status:n?`ready`:`available`,statusText:n?e.id===r?`in use`:`installed`:e.download?`download`:`install`,action:n?m:p}});return[{id:u,title:`voice`,order:10,detail:this.readiness(e),...this.notice?{notice:this.notice,noticeLevel:`error`}:{},fields:[{id:`model`,label:`model`,kind:`choice`,keyPath:`voice.adapters.<engine>.model`,placeholder:`not set`,detail:`enter installs what is missing, or switches to it when it is already there.`,choices:o,...r?{value:r}:{},...a?{busy:!0,steps:C(a)}:{},...a?.progress?{progress:a.progress}:{},...a?.pty?{output:[...a.pty.tail()]}:{},...a?.pty?.awaitingInput()?{awaitingInput:!0}:{},...i?.phase===`confirming`?{detail:w(i.plan),actions:[{key:`y`,label:`install`,action:h},{key:`n`,label:`cancel`,action:g}]}:{}},{id:`engine`,label:`engine`,kind:`info`,detail:`Set by the model you pick, so the two cannot disagree.`,...n?{value:n}:{}},{id:d,label:`language`,kind:`text`,keyPath:`voice.language`,placeholder:`auto`,detail:"Language hint passed to the transcriber. `auto` detects it.",...e?.language&&e.language!==`auto`?{value:e.language}:{}},{id:f,label:`recorder device`,kind:`text`,keyPath:`voice.recorder.device`,placeholder:`none:default`,detail:`ffmpeg avfoundation input, as <video>:<audio>.`,...e?.recorder?.device&&e.recorder.device!==`none:default`?{value:e.recorder.device}:{}}]}]}handlers(){return{[p]:async({value:e})=>this.beginInstall(e),[m]:async({value:e})=>this.selectModel(e),[h]:async()=>this.runInstall(),[g]:async()=>{this.install=void 0},abort:async()=>{this.install?.controller?.abort(),this.install?.pty?.stop()},input:async({value:e})=>{this.install?.pty?.answer(e??``)},[c.CONFIG_ACTION.set]:async({fieldId:e,value:t})=>this.writeSimpleField(e,t),[c.CONFIG_ACTION.clear]:async({fieldId:e})=>this.writeSimpleField(e,void 0)}}reportError(e){this.notice=e instanceof Error?e.message:String(e),this.install?.phase===`running`&&(this.install=void 0),this.deps.onStatus?.(void 0)}safeLoad(){try{return this.deps.loadVoice()}catch(e){this.notice=e instanceof Error?e.message:String(e);return}}activeModelId(e){if(!e?.engine||e.engine===`auto`)return;let n=e.adapters?.[e.engine];if(n)return n.model.id?t.VOICE_CATALOG.find(e=>e.id===n.model.id)?.id:t.VOICE_CATALOG.find(e=>e.download&&x(e,this.configDirectory)===n.model.path)?.id}readiness(e){return this.install?.phase===`running`?`installing`:this.activeModelId(e)?`ready`:`no model installed`}beginInstall(e){let n=e?t.catalogEntryById(e):void 0;if(!n)return;if(this.install?.phase===`running`){this.notice=`An install is already running.`;return}this.notice=void 0;let i=x(n,this.configDirectory),a=r.planInstall(n,{platform:this.deps.platform??process.platform,find:e=>v(this.deps.resolver,e),modelPresent:this.installedIds.has(n.id),...i?{modelPath:i}:{}}),o=r.planBlocker(a);if(o){this.notice=o;return}this.install={entryId:n.id,plan:a,phase:`confirming`}}async selectModel(e){let n=e?t.catalogEntryById(e):void 0;n&&(this.notice=void 0,await this.writeSelection(n),await this.refresh())}async runInstall(){let e=this.install;if(!e||e.phase!==`confirming`)return;let n=t.catalogEntryById(e.entryId);if(!n)return;let r=new AbortController;this.install={...e,phase:`running`,controller:r};try{for(let t of e.plan.steps)t.state!==`satisfied`&&(r.signal.throwIfAborted(),await this.runStep(n,t,r.signal));this.install=void 0,this.deps.onStatus?.(void 0)}catch(e){throw this.install=void 0,this.deps.onStatus?.(void 0),e}await this.refresh()}async runStep(e,r,i){if(this.setProgress(r.label),r.command){await this.runCommandStep(r.command);return}if(r.kind===`model`&&e.download){let a=x(e,this.configDirectory);if(!a)return;n.ensureModelsDirectory(this.configDirectory),await n.downloadModelFile({url:e.download.url,targetPath:a,sha256:e.download.sha256,...e.sizeBytes===void 0?{}:{expectedBytes:e.sizeBytes},onProgress:({receivedBytes:e,totalBytes:n})=>this.setProgress(`${r.label} ${t.formatBytes(e)} / ${t.formatBytes(n)??`?`}`,n?e/n:void 0),signal:i},this.deps.fetchImpl??globalThis.fetch);return}r.kind===`config`&&await this.writeSelection(e),r.kind===`verify`&&this.verifyInstall(e)}verifyInstall(e){let n=t.ENGINE_TOOLING[e.engine];if(!b(this.deps.resolver,n.binary,this.homeDirectory))throw Error(`${n.binary} is still not runnable after \`${n.installers[0]?.command}\`. Check where it was installed and add that directory to PATH.`);let r=x(e,this.configDirectory);if(r&&!i.default.existsSync(r))throw Error(`The model file is missing: ${r}`)}async runCommandStep(e){if(!this.deps.runCommand)throw Error(`Cannot run \`${e}\`: no terminal is available. Run it yourself, then pick the model again.`);let t=await this.deps.runCommand({id:`voice-install-${Date.now()}`,name:e.slice(0,48),command:e,cwd:this.deps.cwd?.()??process.cwd()});this.install&&(this.install.pty=t),this.republish();try{let n=await t.completion;if(n!==0)throw Error(`\`${e}\` exited with code ${n}`)}finally{this.install&&(this.install.pty=void 0)}}setProgress(e,t){this.install&&(this.install.progress={label:e,...t===void 0?{}:{ratio:t}}),this.deps.onStatus?.(`voice: ${e}`),this.republish()}async writeSelection(e){let n=[..._,`adapters`,e.engine],r=x(e,this.configDirectory),i=t.ENGINE_TOOLING[e.engine],a=b(this.deps.resolver,i.binary,this.homeDirectory),o=a&&!v(this.deps.resolver,i.binary)?a:void 0,s=[{keyPath:[..._,`engine`],value:e.engine},{keyPath:[...n,`binary`],...o?{value:o}:{}},e.download&&r?{keyPath:[...n,`model`,`path`],value:r}:{keyPath:[...n,`model`,`id`],value:e.id},e.download&&r?{keyPath:[...n,`model`,`id`]}:{keyPath:[...n,`model`,`path`]}];await(0,l.writeDoomConfigValues)(this.configPath,s)}async writeSimpleField(e,t){let n=e===d?[..._,`language`]:e===f?[..._,`recorder`,`device`]:void 0;n&&(this.notice=void 0,await(0,l.writeDoomConfigValues)(this.configPath,[{keyPath:n,...t?{value:t}:{}}]))}};function C(e){return e.plan.steps.slice(0,24).map(e=>({label:e.label.slice(0,96),state:e.state,...e.detail?{detail:e.detail.slice(0,160)}:{}}))}function w(e){let t=e.commands.length>0?`runs: ${e.commands.join(`; `)}`:`nothing to install, just writes the config`;return`install ${e.entry.label}? ${t}`.slice(0,240)}exports.VOICE_CONFIG_SECTION_ID=u,exports.VoiceConfigController=S;
2
+ //# sourceMappingURL=voiceConfig.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voiceConfig.cjs","names":["path","fs","modelsDirectory","globalDoomConfigDirectory","globalDoomConfigPath","os","VOICE_CATALOG","isDownloaded","ENGINE_TOOLING","formatBytes","engineGroupLabel","CONFIG_ACTION","catalogEntryById","planInstall","planBlocker","downloadModelFile","writeDoomConfigValues"],"sources":["../src/voiceConfig.ts"],"sourcesContent":["/**\n * Voice's slice of the `SPC e c` config panel, and the installer behind it.\n *\n * Picking a model is the whole interaction: enter on an entry either selects it,\n * if everything it needs is already present, or shows exactly which commands\n * would be run and installs it once confirmed. The engine follows from the model\n * rather than being chosen separately, because choosing an engine with no model\n * installed is a state a user can only get out of by choosing a model anyway.\n *\n * The panel never blocks on any of this: an install runs on its own and reports\n * through republished sections, so closing the panel does not stop it and\n * reopening shows where it got to.\n */\n\nimport { globalDoomConfigDirectory, globalDoomConfigPath, type ResolvedVoiceConfig } from '@agimon-ai/doompi-config';\nimport { writeDoomConfigValues, type DoomConfigEdit } from '@agimon-ai/doompi-config/configWriter';\nimport { CONFIG_ACTION, type ConfigChoice, type ConfigSection, type ConfigStep } from '@agimon-ai/doompi-ui/config';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport {\n catalogEntryById,\n ENGINE_TOOLING,\n engineGroupLabel,\n formatBytes,\n VOICE_CATALOG,\n type VoiceCatalogEntry,\n} from './catalog.ts';\nimport { downloadModelFile, ensureModelsDirectory, isDownloaded, modelsDirectory, type FetchLike } from './download.ts';\nimport { planInstall, planBlocker, type InstallPlan, type InstallStep } from './install.ts';\nimport type { PtyRunHandle } from './ptyClient.ts';\nimport type { IExecutableResolver, IProcessSpawner } from './types.ts';\n\nexport const VOICE_CONFIG_SECTION_ID = 'voice';\nconst SECTION_ORDER = 10;\nconst FIELD_ENGINE = 'engine';\nconst FIELD_MODEL = 'model';\nconst FIELD_LANGUAGE = 'language';\nconst FIELD_DEVICE = 'recorder.device';\nconst ACTION_INSTALL = 'install';\nconst ACTION_SELECT = 'select';\nconst ACTION_CONFIRM = 'confirm';\nconst ACTION_CANCEL = 'cancel';\nconst ACTION_ABORT = 'abort';\nconst ACTION_INPUT = 'input';\nconst VOICE_PATH = ['voice'] as const;\n/** The contract's caps, applied here so an overlong label cannot fail a snapshot. */\nconst MAX_DETAIL_LENGTH = 240;\nconst MAX_STEPS = 24;\nconst MAX_STEP_LABEL = 96;\nconst MAX_STEP_DETAIL = 160;\n\nexport interface VoiceConfigDeps {\n resolver: IExecutableResolver;\n spawner: IProcessSpawner;\n loadVoice: () => ResolvedVoiceConfig | undefined;\n /**\n * Runs a command on a terminal the user can watch and type into. Absent in\n * tests and whenever doom-runner is not installed, in which case an install\n * that needs to run a command says so rather than running one blind.\n */\n runCommand?: (options: { id: string; name: string; command: string; cwd: string }) => Promise<PtyRunHandle>;\n cwd?: () => string;\n fetchImpl?: FetchLike;\n homeDirectory?: string;\n platform?: NodeJS.Platform;\n /** Reports the running step so it is visible with the panel closed. */\n onStatus?: (text: string | undefined) => void;\n}\n\n/** `resolve` throws when a binary is absent; the panel needs a question, not an exception. */\nfunction findBinary(resolver: IExecutableResolver, binary: string): string | undefined {\n try {\n return resolver.resolve(undefined, binary);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Directories pip drops console scripts into that are routinely not on PATH.\n *\n * A `pip3 install` succeeds and still leaves `mlx_whisper` unrunnable, because\n * macOS user installs land in `~/Library/Python/<version>/bin` which nothing\n * adds to PATH by default. Finding it here and writing the absolute path into\n * the adapter's `binary` is what makes the install actually finish the job.\n */\nfunction pythonScriptDirectories(homeDirectory: string): string[] {\n const pythonRoot = path.join(homeDirectory, 'Library', 'Python');\n let versioned: string[] = [];\n try {\n versioned = fs\n .readdirSync(pythonRoot, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => path.join(pythonRoot, entry.name, 'bin'));\n } catch {\n versioned = [];\n }\n return [...versioned, path.join(homeDirectory, '.local', 'bin'), '/opt/homebrew/bin', '/usr/local/bin'];\n}\n\nfunction locateBinary(resolver: IExecutableResolver, binary: string, homeDirectory: string): string | undefined {\n const onPath = findBinary(resolver, binary);\n if (onPath) return onPath;\n for (const directory of pythonScriptDirectories(homeDirectory)) {\n const candidate = path.join(directory, binary);\n try {\n fs.accessSync(candidate, fs.constants.X_OK);\n return candidate;\n } catch {\n continue;\n }\n }\n return undefined;\n}\n\nfunction modelTargetPath(entry: VoiceCatalogEntry, configDirectory: string): string | undefined {\n return entry.download ? path.join(modelsDirectory(configDirectory), entry.download.fileName) : undefined;\n}\n\ninterface InstallState {\n entryId: string;\n plan: InstallPlan;\n phase: 'confirming' | 'running';\n progress?: { label: string; ratio?: number };\n controller?: AbortController;\n /** The terminal the current command is running on, when one is. */\n pty?: PtyRunHandle;\n}\n\nexport class VoiceConfigController {\n private install: InstallState | undefined;\n private notice: string | undefined;\n private installedIds = new Set<string>();\n private readonly configDirectory: string;\n private readonly configPath: string;\n private readonly homeDirectory: string;\n\n constructor(\n private readonly deps: VoiceConfigDeps,\n private readonly republish: () => void,\n ) {\n this.configDirectory = globalDoomConfigDirectory(deps.homeDirectory);\n this.configPath = globalDoomConfigPath(deps.homeDirectory);\n this.homeDirectory = deps.homeDirectory ?? os.homedir();\n }\n\n /** Re-checks what is on disk. Cheap enough to run on session start and after a write. */\n async refresh(): Promise<void> {\n const present = new Set<string>();\n for (const entry of VOICE_CATALOG) {\n const target = modelTargetPath(entry, this.configDirectory);\n if (target) {\n if (await isDownloaded(target, entry.sizeBytes)) present.add(entry.id);\n continue;\n }\n // Nothing to download, so the tool being present is the whole requirement.\n // Located rather than just PATH-resolved: pip drops scripts in directories\n // nothing adds to PATH, and reporting those as missing would offer to\n // install something that is already there.\n if (locateBinary(this.deps.resolver, ENGINE_TOOLING[entry.engine].binary, this.homeDirectory)) {\n present.add(entry.id);\n }\n }\n this.installedIds = present;\n }\n\n sections(): readonly ConfigSection[] {\n const voice = this.safeLoad();\n const activeEngine = voice?.engine;\n const activeModel = this.activeModelId(voice);\n const installing = this.install;\n const modelField = installing?.phase === 'running' ? installing : undefined;\n\n const choices: ConfigChoice[] = VOICE_CATALOG.map((entry) => {\n const installed = this.installedIds.has(entry.id);\n const size = formatBytes(entry.sizeBytes);\n return {\n id: entry.id,\n label: entry.label,\n group: engineGroupLabel(entry.engine),\n ...(size ? { detail: size } : {}),\n status: installed ? ('ready' as const) : ('available' as const),\n statusText: installed\n ? entry.id === activeModel\n ? 'in use'\n : 'installed'\n : entry.download\n ? 'download'\n : 'install',\n action: installed ? ACTION_SELECT : ACTION_INSTALL,\n };\n });\n\n return [\n {\n id: VOICE_CONFIG_SECTION_ID,\n title: 'voice',\n order: SECTION_ORDER,\n detail: this.readiness(voice),\n ...(this.notice ? { notice: this.notice, noticeLevel: 'error' as const } : {}),\n fields: [\n {\n id: FIELD_MODEL,\n label: 'model',\n kind: 'choice',\n keyPath: `voice.adapters.<engine>.model`,\n placeholder: 'not set',\n detail: 'enter installs what is missing, or switches to it when it is already there.',\n choices,\n ...(activeModel ? { value: activeModel } : {}),\n ...(modelField ? { busy: true, steps: planSteps(modelField) } : {}),\n ...(modelField?.progress ? { progress: modelField.progress } : {}),\n // The tail of the running command, and whether it is sitting on a\n // question, so the panel can offer a line to answer it.\n ...(modelField?.pty ? { output: [...modelField.pty.tail()] } : {}),\n ...(modelField?.pty?.awaitingInput() ? { awaitingInput: true } : {}),\n // The command list goes in `detail`, which the panel wraps under the\n // row. Right-aligning it as a status ran it off the edge of the pane.\n ...(installing?.phase === 'confirming'\n ? {\n detail: confirmSummary(installing.plan),\n actions: [\n { key: 'y', label: 'install', action: ACTION_CONFIRM },\n { key: 'n', label: 'cancel', action: ACTION_CANCEL },\n ],\n }\n : {}),\n },\n {\n id: FIELD_ENGINE,\n label: 'engine',\n kind: 'info',\n detail: 'Set by the model you pick, so the two cannot disagree.',\n ...(activeEngine ? { value: activeEngine } : {}),\n },\n {\n id: FIELD_LANGUAGE,\n label: 'language',\n kind: 'text',\n keyPath: 'voice.language',\n placeholder: 'auto',\n detail: 'Language hint passed to the transcriber. `auto` detects it.',\n ...(voice?.language && voice.language !== 'auto' ? { value: voice.language } : {}),\n },\n {\n id: FIELD_DEVICE,\n label: 'recorder device',\n kind: 'text',\n keyPath: 'voice.recorder.device',\n placeholder: 'none:default',\n detail: 'ffmpeg avfoundation input, as <video>:<audio>.',\n ...(voice?.recorder?.device && voice.recorder.device !== 'none:default'\n ? { value: voice.recorder.device }\n : {}),\n },\n ],\n },\n ];\n }\n\n handlers(): Readonly<Record<string, (input: { fieldId: string; value?: string }) => Promise<void>>> {\n return {\n [ACTION_INSTALL]: async ({ value }) => this.beginInstall(value),\n [ACTION_SELECT]: async ({ value }) => this.selectModel(value),\n [ACTION_CONFIRM]: async () => this.runInstall(),\n [ACTION_CANCEL]: async () => {\n this.install = undefined;\n },\n [ACTION_ABORT]: async () => {\n this.install?.controller?.abort();\n this.install?.pty?.stop();\n },\n // An empty answer is meaningful: it accepts whatever default the command\n // is offering, which is what pressing enter at a `[Y/n]` prompt does.\n [ACTION_INPUT]: async ({ value }) => {\n this.install?.pty?.answer(value ?? '');\n },\n [CONFIG_ACTION.set]: async ({ fieldId, value }) => this.writeSimpleField(fieldId, value),\n [CONFIG_ACTION.clear]: async ({ fieldId }) => this.writeSimpleField(fieldId, undefined),\n };\n }\n\n reportError(error: unknown): void {\n this.notice = error instanceof Error ? error.message : String(error);\n if (this.install?.phase === 'running') this.install = undefined;\n this.deps.onStatus?.(undefined);\n }\n\n private safeLoad(): ResolvedVoiceConfig | undefined {\n try {\n return this.deps.loadVoice();\n } catch (error) {\n // A malformed config must not stop the panel drawing: it is where someone\n // would go to repair it.\n this.notice = error instanceof Error ? error.message : String(error);\n return undefined;\n }\n }\n\n private activeModelId(voice: ResolvedVoiceConfig | undefined): string | undefined {\n if (!voice?.engine || voice.engine === 'auto') return undefined;\n const adapter = voice.adapters?.[voice.engine];\n if (!adapter) return undefined;\n if (adapter.model.id) return VOICE_CATALOG.find((entry) => entry.id === adapter.model.id)?.id;\n return VOICE_CATALOG.find(\n (entry) => entry.download && modelTargetPath(entry, this.configDirectory) === adapter.model.path,\n )?.id;\n }\n\n private readiness(voice: ResolvedVoiceConfig | undefined): string {\n if (this.install?.phase === 'running') return 'installing';\n return this.activeModelId(voice) ? 'ready' : 'no model installed';\n }\n\n private beginInstall(entryId: string | undefined): void {\n const entry = entryId ? catalogEntryById(entryId) : undefined;\n if (!entry) return;\n if (this.install?.phase === 'running') {\n // One at a time: two installs would race on the same config write.\n this.notice = 'An install is already running.';\n return;\n }\n this.notice = undefined;\n const target = modelTargetPath(entry, this.configDirectory);\n const plan = planInstall(entry, {\n platform: this.deps.platform ?? process.platform,\n find: (binary) => findBinary(this.deps.resolver, binary),\n modelPresent: this.installedIds.has(entry.id),\n ...(target ? { modelPath: target } : {}),\n });\n const blocker = planBlocker(plan);\n if (blocker) {\n this.notice = blocker;\n return;\n }\n this.install = { entryId: entry.id, plan, phase: 'confirming' };\n }\n\n private async selectModel(entryId: string | undefined): Promise<void> {\n const entry = entryId ? catalogEntryById(entryId) : undefined;\n if (!entry) return;\n this.notice = undefined;\n await this.writeSelection(entry);\n await this.refresh();\n }\n\n private async runInstall(): Promise<void> {\n const state = this.install;\n if (!state || state.phase !== 'confirming') return;\n const entry = catalogEntryById(state.entryId);\n if (!entry) return;\n const controller = new AbortController();\n this.install = { ...state, phase: 'running', controller };\n\n try {\n for (const step of state.plan.steps) {\n if (step.state === 'satisfied') continue;\n controller.signal.throwIfAborted();\n await this.runStep(entry, step, controller.signal);\n }\n this.install = undefined;\n this.deps.onStatus?.(undefined);\n } catch (error) {\n this.install = undefined;\n this.deps.onStatus?.(undefined);\n // Config is written last, so an abort or failure leaves the previous\n // working setup exactly as it was.\n throw error;\n }\n await this.refresh();\n }\n\n private async runStep(entry: VoiceCatalogEntry, step: InstallStep, signal: AbortSignal): Promise<void> {\n this.setProgress(step.label);\n if (step.command) {\n await this.runCommandStep(step.command);\n return;\n }\n if (step.kind === 'model' && entry.download) {\n const target = modelTargetPath(entry, this.configDirectory);\n if (!target) return;\n ensureModelsDirectory(this.configDirectory);\n await downloadModelFile(\n {\n url: entry.download.url,\n targetPath: target,\n sha256: entry.download.sha256,\n ...(entry.sizeBytes === undefined ? {} : { expectedBytes: entry.sizeBytes }),\n onProgress: ({ receivedBytes, totalBytes }) =>\n this.setProgress(\n `${step.label} ${formatBytes(receivedBytes)} / ${formatBytes(totalBytes) ?? '?'}`,\n totalBytes ? receivedBytes / totalBytes : undefined,\n ),\n signal,\n },\n this.deps.fetchImpl ?? globalThis.fetch,\n );\n return;\n }\n if (step.kind === 'config') await this.writeSelection(entry);\n if (step.kind === 'verify') this.verifyInstall(entry);\n }\n\n /**\n * Confirms the thing just installed can actually be run.\n *\n * Without this the install reports success and the failure surfaces later, at\n * the first recording, as `Required executable not found on PATH` with no hint\n * that a package manager put it somewhere PATH does not reach.\n */\n private verifyInstall(entry: VoiceCatalogEntry): void {\n const tooling = ENGINE_TOOLING[entry.engine];\n const binary = locateBinary(this.deps.resolver, tooling.binary, this.homeDirectory);\n if (!binary) {\n throw new Error(\n `${tooling.binary} is still not runnable after \\`${tooling.installers[0]?.command}\\`. ` +\n `Check where it was installed and add that directory to PATH.`,\n );\n }\n const target = modelTargetPath(entry, this.configDirectory);\n if (target && !fs.existsSync(target)) throw new Error(`The model file is missing: ${target}`);\n }\n\n /**\n * Runs one install command on a borrowed terminal.\n *\n * Not a buffered spawn: these are package managers, and they ask things. The\n * terminal is also a login shell, which is how `brew` gets found at all when\n * it is only on PATH through a profile script.\n */\n private async runCommandStep(command: string): Promise<void> {\n if (!this.deps.runCommand) {\n throw new Error(\n `Cannot run \\`${command}\\`: no terminal is available. Run it yourself, then pick the model again.`,\n );\n }\n const handle = await this.deps.runCommand({\n id: `voice-install-${Date.now()}`,\n name: command.slice(0, 48),\n command,\n cwd: this.deps.cwd?.() ?? process.cwd(),\n });\n if (this.install) this.install.pty = handle;\n this.republish();\n try {\n const code = await handle.completion;\n if (code !== 0) throw new Error(`\\`${command}\\` exited with code ${code}`);\n } finally {\n if (this.install) this.install.pty = undefined;\n }\n }\n\n private setProgress(label: string, ratio?: number): void {\n if (this.install) this.install.progress = { label, ...(ratio === undefined ? {} : { ratio }) };\n this.deps.onStatus?.(`voice: ${label}`);\n this.republish();\n }\n\n private async writeSelection(entry: VoiceCatalogEntry): Promise<void> {\n const adapterPath = [...VOICE_PATH, 'adapters', entry.engine];\n const target = modelTargetPath(entry, this.configDirectory);\n const tooling = ENGINE_TOOLING[entry.engine];\n const located = locateBinary(this.deps.resolver, tooling.binary, this.homeDirectory);\n // Recorded as an absolute path only when PATH alone would not find it, which\n // is the normal outcome of a pip install on macOS. Left unset otherwise, so\n // the config keeps working if the tool later moves.\n const offPath = located && !findBinary(this.deps.resolver, tooling.binary) ? located : undefined;\n const edits: DoomConfigEdit[] = [\n { keyPath: [...VOICE_PATH, 'engine'], value: entry.engine },\n { keyPath: [...adapterPath, 'binary'], ...(offPath ? { value: offPath } : {}) },\n // path and id are mutually exclusive, so the unused one goes in the same\n // write or the file is briefly invalid.\n entry.download && target\n ? { keyPath: [...adapterPath, 'model', 'path'], value: target }\n : { keyPath: [...adapterPath, 'model', 'id'], value: entry.id },\n entry.download && target\n ? { keyPath: [...adapterPath, 'model', 'id'] }\n : { keyPath: [...adapterPath, 'model', 'path'] },\n ];\n await writeDoomConfigValues(this.configPath, edits);\n }\n\n private async writeSimpleField(fieldId: string, value: string | undefined): Promise<void> {\n const keyPath =\n fieldId === FIELD_LANGUAGE\n ? [...VOICE_PATH, 'language']\n : fieldId === FIELD_DEVICE\n ? [...VOICE_PATH, 'recorder', 'device']\n : undefined;\n if (!keyPath) return;\n this.notice = undefined;\n await writeDoomConfigValues(this.configPath, [{ keyPath, ...(value ? { value } : {}) }]);\n }\n}\n\n/** The plan as the panel shows it: every step, with the one running marked. */\nfunction planSteps(state: InstallState): ConfigStep[] {\n return state.plan.steps.slice(0, MAX_STEPS).map((step) => ({\n label: step.label.slice(0, MAX_STEP_LABEL),\n state: step.state,\n ...(step.detail ? { detail: step.detail.slice(0, MAX_STEP_DETAIL) } : {}),\n }));\n}\n\nfunction confirmSummary(plan: InstallPlan): string {\n const commands =\n plan.commands.length > 0 ? `runs: ${plan.commands.join('; ')}` : 'nothing to install, just writes the config';\n return `install ${plan.entry.label}? ${commands}`.slice(0, MAX_DETAIL_LENGTH);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{ENGINE_TOOLING as e,VOICE_CATALOG as t,catalogEntryById as n,engineGroupLabel as r,formatBytes as i}from"./catalog.mjs";import{downloadModelFile as a,ensureModelsDirectory as o,isDownloaded as s,modelsDirectory as c}from"./download.mjs";import{planBlocker as l,planInstall as u}from"./install.mjs";import d from"node:fs";import f from"node:path";import p from"node:os";import{globalDoomConfigDirectory as m,globalDoomConfigPath as h}from"@agimon-ai/doompi-config";import{CONFIG_ACTION as g}from"@agimon-ai/doompi-ui/config";import{writeDoomConfigValues as _}from"@agimon-ai/doompi-config/configWriter";const v=`voice`,y=`language`,b=`recorder.device`,x=`install`,S=`select`,C=`confirm`,w=`cancel`,T=[`voice`];function E(e,t){try{return e.resolve(void 0,t)}catch{return}}function D(e){let t=f.join(e,`Library`,`Python`),n=[];try{n=d.readdirSync(t,{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>f.join(t,e.name,`bin`))}catch{n=[]}return[...n,f.join(e,`.local`,`bin`),`/opt/homebrew/bin`,`/usr/local/bin`]}function O(e,t,n){let r=E(e,t);if(r)return r;for(let e of D(n)){let n=f.join(e,t);try{return d.accessSync(n,d.constants.X_OK),n}catch{continue}}}function k(e,t){return e.download?f.join(c(t),e.download.fileName):void 0}var A=class{deps;republish;install;notice;installedIds=new Set;configDirectory;configPath;homeDirectory;constructor(e,t){this.deps=e,this.republish=t,this.configDirectory=m(e.homeDirectory),this.configPath=h(e.homeDirectory),this.homeDirectory=e.homeDirectory??p.homedir()}async refresh(){let n=new Set;for(let r of t){let t=k(r,this.configDirectory);if(t){await s(t,r.sizeBytes)&&n.add(r.id);continue}O(this.deps.resolver,e[r.engine].binary,this.homeDirectory)&&n.add(r.id)}this.installedIds=n}sections(){let e=this.safeLoad(),n=e?.engine,a=this.activeModelId(e),o=this.install,s=o?.phase===`running`?o:void 0,c=t.map(e=>{let t=this.installedIds.has(e.id),n=i(e.sizeBytes);return{id:e.id,label:e.label,group:r(e.engine),...n?{detail:n}:{},status:t?`ready`:`available`,statusText:t?e.id===a?`in use`:`installed`:e.download?`download`:`install`,action:t?S:x}});return[{id:v,title:`voice`,order:10,detail:this.readiness(e),...this.notice?{notice:this.notice,noticeLevel:`error`}:{},fields:[{id:`model`,label:`model`,kind:`choice`,keyPath:`voice.adapters.<engine>.model`,placeholder:`not set`,detail:`enter installs what is missing, or switches to it when it is already there.`,choices:c,...a?{value:a}:{},...s?{busy:!0,steps:j(s)}:{},...s?.progress?{progress:s.progress}:{},...s?.pty?{output:[...s.pty.tail()]}:{},...s?.pty?.awaitingInput()?{awaitingInput:!0}:{},...o?.phase===`confirming`?{detail:M(o.plan),actions:[{key:`y`,label:`install`,action:C},{key:`n`,label:`cancel`,action:w}]}:{}},{id:`engine`,label:`engine`,kind:`info`,detail:`Set by the model you pick, so the two cannot disagree.`,...n?{value:n}:{}},{id:y,label:`language`,kind:`text`,keyPath:`voice.language`,placeholder:`auto`,detail:"Language hint passed to the transcriber. `auto` detects it.",...e?.language&&e.language!==`auto`?{value:e.language}:{}},{id:b,label:`recorder device`,kind:`text`,keyPath:`voice.recorder.device`,placeholder:`none:default`,detail:`ffmpeg avfoundation input, as <video>:<audio>.`,...e?.recorder?.device&&e.recorder.device!==`none:default`?{value:e.recorder.device}:{}}]}]}handlers(){return{[x]:async({value:e})=>this.beginInstall(e),[S]:async({value:e})=>this.selectModel(e),[C]:async()=>this.runInstall(),[w]:async()=>{this.install=void 0},abort:async()=>{this.install?.controller?.abort(),this.install?.pty?.stop()},input:async({value:e})=>{this.install?.pty?.answer(e??``)},[g.set]:async({fieldId:e,value:t})=>this.writeSimpleField(e,t),[g.clear]:async({fieldId:e})=>this.writeSimpleField(e,void 0)}}reportError(e){this.notice=e instanceof Error?e.message:String(e),this.install?.phase===`running`&&(this.install=void 0),this.deps.onStatus?.(void 0)}safeLoad(){try{return this.deps.loadVoice()}catch(e){this.notice=e instanceof Error?e.message:String(e);return}}activeModelId(e){if(!e?.engine||e.engine===`auto`)return;let n=e.adapters?.[e.engine];if(n)return n.model.id?t.find(e=>e.id===n.model.id)?.id:t.find(e=>e.download&&k(e,this.configDirectory)===n.model.path)?.id}readiness(e){return this.install?.phase===`running`?`installing`:this.activeModelId(e)?`ready`:`no model installed`}beginInstall(e){let t=e?n(e):void 0;if(!t)return;if(this.install?.phase===`running`){this.notice=`An install is already running.`;return}this.notice=void 0;let r=k(t,this.configDirectory),i=u(t,{platform:this.deps.platform??process.platform,find:e=>E(this.deps.resolver,e),modelPresent:this.installedIds.has(t.id),...r?{modelPath:r}:{}}),a=l(i);if(a){this.notice=a;return}this.install={entryId:t.id,plan:i,phase:`confirming`}}async selectModel(e){let t=e?n(e):void 0;t&&(this.notice=void 0,await this.writeSelection(t),await this.refresh())}async runInstall(){let e=this.install;if(!e||e.phase!==`confirming`)return;let t=n(e.entryId);if(!t)return;let r=new AbortController;this.install={...e,phase:`running`,controller:r};try{for(let n of e.plan.steps)n.state!==`satisfied`&&(r.signal.throwIfAborted(),await this.runStep(t,n,r.signal));this.install=void 0,this.deps.onStatus?.(void 0)}catch(e){throw this.install=void 0,this.deps.onStatus?.(void 0),e}await this.refresh()}async runStep(e,t,n){if(this.setProgress(t.label),t.command){await this.runCommandStep(t.command);return}if(t.kind===`model`&&e.download){let r=k(e,this.configDirectory);if(!r)return;o(this.configDirectory),await a({url:e.download.url,targetPath:r,sha256:e.download.sha256,...e.sizeBytes===void 0?{}:{expectedBytes:e.sizeBytes},onProgress:({receivedBytes:e,totalBytes:n})=>this.setProgress(`${t.label} ${i(e)} / ${i(n)??`?`}`,n?e/n:void 0),signal:n},this.deps.fetchImpl??globalThis.fetch);return}t.kind===`config`&&await this.writeSelection(e),t.kind===`verify`&&this.verifyInstall(e)}verifyInstall(t){let n=e[t.engine];if(!O(this.deps.resolver,n.binary,this.homeDirectory))throw Error(`${n.binary} is still not runnable after \`${n.installers[0]?.command}\`. Check where it was installed and add that directory to PATH.`);let r=k(t,this.configDirectory);if(r&&!d.existsSync(r))throw Error(`The model file is missing: ${r}`)}async runCommandStep(e){if(!this.deps.runCommand)throw Error(`Cannot run \`${e}\`: no terminal is available. Run it yourself, then pick the model again.`);let t=await this.deps.runCommand({id:`voice-install-${Date.now()}`,name:e.slice(0,48),command:e,cwd:this.deps.cwd?.()??process.cwd()});this.install&&(this.install.pty=t),this.republish();try{let n=await t.completion;if(n!==0)throw Error(`\`${e}\` exited with code ${n}`)}finally{this.install&&(this.install.pty=void 0)}}setProgress(e,t){this.install&&(this.install.progress={label:e,...t===void 0?{}:{ratio:t}}),this.deps.onStatus?.(`voice: ${e}`),this.republish()}async writeSelection(t){let n=[...T,`adapters`,t.engine],r=k(t,this.configDirectory),i=e[t.engine],a=O(this.deps.resolver,i.binary,this.homeDirectory),o=a&&!E(this.deps.resolver,i.binary)?a:void 0,s=[{keyPath:[...T,`engine`],value:t.engine},{keyPath:[...n,`binary`],...o?{value:o}:{}},t.download&&r?{keyPath:[...n,`model`,`path`],value:r}:{keyPath:[...n,`model`,`id`],value:t.id},t.download&&r?{keyPath:[...n,`model`,`id`]}:{keyPath:[...n,`model`,`path`]}];await _(this.configPath,s)}async writeSimpleField(e,t){let n=e===y?[...T,`language`]:e===b?[...T,`recorder`,`device`]:void 0;n&&(this.notice=void 0,await _(this.configPath,[{keyPath:n,...t?{value:t}:{}}]))}};function j(e){return e.plan.steps.slice(0,24).map(e=>({label:e.label.slice(0,96),state:e.state,...e.detail?{detail:e.detail.slice(0,160)}:{}}))}function M(e){let t=e.commands.length>0?`runs: ${e.commands.join(`; `)}`:`nothing to install, just writes the config`;return`install ${e.entry.label}? ${t}`.slice(0,240)}export{v as VOICE_CONFIG_SECTION_ID,A as VoiceConfigController};
2
+ //# sourceMappingURL=voiceConfig.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voiceConfig.mjs","names":[],"sources":["../src/voiceConfig.ts"],"sourcesContent":["/**\n * Voice's slice of the `SPC e c` config panel, and the installer behind it.\n *\n * Picking a model is the whole interaction: enter on an entry either selects it,\n * if everything it needs is already present, or shows exactly which commands\n * would be run and installs it once confirmed. The engine follows from the model\n * rather than being chosen separately, because choosing an engine with no model\n * installed is a state a user can only get out of by choosing a model anyway.\n *\n * The panel never blocks on any of this: an install runs on its own and reports\n * through republished sections, so closing the panel does not stop it and\n * reopening shows where it got to.\n */\n\nimport { globalDoomConfigDirectory, globalDoomConfigPath, type ResolvedVoiceConfig } from '@agimon-ai/doompi-config';\nimport { writeDoomConfigValues, type DoomConfigEdit } from '@agimon-ai/doompi-config/configWriter';\nimport { CONFIG_ACTION, type ConfigChoice, type ConfigSection, type ConfigStep } from '@agimon-ai/doompi-ui/config';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport {\n catalogEntryById,\n ENGINE_TOOLING,\n engineGroupLabel,\n formatBytes,\n VOICE_CATALOG,\n type VoiceCatalogEntry,\n} from './catalog.ts';\nimport { downloadModelFile, ensureModelsDirectory, isDownloaded, modelsDirectory, type FetchLike } from './download.ts';\nimport { planInstall, planBlocker, type InstallPlan, type InstallStep } from './install.ts';\nimport type { PtyRunHandle } from './ptyClient.ts';\nimport type { IExecutableResolver, IProcessSpawner } from './types.ts';\n\nexport const VOICE_CONFIG_SECTION_ID = 'voice';\nconst SECTION_ORDER = 10;\nconst FIELD_ENGINE = 'engine';\nconst FIELD_MODEL = 'model';\nconst FIELD_LANGUAGE = 'language';\nconst FIELD_DEVICE = 'recorder.device';\nconst ACTION_INSTALL = 'install';\nconst ACTION_SELECT = 'select';\nconst ACTION_CONFIRM = 'confirm';\nconst ACTION_CANCEL = 'cancel';\nconst ACTION_ABORT = 'abort';\nconst ACTION_INPUT = 'input';\nconst VOICE_PATH = ['voice'] as const;\n/** The contract's caps, applied here so an overlong label cannot fail a snapshot. */\nconst MAX_DETAIL_LENGTH = 240;\nconst MAX_STEPS = 24;\nconst MAX_STEP_LABEL = 96;\nconst MAX_STEP_DETAIL = 160;\n\nexport interface VoiceConfigDeps {\n resolver: IExecutableResolver;\n spawner: IProcessSpawner;\n loadVoice: () => ResolvedVoiceConfig | undefined;\n /**\n * Runs a command on a terminal the user can watch and type into. Absent in\n * tests and whenever doom-runner is not installed, in which case an install\n * that needs to run a command says so rather than running one blind.\n */\n runCommand?: (options: { id: string; name: string; command: string; cwd: string }) => Promise<PtyRunHandle>;\n cwd?: () => string;\n fetchImpl?: FetchLike;\n homeDirectory?: string;\n platform?: NodeJS.Platform;\n /** Reports the running step so it is visible with the panel closed. */\n onStatus?: (text: string | undefined) => void;\n}\n\n/** `resolve` throws when a binary is absent; the panel needs a question, not an exception. */\nfunction findBinary(resolver: IExecutableResolver, binary: string): string | undefined {\n try {\n return resolver.resolve(undefined, binary);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Directories pip drops console scripts into that are routinely not on PATH.\n *\n * A `pip3 install` succeeds and still leaves `mlx_whisper` unrunnable, because\n * macOS user installs land in `~/Library/Python/<version>/bin` which nothing\n * adds to PATH by default. Finding it here and writing the absolute path into\n * the adapter's `binary` is what makes the install actually finish the job.\n */\nfunction pythonScriptDirectories(homeDirectory: string): string[] {\n const pythonRoot = path.join(homeDirectory, 'Library', 'Python');\n let versioned: string[] = [];\n try {\n versioned = fs\n .readdirSync(pythonRoot, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => path.join(pythonRoot, entry.name, 'bin'));\n } catch {\n versioned = [];\n }\n return [...versioned, path.join(homeDirectory, '.local', 'bin'), '/opt/homebrew/bin', '/usr/local/bin'];\n}\n\nfunction locateBinary(resolver: IExecutableResolver, binary: string, homeDirectory: string): string | undefined {\n const onPath = findBinary(resolver, binary);\n if (onPath) return onPath;\n for (const directory of pythonScriptDirectories(homeDirectory)) {\n const candidate = path.join(directory, binary);\n try {\n fs.accessSync(candidate, fs.constants.X_OK);\n return candidate;\n } catch {\n continue;\n }\n }\n return undefined;\n}\n\nfunction modelTargetPath(entry: VoiceCatalogEntry, configDirectory: string): string | undefined {\n return entry.download ? path.join(modelsDirectory(configDirectory), entry.download.fileName) : undefined;\n}\n\ninterface InstallState {\n entryId: string;\n plan: InstallPlan;\n phase: 'confirming' | 'running';\n progress?: { label: string; ratio?: number };\n controller?: AbortController;\n /** The terminal the current command is running on, when one is. */\n pty?: PtyRunHandle;\n}\n\nexport class VoiceConfigController {\n private install: InstallState | undefined;\n private notice: string | undefined;\n private installedIds = new Set<string>();\n private readonly configDirectory: string;\n private readonly configPath: string;\n private readonly homeDirectory: string;\n\n constructor(\n private readonly deps: VoiceConfigDeps,\n private readonly republish: () => void,\n ) {\n this.configDirectory = globalDoomConfigDirectory(deps.homeDirectory);\n this.configPath = globalDoomConfigPath(deps.homeDirectory);\n this.homeDirectory = deps.homeDirectory ?? os.homedir();\n }\n\n /** Re-checks what is on disk. Cheap enough to run on session start and after a write. */\n async refresh(): Promise<void> {\n const present = new Set<string>();\n for (const entry of VOICE_CATALOG) {\n const target = modelTargetPath(entry, this.configDirectory);\n if (target) {\n if (await isDownloaded(target, entry.sizeBytes)) present.add(entry.id);\n continue;\n }\n // Nothing to download, so the tool being present is the whole requirement.\n // Located rather than just PATH-resolved: pip drops scripts in directories\n // nothing adds to PATH, and reporting those as missing would offer to\n // install something that is already there.\n if (locateBinary(this.deps.resolver, ENGINE_TOOLING[entry.engine].binary, this.homeDirectory)) {\n present.add(entry.id);\n }\n }\n this.installedIds = present;\n }\n\n sections(): readonly ConfigSection[] {\n const voice = this.safeLoad();\n const activeEngine = voice?.engine;\n const activeModel = this.activeModelId(voice);\n const installing = this.install;\n const modelField = installing?.phase === 'running' ? installing : undefined;\n\n const choices: ConfigChoice[] = VOICE_CATALOG.map((entry) => {\n const installed = this.installedIds.has(entry.id);\n const size = formatBytes(entry.sizeBytes);\n return {\n id: entry.id,\n label: entry.label,\n group: engineGroupLabel(entry.engine),\n ...(size ? { detail: size } : {}),\n status: installed ? ('ready' as const) : ('available' as const),\n statusText: installed\n ? entry.id === activeModel\n ? 'in use'\n : 'installed'\n : entry.download\n ? 'download'\n : 'install',\n action: installed ? ACTION_SELECT : ACTION_INSTALL,\n };\n });\n\n return [\n {\n id: VOICE_CONFIG_SECTION_ID,\n title: 'voice',\n order: SECTION_ORDER,\n detail: this.readiness(voice),\n ...(this.notice ? { notice: this.notice, noticeLevel: 'error' as const } : {}),\n fields: [\n {\n id: FIELD_MODEL,\n label: 'model',\n kind: 'choice',\n keyPath: `voice.adapters.<engine>.model`,\n placeholder: 'not set',\n detail: 'enter installs what is missing, or switches to it when it is already there.',\n choices,\n ...(activeModel ? { value: activeModel } : {}),\n ...(modelField ? { busy: true, steps: planSteps(modelField) } : {}),\n ...(modelField?.progress ? { progress: modelField.progress } : {}),\n // The tail of the running command, and whether it is sitting on a\n // question, so the panel can offer a line to answer it.\n ...(modelField?.pty ? { output: [...modelField.pty.tail()] } : {}),\n ...(modelField?.pty?.awaitingInput() ? { awaitingInput: true } : {}),\n // The command list goes in `detail`, which the panel wraps under the\n // row. Right-aligning it as a status ran it off the edge of the pane.\n ...(installing?.phase === 'confirming'\n ? {\n detail: confirmSummary(installing.plan),\n actions: [\n { key: 'y', label: 'install', action: ACTION_CONFIRM },\n { key: 'n', label: 'cancel', action: ACTION_CANCEL },\n ],\n }\n : {}),\n },\n {\n id: FIELD_ENGINE,\n label: 'engine',\n kind: 'info',\n detail: 'Set by the model you pick, so the two cannot disagree.',\n ...(activeEngine ? { value: activeEngine } : {}),\n },\n {\n id: FIELD_LANGUAGE,\n label: 'language',\n kind: 'text',\n keyPath: 'voice.language',\n placeholder: 'auto',\n detail: 'Language hint passed to the transcriber. `auto` detects it.',\n ...(voice?.language && voice.language !== 'auto' ? { value: voice.language } : {}),\n },\n {\n id: FIELD_DEVICE,\n label: 'recorder device',\n kind: 'text',\n keyPath: 'voice.recorder.device',\n placeholder: 'none:default',\n detail: 'ffmpeg avfoundation input, as <video>:<audio>.',\n ...(voice?.recorder?.device && voice.recorder.device !== 'none:default'\n ? { value: voice.recorder.device }\n : {}),\n },\n ],\n },\n ];\n }\n\n handlers(): Readonly<Record<string, (input: { fieldId: string; value?: string }) => Promise<void>>> {\n return {\n [ACTION_INSTALL]: async ({ value }) => this.beginInstall(value),\n [ACTION_SELECT]: async ({ value }) => this.selectModel(value),\n [ACTION_CONFIRM]: async () => this.runInstall(),\n [ACTION_CANCEL]: async () => {\n this.install = undefined;\n },\n [ACTION_ABORT]: async () => {\n this.install?.controller?.abort();\n this.install?.pty?.stop();\n },\n // An empty answer is meaningful: it accepts whatever default the command\n // is offering, which is what pressing enter at a `[Y/n]` prompt does.\n [ACTION_INPUT]: async ({ value }) => {\n this.install?.pty?.answer(value ?? '');\n },\n [CONFIG_ACTION.set]: async ({ fieldId, value }) => this.writeSimpleField(fieldId, value),\n [CONFIG_ACTION.clear]: async ({ fieldId }) => this.writeSimpleField(fieldId, undefined),\n };\n }\n\n reportError(error: unknown): void {\n this.notice = error instanceof Error ? error.message : String(error);\n if (this.install?.phase === 'running') this.install = undefined;\n this.deps.onStatus?.(undefined);\n }\n\n private safeLoad(): ResolvedVoiceConfig | undefined {\n try {\n return this.deps.loadVoice();\n } catch (error) {\n // A malformed config must not stop the panel drawing: it is where someone\n // would go to repair it.\n this.notice = error instanceof Error ? error.message : String(error);\n return undefined;\n }\n }\n\n private activeModelId(voice: ResolvedVoiceConfig | undefined): string | undefined {\n if (!voice?.engine || voice.engine === 'auto') return undefined;\n const adapter = voice.adapters?.[voice.engine];\n if (!adapter) return undefined;\n if (adapter.model.id) return VOICE_CATALOG.find((entry) => entry.id === adapter.model.id)?.id;\n return VOICE_CATALOG.find(\n (entry) => entry.download && modelTargetPath(entry, this.configDirectory) === adapter.model.path,\n )?.id;\n }\n\n private readiness(voice: ResolvedVoiceConfig | undefined): string {\n if (this.install?.phase === 'running') return 'installing';\n return this.activeModelId(voice) ? 'ready' : 'no model installed';\n }\n\n private beginInstall(entryId: string | undefined): void {\n const entry = entryId ? catalogEntryById(entryId) : undefined;\n if (!entry) return;\n if (this.install?.phase === 'running') {\n // One at a time: two installs would race on the same config write.\n this.notice = 'An install is already running.';\n return;\n }\n this.notice = undefined;\n const target = modelTargetPath(entry, this.configDirectory);\n const plan = planInstall(entry, {\n platform: this.deps.platform ?? process.platform,\n find: (binary) => findBinary(this.deps.resolver, binary),\n modelPresent: this.installedIds.has(entry.id),\n ...(target ? { modelPath: target } : {}),\n });\n const blocker = planBlocker(plan);\n if (blocker) {\n this.notice = blocker;\n return;\n }\n this.install = { entryId: entry.id, plan, phase: 'confirming' };\n }\n\n private async selectModel(entryId: string | undefined): Promise<void> {\n const entry = entryId ? catalogEntryById(entryId) : undefined;\n if (!entry) return;\n this.notice = undefined;\n await this.writeSelection(entry);\n await this.refresh();\n }\n\n private async runInstall(): Promise<void> {\n const state = this.install;\n if (!state || state.phase !== 'confirming') return;\n const entry = catalogEntryById(state.entryId);\n if (!entry) return;\n const controller = new AbortController();\n this.install = { ...state, phase: 'running', controller };\n\n try {\n for (const step of state.plan.steps) {\n if (step.state === 'satisfied') continue;\n controller.signal.throwIfAborted();\n await this.runStep(entry, step, controller.signal);\n }\n this.install = undefined;\n this.deps.onStatus?.(undefined);\n } catch (error) {\n this.install = undefined;\n this.deps.onStatus?.(undefined);\n // Config is written last, so an abort or failure leaves the previous\n // working setup exactly as it was.\n throw error;\n }\n await this.refresh();\n }\n\n private async runStep(entry: VoiceCatalogEntry, step: InstallStep, signal: AbortSignal): Promise<void> {\n this.setProgress(step.label);\n if (step.command) {\n await this.runCommandStep(step.command);\n return;\n }\n if (step.kind === 'model' && entry.download) {\n const target = modelTargetPath(entry, this.configDirectory);\n if (!target) return;\n ensureModelsDirectory(this.configDirectory);\n await downloadModelFile(\n {\n url: entry.download.url,\n targetPath: target,\n sha256: entry.download.sha256,\n ...(entry.sizeBytes === undefined ? {} : { expectedBytes: entry.sizeBytes }),\n onProgress: ({ receivedBytes, totalBytes }) =>\n this.setProgress(\n `${step.label} ${formatBytes(receivedBytes)} / ${formatBytes(totalBytes) ?? '?'}`,\n totalBytes ? receivedBytes / totalBytes : undefined,\n ),\n signal,\n },\n this.deps.fetchImpl ?? globalThis.fetch,\n );\n return;\n }\n if (step.kind === 'config') await this.writeSelection(entry);\n if (step.kind === 'verify') this.verifyInstall(entry);\n }\n\n /**\n * Confirms the thing just installed can actually be run.\n *\n * Without this the install reports success and the failure surfaces later, at\n * the first recording, as `Required executable not found on PATH` with no hint\n * that a package manager put it somewhere PATH does not reach.\n */\n private verifyInstall(entry: VoiceCatalogEntry): void {\n const tooling = ENGINE_TOOLING[entry.engine];\n const binary = locateBinary(this.deps.resolver, tooling.binary, this.homeDirectory);\n if (!binary) {\n throw new Error(\n `${tooling.binary} is still not runnable after \\`${tooling.installers[0]?.command}\\`. ` +\n `Check where it was installed and add that directory to PATH.`,\n );\n }\n const target = modelTargetPath(entry, this.configDirectory);\n if (target && !fs.existsSync(target)) throw new Error(`The model file is missing: ${target}`);\n }\n\n /**\n * Runs one install command on a borrowed terminal.\n *\n * Not a buffered spawn: these are package managers, and they ask things. The\n * terminal is also a login shell, which is how `brew` gets found at all when\n * it is only on PATH through a profile script.\n */\n private async runCommandStep(command: string): Promise<void> {\n if (!this.deps.runCommand) {\n throw new Error(\n `Cannot run \\`${command}\\`: no terminal is available. Run it yourself, then pick the model again.`,\n );\n }\n const handle = await this.deps.runCommand({\n id: `voice-install-${Date.now()}`,\n name: command.slice(0, 48),\n command,\n cwd: this.deps.cwd?.() ?? process.cwd(),\n });\n if (this.install) this.install.pty = handle;\n this.republish();\n try {\n const code = await handle.completion;\n if (code !== 0) throw new Error(`\\`${command}\\` exited with code ${code}`);\n } finally {\n if (this.install) this.install.pty = undefined;\n }\n }\n\n private setProgress(label: string, ratio?: number): void {\n if (this.install) this.install.progress = { label, ...(ratio === undefined ? {} : { ratio }) };\n this.deps.onStatus?.(`voice: ${label}`);\n this.republish();\n }\n\n private async writeSelection(entry: VoiceCatalogEntry): Promise<void> {\n const adapterPath = [...VOICE_PATH, 'adapters', entry.engine];\n const target = modelTargetPath(entry, this.configDirectory);\n const tooling = ENGINE_TOOLING[entry.engine];\n const located = locateBinary(this.deps.resolver, tooling.binary, this.homeDirectory);\n // Recorded as an absolute path only when PATH alone would not find it, which\n // is the normal outcome of a pip install on macOS. Left unset otherwise, so\n // the config keeps working if the tool later moves.\n const offPath = located && !findBinary(this.deps.resolver, tooling.binary) ? located : undefined;\n const edits: DoomConfigEdit[] = [\n { keyPath: [...VOICE_PATH, 'engine'], value: entry.engine },\n { keyPath: [...adapterPath, 'binary'], ...(offPath ? { value: offPath } : {}) },\n // path and id are mutually exclusive, so the unused one goes in the same\n // write or the file is briefly invalid.\n entry.download && target\n ? { keyPath: [...adapterPath, 'model', 'path'], value: target }\n : { keyPath: [...adapterPath, 'model', 'id'], value: entry.id },\n entry.download && target\n ? { keyPath: [...adapterPath, 'model', 'id'] }\n : { keyPath: [...adapterPath, 'model', 'path'] },\n ];\n await writeDoomConfigValues(this.configPath, edits);\n }\n\n private async writeSimpleField(fieldId: string, value: string | undefined): Promise<void> {\n const keyPath =\n fieldId === FIELD_LANGUAGE\n ? [...VOICE_PATH, 'language']\n : fieldId === FIELD_DEVICE\n ? [...VOICE_PATH, 'recorder', 'device']\n : undefined;\n if (!keyPath) return;\n this.notice = undefined;\n await writeDoomConfigValues(this.configPath, [{ keyPath, ...(value ? { value } : {}) }]);\n }\n}\n\n/** The plan as the panel shows it: every step, with the one running marked. */\nfunction planSteps(state: InstallState): ConfigStep[] {\n return state.plan.steps.slice(0, MAX_STEPS).map((step) => ({\n label: step.label.slice(0, MAX_STEP_LABEL),\n state: step.state,\n ...(step.detail ? { detail: step.detail.slice(0, MAX_STEP_DETAIL) } : {}),\n }));\n}\n\nfunction confirmSummary(plan: InstallPlan): string {\n const commands =\n plan.commands.length > 0 ? `runs: ${plan.commands.join('; ')}` : 'nothing to install, just writes the config';\n return `install ${plan.entry.label}? ${commands}`.slice(0, MAX_DETAIL_LENGTH);\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@agimon-ai/doompi-voice",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "Local speech to text extension for Pi",
5
+ "homepage": "https://agimon.ai",
6
+ "license": "MIT",
7
+ "author": "Vuong Ngo",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "type": "module",
12
+ "main": "./dist/index.cjs",
13
+ "types": "./dist/index.d.mts",
14
+ "jsnext:main": "./dist/index.mjs",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.mts",
18
+ "import": "./dist/index.mjs",
19
+ "require": "./dist/index.cjs"
20
+ },
21
+ "./extensions/pi": {
22
+ "types": "./dist/extensions/pi.d.mts",
23
+ "import": "./dist/extensions/pi.mjs",
24
+ "require": "./dist/extensions/pi.cjs"
25
+ },
26
+ "./extensions/doom": {
27
+ "types": "./dist/extensions/doom.d.mts",
28
+ "import": "./dist/extensions/doom.mjs",
29
+ "require": "./dist/extensions/doom.cjs"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
37
+ "dependencies": {
38
+ "@juicesharp/rpiv-ask-user-question": "2.2.0",
39
+ "inversify": "8.2.1",
40
+ "reflect-metadata": "0.2.2",
41
+ "@agimon-ai/doompi-config": "0.0.1-alpha.0",
42
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.0",
43
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.0",
44
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.0"
45
+ },
46
+ "devDependencies": {
47
+ "@earendil-works/pi-coding-agent": "0.84.1",
48
+ "@types/node": "26.1.1",
49
+ "@vitest/coverage-v8": "4.1.10",
50
+ "tsdown": "0.22.14",
51
+ "typescript": "6.0.3",
52
+ "vitest": "4.1.10"
53
+ },
54
+ "peerDependencies": {
55
+ "@earendil-works/pi-coding-agent": "0.84.1"
56
+ },
57
+ "engines": {
58
+ "node": ">=22.19.0"
59
+ },
60
+ "pi": {
61
+ "extensions": [
62
+ "./dist/extensions/pi.mjs"
63
+ ]
64
+ },
65
+ "scripts": {
66
+ "build": "tsdown",
67
+ "test": "vitest --run",
68
+ "typecheck": "tsc --noEmit",
69
+ "lint": "oxlint . && oxfmt . --check",
70
+ "fixcode": "oxlint . --fix && oxfmt ."
71
+ }
72
+ }