feat: add QNAIGC (Qiniu Cloud) TTS and image providers#356
Open
zhenzhu143321 wants to merge 30 commits intoTHU-MAIC:mainfrom
Open
feat: add QNAIGC (Qiniu Cloud) TTS and image providers#356zhenzhu143321 wants to merge 30 commits intoTHU-MAIC:mainfrom
zhenzhu143321 wants to merge 30 commits intoTHU-MAIC:mainfrom
Conversation
1. TTS voice consistency: generateAndStoreTTS now accepts optional
TTSOverrides to pin voice/provider across a batch run.
backfillMissingTTS snapshots TTS settings before iterating scenes,
preventing voice changes from async provider loading.
2. API-generated classrooms now include images: pass
imageGenerationEnabled: true to generateSceneOutlinesFromRequirements
so LLM includes mediaGenerations in outlines.
3. Add "Publish as public" button (Globe icon) in Header:
- POST classroom.json to /api/classroom (now includes outlines)
- Upload only current classroom's TTS audio from IndexedDB
- Upload completed media generation results
- Fixed TTS cross-classroom leak: filter by scene audioIds instead
of uploading all IndexedDB audio files
4. POST /api/classroom now persists outlines (was previously dropped).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add /api/classroom/media endpoint for uploading/serving cached TTS
and generated images/videos (idempotent, 50MB limit)
- Add media-storage module for persistent file storage under
data/classrooms/{id}/media/
- Enhance classroom-storage with listClassrooms and readClassroom
- Homepage now displays public classrooms from server storage
- Update CLAUDE_DEPLOY_GUIDE with job persistence, media cache docs,
and troubleshooting sections
- Add generation i18n keys for public classroom listing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace hardcoded QN API key with $QN_API_KEY env var reference - Mask key prefix in .env.local status section (sk-****) - Correct proxy docs: global HTTP_PROXY only for Tavily/pnpm, LLM proxy is per-provider in YAML - Update provider count to 11 (qn already built-in), use myProvider as example - Add note that QN_* vars need manual addition to .env.local - Fix frontend storage description: settings in localStorage, classroom data in IndexedDB - Add CLAUDE_DEPLOY_GUIDE.md to .gitignore and untrack it (contains machine-specific config) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Course and CourseListItem types with Omit utility - Implement server-side storage with atomic writes - Create API routes for CRUD and classroom relationships - Add Zustand store with optimistic updates - Build course list, detail pages and form components - Add i18n support (zh-CN/en-US) - Integrate with home page tabs Optimizations: - Parallel file I/O in listCourses using Promise.all - Parallel API loading on homepage - Create generic resource-lock utility - Fix concurrent modification race conditions - Use nanoid for ID generation - Add error handling and validation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix CourseForm type signature to exclude classroomIds - Add classroomIds in createCourse implementation - Fix classroom route from /stage to /classroom - Change default tab to standalone for backward compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…inding flow - Course → Chapter[] → Classroom (replacing flat classroomIds[]) - Read-time migration writes back immediately to stabilize chapter IDs - CourseFormData decoupled from Course type; publishStatus runtime-derived - New API: /api/course/chapters (POST/PUT/DELETE); remove old /classrooms route - ChapterList + ChapterForm + ClassroomPickerDialog UI components - sessionStorage courseChapterContext with stageId binding; auto-clears on cancel/stale - Header auto-binds chapter after publish; validates stageId before writing - Batch /api/classroom fetch replaces N+1 per-chapter requests - COURSE_CHAPTER_CONTEXT_KEY + CourseChapterContext + ClassroomMeta extracted to lib/types/course.ts - Fix standalone build: copy static assets in start-8002.sh - Fix gen_img_* 404: dynamic route app/[mediaId]/route.ts serves media files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1: ClassroomPickerDialog crash — destructure {classrooms} from apiSuccess
response instead of treating it as raw array; read sceneCount not scenes.length
Bug 2: courseChapterContext leak — add createdAt timestamp to CourseChapterContext;
handleGenerate clears context if stageId already set OR older than 5 minutes,
preventing stale context from binding a new classroom to a forgotten chapter
Bug 3: unbind/clear-description silently failed — JSON.stringify drops undefined keys;
introduce ChapterUpdates type (classroomId/description allow null); updateChapter
storage fn now deletes the field when null is received; API route uses 'in body'
check to detect explicit null vs absent field; handleUnbind passes null not undefined
Bug 4: scene count always 0 — ClassroomListItem has sceneCount:number not scenes[];
fix both loadClassroomMeta in course detail page and ClassroomPickerDialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1: reorderChapters now validates that incoming chapterIds exactly matches the current chapter set — partial list previously silently deleted unlisted chapters Bug 2: ChapterForm passes null (not undefined) for empty description so JSON.stringify preserves the field; updateChapter already handles null as an explicit clear; addChapter normalises null description to omit on creation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rovements
- Add async job-based classroom generation endpoint (POST /api/generate-classroom,
GET /api/generate-classroom/[jobId]) backed by in-memory job queue
- Expand classroom-generation.ts with scene outline and content generation helpers
- classroom-storage: support new directory layout {id}/classroom.json alongside
legacy flat {id}.json; add listClassrooms with sceneCount
- provider-config: cache server providers in module scope; fix runtime-env import
- resolve-model: surface clearer error when provider is not configured
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…layback - classroom/[id]: always fetch latest data from server (cache: no-store) on mount, then write-through to IndexedDB; fall back to IndexedDB only if server fetch fails. Prevents stale IndexedDB data from shadowing server-side edits to published classrooms. - BaseTextElement: fix h-full + overflow-hidden so text boxes respect their designed height in read-only playback; clear paragraph margins and use --paragraphSpace CSS var for consistent line spacing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…style Replace ad-hoc HTML+hardcoded-Tailwind course pages with the project's established design system: - All 4 course components now use shadcn/ui primitives (Button, Card, Input, Textarea, Label, Badge, Dialog, AlertDialog, DropdownMenu, ScrollArea) instead of raw HTML elements - Course list and detail pages adopt the glassmorphism layout (gradient bg, backdrop-blur cards, decorative blur blobs) consistent with the home page - window.confirm() replaced by AlertDialog throughout - Hand-rolled classroom picker modal replaced by shadcn Dialog with ScrollArea, skeleton loading states, and motion stagger animations - Chapter action buttons collapsed into DropdownMenu (3 visible → 1); reorder arrows kept visible as high-frequency controls - AnimatePresence + layout animations for chapter add/remove/reorder - Empty states with BookOpen icon and clear CTAs - Create/edit course forms moved into Dialog overlays (no more full-page replacement) - 15 new i18n keys added to lib/i18n/course.ts; hardcoded Chinese strings in classroom-picker-dialog eliminated - Lucide icons replace text-character arrows (↑↓) and × buttons - All hardcoded colors (bg-blue-500, text-gray-500, text-red-500) replaced with semantic tokens (bg-primary, text-muted-foreground, text-destructive) - Purple primary theme consistent with rest of application Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace simple color-bar cards with rich gradient-cover cards — 135deg gradient header with decorative white circles, chapter count badge, teacher name, and hover-reveal delete; matches project's glassmorphism aesthetic throughout both the dedicated course list page and the home page 'My Courses' tab. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move course name into the gradient cover area (88px), expand the white info section to show 3 lines of description, and align home page cards with the dedicated course list page for visual consistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename misleading labels per information architecture review: - 我的课程 → 课程管理 (Course Library): no user isolation exists - 独立课堂 → 本地课堂 (Local Classrooms): browser IndexedDB, not server data - 公共课程 → 已发布课堂 (Published Classrooms): shows classrooms, not courses - 返回我的课程 → 返回课程管理 Also add a hint under the Local Classrooms tab explaining browser-local storage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'twoDaysAgo' i18n key (前天 / Two days ago) to formatDate so 2-day-old classrooms show '前天' instead of '2 天前' - Rename '最近学习' → '本地课堂记录' for terminology consistency within the Local Classrooms tab - Auto-switch default tab to 'courses' when courses exist but no local classrooms are present Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion - Add status/publishedAt fields to Course and CourseListItem types - Backfill status='draft' for old courses without the field - Support GET /api/course?status=published filter - Add publishCourse/unpublishCourse actions to Zustand store - Course detail page: publish button with canPublish guard, tooltip on disabled state, AlertDialog confirmation, X/N bound progress badge - Homepage: fetch and render published courses section above classrooms Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lassroom section collapse - POST /api/course now validates all chapters have classroomId when status=published - publishCourse/unpublishCourse check res.ok and throw on failure - Course detail page shows success/failure toast on publish/unpublish - Homepage: classrooms section is collapsible, defaults collapsed when published courses exist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d default - Extract COURSE_COVERS constant and getCourseGradient() helper to module level (was copy-pasted in 2 map callbacks with ~50 lines each) - Extract saveCourse() helper in store; publishCourse/unpublishCourse now share it - Remove redundant CourseChapter type annotation in API route (inferred from Course) - Fix classroomsExpanded: default true, collapse only after published courses load Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Browsing a published classroom triggered unnecessary POST /api/generate/image requests because generateMediaForOutlines ran before prefillMediaCache finished populating the Zustand store. Similarly, clicking Play before prefill completed caused TTS to fall back to a ~100s reading timer instead of actual audio. - Extract shared prefill promise into lib/media/prefill-state.ts (getter/setter) - Chain generateMediaForOutlines after getPrefillPromise() in page.tsx - Await getPrefillPromise() in stage.tsx handlePlayPause before engine.start() - Remove unused classroomId param from backfillMissingTTS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…adge semantics - Remove "Public Classrooms" section from homepage (-169 lines); only published courses appear publicly - Change publish rule: at least 1 chapter bound (was: all chapters bound), both server and client validation updated - Rename ClassroomMeta.published → .ready to clarify semantics (classroom exists on server ≠ "published") - Update chapter badges: "已就绪/Ready" and "待绑定/Pending" replace "已发布/未发布" wording - Update i18n strings (zh-CN + en-US) for new publish flow and binding dialog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Published courses on the homepage now link to /course/{id}?mode=view,
which hides all editor controls (edit, publish/unpublish, reorder,
bind/unbind classrooms, delete chapters, add chapters). Public viewers
see course info, chapter list with Ready/Coming Soon badges, and can
open bound classrooms for playback.
- Add useSearchParams + Suspense wrapper to course detail page
- isViewMode=true: back button → home, CardAction shows published badge
only, ChapterList is read-only, editor dialogs not rendered
- isViewMode=false: full editor UI unchanged
- Guard loadClassroomMeta effect and usedClassroomIds memo to skip in
view mode (avoids unnecessary API call + Set allocation)
- Unify published badge rendering — single conditional branch
- ChapterList: readOnly prop makes all editor callbacks optional with
non-null assertions; handleAdd/Edit/Remove guarded behind prop check
- Add i18n: backToHome, comingSoon, noChaptersPublic (zh-CN + en-US)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…urse
Previously the publish endpoint only checked that at least one chapter
had a classroomId, but did not verify the classroom JSON existed on disk.
A course with a dangling classroomId could be published and appear on the
homepage, causing 404/white-screen when opening the chapter.
- Add classroomExists() to lib/server/classroom-storage.ts (fs.access,
checks both new {id}/classroom.json and legacy {id}.json paths)
- In POST /api/course, after the bound-count check, verify all bound
classroomIds resolve to real files; reject with 400 listing missing IDs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add qnaigc-tts provider with 38 voices (traditional, special, bilingual) - Add qnaigc-image provider using hidden gemini-3.1-flash-image-preview model - Fix TTS request body to match upstream nested structure (audio/request) - Add voice selector and speed control to TTS settings panel - Fix QN LLM provider icon from qwen.svg to qiniu.svg - Add qiniu.svg logo asset - Add course list "Back to Home" button - Fix interactive scene width (no longer forced 16:9) - Fix course view mode classroom metadata loading Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend already passes speed_ratio to upstream; this exposes the speed slider in the TTS settings UI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The disable was left over after removing isViewMode from the useEffect dependency array. The deps list is now complete so the directive serves no purpose. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ables - classroomExists: add isValidClassroomId guard to prevent path traversal via user-controlled classroomId strings in course publish validation - chapter-list: fix isReady default ?? true → ?? false; metadata loading state should treat missing/unloaded meta as not-ready, not ready - app/page.tsx: remove 4 stale eslint-disable/enable directives left over from the homepage public-courses refactor; add justified exhaustive-deps disable on mount-only useEffect Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gemini-3.1-flash-image-previewmodel with aspect ratio supportaudio.voice_type/request.text)qiniu.svginstead ofqwen.svgChanged files
lib/audio/types.ts,lib/audio/constants.ts,lib/audio/tts-providers.tslib/media/types.ts,lib/media/image-providers.ts,lib/media/adapters/qnaigc-image-adapter.ts(new)components/settings/tts-settings.tsx,components/settings/audio-settings.tsx,components/settings/index.tsxlib/server/provider-config.tslib/i18n/settings.tslib/ai/providers.ts(icon fix)public/logos/qiniu.svg(new)app/course/page.tsx,app/course/[id]/page.tsx,components/canvas/canvas-area.tsxTest plan
/coursepage has "Back to Home" button🤖 Generated with Claude Code