feat(audio): add rename-title endpoint and inline edit UI#24
Conversation
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (7)
WalkthroughThis PR adds audio entry rename/edit functionality and temporary audio storage. The backend introduces a filesystem-based temp audio store with UUID tokens, a ChangesAudio Rename and Temp Storage
Sequence Diagram(s)sequenceDiagram
participant Client
participant Router
participant Service
participant Filesystem
Client->>Router: GET /audio/temp/{token}
Router->>Service: retrieve_temp_audio(token)
Service->>Filesystem: check file mtime for expiry
alt File exists and not expired
Filesystem-->>Service: return audio bytes
Service-->>Router: audio bytes
Router-->>Client: audio/mpeg response
else File missing or expired
Filesystem-->>Service: None
Service-->>Router: None
Router-->>Client: HTTP 404 (Audio not found or expired)
end
sequenceDiagram
participant Client
participant Router
participant Service
participant Database
participant Storage
Client->>Router: PATCH /audio/{entry_id} with AudioEntryPatch
Router->>Service: rename_audio_entry(entry_id, user_id, title)
Service->>Database: update entry title
Service->>Database: fetch updated entry
Service->>Storage: sign URL for entry filename
Storage-->>Service: signed URL
Service-->>Router: AudioEntryOut with new title and signed URL
Router-->>Client: AudioEntryOut (HTTP 200)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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 `@backend/app/features/audio/router.py`:
- Around line 19-33: The public temp-audio endpoint serve_temp_audio currently
accepts arbitrary strings and has no per-route rate limiting; change the path
param type to UUID (use from uuid import UUID and signature
serve_temp_audio(token: UUID)) so FastAPI validates format before handler runs,
keep calling service.retrieve_temp_audio(token) without awaiting since it's
sync, and add a lightweight per-route rate limit (e.g., a dependency or
rate-limit decorator applied to the router.get("/temp/{token}") route) to
throttle requests and protect the unauthenticated surface.
In `@backend/app/features/audio/service.py`:
- Around line 394-450: The file exceeds the 300-line limit and mixes concerns;
move the rename flow into a smaller focused module (e.g.,
audio/rename_service.py) to split responsibilities: extract the
rename_audio_entry coroutine and any tightly-coupled helpers it needs
(references: rename_audio_entry, _row_to_entry, get_supabase_client,
_SELECT_COLS, _SIGNED_URL_TTL, settings.audio_bucket, logger) into the new
module and update imports in the original service to delegate to it; keep shared
constants or utilities (e.g., _row_to_entry, _SELECT_COLS) either in a new
audio/utils.py or export them from the original module so both pieces can import
them without duplicating logic, and run tests to ensure all references and
imports are updated.
- Around line 117-121: The code is sending raw user text to OpenRouter; call the
mask_pii() function from core/privacy.py on the user text before building the
payload. Import mask_pii, assign masked_text = mask_pii(text) and use
masked_text (not text) for the payload "input" value when constructing the dict
in the function that builds the payload (where settings.tts_model /
settings.tts_voice are used). Ensure you don't overwrite the original text
variable if it's needed elsewhere.
- Around line 67-90: The temp-audio helpers perform blocking filesystem I/O and
must be made async: change store_temp_audio, retrieve_temp_audio, and
delete_temp_audio to async def and move their blocking calls (path.write_bytes,
path.exists, path.stat, path.read_bytes, path.unlink) into asyncio.to_thread (or
an async file lib) and await those calls; preserve return types
(retrieve_temp_audio -> bytes | None returns as before). Also update any async
callers (e.g., the serve_temp_audio endpoint’s call to retrieve_temp_audio) to
await retrieve_temp_audio(token) so the event loop isn’t blocked.
In `@frontend/features/audio/components/AudioCard.tsx`:
- Around line 160-161: The delete confirmation text in AudioCard.tsx currently
interpolates entry.title which can be stale after a rename; update the
confirmation string to use optimisticTitle instead so the modal shows the title
the user currently sees. Locate the JSX that renders the confirmation message
(the string containing "{entry.title}") inside the AudioCard component and
replace entry.title with optimisticTitle, ensuring optimisticTitle is
defined/passed into the component where the confirmation is rendered.
In `@frontend/features/audio/hooks/useAudio.ts`:
- Around line 31-37: The rename mutation uses a global AUDIO_ENTRIES_KEY causing
cross-user cache leaks; update to scope keys by user: add a helper
audioEntriesKey(userId: string) (mirroring useConversations.ts), call
useUserId() inside useRenameAudio to get the current userId, and replace
AUDIO_ENTRIES_KEY with audioEntriesKey(userId) in the useMutation onSuccess
invalidation and any related queries; ensure mutationFn still calls
renameAudioEntry(id, title) but all cache operations use audioEntriesKey(userId)
so cache is isolated per user.
In `@frontend/lib/api.ts`:
- Around line 239-247: Add Zod validation at the API boundary inside
renameAudioEntry: define a Zod schema (e.g., renameAudioSchema) that
validates/normalizes the id (nonempty string, maybe uuid or pattern if required)
and title (string, min/max length, trim) and run zod.parse or zod.safeParse on
the inputs before using them; if validation fails, reject/throw with a clear
error instead of calling apiFetch. After successful parse use the
validated/normalized values (then encodeURIComponent(validated.id)) and pass
validated.title in the JSON body. Also add the necessary zod import and exported
types if needed.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: aa463d1b-f2e8-40bb-91e8-b8915f5e1396
📒 Files selected for processing (7)
backend/app/features/audio/router.pybackend/app/features/audio/schemas.pybackend/app/features/audio/service.pyfrontend/app/(dashboard)/audio/page.tsxfrontend/features/audio/components/AudioCard.tsxfrontend/features/audio/hooks/useAudio.tsfrontend/lib/api.ts
| payload = { | ||
| "model": settings.tts_model, | ||
| "input": masked_text, | ||
| "input": text, | ||
| "voice": settings.tts_voice, | ||
| "response_format": "mp3", |
There was a problem hiding this comment.
Reintroduce PII masking before sending text to OpenRouter.
Line 119 sends raw user text to an external provider. That creates a privacy/compliance gap and regresses existing protections.
Proposed fix
+from app.core.privacy import mask_pii
...
- payload = {
+ masked_text = mask_pii(text)
+ if not masked_text.strip():
+ raise RuntimeError("Cannot generate TTS for empty text after masking.")
+
+ payload = {
"model": settings.tts_model,
- "input": text,
+ "input": masked_text,
"voice": settings.tts_voice,
"response_format": "mp3",
}As per coding guidelines: "NEVER send raw user input to OpenAI embeddings or OpenRouter without first calling mask_pii() from core/privacy.py."
🤖 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/app/features/audio/service.py` around lines 117 - 121, The code is
sending raw user text to OpenRouter; call the mask_pii() function from
core/privacy.py on the user text before building the payload. Import mask_pii,
assign masked_text = mask_pii(text) and use masked_text (not text) for the
payload "input" value when constructing the dict in the function that builds the
payload (where settings.tts_model / settings.tts_voice are used). Ensure you
don't overwrite the original text variable if it's needed elsewhere.
| async def rename_audio_entry( | ||
| entry_id: UUID, user_id: str, title: str | ||
| ) -> AudioEntryOut | None: | ||
| """Rename an audio entry's title. | ||
|
|
||
| Returns the updated AudioEntryOut, or None if the entry was not found. | ||
| Raises RuntimeError on DB or Storage failure. | ||
| """ | ||
| client = await get_supabase_client() | ||
|
|
||
| # UPDATE — postgrest-py's FilterRequestBuilder has no .select() after .eq(), | ||
| # so we do the update and re-fetch in a separate query. | ||
| try: | ||
| await ( | ||
| client.table("audio_entries") | ||
| .update({"title": title}) | ||
| .eq("id", str(entry_id)) | ||
| .eq("user_id", user_id) | ||
| .execute() | ||
| ) | ||
| except Exception: | ||
| logger.exception("[rename_audio_entry] UPDATE failed id=%s", entry_id) | ||
| raise RuntimeError("Failed to rename audio entry.") from None | ||
|
|
||
| # Re-fetch the updated row (also confirms the entry belongs to this user). | ||
| try: | ||
| fetch = ( | ||
| await client.table("audio_entries") | ||
| .select(_SELECT_COLS) | ||
| .eq("id", str(entry_id)) | ||
| .eq("user_id", user_id) | ||
| .execute() | ||
| ) | ||
| except Exception: | ||
| logger.exception( | ||
| "[rename_audio_entry] SELECT after UPDATE failed id=%s", entry_id | ||
| ) | ||
| raise RuntimeError("Failed to fetch renamed audio entry.") from None | ||
|
|
||
| if not fetch.data: | ||
| return None | ||
|
|
||
| row = fetch.data[0] | ||
| filename: str = row["filename"] | ||
| safe_log = filename.split("/")[-1] if "/" in filename else filename | ||
|
|
||
| try: | ||
| result = await client.storage.from_(settings.audio_bucket).create_signed_url( | ||
| filename, _SIGNED_URL_TTL | ||
| ) | ||
| signed_url: str = result["signedURL"] | ||
| except Exception: | ||
| logger.exception("[rename_audio_entry] signed URL failed uuid=%s", safe_log) | ||
| raise RuntimeError("Title updated but failed to generate signed URL.") from None | ||
|
|
||
| logger.info("[rename_audio_entry] renamed uuid=%s", safe_log) | ||
| return _row_to_entry(row, signed_url) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Split this service module; it now exceeds repo size limits.
Adding rename flow here pushes backend/app/features/audio/service.py well past the 300-line cap and further mixes responsibilities (TTS, temp storage, CRUD, signing).
As per coding guidelines: "No file longer than 300 lines — split into smaller modules."
🧰 Tools
🪛 Ruff (0.15.12)
[warning] 438-438: String is split more times than necessary
Use str.rsplit() and pass maxsplit=1
(PLC0207)
🤖 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/app/features/audio/service.py` around lines 394 - 450, The file
exceeds the 300-line limit and mixes concerns; move the rename flow into a
smaller focused module (e.g., audio/rename_service.py) to split
responsibilities: extract the rename_audio_entry coroutine and any
tightly-coupled helpers it needs (references: rename_audio_entry, _row_to_entry,
get_supabase_client, _SELECT_COLS, _SIGNED_URL_TTL, settings.audio_bucket,
logger) into the new module and update imports in the original service to
delegate to it; keep shared constants or utilities (e.g., _row_to_entry,
_SELECT_COLS) either in a new audio/utils.py or export them from the original
module so both pieces can import them without duplicating logic, and run tests
to ensure all references and imports are updated.
| export function useRenameAudio() { | ||
| const qc = useQueryClient(); | ||
| return useMutation<AudioEntryOut, Error, { id: string; title: string }>({ | ||
| mutationFn: ({ id, title }) => renameAudioEntry(id, title), | ||
| onSuccess: () => { | ||
| void qc.invalidateQueries({ queryKey: AUDIO_ENTRIES_KEY }); | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the audio hooks file and understand its structure
fd -t f "useAudio.ts" frontend/Repository: Krasimir-Hristov/AXON
Length of output: 107
🏁 Script executed:
# Check the current content of useAudio.ts
cat -n frontend/features/audio/hooks/useAudio.tsRepository: Krasimir-Hristov/AXON
Length of output: 1373
🏁 Script executed:
# Find how AUDIO_ENTRIES_KEY is defined
rg "AUDIO_ENTRIES_KEY" frontend/features/audio/Repository: Krasimir-Hristov/AXON
Length of output: 459
🏁 Script executed:
# Check for cache key patterns across other hooks in the features directory
rg "const.*_KEY\s*=" frontend/features/ --type ts -A 2 | head -80Repository: Krasimir-Hristov/AXON
Length of output: 1016
🏁 Script executed:
# Look at how other mutations handle cache invalidation in features
rg "invalidateQueries" frontend/features/ --type ts -B 2 -A 2 | head -100Repository: Krasimir-Hristov/AXON
Length of output: 2643
🏁 Script executed:
# Check the conversationsKey function to see the proper user-scoping pattern
rg -A 5 "export function conversationsKey" frontend/features/chat/hooks/Repository: Krasimir-Hristov/AXON
Length of output: 588
🏁 Script executed:
# Check how useAudioEntries uses the key and what listAudioEntries does
cat -n frontend/lib/api.ts | grep -A 10 "listAudioEntries\|deleteAudioEntry\|renameAudioEntry"Repository: Krasimir-Hristov/AXON
Length of output: 714
🏁 Script executed:
# Check the full useConversations hook to see how it uses user-scoped keys
rg -B 5 -A 15 "useQuery.*CONVERSATIONS_KEY" frontend/features/chat/hooks/useConversations.tsRepository: Krasimir-Hristov/AXON
Length of output: 47
🏁 Script executed:
# Check if there's a utility or pattern file for cache keys
find frontend -name "*cache*" -o -name "*key*" | grep -E "\.(ts|tsx)$"Repository: Krasimir-Hristov/AXON
Length of output: 47
🏁 Script executed:
# Check how useAudioEntries is used and if userId is available at call site
rg "useAudioEntries" frontend/features/ -B 3 -A 3Repository: Krasimir-Hristov/AXON
Length of output: 1539
🏁 Script executed:
# Check if there's a useCurrentUser or similar hook to get userId
rg "useCurrentUser\|useUser\|userId" frontend/features/audio/ --type ts -A 2Repository: Krasimir-Hristov/AXON
Length of output: 47
🏁 Script executed:
# Verify the complete useConversations implementation to see how it properly uses userId
cat -n frontend/features/chat/hooks/useConversations.ts | head -60Repository: Krasimir-Hristov/AXON
Length of output: 2434
Scope audio query keys by user ID to prevent cross-user cache leaks.
The rename mutation uses an unscoped cache key. If users switch accounts in the same client session, stale entries can be shown under the wrong identity. Adopt the pattern from useConversations.ts — create an audioEntriesKey(userId: string) function, call useUserId() from the Supabase session, and pass userId to all query and mutation cache keys.
🤖 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 `@frontend/features/audio/hooks/useAudio.ts` around lines 31 - 37, The rename
mutation uses a global AUDIO_ENTRIES_KEY causing cross-user cache leaks; update
to scope keys by user: add a helper audioEntriesKey(userId: string) (mirroring
useConversations.ts), call useUserId() inside useRenameAudio to get the current
userId, and replace AUDIO_ENTRIES_KEY with audioEntriesKey(userId) in the
useMutation onSuccess invalidation and any related queries; ensure mutationFn
still calls renameAudioEntry(id, title) but all cache operations use
audioEntriesKey(userId) so cache is isolated per user.
- async temp I/O: wrap store/retrieve/delete_temp_audio in asyncio.to_thread
- extract rename_audio_entry to rename.py (service.py was ~460 lines)
- router: UUID token type + rate limit on /temp/{token} endpoint
- tool: await async temp audio service calls
- AudioCard: show optimisticTitle in delete dialog (was stale entry.title)
- useAudio: user-scoped cache keys to prevent cross-user cache leak
- api: Zod validation on renameAudioEntry inputs
skip: mask_pii intentionally removed (causes TTS to say 'person')
Summary by CodeRabbit