Skip to content

fix: MediaRecorder crash on rapid re-record + add sample sort_order and reorder API#660

Open
neuron-tech-ai wants to merge 2 commits into
jamiepine:mainfrom
neuron-tech-ai:fix/multi-sample-recording
Open

fix: MediaRecorder crash on rapid re-record + add sample sort_order and reorder API#660
neuron-tech-ai wants to merge 2 commits into
jamiepine:mainfrom
neuron-tech-ai:fix/multi-sample-recording

Conversation

@neuron-tech-ai
Copy link
Copy Markdown

@neuron-tech-ai neuron-tech-ai commented May 14, 2026

Bug fix: Rapidly stopping and restarting MediaRecorder (e.g., recording a second sample immediately after stopping the first) throws an "already recording" InvalidStateError. Added proper state checks before start/stop calls so rapid re-record works reliably.

Feature addition: Adds sort_order to profile samples and a PUT /profiles/{id}/samples/reorder endpoint. Users expect to be able to drag-and-drop reorder their voice samples; without server-side order persistence, the frontend can't save that state.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added sample reordering functionality for organizing audio recordings.
  • Documentation

    • Updated audio sample guidance across all supported languages, clarifying that adding multiple diverse recordings improves voice quality through automatic merging.
  • Improvements

    • Enhanced error handling with clearer messages for file uploads and validation errors.
    • Database performance optimizations for improved responsiveness and faster data operations.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This PR introduces sample ordering functionality with a new sort_order field and reorder endpoint, stabilizes audio recording against race conditions during asynchronous conversion, standardizes database timestamps to UTC-aware defaults, adds performance indexes across multiple tables, improves error handling through exception chaining, and updates localization copy encouraging multiple sample uploads.

Changes

Sample Ordering and Core Improvements

Layer / File(s) Summary
Sample Sort Order Data Model and Schema
app/src/lib/api/models/ProfileSampleResponse.ts, app/src/lib/api/schemas/$ProfileSampleResponse.ts, backend/models.py, backend/database/models.py
ProfileSampleResponse gains sort_order: number, SampleReorderRequest model introduced with sample_ids: list[str], and ProfileSample table indexed on profile_id and sort_order.
Sample Reordering Endpoint and Service Logic
backend/routes/profiles.py, backend/services/profiles.py
New PATCH /profiles/{profile_id}/samples/reorder endpoint updates sample ordering via database commit and cache clearing. Service layer assigns deterministic sort_order (max + 1) when adding samples and retrieves ordered samples for cloned profile prompts.
Audio Recording Session Stability
app/src/lib/hooks/useAudioRecording.ts
Introduces monotonically increasing sessionRef counter to detect stale recording sessions during async WAV conversion. startRecording increments counter, onstop captures session ID and skips completion if newer session started, and cancelRecording immediately clears recorder reference.
Database Timestamp Standardization and Performance Indexes
backend/database/models.py
Switches all timestamp defaults from datetime.utcnow to datetime.now(UTC) across VoiceProfile, Story, Project, GenerationVersion, EffectPreset, AudioChannel, CaptureSettings, GenerationSettings, MCPClientBinding, and Capture. Adds index=True to ProfileSample.sort_order, Generation.profile_id, Generation.status, Generation.is_favorited, Generation.created_at, StoryItem.story_id, GenerationVersion.generation_id, and Capture.created_at.
Error Handling and Upload Robustness
backend/routes/profiles.py
Adds exception chaining with from e across profile creation, updates, sample operations, exports, and channel assignments. Refactors profile import and avatar uploads to chunked streaming reads with explicit size caps (HTTP 413 on overflow) and includes exception details in error responses.
Service Layer Timestamp and Profile Updates
backend/services/profiles.py
Updates profile service timestamps to datetime.now(UTC) for creation, updates, avatar operations, and effect modifications. Simplifies name lookup query, assigns deterministic sample sort_order, and retrieves samples ordered by sort_order for cloned profiles.
Migration Helpers for Schema and Performance
backend/database/migrations.py
Adds _migrate_profile_samples() to add sort_order column and index when missing. Adds _add_performance_indexes() to create indexes across multiple tables at startup. Refactors _migrate_story_items() for readability and simplifies SQL execution in _normalize_storage_paths().
Localization Updates for Sample Guidance
app/src/i18n/locales/en/translation.json, app/src/i18n/locales/ja/translation.json, app/src/i18n/locales/zh-CN/translation.json, app/src/i18n/locales/zh-TW/translation.json
Updates sampleList.note copy across all locales to emphasize multiple samples improve quality through diversity and automatic combination, removing prior single-sample recommendations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through ordered samples bright, 🐰
With sessions safe from async racing night,
UTC timestamps stamp the database true,
Indexes bloom where queries once rang slow,
Multiple voices merge—a richer tone in flow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes both main changes: a MediaRecorder crash fix for rapid re-recording and the addition of sample sort_order with a reorder API.
Docstring Coverage ✅ Passed Docstring coverage is 83.87% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

