-
Notifications
You must be signed in to change notification settings - Fork 1
Emotive avatar rendering: expressions, gestures, bone-based eye gaze #279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3e68f0e
0354900
1d32bf2
e8f699d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| /** | ||
| * Live Export Command - Server Implementation | ||
| * | ||
| * Exports recent utterances from the active live voice session | ||
| * to markdown format. Mirrors collaboration/chat/export. | ||
| */ | ||
|
|
||
| import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; | ||
| import type { JTAGContext } from '@system/core/types/JTAGTypes'; | ||
| import { transformPayload } from '@system/core/types/JTAGTypes'; | ||
| import { getTSVoiceOrchestrator } from '@system/voice/server'; | ||
| import { extractSentiment, formatEmotionLabel } from '@system/rag/shared/TextSentiment'; | ||
| import type { LiveExportParams, LiveExportResult } from '../shared/LiveExportTypes'; | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
|
|
||
| export class LiveExportServerCommand extends CommandBase<LiveExportParams, LiveExportResult> { | ||
|
|
||
| constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { | ||
| super('collaboration/live/export', context, subpath, commander); | ||
| } | ||
|
|
||
| async execute(params: LiveExportParams): Promise<LiveExportResult> { | ||
| const orchestrator = getTSVoiceOrchestrator(); | ||
|
|
||
| // Auto-discover active session | ||
| const sessionId = params.callSessionId ?? orchestrator.activeSessionId; | ||
| if (!sessionId) { | ||
| return transformPayload(params, { | ||
| success: false, | ||
| message: 'No active live session.', | ||
| utteranceCount: 0, | ||
| callSessionId: '', | ||
| }); | ||
| } | ||
|
|
||
| const limit = params.limit ?? 20; | ||
| const includeEmotions = params.includeEmotions ?? true; | ||
|
|
||
| // Get utterances from VoiceOrchestrator's in-memory session context | ||
| const utterances = orchestrator.getRecentUtterances(sessionId, limit); | ||
| const participants = orchestrator.getParticipants(sessionId); | ||
|
|
||
| if (utterances.length === 0) { | ||
| const markdown = '# Live Call Transcript\n\nNo utterances yet.\n'; | ||
| console.log(markdown); | ||
| return transformPayload(params, { | ||
| success: true, | ||
| message: 'No utterances in session', | ||
| utteranceCount: 0, | ||
| markdown, | ||
| callSessionId: sessionId, | ||
| }); | ||
| } | ||
|
|
||
| // Generate markdown | ||
| const markdown = this.generateMarkdown(utterances, participants, includeEmotions, sessionId); | ||
|
|
||
| // Write to file or return as string | ||
| if (params.output) { | ||
| const filepath = path.resolve(params.output); | ||
| const dir = path.dirname(filepath); | ||
| if (!fs.existsSync(dir)) { | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| } | ||
| fs.writeFileSync(filepath, markdown, 'utf-8'); | ||
| console.log(`Exported ${utterances.length} utterances to ${filepath}`); | ||
|
|
||
| return transformPayload(params, { | ||
| success: true, | ||
| message: `Exported ${utterances.length} utterances to ${filepath}`, | ||
| utteranceCount: utterances.length, | ||
| filepath, | ||
| callSessionId: sessionId, | ||
| }); | ||
| } | ||
|
|
||
| console.log(markdown); | ||
| return transformPayload(params, { | ||
| success: true, | ||
| message: `Exported ${utterances.length} utterances`, | ||
| utteranceCount: utterances.length, | ||
| markdown, | ||
| callSessionId: sessionId, | ||
| }); | ||
| } | ||
|
|
||
| private generateMarkdown( | ||
| utterances: Array<{ | ||
| sessionId: string; | ||
| speakerId: string; | ||
| speakerName: string; | ||
| speakerType: 'human' | 'persona' | 'agent'; | ||
| transcript: string; | ||
| confidence: number; | ||
| timestamp: number; | ||
| }>, | ||
| participants: Array<{ userId: string; displayName: string; type: string }>, | ||
| includeEmotions: boolean, | ||
| sessionId: string, | ||
| ): string { | ||
| const lines: string[] = []; | ||
|
|
||
| // Header | ||
| lines.push('# Live Call Transcript'); | ||
| lines.push(''); | ||
| lines.push(`Session: ${sessionId}`); | ||
| lines.push(`Exported: ${new Date().toISOString()}`); | ||
| lines.push(`Utterances: ${utterances.length}`); | ||
| lines.push(`Participants: ${participants.map(p => p.displayName).join(', ')}`); | ||
| lines.push(''); | ||
| lines.push('---'); | ||
| lines.push(''); | ||
|
|
||
| // Utterances | ||
| for (const u of utterances) { | ||
| const timestamp = new Date(u.timestamp).toLocaleString(); | ||
| const speakerLabel = this.getSpeakerLabel(u.speakerType); | ||
|
|
||
| let emotionTag = ''; | ||
| if (includeEmotions) { | ||
| const sentiment = extractSentiment(u.transcript); | ||
| const label = formatEmotionLabel(sentiment); | ||
| if (label) emotionTag = ` (${label})`; | ||
| } | ||
|
|
||
| lines.push(`**${speakerLabel} ${u.speakerName}${emotionTag}** — *${timestamp}*`); | ||
| lines.push(''); | ||
| lines.push(u.transcript); | ||
| lines.push(''); | ||
| lines.push('---'); | ||
| lines.push(''); | ||
| } | ||
|
|
||
| return lines.join('\n'); | ||
| } | ||
|
|
||
| private getSpeakerLabel(type: string): string { | ||
| switch (type) { | ||
| case 'human': return '[HUMAN]'; | ||
| case 'persona': return '[AI]'; | ||
| case 'agent': return '[AGENT]'; | ||
| default: return '[UNKNOWN]'; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** | ||
| * Live Export Command | ||
| * Export recent utterances from the active live voice session. | ||
| * Mirrors collaboration/chat/export but for voice sessions. | ||
| * | ||
| * Usage: | ||
| * ./jtag collaboration/live/export | ||
| * ./jtag collaboration/live/export --limit=20 | ||
| * ./jtag collaboration/live/export --output="/tmp/call-transcript.md" | ||
| */ | ||
|
|
||
| import type { CommandParams, CommandResult, CommandInput } from '@system/core/types/JTAGTypes'; | ||
| import { Commands } from '@system/core/shared/Commands'; | ||
|
|
||
| export interface LiveExportParams extends CommandParams { | ||
| /** Number of utterances to export (default: 20) */ | ||
| limit?: number; | ||
|
|
||
| /** Output file path (optional — prints to stdout if not provided) */ | ||
| output?: string; | ||
|
|
||
| /** Explicit session ID (auto-discovered if not provided) */ | ||
| callSessionId?: string; | ||
|
|
||
| /** Include emotional annotations (default: true) */ | ||
| includeEmotions?: boolean; | ||
| } | ||
|
|
||
| export interface LiveExportResult extends CommandResult { | ||
| success: boolean; | ||
| message: string; | ||
|
|
||
| /** Number of utterances exported */ | ||
| utteranceCount: number; | ||
|
|
||
| /** Markdown content (if output not specified) */ | ||
| markdown?: string; | ||
|
|
||
| /** Output file path (if output specified) */ | ||
| filepath?: string; | ||
|
|
||
| /** Session ID exported from */ | ||
| callSessionId: string; | ||
| } | ||
|
|
||
| /** | ||
| * LiveExport — Type-safe command executor | ||
| * | ||
| * Usage: | ||
| * import { LiveExport } from '@commands/collaboration/live/export/shared/LiveExportTypes'; | ||
| * const result = await LiveExport.execute({ limit: 20 }); | ||
| */ | ||
| export const LiveExport = { | ||
| execute(params?: CommandInput<LiveExportParams>): Promise<LiveExportResult> { | ||
| return Commands.execute<LiveExportParams, LiveExportResult>('collaboration/live/export', params as Partial<LiveExportParams>); | ||
| }, | ||
| commandName: 'collaboration/live/export' as const, | ||
| } as const; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,85 @@ | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Live Send Command - Server Implementation | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Sends a text message into the active live voice session. | ||||||||||||||||||||||||||||||||||
| * Auto-discovers session and speaker identity. | ||||||||||||||||||||||||||||||||||
| * Mirrors collaboration/chat/send but for voice sessions. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; | ||||||||||||||||||||||||||||||||||
| import type { JTAGContext } from '@system/core/types/JTAGTypes'; | ||||||||||||||||||||||||||||||||||
| import { transformPayload } from '@system/core/types/JTAGTypes'; | ||||||||||||||||||||||||||||||||||
| import { getTSVoiceOrchestrator } from '@system/voice/server'; | ||||||||||||||||||||||||||||||||||
| import { CollaborationLiveTranscription } from '../../transcription/shared/CollaborationLiveTranscriptionTypes'; | ||||||||||||||||||||||||||||||||||
| import type { LiveSendParams, LiveSendResult } from '../shared/LiveSendTypes'; | ||||||||||||||||||||||||||||||||||
| import { Commands } from '@system/core/shared/Commands'; | ||||||||||||||||||||||||||||||||||
| import type { DataListResult } from '@commands/data/list/shared/DataListTypes'; | ||||||||||||||||||||||||||||||||||
| import { DataList } from '@commands/data/list/shared/DataListTypes'; | ||||||||||||||||||||||||||||||||||
| import type { UserEntity } from '@system/data/entities/UserEntity'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export class LiveSendServerCommand extends CommandBase<LiveSendParams, LiveSendResult> { | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { | ||||||||||||||||||||||||||||||||||
| super('collaboration/live/send', context, subpath, commander); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async execute(params: LiveSendParams): Promise<LiveSendResult> { | ||||||||||||||||||||||||||||||||||
| const orchestrator = getTSVoiceOrchestrator(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Auto-discover active session | ||||||||||||||||||||||||||||||||||
| const sessionId = params.callSessionId ?? orchestrator.activeSessionId; | ||||||||||||||||||||||||||||||||||
| if (!sessionId) { | ||||||||||||||||||||||||||||||||||
| return transformPayload(params, { | ||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||
| message: 'No active live session. Start a call first.', | ||||||||||||||||||||||||||||||||||
| callSessionId: '', | ||||||||||||||||||||||||||||||||||
| responderCount: 0, | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Resolve speaker identity | ||||||||||||||||||||||||||||||||||
| const speakerId = params.speakerId ?? params.userId ?? ''; | ||||||||||||||||||||||||||||||||||
| let speakerName = params.speakerName ?? ''; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!speakerName && speakerId) { | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const result = await DataList.execute<UserEntity>({ | ||||||||||||||||||||||||||||||||||
| collection: 'users', | ||||||||||||||||||||||||||||||||||
| filter: { id: speakerId }, | ||||||||||||||||||||||||||||||||||
| limit: 1, | ||||||||||||||||||||||||||||||||||
| dbHandle: 'default', | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| if (result.success && result.items?.length) { | ||||||||||||||||||||||||||||||||||
| speakerName = result.items[0].displayName || result.items[0].uniqueId; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||
| // Fall through to default | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (!speakerName) speakerName = 'CLI'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Route through the existing transcription command | ||||||||||||||||||||||||||||||||||
| const transcriptionResult = await CollaborationLiveTranscription.execute({ | ||||||||||||||||||||||||||||||||||
| callSessionId: sessionId, | ||||||||||||||||||||||||||||||||||
| speakerId, | ||||||||||||||||||||||||||||||||||
| speakerName, | ||||||||||||||||||||||||||||||||||
| transcript: params.message, | ||||||||||||||||||||||||||||||||||
| confidence: params.confidence ?? 1.0, | ||||||||||||||||||||||||||||||||||
| language: 'en', | ||||||||||||||||||||||||||||||||||
| timestamp: Date.now(), | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const responderCount = transcriptionResult.success | ||||||||||||||||||||||||||||||||||
| ? parseInt(transcriptionResult.message.match(/(\d+) AI/)?.[1] ?? '0', 10) | ||||||||||||||||||||||||||||||||||
| : 0; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+72
to
+75
|
||||||||||||||||||||||||||||||||||
| const responderCount = transcriptionResult.success | |
| ? parseInt(transcriptionResult.message.match(/(\d+) AI/)?.[1] ?? '0', 10) | |
| : 0; | |
| let responderCount = 0; | |
| if (transcriptionResult.success) { | |
| const structuredResponderCount = (transcriptionResult as any).responderCount; | |
| if (typeof structuredResponderCount === 'number' && Number.isFinite(structuredResponderCount) && structuredResponderCount >= 0) { | |
| responderCount = structuredResponderCount; | |
| } else { | |
| const match = transcriptionResult.message.match(/(\d+)\s+AI/); | |
| if (match) { | |
| responderCount = parseInt(match[1], 10); | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LiveSendServerCommandhas unused imports (Commands,DataListResult) which will trigger lint/tsc failures in setups with unused-import checks. Please remove them (or use them) to keep the file warning-free.