karaoke-gen 0.75.53__py3-none-any.whl → 0.81.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. karaoke_gen/audio_fetcher.py +218 -0
  2. karaoke_gen/instrumental_review/static/index.html +179 -16
  3. karaoke_gen/karaoke_gen.py +191 -25
  4. karaoke_gen/lyrics_processor.py +39 -31
  5. karaoke_gen/utils/__init__.py +26 -0
  6. karaoke_gen/utils/cli_args.py +9 -1
  7. karaoke_gen/utils/gen_cli.py +1 -1
  8. karaoke_gen/utils/remote_cli.py +33 -6
  9. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/METADATA +80 -4
  10. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/RECORD +50 -43
  11. lyrics_transcriber/core/config.py +8 -0
  12. lyrics_transcriber/core/controller.py +43 -1
  13. lyrics_transcriber/correction/agentic/providers/config.py +6 -0
  14. lyrics_transcriber/correction/agentic/providers/model_factory.py +24 -1
  15. lyrics_transcriber/correction/agentic/router.py +17 -13
  16. lyrics_transcriber/frontend/.gitignore +1 -0
  17. lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +207 -0
  18. lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +226 -0
  19. lyrics_transcriber/frontend/index.html +5 -1
  20. lyrics_transcriber/frontend/package-lock.json +4553 -0
  21. lyrics_transcriber/frontend/package.json +7 -1
  22. lyrics_transcriber/frontend/playwright.config.ts +69 -0
  23. lyrics_transcriber/frontend/public/nomad-karaoke-logo.svg +5 -0
  24. lyrics_transcriber/frontend/src/App.tsx +88 -59
  25. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +55 -21
  26. lyrics_transcriber/frontend/src/components/AppHeader.tsx +65 -0
  27. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +39 -35
  28. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +9 -9
  29. lyrics_transcriber/frontend/src/components/EditModal.tsx +1 -1
  30. lyrics_transcriber/frontend/src/components/EditWordList.tsx +1 -1
  31. lyrics_transcriber/frontend/src/components/Header.tsx +96 -3
  32. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +120 -3
  33. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +22 -21
  34. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  35. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +12 -2
  36. lyrics_transcriber/frontend/src/components/WordDivider.tsx +3 -3
  37. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +122 -35
  38. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +2 -2
  39. lyrics_transcriber/frontend/src/components/shared/constants.ts +15 -5
  40. lyrics_transcriber/frontend/src/components/shared/types.ts +6 -0
  41. lyrics_transcriber/frontend/src/main.tsx +1 -7
  42. lyrics_transcriber/frontend/src/theme.ts +337 -135
  43. lyrics_transcriber/frontend/vite.config.ts +5 -0
  44. lyrics_transcriber/frontend/yarn.lock +1005 -1046
  45. lyrics_transcriber/output/generator.py +50 -3
  46. lyrics_transcriber/review/server.py +1 -1
  47. lyrics_transcriber/transcribers/local_whisper.py +260 -0
  48. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/WHEEL +0 -0
  49. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/entry_points.txt +0 -0
  50. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/licenses/LICENSE +0 -0
@@ -11,7 +11,10 @@
11
11
  "lint": "eslint .",
12
12
  "preview": "vite preview",
13
13
  "predeploy": "npm run build-prod",
14
- "deploy": "gh-pages -d dist"
14
+ "deploy": "gh-pages -d dist",
15
+ "test": "playwright test",
16
+ "test:ui": "playwright test --ui",
17
+ "test:headed": "playwright test --headed"
15
18
  },
16
19
  "dependencies": {
17
20
  "@emotion/react": "^11.14.0",
@@ -19,6 +22,7 @@
19
22
  "@mui/icons-material": "^6.3.0",
20
23
  "@mui/material": "^6.3.0",
21
24
  "@mui/system": "^6.4.3",
25
+ "lucide-react": "^0.562.0",
22
26
  "nanoid": "^5.0.9",
23
27
  "react": "^18.3.1",
24
28
  "react-dom": "^18.3.1",
@@ -26,6 +30,7 @@
26
30
  },