… to models.py

The /profiles/{profile_id}/samples/reorder endpoint referenced
models.SampleReorderRequest which was never defined, causing an
AttributeError at import time.  ProfileSampleResponse was also missing
the sort_order field added to the DB model, so serialization would fail.

Add both to backend/models.py.
@neuron-tech-ai
Copy link
Copy Markdown
Author

Fixed two bugs found during review:

  1. Missing SampleReorderRequest model: The new PATCH /profiles/{profile_id}/samples/reorder endpoint referenced models.SampleReorderRequest, but this class was never added to backend/models.py. This would cause an AttributeError at import time, crashing the server on startup. Added SampleReorderRequest with a sample_ids: list[str] field.

  2. Missing sort_order field in ProfileSampleResponse: The DB model gained a sort_order column and the TypeScript client types were updated, but the Python ProfileSampleResponse Pydantic model was never updated to include it. Serializing samples would either omit the field or raise a validation error depending on Pydantic mode. Added sort_order: int = 0 to ProfileSampleResponse.

Fixed in commit bf0ad3e.

Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (3)
backend/routes/profiles.py (1)

53-53: 💤 Low value

Follow Python naming convention for constants.

The variable CHUNK_SIZE is a module-level constant and correctly uses uppercase. However, the static analysis tool flagged it because it's defined inside a function scope. Consider moving it to module level alongside MAX_FILE_SIZE (line 52) for consistency.

📝 Proposed refactoring
+IMPORT_MAX_FILE_SIZE = 100 * 1024 * 1024  # 100 MB
+IMPORT_CHUNK_SIZE = 1024 * 1024  # 1 MB
+
 `@router.post`("/profiles/import", response_model=models.VoiceProfileResponse)
 async def import_profile(
     file: UploadFile = File(...),
     db: Session = Depends(get_db),
 ):
     """Import a voice profile from a ZIP archive."""
-    MAX_FILE_SIZE = 100 * 1024 * 1024
-    CHUNK_SIZE = 1024 * 1024  # 1 MB
 
     # Stream-read with an early size cap so oversized uploads are rejected
     # before the entire payload is buffered into memory.
     chunks: list[bytes] = []
     total = 0
-    while chunk := await file.read(CHUNK_SIZE):
+    while chunk := await file.read(IMPORT_CHUNK_SIZE):
         total += len(chunk)
-        if total > MAX_FILE_SIZE:
+        if total > IMPORT_MAX_FILE_SIZE:
             raise HTTPException(
                 status_code=413,
-                detail=f"File too large. Maximum size is {MAX_FILE_SIZE // (1024 * 1024)} MB.",
+                detail=f"File too large. Maximum size is {IMPORT_MAX_FILE_SIZE // (1024 * 1024)} MB.",
             )
         chunks.append(chunk)
     content = b"".join(chunks)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/routes/profiles.py` at line 53, CHUNK_SIZE is currently defined
inside a function but should be a true module-level constant like MAX_FILE_SIZE;
move the CHUNK_SIZE declaration out of the function and place it at module scope
alongside MAX_FILE_SIZE, keep the name CHUNK_SIZE and value 1024 * 1024, and
update any local references inside the function (if any) to use the module-level
CHUNK_SIZE constant.
backend/models.py (1)

80-84: 💤 Low value

Consider adding validation to sample_ids.

The sample_ids field accepts any list, including an empty list. While the reorder endpoint may handle this gracefully, adding Field(..., min_length=1) would make the API contract more explicit and provide better error messages at validation time.

📝 Proposed validation enhancement
 class SampleReorderRequest(BaseModel):
     """Request model for reordering profile samples."""
 
-    sample_ids: list[str]
+    sample_ids: list[str] = Field(..., min_length=1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/models.py` around lines 80 - 84, Add validation to
SampleReorderRequest.sample_ids by declaring it with a Pydantic Field that
enforces at least one element; import Field from pydantic if not already present
and change the declaration to use Field(..., min_length=1) on the sample_ids
attribute of the SampleReorderRequest model so empty lists are rejected with a
clear validation error.
backend/services/profiles.py (1)

233-251: ⚡ Quick win

Remove redundant import.

The code imports func as _func at line 236, but func is already imported at line 9. Use the existing import instead.

♻️ Proposed fix
     # Assign sort_order = max(existing) + 1 so each new sample appends at
     # the end rather than landing at 0 (which would make all samples tie for
     # the top position and produce non-deterministic ordering).
-    from sqlalchemy import func as _func
-
-    max_order = db.query(_func.max(DBProfileSample.sort_order)).filter_by(profile_id=profile_id).scalar()
+    max_order = db.query(func.max(DBProfileSample.sort_order)).filter_by(profile_id=profile_id).scalar()
     next_order = (max_order or 0) + 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/profiles.py` around lines 233 - 251, Remove the redundant
"from sqlalchemy import func as _func" import and update the max_order
computation to use the existing func symbol already imported earlier;
specifically, delete the _func import and change the call in the query that
references _func.max(DBProfileSample.sort_order) to use
func.max(DBProfileSample.sort_order) so DBProfileSample, next_order and
profile.updated_at logic remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/lib/hooks/useAudioRecording.ts`:
- Line 35: The session id (sessionRef.current) must be captured into a
session-local variable before any awaited/async setup so concurrent
startRecording calls don't share the later session id; inside startRecording()
increment sessionRef.current, then immediately assign const thisSession =
sessionRef.current and use thisSession in the dataavailable event handler and
any later session checks (instead of reading sessionRef.current after await).
Apply the same pattern where the code currently reads sessionRef.current around
the async path (the block referenced by lines ~97-101) so each recorder closure
validates against its own captured thisSession to avoid leaking old
dataavailable events into new recordings.

In `@backend/database/migrations.py`:
- Around line 307-313: The index creation for ix_profile_samples_sort_order is
currently inside the "if 'sort_order' not in columns" block so a
partially-applied migration (column added but index missing) will never
self-heal; either move the CREATE INDEX IF NOT EXISTS call for
profile_samples(sort_order) out of that conditional so it runs on every startup
after calling _add_column, or add ("profile_samples",
"ix_profile_samples_sort_order", "sort_order") to the indexes list handled by
_add_performance_indexes so the helper ensures the index exists independently of
the column-add conditional; keep the existing use of IF NOT EXISTS and reference
_add_column, _add_performance_indexes, profile_samples, sort_order, and
ix_profile_samples_sort_order when making the change.

In `@backend/routes/profiles.py`:
- Around line 238-253: reorder_samples currently performs blind updates; add
validation and error handling in the reorder_samples function: fetch all
DBProfileSample rows for the given profile_id first (query DBProfileSample where
profile_id == profile_id) and compare their ids to data.sample_ids to detect
missing ids, extra ids (ownership violations), and duplicates (use counts or set
length mismatch); if any sample_id in data.sample_ids does not belong to this
profile or if there are duplicates, raise an HTTPException with a 400/404 and a
clear message; ensure data.sample_ids includes every existing sample id for the
profile (otherwise return 400 asking the client to supply a complete ordering)
or alternatively compute a stable reassign for omitted samples; perform the
updates inside a transaction and verify each update affected exactly one row (or
update via explicit loaded objects and assign sort_order then commit), and only
call clear_profile_cache(profile_id) after successful commit.

---

Nitpick comments:
In `@backend/models.py`:
- Around line 80-84: Add validation to SampleReorderRequest.sample_ids by
declaring it with a Pydantic Field that enforces at least one element; import
Field from pydantic if not already present and change the declaration to use
Field(..., min_length=1) on the sample_ids attribute of the SampleReorderRequest
model so empty lists are rejected with a clear validation error.

In `@backend/routes/profiles.py`:
- Line 53: CHUNK_SIZE is currently defined inside a function but should be a
true module-level constant like MAX_FILE_SIZE; move the CHUNK_SIZE declaration
out of the function and place it at module scope alongside MAX_FILE_SIZE, keep
the name CHUNK_SIZE and value 1024 * 1024, and update any local references
inside the function (if any) to use the module-level CHUNK_SIZE constant.

In `@backend/services/profiles.py`:
- Around line 233-251: Remove the redundant "from sqlalchemy import func as
_func" import and update the max_order computation to use the existing func
symbol already imported earlier; specifically, delete the _func import and
change the call in the query that references
_func.max(DBProfileSample.sort_order) to use
func.max(DBProfileSample.sort_order) so DBProfileSample, next_order and
profile.updated_at logic remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3befb537-2cc3-433b-b53e-45c18fa3d77e

📥 Commits

Reviewing files that changed from the base of the PR and between b35b909 and bf0ad3e.

📒 Files selected for processing (12)
  • app/src/i18n/locales/en/translation.json
  • app/src/i18n/locales/ja/translation.json
  • app/src/i18n/locales/zh-CN/translation.json
  • app/src/i18n/locales/zh-TW/translation.json
  • app/src/lib/api/models/ProfileSampleResponse.ts
  • app/src/lib/api/schemas/$ProfileSampleResponse.ts
  • app/src/lib/hooks/useAudioRecording.ts
  • backend/database/migrations.py
  • backend/database/models.py
  • backend/models.py
  • backend/routes/profiles.py
  • backend/services/profiles.py

setError(null);
chunksRef.current = [];
cancelledRef.current = false;
sessionRef.current += 1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Capture session-local state before the async start path.

Because thisSession is only read on Line 101, after the awaited setup path, two overlapping startRecording() calls can both inherit the later session id. The older recorder still appends into the shared refs, so its final dataavailable can leak into the next recording and the stale-session guard won't reliably suppress it.

Suggested fix
-      sessionRef.current += 1;
+      const thisSession = sessionRef.current + 1;
+      sessionRef.current = thisSession;
+      const sessionChunks: Blob[] = [];
+      let recordingStartedAt: number | null = null;
...
       const stream = await navigator.mediaDevices.getUserMedia({
         audio: {
           echoCancellation: true,
           noiseSuppression: true,
           autoGainControl: true,
         },
       });
+
+      if (sessionRef.current !== thisSession) {
+        stream.getTracks().forEach((track) => track.stop());
+        return;
+      }

       streamRef.current = stream;
...
       mediaRecorder.ondataavailable = (event) => {
         if (event.data.size > 0) {
-          chunksRef.current.push(event.data);
+          sessionChunks.push(event.data);
         }
       };
-
-      const thisSession = sessionRef.current;
...
         const recordedDuration = startTimeRef.current
-          ? (Date.now() - startTimeRef.current) / 1000
+          ? (Date.now() - recordingStartedAt!) / 1000
           : undefined;

-        const webmBlob = new Blob(chunksRef.current, { type: 'audio/webm' });
+        const webmBlob = new Blob(sessionChunks, { type: 'audio/webm' });
...
-      startTimeRef.current = Date.now();
+      recordingStartedAt = Date.now();
+      startTimeRef.current = recordingStartedAt;

