Skip to content

feat: add QNAIGC (Qiniu Cloud) TTS and image providers#356

Open
zhenzhu143321 wants to merge 30 commits intoTHU-MAIC:mainfrom
zhenzhu143321:feat/qnaigc-tts-image-integration
Open

feat: add QNAIGC (Qiniu Cloud) TTS and image providers#356
zhenzhu143321 wants to merge 30 commits intoTHU-MAIC:mainfrom
zhenzhu143321:feat/qnaigc-tts-image-integration

Conversation

@zhenzhu143321
Copy link
Copy Markdown

Summary

  • Add QNAIGC TTS provider (Qiniu Cloud) with 38 selectable voices across 3 categories (traditional, special, bilingual)
  • Add QNAIGC Image provider using hidden gemini-3.1-flash-image-preview model with aspect ratio support
  • Fix TTS settings panel to include voice selector and speed control (was previously missing)
  • Fix TTS request body format to match upstream API's nested structure (audio.voice_type / request.text)
  • Fix QN LLM provider icon to use qiniu.svg instead of qwen.svg
  • Add course list "Back to Home" button, fix interactive scene width, fix view mode metadata loading

Changed files

Area Files
TTS provider lib/audio/types.ts, lib/audio/constants.ts, lib/audio/tts-providers.ts
Image provider lib/media/types.ts, lib/media/image-providers.ts, lib/media/adapters/qnaigc-image-adapter.ts (new)
Settings UI components/settings/tts-settings.tsx, components/settings/audio-settings.tsx, components/settings/index.tsx
Server config lib/server/provider-config.ts
i18n lib/i18n/settings.ts
LLM provider lib/ai/providers.ts (icon fix)
Assets public/logos/qiniu.svg (new)
Course/Canvas app/course/page.tsx, app/course/[id]/page.tsx, components/canvas/canvas-area.tsx

Test plan

  • Select QNAIGC TTS in settings, verify 38 voices appear in dropdown
  • Fill API key, click Test TTS — should hear audio playback
  • Select QNAIGC Image in settings, click Test Connection — should succeed
  • Generate a classroom with QNAIGC TTS — verify audio in playback
  • Verify QN provider shows qiniu logo (not qwen) in LLM settings
  • Verify /course page has "Back to Home" button
  • Verify interactive scenes render full-width (not 16:9)

🤖 Generated with Claude Code

zhenzhu143321 and others added 30 commits March 20, 2026 08:55
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant