Skip to content

feat: implement video length selection in TopicVideoGenerator#19

Open
suganth07 wants to merge 13 commits intodevfrom
feat/Emotion-Video
Open

feat: implement video length selection in TopicVideoGenerator#19
suganth07 wants to merge 13 commits intodevfrom
feat/Emotion-Video

Conversation

@suganth07
Copy link
Collaborator

@suganth07 suganth07 commented Jan 25, 2026

  • Added a dialog for users to select video length before generation.
  • Updated the generateVideoScript function to accept a length parameter.
  • Modified the script generation logic to adjust based on selected length.
  • Enhanced database schema to ensure unique subtopic names per parent.
  • Added a batch script for deleting and regenerating the database and Android build.
  • Updated EmotionDetector to ensure proper emotion handling.
  • Improved logging for video generation processes.

Summary by CodeRabbit

  • New Features

    • Video length selection dialog with preset durations and length-aware script generation
    • App rebranded to "SkillSpark"
  • Documentation

    • Added comprehensive emotion-detection setup and troubleshooting guide
  • Improvements

    • Dynamic script timing based on selected video length
    • Safer DB startup with loading/error UI and migration verification
    • Better subtopic deduplication/ordering and runtime native face-landmark guard with graceful fallback
  • Removed

    • Example labels file and image-resize utility
  • Chores

    • Android asset handling, prebuild/build scripts, cleanup/reprovisioning script, and full DB migrations snapshot added

✏️ Tip: You can customize this high-level summary in your review settings.

- Added a dialog for users to select video length before generation.
- Updated the generateVideoScript function to accept a length parameter.
- Modified the script generation logic to adjust based on selected length.
- Enhanced database schema to ensure unique subtopic names per parent.
- Added a batch script for deleting and regenerating the database and Android build.
- Updated EmotionDetector to ensure proper emotion handling.
- Improved logging for video generation processes.
@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Adds video-length selection end-to-end (UI → agent → Gemini) with dynamic script timing, large DB schema/migration bundle and guarded DB init, subtopic deduplication with ordered inserts and retries, platform-aware MediaPipe face-landmarks loading with asset plugin and setup guide, and assorted build, script, and config updates.

Changes