Also applies to: 97-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/hooks/useAudioRecording.ts` at line 35, The session id
(sessionRef.current) must be captured into a session-local variable before any
awaited/async setup so concurrent startRecording calls don't share the later
session id; inside startRecording() increment sessionRef.current, then
immediately assign const thisSession = sessionRef.current and use thisSession in
the dataavailable event handler and any later session checks (instead of reading
sessionRef.current after await). Apply the same pattern where the code currently
reads sessionRef.current around the async path (the block referenced by lines
~97-101) so each recorder closure validates against its own captured thisSession
to avoid leaking old dataavailable events into new recordings.

Comment on lines +307 to +313
if "sort_order" not in columns:
_add_column(engine, "profile_samples", "sort_order INTEGER NOT NULL DEFAULT 0", "sort_order")
with engine.connect() as conn:
conn.execute(
text("CREATE INDEX IF NOT EXISTS ix_profile_samples_sort_order ON profile_samples (sort_order)")
)
conn.commit()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Index creation gated on column absence — won't self-heal a partial migration.

_add_column commits in its own transaction (line 62), so if the column add succeeds but the subsequent CREATE INDEX fails (or the DB is later restored from a snapshot taken in between, or the index is dropped manually), the next startup will see sort_order already present, skip the whole block, and the ix_profile_samples_sort_order index will never be recreated. Note _add_performance_indexes also does not include this particular index, so there's no second-chance path.

Since CREATE INDEX IF NOT EXISTS is idempotent, lift it out of the if (or add it to the _add_performance_indexes list) so every startup ensures the index exists.

🛡️ Proposed fix
 def _migrate_profile_samples(engine, inspector, tables: set[str]) -> None:
     if "profile_samples" not in tables:
         return
     columns = _get_columns(inspector, "profile_samples")
     if "sort_order" not in columns:
         _add_column(engine, "profile_samples", "sort_order INTEGER NOT NULL DEFAULT 0", "sort_order")
-        with engine.connect() as conn:
-            conn.execute(
-                text("CREATE INDEX IF NOT EXISTS ix_profile_samples_sort_order ON profile_samples (sort_order)")
-            )
-            conn.commit()
+    with engine.connect() as conn:
+        conn.execute(
+            text("CREATE INDEX IF NOT EXISTS ix_profile_samples_sort_order ON profile_samples (sort_order)")
+        )
+        conn.commit()

Alternatively, append ("profile_samples", "ix_profile_samples_sort_order", "sort_order") to the indexes list in _add_performance_indexes and let that helper own it.

📝 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
if "sort_order" not in columns:
_add_column(engine, "profile_samples", "sort_order INTEGER NOT NULL DEFAULT 0", "sort_order")
with engine.connect() as conn:
conn.execute(
text("CREATE INDEX IF NOT EXISTS ix_profile_samples_sort_order ON profile_samples (sort_order)")
)
conn.commit()
if "sort_order" not in columns:
_add_column(engine, "profile_samples", "sort_order INTEGER NOT NULL DEFAULT 0", "sort_order")
with engine.connect() as conn:
conn.execute(
text("CREATE INDEX IF NOT EXISTS ix_profile_samples_sort_order ON profile_samples (sort_order)")
)
conn.commit()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/database/migrations.py` around lines 307 - 313, The index creation
for ix_profile_samples_sort_order is currently inside the "if 'sort_order' not
in columns" block so a partially-applied migration (column added but index
missing) will never self-heal; either move the CREATE INDEX IF NOT EXISTS call
for profile_samples(sort_order) out of that conditional so it runs on every
startup after calling _add_column, or add ("profile_samples",
"ix_profile_samples_sort_order", "sort_order") to the indexes list handled by
_add_performance_indexes so the helper ensures the index exists independently of
the column-add conditional; keep the existing use of IF NOT EXISTS and reference
_add_column, _add_performance_indexes, profile_samples, sort_order, and
ix_profile_samples_sort_order when making the change.

Comment on lines +238 to +253
@router.patch("/profiles/{profile_id}/samples/reorder")
async def reorder_samples(
profile_id: str,
data: models.SampleReorderRequest,
db: Session = Depends(get_db),
):
"""Reorder voice profile samples. Pass sample_ids in desired order."""
for idx, sample_id in enumerate(data.sample_ids):
db.query(DBProfileSample).filter(
DBProfileSample.id == sample_id,
DBProfileSample.profile_id == profile_id,
).update({"sort_order": idx})
db.commit()
clear_profile_cache(profile_id)
return {"ok": True}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add validation and error handling to the reorder endpoint.

