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
17 changes: 13 additions & 4 deletions packages/freecut-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@ The `EditorHost` contract carries opaque media locators and authoritative
snapshots. It never accepts filesystem paths, permanent URLs, provider keys,
or media bytes. Supported edits are submitted through `submitEdit`; rejected
or conflicting results return an authoritative snapshot to the surface. The
0.2.0 surface adds host-backed caption tracks, bounded cues, caption styles,
and display toggles to that same command path.
0.3.0 surface adds an optional host-backed transcript consumer. Hosts opt into the
transcript tab by providing `EditorHost.transcript` and explicitly enabling
`media.transcription`. The port returns a compact status receipt and bounded
microsecond sections, and previews source-bound caption commands with
`willMutateTimeline: false`; only an explicit user action submits that returned
batch through `submitEdit`. Transcript IDs, asset IDs, source hashes, cursors,
and structured errors are opaque browser data—authentication, transport,
provider details, URLs, paths, and media bytes remain host-owned.

The same 0.3.0 surface retains the host-backed caption tracks, bounded cues,
caption styles, and display toggles from 0.2.0.

This package is built from a specific FreeCut commit. To create the local
consumer artifact from a clean checkout, run:
Expand All @@ -49,7 +58,7 @@ that has `write:packages` and run:

```bash
NODE_AUTH_TOKEN="$GITHUB_CLASSIC_PAT" npm publish \
artifacts/freecut-editor-surface-0.2.0.tgz \
artifacts/freecut-editor-surface-0.3.0.tgz \
--registry=https://npm.pkg.github.com
```

Expand All @@ -69,5 +78,5 @@ It can then install the exact published version and keep it pinned in its
lockfile:

```bash
npm install @quantfive/freecut-editor-surface@0.2.0
npm install @quantfive/freecut-editor-surface@0.3.0
```
2 changes: 1 addition & 1 deletion packages/freecut-editor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@quantfive/freecut-editor-surface",
"version": "0.2.0",
"version": "0.3.0",
"description": "The host-backed FreeCut browser editor surface.",
"license": "MIT",
"repository": {
Expand Down
134 changes: 134 additions & 0 deletions packages/freecut-editor/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,139 @@ export interface HostNotice {
operationId?: string
}

export declare const MAX_TRANSCRIPT_SELECTIONS: number
export declare const MAX_TRANSCRIPT_SECTION_PAGE_SIZE: number
export declare const MAX_TRANSCRIPT_SECTION_TEXT_BYTES: number
export declare const MAX_TRANSCRIPT_COMMAND_TEXT_BYTES: number
export declare const MAX_TRANSCRIPT_DURATION_US: number
export declare const MAX_TRANSCRIPT_CURSOR_LENGTH: number
export declare const MAX_TRANSCRIPT_QUERY_LENGTH: number

export type HostTranscriptStatus =
| 'pending'
| 'running'
| 'succeeded'
| 'failed'
| 'stale'
| 'purged'

export interface HostTranscriptError {
code: string
message: string
retryable: boolean
details?: Readonly<Record<string, unknown>>
}

export interface HostTranscriptStatusReceipt {
transcriptId: string
assetId: string | null
sourceAssetHash: string
status: HostTranscriptStatus
language?: string | null
durationUs: number | null
sectionCount: number
error?: HostTranscriptError | null
}

export interface HostTranscriptSection {
id: string
transcriptId: string
ordinal: number
startUs: number
endUs: number
text: string
speaker?: string | null
}

export interface HostTranscriptSectionsRequest {
transcriptId: string
cursor?: string | null
limit?: number
startUs?: number
endUs?: number
}

export interface HostTranscriptSectionsPage {
transcriptId: string
sections: readonly HostTranscriptSection[]
nextCursor?: string | null
hasMore: boolean
}

export interface HostTranscriptSearchRequest {
transcriptId: string
query: string
cursor?: string | null
limit?: number
}

export interface HostTranscriptSearchPage {
transcriptId: string
query: string
sections: readonly HostTranscriptSection[]
nextCursor?: string | null
hasMore: boolean
}

export interface HostTranscriptRange {
startUs: number
endUs: number
text?: string
}

export type HostTranscriptCommandAction = 'cut' | 'captions' | 'ripple_cut' | 'caption'

export interface HostTranscriptCommandPreviewRequest {
transcriptId: string
assetId: string
sourceAssetHash: string
operationId: string
idempotencyKey: string
baseRevision: number
action: HostTranscriptCommandAction
timestampCapability: 'section' | 'word' | 'frame'
sectionIds?: readonly string[]
ranges?: readonly HostTranscriptRange[]
captionTrackId?: string
captionTrackName?: string
captionLanguage?: string | null
preconditions?: readonly object[]
}

export interface HostTranscriptCommandPreview {
status: 'preview' | 'replayed'
receiptId: string
transcriptId: string
assetId: string
sourceAssetHash: string
timestampCapability: 'section'
timelineId: string
operationId: string
idempotencyKey: string
baseRevision: number
commandBatch: EditCommandBatch
preview: Readonly<{
action?: string
sectionCount?: number
captionCount?: number
willMutateTimeline: false
[key: string]: unknown
}>
}

export interface EditorTranscriptPort {
getStatus(): Promise<HostTranscriptStatusReceipt | null> | HostTranscriptStatusReceipt | null
getSections(
request: HostTranscriptSectionsRequest,
): Promise<HostTranscriptSectionsPage> | HostTranscriptSectionsPage
search?(
request: HostTranscriptSearchRequest,
): Promise<HostTranscriptSearchPage> | HostTranscriptSearchPage
previewCommands(
request: HostTranscriptCommandPreviewRequest,
): Promise<HostTranscriptCommandPreview> | HostTranscriptCommandPreview
}

export interface HostAppliedEditResult {
status: 'applied' | 'replayed'
snapshot: EmbeddedEditorSnapshot
Expand Down Expand Up @@ -207,6 +340,7 @@ export interface EditorHost {
locator: MediaLocator,
): Promise<ResolvedMediaLocator | null> | ResolvedMediaLocator | null
submitEdit(batch: EditCommandBatch): Promise<HostEditResult> | HostEditResult
transcript?: EditorTranscriptPort
navigation?: EditorHostNavigation
notify?(notice: HostNotice): void
}
Expand Down
20 changes: 20 additions & 0 deletions packages/freecut-editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ export { FreeCutEditorSurface } from '@/features/editor/host/editor-surface'
export { EditorHostProvider } from '@/features/editor/host/context-provider'
export {
DEFAULT_HOST_CAPABILITIES,
MAX_TRANSCRIPT_COMMAND_TEXT_BYTES,
MAX_TRANSCRIPT_CURSOR_LENGTH,
MAX_TRANSCRIPT_DURATION_US,
MAX_TRANSCRIPT_QUERY_LENGTH,
MAX_TRANSCRIPT_SECTION_PAGE_SIZE,
MAX_TRANSCRIPT_SECTION_TEXT_BYTES,
MAX_TRANSCRIPT_SELECTIONS,
SUPPORTED_HOST_COMMANDS,
capabilityForCommand,
createLocalEditorHost,
Expand All @@ -17,11 +24,24 @@ export type {
EmbeddedEditorAsset,
EmbeddedEditorProject,
EmbeddedEditorSnapshot,
EditorTranscriptPort,
HostAppliedEditResult,
HostConflictResult,
HostEditResult,
HostMediaKind,
HostNotice,
HostTranscriptCommandAction,
HostTranscriptCommandPreview,
HostTranscriptCommandPreviewRequest,
HostTranscriptError,
HostTranscriptRange,
HostTranscriptSearchPage,
HostTranscriptSearchRequest,
HostTranscriptSection,
HostTranscriptSectionsPage,
HostTranscriptSectionsRequest,
HostTranscriptStatus,
HostTranscriptStatusReceipt,
LocalEditorHostOptions,
MediaLocator,
ResolvedMediaLocator,
Expand Down
18 changes: 14 additions & 4 deletions src/features/editor/components/media-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ import { EffectThumbnail, useGpuEffectPreviewData } from '@/features/editor/deps
import { createLogger } from '@/shared/logging/logger'
import { useSettingsStore } from '@/features/editor/deps/settings'
import { resolveGeneratedLayerCanvasSize } from '../utils/generated-layer-canvas-size'
import { useEditorCapability, useEditorHostMode } from '../host/context'
import { useEditorCapability, useEditorHostContext, useEditorHostMode } from '../host/context'
import { HostTranscriptEditor } from '../host/transcript-editor'
const LazyAiPanel = lazy(() => import('./ai-tab').then((m) => ({ default: m.AiTab })))
const LazyTranscriptEditorPanel = lazy(() =>
importTranscriptEditorPanel().then(({ TranscriptEditorPanel }) => ({
Expand Down Expand Up @@ -290,7 +291,9 @@ const ADD_TEXT_TEMPLATE_LABEL = 'Add Text'
export const MediaSidebar = memo(function MediaSidebar() {
const { t } = useTranslation()
const hostMode = useEditorHostMode()
const { host } = useEditorHostContext()
const canAddTimeline = useEditorCapability('timeline.add')
const canTranscribe = useEditorCapability('media.transcription')
const editorDensity = useSettingsStore((s) => s.editorDensity)
const editorLayout = getEditorLayout(editorDensity)
// Use granular selectors - Zustand v5 best practice
Expand Down Expand Up @@ -557,7 +560,12 @@ export const MediaSidebar = memo(function MediaSidebar() {
{ id: 'ai' as const, icon: WandSparkles, label: t('editor.mediaSidebar.ai') },
]
const visibleCategories = hostMode
? categories.filter(({ id }) => id === 'media' || (id === 'text' && canAddTimeline))
? categories.filter(
({ id }) =>
id === 'media' ||
(id === 'text' && canAddTimeline) ||
(id === 'transcript' && canTranscribe && !!host?.transcript),
)
: categories

const shouldSuppressGeneratedItemClick = useCallback(() => {
Expand Down Expand Up @@ -1165,11 +1173,13 @@ export const MediaSidebar = memo(function MediaSidebar() {
<div
className={`min-h-0 flex-1 overflow-hidden ${activeTab === 'transcript' ? 'block' : 'hidden'}`}
>
{activeTab === 'transcript' && (
{activeTab === 'transcript' && hostMode ? (
<HostTranscriptEditor active />
) : activeTab === 'transcript' ? (
<Suspense fallback={null}>
<LazyTranscriptEditorPanel active />
</Suspense>
)}
) : null}
</div>

{/* AI Tab */}
Expand Down
Loading
Loading