[{"data":1,"prerenderedAt":4308},["ShallowReactive",2],{"navigation":3,"/blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders-post":734,"/blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders-surround":874,"/blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders-related":879},[4,70,207,312,721],{"title":5,"path":6,"stem":7,"children":8,"page":69},"Case Study","/blog/case-study","blog/case-study",[9,13,17,21,25,29,33,37,41,45,49,53,57,61,65],{"title":10,"path":11,"stem":12},"Ambistream – Multi-Layer Streaming Platform","/blog/case-study/ambistream-building-a-multi-layer-streaming-platform-from-a-spark-of-an-idea","blog/case-study/ambistream-building-a-multi-layer-streaming-platform-from-a-spark-of-an-idea",{"title":14,"path":15,"stem":16},"Custom Chromecast & AirPlay Casting App","/blog/case-study/chromecast-airplay-casting-app-case-study","blog/case-study/chromecast-airplay-casting-app-case-study",{"title":18,"path":19,"stem":20},"Direct Music Licensing Platform Case Study","/blog/case-study/dlm-music-catalog-case-study","blog/case-study/dlm-music-catalog-case-study",{"title":22,"path":23,"stem":24},"Assembly Instructions App - Galeco Mobile App","/blog/case-study/galeco-mobile-app-case-study","blog/case-study/galeco-mobile-app-case-study",{"title":26,"path":27,"stem":28},"MemoSonic: Building an Audio Memory Game","/blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter","blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter",{"title":30,"path":31,"stem":32},"Loyalty Program App for Shopping Mall","/blog/case-study/loyalty-program-application-case-study","blog/case-study/loyalty-program-application-case-study",{"title":34,"path":35,"stem":36},"Mobile Application for Music Catalogs","/blog/case-study/mobile-app-for-music-catalog","blog/case-study/mobile-app-for-music-catalog",{"title":38,"path":39,"stem":40},"Universal Music Data Parser for 20+ Platforms","/blog/case-study/musicdata-lab-universal-music-data-parser-case-study","blog/case-study/musicdata-lab-universal-music-data-parser-case-study",{"title":42,"path":43,"stem":44},"Panther ML/AI Pricing Recommendation Tool","/blog/case-study/panther-pricing-recommendation-tool-case-study","blog/case-study/panther-pricing-recommendation-tool-case-study",{"title":46,"path":47,"stem":48},"4D Grupa Roofing Wholesalers Platform","/blog/case-study/roofing-wholesalers-website-case-study","blog/case-study/roofing-wholesalers-website-case-study",{"title":50,"path":51,"stem":52},"Talent Alpha HR Frontend Platform","/blog/case-study/talent-alpha-hr-platform-case-study","blog/case-study/talent-alpha-hr-platform-case-study",{"title":54,"path":55,"stem":56},"Ticketing & Events Platform Development","/blog/case-study/ticketing-events-platform-case-study","blog/case-study/ticketing-events-platform-case-study",{"title":58,"path":59,"stem":60},"Turn Fans into Superfans — Roadie.co","/blog/case-study/turn-fans-into-superfans-roadie-co","blog/case-study/turn-fans-into-superfans-roadie-co",{"title":62,"path":63,"stem":64},"Walkative 2.0 Global Booking Engine","/blog/case-study/walkative-2-booking-platform-case-study","blog/case-study/walkative-2-booking-platform-case-study",{"title":66,"path":67,"stem":68},"Why Merch Is the New Royalty Check, and How Tech Is Closing the Gap","/blog/case-study/why-merch-is-the-new-royalty-check","blog/case-study/why-merch-is-the-new-royalty-check",false,{"title":71,"path":72,"stem":73,"children":74,"page":69},"Music Data","/blog/music-data","blog/music-data",[75,79,83,87,91,95,99,103,107,111,115,119,123,127,131,135,139,143,147,151,155,159,163,167,171,175,179,183,187,191,195,199,203],{"title":76,"path":77,"stem":78},"13 Distributors, 5 File Formats, Zero Standards -The Reality of Music Royalty Data","/blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data","blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data",{"title":80,"path":81,"stem":82},"830 Ways to Say Spotify - Normalizing Music Streaming Data","/blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data","blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data",{"title":84,"path":85,"stem":86},"Why Music Companies Need AI-Powered Analytics (And How We Built One)","/blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama","blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama",{"title":88,"path":89,"stem":90},"AI Rehearsal: Spaced Repetition for Your Musical Ideas","/blog/music-data/ai-rehearsal-spaced-repetition-for-musical-ideas","blog/music-data/ai-rehearsal-spaced-repetition-for-musical-ideas",{"title":92,"path":93,"stem":94},"Audio Project Organization Is a Mess — Here's Why","/blog/music-data/audio-project-organization-mess","blog/music-data/audio-project-organization-mess",{"title":96,"path":97,"stem":98},"Why Audio Search Is Still Broken and How to Fix It with Embeddings","/blog/music-data/audio-search-broken-fix-with-embeddings","blog/music-data/audio-search-broken-fix-with-embeddings",{"title":100,"path":101,"stem":102},"AI Song Structure Analysis: Intro, Verse, Chorus","/blog/music-data/automatic-song-structure-analysis-how-ai-detects-intro-verse-chorus","blog/music-data/automatic-song-structure-analysis-how-ai-detects-intro-verse-chorus",{"title":104,"path":105,"stem":106},"The Broken Feedback Loop in Music Collaboration","/blog/music-data/broken-feedback-loop-music-collaboration","blog/music-data/broken-feedback-loop-music-collaboration",{"title":108,"path":109,"stem":110},"Building a Claude Skill for DDEX Validation: Automate Music Metadata Checks with AI","/blog/music-data/building-a-claude-skill-for-ddex-validation-music-metadata","blog/music-data/building-a-claude-skill-for-ddex-validation-music-metadata",{"title":112,"path":113,"stem":114},"Building a Custom Music Delivery Platform on the Revelator API","/blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api","blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api",{"title":116,"path":117,"stem":118},"Building a Suno AI Remix App with Nuxt & Firebase","/blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase","blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase",{"title":120,"path":121,"stem":122},"C2PA & DDEX: Authenticity Meets Rights in the Age of AI Music","/blog/music-data/c2pa-and-ddex-authenticity-meets-rights-in-the-age-of-ai-music","blog/music-data/c2pa-and-ddex-authenticity-meets-rights-in-the-age-of-ai-music",{"title":124,"path":125,"stem":126},"C2PA in Music: A Claude MCP for Reading Content Provenance","/blog/music-data/c2pa-in-music-mcp","blog/music-data/c2pa-in-music-mcp",{"title":128,"path":129,"stem":130},"Data Modeling in MongoDB Using Design Patterns","/blog/music-data/data-modeling-in-mongodb-with-the-usage-of-design-patterns","blog/music-data/data-modeling-in-mongodb-with-the-usage-of-design-patterns",{"title":132,"path":133,"stem":134},"Office Hours with MusicTech Lab's DDEX Expert","/blog/music-data/ddex-office-hours-musictech","blog/music-data/ddex-office-hours-musictech",{"title":136,"path":137,"stem":138},"DDEX Open Source Projects Review","/blog/music-data/ddex-open-source-projects-review","blog/music-data/ddex-open-source-projects-review",{"title":140,"path":141,"stem":142},"Extracting Data from Ableton .als and .asd Files","/blog/music-data/extracting-data-from-ableton-als-asd-files","blog/music-data/extracting-data-from-ableton-als-asd-files",{"title":144,"path":145,"stem":146},"Maciej Dulski on Sound Connections Podcast","/blog/music-data/from-startups-to-musictech-maciej-dulski-on-sound-connections-podcast","blog/music-data/from-startups-to-musictech-maciej-dulski-on-sound-connections-podcast",{"title":148,"path":149,"stem":150},"How to Transcribe Video to Text Using OpenAI Whisper","/blog/music-data/how-to-transcribe-video-to-text-using-whisper","blog/music-data/how-to-transcribe-video-to-text-using-whisper",{"title":152,"path":153,"stem":154},"Epidemic Sound MCP with Claude for Devs","/blog/music-data/how-to-use-epidemic-sound-mcp-with-claude","blog/music-data/how-to-use-epidemic-sound-mcp-with-claude",{"title":156,"path":157,"stem":158},"Hybrid Database Model in Django for Speed","/blog/music-data/hybrid-database-model-in-django-as-a-performance-booster","blog/music-data/hybrid-database-model-in-django-as-a-performance-booster",{"title":160,"path":161,"stem":162},"Introduction to generating DDEX file using Python","/blog/music-data/introduction-to-generating-ddex-file-using-python","blog/music-data/introduction-to-generating-ddex-file-using-python",{"title":164,"path":165,"stem":166},"Maintaining Music Tech Tools: The SLA Dilemma for Small Teams","/blog/music-data/maintaining-music-tech-tools-the-sla-dilemma-for-small-teams","blog/music-data/maintaining-music-tech-tools-the-sla-dilemma-for-small-teams",{"title":168,"path":169,"stem":170},"Querying Bandcamp Revenue Reports with Natural Language — Meet mtl-bandcamp-mcp","/blog/music-data/mtl-bandcamp-mcp-open-source-revenue-dashboard","blog/music-data/mtl-bandcamp-mcp-open-source-revenue-dashboard",{"title":172,"path":173,"stem":174},"mtl-metadata-mcp: Open Source Audio Metadata Embedding for Claude Code","/blog/music-data/mtl-metadata-mcp-open-source-audio-metadata-embedding","blog/music-data/mtl-metadata-mcp-open-source-audio-metadata-embedding",{"title":176,"path":177,"stem":178},"MusicTech Resources for Builders","/blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders","blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders",{"title":180,"path":181,"stem":182},"Poland's Creative Tech and MusicTech Rise","/blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it","blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it",{"title":184,"path":185,"stem":186},"Batch ISRC Enrichment That Turns Messy Catalogs Into Clean Data","/blog/music-data/scout-isrc-metadata-enrichment-spotify-musicbrainz","blog/music-data/scout-isrc-metadata-enrichment-spotify-musicbrainz",{"title":188,"path":189,"stem":190},"Music Self-Publishing: The Emuze.me Story","/blog/music-data/self-publishing-in-the-music-industry-a-tale-of-emuze-me","blog/music-data/self-publishing-in-the-music-industry-a-tale-of-emuze-me",{"title":192,"path":193,"stem":194},"Understanding the API First Approach","/blog/music-data/understanding-the-api-first-approach","blog/music-data/understanding-the-api-first-approach",{"title":196,"path":197,"stem":198},"The Voice Memo Graveyard Problem","/blog/music-data/voice-memo-graveyard-problem","blog/music-data/voice-memo-graveyard-problem",{"title":200,"path":201,"stem":202},"Which Database for Music Data? Redshift vs BigQuery vs ClickHouse and When to Use Each","/blog/music-data/which-database-for-music-data-redshift-vs-bigquery-vs-clickhouse","blog/music-data/which-database-for-music-data-redshift-vs-bigquery-vs-clickhouse",{"title":204,"path":205,"stem":206},"Why we decided to use wavesurfer.js","/blog/music-data/why-we-decided-to-use-wavesurfer","blog/music-data/why-we-decided-to-use-wavesurfer",{"title":208,"path":209,"stem":210,"children":211,"page":69},"Newsletter","/blog/newsletter","blog/newsletter",[212,216,220,224,228,232,236,240,244,248,252,256,260,264,268,272,276,280,284,288,292,296,300,304,308],{"title":213,"path":214,"stem":215},"Music Industry Tech Openings (April 2024 Update)","/blog/newsletter/music-industry-tech-openings-april-2024-update","blog/newsletter/music-industry-tech-openings-april-2024-update",{"title":217,"path":218,"stem":219},"Music Industry Tech Openings (April 2025 Update)","/blog/newsletter/music-industry-tech-openings-april-2025-update","blog/newsletter/music-industry-tech-openings-april-2025-update",{"title":221,"path":222,"stem":223},"Music Industry Tech Openings (August 2024 Update)","/blog/newsletter/music-industry-tech-openings-august-2024-update","blog/newsletter/music-industry-tech-openings-august-2024-update",{"title":225,"path":226,"stem":227},"Music Industry Tech Openings (December 2024 Update)","/blog/newsletter/music-industry-tech-openings-december-2024-update","blog/newsletter/music-industry-tech-openings-december-2024-update",{"title":229,"path":230,"stem":231},"Music Industry Tech Openings (February 2025 Update)","/blog/newsletter/music-industry-tech-openings-february-2025-update","blog/newsletter/music-industry-tech-openings-february-2025-update",{"title":233,"path":234,"stem":235},"Music Industry Tech Openings (January 2025 Update)","/blog/newsletter/music-industry-tech-openings-january-2025-update","blog/newsletter/music-industry-tech-openings-january-2025-update",{"title":237,"path":238,"stem":239},"Music Industry Tech Openings (July 2024 Update)","/blog/newsletter/music-industry-tech-openings-july-2024-update","blog/newsletter/music-industry-tech-openings-july-2024-update",{"title":241,"path":242,"stem":243},"Music Industry Tech Openings (June 2024 Update)","/blog/newsletter/music-industry-tech-openings-june-2024-update","blog/newsletter/music-industry-tech-openings-june-2024-update",{"title":245,"path":246,"stem":247},"Music Industry Tech Openings (March 2025 Update)","/blog/newsletter/music-industry-tech-openings-march-2025-update","blog/newsletter/music-industry-tech-openings-march-2025-update",{"title":249,"path":250,"stem":251},"Music Industry Tech Openings (May 2024 Update)","/blog/newsletter/music-industry-tech-openings-may-2024-update","blog/newsletter/music-industry-tech-openings-may-2024-update",{"title":253,"path":254,"stem":255},"Music Industry Tech Openings (May 2025 Update)","/blog/newsletter/music-industry-tech-openings-may-2025","blog/newsletter/music-industry-tech-openings-may-2025",{"title":257,"path":258,"stem":259},"Music Industry Tech Openings (November 2024 Update)","/blog/newsletter/music-industry-tech-openings-november-2024-update","blog/newsletter/music-industry-tech-openings-november-2024-update",{"title":261,"path":262,"stem":263},"Music Industry Tech Openings (October 2024 Update)","/blog/newsletter/music-industry-tech-openings-october-2024-update","blog/newsletter/music-industry-tech-openings-october-2024-update",{"title":265,"path":266,"stem":267},"Music Industry Tech Openings (September 2024 Update)","/blog/newsletter/music-industry-tech-openings-september-2024-update","blog/newsletter/music-industry-tech-openings-september-2024-update",{"title":269,"path":270,"stem":271},"MusicTech Insights #1 by Maciej Dulski","/blog/newsletter/musictech-insights-1-curated-by-maciej-dulski","blog/newsletter/musictech-insights-1-curated-by-maciej-dulski",{"title":273,"path":274,"stem":275},"Provenance, Not Detection, Is the Durable Answer","/blog/newsletter/musictech-insights-10-curated-by-jim-anderson","blog/newsletter/musictech-insights-10-curated-by-jim-anderson",{"title":277,"path":278,"stem":279},"What CMOs can teach us about innovation in uncertain times","/blog/newsletter/musictech-insights-10-curated-by-joanna-kurkowska","blog/newsletter/musictech-insights-10-curated-by-joanna-kurkowska",{"title":281,"path":282,"stem":283},"Feeling the MusicTech Momentum","/blog/newsletter/musictech-insights-2-curated-by-maciej-dulski","blog/newsletter/musictech-insights-2-curated-by-maciej-dulski",{"title":285,"path":286,"stem":287},"AI in Music: Hype, Hope, and a Human Touch","/blog/newsletter/musictech-insights-3-curated-by-drew-thurlow","blog/newsletter/musictech-insights-3-curated-by-drew-thurlow",{"title":289,"path":290,"stem":291},"The Music Metadata Conundrum","/blog/newsletter/musictech-insights-4-curated-by-amanda-schupf","blog/newsletter/musictech-insights-4-curated-by-amanda-schupf",{"title":293,"path":294,"stem":295},"7 Rounds in the First 10 Days of November 2025","/blog/newsletter/musictech-insights-5-curated-by-maciej-dulski","blog/newsletter/musictech-insights-5-curated-by-maciej-dulski",{"title":297,"path":298,"stem":299},"The End of an Era: It's All About to Crash","/blog/newsletter/musictech-insights-6-curated-by-sigurdur-arnason","blog/newsletter/musictech-insights-6-curated-by-sigurdur-arnason",{"title":301,"path":302,"stem":303},"Low-Code Magic Won't Solve MusicTech Reality","/blog/newsletter/musictech-insights-7-curated-by-mariusz-smenzyk","blog/newsletter/musictech-insights-7-curated-by-mariusz-smenzyk",{"title":305,"path":306,"stem":307},"The New Economics of Game Music","/blog/newsletter/musictech-insights-8-curated-by-kenny-vaughan","blog/newsletter/musictech-insights-8-curated-by-kenny-vaughan",{"title":309,"path":310,"stem":311},"Music Business Meets Direct-to-Fan","/blog/newsletter/musictech-insights-9-curated-by-yaw-asamani","blog/newsletter/musictech-insights-9-curated-by-yaw-asamani",{"title":313,"path":314,"stem":315,"children":316,"page":69},"Software Development","/blog/software-development","blog/software-development",[317,321,325,329,333,337,341,345,349,353,357,361,365,369,373,377,381,385,389,393,397,401,405,409,413,417,421,425,429,433,437,441,445,449,453,457,461,465,469,473,477,481,485,489,493,497,501,505,509,513,517,521,525,529,533,537,541,545,549,553,557,561,565,569,573,577,581,585,589,593,597,601,605,609,613,617,621,625,629,633,637,641,645,649,653,657,661,665,669,673,677,681,685,689,693,697,701,705,709,713,717],{"title":318,"path":319,"stem":320},"Benefits of Outsourcing Software Development","/blog/software-development/10-benefits-of-outsourcing-software-development-services","blog/software-development/10-benefits-of-outsourcing-software-development-services",{"title":322,"path":323,"stem":324},"10 Steps to Find the Best MVP Developers","/blog/software-development/10-steps-to-find-the-best-mvp-developers-for-your-startup-idea","blog/software-development/10-steps-to-find-the-best-mvp-developers-for-your-startup-idea",{"title":326,"path":327,"stem":328},"1,200 Looms Later: How Async Video Became My Development Superpower","/blog/software-development/1200-looms-how-async-video-became-our-development-superpower","blog/software-development/1200-looms-how-async-video-became-our-development-superpower",{"title":330,"path":331,"stem":332},"Communication Strategy in Outsourcing Projects","/blog/software-development/5-steps-to-implement-an-effective-communication-strategy-in-outsourcing-software-development-project","blog/software-development/5-steps-to-implement-an-effective-communication-strategy-in-outsourcing-software-development-project",{"title":334,"path":335,"stem":336},"7 Best Practices for Outsourcing Software Development","/blog/software-development/7-best-practices-for-outsourcing-software-development","blog/software-development/7-best-practices-for-outsourcing-software-development",{"title":338,"path":339,"stem":340},"9 Reasons Why Saleor.io Is Best for eCommerce","/blog/software-development/9-reasons-why-the-saleor-io-platform-is-the-best-choice-for-your-ecommerce-website","blog/software-development/9-reasons-why-the-saleor-io-platform-is-the-best-choice-for-your-ecommerce-website",{"title":342,"path":343,"stem":344},"A Look at Bravelab.io’s Clutch 2021 Year In Review","/blog/software-development/a-look-at-bravelab-ios-clutch-2021-year-in-review","blog/software-development/a-look-at-bravelab-ios-clutch-2021-year-in-review",{"title":346,"path":347,"stem":348},"A quick introduction to profit sharing implementation","/blog/software-development/a-quick-introduction-to-profit-sharing-implementation","blog/software-development/a-quick-introduction-to-profit-sharing-implementation",{"title":350,"path":351,"stem":352},"AI Audio Similarity Search: The Future of Sound Library Discovery","/blog/software-development/ai-audio-similarity-search-for-sound-libraries","blog/software-development/ai-audio-similarity-search-for-sound-libraries",{"title":354,"path":355,"stem":356},"Automate Repetitive Tasks for Better Results","/blog/software-development/automate-repetitive-tasks-to-improve-your-business-performance","blog/software-development/automate-repetitive-tasks-to-improve-your-business-performance",{"title":358,"path":359,"stem":360},"Automating Success: The Art of Unified Documentation","/blog/software-development/automating-success-the-art-of-unified-documentation","blog/software-development/automating-success-the-art-of-unified-documentation",{"title":362,"path":363,"stem":364},"Brave 3.0 Website Redesign, Part 2: Solution","/blog/software-development/brave-3-0-how-we-conducted-website-redesign-part-2-solution","blog/software-development/brave-3-0-how-we-conducted-website-redesign-part-2-solution",{"title":366,"path":367,"stem":368},"Brave 3.0, Part 4: Tech Stack and Recap","/blog/software-development/brave-3-0-part-4-technologies-behind-and-final-series-recap","blog/software-development/brave-3-0-part-4-technologies-behind-and-final-series-recap",{"title":370,"path":371,"stem":372},"Brave 3.0 – redesign process part 1. The Challenge","/blog/software-development/brave-3-0-redesign-process-part-1-challenge","blog/software-development/brave-3-0-redesign-process-part-1-challenge",{"title":374,"path":375,"stem":376},"Brave 3.0 – redesign process, part 3. Lesson learned","/blog/software-development/brave-3-0-redesign-process-part-3-lesson-learned","blog/software-development/brave-3-0-redesign-process-part-3-lesson-learned",{"title":378,"path":379,"stem":380},"Bravelab.io: Top Software Developer by Clutch","/blog/software-development/bravelab-io-is-recognized-as-a-top-custom-software-developer-by-clutch","blog/software-development/bravelab-io-is-recognized-as-a-top-custom-software-developer-by-clutch",{"title":382,"path":383,"stem":384},"Bravelab.io: Top Developer in Poland by Clutch","/blog/software-development/bravelab-io-named-top-software-developer-in-poland-by-clutch","blog/software-development/bravelab-io-named-top-software-developer-in-poland-by-clutch",{"title":386,"path":387,"stem":388},"MusicTech Lab Partners with LALAL.AI","/blog/software-development/bravelab-partners-with-the-audio-lalal-ai","blog/software-development/bravelab-partners-with-the-audio-lalal-ai",{"title":390,"path":391,"stem":392},"MusicTech Lab Partners with The Audio Programmer","/blog/software-development/bravelab-partners-with-the-audio-programmer","blog/software-development/bravelab-partners-with-the-audio-programmer",{"title":394,"path":395,"stem":396},"Bravelab's team about productivity","/blog/software-development/bravelabs-team-about-productivity","blog/software-development/bravelabs-team-about-productivity",{"title":398,"path":399,"stem":400},"Braveloper","/blog/software-development/braveloper","blog/software-development/braveloper",{"title":402,"path":403,"stem":404},"Bravely App: Boost Productivity with Django","/blog/software-development/bravely-app-how-to-be-more-productive-with-django-quick","blog/software-development/bravely-app-how-to-be-more-productive-with-django-quick",{"title":406,"path":407,"stem":408},"DIY MIDI Controller for Ableton with Arduino","/blog/software-development/building-a-diy-midi-controller-for-ableton-live-with-arduino","blog/software-development/building-a-diy-midi-controller-for-ableton-live-with-arduino",{"title":410,"path":411,"stem":412},"C2PA in Ableton: Making AI Music Provenance Visible Inside Your DAW","/blog/software-development/c2pa-in-ableton-max-for-live","blog/software-development/c2pa-in-ableton-max-for-live",{"title":414,"path":415,"stem":416},"Change Detection mechanism in Angular","/blog/software-development/change-detection-mechanism-in-angular","blog/software-development/change-detection-mechanism-in-angular",{"title":418,"path":419,"stem":420},"Communication Channels in Remote Work","/blog/software-development/comparison-of-the-communication-channels-in-remote-work","blog/software-development/comparison-of-the-communication-channels-in-remote-work",{"title":422,"path":423,"stem":424},"Connecting Your Max for Live Device to a Cloud API","/blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api","blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api",{"title":426,"path":427,"stem":428},"From Voice Memo to Studio: The Cross-Platform Problem for Creators","/blog/software-development/cross-platform-problem-for-creators","blog/software-development/cross-platform-problem-for-creators",{"title":430,"path":431,"stem":432},"Cultural transformation through the pandemic era","/blog/software-development/cultural-transformation-through-the-pandemic-era","blog/software-development/cultural-transformation-through-the-pandemic-era",{"title":434,"path":435,"stem":436},"D-Commerce Decoded: Cutting Through the Hype","/blog/software-development/d-commerce-decoded-cutting-through-the-hype","blog/software-development/d-commerce-decoded-cutting-through-the-hype",{"title":438,"path":439,"stem":440},"Dev Meeting 002: Intro to DDD","/blog/software-development/dev-meeting-002-introduction-to-domain-driven-design-ddd","blog/software-development/dev-meeting-002-introduction-to-domain-driven-design-ddd",{"title":442,"path":443,"stem":444},"Dev Meeting 003: Web3 Primer","/blog/software-development/dev-meeting-003-web3-primer","blog/software-development/dev-meeting-003-web3-primer",{"title":446,"path":447,"stem":448},"Dev Meeting 004: Introduction to Event Storming","/blog/software-development/dev-meeting-004-introduction-to-event-storming","blog/software-development/dev-meeting-004-introduction-to-event-storming",{"title":450,"path":451,"stem":452},"Dev Meeting 001: Kubernetes is a Framework","/blog/software-development/dev-meeting-kubernetes-is-a-framework","blog/software-development/dev-meeting-kubernetes-is-a-framework",{"title":454,"path":455,"stem":456},"Did You Know? 10 Developer Tips from Real Codebases","/blog/software-development/did-you-know-dev-tips-part-1","blog/software-development/did-you-know-dev-tips-part-1",{"title":458,"path":459,"stem":460},"10 Surprising MusicTech Facts (Part 2)","/blog/software-development/did-you-know-musictech-facts-part-2","blog/software-development/did-you-know-musictech-facts-part-2",{"title":462,"path":463,"stem":464},"Django-cms and GraphQL","/blog/software-development/django-cms-and-graphql","blog/software-development/django-cms-and-graphql",{"title":466,"path":467,"stem":468},"Does Zappa make it super easy?","/blog/software-development/does-zappa-make-it-super-easy","blog/software-development/does-zappa-make-it-super-easy",{"title":470,"path":471,"stem":472},"Establishing cooperation between Netlify and Bravelab","/blog/software-development/establishing-cooperation-between-netlify-and-bravelab","blog/software-development/establishing-cooperation-between-netlify-and-bravelab",{"title":474,"path":475,"stem":476},"Export Ableton Locators to JSON via Max for Live","/blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live","blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live",{"title":478,"path":479,"stem":480},"IT Outsourcing: Success and Failure Factors","/blog/software-development/factors-that-contribute-to-the-success-or-failure-of-an-it-outsourcing-project","blog/software-development/factors-that-contribute-to-the-success-or-failure-of-an-it-outsourcing-project",{"title":482,"path":483,"stem":484},"Flutter 2022 Strategy: Analyzing the Roadmap","/blog/software-development/flutter-strategy-for-2022-analyzing-the-new-flutter-roadmap","blog/software-development/flutter-strategy-for-2022-analyzing-the-new-flutter-roadmap",{"title":486,"path":487,"stem":488},"Git Better #1 — Commit Message Convention","/blog/software-development/git-better-1-see-more-with-a-commit-message-convention","blog/software-development/git-better-1-see-more-with-a-commit-message-convention",{"title":490,"path":491,"stem":492},"Hasura in action. How to use it with Django","/blog/software-development/hasura-in-action","blog/software-development/hasura-in-action",{"title":494,"path":495,"stem":496},"Holacracy why and where we are","/blog/software-development/holacracy-why-and-where-we-are","blog/software-development/holacracy-why-and-where-we-are",{"title":498,"path":499,"stem":500},"How does JavaScript work","/blog/software-development/how-does-javascript-work","blog/software-development/how-does-javascript-work",{"title":502,"path":503,"stem":504},"How important is good UX/UI design?","/blog/software-development/how-important-is-good-ux-ui-design","blog/software-development/how-important-is-good-ux-ui-design",{"title":506,"path":507,"stem":508},"How repetitive tasks impact your business","/blog/software-development/how-repetitive-tasks-impact-your-business","blog/software-development/how-repetitive-tasks-impact-your-business",{"title":510,"path":511,"stem":512},"Becoming a Vue.js Dev: Do Paid Trials Work?","/blog/software-development/how-to-become-a-vue-js-developer-and-whether-paid-trials-in-it-work-out","blog/software-development/how-to-become-a-vue-js-developer-and-whether-paid-trials-in-it-work-out",{"title":514,"path":515,"stem":516},"How to Build an MVP in 6 Steps","/blog/software-development/how-to-build-a-minimum-viable-product-mvp-in-6-steps","blog/software-development/how-to-build-a-minimum-viable-product-mvp-in-6-steps",{"title":518,"path":519,"stem":520},"How to conduct workshops for creative industry?","/blog/software-development/how-to-conduct-workshops-for-creative-industry","blog/software-development/how-to-conduct-workshops-for-creative-industry",{"title":522,"path":523,"stem":524},"How to easily create form in Angular","/blog/software-development/how-to-easily-create-form-in-angular","blog/software-development/how-to-easily-create-form-in-angular",{"title":526,"path":527,"stem":528},"How to export orders in Saleor.io to XLSX file","/blog/software-development/how-to-export-orders-in-saleor-io-to-xlsx-file","blog/software-development/how-to-export-orders-in-saleor-io-to-xlsx-file",{"title":530,"path":531,"stem":532},"Handling High Loads on E-Commerce Platforms","/blog/software-development/how-to-handle-high-loads-on-e-commerce-platform-with-ease","blog/software-development/how-to-handle-high-loads-on-e-commerce-platform-with-ease",{"title":534,"path":535,"stem":536},"How to launch Saleor.io shop instance within 40h","/blog/software-development/how-to-launch-saleor-io-shop-instance-within-40h","blog/software-development/how-to-launch-saleor-io-shop-instance-within-40h",{"title":538,"path":539,"stem":540},"First Steps to Build a Business Relationship","/blog/software-development/how-to-make-the-first-step-to-establish-a-business-relationship","blog/software-development/how-to-make-the-first-step-to-establish-a-business-relationship",{"title":542,"path":543,"stem":544},"Multi-Tenant Apps with Django and Saleor.io","/blog/software-development/how-to-manage-tenants-in-the-multitenant-app-based-on-django-tenants-and-saleor-io-platform","blog/software-development/how-to-manage-tenants-in-the-multitenant-app-based-on-django-tenants-and-saleor-io-platform",{"title":546,"path":547,"stem":548},"Notion Backup Tool Built in 3 Days with Python","/blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why","blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why",{"title":550,"path":551,"stem":552},"Important new features in Python 3.8","/blog/software-development/important-new-features-in-python-3-8","blog/software-development/important-new-features-in-python-3-8",{"title":554,"path":555,"stem":556},"Installing Proxmox on dedicated server from OVH","/blog/software-development/installing-proxmox-on-dedicated-server-from-ovh","blog/software-development/installing-proxmox-on-dedicated-server-from-ovh",{"title":558,"path":559,"stem":560},"Integrating SignNow E-Signatures into Your Django Application","/blog/software-development/integrating-signnow-e-signatures-into-your-django-application","blog/software-development/integrating-signnow-e-signatures-into-your-django-application",{"title":562,"path":563,"stem":564},"Tempus Metronome and GetSongBPM API","/blog/software-development/integrating-tempus-metronome-with-the-getsongbpm-api-what-bpm-really-means-and-how-to-use-it","blog/software-development/integrating-tempus-metronome-with-the-getsongbpm-api-what-bpm-really-means-and-how-to-use-it",{"title":566,"path":567,"stem":568},"Introducing MusicTech Poland","/blog/software-development/introducing-musictech-poland","blog/software-development/introducing-musictech-poland",{"title":570,"path":571,"stem":572},"Vue.js as a Frontend for Saleor.io","/blog/software-development/is-it-possible-to-use-vue-js-as-a-frontend-for-saleor-io-platform","blog/software-development/is-it-possible-to-use-vue-js-as-a-frontend-for-saleor-io-platform",{"title":574,"path":575,"stem":576},"Is your business ready for the cashless era?","/blog/software-development/is-your-business-ready-for-the-cashless-era","blog/software-development/is-your-business-ready-for-the-cashless-era",{"title":578,"path":579,"stem":580},"Is your face ready to buy?","/blog/software-development/is-your-face-ready-to-buy","blog/software-development/is-your-face-ready-to-buy",{"title":582,"path":583,"stem":584},"JS Frameworks: Trends and Opportunities","/blog/software-development/javascript-trending-frameworks-and-market-opportunities","blog/software-development/javascript-trending-frameworks-and-market-opportunities",{"title":586,"path":587,"stem":588},"Kanban Board: Boost Your Team Productivity","/blog/software-development/kanban-board-methodology-hack-your-companys-productivity","blog/software-development/kanban-board-methodology-hack-your-companys-productivity",{"title":590,"path":591,"stem":592},"Verified Human Cert MCP Server: Prove Your Music Is Human-Made, Right from the Terminal","/blog/software-development/mcp-verified-human-cert-open-source","blog/software-development/mcp-verified-human-cert-open-source",{"title":594,"path":595,"stem":596},"Migrating from TravisCI to Github Actions","/blog/software-development/migrating-from-travisci-to-github-actions","blog/software-development/migrating-from-travisci-to-github-actions",{"title":598,"path":599,"stem":600},"MusicTech Lab: Top Software Developer by Clutch","/blog/software-development/musictechlab-is-recognized-as-a-top-custom-software-developer-by-clutch","blog/software-development/musictechlab-is-recognized-as-a-top-custom-software-developer-by-clutch",{"title":602,"path":603,"stem":604},"MusicTech Lab x Verified Human: Building a Trust Layer for Human-Made Music","/blog/software-development/musictechlab_blog_verified_human_partnership","blog/software-development/musictechlab_blog_verified_human_partnership",{"title":606,"path":607,"stem":608},"MusicXML: Standard for Music Notation","/blog/software-development/musicxml-standard-for-music-notation-and-education","blog/software-development/musicxml-standard-for-music-notation-and-education",{"title":610,"path":611,"stem":612},"Only a few books but dozens of ideas","/blog/software-development/only-a-few-books-but-dozens-of-ideas","blog/software-development/only-a-few-books-but-dozens-of-ideas",{"title":614,"path":615,"stem":616},"Overdue Invoices and Issue Tracker Integration","/blog/software-development/overdue-invoices-integration-with-the-issue-tracking-system","blog/software-development/overdue-invoices-integration-with-the-issue-tracking-system",{"title":618,"path":619,"stem":620},"Performing SAML SSO using JWT in Django","/blog/software-development/performing-saml-sso-using-jwt-in-django","blog/software-development/performing-saml-sso-using-jwt-in-django",{"title":622,"path":623,"stem":624},"Progressive Web Apps for Mobile Development","/blog/software-development/progressive-web-apps-a-new-way-of-creating-mobile-application","blog/software-development/progressive-web-apps-a-new-way-of-creating-mobile-application",{"title":626,"path":627,"stem":628},"Recruitment System: Gmail, Jira, and CRM","/blog/software-development/recruitment-system-integrating-gmail-bravely-jira-slack-and-copper-crm","blog/software-development/recruitment-system-integrating-gmail-bravely-jira-slack-and-copper-crm",{"title":630,"path":631,"stem":632},"Scratch Me: Chrome Extension for Leads","/blog/software-development/scratch-me-a-simple-chrome-extension-which-will-increase-your-productivity","blog/software-development/scratch-me-a-simple-chrome-extension-which-will-increase-your-productivity",{"title":634,"path":635,"stem":636},"Scratch Me – integration with the Copper CRM","/blog/software-development/scratch-me-integration-with-the-copper-crm","blog/software-development/scratch-me-integration-with-the-copper-crm",{"title":638,"path":639,"stem":640},"SignNow MCP Server: E-Signatures Straight from Claude Code","/blog/software-development/signnow-mcp-server-e-signatures-from-claude-code","blog/software-development/signnow-mcp-server-e-signatures-from-claude-code",{"title":642,"path":643,"stem":644},"Music Industry Tech Openings (March 2024 Update)","/blog/software-development/technical-job-opportunities-in-the-music-industry","blog/software-development/technical-job-opportunities-in-the-music-industry",{"title":646,"path":647,"stem":648},"Thanks app – a Management 3.0 solution","/blog/software-development/thanks-app-a-management-3-0-solution","blog/software-development/thanks-app-a-management-3-0-solution",{"title":650,"path":651,"stem":652},"Colonial Pipeline Case: 7 Security Reminders","/blog/software-development/the-case-of-colonial-pipeline-and-7-security-reminders","blog/software-development/the-case-of-colonial-pipeline-and-7-security-reminders",{"title":654,"path":655,"stem":656},"The Evolution and Future of E-commerce Platforms","/blog/software-development/the-evolution-and-future-of-e-commerce-platforms","blog/software-development/the-evolution-and-future-of-e-commerce-platforms",{"title":658,"path":659,"stem":660},"The Gender Gap in the Tech Industry","/blog/software-development/the-gender-gap-in-the-tech-industry","blog/software-development/the-gender-gap-in-the-tech-industry",{"title":662,"path":663,"stem":664},"First Attempt to Implement 4DX at Bravelab.io","/blog/software-development/the-very-first-attempt-to-implement-4dx-in-bravelab-io","blog/software-development/the-very-first-attempt-to-implement-4dx-in-bravelab-io",{"title":666,"path":667,"stem":668},"The WTF Scale: IT Project Complexity","/blog/software-development/the-wtf-programming-scale-measuring-it-project-complexity","blog/software-development/the-wtf-programming-scale-measuring-it-project-complexity",{"title":670,"path":671,"stem":672},"Top 10 articles through the eyes of our developers","/blog/software-development/top-10-articles-through-the-eyes-of-our-developers","blog/software-development/top-10-articles-through-the-eyes-of-our-developers",{"title":674,"path":675,"stem":676},"Top 6 apps made with Flutter","/blog/software-development/top-6-apps-made-with-flutter","blog/software-development/top-6-apps-made-with-flutter",{"title":678,"path":679,"stem":680},"Uber 101: How Uber Made It to the Top","/blog/software-development/uber-101-how-this-ride-sharing-behemoth-made-it-to-the-top","blog/software-development/uber-101-how-this-ride-sharing-behemoth-made-it-to-the-top",{"title":682,"path":683,"stem":684},"MusicTech Lab Partners with Music Glue","/blog/software-development/unifying-artists-and-audiences-exploring-music-glue","blog/software-development/unifying-artists-and-audiences-exploring-music-glue",{"title":686,"path":687,"stem":688},"Why AI Will Defeat Traditional HR","/blog/software-development/warning-why-artificial-intelligence-will-defeat-traditional-hr","blog/software-development/warning-why-artificial-intelligence-will-defeat-traditional-hr",{"title":690,"path":691,"stem":692},"What is a Discovery Document?","/blog/software-development/what-is-discovery-document","blog/software-development/what-is-discovery-document",{"title":694,"path":695,"stem":696},"What is Flutter, and Why is it Worth Considering?","/blog/software-development/what-is-flutter-and-why-is-it-worth-considering","blog/software-development/what-is-flutter-and-why-is-it-worth-considering",{"title":698,"path":699,"stem":700},"What is a Watermarked Song?","/blog/software-development/what-is-watermarked-song","blog/software-development/what-is-watermarked-song",{"title":702,"path":703,"stem":704},"Choosing a Frontend Framework for the Web","/blog/software-development/which-framework-should-you-choose-for-the-frontend-web-platform-development","blog/software-development/which-framework-should-you-choose-for-the-frontend-web-platform-development",{"title":706,"path":707,"stem":708},"Why DAWs Are the Wrong Tool for Starting a Song","/blog/software-development/why-daws-wrong-tool-for-starting-song","blog/software-development/why-daws-wrong-tool-for-starting-song",{"title":710,"path":711,"stem":712},"Why the Programming World Loves Python","/blog/software-development/why-the-programming-world-loves-python","blog/software-development/why-the-programming-world-loves-python",{"title":714,"path":715,"stem":716},"Why We Don't Build Chat From Scratch (And Neither Should You)","/blog/software-development/why-we-dont-build-chat-from-scratch","blog/software-development/why-we-dont-build-chat-from-scratch",{"title":718,"path":719,"stem":720},"Why we use Sanity.io","/blog/software-development/why-we-use-sanity-io","blog/software-development/why-we-use-sanity-io",{"title":722,"path":723,"stem":724,"children":725,"page":69},"Sportstech","/blog/sportstech","blog/sportstech",[726,730],{"title":727,"path":728,"stem":729},"BeatBuddy Replay: Video Analysis App Challenges","/blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter","blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter",{"title":731,"path":732,"stem":733},"How to Create a Watch Face App for Garmin Watch","/blog/sportstech/how-to-create-watch-face-app-for-garmin-watch","blog/sportstech/how-to-create-watch-face-app-for-garmin-watch",{"id":735,"title":176,"authors":736,"badge":742,"body":745,"category":850,"client":851,"date":852,"description":853,"extension":854,"faq":851,"featured":69,"featuredOrder":851,"hidden":69,"image":855,"keyTakeaways":857,"meta":869,"navigation":858,"path":177,"seo":870,"status":851,"stem":178,"tags":871,"teaser":851,"__hash__":873},"posts/blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders.md",[737],{"name":738,"to":739,"avatar":740},"Maciej Dulski","https://www.linkedin.com/in/maciej-dulski/",{"src":741},"/images/cdn-migrated/maciej-dulski-400x400.webp",{"label":743,"color":744},"Featured","#E91E63",{"type":746,"value":747,"toc":841},"minimark",[748,752,755,769,774,777,799,802,806,809,813,816,819,823,826],[749,750,751],"p",{},"At MusicTech Lab, we often found ourselves wishing for a single, reliable place to track what’s happening across the MusicTech landscape - from investments and acquisitions to the people, tools, and ideas shaping the future of music and technology.",[749,753,754],{},"So we built it.",[749,756,757,758,768],{},"We’re proud to introduce ",[759,760,764],"a",{"href":761,"rel":762},"https://musictechlab.io/resources",[763],"nofollow",[765,766,767],"strong",{},"MusicTech Resources"," - a curated platform designed to support founders, professionals, and teams working at the intersection of music, audio, and innovation.",[770,771,773],"h3",{"id":772},"what-youll-find","What you’ll find",[749,775,776],{},"MusicTech Resources brings together the most relevant and up-to-date information from across the sector. If you're building a startup in music, audio, or entertainment, the platform includes:",[778,779,780,784,787,790,793,796],"ul",{},[781,782,783],"li",{},"A dataset of ~500 investor/startup deals",[781,785,786],{},"A timeline of recent acquisitions in the space",[781,788,789],{},"A growing database of MusicTech startups",[781,791,792],{},"A global calendar of upcoming events and webinars",[781,794,795],{},"A curated list of open-source projects",[781,797,798],{},"A Partner section featuring experts we trust in areas like fundraising, metadata, and market strategy",[749,800,801],{},"Each collection is designed to be practical, relevant, and easy to navigate - whether you're raising a round, looking for collaborators, or staying informed about emerging trends.",[770,803,805],{"id":804},"why-subscribe","Why subscribe?",[749,807,808],{},"To access the full platform, you’ll need to sign up. It’s free and gives you full access to all resources, along with a newsletter focused on real updates, fresh insights, and curated content that actually matters to MusicTech professionals.",[770,810,812],{"id":811},"whats-next","What’s next?",[749,814,815],{},"We’re already working on expanding the platform with new Collections. Our long-term goal is to build the most useful, up-to-date resource for anyone serious about MusicTech - whether you're a founder, investor, researcher, or part of a larger organization.",[749,817,818],{},"We know there are other ways to map this space. This is ours - and we’re excited to keep improving it.",[770,820,822],{"id":821},"built-with-care","Built with care",[749,824,825],{},"This project was made possible by the dedicated team at MusicTech Lab, with contributions from Mariusz Smenżyk, Klaudia Matysiak, Miłosz Jasica, Katarzyna Czerniak, and many others. We’re proud of what we’ve created, and we hope it helps move the industry forward.",[827,828,834],"div",{"className":829},[830,831,832,833],"flex","flex-wrap","gap-3","my-8",[835,836],"u-button",{"color":837,"label":838,"target":839,"to":761,"variant":840},"primary","Explore MusicTech Resources","_blank","subtle",{"title":842,"searchDepth":843,"depth":843,"links":844},"",2,[845,847,848,849],{"id":772,"depth":846,"text":773},3,{"id":804,"depth":846,"text":805},{"id":811,"depth":846,"text":812},{"id":821,"depth":846,"text":822},"music-data",null,"2025-06-03T00:00:00.000Z","A curated platform for MusicTech founders and teams. Track investor deals, explore open-source tools, and find partners and events in the music-tech space.","md",{"src":856},"/images/blog/musictechlab_blog_musictech-resources-curated-insights-for-the-musictech-builders.webp",{"enabled":858,"items":859},true,[860,863,866],{"text":861,"icon":862},"MusicTech Resources includes ~500 investor/startup deals and a global events calendar.","i-lucide-trending-up",{"text":864,"icon":865},"Free sign-up gives full access to startup databases, open source tools, and partner listings.","i-lucide-database",{"text":867,"icon":868},"Curated by MusicTech Lab for founders, investors, and professionals in the music tech space.","i-lucide-users",{},{"title":176,"description":853},[872,850],"musictech","yTVEwP5NsrQn4asZwtNXuaM8HNJtGN_-t6dBSa76Rxk",[875,877],{"title":172,"path":173,"stem":174,"description":876,"children":-1},"We built an open source MCP server that reads and writes metadata tags in MP3, FLAC, and OGG files directly from Claude Code. Here is how it works.",{"title":180,"path":181,"stem":182,"description":878,"children":-1},"Poland’s first Creative Tech report is here and MusicTech Lab’s Maciej Dulski shares insights on how music innovation in Poland is gaining momentum.",[880,2815,3502,4210],{"id":881,"title":84,"authors":882,"badge":888,"body":890,"category":850,"client":851,"date":2775,"description":2776,"extension":854,"faq":2777,"featured":69,"featuredOrder":851,"hidden":69,"image":2790,"keyTakeaways":2792,"meta":2805,"navigation":858,"path":85,"seo":2806,"status":851,"stem":86,"tags":2809,"teaser":851,"__hash__":2814,"score":993},"posts/blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama.md",[883],{"name":884,"to":885,"avatar":886},"Mariusz Smenżyk","https://www.linkedin.com/in/mariusz-smenzyk/",{"src":887},"/images/people/mariusz-smenzyk2.webp",{"label":5,"color":889},"#f59e0b",{"type":746,"value":891,"toc":2760},[892,895,898,903,906,909,927,930,936,940,943,946,960,963,1081,1103,1107,1118,1121,1480,1483,1634,1637,1648,1652,1655,1658,1678,1681,1819,1822,1828,1832,1835,1841,2403,2406,2437,2443,2631,2638,2641,2647,2651,2654,2657,2660,2697,2701,2704,2708,2711,2715,2718,2722,2725,2729,2732,2746,2749,2753,2756],[749,893,894],{},"Your data team knows the answer is in the database. Your A&R lead, your finance director, and your label manager do not know how to get it out. This is the gap that costs music companies real money, not in licensing fees or infrastructure, but in decisions delayed, trends missed, and reports that arrive a week too late.",[749,896,897],{},"We built an AI-powered analytics dashboard to close that gap. It sits inside MusicData Lab, our royalty analytics platform, and it lets anyone with access type a question in plain English and get a chart back in seconds.",[899,900,902],"h2",{"id":901},"the-problem-everybody-talks-about-at-conferences","The problem everybody talks about at conferences",[749,904,905],{},"The music industry has a data access problem. Not a data collection problem. Labels and distributors already have millions of rows of streaming data, royalty reports, and territorial breakdowns sitting in their databases. The bottleneck is getting from \"I need to know which retailers drove revenue last quarter\" to an actual answer.",[749,907,908],{},"Today, that journey typically looks like this:",[910,911,912,915,918,921,924],"ol",{},[781,913,914],{},"A business stakeholder writes an email or Slack message to the data team",[781,916,917],{},"The data team interprets the request, writes a SQL query, exports the results",[781,919,920],{},"Someone pastes the numbers into a spreadsheet and builds a chart",[781,922,923],{},"The chart goes back to the stakeholder, who asks a follow-up question",[781,925,926],{},"Repeat from step 1",[749,928,929],{},"This loop can take hours, sometimes days. Multiply it across every label, every territory, every reporting period, and you start to see the scale of the problem.",[931,932,933],"note",{},[749,934,935],{},"A 2024 IFPI report noted that the global recorded music market generated $28.6 billion in revenue, with streaming accounting for 67% of that. The volume of data behind those numbers is staggering, and growing every quarter.",[899,937,939],{"id":938},"what-if-business-users-could-just-ask","What if business users could just ask?",[749,941,942],{},"That was the design question behind our AI Dashboard. Instead of routing every data request through a technical team, what if the platform could understand a question like \"top 5 artists from the US by income\" and return a bar chart?",[749,944,945],{},"The workflow is simple:",[910,947,948,951,954,957],{},[781,949,950],{},"A user types a question in the chat interface",[781,952,953],{},"An LLM translates the question into a ClickHouse SQL query",[781,955,956],{},"The query runs against the analytics database (read-only, with safety guardrails)",[781,958,959],{},"Results come back as an interactive chart and a data table",[749,961,962],{},"No SQL knowledge required. No waiting for the data team. No spreadsheets.",[964,965,969],"pre",{"className":966,"code":967,"language":968,"meta":842,"style":842},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","sequenceDiagram\n    participant U as User\n    participant UI as Chat UI\n    participant LLM as LLM (Ollama/Claude)\n    participant SG as SQL Guard\n    participant CH as ClickHouse\n    participant CB as Chart Builder\n\n    U->>UI: \"Top 5 artists by income\"\n    UI->>LLM: System prompt + question\n    LLM-->>UI: Generated SQL query\n    UI->>SG: Validate SQL\n    SG-->>UI: Sanitised query (SELECT only, LIMIT enforced)\n    UI->>CH: Execute (read-only, 10s timeout)\n    CH-->>UI: Result rows + columns\n    UI->>CB: Build chart config\n    CB-->>UI: Chart.js config (type, labels, datasets)\n    UI-->>U: Interactive chart + data table\n","mermaid",[970,971,972,981,986,991,997,1003,1009,1015,1021,1027,1033,1039,1045,1051,1057,1063,1069,1075],"code",{"__ignoreMap":842},[973,974,977],"span",{"class":975,"line":976},"line",1,[973,978,980],{"class":979},"sTEyZ","sequenceDiagram\n",[973,982,983],{"class":975,"line":843},[973,984,985],{"class":979},"    participant U as User\n",[973,987,988],{"class":975,"line":846},[973,989,990],{"class":979},"    participant UI as Chat UI\n",[973,992,994],{"class":975,"line":993},4,[973,995,996],{"class":979},"    participant LLM as LLM (Ollama/Claude)\n",[973,998,1000],{"class":975,"line":999},5,[973,1001,1002],{"class":979},"    participant SG as SQL Guard\n",[973,1004,1006],{"class":975,"line":1005},6,[973,1007,1008],{"class":979},"    participant CH as ClickHouse\n",[973,1010,1012],{"class":975,"line":1011},7,[973,1013,1014],{"class":979},"    participant CB as Chart Builder\n",[973,1016,1018],{"class":975,"line":1017},8,[973,1019,1020],{"emptyLinePlaceholder":858},"\n",[973,1022,1024],{"class":975,"line":1023},9,[973,1025,1026],{"class":979},"    U->>UI: \"Top 5 artists by income\"\n",[973,1028,1030],{"class":975,"line":1029},10,[973,1031,1032],{"class":979},"    UI->>LLM: System prompt + question\n",[973,1034,1036],{"class":975,"line":1035},11,[973,1037,1038],{"class":979},"    LLM-->>UI: Generated SQL query\n",[973,1040,1042],{"class":975,"line":1041},12,[973,1043,1044],{"class":979},"    UI->>SG: Validate SQL\n",[973,1046,1048],{"class":975,"line":1047},13,[973,1049,1050],{"class":979},"    SG-->>UI: Sanitised query (SELECT only, LIMIT enforced)\n",[973,1052,1054],{"class":975,"line":1053},14,[973,1055,1056],{"class":979},"    UI->>CH: Execute (read-only, 10s timeout)\n",[973,1058,1060],{"class":975,"line":1059},15,[973,1061,1062],{"class":979},"    CH-->>UI: Result rows + columns\n",[973,1064,1066],{"class":975,"line":1065},16,[973,1067,1068],{"class":979},"    UI->>CB: Build chart config\n",[973,1070,1072],{"class":975,"line":1071},17,[973,1073,1074],{"class":979},"    CB-->>UI: Chart.js config (type, labels, datasets)\n",[973,1076,1078],{"class":975,"line":1077},18,[973,1079,1080],{"class":979},"    UI-->>U: Interactive chart + data table\n",[827,1082,1088,1094,1098],{"className":1083},[1084,1085,1086,1087,833],"grid","grid-cols-1","md:grid-cols-3","gap-4",[1089,1090],"spotlight-card",{"description":1091,"icon":1092,"title":1093},"Ask questions in plain English. No SQL, no training, no onboarding friction.","i-lucide-message-square","Natural Language Input",[1089,1095],{"description":1096,"icon":865,"title":1097},"Columnar storage handles millions of streaming records with sub-second query times.","ClickHouse Analytics",[1089,1099],{"description":1100,"icon":1101,"title":1102},"Results render as interactive charts. Bar, line, and doughnut, selected automatically.","i-lucide-bar-chart-3","Instant Visualization",[899,1104,1106],{"id":1105},"why-this-matters-for-music-companies-specifically","Why this matters for music companies specifically",[749,1108,1109,1110,1113,1114,1117],{},"Music royalty data is uniquely complex. A single label might receive reports from ",[759,1111,1112],{"href":77},"13 different distributors, each with its own file format",", column naming, and date conventions. Once that data is normalised and loaded into an analytics database, the schema reflects that complexity: dozens of fields covering artists, tracks, retailers, territories, currencies, and time periods. Even the ",[759,1115,1116],{"href":81},"retailer names need normalisation"," before they become queryable.",[749,1119,1120],{},"This is precisely the kind of dataset where AI-assisted querying shines. The system prompt includes the full database schema, domain-specific hints, and few-shot examples that teach the model how to write correct ClickHouse SQL. We build it dynamically from Django model metadata, so the prompt stays in sync with the schema automatically:",[964,1122,1127],{"className":1123,"code":1124,"filename":1125,"language":1126,"meta":842,"style":842},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","def build_system_prompt() -> str:\n    fields = []\n    for field in StreamDataCH._meta.get_fields():\n        fields.append(f\"  - {field.name} ({field.__class__.__name__})\")\n\n    schema_block = \"\\n\".join(fields)\n\n    return (\n        \"You are a SQL analyst for a music streaming analytics platform.\\n\"\n        f\"The database is ClickHouse. There is one table: `{TABLE}`.\\n\"\n        f\"\\n## Schema\\n\\n{schema_block}\\n\"\n        \"\\n## Domain hints\\n\\n\"\n        \"- `final_income` is income converted to the target currency\\n\"\n        \"- `retailer_union` is the normalized retailer name\\n\"\n        \"- Always filter out empty strings when grouping by text fields\\n\"\n        \"\\n## Output rules\\n\\n\"\n        \"1. Output ONLY a single SELECT statement\\n\"\n        \"2. Always include a LIMIT clause (max 100)\\n\"\n        \"3. Never use INSERT, UPDATE, DELETE, DROP, or any DDL\\n\"\n        f\"\\n## Examples\\n\\n{examples_block}\"\n    )\n","prompts.py","python",[970,1128,1129,1153,1164,1194,1254,1258,1286,1290,1298,1311,1333,1358,1371,1382,1393,1404,1417,1428,1439,1451,1474],{"__ignoreMap":842},[973,1130,1131,1135,1139,1143,1146,1150],{"class":975,"line":976},[973,1132,1134],{"class":1133},"spNyl","def",[973,1136,1138],{"class":1137},"s2Zo4"," build_system_prompt",[973,1140,1142],{"class":1141},"sMK4o","()",[973,1144,1145],{"class":1141}," ->",[973,1147,1149],{"class":1148},"sBMFI"," str",[973,1151,1152],{"class":1141},":\n",[973,1154,1155,1158,1161],{"class":975,"line":843},[973,1156,1157],{"class":979},"    fields ",[973,1159,1160],{"class":1141},"=",[973,1162,1163],{"class":1141}," []\n",[973,1165,1166,1170,1173,1176,1179,1182,1186,1188,1191],{"class":975,"line":846},[973,1167,1169],{"class":1168},"s7zQu","    for",[973,1171,1172],{"class":979}," field ",[973,1174,1175],{"class":1168},"in",[973,1177,1178],{"class":979}," StreamDataCH",[973,1180,1181],{"class":1141},".",[973,1183,1185],{"class":1184},"swJcz","_meta",[973,1187,1181],{"class":1141},[973,1189,1190],{"class":1137},"get_fields",[973,1192,1193],{"class":1141},"():\n",[973,1195,1196,1199,1201,1204,1207,1210,1214,1218,1221,1223,1226,1229,1232,1234,1236,1238,1241,1243,1246,1248,1251],{"class":975,"line":993},[973,1197,1198],{"class":979},"        fields",[973,1200,1181],{"class":1141},[973,1202,1203],{"class":1137},"append",[973,1205,1206],{"class":1141},"(",[973,1208,1209],{"class":1133},"f",[973,1211,1213],{"class":1212},"sfazB","\"  - ",[973,1215,1217],{"class":1216},"sbssI","{",[973,1219,1220],{"class":1137},"field",[973,1222,1181],{"class":1141},[973,1224,1225],{"class":1184},"name",[973,1227,1228],{"class":1216},"}",[973,1230,1231],{"class":1212}," (",[973,1233,1217],{"class":1216},[973,1235,1220],{"class":1137},[973,1237,1181],{"class":1141},[973,1239,1240],{"class":979},"__class__",[973,1242,1181],{"class":1141},[973,1244,1245],{"class":979},"__name__",[973,1247,1228],{"class":1216},[973,1249,1250],{"class":1212},")\"",[973,1252,1253],{"class":1141},")\n",[973,1255,1256],{"class":975,"line":999},[973,1257,1020],{"emptyLinePlaceholder":858},[973,1259,1260,1263,1265,1268,1271,1274,1276,1279,1281,1284],{"class":975,"line":1005},[973,1261,1262],{"class":979},"    schema_block ",[973,1264,1160],{"class":1141},[973,1266,1267],{"class":1141}," \"",[973,1269,1270],{"class":979},"\\n",[973,1272,1273],{"class":1141},"\"",[973,1275,1181],{"class":1141},[973,1277,1278],{"class":1137},"join",[973,1280,1206],{"class":1141},[973,1282,1283],{"class":1137},"fields",[973,1285,1253],{"class":1141},[973,1287,1288],{"class":975,"line":1011},[973,1289,1020],{"emptyLinePlaceholder":858},[973,1291,1292,1295],{"class":975,"line":1017},[973,1293,1294],{"class":1168},"    return",[973,1296,1297],{"class":1141}," (\n",[973,1299,1300,1303,1306,1308],{"class":975,"line":1023},[973,1301,1302],{"class":1141},"        \"",[973,1304,1305],{"class":1212},"You are a SQL analyst for a music streaming analytics platform.",[973,1307,1270],{"class":979},[973,1309,1310],{"class":1141},"\"\n",[973,1312,1313,1316,1319,1321,1324,1326,1329,1331],{"class":975,"line":1029},[973,1314,1315],{"class":1133},"        f",[973,1317,1318],{"class":1212},"\"The database is ClickHouse. There is one table: `",[973,1320,1217],{"class":1216},[973,1322,1323],{"class":979},"TABLE",[973,1325,1228],{"class":1216},[973,1327,1328],{"class":1212},"`.",[973,1330,1270],{"class":979},[973,1332,1310],{"class":1212},[973,1334,1335,1337,1339,1341,1344,1347,1349,1352,1354,1356],{"class":975,"line":1035},[973,1336,1315],{"class":1133},[973,1338,1273],{"class":1212},[973,1340,1270],{"class":979},[973,1342,1343],{"class":1212},"## Schema",[973,1345,1346],{"class":979},"\\n\\n",[973,1348,1217],{"class":1216},[973,1350,1351],{"class":979},"schema_block",[973,1353,1228],{"class":1216},[973,1355,1270],{"class":979},[973,1357,1310],{"class":1212},[973,1359,1360,1362,1364,1367,1369],{"class":975,"line":1041},[973,1361,1302],{"class":1141},[973,1363,1270],{"class":979},[973,1365,1366],{"class":1212},"## Domain hints",[973,1368,1346],{"class":979},[973,1370,1310],{"class":1141},[973,1372,1373,1375,1378,1380],{"class":975,"line":1047},[973,1374,1302],{"class":1141},[973,1376,1377],{"class":1212},"- `final_income` is income converted to the target currency",[973,1379,1270],{"class":979},[973,1381,1310],{"class":1141},[973,1383,1384,1386,1389,1391],{"class":975,"line":1053},[973,1385,1302],{"class":1141},[973,1387,1388],{"class":1212},"- `retailer_union` is the normalized retailer name",[973,1390,1270],{"class":979},[973,1392,1310],{"class":1141},[973,1394,1395,1397,1400,1402],{"class":975,"line":1059},[973,1396,1302],{"class":1141},[973,1398,1399],{"class":1212},"- Always filter out empty strings when grouping by text fields",[973,1401,1270],{"class":979},[973,1403,1310],{"class":1141},[973,1405,1406,1408,1410,1413,1415],{"class":975,"line":1065},[973,1407,1302],{"class":1141},[973,1409,1270],{"class":979},[973,1411,1412],{"class":1212},"## Output rules",[973,1414,1346],{"class":979},[973,1416,1310],{"class":1141},[973,1418,1419,1421,1424,1426],{"class":975,"line":1071},[973,1420,1302],{"class":1141},[973,1422,1423],{"class":1212},"1. Output ONLY a single SELECT statement",[973,1425,1270],{"class":979},[973,1427,1310],{"class":1141},[973,1429,1430,1432,1435,1437],{"class":975,"line":1077},[973,1431,1302],{"class":1141},[973,1433,1434],{"class":1212},"2. Always include a LIMIT clause (max 100)",[973,1436,1270],{"class":979},[973,1438,1310],{"class":1141},[973,1440,1442,1444,1447,1449],{"class":975,"line":1441},19,[973,1443,1302],{"class":1141},[973,1445,1446],{"class":1212},"3. Never use INSERT, UPDATE, DELETE, DROP, or any DDL",[973,1448,1270],{"class":979},[973,1450,1310],{"class":1141},[973,1452,1454,1456,1458,1460,1463,1465,1467,1470,1472],{"class":975,"line":1453},20,[973,1455,1315],{"class":1133},[973,1457,1273],{"class":1212},[973,1459,1270],{"class":979},[973,1461,1462],{"class":1212},"## Examples",[973,1464,1346],{"class":979},[973,1466,1217],{"class":1216},[973,1468,1469],{"class":979},"examples_block",[973,1471,1228],{"class":1216},[973,1473,1310],{"class":1212},[973,1475,1477],{"class":975,"line":1476},21,[973,1478,1479],{"class":1141},"    )\n",[749,1481,1482],{},"The few-shot examples are critical. They teach the model patterns like subqueries for hierarchical groupings (\"top 5 artists with their retailers by income\") that a generic LLM would otherwise flatten into a single GROUP BY:",[964,1484,1488],{"className":1485,"code":1486,"language":1487,"meta":842,"style":842},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","-- Example: top 5 artists with their retailers by income\nSELECT artist, retailer_union, sum(final_income) AS total_income\nFROM analytics_streamdatach\nWHERE artist != '' AND retailer_union != ''\n  AND artist IN (\n    SELECT artist FROM analytics_streamdatach\n    WHERE artist != ''\n    GROUP BY artist ORDER BY sum(final_income) DESC LIMIT 5\n  )\nGROUP BY artist, retailer_union\nORDER BY artist, total_income DESC LIMIT 100\n","sql",[970,1489,1490,1496,1516,1524,1549,1561,1572,1583,1607,1612,1620],{"__ignoreMap":842},[973,1491,1492],{"class":975,"line":976},[973,1493,1495],{"class":1494},"sHwdD","-- Example: top 5 artists with their retailers by income\n",[973,1497,1498,1501,1504,1507,1510,1513],{"class":975,"line":843},[973,1499,1500],{"class":1216},"SELECT",[973,1502,1503],{"class":979}," artist, retailer_union, ",[973,1505,1506],{"class":1137},"sum",[973,1508,1509],{"class":979},"(final_income) ",[973,1511,1512],{"class":1216},"AS",[973,1514,1515],{"class":979}," total_income\n",[973,1517,1518,1521],{"class":975,"line":846},[973,1519,1520],{"class":1216},"FROM",[973,1522,1523],{"class":979}," analytics_streamdatach\n",[973,1525,1526,1529,1532,1535,1538,1541,1544,1546],{"class":975,"line":993},[973,1527,1528],{"class":1216},"WHERE",[973,1530,1531],{"class":979}," artist ",[973,1533,1534],{"class":1141},"!=",[973,1536,1537],{"class":1141}," ''",[973,1539,1540],{"class":1216}," AND",[973,1542,1543],{"class":979}," retailer_union ",[973,1545,1534],{"class":1141},[973,1547,1548],{"class":1141}," ''\n",[973,1550,1551,1554,1556,1559],{"class":975,"line":999},[973,1552,1553],{"class":1216},"  AND",[973,1555,1531],{"class":979},[973,1557,1558],{"class":1216},"IN",[973,1560,1297],{"class":979},[973,1562,1563,1566,1568,1570],{"class":975,"line":1005},[973,1564,1565],{"class":1216},"    SELECT",[973,1567,1531],{"class":979},[973,1569,1520],{"class":1216},[973,1571,1523],{"class":979},[973,1573,1574,1577,1579,1581],{"class":975,"line":1011},[973,1575,1576],{"class":1216},"    WHERE",[973,1578,1531],{"class":979},[973,1580,1534],{"class":1141},[973,1582,1548],{"class":1141},[973,1584,1585,1588,1590,1593,1596,1598,1601,1604],{"class":975,"line":1017},[973,1586,1587],{"class":1216},"    GROUP BY",[973,1589,1531],{"class":979},[973,1591,1592],{"class":1216},"ORDER BY",[973,1594,1595],{"class":1137}," sum",[973,1597,1509],{"class":979},[973,1599,1600],{"class":1216},"DESC",[973,1602,1603],{"class":1216}," LIMIT",[973,1605,1606],{"class":1216}," 5\n",[973,1608,1609],{"class":975,"line":1023},[973,1610,1611],{"class":979},"  )\n",[973,1613,1614,1617],{"class":975,"line":1029},[973,1615,1616],{"class":1216},"GROUP BY",[973,1618,1619],{"class":979}," artist, retailer_union\n",[973,1621,1622,1624,1627,1629,1631],{"class":975,"line":1035},[973,1623,1592],{"class":1216},[973,1625,1626],{"class":979}," artist, total_income ",[973,1628,1600],{"class":1216},[973,1630,1603],{"class":1216},[973,1632,1633],{"class":1216}," 100\n",[749,1635,1636],{},"The result is that even someone who has never seen a database can ask:",[778,1638,1639,1642,1645],{},[781,1640,1641],{},"\"Monthly income trend for 2024\" and get a line chart",[781,1643,1644],{},"\"Income by country\" and get a ranked bar chart",[781,1646,1647],{},"\"Top 10 tracks by streams\" and get a sortable table with the data",[899,1649,1651],{"id":1650},"data-privacy-keeping-everything-local","Data privacy: keeping everything local",[749,1653,1654],{},"Here is where most \"AI analytics\" solutions stumble. They require sending your data, or at least your queries, to a third-party API. For a music company handling confidential royalty data, artist earnings, and pre-release catalogue information, that is often a non-starter.",[749,1656,1657],{},"Our architecture solves this with a pluggable LLM design:",[827,1659,1662,1670],{"className":1660},[1084,1085,1661,1087,833],"md:grid-cols-2",[1089,1663,1667],{"description":1664,"icon":1665,"title":1666},"Open-source LLM runtime running in Docker alongside the application. All inference happens on your infrastructure. Zero data exposure.","i-lucide-server","Ollama (Local, Default)",[749,1668,1669],{},"Recommended for production environments handling sensitive royalty data. Runs models like Mistral 7B locally with 4GB of memory.",[1089,1671,1675],{"description":1672,"icon":1673,"title":1674},"Anthropic's Claude API for higher-quality SQL generation. Swap in with a single environment variable change.","i-lucide-cloud","Claude API (Optional)",[749,1676,1677],{},"Useful for development, testing, or environments where data sensitivity is lower and query accuracy is the priority.",[749,1679,1680],{},"Switching between providers is a configuration change, not a code change. The factory pattern reads a single setting and returns the right client:",[964,1682,1685],{"className":1123,"code":1683,"filename":1684,"language":1126,"meta":842,"style":842},"def get_llm_provider() -> LLMProvider:\n    provider = getattr(settings, \"AI_DASHBOARD_PROVIDER\", \"ollama\")\n\n    if provider == \"claude\":\n        from .claude import ClaudeProvider\n        return ClaudeProvider()\n\n    from .ollama import OllamaProvider\n    return OllamaProvider()\n","factory.py",[970,1686,1687,1703,1739,1743,1763,1780,1791,1795,1810],{"__ignoreMap":842},[973,1688,1689,1691,1694,1696,1698,1701],{"class":975,"line":976},[973,1690,1134],{"class":1133},[973,1692,1693],{"class":1137}," get_llm_provider",[973,1695,1142],{"class":1141},[973,1697,1145],{"class":1141},[973,1699,1700],{"class":979}," LLMProvider",[973,1702,1152],{"class":1141},[973,1704,1705,1708,1710,1713,1715,1718,1721,1723,1726,1728,1730,1732,1735,1737],{"class":975,"line":843},[973,1706,1707],{"class":979},"    provider ",[973,1709,1160],{"class":1141},[973,1711,1712],{"class":1137}," getattr",[973,1714,1206],{"class":1141},[973,1716,1717],{"class":1137},"settings",[973,1719,1720],{"class":1141},",",[973,1722,1267],{"class":1141},[973,1724,1725],{"class":1212},"AI_DASHBOARD_PROVIDER",[973,1727,1273],{"class":1141},[973,1729,1720],{"class":1141},[973,1731,1267],{"class":1141},[973,1733,1734],{"class":1212},"ollama",[973,1736,1273],{"class":1141},[973,1738,1253],{"class":1141},[973,1740,1741],{"class":975,"line":846},[973,1742,1020],{"emptyLinePlaceholder":858},[973,1744,1745,1748,1751,1754,1756,1759,1761],{"class":975,"line":993},[973,1746,1747],{"class":1168},"    if",[973,1749,1750],{"class":979}," provider ",[973,1752,1753],{"class":1141},"==",[973,1755,1267],{"class":1141},[973,1757,1758],{"class":1212},"claude",[973,1760,1273],{"class":1141},[973,1762,1152],{"class":1141},[973,1764,1765,1768,1771,1774,1777],{"class":975,"line":999},[973,1766,1767],{"class":1168},"        from",[973,1769,1770],{"class":1141}," .",[973,1772,1773],{"class":979},"claude ",[973,1775,1776],{"class":1168},"import",[973,1778,1779],{"class":979}," ClaudeProvider\n",[973,1781,1782,1785,1788],{"class":975,"line":1005},[973,1783,1784],{"class":1168},"        return",[973,1786,1787],{"class":1137}," ClaudeProvider",[973,1789,1790],{"class":1141},"()\n",[973,1792,1793],{"class":975,"line":1011},[973,1794,1020],{"emptyLinePlaceholder":858},[973,1796,1797,1800,1802,1805,1807],{"class":975,"line":1017},[973,1798,1799],{"class":1168},"    from",[973,1801,1770],{"class":1141},[973,1803,1804],{"class":979},"ollama ",[973,1806,1776],{"class":1168},[973,1808,1809],{"class":979}," OllamaProvider\n",[973,1811,1812,1814,1817],{"class":975,"line":1023},[973,1813,1294],{"class":1168},[973,1815,1816],{"class":1137}," OllamaProvider",[973,1818,1790],{"class":1141},[749,1820,1821],{},"The same application code, the same security layer, the same chart rendering.",[1823,1824,1825],"tip",{},[749,1826,1827],{},"The pluggable pattern means you can start with Ollama for privacy, evaluate quality, and switch to Claude API for specific use cases without any migration work.",[899,1829,1831],{"id":1830},"security-by-design-not-by-afterthought","Security by design, not by afterthought",[749,1833,1834],{},"Letting an AI write SQL that runs against your production database sounds risky. It is, if you do it naively. Our approach layers multiple security controls:",[749,1836,1837,1840],{},[765,1838,1839],{},"SQL Guard"," validates every query before execution. Here is the core of it:",[964,1842,1845],{"className":1123,"code":1843,"filename":1844,"language":1126,"meta":842,"style":842},"BLOCKED_KEYWORDS = [\n    \"INSERT\", \"UPDATE\", \"DELETE\", \"DROP\", \"ALTER\",\n    \"TRUNCATE\", \"CREATE\", \"GRANT\", \"REVOKE\",\n    \"ATTACH\", \"DETACH\", \"RENAME\", \"OPTIMIZE\", \"KILL\",\n]\n\nclass SQLGuard:\n    @staticmethod\n    def validate(sql: str) -> str:\n        sql = sql.rstrip(\";\").strip()\n\n        if not sql.upper().startswith(\"SELECT\"):\n            raise SQLGuardError(\"Only SELECT queries are allowed\")\n        if \";\" in sql:\n            raise SQLGuardError(\"Multiple statements are not allowed\")\n\n        sql_upper = sql.upper()\n        for keyword in BLOCKED_KEYWORDS:\n            if re.search(rf\"\\b{keyword}\\b\", sql_upper):\n                raise SQLGuardError(f\"Forbidden keyword: {keyword}\")\n\n        # Only allow the analytics table\n        for table in re.findall(r\"(?:FROM|JOIN)\\s+(\\w+)\", sql_upper):\n            if table.lower() != ALLOWED_TABLE:\n                raise SQLGuardError(f\"Table '{table}' is not allowed\")\n\n        # Enforce LIMIT \u003C= 100\n        return _enforce_limit(sql, sql_upper)\n","guard.py",[970,1846,1847,1857,1906,1944,1991,1996,2000,2010,2018,2045,2077,2081,2113,2132,2149,2166,2170,2185,2200,2238,2262,2266,2272,2325,2348,2374,2379,2385],{"__ignoreMap":842},[973,1848,1849,1852,1854],{"class":975,"line":976},[973,1850,1851],{"class":979},"BLOCKED_KEYWORDS ",[973,1853,1160],{"class":1141},[973,1855,1856],{"class":1141}," [\n",[973,1858,1859,1862,1865,1867,1869,1871,1874,1876,1878,1880,1883,1885,1887,1889,1892,1894,1896,1898,1901,1903],{"class":975,"line":843},[973,1860,1861],{"class":1141},"    \"",[973,1863,1864],{"class":1212},"INSERT",[973,1866,1273],{"class":1141},[973,1868,1720],{"class":1141},[973,1870,1267],{"class":1141},[973,1872,1873],{"class":1212},"UPDATE",[973,1875,1273],{"class":1141},[973,1877,1720],{"class":1141},[973,1879,1267],{"class":1141},[973,1881,1882],{"class":1212},"DELETE",[973,1884,1273],{"class":1141},[973,1886,1720],{"class":1141},[973,1888,1267],{"class":1141},[973,1890,1891],{"class":1212},"DROP",[973,1893,1273],{"class":1141},[973,1895,1720],{"class":1141},[973,1897,1267],{"class":1141},[973,1899,1900],{"class":1212},"ALTER",[973,1902,1273],{"class":1141},[973,1904,1905],{"class":1141},",\n",[973,1907,1908,1910,1913,1915,1917,1919,1922,1924,1926,1928,1931,1933,1935,1937,1940,1942],{"class":975,"line":846},[973,1909,1861],{"class":1141},[973,1911,1912],{"class":1212},"TRUNCATE",[973,1914,1273],{"class":1141},[973,1916,1720],{"class":1141},[973,1918,1267],{"class":1141},[973,1920,1921],{"class":1212},"CREATE",[973,1923,1273],{"class":1141},[973,1925,1720],{"class":1141},[973,1927,1267],{"class":1141},[973,1929,1930],{"class":1212},"GRANT",[973,1932,1273],{"class":1141},[973,1934,1720],{"class":1141},[973,1936,1267],{"class":1141},[973,1938,1939],{"class":1212},"REVOKE",[973,1941,1273],{"class":1141},[973,1943,1905],{"class":1141},[973,1945,1946,1948,1951,1953,1955,1957,1960,1962,1964,1966,1969,1971,1973,1975,1978,1980,1982,1984,1987,1989],{"class":975,"line":993},[973,1947,1861],{"class":1141},[973,1949,1950],{"class":1212},"ATTACH",[973,1952,1273],{"class":1141},[973,1954,1720],{"class":1141},[973,1956,1267],{"class":1141},[973,1958,1959],{"class":1212},"DETACH",[973,1961,1273],{"class":1141},[973,1963,1720],{"class":1141},[973,1965,1267],{"class":1141},[973,1967,1968],{"class":1212},"RENAME",[973,1970,1273],{"class":1141},[973,1972,1720],{"class":1141},[973,1974,1267],{"class":1141},[973,1976,1977],{"class":1212},"OPTIMIZE",[973,1979,1273],{"class":1141},[973,1981,1720],{"class":1141},[973,1983,1267],{"class":1141},[973,1985,1986],{"class":1212},"KILL",[973,1988,1273],{"class":1141},[973,1990,1905],{"class":1141},[973,1992,1993],{"class":975,"line":999},[973,1994,1995],{"class":1141},"]\n",[973,1997,1998],{"class":975,"line":1005},[973,1999,1020],{"emptyLinePlaceholder":858},[973,2001,2002,2005,2008],{"class":975,"line":1011},[973,2003,2004],{"class":1133},"class",[973,2006,2007],{"class":1148}," SQLGuard",[973,2009,1152],{"class":1141},[973,2011,2012,2015],{"class":975,"line":1017},[973,2013,2014],{"class":1141},"    @",[973,2016,2017],{"class":1148},"staticmethod\n",[973,2019,2020,2023,2026,2028,2031,2034,2036,2039,2041,2043],{"class":975,"line":1023},[973,2021,2022],{"class":1133},"    def",[973,2024,2025],{"class":1137}," validate",[973,2027,1206],{"class":1141},[973,2029,1487],{"class":2030},"sHdIc",[973,2032,2033],{"class":1141},":",[973,2035,1149],{"class":1148},[973,2037,2038],{"class":1141},")",[973,2040,1145],{"class":1141},[973,2042,1149],{"class":1148},[973,2044,1152],{"class":1141},[973,2046,2047,2050,2052,2055,2057,2060,2062,2064,2067,2069,2072,2075],{"class":975,"line":1029},[973,2048,2049],{"class":979},"        sql ",[973,2051,1160],{"class":1141},[973,2053,2054],{"class":979}," sql",[973,2056,1181],{"class":1141},[973,2058,2059],{"class":1137},"rstrip",[973,2061,1206],{"class":1141},[973,2063,1273],{"class":1141},[973,2065,2066],{"class":1212},";",[973,2068,1273],{"class":1141},[973,2070,2071],{"class":1141},").",[973,2073,2074],{"class":1137},"strip",[973,2076,1790],{"class":1141},[973,2078,2079],{"class":975,"line":1035},[973,2080,1020],{"emptyLinePlaceholder":858},[973,2082,2083,2086,2089,2091,2093,2096,2099,2102,2104,2106,2108,2110],{"class":975,"line":1041},[973,2084,2085],{"class":1168},"        if",[973,2087,2088],{"class":1141}," not",[973,2090,2054],{"class":979},[973,2092,1181],{"class":1141},[973,2094,2095],{"class":1137},"upper",[973,2097,2098],{"class":1141},"().",[973,2100,2101],{"class":1137},"startswith",[973,2103,1206],{"class":1141},[973,2105,1273],{"class":1141},[973,2107,1500],{"class":1212},[973,2109,1273],{"class":1141},[973,2111,2112],{"class":1141},"):\n",[973,2114,2115,2118,2121,2123,2125,2128,2130],{"class":975,"line":1047},[973,2116,2117],{"class":1168},"            raise",[973,2119,2120],{"class":1137}," SQLGuardError",[973,2122,1206],{"class":1141},[973,2124,1273],{"class":1141},[973,2126,2127],{"class":1212},"Only SELECT queries are allowed",[973,2129,1273],{"class":1141},[973,2131,1253],{"class":1141},[973,2133,2134,2136,2138,2140,2142,2145,2147],{"class":975,"line":1053},[973,2135,2085],{"class":1168},[973,2137,1267],{"class":1141},[973,2139,2066],{"class":1212},[973,2141,1273],{"class":1141},[973,2143,2144],{"class":1141}," in",[973,2146,2054],{"class":979},[973,2148,1152],{"class":1141},[973,2150,2151,2153,2155,2157,2159,2162,2164],{"class":975,"line":1059},[973,2152,2117],{"class":1168},[973,2154,2120],{"class":1137},[973,2156,1206],{"class":1141},[973,2158,1273],{"class":1141},[973,2160,2161],{"class":1212},"Multiple statements are not allowed",[973,2163,1273],{"class":1141},[973,2165,1253],{"class":1141},[973,2167,2168],{"class":975,"line":1065},[973,2169,1020],{"emptyLinePlaceholder":858},[973,2171,2172,2175,2177,2179,2181,2183],{"class":975,"line":1071},[973,2173,2174],{"class":979},"        sql_upper ",[973,2176,1160],{"class":1141},[973,2178,2054],{"class":979},[973,2180,1181],{"class":1141},[973,2182,2095],{"class":1137},[973,2184,1790],{"class":1141},[973,2186,2187,2190,2193,2195,2198],{"class":975,"line":1077},[973,2188,2189],{"class":1168},"        for",[973,2191,2192],{"class":979}," keyword ",[973,2194,1175],{"class":1168},[973,2196,2197],{"class":979}," BLOCKED_KEYWORDS",[973,2199,1152],{"class":1141},[973,2201,2202,2205,2208,2210,2213,2215,2218,2221,2223,2226,2228,2231,2233,2236],{"class":975,"line":1441},[973,2203,2204],{"class":1168},"            if",[973,2206,2207],{"class":979}," re",[973,2209,1181],{"class":1141},[973,2211,2212],{"class":1137},"search",[973,2214,1206],{"class":1141},[973,2216,2217],{"class":1133},"rf",[973,2219,2220],{"class":1212},"\"\\b",[973,2222,1217],{"class":1216},[973,2224,2225],{"class":1137},"keyword",[973,2227,1228],{"class":1216},[973,2229,2230],{"class":1212},"\\b\"",[973,2232,1720],{"class":1141},[973,2234,2235],{"class":1137}," sql_upper",[973,2237,2112],{"class":1141},[973,2239,2240,2243,2245,2247,2249,2252,2254,2256,2258,2260],{"class":975,"line":1453},[973,2241,2242],{"class":1168},"                raise",[973,2244,2120],{"class":1137},[973,2246,1206],{"class":1141},[973,2248,1209],{"class":1133},[973,2250,2251],{"class":1212},"\"Forbidden keyword: ",[973,2253,1217],{"class":1216},[973,2255,2225],{"class":1137},[973,2257,1228],{"class":1216},[973,2259,1273],{"class":1212},[973,2261,1253],{"class":1141},[973,2263,2264],{"class":975,"line":1476},[973,2265,1020],{"emptyLinePlaceholder":858},[973,2267,2269],{"class":975,"line":2268},22,[973,2270,2271],{"class":1494},"        # Only allow the analytics table\n",[973,2273,2275,2277,2280,2282,2284,2286,2289,2291,2294,2297,2299,2302,2305,2307,2310,2313,2316,2319,2321,2323],{"class":975,"line":2274},23,[973,2276,2189],{"class":1168},[973,2278,2279],{"class":979}," table ",[973,2281,1175],{"class":1168},[973,2283,2207],{"class":979},[973,2285,1181],{"class":1141},[973,2287,2288],{"class":1137},"findall",[973,2290,1206],{"class":1141},[973,2292,2293],{"class":1133},"r",[973,2295,2296],{"class":1141},"\"(?:",[973,2298,1520],{"class":1212},[973,2300,2301],{"class":1141},"|",[973,2303,2304],{"class":1212},"JOIN",[973,2306,2038],{"class":1141},[973,2308,2309],{"class":1212},"\\s",[973,2311,2312],{"class":1141},"+(",[973,2314,2315],{"class":1212},"\\w",[973,2317,2318],{"class":1141},"+)\"",[973,2320,1720],{"class":1141},[973,2322,2235],{"class":1137},[973,2324,2112],{"class":1141},[973,2326,2328,2330,2333,2335,2338,2340,2343,2346],{"class":975,"line":2327},24,[973,2329,2204],{"class":1168},[973,2331,2332],{"class":979}," table",[973,2334,1181],{"class":1141},[973,2336,2337],{"class":1137},"lower",[973,2339,1142],{"class":1141},[973,2341,2342],{"class":1141}," !=",[973,2344,2345],{"class":979}," ALLOWED_TABLE",[973,2347,1152],{"class":1141},[973,2349,2351,2353,2355,2357,2359,2362,2364,2367,2369,2372],{"class":975,"line":2350},25,[973,2352,2242],{"class":1168},[973,2354,2120],{"class":1137},[973,2356,1206],{"class":1141},[973,2358,1209],{"class":1133},[973,2360,2361],{"class":1212},"\"Table '",[973,2363,1217],{"class":1216},[973,2365,2366],{"class":1137},"table",[973,2368,1228],{"class":1216},[973,2370,2371],{"class":1212},"' is not allowed\"",[973,2373,1253],{"class":1141},[973,2375,2377],{"class":975,"line":2376},26,[973,2378,1020],{"emptyLinePlaceholder":858},[973,2380,2382],{"class":975,"line":2381},27,[973,2383,2384],{"class":1494},"        # Enforce LIMIT \u003C= 100\n",[973,2386,2388,2390,2393,2395,2397,2399,2401],{"class":975,"line":2387},28,[973,2389,1784],{"class":1168},[973,2391,2392],{"class":1137}," _enforce_limit",[973,2394,1206],{"class":1141},[973,2396,1487],{"class":1137},[973,2398,1720],{"class":1141},[973,2400,2235],{"class":1137},[973,2402,1253],{"class":1141},[749,2404,2405],{},"Key constraints enforced:",[778,2407,2408,2424,2427,2434],{},[781,2409,2410,2411,2413,2414,2416,2417,2416,2419,2416,2421,2423],{},"Only ",[970,2412,1500],{}," statements are allowed. Any ",[970,2415,1864],{},", ",[970,2418,1873],{},[970,2420,1882],{},[970,2422,1891],{},", or DDL keyword is blocked",[781,2425,2426],{},"Queries are restricted to a single analytics table (no access to user accounts, credentials, or other application data)",[781,2428,2429,2430,2433],{},"Result sets are capped at 100 rows, with ",[970,2431,2432],{},"LIMIT"," enforced automatically",[781,2435,2436],{},"Comments, semicolons, and multi-statement queries are stripped or rejected",[749,2438,2439,2442],{},[765,2440,2441],{},"Read-only execution"," provides a second layer. The ClickHouse query runs over HTTP with safety parameters baked into every request:",[964,2444,2447],{"className":1123,"code":2445,"filename":2446,"language":1126,"meta":842,"style":842},"response = httpx.post(\n    f\"http://{host}:{port}\",\n    params={\n        \"query\": f\"{sql} FORMAT JSON\",\n        \"database\": \"default\",\n        \"readonly\": \"1\",\n        \"max_execution_time\": \"10\",\n        \"max_result_rows\": \"100\",\n    },\n    timeout=15,\n)\n","executor.py",[970,2448,2449,2467,2495,2503,2530,2550,2570,2590,2610,2615,2627],{"__ignoreMap":842},[973,2450,2451,2454,2456,2459,2461,2464],{"class":975,"line":976},[973,2452,2453],{"class":979},"response ",[973,2455,1160],{"class":1141},[973,2457,2458],{"class":979}," httpx",[973,2460,1181],{"class":1141},[973,2462,2463],{"class":1137},"post",[973,2465,2466],{"class":1141},"(\n",[973,2468,2469,2472,2475,2477,2480,2482,2484,2486,2489,2491,2493],{"class":975,"line":843},[973,2470,2471],{"class":1133},"    f",[973,2473,2474],{"class":1212},"\"http://",[973,2476,1217],{"class":1216},[973,2478,2479],{"class":1137},"host",[973,2481,1228],{"class":1216},[973,2483,2033],{"class":1212},[973,2485,1217],{"class":1216},[973,2487,2488],{"class":1137},"port",[973,2490,1228],{"class":1216},[973,2492,1273],{"class":1212},[973,2494,1905],{"class":1141},[973,2496,2497,2500],{"class":975,"line":846},[973,2498,2499],{"class":2030},"    params",[973,2501,2502],{"class":1141},"={\n",[973,2504,2505,2507,2510,2512,2514,2517,2519,2521,2523,2525,2528],{"class":975,"line":993},[973,2506,1302],{"class":1141},[973,2508,2509],{"class":1212},"query",[973,2511,1273],{"class":1141},[973,2513,2033],{"class":1141},[973,2515,2516],{"class":1133}," f",[973,2518,1273],{"class":1212},[973,2520,1217],{"class":1216},[973,2522,1487],{"class":1137},[973,2524,1228],{"class":1216},[973,2526,2527],{"class":1212}," FORMAT JSON\"",[973,2529,1905],{"class":1141},[973,2531,2532,2534,2537,2539,2541,2543,2546,2548],{"class":975,"line":999},[973,2533,1302],{"class":1141},[973,2535,2536],{"class":1212},"database",[973,2538,1273],{"class":1141},[973,2540,2033],{"class":1141},[973,2542,1267],{"class":1141},[973,2544,2545],{"class":1212},"default",[973,2547,1273],{"class":1141},[973,2549,1905],{"class":1141},[973,2551,2552,2554,2557,2559,2561,2563,2566,2568],{"class":975,"line":1005},[973,2553,1302],{"class":1141},[973,2555,2556],{"class":1212},"readonly",[973,2558,1273],{"class":1141},[973,2560,2033],{"class":1141},[973,2562,1267],{"class":1141},[973,2564,2565],{"class":1212},"1",[973,2567,1273],{"class":1141},[973,2569,1905],{"class":1141},[973,2571,2572,2574,2577,2579,2581,2583,2586,2588],{"class":975,"line":1011},[973,2573,1302],{"class":1141},[973,2575,2576],{"class":1212},"max_execution_time",[973,2578,1273],{"class":1141},[973,2580,2033],{"class":1141},[973,2582,1267],{"class":1141},[973,2584,2585],{"class":1212},"10",[973,2587,1273],{"class":1141},[973,2589,1905],{"class":1141},[973,2591,2592,2594,2597,2599,2601,2603,2606,2608],{"class":975,"line":1017},[973,2593,1302],{"class":1141},[973,2595,2596],{"class":1212},"max_result_rows",[973,2598,1273],{"class":1141},[973,2600,2033],{"class":1141},[973,2602,1267],{"class":1141},[973,2604,2605],{"class":1212},"100",[973,2607,1273],{"class":1141},[973,2609,1905],{"class":1141},[973,2611,2612],{"class":975,"line":1023},[973,2613,2614],{"class":1141},"    },\n",[973,2616,2617,2620,2622,2625],{"class":975,"line":1029},[973,2618,2619],{"class":2030},"    timeout",[973,2621,1160],{"class":1141},[973,2623,2624],{"class":1216},"15",[973,2626,1905],{"class":1141},[973,2628,2629],{"class":975,"line":1035},[973,2630,1253],{"class":1141},[749,2632,2633,2634,2637],{},"Even if SQL Guard missed something, ClickHouse itself would block writes (",[970,2635,2636],{},"readonly=1","), kill slow queries (10 seconds), and cap the result set.",[749,2639,2640],{},"This belt-and-suspenders approach means that even if the LLM generates a malicious query (unlikely, but possible), it cannot modify data, access unauthorised tables, or run expensive long-running operations.",[2642,2643,2644],"warning",{},[749,2645,2646],{},"No AI system should have write access to production data. Always enforce read-only execution at both the application layer and the database layer.",[899,2648,2650],{"id":2649},"why-clickhouse-for-music-analytics","Why ClickHouse for music analytics",[749,2652,2653],{},"We chose ClickHouse as the analytics engine for MusicData Lab because music streaming data is a textbook columnar analytics workload: append-only, time-series, high-volume, and query-heavy.",[749,2655,2656],{},"A typical label might have 10 to 50 million rows of streaming data, partitioned by month. Common queries aggregate by artist, retailer, territory, or time period. ClickHouse handles these in milliseconds where PostgreSQL would take seconds or minutes.",[749,2658,2659],{},"Key advantages for music data:",[778,2661,2662,2668,2685,2691],{},[781,2663,2664,2667],{},[765,2665,2666],{},"MergeTree engine"," with monthly partitioning matches the natural cadence of royalty reporting",[781,2669,2670,2673,2674,2416,2677,2680,2681,2684],{},[765,2671,2672],{},"Low-cardinality string optimisation"," is perfect for fields like ",[970,2675,2676],{},"retailer_union",[970,2678,2679],{},"artist",", and ",[970,2682,2683],{},"country_code"," that have a bounded set of values",[781,2686,2687,2690],{},[765,2688,2689],{},"Columnar compression"," keeps storage costs low even as data grows to hundreds of millions of rows",[781,2692,2693,2696],{},[765,2694,2695],{},"HTTP API"," makes it straightforward to build read-only query interfaces with proper access controls",[899,2698,2700],{"id":2699},"what-we-learned-building-this","What we learned building this",[749,2702,2703],{},"Three insights from the implementation that apply to any company considering AI-powered analytics:",[770,2705,2707],{"id":2706},"_1-the-prompt-is-the-product","1. The prompt is the product",[749,2709,2710],{},"The quality of SQL generation depends almost entirely on the system prompt. Including the full schema, domain-specific hints, and few-shot examples made the difference between \"sometimes works\" and \"reliably useful.\" As shown in the code above, we dynamically build the prompt from Django model metadata, so it stays in sync with schema changes automatically. No manual updates when a column is added or renamed.",[770,2712,2714],{"id":2713},"_2-start-local-scale-to-cloud","2. Start local, scale to cloud",[749,2716,2717],{},"Running Ollama locally for development and testing removed the biggest adoption barrier: \"we can't send data to an external API.\" Once stakeholders see the value, the conversation about using a cloud API for better quality becomes much easier.",[770,2719,2721],{"id":2720},"_3-security-is-a-feature-not-a-constraint","3. Security is a feature, not a constraint",[749,2723,2724],{},"The SQL Guard and read-only execution are not just safety nets. They are what made the business comfortable deploying this. When your CFO asks \"can this AI delete our data?\", the answer needs to be a confident \"no, here's why.\"",[899,2726,2728],{"id":2727},"who-is-this-for","Who is this for?",[749,2730,2731],{},"This is not a product launch. It is a proof of concept that we built for our own platform and for our clients. If you recognise any of these situations, it might be relevant to you:",[778,2733,2734,2737,2740,2743],{},[781,2735,2736],{},"Your data team spends too much time answering ad-hoc reporting requests",[781,2738,2739],{},"Business stakeholders wait days for insights that should take seconds",[781,2741,2742],{},"You have sensitive royalty or financial data and cannot use cloud-based AI tools",[781,2744,2745],{},"You already have analytics data in ClickHouse, PostgreSQL, or a similar database and want to make it more accessible",[749,2747,2748],{},"At MusicTech Lab, we build data platforms for the music industry. The AI Dashboard is one piece of a larger system that handles royalty ingestion, normalisation, currency conversion, and reporting. If this resonates, we should talk.",[899,2750,2752],{"id":2751},"what-comes-next","What comes next",[749,2754,2755],{},"This is a v1. The underlying pattern, natural language to SQL to visualisation, is not limited to music data. Any company with a structured analytics database can benefit from making that data conversational. The technology is ready. The question is whether your organisation is ready to let business users ask their own questions.",[2757,2758,2759],"style",{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":842,"searchDepth":843,"depth":843,"links":2761},[2762,2763,2764,2765,2766,2767,2768,2773,2774],{"id":901,"depth":843,"text":902},{"id":938,"depth":843,"text":939},{"id":1105,"depth":843,"text":1106},{"id":1650,"depth":843,"text":1651},{"id":1830,"depth":843,"text":1831},{"id":2649,"depth":843,"text":2650},{"id":2699,"depth":843,"text":2700,"children":2769},[2770,2771,2772],{"id":2706,"depth":846,"text":2707},{"id":2713,"depth":846,"text":2714},{"id":2720,"depth":846,"text":2721},{"id":2727,"depth":843,"text":2728},{"id":2751,"depth":843,"text":2752},"2026-03-24T00:00:00.000Z","Music royalty data is complex. Non-technical stakeholders need insights without SQL. Here's how we built an AI dashboard that turns plain English into charts.",[2778,2781,2784,2787],{"question":2779,"answer":2780},"Can the AI dashboard work without sending data to the cloud?","Yes. The default setup uses Ollama, an open-source local LLM runtime. All queries, data, and model inference stay on your own infrastructure. No streaming data, royalty figures, or artist names ever leave your servers.",{"question":2782,"answer":2783},"What kind of questions can non-technical users ask?","Users can ask plain English questions like 'top 5 artists by income', 'monthly revenue trend for 2024', or 'income by country'. The AI generates a ClickHouse SQL query, executes it safely, and returns a chart with the results.",{"question":2785,"answer":2786},"How does the system prevent dangerous SQL queries?","A dedicated SQL security layer validates every generated query before execution. It enforces SELECT-only access, restricts queries to a single analytics table, caps result sizes, and runs against a read-only ClickHouse connection with query timeouts.",{"question":2788,"answer":2789},"What technology stack powers the AI dashboard?","Django for the web framework, ClickHouse for columnar analytics, Ollama (or Claude API) for LLM inference, and Chart.js for visualization. The architecture is pluggable, so the LLM provider can be swapped without changing application code.",{"src":2791},"/images/blog/musictechlab_blog_ai-powered-analytics-dashboard.webp",{"enabled":858,"items":2793},[2794,2796,2799,2802],{"text":2795,"icon":1101},"Business users type questions in plain English and get charts back in seconds, no SQL needed.",{"text":2797,"icon":2798},"Ollama runs locally by default so no royalty data ever leaves your servers.","i-lucide-shield",{"text":2800,"icon":2801},"SQL Guard enforces SELECT-only, single-table access with a 100-row limit on every query.","i-lucide-lock",{"text":2803,"icon":2804},"ClickHouse handles 10-50M rows with sub-second query times for columnar analytics.","i-lucide-zap",{},{"title":2807,"description":2808},"AI Analytics Dashboard for Music Data | MusicTech Lab","How we built a generative AI dashboard that turns natural language into ClickHouse SQL and charts - with local LLM, zero data exposure.",[2810,872,2811,2812,850,2813],"ai-analytics","clickhouse","llm","data-visualization","pS6Y0h5-QTtmmgq3FGyIwCVS8U7L5KQD1FBN31L-WS8",{"id":2816,"title":80,"authors":2817,"badge":2820,"body":2821,"category":850,"client":851,"date":3467,"description":3468,"extension":854,"faq":3469,"featured":858,"featuredOrder":843,"hidden":69,"image":3479,"keyTakeaways":3481,"meta":3494,"navigation":858,"path":81,"seo":3495,"status":851,"stem":82,"tags":3498,"teaser":851,"__hash__":3501,"score":993},"posts/blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data.md",[2818],{"name":884,"to":885,"avatar":2819},{"src":887},{"label":5,"color":889},{"type":746,"value":2822,"toc":3456},[2823,2830,2833,2837,2840,2935,2942,2946,2953,3059,3062,3069,3080,3095,3098,3102,3105,3108,3113,3119,3123,3126,3130,3133,3217,3220,3224,3227,3293,3296,3300,3303,3433,3436,3440,3443,3450,3453],[749,2824,2825,2826,2829],{},"In our ",[759,2827,2828],{"href":77},"previous article",", we showed what a real label's download folder looks like - 18 distributors, 5 file formats, 500+ files per year. But solving the file format problem is only step one.",[749,2831,2832],{},"Open those files. The data inside is just as messy.",[899,2834,2836],{"id":2835},"the-name-problem","The name problem",[749,2838,2839],{},"Ask 18 distributors what \"Spotify\" is called. You'll get more answers than you expect:",[2366,2841,2842,2855],{},[2843,2844,2845],"thead",{},[2846,2847,2848,2852],"tr",{},[2849,2850,2851],"th",{},"Distributor",[2849,2853,2854],{},"How they label Spotify",[2856,2857,2858,2869,2879,2889,2899,2907,2916,2925],"tbody",{},[2846,2859,2860,2864],{},[2861,2862,2863],"td",{},"FUGA",[2861,2865,2866],{},[970,2867,2868],{},"Spotify",[2846,2870,2871,2874],{},[2861,2872,2873],{},"ADA",[2861,2875,2876],{},[970,2877,2878],{},"SPOTIFY",[2846,2880,2881,2884],{},[2861,2882,2883],{},"Ingrooves",[2861,2885,2886],{},[970,2887,2888],{},"spotify",[2846,2890,2891,2894],{},[2861,2892,2893],{},"The Orchard",[2861,2895,2896],{},[970,2897,2898],{},"Spotify Premium",[2846,2900,2901,2904],{},[2861,2902,2903],{},"Bandcamp",[2861,2905,2906],{},"N/A",[2846,2908,2909,2912],{},[2861,2910,2911],{},"MVD",[2861,2913,2914],{},[970,2915,2868],{},[2846,2917,2918,2921],{},[2861,2919,2920],{},"Emerald",[2861,2922,2923],{},[970,2924,2878],{},[2846,2926,2927,2930],{},[2861,2928,2929],{},"SFM",[2861,2931,2932],{},[970,2933,2934],{},"Spotify AB",[749,2936,2937,2938,2941],{},"That's just one platform. Now multiply across ",[765,2939,2940],{},"every retailer, every label name, and every service type"," in the dataset. The same entity appears under different names, different capitalizations, and different abbreviations depending on which distributor sent the file.",[899,2943,2945],{"id":2944},"_830-values-19-names","830 values, 19 names",[749,2947,2948,2949,2952],{},"The solution is what we call ",[765,2950,2951],{},"unions"," - normalization groups that map many raw values to one canonical name.",[827,2954,2957],{"className":2955},[830,2956,833],"justify-center",[964,2958,2960],{"className":966,"code":2959,"language":968,"meta":842,"style":842},"flowchart LR\n    subgraph raw[\"Raw Values\"]\n        A[\"Spotify\"]\n        B[\"SPOTIFY\"]\n        C[\"spotify\"]\n        D[\"Spotify Premium\"]\n        E[\"Spotify AB\"]\n        F[\"Spotify Ltd\"]\n    end\n\n    subgraph union[\"Union\"]\n        G([\"SPOTIFY\"])\n    end\n\n    A --> G\n    B --> G\n    C --> G\n    D --> G\n    E --> G\n    F --> G\n",[970,2961,2962,2967,2972,2977,2982,2987,2992,2997,3002,3007,3011,3016,3021,3025,3029,3034,3039,3044,3049,3054],{"__ignoreMap":842},[973,2963,2964],{"class":975,"line":976},[973,2965,2966],{"class":979},"flowchart LR\n",[973,2968,2969],{"class":975,"line":843},[973,2970,2971],{"class":979},"    subgraph raw[\"Raw Values\"]\n",[973,2973,2974],{"class":975,"line":846},[973,2975,2976],{"class":979},"        A[\"Spotify\"]\n",[973,2978,2979],{"class":975,"line":993},[973,2980,2981],{"class":979},"        B[\"SPOTIFY\"]\n",[973,2983,2984],{"class":975,"line":999},[973,2985,2986],{"class":979},"        C[\"spotify\"]\n",[973,2988,2989],{"class":975,"line":1005},[973,2990,2991],{"class":979},"        D[\"Spotify Premium\"]\n",[973,2993,2994],{"class":975,"line":1011},[973,2995,2996],{"class":979},"        E[\"Spotify AB\"]\n",[973,2998,2999],{"class":975,"line":1017},[973,3000,3001],{"class":979},"        F[\"Spotify Ltd\"]\n",[973,3003,3004],{"class":975,"line":1023},[973,3005,3006],{"class":979},"    end\n",[973,3008,3009],{"class":975,"line":1029},[973,3010,1020],{"emptyLinePlaceholder":858},[973,3012,3013],{"class":975,"line":1035},[973,3014,3015],{"class":979},"    subgraph union[\"Union\"]\n",[973,3017,3018],{"class":975,"line":1041},[973,3019,3020],{"class":979},"        G([\"SPOTIFY\"])\n",[973,3022,3023],{"class":975,"line":1047},[973,3024,3006],{"class":979},[973,3026,3027],{"class":975,"line":1053},[973,3028,1020],{"emptyLinePlaceholder":858},[973,3030,3031],{"class":975,"line":1059},[973,3032,3033],{"class":979},"    A --> G\n",[973,3035,3036],{"class":975,"line":1065},[973,3037,3038],{"class":979},"    B --> G\n",[973,3040,3041],{"class":975,"line":1071},[973,3042,3043],{"class":979},"    C --> G\n",[973,3045,3046],{"class":975,"line":1077},[973,3047,3048],{"class":979},"    D --> G\n",[973,3050,3051],{"class":975,"line":1441},[973,3052,3053],{"class":979},"    E --> G\n",[973,3055,3056],{"class":975,"line":1453},[973,3057,3058],{"class":979},"    F --> G\n",[749,3060,3061],{},"Here's what the union management interface looks like in practice - TIDAL alone has 10 variations, SoundCloud over 30:",[749,3063,3064],{},[3065,3066],"img",{"alt":3067,"src":3068},"Union management interface showing raw value mappings","/images/blog/musictechlab_blog_unions-admin-interface.webp",[749,3070,3071,3072,3075,3076,3079],{},"In production, this system maps ",[765,3073,3074],{},"830 raw tag values"," to just ",[765,3077,3078],{},"19 unions"," across three dimensions:",[827,3081,3083,3087,3091],{"className":3082},[1084,1085,1086,1087,833],[1089,3084],{"description":3085,"title":3086},"Spotify, Amazon, YouTube, Pandora, Facebook, and more. Each with dozens of naming variations across sources.","Retailer unions",[1089,3088],{"description":3089,"title":3090},"The label itself can appear under different names, abbreviations, or legal entity variations across distributors.","Label unions",[1089,3092],{"description":3093,"title":3094},"Streaming, downloads, physical, sync - each distributor categorizes their services differently.","Service unions",[749,3096,3097],{},"When a file is imported, every raw value is checked against the union mappings. If it matches, the canonical name is stored alongside the original. If it doesn't, the raw value is preserved - and flagged for review.",[899,3099,3101],{"id":3100},"why-not-just-find-and-replace","Why not just find-and-replace?",[749,3103,3104],{},"Because new values appear constantly. A distributor adds a sub-brand. Another changes their internal naming. A third introduces a typo that persists for six months before anyone notices. Static find-and-replace breaks every time the data evolves.",[749,3106,3107],{},"Unions are dynamic. A non-technical user can add a new mapping through an admin interface - no code changes, no redeployment. The next import picks it up automatically.",[931,3109,3110],{},[749,3111,3112],{},"One real example: a distributor renamed their Spotify column from \"Spotify\" to \"Spotify AB\" mid-year. Without union mapping, this would have created a second \"Spotify\" in every report, splitting the data and making quarterly comparisons unreliable.",[749,3114,3115],{},[3065,3116],{"alt":3117,"src":3118},"Currency exchange and data normalization","/images/blog/musictechlab_blog_currency-exchange-data.webp",[899,3120,3122],{"id":3121},"the-cherry-on-top-currencies-and-territories","The cherry on top: currencies and territories",[749,3124,3125],{},"Even after normalizing names, two more problems remain.",[770,3127,3129],{"id":3128},"currencies","Currencies",[749,3131,3132],{},"Every distributor reports income in their own currency. And they don't even agree on what to call the currency column:",[2366,3134,3135,3147],{},[2843,3136,3137],{},[2846,3138,3139,3141,3144],{},[2849,3140,2851],{},[2849,3142,3143],{},"Column name",[2849,3145,3146],{},"Default currency",[2856,3148,3149,3161,3171,3182,3194,3206],{},[2846,3150,3151,3153,3158],{},[2861,3152,2863],{},[2861,3154,3155],{},[970,3156,3157],{},"Original currency",[2861,3159,3160],{},"varies",[2846,3162,3163,3165,3168],{},[2861,3164,2873],{},[2861,3166,3167],{},"(file-level)",[2861,3169,3170],{},"GBP",[2846,3172,3173,3175,3180],{},[2861,3174,2883],{},[2861,3176,3177],{},[970,3178,3179],{},"CURRENCY_CODE",[2861,3181,3170],{},[2846,3183,3184,3186,3191],{},[2861,3185,2893],{},[2861,3187,3188],{},[970,3189,3190],{},"Preferred Currency",[2861,3192,3193],{},"USD",[2846,3195,3196,3199,3204],{},[2861,3197,3198],{},"Merlin",[2861,3200,3201],{},[970,3202,3203],{},"Payable currency",[2861,3205,3160],{},[2846,3207,3208,3210,3215],{},[2861,3209,2911],{},[2861,3211,3212],{},[970,3213,3214],{},"currency-code",[2861,3216,3160],{},[749,3218,3219],{},"To compare income across sources, every value needs to be converted to a single target currency using exchange rates tied to specific dates. A $1,000 Orchard payment and a £800 ADA payment aren't comparable until you normalize them.",[770,3221,3223],{"id":3222},"territories","Territories",[749,3225,3226],{},"Countries seem straightforward - until you see how distributors label them:",[2366,3228,3229,3239],{},[2843,3230,3231],{},[2846,3232,3233,3236],{},[2849,3234,3235],{},"The same country",[2849,3237,3238],{},"Variations across sources",[2856,3240,3241,3259,3277],{},[2846,3242,3243,3246],{},[2861,3244,3245],{},"United States",[2861,3247,3248,2416,3251,2416,3254,2416,3257],{},[970,3249,3250],{},"US",[970,3252,3253],{},"USA",[970,3255,3256],{},"UNITED STATES",[970,3258,3245],{},[2846,3260,3261,3264],{},[2861,3262,3263],{},"United Kingdom",[2861,3265,3266,2416,3269,2416,3272,2416,3275],{},[970,3267,3268],{},"UK",[970,3270,3271],{},"GB",[970,3273,3274],{},"UNITED KINGDOM (GB)",[970,3276,3263],{},[2846,3278,3279,3282],{},[2861,3280,3281],{},"Czech Republic",[2861,3283,3284,2416,3287,2416,3290],{},[970,3285,3286],{},"CZ",[970,3288,3289],{},"CZECH REPUBLIC",[970,3291,3292],{},"Czechia",[749,3294,3295],{},"Each variation needs to map to a standard two-letter code. Without this, \"show me all US streams\" misses half the data.",[899,3297,3299],{"id":3298},"three-layers-of-normalization","Three layers of normalization",[749,3301,3302],{},"Here's the full picture - what it actually takes to turn raw distributor data into something usable:",[827,3304,3306],{"className":3305},[830,2956,833],[964,3307,3309],{"className":966,"code":3308,"language":968,"meta":842,"style":842},"flowchart TD\n    subgraph layer1[\"Layer 1: File Formats\"]\n        A[\"18 adapters\"]\n        B[\"5 formats\"]\n        C[\"500+ files/year\"]\n    end\n\n    subgraph layer2[\"Layer 2: Name Normalization\"]\n        D[\"830 raw values\"]\n        E[\"19 unions\"]\n        F[\"3 dimensions\"]\n    end\n\n    subgraph layer3[\"Layer 3: Currency & Territory\"]\n        G[\"Multiple currencies\"]\n        H[\"Exchange rates\"]\n        I[\"Country code mapping\"]\n    end\n\n    subgraph result[\"Result\"]\n        J[(\"One clean dataset\")]\n    end\n\n    layer1 --> layer2\n    layer2 --> layer3\n    layer3 --> result\n",[970,3310,3311,3316,3321,3326,3331,3336,3340,3344,3349,3354,3359,3364,3368,3372,3377,3382,3387,3392,3396,3400,3405,3410,3414,3418,3423,3428],{"__ignoreMap":842},[973,3312,3313],{"class":975,"line":976},[973,3314,3315],{"class":979},"flowchart TD\n",[973,3317,3318],{"class":975,"line":843},[973,3319,3320],{"class":979},"    subgraph layer1[\"Layer 1: File Formats\"]\n",[973,3322,3323],{"class":975,"line":846},[973,3324,3325],{"class":979},"        A[\"18 adapters\"]\n",[973,3327,3328],{"class":975,"line":993},[973,3329,3330],{"class":979},"        B[\"5 formats\"]\n",[973,3332,3333],{"class":975,"line":999},[973,3334,3335],{"class":979},"        C[\"500+ files/year\"]\n",[973,3337,3338],{"class":975,"line":1005},[973,3339,3006],{"class":979},[973,3341,3342],{"class":975,"line":1011},[973,3343,1020],{"emptyLinePlaceholder":858},[973,3345,3346],{"class":975,"line":1017},[973,3347,3348],{"class":979},"    subgraph layer2[\"Layer 2: Name Normalization\"]\n",[973,3350,3351],{"class":975,"line":1023},[973,3352,3353],{"class":979},"        D[\"830 raw values\"]\n",[973,3355,3356],{"class":975,"line":1029},[973,3357,3358],{"class":979},"        E[\"19 unions\"]\n",[973,3360,3361],{"class":975,"line":1035},[973,3362,3363],{"class":979},"        F[\"3 dimensions\"]\n",[973,3365,3366],{"class":975,"line":1041},[973,3367,3006],{"class":979},[973,3369,3370],{"class":975,"line":1047},[973,3371,1020],{"emptyLinePlaceholder":858},[973,3373,3374],{"class":975,"line":1053},[973,3375,3376],{"class":979},"    subgraph layer3[\"Layer 3: Currency & Territory\"]\n",[973,3378,3379],{"class":975,"line":1059},[973,3380,3381],{"class":979},"        G[\"Multiple currencies\"]\n",[973,3383,3384],{"class":975,"line":1065},[973,3385,3386],{"class":979},"        H[\"Exchange rates\"]\n",[973,3388,3389],{"class":975,"line":1071},[973,3390,3391],{"class":979},"        I[\"Country code mapping\"]\n",[973,3393,3394],{"class":975,"line":1077},[973,3395,3006],{"class":979},[973,3397,3398],{"class":975,"line":1441},[973,3399,1020],{"emptyLinePlaceholder":858},[973,3401,3402],{"class":975,"line":1453},[973,3403,3404],{"class":979},"    subgraph result[\"Result\"]\n",[973,3406,3407],{"class":975,"line":1476},[973,3408,3409],{"class":979},"        J[(\"One clean dataset\")]\n",[973,3411,3412],{"class":975,"line":2268},[973,3413,3006],{"class":979},[973,3415,3416],{"class":975,"line":2274},[973,3417,1020],{"emptyLinePlaceholder":858},[973,3419,3420],{"class":975,"line":2327},[973,3421,3422],{"class":979},"    layer1 --> layer2\n",[973,3424,3425],{"class":975,"line":2350},[973,3426,3427],{"class":979},"    layer2 --> layer3\n",[973,3429,3430],{"class":975,"line":2376},[973,3431,3432],{"class":979},"    layer3 --> result\n",[749,3434,3435],{},"Most teams get stuck at Layer 1 and never even reach the name and currency problems. But without all three layers, you can't answer basic questions like \"What were our total Spotify streams in Q3, in GBP?\"",[899,3437,3439],{"id":3438},"the-payoff","The payoff",[749,3441,3442],{},"After all three normalization layers, that question becomes trivial. Filter by retailer union, filter by date range, and every value is already in GBP. One query. One answer. No spreadsheet wrangling.",[749,3444,3445,3446,3449],{},"And once the data is clean, even non-technical users can get answers. We built an ",[759,3447,3448],{"href":85},"AI-powered analytics dashboard"," that lets anyone type a question in plain English and get a chart back in seconds, no SQL required.",[749,3451,3452],{},"That's the difference between a collection of files and a data platform. We build the latter.",[2757,3454,3455],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":842,"searchDepth":843,"depth":843,"links":3457},[3458,3459,3460,3461,3465,3466],{"id":2835,"depth":843,"text":2836},{"id":2944,"depth":843,"text":2945},{"id":3100,"depth":843,"text":3101},{"id":3121,"depth":843,"text":3122,"children":3462},[3463,3464],{"id":3128,"depth":846,"text":3129},{"id":3222,"depth":846,"text":3223},{"id":3298,"depth":843,"text":3299},{"id":3438,"depth":843,"text":3439},"2026-03-03T00:00:00.000Z","After solving the file format problem, the data inside is just as messy. Different names for the same platforms, labels, currencies, and territories. Here's how we normalize it.",[3470,3473,3476],{"question":3471,"answer":3472},"Why is music streaming data so inconsistent?","Every distributor uses their own naming conventions for retailers, labels, services, territories, and currencies. There is no shared standard, so the same platform can appear under dozens of different names across data sources.",{"question":3474,"answer":3475},"What is a data union in the context of music royalties?","A union is a normalization group that maps multiple raw values to one canonical name. For example, all variations of 'Spotify' across 18 distributors get mapped to a single SPOTIFY union, making cross-source analysis possible.",{"question":3477,"answer":3478},"How do currency differences affect music royalty reporting?","Distributors report income in different currencies (USD, GBP, EUR) and even label the currency column differently. To compare or aggregate income across sources, every value must be converted to a single target currency using exchange rates.",{"src":3480},"/images/blog/musictechlab_blog_830-ways-to-say-spotify.webp",{"enabled":858,"items":3482},[3483,3485,3488,3491],{"text":3484,"icon":865},"830 raw data values map to just 19 canonical names across retailers, labels, and services.",{"text":3486,"icon":3487},"Three normalization layers are needed: file formats, name unions, and currency/territory mapping.","i-lucide-layers",{"text":3489,"icon":3490},"Unions are dynamic and admin-editable, so new naming variations need no code changes.","i-lucide-settings",{"text":3492,"icon":3493},"Without all three layers, even basic queries like total Spotify streams in Q3 are impossible.","i-lucide-alert-triangle",{},{"title":3496,"description":3497},"Normalizing Music Streaming Data: 830 Values, 19 Unions | MusicTech Lab","How we normalize 830 raw data values into 19 canonical names across retailers, labels, and services. Real-world music data normalization.",[850,872,3499,3500],"streaming","metadata","nkhKea-amz1NLgquVd-h3Qvo8Tq2jJzXtEx0Fmt3TeE",{"id":3503,"title":76,"authors":3504,"badge":3507,"body":3510,"category":850,"client":851,"date":4177,"description":4178,"extension":854,"faq":4179,"featured":858,"featuredOrder":976,"hidden":69,"image":4189,"keyTakeaways":4191,"meta":4203,"navigation":858,"path":77,"seo":4204,"status":851,"stem":78,"tags":4207,"teaser":851,"__hash__":4209,"score":993},"posts/blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data.md",[3505],{"name":884,"to":885,"avatar":3506},{"src":887},{"label":3508,"color":3509},"Distribution","#0ea5e9",{"type":746,"value":3511,"toc":4169},[3512,3515,3518,3522,3525,3806,3813,3817,3820,3839,3842,3846,3849,3852,3863,3868,3874,3878,3881,3981,3984,3988,3991,4139,4142,4153,4160,4164,4167],[749,3513,3514],{},"Every month, an independent label receives royalty reports from over a dozen distributors. Not a single one looks the same.",[749,3516,3517],{},"This isn't a hypothetical. This is what a real download folder looks like when you work with music royalty data at scale.",[899,3519,3521],{"id":3520},"the-wall-of-files","The wall of files",[749,3523,3524],{},"Here's a small sample of actual filenames from a single label's monthly data intake -anonymised, but otherwise untouched:",[2366,3526,3527,3542],{},[2843,3528,3529],{},[2846,3530,3531,3533,3536,3539],{},[2849,3532,2851],{},[2849,3534,3535],{},"Example Filename",[2849,3537,3538],{},"Format",[2849,3540,3541],{},"File Size",[2856,3543,3544,3559,3574,3589,3604,3618,3632,3645,3659,3674,3690,3705,3720,3735,3749,3763,3777,3792],{},[2846,3545,3546,3548,3553,3556],{},[2861,3547,2863],{},[2861,3549,3550],{},[970,3551,3552],{},"FUGA_Statement_June_2024.xlsx",[2861,3554,3555],{},".xlsx",[2861,3557,3558],{},"~1 MB",[2846,3560,3561,3563,3568,3571],{},[2861,3562,2873],{},[2861,3564,3565],{},[970,3566,3567],{},"SR1_Distribution_Aug_24_-_00054061_-_2024-8.xlsb",[2861,3569,3570],{},".xlsb",[2861,3572,3573],{},"~70 KB",[2846,3575,3576,3578,3583,3586],{},[2861,3577,2883],{},[2861,3579,3580],{},[970,3581,3582],{},"20240801-1496-DS-GBP_Digital_Sales.csv",[2861,3584,3585],{},".csv",[2861,3587,3588],{},"up to 226 MB",[2846,3590,3591,3593,3598,3601],{},[2861,3592,2893],{},[2861,3594,3595],{},[970,3596,3597],{},"The_Orchard20240821_Jun2024_fullreport_catalogue_US.xls",[2861,3599,3600],{},".xls",[2861,3602,3603],{},"up to 700 MB",[2846,3605,3606,3608,3613,3615],{},[2861,3607,2903],{},[2861,3609,3610],{},[970,3611,3612],{},"bandcamp_rev_report_20240801-20240831.csv",[2861,3614,3585],{},[2861,3616,3617],{},"~6 KB",[2846,3619,3620,3622,3627,3629],{},[2861,3621,2911],{},[2861,3623,3624],{},[970,3625,3626],{},"MVD_Statement_DigitalSales_2024-07.xls",[2861,3628,3600],{},[2861,3630,3631],{},"~12 KB",[2846,3633,3634,3636,3641,3643],{},[2861,3635,2911],{},[2861,3637,3638],{},[970,3639,3640],{},"MVD_Statement_DigitalSales_2024-07.xlsx",[2861,3642,3555],{},[2861,3644,3631],{},[2846,3646,3647,3649,3654,3656],{},[2861,3648,2920],{},[2861,3650,3651],{},[970,3652,3653],{},"Emerald_202408_DSR.csv",[2861,3655,3585],{},[2861,3657,3658],{},"~46 MB",[2846,3660,3661,3664,3669,3671],{},[2861,3662,3663],{},"Safari Records",[2861,3665,3666],{},[970,3667,3668],{},"Safari_Records_202408_DSR.xlsx",[2861,3670,3555],{},[2861,3672,3673],{},"~2 MB",[2846,3675,3676,3679,3684,3687],{},[2861,3677,3678],{},"ADA (legacy)",[2861,3680,3681],{},[970,3682,3683],{},"ADAOCT1.XLS",[2861,3685,3686],{},".XLS",[2861,3688,3689],{},"up to 150 MB",[2846,3691,3692,3695,3700,3702],{},[2861,3693,3694],{},"MAC",[2861,3696,3697],{},[970,3698,3699],{},"MAC_Developments_iTunes_August_2024.xlsx",[2861,3701,3555],{},[2861,3703,3704],{},"~49 KB",[2846,3706,3707,3710,3715,3718],{},[2861,3708,3709],{},"Absolute",[2861,3711,3712],{},[970,3713,3714],{},"Absolute_2024021.CSV",[2861,3716,3717],{},".CSV",[2861,3719,3588],{},[2846,3721,3722,3725,3730,3732],{},[2861,3723,3724],{},"Qello",[2861,3726,3727],{},[970,3728,3729],{},"DetailedSheet_Records_Ltd_20240801_20240831.xlsx",[2861,3731,3555],{},[2861,3733,3734],{},"~10 KB",[2846,3736,3737,3739,3744,3746],{},[2861,3738,2929],{},[2861,3740,3741],{},[970,3742,3743],{},"sfmaug2024.xlsx",[2861,3745,3555],{},[2861,3747,3748],{},"~2.5 MB",[2846,3750,3751,3754,3759,3761],{},[2861,3752,3753],{},"BOFM",[2861,3755,3756],{},[970,3757,3758],{},"BOFM_Aug2024.xlsx",[2861,3760,3555],{},[2861,3762,3748],{},[2846,3764,3765,3768,3773,3775],{},[2861,3766,3767],{},"Dome Records",[2861,3769,3770],{},[970,3771,3772],{},"Dome_Records_202408_DSR.csv",[2861,3774,3585],{},[2861,3776,3558],{},[2846,3778,3779,3782,3787,3789],{},[2861,3780,3781],{},"MDR",[2861,3783,3784],{},[970,3785,3786],{},"MDR_May-2024_65634.92_Records.xlsx",[2861,3788,3555],{},[2861,3790,3791],{},"~500 KB",[2846,3793,3794,3796,3801,3803],{},[2861,3795,3198],{},[2861,3797,3798],{},[970,3799,3800],{},"Merlin_Nov24_eg.for.jack.xlsx",[2861,3802,3555],{},[2861,3804,3805],{},"~703 KB",[749,3807,3808,3809,3812],{},"That's ",[765,3810,3811],{},"18 adapters across 500+ files per year"," -each with its own naming convention, file format, and internal structure. From a 6 KB Bandcamp CSV to a single Orchard report that can reach 700 MB.",[899,3814,3816],{"id":3815},"spot-the-pattern","Spot the pattern",[749,3818,3819],{},"Go ahead, try. You won't find one.",[827,3821,3823,3827,3831,3835],{"className":3822},[1084,1085,1661,1087,833],[1089,3824],{"description":3825,"title":3826},"`.xlsx` · `.xlsb` · `.xls` · `.XLS` · `.csv` · `.CSV`","5 file formats",[1089,3828],{"description":3829,"title":3830},"`2024-07` · `202408` · `Aug_24` · `August_2024` · `20240801-20240831` · `2024021`","6 date conventions in filenames",[1089,3832],{"description":3833,"title":3834},"Some distributors send both `.xls` and `.xlsx` versions of the exact same data.","Same report, multiple formats",[1089,3836],{"description":3837,"title":3838},"camelCase, ALLCAPS, underscores, hyphens, internal reference numbers, random hash suffixes.","No naming standard",[749,3840,3841],{},"And that's just the filenames. Open these files and you'll find different column names for the same data, different date formats inside the cells, different encodings, and multi-sheet workbooks where each sheet follows its own rules.",[899,3843,3845],{"id":3844},"why-this-matters","Why this matters",[749,3847,3848],{},"Someone has to make sense of all this. Every month.",[749,3850,3851],{},"For most independent labels, that means hours of manual work -copying data between spreadsheets, reformatting dates, matching column names, fixing encoding issues that turn artist names into garbled text.",[749,3853,3854,3855,3858,3859,3862],{},"The cost isn't just time. It's ",[765,3856,3857],{},"delayed royalty payments"," to artists. It's ",[765,3860,3861],{},"reporting errors"," that erode trust. It's the finance team spending their week on data cleanup instead of analysis.",[931,3864,3865],{},[749,3866,3867],{},"One distributor changed their report format mid-year without notice. The same filename pattern, but completely different column structure inside. Manual processes break silently when this happens.",[749,3869,3870],{},[3065,3871],{"alt":3872,"src":3873},"Someone working on a laptop with spreadsheet data","/images/blog/musictechlab_blog_royalty-data-spreadsheet-laptop.webp",[899,3875,3877],{"id":3876},"how-teams-try-to-solve-this","How teams try to solve this",[749,3879,3880],{},"There's more than one way to tackle this problem. Here's how the most common approaches compare:",[2366,3882,3883,3902],{},[2843,3884,3885],{},[2846,3886,3887,3890,3893,3896,3899],{},[2849,3888,3889],{},"Approach",[2849,3891,3892],{},"Setup effort",[2849,3894,3895],{},"Maintenance",[2849,3897,3898],{},"Handles format changes",[2849,3900,3901],{},"Scales with new sources",[2856,3903,3904,3923,3943,3962],{},[2846,3905,3906,3911,3914,3917,3920],{},[2861,3907,3908],{},[765,3909,3910],{},"Manual spreadsheets",[2861,3912,3913],{},"None",[2861,3915,3916],{},"Hours every month",[2861,3918,3919],{},"Breaks silently",[2861,3921,3922],{},"Every new source = more hours",[2846,3924,3925,3931,3934,3937,3940],{},[2861,3926,3927,3930],{},[765,3928,3929],{},"Generic ETL tools"," (Fivetran, Airbyte)",[2861,3932,3933],{},"Medium",[2861,3935,3936],{},"Low",[2861,3938,3939],{},"Limited - connectors are generic",[2861,3941,3942],{},"Only if a connector exists",[2846,3944,3945,3950,3953,3956,3959],{},[2861,3946,3947],{},[765,3948,3949],{},"Custom Python scripts",[2861,3951,3952],{},"High",[2861,3954,3955],{},"High - fragile, hard to maintain",[2861,3957,3958],{},"Depends on the developer",[2861,3960,3961],{},"Every new source = new script",[2846,3963,3964,3969,3972,3975,3978],{},[2861,3965,3966],{},[765,3967,3968],{},"Adapter-based pipeline",[2861,3970,3971],{},"High upfront",[2861,3973,3974],{},"Low - each adapter is isolated",[2861,3976,3977],{},"Adapter update, no side effects",[2861,3979,3980],{},"Add an adapter, done",[749,3982,3983],{},"Generic ETL tools work well for standardised APIs and databases. But music royalty data doesn't come from APIs - it comes from email attachments, FTP servers, and distributor portals. Each source is its own special case. That's why an adapter-based approach wins here: each distributor gets its own parser, isolated from the rest, easy to update when formats change.",[899,3985,3987],{"id":3986},"one-clean-dataset","One clean dataset",[749,3989,3990],{},"Here's what the pipeline looks like in practice:",[964,3992,3994],{"className":966,"code":3993,"language":968,"meta":842,"style":842},"flowchart LR\n    subgraph sources[\"Raw Files\"]\n        A[\".xlsx\"]\n        B[\".xlsb\"]\n        C[\".xls\"]\n        D[\".csv\"]\n    end\n\n    subgraph adapters[\"Adapter Layer\"]\n        E[\"FUGA\"]\n        F[\"ADA\"]\n        G[\"Orchard\"]\n        H[\"Bandcamp\"]\n        I[\"+ 14 more\"]\n    end\n\n    subgraph output[\"Unified Output\"]\n        J[(\"Clean dataset\")]\n    end\n\n    A --> E\n    B --> F\n    C --> G\n    D --> H\n\n    E --> J\n    F --> J\n    G --> J\n    H --> J\n    I --> J\n",[970,3995,3996,4000,4005,4010,4015,4020,4025,4029,4033,4038,4043,4048,4053,4058,4063,4067,4071,4076,4081,4085,4089,4094,4099,4103,4108,4112,4117,4122,4127,4133],{"__ignoreMap":842},[973,3997,3998],{"class":975,"line":976},[973,3999,2966],{"class":979},[973,4001,4002],{"class":975,"line":843},[973,4003,4004],{"class":979},"    subgraph sources[\"Raw Files\"]\n",[973,4006,4007],{"class":975,"line":846},[973,4008,4009],{"class":979},"        A[\".xlsx\"]\n",[973,4011,4012],{"class":975,"line":993},[973,4013,4014],{"class":979},"        B[\".xlsb\"]\n",[973,4016,4017],{"class":975,"line":999},[973,4018,4019],{"class":979},"        C[\".xls\"]\n",[973,4021,4022],{"class":975,"line":1005},[973,4023,4024],{"class":979},"        D[\".csv\"]\n",[973,4026,4027],{"class":975,"line":1011},[973,4028,3006],{"class":979},[973,4030,4031],{"class":975,"line":1017},[973,4032,1020],{"emptyLinePlaceholder":858},[973,4034,4035],{"class":975,"line":1023},[973,4036,4037],{"class":979},"    subgraph adapters[\"Adapter Layer\"]\n",[973,4039,4040],{"class":975,"line":1029},[973,4041,4042],{"class":979},"        E[\"FUGA\"]\n",[973,4044,4045],{"class":975,"line":1035},[973,4046,4047],{"class":979},"        F[\"ADA\"]\n",[973,4049,4050],{"class":975,"line":1041},[973,4051,4052],{"class":979},"        G[\"Orchard\"]\n",[973,4054,4055],{"class":975,"line":1047},[973,4056,4057],{"class":979},"        H[\"Bandcamp\"]\n",[973,4059,4060],{"class":975,"line":1053},[973,4061,4062],{"class":979},"        I[\"+ 14 more\"]\n",[973,4064,4065],{"class":975,"line":1059},[973,4066,3006],{"class":979},[973,4068,4069],{"class":975,"line":1065},[973,4070,1020],{"emptyLinePlaceholder":858},[973,4072,4073],{"class":975,"line":1071},[973,4074,4075],{"class":979},"    subgraph output[\"Unified Output\"]\n",[973,4077,4078],{"class":975,"line":1077},[973,4079,4080],{"class":979},"        J[(\"Clean dataset\")]\n",[973,4082,4083],{"class":975,"line":1441},[973,4084,3006],{"class":979},[973,4086,4087],{"class":975,"line":1453},[973,4088,1020],{"emptyLinePlaceholder":858},[973,4090,4091],{"class":975,"line":1476},[973,4092,4093],{"class":979},"    A --> E\n",[973,4095,4096],{"class":975,"line":2268},[973,4097,4098],{"class":979},"    B --> F\n",[973,4100,4101],{"class":975,"line":2274},[973,4102,3043],{"class":979},[973,4104,4105],{"class":975,"line":2327},[973,4106,4107],{"class":979},"    D --> H\n",[973,4109,4110],{"class":975,"line":2350},[973,4111,1020],{"emptyLinePlaceholder":858},[973,4113,4114],{"class":975,"line":2376},[973,4115,4116],{"class":979},"    E --> J\n",[973,4118,4119],{"class":975,"line":2381},[973,4120,4121],{"class":979},"    F --> J\n",[973,4123,4124],{"class":975,"line":2387},[973,4125,4126],{"class":979},"    G --> J\n",[973,4128,4130],{"class":975,"line":4129},29,[973,4131,4132],{"class":979},"    H --> J\n",[973,4134,4136],{"class":975,"line":4135},30,[973,4137,4138],{"class":979},"    I --> J\n",[749,4140,4141],{},"Every file goes through its format-specific adapter - handling encoding, column mapping, date parsing, and multi-sheet logic. What comes out the other side is one consistent dataset: same columns, same date format, same encoding. Ready for analysis, reporting, and artist payouts.",[749,4143,4144,4145,4148,4149,4152],{},"But consistent columns are only the beginning. The data inside those files is ",[759,4146,4147],{"href":81},"just as messy"," - 830 raw values that need mapping to 19 canonical names before you can run a single meaningful query. And once the data is truly clean, it opens the door to ",[759,4150,4151],{"href":85},"AI-powered analytics"," where business users ask questions in plain English and get charts back in seconds.",[749,4154,4155,4156,4159],{},"That's what we build at MusicTech Lab. Not another dashboard on top of messy data - but the ",[765,4157,4158],{},"data layer underneath"," that turns chaos into clarity.",[899,4161,4163],{"id":4162},"looks-familiar","Looks familiar?",[749,4165,4166],{},"If your monthly royalty workflow involves more spreadsheet wrangling than actual analysis, we should talk. We've built data pipelines for independent labels handling exactly this kind of complexity - and we can do the same for you.",[2757,4168,3455],{},{"title":842,"searchDepth":843,"depth":843,"links":4170},[4171,4172,4173,4174,4175,4176],{"id":3520,"depth":843,"text":3521},{"id":3815,"depth":843,"text":3816},{"id":3844,"depth":843,"text":3845},{"id":3876,"depth":843,"text":3877},{"id":3986,"depth":843,"text":3987},{"id":4162,"depth":843,"text":4163},"2026-02-27T00:00:00.000Z","Every month, independent labels receive royalty reports from over a dozen distributors. No two look the same. Here's what that actually looks like.",[4180,4183,4186],{"question":4181,"answer":4182},"Why do music distributors use different file formats?","There is no industry-wide standard for royalty report delivery. Each distributor built their own reporting system independently, resulting in different file formats, column names, date conventions, and encodings.",{"question":4184,"answer":4185},"What file formats are used in music royalty reporting?","Common formats include XLSX (modern Excel), XLSB (binary Excel), XLS (legacy Excel), and CSV with various encodings (UTF-8, UTF-16, ISO-8859-1). Some distributors send the same report in multiple formats.",{"question":4187,"answer":4188},"How can labels automate royalty data processing?","By building format-aware adapters that recognise each distributor's file structure and automatically normalise everything into a single, clean dataset -eliminating manual cleanup and reducing errors.",{"src":4190},"/images/blog/musictechlab_blog_13-distributors-5-file-formats-zero-standards.webp",{"enabled":858,"items":4192},[4193,4196,4199,4201],{"text":4194,"icon":4195},"18 adapters parse 500+ files per year from 13 distributors in 5 different formats.","i-lucide-blocks",{"text":4197,"icon":4198},"File sizes range from 6 KB (Bandcamp) to 700 MB (The Orchard) with zero naming standards.","i-lucide-file-text",{"text":4200,"icon":3487},"Adapter-based pipelines isolate each source, making format changes safe and side-effect-free.",{"text":4202,"icon":3493},"Manual spreadsheet processing delays royalty payments and introduces silent reporting errors.",{},{"title":4205,"description":4206},"Music Royalty Data Chaos: 13 Distributors, 5 Formats | MusicTech Lab","See the real complexity of music royalty reporting -different file formats, naming conventions, and schemas from every distributor. Learn how automation solves it.",[850,872,4208,3499,3500],"royalties","RALDAG90DAM9IgwQPuVrQkFWVhTDlAAddo9XQWvnld4",{"id":4211,"title":180,"authors":4212,"badge":4215,"body":4216,"category":850,"client":851,"date":4292,"description":878,"extension":854,"faq":851,"featured":69,"featuredOrder":851,"hidden":69,"image":4293,"keyTakeaways":4295,"meta":4304,"navigation":858,"path":181,"seo":4305,"status":851,"stem":182,"tags":4306,"teaser":851,"__hash__":4307,"score":993},"posts/blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it.md",[4213],{"name":884,"to":885,"avatar":4214},{"src":887},{"label":743,"color":744},{"type":746,"value":4217,"toc":4288},[4218,4221,4227,4239,4243,4253,4256,4267,4271,4274,4285],[749,4219,4220],{},"A new report just dropped that every builder, founder, and policymaker working at the intersection of culture and technology should take a look at.",[749,4222,4223,4226],{},[765,4224,4225],{},"“Poland Creative Tech Startups Report 2025”"," is the first comprehensive study of Poland’s creative tech scene - covering over 400 startups, investors, and public institutions. It includes key data, expert commentary, and practical recommendations for developing this fast-moving space.",[827,4228,4230,4234],{"className":4229},[830,831,832,833],[835,4231],{"color":837,"label":4232,"target":839,"to":4233,"variant":840},"Polish version","https://lnkd.in/e2qeNwTr",[835,4235],{"color":4236,"label":4237,"target":839,"to":4238,"variant":840},"neutral","English version","https://lnkd.in/ed-SWFJj",[899,4240,4242],{"id":4241},"musictech-labs-take","MusicTech Lab’s Take",[749,4244,4245,4246,4248,4249,4252],{},"We’re excited that our R&D Partner, ",[765,4247,738],{},", was invited to share his perspective in the report, especially as co-founder of ",[765,4250,4251],{},"MusicTech Poland",", a growing community that supports music + tech collaboration in the region.",[749,4254,4255],{},"In the featured case study, Maciej talks about:",[778,4257,4258,4261,4264],{},[781,4259,4260],{},"the current innovation gap in Poland’s music sector,",[781,4262,4263],{},"the role of MusicTech Poland in building a local ecosystem,",[781,4265,4266],{},"and how we at MusicTech Lab support founders working on tools, platforms, and AI-driven products for the music industry.",[899,4268,4270],{"id":4269},"why-this-report-matters","Why This Report Matters",[749,4272,4273],{},"This report shows that Poland has everything needed to lead creative innovation in Central Europe - talent, products, and ideas that can compete globally. But to get there, we need:",[778,4275,4276,4279,4282],{},[781,4277,4278],{},"better support for founders and builders,",[781,4280,4281],{},"more partnerships between culture and tech,",[781,4283,4284],{},"and brave investments in the tools that shape how music and art are created today.",[749,4286,4287],{},"We’re proud to contribute to this momentum. If you’re a startup, investor, or institution working in musictech, let’s talk.",{"title":842,"searchDepth":843,"depth":843,"links":4289},[4290,4291],{"id":4241,"depth":843,"text":4242},{"id":4269,"depth":843,"text":4270},"2025-06-02T00:00:00.000Z",{"src":4294},"/images/blog/musictechlab_blog_polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it.webp",{"enabled":858,"items":4296},[4297,4299,4302],{"text":4298,"icon":1101},"Poland's first Creative Tech report covers 400+ startups, investors, and public institutions.",{"text":4300,"icon":4301},"Maciej Dulski contributed a case study on the innovation gap in Poland's music sector.","i-lucide-lightbulb",{"text":4303,"icon":868},"MusicTech Poland community supports music and tech collaboration across the region.",{},{"title":180,"description":878},[872,850],"z1sKzcQtqgbYZYKDz2XGi6YFXaUsZPGk6mAypUwapQU",1780305181082]