The current implementation has several issues:

  1. No validation of ownership: If sample_ids contains samples from other profiles, the profile_id filter prevents updating them, but no error is raised to inform the caller.
  2. Silent partial updates: If a sample_id doesn't exist, the update silently succeeds with 0 rows affected.
  3. Incomplete reordering: If sample_ids doesn't include all samples for the profile, those omitted samples retain their old sort_order, potentially causing ordering conflicts.
  4. No duplicate detection: If sample_ids contains duplicates, the last occurrence determines the final sort_order.
🔧 Proposed fix with validation
 `@router.patch`("/profiles/{profile_id}/samples/reorder")
 async def reorder_samples(
     profile_id: str,
     data: models.SampleReorderRequest,
     db: Session = Depends(get_db),
 ):
     """Reorder voice profile samples. Pass sample_ids in desired order."""
+    # Verify profile exists
+    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()
+    if not profile:
+        raise HTTPException(status_code=404, detail="Profile not found")
+    
+    # Check for duplicates
+    if len(data.sample_ids) != len(set(data.sample_ids)):
+        raise HTTPException(status_code=400, detail="Duplicate sample IDs in request")
+    
+    # Verify all samples exist and belong to this profile
+    existing_samples = db.query(DBProfileSample).filter(
+        DBProfileSample.id.in_(data.sample_ids),
+        DBProfileSample.profile_id == profile_id,
+    ).all()
+    
+    if len(existing_samples) != len(data.sample_ids):
+        raise HTTPException(status_code=400, detail="One or more sample IDs not found or don't belong to this profile")
+    
     for idx, sample_id in enumerate(data.sample_ids):
         db.query(DBProfileSample).filter(
             DBProfileSample.id == sample_id,
             DBProfileSample.profile_id == profile_id,
         ).update({"sort_order": idx})
     db.commit()
     clear_profile_cache(profile_id)
     return {"ok": True}
📝 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
@router.patch("/profiles/{profile_id}/samples/reorder")
async def reorder_samples(
profile_id: str,
data: models.SampleReorderRequest,
db: Session = Depends(get_db),
):
"""Reorder voice profile samples. Pass sample_ids in desired order."""
for idx, sample_id in enumerate(data.sample_ids):
db.query(DBProfileSample).filter(
DBProfileSample.id == sample_id,
DBProfileSample.profile_id == profile_id,
).update({"sort_order": idx})
db.commit()
clear_profile_cache(profile_id)
return {"ok": True}
`@router.patch`("/profiles/{profile_id}/samples/reorder")
async def reorder_samples(
profile_id: str,
data: models.SampleReorderRequest,
db: Session = Depends(get_db),
):
"""Reorder voice profile samples. Pass sample_ids in desired order."""
# Verify profile exists
profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
# Check for duplicates
if len(data.sample_ids) != len(set(data.sample_ids)):
raise HTTPException(status_code=400, detail="Duplicate sample IDs in request")
# Verify all samples exist and belong to this profile
existing_samples = db.query(DBProfileSample).filter(
DBProfileSample.id.in_(data.sample_ids),
DBProfileSample.profile_id == profile_id,
).all()
if len(existing_samples) != len(data.sample_ids):
raise HTTPException(status_code=400, detail="One or more sample IDs not found or don't belong to this profile")
for idx, sample_id in enumerate(data.sample_ids):
db.query(DBProfileSample).filter(
DBProfileSample.id == sample_id,
DBProfileSample.profile_id == profile_id,
).update({"sort_order": idx})
db.commit()
clear_profile_cache(profile_id)
return {"ok": True}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/routes/profiles.py` around lines 238 - 253, reorder_samples currently
performs blind updates; add validation and error handling in the reorder_samples
function: fetch all DBProfileSample rows for the given profile_id first (query
DBProfileSample where profile_id == profile_id) and compare their ids to
data.sample_ids to detect missing ids, extra ids (ownership violations), and
duplicates (use counts or set length mismatch); if any sample_id in
data.sample_ids does not belong to this profile or if there are duplicates,
raise an HTTPException with a 400/404 and a clear message; ensure
data.sample_ids includes every existing sample id for the profile (otherwise
return 400 asking the client to supply a complete ordering) or alternatively
compute a stable reassign for omitted samples; perform the updates inside a
transaction and verify each update affected exactly one row (or update via
explicit loaded objects and assign sort_order then commit), and only call
clear_profile_cache(profile_id) after successful commit.

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