Ollama AI ChatGPT C Sharp Programming Data

AI on your Computer? - From Doubt to Local Hosted ChatGPT/DeepSeek in projects

8 Mar 2025 · Rabington Chitima

Well, for the longest time, I hesitated to dive into the AI world. The fear of complex deployments and endless credit/token issues always held me back. But then something unexpected happened: I stumbled upon a YouTube series on building a WhatsApp chatbot.

Important Discovery

At first, I was skeptical. As a .NET developer, my comfort zone was all about C#, not AI. But then the creator took it a step further by introducing self-hosted AI models in another video. Suddenly, the idea of integrating AI didn't feel so intimidating. Instead, it felt like an opportunity waiting to be seized.

A Lightbulb Moment in a Cyber Security Project

I had helped build an in-house Cyber Security Awareness platform that relied heavily on manual processes, like generating questions to test employee awareness of cyber risks after assigned courses. The potential to automate this process using AI was too good to pass up! This was a good opportunity to integrate AI and automatically generate questions and even provide timestamps for video materials.

After this you should also be able to set up your own locally hosted ChatGPT/DeepSeek AI model and have an app that takes a YouTube URL and generates Question & Answers (timestamped to the video).

Staying True to My Roots

When it comes to AI most people jump to Python and some popular JavaScript frameworks, but most of my projects' stake is .NET. Even as I ventured into the AI realm, I knew I wanted to stick with my tried-and-true tech stack. So, I decided to blend my familiarity with C# with the new magic of ChatGPT/DeepSeek AI models running locally using Ollama. It was like adding a powerful new tool to my toolbox — one that didn't disrupt my usual coding rhythm.

Prerequisites / Dependencies

  • ASP.NET Core — Framework for building the backend API and hosting the application.
  • Entity Framework Core — Used for data persistence, storing video metadata, transcripts, questions, and timestamps. You can use any database.
  • YoutubeExplode — NuGet package used to download video streams and metadata from YouTube.
  • Whisper.net — A .NET wrapper for Whisper's transcription model. Handles audio-to-text conversion using local AI models.
  • FFMPEG (external dependency) — Command-line tool used for converting video/audio files (e.g. MP4 to WAV) to ensure compatibility with transcription tools. Ensure it's installed and on the system PATH.
  • Ollama — this is basically your AI. Get it from https://ollama.com/download, then head to the models page to choose which model to run (I'm running llama3.2, GPU-poor 🙂).
  • OllamaSharp — Client library for interfacing with your locally deployed Ollama model to generate questions and summaries.
  • Frontend — using Angular, not covered here.

Ensure Ollama is running:

ollama run llama3.2

Once it's running you can confirm on the default port http://localhost:11434/, which we'll need for the project.

Code

Models

public class Question
{
    public string Text { get; set; }
    public string Answer { get; set; }
    public TimeSpan Timestamp { get; set; }
}

public class VideoData
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Transcript { get; set; }
    public List<Question> Questions { get; set; } = new();
}

public class VideoRequest
{
    public string VideoUrl { get; set; }
}

Services — first step: download the video and get the stream using YoutubeExplode:

private readonly YoutubeClient _youtube = new();

public async Task<VideoData> DownloadAndTranscribe(string videoUrl)
{
    var video = await _youtube.Videos.GetAsync(videoUrl);
    var streamInfo = await _youtube.Videos.Streams.GetManifestAsync(videoUrl);
    var stream = await _youtube.Videos.Streams.GetAsync(streamInfo.GetAudioOnlyStreams().First());
    var videoDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Videos");
    if (!Directory.Exists(videoDirectory))
    {
        Directory.CreateDirectory(videoDirectory);
    }

    var filePath = Path.Combine(videoDirectory, $"{video.Id}.mp4");
    await using (var fileStream = File.Create(filePath))
    {
        await stream.CopyToAsync(fileStream);
    }

    var transcript = await TranscribeAudio(filePath); // second step
    var questions = await GenerateQuestions(transcript); // third step

    return new VideoData
    {
        Title = video.Title,
        Transcript = transcript,
        Questions = questions
    };
}

Second step: transcribe audio using Whisper.net (convert MP4 to WAV first, mono audio at 16kHz for ASR compatibility):