Cohort / File(s) Summary
Video Generation Length Feature
components/topic/TopicVideoGenerator.tsx, lib/gemini.ts, server/agents/VideoGen.ts
Adds UI length-selection dialog; threads lengthInSeconds through UI → agent → GeminiService; updates generateVideoScript signature and makes section durations/word counts dynamic; persists length in agent state and logs it.
Subtopic Management & DB Migrations
server/queries/topics.ts, db/schema.ts, db/migrations/*, db/migrations/migrations.js, db/migrations/meta/*, db/drizzle.ts, db/provider.tsx
Adds case-insensitive/trimming dedupe, per-item insert with retry on (parentTopicId, order) conflicts using atomic max(order); introduces composite unique indexes; adds large SQL migration + migration metadata and migrations.js; runs migrations synchronously; adds guarded DB init with loading/error UI.
Emotion Detection & MediaPipe Assets
components/emotion/TopicEmotionDetector.tsx, lib/emotion/detectEmotion.ts, lib/emotion/EmotionDetector.ts, EMOTION_DETECTION_SETUP.md, plugins/withFaceLandmarkerAsset.js, assets/model/labels.txt
Switches to platform-aware runtime require of FaceLandmarks with safe fallbacks and default/no-face results when unavailable; adds detailed setup/troubleshooting doc; adds Expo plugin to copy face_landmarker.task into Android assets; removes assets/model/labels.txt.
Build, Scripts & Config
deleting_migration_deleting_android.sh, package.json, eas.json, .gitignore, app.config.ts, plugins/...
Adds cleanup/setup shell script, prebuild and Android build npm scripts, EAS config, updates .gitignore to track Android model asset, renames Expo app identifiers, and registers withFaceLandmarkerAsset plugin.
Misc / Cleanup
resize_image.py, components/topic/*, lib/emotion/EmotionDetector.ts
Removes resize_image.py; minor UI wiring and formatting/typing tweaks (length dialog integration) and small formatting change in EmotionDetector.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as TopicVideoGenerator
    participant Agent as VideoAgent
    participant Gemini as GeminiService
    participant External as GeminiAPI

    User->>UI: Click "Generate Video"
    UI->>UI: Show length-selection dialog
    User->>UI: Select duration (e.g., 60s)
    UI->>Agent: request generateTopicVideo(topic, subtopics, lengthInSeconds=60)
    Agent->>Agent: save lengthInSeconds in state
    Agent->>Gemini: generateVideoScript(topic, context, subtopics, tone, lengthInSeconds=60)
    Gemini->>Gemini: compute durations & target word counts
    Gemini->>External: send prompt with dynamic section durations
    External-->>Gemini: return script
    Gemini-->>Agent: return script + metadata
    Agent-->>UI: deliver script
    UI-->>User: display generated script
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat/video agent #7: Modifies video-generation flow and related services (TopicVideoGenerator, lib/gemini.ts, server/agents/VideoGen) — overlaps length-selection plumbing.
  • Feat/graph #3: Touches migration bundle and schema files (db/migrations/*, db/schema.ts, server/queries/topics.ts) — likely conflicts with new migrations and schema.
  • Feat/emotion #6: Changes emotion-detection/native-module handling and assets — overlaps platform-aware FaceLandmarks loading and asset plugin.

Poem

🐰 I thump my paws and pick the time,
I hop through scripts and tweak each line.
I guard the eyes when natives are gone,
I order subtopics, retry at dawn.
A carrot-sized prompt — now voilà, a perfect rhyme! 🎬

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main change: implementing video length selection in the TopicVideoGenerator component, which is supported by comprehensive changes to the component, gemini service, and video generation pipeline.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/queries/topics.ts (1)

119-151: Consider updating parent topic metadata even when no new subtopics are created.
The early return skips the metadata update block, so bestPractices/commonPitfalls/whyLearn/difficulty won’t refresh on subsequent runs. If this function can be called for regeneration or web‑search refreshes, move the metadata update before the early return or update it unconditionally.

components/topic/TopicVideoGenerator.tsx (1)

241-260: Use dynamic script length in status message and update dependency array

The selectedLength variable is actively used throughout the component (with options from 10s to 5m), but the status message still hardcodes "Generating 2-second script..." which is misleading. Include selectedLength in both the message template and the useCallback dependency array.

Suggested fix
-      case 'generating-script':
-        return 'Generating 2-second script...';
+      case 'generating-script':
+        return `Generating ${selectedLength}s script...`;
...
-  }, [videoStatus, downloadProgress, isLocalVideo, videoError]);
+  }, [videoStatus, downloadProgress, isLocalVideo, videoError, selectedLength]);
🤖 Fix all issues with AI agents
In `@components/topic/TopicVideoGenerator.tsx`:
- Around line 379-466: The length selector currently uses Pressable buttons but
should use radio semantics: change the container wrapping the options (the View
elements that hold the Pressable groups) to have accessibilityRole="radiogroup"
and for each option (the Pressable that calls setSelectedLength and reads
selectedLength) set accessibilityRole="radio" and accessibilityState={{ checked:
selectedLength === <value> }} (and update accessibilityLabel/accessible if
needed) so screen readers treat them as mutually-exclusive radios; update all
Pressable instances that setSelectedLength (and any parent View that groups
them) to apply these attributes consistently.

In `@db/schema.ts`:
- Around line 106-110: The new unique index
uniqueIndex("subtopics_parent_name_idx") on (parentTopicId, name) will fail if
duplicates exist; before applying the migration, write a data-cleanup migration
that (1) queries the subtopics table for duplicate groups by parentTopicId and
name, (2) resolves each group by either merging records or choosing a canonical
row and updating references, or renaming duplicates (e.g., append suffix) so
names become unique, and (3) deletes or updates the non-canonical rows and any
related foreign keys; run that cleanup migration (or backfill script) prior to
adding uniqueIndex("subtopics_parent_name_idx") in db/schema.ts to ensure the
unique constraint can be created successfully.

In `@deleting_migration_deleting_android.bat`:
- Around line 1-31: The batch script uses POSIX commands (rm, clear, sleep,
touch, mkdir -p, cp) which will fail in Windows cmd.exe; either rename this file
to a .sh and document it for bash/Git Bash usage, or convert the commands to
Windows cmd equivalents (use del /f, cls, timeout /t, type NUL > for creating
files, rmdir /s /q for folders, mkdir, copy) while preserving the same sequence
(remove public/database.sqlite, recreate it, remove db/migrations, run bun
db:generate and bun db:migrate, remove android, run bun expo prebuild --platform
android, create android/app/src/main/assets, copy face_landmarker.task there,
then bun run android); update the filename extension and any CI/dev docs
accordingly so callers use the correct shell.

In `@EMOTION_DETECTION_SETUP.md`:
- Around line 70-76: Several fenced code blocks in EMOTION_DETECTION_SETUP.md
are missing language identifiers (MD040); update each fence to include an
appropriate language specifier — use "text" for output-only blocks like the
example output (the block containing "✔ Created native directory...") and "bash"
(or other appropriate shell) for command blocks referenced elsewhere (e.g., the
blocks at the ranges noted). Locate the offending fences by searching for
triple-backtick blocks with no language and add the language token immediately
after the opening ```; apply the same change to the other instances mentioned
(around the other ranges/blocks) to satisfy markdownlint.
- Around line 64-68: The phrase "Expo managed" in the documentation text should
be hyphenated as "Expo-managed"; update the sentence that currently reads "Expo
managed projects don't have native `android/` folders by default." to
"Expo-managed projects don't have native `android/` folders by default." and
apply the same hyphenation to any other occurrences of "Expo managed" (e.g.,
within the paragraph describing generating native Android project structure,
applying plugins like `app.config.ts`, linking expo modules and creating the
gradle build system) to keep terminology consistent.

In `@server/queries/topics.ts`:
- Around line 99-101: The code currently uses existingSubtopics.length to assign
new subtopic order which can collide if rows were deleted or orders are
non‑contiguous; instead query the maximum order value from the subtopics table
within the same transaction (using tx and the subtopics model) and set the new
subtopic's order to (maxOrder ?? -1) + 1 (or 0 when no rows), updating the logic
around existingSubtopics and any other spot in this file (the second occurrence
around lines handling insertion at 160-178) to derive order from the max
aggregate rather than the array length.
- Around line 104-117: The current filter only checks against DB existingNames
and doesn't dedupe duplicate titles inside the same explanation.subtopics batch;
update the logic in server/queries/topics.ts (around existingNames,
newSubtopicsToCreate, explanation.subtopics, and normalizedName) to maintain a
single seen set seeded with existingNames and, as you iterate
explanation.subtopics, skip and log any subtopic whose normalized title is
already in seen, otherwise add it to seen and include it in newSubtopicsToCreate
so duplicates in the same AI payload are filtered out before inserting.
🧹 Nitpick comments (1)
server/agents/VideoGen.ts (1)

147-174: Add guardrails for lengthInSeconds.
Right now a non‑finite/negative or extremely large value could produce invalid prompts or excessive cost. Consider validating and capping to a product‑approved range.

🔧 Suggested guardrails (adjust limits to match UI options)
   if (!apiKey || !avatarId || !voiceId) {
     const error = 'Missing required HeyGen configuration (API key, avatar ID, or voice ID)';
     console.error('❌', error);
     throw new Error(error);
   }
+
+  if (!Number.isFinite(lengthInSeconds) || lengthInSeconds <= 0) {
+    throw new Error('lengthInSeconds must be a positive number');
+  }
+  const MAX_LENGTH_SECONDS = 600; // align with UI / product limits
+  if (lengthInSeconds > MAX_LENGTH_SECONDS) {
+    throw new Error(`lengthInSeconds exceeds max allowed (${MAX_LENGTH_SECONDS}s)`);
+  }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/queries/topics.ts (1)

243-244: Comment doesn't match code behavior.

The comment states "By default, return only 'original' subtopics (not websearch)" but the code returns all results without filtering. Either update the comment to reflect actual behavior, or add the filtering logic if original-only is the intended default.

   // By default, return only 'original' subtopics (not websearch)
-  return result;
+  return result.filter(st => {
+    const meta = (st.metadata as Record<string, any>) || {};
+    return (meta.source || 'original') === 'original';
+  });

Or if returning all is intended:

-  // By default, return only 'original' subtopics (not websearch)
+  // Return all subtopics when no source filter is specified
   return result;
components/topic/TopicVideoGenerator.tsx (1)

241-247: Update the status copy now that lengths vary.

The message still says “Generating 2-second script…”, which is misleading with 10s–5m presets.

✏️ Suggested tweak
-      case 'generating-script':
-        return 'Generating 2-second script...';
+      case 'generating-script':
+        return 'Generating script...';
🤖 Fix all issues with AI agents
In `@deleting_migration_deleting_android.sh`:
- Around line 1-5: Add a bash shebang to the top of the script so it can be
executed directly: insert "#!/usr/bin/env bash" as the very first line of
deleting_migration_deleting_android.sh (above the existing
rm/clear/echo/sleep/touch commands) to satisfy Shellcheck SC2148 and ensure the
script runs with bash when invoked as ./deleting_migration_deleting_android.sh.

In `@server/queries/topics.ts`:
- Around line 162-168: The current logic in createSubtopics queries
max(subtopics.order) via maxOrderResult and computes startOrder, which can race
when concurrent transactions insert for the same parentTopicId and produce
duplicate order values; to fix, switch to a DB-side sequence or auto-increment
for ordering (use a sequence and fetch NEXTVAL in the insert), or add a unique
constraint on (parentTopicId, order) and implement retry-on-conflict around the
insert loop, or perform the order assignment inside a single serialized
transaction/SELECT FOR UPDATE on the parentTopic row to serialize concurrent
creators; update references in createSubtopics, the maxOrderResult/startOrder
calculation and tx insert logic to use the chosen approach so ordering is
assigned atomically.
♻️ Duplicate comments (1)
components/topic/TopicVideoGenerator.tsx (1)

390-493: Use a single radiogroup with a label for all length options.

Each row is currently marked as its own radiogroup, which can make screen readers announce them as separate groups. Wrap all options in one radiogroup container (with an accessible label), and keep rows as layout-only containers.

♿ Suggested structure
-          <View className="flex-row gap-3" accessibilityRole="radiogroup">
+          <View accessibilityRole="radiogroup" accessibilityLabel="Video length" className="gap-3">
+            <View className="flex-row gap-3">
               <Pressable
                 onPress={() => setSelectedLength(10)}
                 className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 10 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
                 accessibilityRole="radio"
                 accessibilityState={{ checked: selectedLength === 10 }}
                 accessibilityLabel="10 seconds, ultra short"
               >
                 <Text className={`font-semibold text-center ${selectedLength === 10 ? 'text-primary' : 'text-foreground'}`}>10s</Text>
                 <Text className="text-xs text-muted-foreground text-center mt-0.5">Ultra short</Text>
               </Pressable>
-          </View>
+            </View>

-          <View className="flex-row gap-3" accessibilityRole="radiogroup">
+            <View className="flex-row gap-3">
               ...
-          </View>
+            </View>

-          <View className="flex-row gap-3" accessibilityRole="radiogroup">
+            <View className="flex-row gap-3">
               ...
-          </View>
+            </View>
+          </View>
React Native 0.81 accessibilityRole radiogroup radio accessibilityState checked

Comment on lines +1 to +5
rm public/database.sqlite
clear
echo "file removed"
sleep 2
touch public/database.sqlite
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the script declares an interpreter
fd -a '^deleting_migration_deleting_android\.sh$' -x head -n 5 {}

Repository: TharunCodes07/SkillSpark

Length of output: 156


Add a shebang so the script is directly executable.

Without a shebang, running this script via ./deleting_migration_deleting_android.sh will fail and Shellcheck flags it as SC2148. Add #!/usr/bin/env bash at the top.

🔧 Proposed fix
+#!/usr/bin/env bash
+
 rm public/database.sqlite
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rm public/database.sqlite
clear
echo "file removed"
sleep 2
touch public/database.sqlite
#!/usr/bin/env bash
rm public/database.sqlite
clear
echo "file removed"
sleep 2
touch public/database.sqlite
🧰 Tools
🪛 Shellcheck (0.11.0)

[error] 1-1: Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.

(SC2148)

🤖 Prompt for AI Agents
In `@deleting_migration_deleting_android.sh` around lines 1 - 5, Add a bash
shebang to the top of the script so it can be executed directly: insert
"#!/usr/bin/env bash" as the very first line of
deleting_migration_deleting_android.sh (above the existing
rm/clear/echo/sleep/touch commands) to satisfy Shellcheck SC2148 and ensure the
script runs with bash when invoked as ./deleting_migration_deleting_android.sh.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/topic/TopicVideoGenerator.tsx (1)

241-260: Status message is hardcoded and misleading.

Line 246 displays "Generating 2-second script..." but the video length is now configurable (10s to 5m). The message should reflect the user's actual selection.

Consider storing the selected length when generation starts and using it in the status message:

+  const [generatingLength, setGeneratingLength] = useState<number | null>(null);

Then in handleGenerateVideo:

+      setGeneratingLength(lengthInSeconds);
       setVideoStatus('generating-script');

And update the message:

       case 'generating-script':
-        return 'Generating 2-second script...';
+        return `Generating ${generatingLength}s script...`;
🤖 Fix all issues with AI agents
In `@components/topic/TopicVideoGenerator.tsx`:
- Around line 390-493: The current implementation places
accessibilityRole="radiogroup" on each row View, creating three separate radio
groups; change to a single radiogroup by moving accessibilityRole="radiogroup"
to the outer container that wraps all Pressable options (the Views containing
the Pressable items) and remove accessibilityRole="radiogroup" from the inner
row Views; keep each Pressable's accessibilityRole="radio", accessibilityState
using selectedLength, and onPress handlers (setSelectedLength) so all nine
options remain mutually exclusive but are announced as one coherent radio group.

In `@server/queries/topics.ts`:
- Around line 162-221: The UNIQUE-constraint retry never triggers in
insertSubtopicWithRetry because the error check looks for camelCase
"parentTopicId" while SQLite reports snake_case column names; update the
isOrderConflict detection in that catch block (within insertSubtopicWithRetry)
to match the actual SQLite message—e.g., normalize errorMessage to lowercase and
check for 'unique constraint failed' plus both 'subtopics.order' and the
snake_case 'subtopics.parent_topic_id' (or use a regex like /parent[_ ]topic[_
]id/), or alternatively detect by SQLite error code if available from tx—so
order conflicts are correctly recognized and retried.
♻️ Duplicate comments (2)
deleting_migration_deleting_android.sh (1)

1-1: Add a bash shebang so the script runs via ./....

Without a shebang, direct execution can fail or default to /bin/sh.

🔧 Proposed fix
+#!/usr/bin/env bash
+
db/schema.ts (1)

107-110: Plan a cleanup before applying the new unique indexes.

If duplicates already exist for (parentTopicId, name) or (parentTopicId, order), the migration will fail. A pre-migration dedupe/backfill is needed.

🧹 Nitpick comments (3)
components/topic/TopicVideoGenerator.tsx (1)

10-11: Remove unused imports.

Label and DialogFooter are imported but never used in this file.

-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
-import { Label } from '@/components/ui/label';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
deleting_migration_deleting_android.sh (1)

1-9: Make the script fail fast and avoid noisy deletes.

Consider set -euo pipefail (after the shebang) and rm -f so missing files don’t break once -e is enabled.

🔧 Proposed fix
+set -euo pipefail
-rm public/database.sqlite
+rm -f public/database.sqlite
db/schema.ts (1)

107-109: Unique index already covers (parentTopicId, order).

The new unique index provides the same lookup path as the non‑unique index, so keeping both adds write overhead. Consider removing the redundant index unless you need it for a different collation or partial index.

♻️ Proposed change
-    index("subtopics_order_idx").on(t.parentTopicId, t.order),

Comment on lines +390 to +493
<View className="flex-row gap-3" accessibilityRole="radiogroup">
<Pressable
onPress={() => setSelectedLength(10)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 10 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 10 }}
accessibilityLabel="10 seconds, ultra short"
>
<Text className={`font-semibold text-center ${selectedLength === 10 ? 'text-primary' : 'text-foreground'}`}>10s</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Ultra short</Text>
</Pressable>

<Pressable
onPress={() => setSelectedLength(30)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 30 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 30 }}
accessibilityLabel="30 seconds, quick"
>
<Text className={`font-semibold text-center ${selectedLength === 30 ? 'text-primary' : 'text-foreground'}`}>30s</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Quick</Text>
</Pressable>

<Pressable
onPress={() => setSelectedLength(45)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 45 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 45 }}
accessibilityLabel="45 seconds, brief"
>
<Text className={`font-semibold text-center ${selectedLength === 45 ? 'text-primary' : 'text-foreground'}`}>45s</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Brief</Text>
</Pressable>
</View>

<View className="flex-row gap-3" accessibilityRole="radiogroup">
<Pressable
onPress={() => setSelectedLength(60)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 60 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 60 }}
accessibilityLabel="1 minute, short"
>
<Text className={`font-semibold text-center ${selectedLength === 60 ? 'text-primary' : 'text-foreground'}`}>1m</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Short</Text>
</Pressable>

<Pressable
onPress={() => setSelectedLength(90)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 90 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 90 }}
accessibilityLabel="1.5 minutes, concise"
>
<Text className={`font-semibold text-center ${selectedLength === 90 ? 'text-primary' : 'text-foreground'}`}>1.5m</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Concise</Text>
</Pressable>

<Pressable
onPress={() => setSelectedLength(120)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 120 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 120 }}
accessibilityLabel="2 minutes, medium"
>
<Text className={`font-semibold text-center ${selectedLength === 120 ? 'text-primary' : 'text-foreground'}`}>2m</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Medium</Text>
</Pressable>
</View>

<View className="flex-row gap-3" accessibilityRole="radiogroup">
<Pressable
onPress={() => setSelectedLength(180)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 180 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 180 }}
accessibilityLabel="3 minutes, long"
>
<Text className={`font-semibold text-center ${selectedLength === 180 ? 'text-primary' : 'text-foreground'}`}>3m</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Long</Text>
</Pressable>

<Pressable
onPress={() => setSelectedLength(240)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 240 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 240 }}
accessibilityLabel="4 minutes, extended"
>
<Text className={`font-semibold text-center ${selectedLength === 240 ? 'text-primary' : 'text-foreground'}`}>4m</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Extended</Text>
</Pressable>

<Pressable
onPress={() => setSelectedLength(300)}
className={`flex-1 p-3 rounded-lg border-2 ${selectedLength === 300 ? 'border-primary bg-primary/10' : 'border-border bg-card'}`}
accessibilityRole="radio"
accessibilityState={{ checked: selectedLength === 300 }}
accessibilityLabel="5 minutes, full"
>
<Text className={`font-semibold text-center ${selectedLength === 300 ? 'text-primary' : 'text-foreground'}`}>5m</Text>
<Text className="text-xs text-muted-foreground text-center mt-0.5">Full</Text>
</Pressable>
</View>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Multiple radiogroups break screen reader semantics.

Each row has its own accessibilityRole="radiogroup" (lines 390, 425, 460), creating three separate radio groups instead of one. Screen readers will announce these as independent groups, confusing users since all 9 options are mutually exclusive.

Wrap all options in a single radiogroup:

+        <View accessibilityRole="radiogroup" accessibilityLabel="Video length options">
-          <View className="flex-row gap-3" accessibilityRole="radiogroup">
+          <View className="flex-row gap-3">
             {/* ... first row options ... */}
           </View>

-          <View className="flex-row gap-3" accessibilityRole="radiogroup">
+          <View className="flex-row gap-3">
             {/* ... second row options ... */}
           </View>

-          <View className="flex-row gap-3" accessibilityRole="radiogroup">
+          <View className="flex-row gap-3">
             {/* ... third row options ... */}
           </View>
+        </View>
🤖 Prompt for AI Agents
In `@components/topic/TopicVideoGenerator.tsx` around lines 390 - 493, The current
implementation places accessibilityRole="radiogroup" on each row View, creating
three separate radio groups; change to a single radiogroup by moving
accessibilityRole="radiogroup" to the outer container that wraps all Pressable
options (the Views containing the Pressable items) and remove
accessibilityRole="radiogroup" from the inner row Views; keep each Pressable's
accessibilityRole="radio", accessibilityState using selectedLength, and onPress
handlers (setSelectedLength) so all nine options remain mutually exclusive but
are announced as one coherent radio group.

Comment on lines +162 to +221
// Helper function to insert subtopic with atomic order assignment and retry on conflict
const insertSubtopicWithRetry = async (subtopic: typeof newSubtopicsToCreate[0], maxRetries = 5): Promise<void> => {
const subtopicId = createId();

console.log(`🗄️ [DB] Inserting subtopic ${i + 1}/${explanation.subtopics.length}: "${subtopic.title}"`);
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Query max order atomically within each attempt to avoid race conditions
const maxOrderResult = await tx
.select({ maxOrder: max(subtopics.order) })
.from(subtopics)
.where(eq(subtopics.parentTopicId, parentTopicId));

const nextOrder = (maxOrderResult[0]?.maxOrder ?? -1) + 1;

// Attempt insert with the calculated order
await tx.insert(subtopics).values({
id: subtopicId,
parentTopicId: parentTopicId,
name: subtopic.title,
contentDefault: subtopic.explanationDefault,
contentSimplified: subtopic.explanationSimplified,
contentStory: subtopic.explanationStory,
order: nextOrder,
metadata: JSON.stringify({
source: source,
example: subtopic.example,
exampleExplanation: subtopic.exampleExplanation,
exampleSimplified: subtopic.exampleSimplified,
exampleStory: subtopic.exampleStory,
keyPoints: subtopic.keyPoints
})
});

console.log(`✅ [DB] Created subtopic with 3 content types: ${subtopic.title} (order: ${nextOrder}, source: ${source})`);
return; // Success, exit retry loop

} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';

