๐Ÿฆ‰ ~/projects/call-center — pwsh

PS andrewfry.dev> cat /projects/call-center.md

cd .. to return to /projects

Call Center Audio Analysis

Personal Project ยท Generative AI โ€” raw call audio in, reportable analytics out

STATUS: POC_COMPLETE TYPE: GEN_AI BUILT: SIDE_PROJECT
[.NET MVC][Semantic Kernel][OpenAI Whisper][GPT-3.5 Instruct]
  • 3 outputs per call (summary ยท category ยท sentiment)
  • 5 call categories
  • 0.5 temperature, for rigid output
  • 80/99 work-to-glory ratio of a good POC

01 The POC

Proof of concepts are every developer's favorite: you do 80% of the work demonstrating the possibility and collect 99% of the glory. Then you hand it to some poor group of developers who get to do the last 20% โ€” the hard part nobody priced in: UI, performance, security, scalability.

This one was a GenAI side project: take raw call center audio, transcribe it, then categorize, summarize, and score the sentiment of each call. The output had to land in a reportable format, so trends and pain points can be derived across thousands of calls instead of anecdotes.

02 Data Flow

The pipeline is deliberately boring โ€” five steps, two API calls. The WAV bytes for each side of the conversation (rep vs. customer) go to OpenAI's speech-to-text endpoint (/audio/transcriptions, the Whisper model). The returned transcript is bound into a prompt template and sent to gpt-3.5-turbo-instruct through Semantic Kernel.

The model answers in JSON, which deserializes straight into a typed object and renders in a .NET MVC front end โ€” and, more importantly, can be written to a database column like any other structured data.

fig 1 โ€” data flow
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  Caller audio      โ”‚
  โ”‚  (WAV, per side)   โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
            โ”‚  POST /audio/transcriptions
            โ–ผ
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  Whisper           โ”‚
  โ”‚  transcription     โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
            โ”‚  transcript text
            โ–ผ
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  Bind transcript   โ”‚
  โ”‚  into PROMPT       โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
            โ”‚  Semantic Kernel
            โ–ผ
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  gpt-3.5-turbo-    โ”‚
  โ”‚  instruct @ t=0.5  โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
            โ”‚  JSON response
            โ–ผ
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  Deserialize to    โ”‚
  โ”‚  typed object      โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
            โ”‚
            โ–ผ
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  Present on UI     โ”‚
  โ”‚  (.NET MVC)        โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

03 The Prompt

The prompt does three jobs: it sets a persona (a customer service analyst who knows credit cards), it enumerates the categories the call must be sorted into, and it pins the response format to a JSON example so the answer is machine-readable instead of conversational.

Everything the business wants out of the call โ€” summary, category, sentiment โ€” comes back from a single completion.

fig 2 โ€” Prompts.cs
public static string
  SUMMARIZE_SENTIMENT_CATEGORIZE = @"
Act as if you are a knowledgeable customer
service analyst, who has expertise with
credit cards. You will be tasked with
analyzing the contents of a call with the
customer and you must perform some tasks
on the data.

  1. Provide a summary of the call in
     less than 75 characters.
  2. Categorize the call using the
     following categories:
       a. Account Closure
       b. Payments
       c. Issue with Card
       d. Card Damaged
       e. Other issue
  3. If the call involves more than one
     of the categories then list both.
  4. Provide a comment on the sentiment
     of the caller.
  5. Provide your answer in the form of
     a JSON object. Example:

     {
       ""summary""   : ""SUMMARY GOES HERE"",
       ""category""  : ""CATEGORY GOES HERE"",
       ""sentiment"" : ""SENTIMENT GOES HERE""
     }

  Here is the call: ##CALL##
";

04 Temperature = 0.5

The completion runs at a temperature of 0.5. Temperature controls how the model spreads probability across candidate words: low values make it pick the most likely token nearly every time, high values make it adventurous.

For categorization you want rigid and repeatable โ€” the same call should land in the same bucket every run. Creative writing this is not.

fig 3 โ€” temperature, completing "I like red ___"
  p(x)      temp 0.2        temp 0.5        temp 1.0
  1.0 |        โ–ˆ
  0.8 |        โ–ˆ
  0.6 |        โ–ˆ               โ–ˆ
  0.4 |        โ–ˆ            โ–ˆ  โ–ˆ
  0.2 |  .  .  โ–ˆ  .      .  โ–ˆ  โ–ˆ  .      โ–ˆ  โ–ˆ  โ–ˆ  โ–ˆ
      +----------------------------------------------
         on pa ap sh     on pa ap sh     on pa ap sh
         ([on]ions, [pa]nts, [ap]ples, [sh]oes)

  [ deterministic ] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ [ random / creative ]

  low temp  = picks the single most likely word
              every time (rigid, consistent)
  high temp = spreads probability across many
              options (varied, creative)

05 The Result

The caller's opening statement went through the pipeline and came back structured. The transcript below is Whisper's output; the JSON is the model's analysis, exactly as it deserialized.

Multiply this by every call in the center and the reporting team has categories to count, sentiment to trend, and summaries to skim โ€” from data that used to be an opaque pile of audio files.

fig 4 โ€” sample output
> analyze --call caller_statement.wav

transcript:
  "Hey, I'm calling to see if I can have my
   card activated. I've tried like 19
   different times to activate it, nothing
   seems to work. I went on the website, it
   doesn't work. I tried everything,
   please help."

analysis:
{
  "summary":   "Customer requesting card
                activation help",
  "category":  "Issue with Card",
  "sentiment": "Frustrated"
}

06 Hardening the POC โ€” the Last 20%

What it would take to turn the demo into the thing the poor production team inherits:

  • Retry on malformed JSON โ€” it happens every so often, and resending the content plus the error back to the model usually corrects it
  • An "other" escape hatch โ€” give the prompt an out when no category fits, and use that bucket to route calls into deeper analysis by more powerful (and more expensive) models
  • Domain-specific categories โ€” niche categories need detailed descriptions and examples in the prompt to classify accurately
  • Tiered model pipeline โ€” Semantic Kernel orchestrating a cheap first-pass categorization, escalating only flagged calls to a pricier model with example-rich prompts
  • Considerable Infra โ€” This demonstrates the capability but there is a considerable amount of infrastructures and resources to bring this to a production ready state

> next: cat /projects/podcast-chatbot โ€” the RAG podcast chatbot