private async Task<string> TranscribeAudio(string filePath)
{
    var wavFilePath = Path.ChangeExtension(filePath, ".wav");
    if (!File.Exists(wavFilePath))
    {
        var ffmpegArgs = $"-y -i \"{filePath}\" -ac 1 -ar 16000 \"{wavFilePath}\"";

        var processStartInfo = new ProcessStartInfo
        {
            FileName = "ffmpeg",
            Arguments = ffmpegArgs,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        using (var process = Process.Start(processStartInfo))
        {
            string output = await process.StandardOutput.ReadToEndAsync();
            string error = await process.StandardError.ReadToEndAsync();
            await process.WaitForExitAsync();

            if (process.ExitCode != 0)
            {
                throw new Exception("FFMPEG conversion failed: " + error);
            }
        }
    }

    using var wavStream = new FileStream(wavFilePath, FileMode.Open, FileAccess.Read);

    var modelName = "ggml-base.bin"; // choose the model that works with your setup

    if (!File.Exists(modelName))
    {
        using var modelStream = await WhisperGgmlDownloader.GetGgmlModelAsync(GgmlType.Base);
        using var fileWriter = File.OpenWrite(modelName);
        await modelStream.CopyToAsync(fileWriter);
    }

    using var whisperFactory = WhisperFactory.FromPath(modelName);

    using var processor = whisperFactory.CreateBuilder()
        .WithLanguage("auto")
        .Build();

    var transcriptBuilder = new System.Text.StringBuilder();
    await foreach (var result in processor.ProcessAsync(wavStream))
    {
        transcriptBuilder.Append(result.Text + " ");
    }

    return transcriptBuilder.ToString().Trim();
}

Third step: generate questions using Ollama via OllamaSharp:

private async Task<List<Question>> GenerateQuestions(string transcript)
{
    try
    {
        var ollama = new OllamaApiClient(new Uri("http://localhost:11434/"), "llama3.2");
        var prompt = $"Generate 5 educational questions based on the transcript below. " +
                "For each question, provide the approximate video timestamp (in HH:MM:SS format) that best corresponds to where the answer can be found. " +
                "Output each question, answer and its corresponding timestamp on one line. " +
                $"Transcript:{transcript}" +
                "Keep the questions human-like, personable, and infused with creativity while maintaining a professional demeanor and video context.";

        var responseStream = ollama.GenerateAsync(prompt);

        var contentBuilder = new System.Text.StringBuilder();
        if (responseStream != null)
        {
            await foreach (var response in responseStream)
            {
                contentBuilder.Append(response.Response);
            }
        }

        var content = contentBuilder.ToString();
        var questions = new List<Question>();
        var lines = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries).ToList();

        if (lines.Count > 0 && lines[0].StartsWith("Here are", StringComparison.OrdinalIgnoreCase))
        {
            lines.RemoveAt(0);
        }

        // Every 3 lines is one complete question entry
        for (int i = 0; i < lines.Count; i += 3)
        {
            if (i + 2 >= lines.Count) break;

            var questionLine = lines[i].Trim();
            var answerLine = lines[i + 1].Trim();
            var timestampLine = lines[i + 2].Trim();

            if (questionLine.StartsWith("Question:", StringComparison.OrdinalIgnoreCase))
                questionLine = questionLine.Substring("Question:".Length).Trim();
            if (answerLine.StartsWith("Answer:", StringComparison.OrdinalIgnoreCase))
                answerLine = answerLine.Substring("Answer:".Length).Trim();
            if (timestampLine.StartsWith("Timestamp:", StringComparison.OrdinalIgnoreCase))
                timestampLine = timestampLine.Substring("Timestamp:".Length).Trim();

            if (!TimeSpan.TryParse(timestampLine, out TimeSpan parsedTimestamp))
                parsedTimestamp = TimeSpan.Zero;

            questions.Add(new Question
            {
                Text = questionLine,
                Answer = answerLine,
                Timestamp = parsedTimestamp
            });
        }
        return questions;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
}

Controller

public class VideoController : ControllerBase
{
    private readonly VideoService _videoService;

    public VideoController(VideoService videoService)
    {
        _videoService = videoService;
    }

    [HttpPost("process")]
    public async Task<IActionResult> ProcessVideo([FromBody] VideoRequest request)
    {
        var videoData = await _videoService.DownloadAndTranscribe(request.VideoUrl);
        return Ok(videoData);
    }
}

Conclusion

This simple code can be integrated into your existing C# backend, allowing you to leverage AI capabilities without straying far from your familiar coding environment. A lot of refinement and accuracy still needs work, but with this your journey with AI begins. Happy coding — here's to embracing new technologies without losing sight of what you do best.

← Back to blog