Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const utteranceNode: NodeSpec = {
startTimeSeconds: { default: null },
endTimeSeconds: { default: null },
continuationOfId: { default: null },
intonation: { default: false }, // show a pitch contour above the words
intonationChannel: { default: null }, // per-block audio channel override (number | null)
},
toDOM(node) {
return ['p', {
Expand All @@ -23,18 +25,23 @@ const utteranceNode: NodeSpec = {
...(node.attrs.tierId ? { 'data-tier-id': node.attrs.tierId } : {}),
'data-participant': node.attrs.participant,
...(node.attrs.continuationOfId ? { 'data-continuation-of': node.attrs.continuationOfId } : {}),
...(node.attrs.intonation ? { 'data-intonation': 'true' } : {}),
...(node.attrs.intonationChannel != null ? { 'data-intonation-channel': String(node.attrs.intonationChannel) } : {}),
}, 0]
},
parseDOM: [{
tag: 'p.utt',
getAttrs(dom) {
const el = dom
const chAttr = el.getAttribute('data-intonation-channel')
return {
id: el.getAttribute('data-id'),
tier: el.getAttribute('data-tier') ?? '',
tierId: el.getAttribute('data-tier-id') ?? null,
participant: el.getAttribute('data-participant') ?? '',
continuationOfId: el.getAttribute('data-continuation-of') ?? null,
intonation: el.getAttribute('data-intonation') === 'true',
intonationChannel: chAttr != null ? Number(chAttr) : null,
}
},
}],
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,16 @@ export class AnnotationStore extends TypedEmitter<StoreEvents> {
this.ydoc.transact(() => { this.yParticipants.delete(id) }, USER_ORIGIN)
}

/** Set (or clear, with null) a participant's default audio channel. */
setParticipantChannel(id: ID, channel: number | null): void {
const p = this.yParticipants.get(id)
if (!p) return
const next = { ...p }
if (channel == null) delete next.channel
else next.channel = channel
this.ydoc.transact(() => { this.yParticipants.set(id, next) }, USER_ORIGIN)
}

getParticipant(id: ID): ParticipantJSON | undefined {
return this.yParticipants.get(id)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface ParticipantJSON {
id: ID
label: string
attrs?: Record<string, string>
/** Default audio channel index for this speaker (e.g. intonation contour source). */
channel?: number
}

export interface TierDefJSON {
Expand Down
4 changes: 4 additions & 0 deletions packages/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ function createWindow(): BrowserWindow {
if (!isMac) win.setMenuBarVisibility(false)

if (process.env['ELECTRON_RENDERER_URL']) {
// [PERF] temporary: mirror renderer [PERF] logs to the terminal for easy copy/paste
win.webContents.on('console-message', (_e, _level, message) => {
if (message.startsWith('[PERF')) console.log(message)
})
void win.loadURL(process.env['ELECTRON_RENDERER_URL'])
win.webContents.openDevTools()
} else {
Expand Down
109 changes: 108 additions & 1 deletion packages/editor/src/TranscriptEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import { buildAnchorPlugin, buildAnchorAlignmentPlugin } from './plugins/anchor.js'
import { buildImageInputRulePlugin } from './plugins/image-command.js'
import { buildSpectInputRulePlugin } from './commands/viz-commands.js'
import type { GetIntonation } from './nodeviews/ProsodyLayer.js'
import { buildSymbolInputRulePlugin } from './plugins/symbol-input.js'
import type { SymbolDef } from '@mumo/core'
import type { VizContextMenuCallback } from './nodeviews/VisualizationNodeView.js'
Expand All @@ -65,6 +66,9 @@
showEnd?: boolean
onEscapeKey?: () => void
getTokenTime?: (id: string) => { start: number; end: number } | undefined
getIntonation?: GetIntonation
getAudioChannels?: () => Array<{ index: number; label: string }>
getParticipantChannel?: (participant: string) => number | null
editable?: boolean
tokenClickMode?: boolean
ontokenclick?: (token: TokenRecord) => void
Expand Down Expand Up @@ -106,6 +110,9 @@
showEnd = false,
onEscapeKey,
getTokenTime,
getIntonation,
getAudioChannels,
getParticipantChannel,
editable = true,
tokenClickMode = false,
ontokenclick,
Expand Down Expand Up @@ -473,7 +480,7 @@
},
nodeViews: {
utterance: (node, editorView, getPos) =>
new UtteranceNodeView(node, editorView, getPos, onSeek),
new UtteranceNodeView(node, editorView, getPos, onSeek, tokenStore, getTokenTime, getIntonation, getAudioChannels, getParticipantChannel),
visualization: (node, editorView, getPos) =>
new VisualizationNodeView(node, editorView, getPos, onSeek, onVizContextMenu),
image: (node, editorView, getPos) =>
Expand Down Expand Up @@ -1380,6 +1387,76 @@
color: var(--color-primary, #4a90d9);
}

:global(.utt-ctx-check) {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Checkbox indicator (boolean toggle, e.g. Intonation) */
:global(.utt-ctx-box) {
width: 12px;
height: 12px;
border: 1.5px solid #aaa;
border-radius: 3px;
box-sizing: border-box;
flex-shrink: 0;
position: relative;
}
:global(.utt-ctx-box.on) {
background: var(--color-primary, #4a90d9);
border-color: var(--color-primary, #4a90d9);
}
:global(.utt-ctx-box.on::after) {
content: '';
position: absolute;
left: 3px;
top: 0.5px;
width: 4px;
height: 7px;
border: solid #fff;
border-width: 0 1.5px 1.5px 0;
box-sizing: border-box;
transform: rotate(45deg);
}
/* Radio indicator (single choice, e.g. intonation channel) */
:global(.utt-ctx-radio) {
width: 12px;
height: 12px;
border: 1.5px solid #aaa;
border-radius: 50%;
box-sizing: border-box;
flex-shrink: 0;
position: relative;
}
:global(.utt-ctx-radio.on) {
border-color: var(--color-primary, #4a90d9);
}
:global(.utt-ctx-radio.on::after) {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--color-primary, #4a90d9);
transform: translate(-50%, -50%);
}

:global(.utt-ctx-submenu-parent) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}

:global(.utt-ctx-arrow) {
color: var(--color-text-muted, #888);
font-size: 1rem;
line-height: 1;
margin-left: auto;
}

:global(.utt-tier) {
flex-shrink: 0;
width: var(--tier-col-w, 0rem);
Expand Down Expand Up @@ -1415,6 +1492,36 @@
font-family: var(--transcript-font, 'CMU Serif', 'Computer Modern', Georgia, serif);
}

/* Intonation contour: extra leading above each text line makes room for the band. */
:global(.utt--intonation .utt-content) {
line-height: 3.1;
}
/* Reserve room above the FIRST line so its band doesn't bleed into the block above.
24px = BAND_H (22) + BAND_GAP (2) in ProsodyLayer.ts. */
:global(.utt--intonation) {
padding-top: 24px;
}
:global(.utt-intonation-overlay) {
position: absolute;
inset: 0;
pointer-events: none;
overflow: visible;
z-index: 1;
}
:global(.utt-intonation-svg) {
position: absolute;
top: 0;
left: 0;
overflow: visible;
}
:global(.utt-intonation-path) {
fill: none;
stroke: #2979ff;
stroke-width: 1.5;
stroke-linejoin: round;
stroke-linecap: round;
}

/* --utt-meta-w is defined on .transcript-editor using --ln-w */

:global(.utt-gloss) {
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type { FormattingState } from './format.js'
export { setAllGlosses, setGlossesVisible, isGlossesVisible, getGlossEntryFor } from './gloss.js'
export type { GlossEntry } from './gloss.js'
export { setUttTiersVisible, isUttTiersVisible } from './utt-tier.js'
export { redrawAllProsody } from './prosody.js'
export { default as TranscriptOverlay } from './TranscriptOverlay.svelte'
export type { OverlayPlugin, OverlayContext } from './overlay.js'
export { overlapAlignmentKey } from './plugins/overlap.js'
Expand All @@ -13,6 +14,7 @@ export { UtteranceNodeView } from './nodeviews/UtteranceNodeView.js'
export { ImageNodeView } from './nodeviews/ImageNodeView.js'
export { VisualizationNodeView } from './nodeviews/VisualizationNodeView.js'
export { buildImageInputRulePlugin } from './plugins/image-command.js'
export type { GetIntonation } from './nodeviews/ProsodyLayer.js'
export { buildKeymapPlugin } from './commands/keymaps.js'
export { buildSymbolInputRulePlugin, SYMBOL_COMMANDS, DEFAULT_SYMBOL_DEFS } from './plugins/symbol-input.js'
export { runToggleBold, runToggleItalic, runToggleStrike, runToggleUnderline, runInsertChar, runInsertOverlapBracket, runSelectParentBlock, runApplyFont, runInsertComment } from './commands/toolbar-commands.js'
Expand Down
Loading
Loading