Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6d4905b
feat(ai-tts): add ai_tts and ai_tts_blocks tables
Innei Aug 5, 2026
fa64f4c
feat(ai-tts): add ttsOptions configuration section
Innei Aug 5, 2026
d33da80
test(ai-tts): cover ttsOptions provider validation and DSL rendering
Innei Aug 5, 2026
ac14a8d
feat(file): support audio uploads with an explicit object key
Innei Aug 5, 2026
7192738
feat(ai-tts): register TTS error codes
Innei Aug 5, 2026
a55cb74
refactor(lexical): expose extractRootBlockNodes
Innei Aug 5, 2026
75bc712
feat(ai-tts): add pure block planning module
Innei Aug 5, 2026
83bd0b0
fix(ai-tts): guard splitIntoChunks against non-positive maxChars, rec…
Innei Aug 5, 2026
8e339e7
feat(ai-tts): add content-addressed object key builder
Innei Aug 5, 2026
569253f
feat(ai-tts): add OpenAI-compatible speech runtime adapter
Innei Aug 5, 2026
0142ee7
fix(ai-tts): fail fast on an aborted signal instead of retrying
Innei Aug 5, 2026
05c2d26
feat(ai-tts): add repository
Innei Aug 5, 2026
bca40d9
fix(ai-tts): close delete TOCTOU race, drop unused listPaginated search
Innei Aug 5, 2026
95ce652
feat(ai-tts): add generation task handler
Innei Aug 5, 2026
1f6da9a
test(file): cover deleteObject and the repeat-write FILE_EXISTS contract
Innei Aug 5, 2026
6b450e8
fix(ai-tts): address review — voice-addressed objects, translation fr…
Innei Aug 5, 2026
a0ac135
fix(ai-tts): hash translation currency with the row's own sourceLang
Innei Aug 5, 2026
ffcd433
feat(ai-tts): clean up audio on article deletion and reconcile orphans
Innei Aug 5, 2026
e6d11f0
refactor(ai-tts): ride the generalized file-reference system instead …
Innei Aug 5, 2026
a7cc05e
test(file): assert the Active intermediate state before deleting the …
Innei Aug 5, 2026
092d493
feat(ai-tts): add admin and public HTTP endpoints
Innei Aug 5, 2026
3a446ed
feat(ai-tts): advertise narration availability in article detail meta
Innei Aug 5, 2026
7bb7c75
feat(api-client): add getTts
Innei Aug 5, 2026
fdde3fc
test(api-client): fix getTts test to assert response, not just throw
Innei Aug 5, 2026
8b21f29
feat(admin): add TTS generation panel to the editor
Innei Aug 5, 2026
5dcc8ee
fix(admin): drop TTS panel delete action, reset run state on refId ch…
Innei Aug 5, 2026
f2744c9
feat(ai-tts): resolve article titles in the admin TTS list meta
Innei Aug 5, 2026
e8cbfac
feat(admin): add TTS narration management page
Innei Aug 5, 2026
3da0ba9
fix(ai-tts): make public narration entitlement-aware instead of premi…
Innei Aug 5, 2026
af509e6
fix(ai-tts): gate availability on a published block order and order s…
Innei Aug 5, 2026
c743ea1
fix(ai-tts): key the reuse decision on the object key, not the text f…
Innei Aug 5, 2026
83f5726
docs(ai-tts): record post-implementation deviations; drop the now-cal…
Innei Aug 5, 2026
c5d5f40
fix(admin): unstick the TTS panel on a failed poll and make the batch…
Innei Aug 5, 2026
09dab36
fix(ai-tts): require both note gates, not just the password one
Innei Aug 5, 2026
b74d5a9
fix(admin): confirm and relabel the TTS batch action; refresh a stale…
Innei Aug 5, 2026
5eaf1aa
polish(admin): refine TTS detail UI — timeline segments, cleaner meta…
Innei Aug 7, 2026
330dfe3
feat(admin): add AI quick actions to Note and Post context menus
Innei Aug 7, 2026
d387a43
merge: resolve migration conflict with master, renumber ai_tts to 0028
Innei Aug 7, 2026
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
57 changes: 57 additions & 0 deletions apps/admin/src/api/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,60 @@ export function updateTranslationEntry(
export function deleteTranslationEntry(id: string) {
return deleteJson<void>(`/ai/translations/entries/${id}`)
}

export interface AITtsSegment {
blockId: string
chunkIndex: number
text: string
url: string
}

export interface AITtsRow {
blockOrder: string[]
charCount: number
id: string
isTranslation: boolean
lang: string
model: string
segments: AITtsSegment[]
speed: number
updatedAt?: null | string
voice: string
}

export function createTtsTask(data: {
force?: boolean
langs?: string[]
refId: string
}) {
return postJson<CreateTaskResponse, typeof data>('/ai/tts/task', data)
}

export function getTtsByRefId(refId: string) {
return getJson<AITtsRow[]>(`/ai/tts/ref/${refId}`)
}

export function deleteTts(id: string) {
return deleteJson<void>(`/ai/tts/${id}`)
}

export interface AITtsListRow {
blockCount: number
charCount: number
id: string
lang: string
refId: string
updatedAt?: null | string
}

export type AITtsArticleMap = Record<string, ArticleInfo>

export interface AITtsListResponse {
articles: AITtsArticleMap
data: AITtsListRow[]
pagination: PaginationInfo
}

export function getTtsList(params: { page?: number; size?: number }) {
return getJson<AITtsListResponse>('/ai/tts', params)
}
45 changes: 45 additions & 0 deletions apps/admin/src/api/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,48 @@ describe('requestJson error handling', () => {
expect((error as ApiRequestError).code).toBeUndefined()
})
})

describe('requestJson meta unwrapping', () => {
afterEach(() => {
vi.restoreAllMocks()
})

it('flattens pagination alongside data', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
jsonResponse({
data: [{ id: '1' }],
meta: { pagination: { page: 1, size: 10, total: 1, totalPages: 1 } },
}),
)

const result = await getJson<{
data: unknown[]
pagination: { total: number }
}>('/ai/tts')

expect(result.data).toEqual([{ id: '1' }])
expect(result.pagination.total).toBe(1)
})

it('carries sibling meta fields (e.g. articles) alongside pagination', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
jsonResponse({
data: [{ id: '1', ref_id: '9' }],
meta: {
articles: { '9': { id: '9', title: 'Hello', type: 'Post' } },
pagination: { page: 1, size: 10, total: 1, totalPages: 1 },
},
}),
)

const result = await getJson<{
articles: Record<string, { id: string; title: string }>
data: unknown[]
pagination: { total: number }
}>('/ai/tts')

expect(result.articles).toEqual({
'9': { id: '9', title: 'Hello', type: 'Post' },
})
})
})
5 changes: 4 additions & 1 deletion apps/admin/src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ResponseEnvelope<T> = {
}
meta?: {
pagination?: unknown
[key: string]: unknown
}
message?: string | string[]
}
Expand Down Expand Up @@ -94,9 +95,11 @@ export async function requestJson<TResponse>(

if (responseData && 'data' in responseData) {
if (responseData.meta?.pagination) {
const { pagination, ...restMeta } = responseData.meta
return {
data: responseData.data,
pagination: responseData.meta.pagination,
pagination,
...restMeta,
} as TResponse
}

Expand Down
1 change: 1 addition & 0 deletions apps/admin/src/api/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum AITaskType {
Insights = 'ai:insights',
InsightsTranslation = 'ai:insights:translation',
ImageGeneration = 'ai:image:generation',
Tts = 'ai:tts',
}

export enum AITaskStatus {
Expand Down
Loading