27
31
  "devDependencies": {
28
32
  "@eslint/js": "^9.17.0",
33
+ "@playwright/test": "^1.57.0",
29
34
  "@types/react": "^18.3.18",
30
35
  "@types/react-dom": "^18.3.5",
31
36
  "@vitejs/plugin-react": "^4.3.4",
@@ -34,6 +39,7 @@
34
39
  "eslint-plugin-react-refresh": "^0.4.16",
35
40
  "gh-pages": "^6.3.0",
36
41
  "globals": "^15.14.0",
42
+ "playwright": "^1.57.0",
37
43
  "typescript": "~5.6.2",
38
44
  "typescript-eslint": "^8.18.2",
39
45
  "vite": "^6.0.5"
@@ -0,0 +1,69 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ /**
4
+ * Playwright configuration for lyrics-transcriber frontend E2E tests.
5
+ * Tests run against the local dev server (localhost:5173) served by vite.
6
+ */
7
+ export default defineConfig({
8
+ testDir: './e2e',
9
+
10
+ // Run tests sequentially for now
11
+ fullyParallel: false,
12
+
13
+ // Fail the build on CI if you accidentally left test.only in the source code
14
+ forbidOnly: !!process.env.CI,
15
+
16
+ // Retry on CI only
17
+ retries: process.env.CI ? 2 : 0,
18
+
19
+ // Single worker
20
+ workers: 1,
21
+
22
+ // Reporter to use
23
+ reporter: [
24
+ ['html', { open: 'never' }],
25
+ ['list'],
26
+ ],
27
+
28
+ // Shared settings for all tests
29
+ use: {
30
+ // Base URL for the local dev server
31
+ baseURL: 'http://localhost:5173',
32
+
33
+ // Collect trace on failure for debugging
34
+ trace: 'on-first-retry',
35
+
36
+ // Screenshot on failure
37
+ screenshot: 'only-on-failure',
38
+
39
+ // Video on failure
40
+ video: 'on-first-retry',
41
+
42
+ // Increase timeout for complex interactions
43
+ actionTimeout: 30000,
44
+ },
45
+
46
+ // Longer timeout for tests
47
+ timeout: 120000, // 2 minutes per test
48
+
49
+ // Expect timeout for assertions
50
+ expect: {
51
+ timeout: 30000, // 30 seconds for expects
52
+ },
53
+
54
+ // Configure projects (browsers)
55
+ projects: [
56
+ {
57
+ name: 'chromium',
58
+ use: { ...devices['Desktop Chrome'] },
59
+ },
60
+ ],
61
+
62
+ // Run local dev server before starting tests
63
+ webServer: {
64
+ command: 'npm run dev',
65
+ url: 'http://localhost:5173',
66
+ reuseExistingServer: !process.env.CI,
67
+ timeout: 120000,
68
+ },
69
+ });
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="45 60 255 135">
4
+ <g data-v-70b83f88="" fill="#ff7acc" class="basesvg" transform="translate(48.368003845214844,62.62239074707031)"><g fill-rule="" class="tp-name" transform="translate(0,0)"><g transform="scale(1.4000000000000004)"><g stroke="#ff7acc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" paint-order="stroke" data-gra-attr="stroke" fill-opacity="0"><path d="M1.4-33.44L1.4 0C31.48 0 3.96 0 34.04 0L34.04-32.64 17.81-32.64 17.81-20.61 1.4-33.44ZM52.21-33.76C42.6-33.76 34.81-25.97 34.81-16.37 34.81-6.76 42.6 1.03 52.21 1.03 61.81 1.03 69.6-6.76 69.6-16.37 69.6-25.97 61.81-33.76 52.21-33.76ZM70.7 0L103.34 0 103.25-33.9 86.93-21.54 70.61-33.9 70.7 0ZM102.34 0C138.2 0 107.52 0 143.56 0L122.95-34.93 102.34 0ZM158.84 0.05C167.84 0.05 175.16-7.27 175.16-16.27 175.16-25.27 167.84-32.64 158.84-32.64L142.52-32.64C142.52 0.75 142.52-33.34 142.52 0.05L158.84 0.05Z" transform="translate(-1.399999976158142, 34.93000030517578)"></path></g><g transform="translate(0,38.959999084472656)"><g stroke="#ff7acc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" paint-order="stroke" data-gra-attr="stroke" fill="#ff7acc" fill-opacity="0" transform="scale(1.02719)"><path d="M24.43 0L17.67 0 7.98-11.87 7.98 0 2.63 0 2.63-26.64 7.98-26.64 7.98-14.69 17.67-26.64 24.12-26.64 13.13-13.44 24.43 0ZM45.03 0L43.27-5.08 32.66-5.08 30.91 0 25.3 0 34.88-26.68 41.1-26.68 50.68 0 45.03 0ZM34.11-9.35L41.82-9.35 37.97-20.5 34.11-9.35ZM73.19 0L67.01 0 61.13-10.38 58.61-10.38 58.61 0 53.27 0 53.27-26.64 63.27-26.64Q66.36-26.64 68.53-25.55 70.71-24.47 71.8-22.61 72.89-20.76 72.89-18.47L72.89-18.47Q72.89-15.84 71.36-13.72 69.83-11.6 66.82-10.8L66.82-10.8 73.19 0ZM58.61-22.21L58.61-14.39 63.08-14.39Q65.25-14.39 66.32-15.44 67.39-16.49 67.39-18.36L67.39-18.36Q67.39-20.19 66.32-21.2 65.25-22.21 63.08-22.21L63.08-22.21 58.61-22.21ZM94.82 0L93.07-5.08 82.46-5.08 80.7 0 75.09 0 84.67-26.68 90.89-26.68 100.47 0 94.82 0ZM83.91-9.35L91.62-9.35 87.76-20.5 83.91-9.35ZM115.43 0.27Q111.69 0.27 108.56-1.49 105.43-3.24 103.6-6.35 101.76-9.47 101.76-13.4L101.76-13.4Q101.76-17.29 103.6-20.4 105.43-23.51 108.56-25.27 111.69-27.02 115.43-27.02L115.43-27.02Q119.21-27.02 122.32-25.27 125.43-23.51 127.24-20.4 129.05-17.29 129.05-13.4L129.05-13.4Q129.05-9.47 127.24-6.35 125.43-3.24 122.3-1.49 119.17 0.27 115.43 0.27L115.43 0.27ZM115.43-4.5Q117.83-4.5 119.66-5.59 121.5-6.68 122.53-8.7 123.56-10.73 123.56-13.4L123.56-13.4Q123.56-16.07 122.53-18.07 121.5-20.08 119.66-21.15 117.83-22.21 115.43-22.21L115.43-22.21Q113.02-22.21 111.17-21.15 109.32-20.08 108.29-18.07 107.26-16.07 107.26-13.4L107.26-13.4Q107.26-10.73 108.29-8.7 109.32-6.68 111.17-5.59 113.02-4.5 115.43-4.5L115.43-4.5ZM153.82 0L147.06 0 137.37-11.87 137.37 0 132.02 0 132.02-26.64 137.37-26.64 137.37-14.69 147.06-26.64 153.51-26.64 142.52-13.44 153.82 0ZM171.79-22.33L161.67-22.33 161.67-15.65 170.64-15.65 170.64-11.41 161.67-11.41 161.67-4.35 171.79-4.35 171.79 0 156.33 0 156.33-26.68 171.79-26.68 171.79-22.33Z" transform="translate(-2.630000114440918, 27.020000457763672)"></path></g></g></g></g> <g data-gra="path-slogan" fill-rule="" class="tp-slogan" fill="#ffdf6b" transform="translate(5,111.788818359375)"><rect x="0" height="1" y="5.9832000732421875" width="14.278396606445312"></rect> <rect height="1" y="5.9832000732421875" width="14.278396606445312" x="218.985595703125"></rect> <g transform="translate(17.278396606445312,0)"><g transform="scale(1.2800000000000002)"><path d="M10.21-8.38L12.02-8.38L9.68 0L7.70 0L6.13-5.96L4.49 0L2.52 0.01L0.26-8.38L2.06-8.38L3.54-1.87L5.24-8.38L7.12-8.38L8.72-1.91L10.21-8.38ZM18.38-8.38L20.06-8.38L20.06 0L18.38 0L18.38-3.56L14.80-3.56L14.80 0L13.12 0L13.12-8.38L14.80-8.38L14.80-4.93L18.38-4.93L18.38-8.38ZM26.58-7.02L23.40-7.02L23.40-4.92L26.22-4.92L26.22-3.59L23.40-3.59L23.40-1.37L26.58-1.37L26.58 0L21.72 0L21.72-8.39L26.58-8.39L26.58-7.02ZM34.37 0L32.42 0L30.58-3.26L29.78-3.26L29.78 0L28.10 0L28.10-8.38L31.25-8.38Q32.22-8.38 32.90-8.03Q33.59-7.69 33.93-7.11Q34.27-6.53 34.27-5.81L34.27-5.81Q34.27-4.98 33.79-4.31Q33.31-3.65 32.36-3.40L32.36-3.40L34.37 0ZM29.78-6.98L29.78-4.52L31.19-4.52Q31.87-4.52 32.21-4.85Q32.54-5.18 32.54-5.77L32.54-5.77Q32.54-6.35 32.21-6.67Q31.87-6.98 31.19-6.98L31.19-6.98L29.78-6.98ZM40.66-7.02L37.48-7.02L37.48-4.92L40.30-4.92L40.30-3.59L37.48-3.59L37.48-1.37L40.66-1.37L40.66 0L35.80 0L35.80-8.39L40.66-8.39L40.66-7.02ZM47.92-8.38L49.70-8.38L46.63 0L44.59 0L41.52-8.38L43.32-8.38L45.62-1.72L47.92-8.38ZM55.57-7.02L52.39-7.02L52.39-4.92L55.21-4.92L55.21-3.59L52.39-3.59L52.39-1.37L55.57-1.37L55.57 0L50.71 0L50.71-8.39L55.57-8.39L55.57-7.02ZM63.36 0L61.42 0L59.57-3.26L58.78-3.26L58.78 0L57.10 0L57.10-8.38L60.24-8.38Q61.21-8.38 61.90-8.03Q62.58-7.69 62.92-7.11Q63.26-6.53 63.26-5.81L63.26-5.81Q63.26-4.98 62.78-4.31Q62.30-3.65 61.36-3.40L61.36-3.40L63.36 0ZM58.78-6.98L58.78-4.52L60.18-4.52Q60.86-4.52 61.20-4.85Q61.54-5.18 61.54-5.77L61.54-5.77Q61.54-6.35 61.20-6.67Q60.86-6.98 60.18-6.98L60.18-6.98L58.78-6.98ZM72.43-8.38L74.30-8.38L71.47-2.92L71.47 0L69.79 0L69.79-2.92L66.95-8.38L68.84-8.38L70.64-4.55L72.43-8.38ZM79.16 0.08Q77.99 0.08 77.00-0.47Q76.02-1.02 75.44-2.00Q74.87-2.98 74.87-4.21L74.87-4.21Q74.87-5.44 75.44-6.41Q76.02-7.39 77.00-7.94Q77.99-8.50 79.16-8.50L79.16-8.50Q80.35-8.50 81.33-7.94Q82.31-7.39 82.88-6.41Q83.45-5.44 83.45-4.21L83.45-4.21Q83.45-2.98 82.88-2.00Q82.31-1.02 81.32-0.47Q80.34 0.08 79.16 0.08L79.16 0.08ZM79.16-1.42Q79.92-1.42 80.50-1.76Q81.07-2.10 81.40-2.74Q81.72-3.37 81.72-4.21L81.72-4.21Q81.72-5.05 81.40-5.68Q81.07-6.31 80.50-6.65Q79.92-6.98 79.16-6.98L79.16-6.98Q78.41-6.98 77.83-6.65Q77.24-6.31 76.92-5.68Q76.60-5.05 76.60-4.21L76.60-4.21Q76.60-3.37 76.92-2.74Q77.24-2.10 77.83-1.76Q78.41-1.42 79.16-1.42L79.16-1.42ZM84.67-8.38L86.35-8.38L86.35-3.19Q86.35-2.34 86.80-1.89Q87.24-1.44 88.04-1.44L88.04-1.44Q88.86-1.44 89.30-1.89Q89.75-2.34 89.75-3.19L89.75-3.19L89.75-8.38L91.44-8.38L91.44-3.20Q91.44-2.14 90.98-1.40Q90.52-0.66 89.74-0.29Q88.97 0.08 88.02 0.08L88.02 0.08Q87.08 0.08 86.32-0.29Q85.56-0.66 85.12-1.40Q84.67-2.14 84.67-3.20L84.67-3.20L84.67-8.38ZM101.60 0L101.05-1.60L97.72-1.60L97.16 0L95.40 0L98.41-8.39L100.37-8.39L103.38 0L101.60 0ZM98.17-2.94L100.60-2.94L99.38-6.44L98.17-2.94ZM110.77 0L108.83 0L106.98-3.26L106.19-3.26L106.19 0L104.51 0L104.51-8.38L107.65-8.38Q108.62-8.38 109.31-8.03Q109.99-7.69 110.33-7.11Q110.68-6.53 110.68-5.81L110.68-5.81Q110.68-4.98 110.20-4.31Q109.72-3.65 108.77-3.40L108.77-3.40L110.77 0ZM106.19-6.98L106.19-4.52L107.59-4.52Q108.28-4.52 108.61-4.85Q108.95-5.18 108.95-5.77L108.95-5.77Q108.95-6.35 108.61-6.67Q108.28-6.98 107.59-6.98L107.59-6.98L106.19-6.98ZM117.06-7.02L113.88-7.02L113.88-4.92L116.70-4.92L116.70-3.59L113.88-3.59L113.88-1.37L117.06-1.37L117.06 0L112.20 0L112.20-8.39L117.06-8.39L117.06-7.02ZM118.72-1.76L120.42-1.76L119.02 1.61L117.94 1.61L118.72-1.76ZM127.38 0.08Q126.50 0.08 125.80-0.22Q125.10-0.52 124.69-1.08Q124.28-1.64 124.27-2.41L124.27-2.41L126.07-2.41Q126.11-1.90 126.44-1.60Q126.77-1.30 127.34-1.30L127.34-1.30Q127.93-1.30 128.27-1.58Q128.60-1.86 128.60-2.32L128.60-2.32Q128.60-2.69 128.38-2.93Q128.15-3.17 127.81-3.31Q127.46-3.44 126.86-3.61L126.86-3.61Q126.05-3.85 125.54-4.09Q125.03-4.32 124.66-4.79Q124.30-5.27 124.30-6.06L124.30-6.06Q124.30-6.80 124.67-7.36Q125.04-7.91 125.71-8.20Q126.38-8.50 127.25-8.50L127.25-8.50Q128.54-8.50 129.35-7.87Q130.16-7.24 130.25-6.11L130.25-6.11L128.40-6.11Q128.38-6.54 128.03-6.82Q127.69-7.10 127.13-7.10L127.13-7.10Q126.64-7.10 126.34-6.85Q126.05-6.60 126.05-6.12L126.05-6.12Q126.05-5.78 126.27-5.56Q126.49-5.34 126.82-5.20Q127.15-5.06 127.75-4.88L127.75-4.88Q128.57-4.64 129.08-4.40Q129.60-4.16 129.97-3.68Q130.34-3.20 130.34-2.42L130.34-2.42Q130.34-1.75 130.00-1.18Q129.65-0.60 128.98-0.26Q128.30 0.08 127.38 0.08L127.38 0.08ZM131.80-8.38L133.48-8.38L133.48 0L131.80 0L131.80-8.38ZM142.30-8.39L142.30 0L140.62 0L136.81-5.75L136.81 0L135.13 0L135.13-8.39L136.81-8.39L140.62-2.63L140.62-8.39L142.30-8.39ZM151.72-5.86L149.78-5.86Q149.50-6.38 148.99-6.66Q148.49-6.94 147.82-6.94L147.82-6.94Q147.07-6.94 146.50-6.60Q145.92-6.26 145.60-5.64Q145.27-5.02 145.27-4.20L145.27-4.20Q145.27-3.36 145.60-2.74Q145.93-2.11 146.52-1.78Q147.11-1.44 147.89-1.44L147.89-1.44Q148.85-1.44 149.46-1.95Q150.07-2.46 150.26-3.37L150.26-3.37L147.38-3.37L147.38-4.66L151.92-4.66L151.92-3.19Q151.75-2.32 151.20-1.57Q150.65-0.83 149.78-0.38Q148.91 0.07 147.83 0.07L147.83 0.07Q146.62 0.07 145.64-0.47Q144.66-1.02 144.10-1.99Q143.54-2.96 143.54-4.20L143.54-4.20Q143.54-5.44 144.10-6.41Q144.66-7.39 145.64-7.94Q146.62-8.48 147.82-8.48L147.82-8.48Q149.23-8.48 150.28-7.79Q151.32-7.10 151.72-5.86L151.72-5.86ZM153.53-8.52L155.34-8.52L155.15-2.72L153.73-2.72L153.53-8.52ZM154.48 0.08Q154.02 0.08 153.73-0.20Q153.43-0.48 153.43-0.90L153.43-0.90Q153.43-1.32 153.73-1.60Q154.02-1.88 154.48-1.88L154.48-1.88Q154.92-1.88 155.21-1.60Q155.50-1.32 155.50-0.90L155.50-0.90Q155.50-0.48 155.21-0.20Q154.92 0.08 154.48 0.08L154.48 0.08Z" transform="translate(-0.264, 8.52)"></path></g></g></g></g>
5
+ </svg>
@@ -1,12 +1,23 @@
1
1
  import UploadFileIcon from '@mui/icons-material/UploadFile'
2
2
  import { Alert, Box, Button, Modal, Typography } from '@mui/material'
3
- import { useEffect, useState } from 'react'
3
+ import { ThemeProvider } from '@mui/material/styles'
4
+ import CssBaseline from '@mui/material/CssBaseline'
5
+ import { useEffect, useState, useMemo } from 'react'
4
6
  import { ApiClient, FileOnlyClient, LiveApiClient } from './api'
5
7
  import CorrectionMetrics from './components/CorrectionMetrics'
6
8
  import LyricsAnalyzer from './components/LyricsAnalyzer'
9
+ import AppHeader from './components/AppHeader'
7
10
  import { CorrectionData } from './types'
11
+ import { createAppTheme } from './theme'
12
+
13
+ const THEME_STORAGE_KEY = 'nomad-karaoke-lyrics-theme'
8
14
 
9
15
  export default function App() {
16
+ const [isDarkMode, setIsDarkMode] = useState(() => {
17
+ // Initialize from localStorage
18
+ const stored = localStorage.getItem(THEME_STORAGE_KEY)
19
+ return stored !== 'light' // Default to dark
20
+ })
10
21
  const [data, setData] = useState<CorrectionData | null>(null)
11
22
  const [showMetadata, setShowMetadata] = useState(false)
12
23
  const [error, setError] = useState<string | null>(null)
@@ -14,6 +25,16 @@ export default function App() {
14
25
  const [isReadOnly, setIsReadOnly] = useState(true)
15
26
  const [audioHash, setAudioHash] = useState<string>('')
16
27
 
28
+ // Create theme based on mode
29
+ const theme = useMemo(() => createAppTheme(isDarkMode ? 'dark' : 'light'), [isDarkMode])
30
+
31
+ // Handle theme toggle
32
+ const handleToggleTheme = () => {
33
+ const newIsDark = !isDarkMode
34
+ setIsDarkMode(newIsDark)
35
+ localStorage.setItem(THEME_STORAGE_KEY, newIsDark ? 'dark' : 'light')
36
+ }
37
+
17
38
  useEffect(() => {
18
39
  // Parse query parameters
19
40
  const params = new URLSearchParams(window.location.search)
@@ -145,70 +166,78 @@ export default function App() {
145
166
 
146
167
  if (!data) {
147
168
  return (
148
- <Box sx={{ p: 3 }}>
149
- {error && (
150
- <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
151
- {error}
152
- </Alert>
153
- )}
154
- {isReadOnly ? (
155
- <>
156
- <Alert severity="info" sx={{ mb: 2 }}>
157
- Running in read-only mode. Connect to an API to enable editing.
169
+ <ThemeProvider theme={theme}>
170
+ <CssBaseline />
171
+ <AppHeader isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />
172
+ <Box sx={{ p: 3 }}>
173
+ {error && (
174
+ <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
175
+ {error}
158
176
  </Alert>
159
- <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
160
- <Typography variant="h4">
161
- Lyrics Correction Review
177
+ )}
178
+ {isReadOnly ? (
179
+ <>
180
+ <Alert severity="info" sx={{ mb: 2 }}>
181
+ Running in read-only mode. Connect to an API to enable editing.
182
+ </Alert>
183
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
184
+ <Typography variant="h4">
185
+ Lyrics Correction Review
186
+ </Typography>
187
+ <Button
188
+ variant="outlined"
189
+ startIcon={<UploadFileIcon />}
190
+ onClick={handleFileLoad}
191
+ >
192
+ Load File
193
+ </Button>
194
+ </Box>
195
+ <Box sx={{ mb: 3 }}>
196
+ <CorrectionMetrics />
197
+ </Box>
198
+ </>
199
+ ) : (
200
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
201
+ <Typography variant="h6" color="text.secondary">
202
+ Loading Lyrics Transcription Review...
162
203
  </Typography>
163
- <Button
164
- variant="outlined"
165
- startIcon={<UploadFileIcon />}
166
- onClick={handleFileLoad}
167
- >
168
- Load File
169
- </Button>
170
- </Box>
171
- <Box sx={{ mb: 3 }}>
172
- <CorrectionMetrics />
173
204
  </Box>
174
- </>
175
- ) : (
176
- <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
177
- <Typography variant="h6" color="text.secondary">
178
- Loading Lyrics Correction Review...
179
- </Typography>
180
- </Box>
181
- )}
182
- </Box>
205
+ )}
206
+ </Box>
207
+ </ThemeProvider>
183
208
  )
184
209
  }
185
210
 
186
211
  return (
187
- <Box sx={{
188
- p: 1.5,
189
- pb: 3,
190
- maxWidth: '100%',
191
- overflowX: 'hidden'
192
- }}>
193
- {error && (
194
- <Alert severity="error" sx={{ mb: 1 }} onClose={() => setError(null)}>
195
- {error}
196
- </Alert>
197
- )}
198
- {isReadOnly && (
199
- <Alert severity="info" sx={{ mb: 1 }}>
200
- Running in read-only mode. Connect to an API to enable editing.
201
- </Alert>
202
- )}
203
- <LyricsAnalyzer
204
- data={data}
205
- onFileLoad={handleFileLoad}
206
- onShowMetadata={() => setShowMetadata(true)}
207
- apiClient={apiClient}
208
- isReadOnly={isReadOnly}
209
- audioHash={audioHash}
210
- />
211
- {renderMetadataModal()}
212
- </Box>
212
+ <ThemeProvider theme={theme}>
213
+ <CssBaseline />
214
+ <AppHeader isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />
215
+ <Box sx={{
216
+ p: 1.5,
217
+ pb: 3,
218
+ maxWidth: '100%',
219
+ overflowX: 'hidden'
220
+ }}>
221
+ {error && (
222
+ <Alert severity="error" sx={{ mb: 1 }} onClose={() => setError(null)}>
223
+ {error}
224
+ </Alert>
225
+ )}
226
+ {isReadOnly && (
227
+ <Alert severity="info" sx={{ mb: 1 }}>
228
+ Running in read-only mode. Connect to an API to enable editing.
229
+ </Alert>
230
+ )}
231
+ <LyricsAnalyzer
232
+ data={data}
233
+ onFileLoad={handleFileLoad}
234
+ onShowMetadata={() => setShowMetadata(true)}
235
+ apiClient={apiClient}
236
+ isReadOnly={isReadOnly}
237
+ audioHash={audioHash}
238
+ />
239
+ {renderMetadataModal()}
240
+ </Box>
241
+ </ThemeProvider>
213
242
  )
214
243
  }
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useEffect, useRef } from "react";
2
2
 
3
3
  type Props = {
4
4
  isOpen: boolean;
@@ -12,22 +12,57 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
12
12
  const [finalText, setFinalText] = React.useState("");
13
13
  const [reasonCategory, setReason] = React.useState("AI_CORRECT");
14
14
  const [reasonDetail, setDetail] = React.useState("");
15
+ const modalRef = useRef<HTMLDivElement>(null);
16
+
17
+ // Handle Escape key to close modal
18
+ useEffect(() => {
19
+ const handleKeyDown = (e: KeyboardEvent) => {
20
+ if (e.key === 'Escape' && isOpen) {
21
+ onClose();
22
+ }
23
+ };
24
+ document.addEventListener('keydown', handleKeyDown);
25
+ return () => document.removeEventListener('keydown', handleKeyDown);
26
+ }, [isOpen, onClose]);
27
+
28
+ // Focus modal on open
29
+ useEffect(() => {
30
+ if (isOpen && modalRef.current) {
31
+ modalRef.current.focus();
32
+ }
33
+ }, [isOpen]);
15
34
 
16
35
  if (!isOpen) return null;
17
36
 
37
+ // Dark theme colors matching karaoke-gen
38
+ const colors = {
39
+ background: '#1a1a1a', // slate-800
40
+ text: '#f8fafc', // slate-50
41
+ textSecondary: '#888888', // slate-400
42
+ border: '#2a2a2a', // slate-700
43
+ inputBg: '#0f0f0f', // slate-900
44
+ };
45
+
18
46
  return (
19
- <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", display: "flex", alignItems: "center", justifyContent: "center" }}>
20
- <div style={{ background: "#fff", padding: 16, width: 480, borderRadius: 8 }}>
21
- <h3>AI Suggestion</h3>
22
- <p style={{ marginTop: 8 }}>
47
+ <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1300 }}>
48
+ <div
49
+ ref={modalRef}
50
+ role="dialog"
51
+ aria-modal="true"
52
+ aria-labelledby="ai-feedback-title"
53
+ tabIndex={-1}
54
+ style={{ background: colors.background, padding: 16, width: 480, borderRadius: 8, border: `1px solid ${colors.border}`, color: colors.text, outline: 'none' }}
55
+ >
56
+ <h3 id="ai-feedback-title" style={{ color: colors.text, margin: 0 }}>AI Suggestion</h3>
57
+ <p style={{ marginTop: 8, color: colors.text }}>
23
58
  {suggestion?.text ?? "No suggestion"}
24
59
  {suggestion?.confidence != null ? ` (confidence ${Math.round((suggestion.confidence || 0) * 100)}%)` : null}
25
60
  </p>
26
- {suggestion?.reasoning ? <small>{suggestion.reasoning}</small> : null}
61
+ {suggestion?.reasoning ? <small style={{ color: colors.textSecondary }}>{suggestion.reasoning}</small> : null}
27
62
 
28
- <div style={{ marginTop: 12 }}>
29
- <label>Action</label>
30
- <select value={reviewerAction} onChange={(e) => setAction(e.target.value)} style={{ marginLeft: 8 }}>
63
+ <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
64
+ <label htmlFor="ai-action-select" style={{ color: colors.text }}>Action</label>
65
+ <select id="ai-action-select" value={reviewerAction} onChange={(e) => setAction(e.target.value)} style={{ background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px' }}>
31
66
  <option value="ACCEPT">Accept</option>
32
67
  <option value="REJECT">Reject</option>
33
68
  <option value="MODIFY">Modify</option>
@@ -35,15 +70,15 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
35
70
  </div>
36
71
 
37
72
  {reviewerAction === "MODIFY" ? (
38
- <div style={{ marginTop: 12 }}>
39
- <label>Final Text</label>
40
- <input value={finalText} onChange={(e) => setFinalText(e.target.value)} style={{ marginLeft: 8, width: "100%" }} />
73
+ <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 4 }}>
74
+ <label htmlFor="ai-final-text" style={{ color: colors.text }}>Final Text</label>
75
+ <input id="ai-final-text" value={finalText} onChange={(e) => setFinalText(e.target.value)} style={{ width: "100%", background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px', boxSizing: 'border-box' }} />
41
76
  </div>
42
77
  ) : null}
43
78
 
44
- <div style={{ marginTop: 12 }}>
45
- <label>Reason</label>
46
- <select value={reasonCategory} onChange={(e) => setReason(e.target.value)} style={{ marginLeft: 8 }}>
79
+ <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
80
+ <label htmlFor="ai-reason-select" style={{ color: colors.text }}>Reason</label>
81
+ <select id="ai-reason-select" value={reasonCategory} onChange={(e) => setReason(e.target.value)} style={{ background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px' }}>
47
82
  <option value="AI_CORRECT">AI_CORRECT</option>
48
83
  <option value="AI_INCORRECT">AI_INCORRECT</option>
49
84
  <option value="AI_SUBOPTIMAL">AI_SUBOPTIMAL</option>
@@ -52,17 +87,18 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
52
87
  </select>
53
88
  </div>
54
89
 
55
- <div style={{ marginTop: 12 }}>
56
- <label>Details</label>
57
- <textarea value={reasonDetail} onChange={(e) => setDetail(e.target.value)} style={{ marginLeft: 8, width: "100%" }} />
90
+ <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 4 }}>
91
+ <label htmlFor="ai-details-textarea" style={{ color: colors.text }}>Details</label>
92
+ <textarea id="ai-details-textarea" value={reasonDetail} onChange={(e) => setDetail(e.target.value)} style={{ width: "100%", background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px', boxSizing: 'border-box' }} />
58
93
  </div>
59
94
 
60
95
  <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 }}>
61
- <button onClick={onClose}>Cancel</button>
96
+ <button onClick={onClose} style={{ background: colors.border, color: colors.text, border: 'none', borderRadius: 4, padding: '6px 12px', cursor: 'pointer' }}>Cancel</button>
62
97
  <button
63
98
  onClick={() =>
64
99
  onSubmit({ reviewerAction, finalText: finalText || undefined, reasonCategory, reasonDetail: reasonDetail || undefined })
65
100
  }
101
+ style={{ background: '#f97316', color: '#fff', border: 'none', borderRadius: 4, padding: '6px 12px', cursor: 'pointer' }}
66
102
  >
67
103
  Submit
68
104
  </button>
@@ -73,5 +109,3 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
73
109
  };
74
110
 
75
111
  export default AIFeedbackModal;
76
-
77
-
@@ -0,0 +1,65 @@
1
+ import { Box, IconButton, Tooltip, Typography, useTheme } from '@mui/material'
2
+ import { Sun, Moon } from 'lucide-react'
3
+
4
+ interface AppHeaderProps {
5
+ isDarkMode?: boolean
6
+ onToggleTheme?: () => void
7
+ }
8
+
9
+ export default function AppHeader({ isDarkMode = true, onToggleTheme }: AppHeaderProps) {
10
+ const theme = useTheme()
11
+
12
+ return (
13
+ <Box
14
+ component="header"
15
+ sx={{
16
+ borderBottom: `1px solid ${theme.palette.divider}`,
17
+ backgroundColor: theme.palette.background.paper,
18
+ backdropFilter: 'blur(8px)',
19
+ position: 'sticky',
20
+ top: 0,
21
+ zIndex: 1100,
22
+ px: 2,
23
+ py: 1.5,
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ justifyContent: 'space-between',
27
+ }}
28
+ >
29
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
30
+ <img src="/nomad-karaoke-logo.svg" alt="Nomad Karaoke" style={{ height: 40 }} />
31
+ <Typography
32
+ variant="h6"
33
+ sx={{
34
+ fontWeight: 'bold',
35
+ color: theme.palette.text.primary,
36
+ fontSize: '1.1rem',
37
+ lineHeight: 1,
38
+ m: 0,
39
+ }}
40
+ >
41
+ Lyrics Transcription Review
42
+ </Typography>
43
+ </Box>
44
+
45
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
46
+ {onToggleTheme && (
47
+ <Tooltip title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}>
48
+ <IconButton
49
+ onClick={onToggleTheme}
50
+ sx={{
51
+ color: theme.palette.text.secondary,
52
+ '&:hover': {
53
+ color: theme.palette.text.primary,
54
+ backgroundColor: theme.palette.action.hover,
55
+ },
56
+ }}
57
+ >
58
+ {isDarkMode ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
59
+ </IconButton>
60
+ </Tooltip>
61
+ )}
62
+ </Box>
63
+ </Box>
64
+ )
65
+ }
@@ -23,6 +23,7 @@ interface CorrectedWordWithActionsProps {
23
23
  onClick?: () => void
24
24
  backgroundColor?: string
25
25
  shouldFlash?: boolean
26
+ showActions?: boolean // Controls whether inline action buttons are visible
26
27
  }
27
28
 
28
29
  const WordContainer = styled(Box, {
@@ -42,7 +43,7 @@ const WordContainer = styled(Box, {
42
43
  '50%': { opacity: 0.5 }
43
44
  },
44
45
  '&:hover': {
45
- backgroundColor: '#c8e6c9'
46
+ backgroundColor: 'rgba(34, 197, 94, 0.35)' // green tint hover for dark mode
46
47
  }
47
48
  }))
48
49
 
@@ -51,7 +52,7 @@ const OriginalWordLabel = styled(Box)({
51
52
  top: '-14px',
52
53
  left: '0',
53
54
  fontSize: '0.6rem',
54
- color: '#666',
55
+ color: '#888888', // slate-400 for dark mode
55
56
  textDecoration: 'line-through',
56
57
  opacity: 0.7,
57
58
  whiteSpace: 'nowrap',
@@ -71,10 +72,10 @@ const ActionButton = styled(IconButton)(({ theme }) => ({
71
72
  minHeight: '20px',
72
73
  width: '20px',
73
74
  height: '20px',
74
- backgroundColor: 'rgba(255, 255, 255, 0.9)',
75
- border: '1px solid rgba(0, 0, 0, 0.1)',
75
+ backgroundColor: 'rgba(30, 41, 59, 0.9)', // slate-800 with opacity for dark mode
76
+ border: '1px solid rgba(248, 250, 252, 0.1)', // light border for dark mode
76
77
  '&:hover': {
77
- backgroundColor: 'rgba(255, 255, 255, 1)',
78
+ backgroundColor: 'rgba(51, 65, 85, 1)', // slate-700 for dark mode
78
79
  transform: 'scale(1.1)'
79
80
  },
80
81
  '& .MuiSvgIcon-root': {
@@ -98,7 +99,8 @@ export default function CorrectedWordWithActions({
98
99
  onAccept,
99
100
  onClick,
100
101
  backgroundColor,
101
- shouldFlash
102
+ shouldFlash,
103
+ showActions = true
102
104
  }: CorrectedWordWithActionsProps) {
103
105
  const theme = useTheme()
104
106
  const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
@@ -115,7 +117,7 @@ export default function CorrectedWordWithActions({
115
117
  onClick={onClick}
116
118
  >
117
119
  <OriginalWordLabel>{originalWord}</OriginalWordLabel>
118
-
120
+
119
121
  <Box
120
122
  component="span"
121
123
  sx={{
@@ -127,40 +129,42 @@ export default function CorrectedWordWithActions({
127
129
  {word}
128
130
  </Box>
129
131
 
130
- <ActionsContainer>
131
- <Tooltip title="Revert to original" placement="top" arrow>
132
- <ActionButton
133
- size="small"
134
- onClick={(e) => handleAction(e, onRevert)}
135
- aria-label="revert correction"
136
- >
137
- <UndoIcon />
138
- </ActionButton>
139
- </Tooltip>
140
-
141
- <Tooltip title="Edit correction" placement="top" arrow>
142
- <ActionButton
143
- size="small"
144
- onClick={(e) => handleAction(e, onEdit)}
145
- aria-label="edit correction"
146
- >
147
- <EditIcon />
148
- </ActionButton>
149
- </Tooltip>
132
+ {showActions && (
133
+ <ActionsContainer>
134
+ <Tooltip title="Revert to original" placement="top" arrow>
135
+ <ActionButton
136
+ size="small"
137
+ onClick={(e) => handleAction(e, onRevert)}
138
+ aria-label="revert correction"
139
+ >
140
+ <UndoIcon />
141
+ </ActionButton>
142
+ </Tooltip>
150
143
 
151
- {!isMobile && (
152
- <Tooltip title="Accept correction" placement="top" arrow>
144
+ <Tooltip title="Edit correction" placement="top" arrow>
153
145
  <ActionButton
154
146
  size="small"
155
- onClick={(e) => handleAction(e, onAccept)}
156
- aria-label="accept correction"
157
- sx={{ color: 'success.main' }}
147
+ onClick={(e) => handleAction(e, onEdit)}
148
+ aria-label="edit correction"
158
149
  >
159
- <CheckCircleOutlineIcon />
150
+ <EditIcon />
160
151
  </ActionButton>
161
152
  </Tooltip>
162
- )}
163
- </ActionsContainer>
153
+
154
+ {!isMobile && (
155
+ <Tooltip title="Accept correction" placement="top" arrow>
156
+ <ActionButton
157
+ size="small"
158
+ onClick={(e) => handleAction(e, onAccept)}
159
+ aria-label="accept correction"
160
+ sx={{ color: 'success.main' }}
161
+ >
162
+ <CheckCircleOutlineIcon />
163
+ </ActionButton>
164
+ </Tooltip>
165
+ )}
166
+ </ActionsContainer>
167
+ )}
164
168
  </WordContainer>
165
169
  )
166
170
  }