// Check if error is due to unique constraint violation on (parentTopicId, order)
const isOrderConflict = errorMessage.includes('UNIQUE constraint failed') &&
errorMessage.includes('parentTopicId') &&
errorMessage.includes('order');

if (isOrderConflict && attempt < maxRetries - 1) {
// Retry after a brief delay with exponential backoff
const delayMs = Math.pow(2, attempt) * 10; // 10ms, 20ms, 40ms, 80ms, 160ms
console.warn(`⚠️ [DB] Order conflict for "${subtopic.title}", retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
continue;
}

// Either not an order conflict or max retries reached
console.error(`❌ [DB] Failed to create subtopic ${subtopic.title}:`, error);
throw error; // Propagate error to outer catch
}
}

throw new Error(`Failed to insert subtopic "${subtopic.title}" after ${maxRetries} attempts`);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the database schema/migration files
find . -type f \( -name "*.ts" -o -name "*.sql" -o -name "*.js" \) | grep -E "(schema|migration|db)" | head -20

Repository: TharunCodes07/SkillSpark

Length of output: 134


🏁 Script executed:

# Search for subtopics table definition
rg "subtopics" --type ts --type sql -A 5 -B 1 | head -100

Repository: TharunCodes07/SkillSpark

Length of output: 5667


🏁 Script executed:

# Look for drizzle schema definitions
fd -e ts -path "*/schema*" -o -path "*drizzle*"

Repository: TharunCodes07/SkillSpark

Length of output: 236


🏁 Script executed:

cat db/schema.ts

Repository: TharunCodes07/SkillSpark

Length of output: 14355


🏁 Script executed:

rg "UNIQUE constraint failed" server/ -A 3 -B 3

Repository: TharunCodes07/SkillSpark

Length of output: 676


🏁 Script executed:

# Search for other error handling patterns with database operations
rg "error.*message" server/queries/ -A 2 -B 2 | head -50

Repository: TharunCodes07/SkillSpark

Length of output: 1173


🌐 Web query:

SQLite UNIQUE constraint failed error message format column name

💡 Result:

SQLite formats the error text as:

  • Single-column UNIQUE/PK violation:
    UNIQUE constraint failed: <table>.<column> [1]

  • Multi-column (composite) UNIQUE violation:
    UNIQUE constraint failed: <table>.<col1>, <table>.<col2>[, ...] (comma+space separated; table name repeated for each column) [1]


Retry detection never fires due to snake_case column name mismatch in SQLite error message.

SQLite reports UNIQUE constraint violations as UNIQUE constraint failed: subtopics.parent_topic_id, subtopics.order, but the code checks for camelCase parentTopicId. Since all three conditions are joined with &&, the mismatch causes the condition to always be false, preventing retries entirely.

Proposed fix
-          const isOrderConflict = errorMessage.includes('UNIQUE constraint failed') && 
-                                  errorMessage.includes('parentTopicId') && 
-                                  errorMessage.includes('order');
+          const isOrderConflict = errorMessage.includes('UNIQUE constraint failed') && 
+                                  errorMessage.includes('parent_topic_id') && 
+                                  errorMessage.includes('order');
🤖 Prompt for AI Agents
In `@server/queries/topics.ts` around lines 162 - 221, The UNIQUE-constraint retry
never triggers in insertSubtopicWithRetry because the error check looks for
camelCase "parentTopicId" while SQLite reports snake_case column names; update
the isOrderConflict detection in that catch block (within
insertSubtopicWithRetry) to match the actual SQLite message—e.g., normalize
errorMessage to lowercase and check for 'unique constraint failed' plus both
'subtopics.order' and the snake_case 'subtopics.parent_topic_id' (or use a regex
like /parent[_ ]topic[_ ]id/), or alternatively detect by SQLite error code if
available from tx—so order conflicts are correctly recognized and retried.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
db/drizzle.ts (1)

58-60: Remove unused export useMigrationHelper.

This hook is exported from db/drizzle.ts but is not imported or used anywhere in the codebase. The initialize function already handles migrations during startup, making this export unnecessary.

🤖 Fix all issues with AI agents
In @.gitignore:
- Around line 44-46: The .gitignore contains redundant/suspicious entries: the
duplicated "android/app/src/main/java/com/expostarter/base/MainActivity.kt" and
" .github/copilot-instructions.md" after an existing "android/" rule; decide
whether you intended to ignore or unignore these files and fix accordingly: if
you meant to keep them ignored, remove the duplicate and the explicit android
path entries (they're already covered by "android/"); if you meant to unignore
them, prefix each path with "!" (e.g.,
"!android/app/src/main/java/com/expostarter/base/MainActivity.kt" and
"!.github/copilot-instructions.md") and remove the duplicate line; ensure no
duplicate lines remain.
- Around line 41-43: The .gitignore currently ignores the entire android/
directory so the negation for !android/app/src/main/assets/face_landmarker.task
is ineffective; update the ignore rules to unignore the path hierarchy leading
to that file (e.g., add negations for android/app/, android/app/src/,
android/app/src/main/, android/app/src/main/assets/ before the specific file
negation) or instead remove the parent android/ ignore and/or use Git LFS or
build-time download for the binary asset if you don't want it committed.

In `@lib/emotion/detectEmotion.ts`:
- Around line 25-44: The early-return in detectEmotion.ts currently returns
emotion: "no_face" when the FaceLandmarks module is missing; change this
sentinel to a distinct value like "unavailable" (or "error") so callers can
differentiate module-unavailable from a true "no_face" detection, keeping
confidence: 0.0 and the same default features shape; update the return in the
FaceLandmarks null-check (the block referencing FaceLandmarks) and any related
tests or callers that expect "no_face" to instead handle the new "unavailable"
value.
🧹 Nitpick comments (5)
app.config.ts (1)

59-62: Inconsistent baseUrl after rebranding.

The app has been renamed to "SkillSpark" throughout the configuration, but experiments.baseUrl still references /expo-local-first-template. If this is used for web builds or routing, it should likely be updated to match the new project identity.

   experiments: {
     typedRoutes: true,
-    baseUrl: "/expo-local-first-template",
+    baseUrl: "/skillspark",
   },
package.json (2)

13-15: build:apk is an exact duplicate of build:android.

Both scripts execute identical commands. Consider removing one or differentiating them (e.g., different EAS profiles).

     "build:android": "bun run prebuild:copy-assets && eas build --platform android --profile production",
     "build:android-local": "bun run prebuild:copy-assets && cd android && ./gradlew assembleRelease",
-    "build:apk": "bun run prebuild:copy-assets && eas build --platform android --profile production",

12-12: prebuild:copy-assets is not cross-platform.

The script uses mkdir -p and cp, which are Unix commands and will fail on Windows. If Windows support is needed, consider using a cross-platform approach:

-    "prebuild:copy-assets": "mkdir -p android/app/src/main/assets && cp face_landmarker.task android/app/src/main/assets/",
+    "prebuild:copy-assets": "node -e \"const fs=require('fs');const p='android/app/src/main/assets';fs.mkdirSync(p,{recursive:true});fs.copyFileSync('face_landmarker.task',p+'/face_landmarker.task')\"",

Alternatively, use a tool like shx or cpy-cli, or document that builds require a Unix environment.

db/migrations/meta/0000_snapshot.json (1)

814-837: Redundant indexes on subtopics table.

There are two indexes on the same columns (parent_topic_id, order):

  • subtopics_order_idx (non-unique, lines 814-821)
  • subtopics_parent_order_idx (unique, lines 830-837)

The unique index already provides the same lookup capability as the non-unique one. Having both wastes storage and slows down writes.

Consider removing subtopics_order_idx from the schema definition, as subtopics_parent_order_idx already covers those queries while also enforcing uniqueness.

db/migrations/0000_striped_impossible_man.sql (1)

115-118: Redundant index: subtopics_order_idx duplicates subtopics_parent_order_idx.

Both indexes cover (parent_topic_id, order). The unique index at line 118 already provides the same query optimization as the non-unique index at line 116, while also enforcing uniqueness.

Remove subtopics_order_idx to reduce storage overhead and improve write performance.

♻️ Suggested fix
 CREATE INDEX `subtopics_parent_idx` ON `subtopics` (`parent_topic_id`);--> statement-breakpoint
-CREATE INDEX `subtopics_order_idx` ON `subtopics` (`parent_topic_id`,`order`);--> statement-breakpoint
 CREATE UNIQUE INDEX `subtopics_parent_name_idx` ON `subtopics` (`parent_topic_id`,`name`);--> statement-breakpoint
 CREATE UNIQUE INDEX `subtopics_parent_order_idx` ON `subtopics` (`parent_topic_id`,`order`);--> statement-breakpoint

Comment on lines 41 to +43
android/
# But keep the model file for emotion detection
!android/app/src/main/assets/face_landmarker.task
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Negation pattern is ineffective after ignoring parent directory.

Git ignores the entire android/ directory on line 41, so the negation on line 43 won't work—Git doesn't check children of already-ignored directories. To track a specific file inside an ignored directory, you must unignore the entire path hierarchy:

 # Ignore the entire Android directory
 android/
 # But keep the model file for emotion detection
-!android/app/src/main/assets/face_landmarker.task
+!android/app/
+!android/app/src/
+!android/app/src/main/
+!android/app/src/main/assets/
+!android/app/src/main/assets/face_landmarker.task

Alternatively, consider whether committing this binary asset to the repo is the right approach vs. downloading it during build or using Git LFS.

🤖 Prompt for AI Agents
In @.gitignore around lines 41 - 43, The .gitignore currently ignores the entire
android/ directory so the negation for
!android/app/src/main/assets/face_landmarker.task is ineffective; update the
ignore rules to unignore the path hierarchy leading to that file (e.g., add
negations for android/app/, android/app/src/, android/app/src/main/,
android/app/src/main/assets/ before the specific file negation) or instead
remove the parent android/ ignore and/or use Git LFS or build-time download for
the binary asset if you don't want it committed.

Comment on lines 44 to 46
android/app/src/main/java/com/expostarter/base/MainActivity.kt
android/app/src/main/java/com/expostarter/base/MainActivity.kt
.github/copilot-instructions.md
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Suspicious bare paths without ignore semantics.

Lines 44-46 list paths without a negation (!) or wildcard, yet they appear after an android/ ignore rule. If the intent was to ignore these specific files, they're already covered by android/. If the intent was to unignore them, they're missing the ! prefix. Additionally, line 44 and 45 are duplicates.

Please clarify the intent and remove or fix these entries.

🤖 Prompt for AI Agents
In @.gitignore around lines 44 - 46, The .gitignore contains
redundant/suspicious entries: the duplicated
"android/app/src/main/java/com/expostarter/base/MainActivity.kt" and "
.github/copilot-instructions.md" after an existing "android/" rule; decide
whether you intended to ignore or unignore these files and fix accordingly: if
you meant to keep them ignored, remove the duplicate and the explicit android
path entries (they're already covered by "android/"); if you meant to unignore
them, prefix each path with "!" (e.g.,
"!android/app/src/main/java/com/expostarter/base/MainActivity.kt" and
"!.github/copilot-instructions.md") and remove the duplicate line; ensure no
duplicate lines remain.

Comment on lines +25 to +44
// Check if module is available
if (!FaceLandmarks) {
return {
emotion: "no_face",
confidence: 0.0,
features: {
ear: 0,
mar: 0,
brow_height: 0,
brow_asymmetry: 0,
tilt_angle: 0,
rotation_ratio: 0,
symmetry_ratio: 0,
energy_concentration: 0,
jaw_width: 0,
brow_variance: 0,
looking_at_camera: false,
},
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Differentiate “module unavailable” from real “no face”

Returning "no_face" when the module is missing can mask integration issues and mislead downstream UI/analytics. Consider a distinct value (e.g., "error" or "unavailable") so callers can handle this separately.

💡 Suggested tweak
-    return {
-      emotion: "no_face",
+    return {
+      emotion: "error",
       confidence: 0.0,
       features: {
         ear: 0,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if module is available
if (!FaceLandmarks) {
return {
emotion: "no_face",
confidence: 0.0,
features: {
ear: 0,
mar: 0,
brow_height: 0,
brow_asymmetry: 0,
tilt_angle: 0,
rotation_ratio: 0,
symmetry_ratio: 0,
energy_concentration: 0,
jaw_width: 0,
brow_variance: 0,
looking_at_camera: false,
},
};
}
// Check if module is available
if (!FaceLandmarks) {
return {
emotion: "error",
confidence: 0.0,
features: {
ear: 0,
mar: 0,
brow_height: 0,
brow_asymmetry: 0,
tilt_angle: 0,
rotation_ratio: 0,
symmetry_ratio: 0,
energy_concentration: 0,
jaw_width: 0,
brow_variance: 0,
looking_at_camera: false,
},
};
}
🤖 Prompt for AI Agents
In `@lib/emotion/detectEmotion.ts` around lines 25 - 44, The early-return in
detectEmotion.ts currently returns emotion: "no_face" when the FaceLandmarks
module is missing; change this sentinel to a distinct value like "unavailable"
(or "error") so callers can differentiate module-unavailable from a true
"no_face" detection, keeping confidence: 0.0 and the same default features
shape; update the return in the FaceLandmarks null-check (the block referencing
FaceLandmarks) and any related tests or callers that expect "no_face" to instead
handle the new "unavailable" value.

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